TellWorldAI 世界档案
问档案English

首页›实践›实践

实践

MCP 服务器开发最佳实践

开发 MCP 服务器时,怎样拆分工具、校验参数、处理错误、控制权限和限流,以及有副作用的工具为什么不能盲目重试。

这篇适合谁,读完能做到什么

适合已经写过一个 MCP 工具、准备把服务器交给别人用或放到生产环境的开发者。读完你应该能按清单检查自己的服务器:工具是否职责单一,参数是否校验,错误信息是否可操作,权限和限流是否到位,以及有副作用的操作是否做好了重试设计。

先定几条原则

这一课归纳了几条核心原则:用 JSON-RPC 2.0 做标准化通信;以用户为中心,优先考虑用户的同意、控制和知情;安全第一;采用模块化设计,每个工具和资源都有清晰、单一的用途;状态要显式管理[1]。

其中最后一条需要特别留意。MCP 2026-07-28 版在协议层是无状态的。如果一个流程需要跨多次调用保持状态,就要用显式的句柄,或者用普通的工具参数,背后由可靠的应用层状态支撑[1]。不要指望协议替你记住上一次调用的结果。

安全:先做这几件事

官方的安全最佳实践列表以用户同意为起点:访问数据或执行操作之前,要获得用户的明确同意;调用任何工具之前,也要获得明确同意[1][2]。

接下来是几项具体要求[1]:

  • 认证:访问工具、资源或敏感操作之前,要先完成认证,可以使用 API 密钥、OAuth 令牌或其他安全的方式。
  • 参数校验:对每一次工具调用都做校验,防止畸形或恶意的输入进入工具实现。
  • 限流:限制调用频率,防止滥用,并保证服务器资源被公平使用。
  • 工具权限控制:为每个请求和授权上下文配置模型可以使用哪些工具,只允许明确授权的工具被访问。

这一课还列出了 OWASP MCP Top 10,作为安全风险和缓解措施的参考[1]。

工具设计:一个工具只做一件事

工具应该职责单一,每个工具专注做好一件具体的事。把所有功能塞进一个“万能”工具,会让参数变得复杂,模型也更难判断该调用哪一个[1]。这和模块化的原则是一致的。

参数校验:按顺序检查

参数校验要彻底。下面的 JavaScript 示例在检查参数值的阶段,先确认操作类型属于允许的集合,然后再继续检查其他条件[1]:

    // 3. Validate parameter values
    const validOperations = ["read", "write", "delete"];
    if (!validOperations.includes(parameters.operation)) {
      throw new ToolError(`Invalid operation. Must be one of: ${validOperations.join(", ")}`);
    }

完整的顺序是:先检查参数是否存在,再检查类型,然后检查取值是否合法,接着检查 write 操作是否带了内容,最后检查路径是否在允许的目录之内[1]。文件操作示例里,operation 参数是 read、write、delete 三者之一的枚举,path 必须位于允许的目录中[1]。

错误处理:分清类型,给出可操作的信息

标准化的错误信息和错误码,能帮助人快速定位问题,也能让失败被优雅地处理,并给出可操作的反馈[1]。下面的 Python 示例把超时、数据库连接错误和查询错误分开处理,每一类抛出不同的工具错误,数据库调用还设置了 10 秒超时[1]:

            except asyncio.TimeoutError:
                raise ToolExecutionError("Database query timed out after 10 seconds")
            except DatabaseConnectionError as e:
                # Connection errors might be transient
                self._log_error("Database connection error", e)
                raise ToolExecutionError(f"Database connection error: {str(e)}")
            except DatabaseQueryError as e:
                # Query errors are likely client errors
                self._log_error("Database query error", e)
                raise ToolExecutionError(f"Invalid query: {str(e)}")

示例里的注释说明了分类的理由:连接错误可能是暂时的,查询错误多半来自调用方的输入。分类之后,调用方才知道该重试还是该修改请求。

限流和认证的写法

这一课给了一个 C# 限流中间件的例子。它从配置项 RateLimit:MaxRequestsPerMinute 读取每分钟的上限,默认值为 60[1]。它按客户端标识(API 密钥或用户 ID)和当前分钟统计请求数,超过上限时返回 HTTP 429,并带上值为 60 的 Retry-After 头;正常响应则附带 X-RateLimit-Limit 和 X-RateLimit-Remaining 头[1]。

认证的例子用 Java 写成。它先校验认证令牌,再针对具体的数据 ID 和操作检查授权,最后才执行读取或更新[1]。注意顺序:认证在前,授权紧随其后,二者缺一不可。

可观测性:日志写到 stderr

在 stdio 方式下,标准输出承载协议消息,诊断信息应当写到 stderr。结构化的可观测性可以使用 OpenTelemetry。规范 2026-07-28 版已把 MCP 自带的日志功能标记为弃用[1]。

有副作用的工具不要盲目重试

通用的重试循环对创建工单、支付、发送消息、部署之类会产生真实影响的工具并不安全。原因是响应可能在操作已经提交之后丢失,客户端以为失败了,于是重试,结果就是重复执行[1]。

这类工具需要更仔细的设计,比如为每次操作设置稳定的操作键,防止重复受理;用检查点记录进度;定期与外部系统对账。这一课还推荐了一篇配套课程,专门讲这些内容[1]。

长任务:报告进度,允许取消

对耗时较长的操作,服务器应当报告进度,让界面保持响应;客户端也应当能取消已经不再需要、或者耗时过长的请求[1]。

限制和常见问题

  • 这些示例大多是片段,省略了实现细节,例如 _log_error 没有给出实现。直接复制到项目里会跑不起来,应该理解其中的思路再写。
  • 最佳实践会随规范变化。这一课以 2026-07-28 版为基准,旧版本的握手和日志方式不同,迁移时要以当前规范为准[1][2]。
  • 限流和重试的参数(如 60 次每分钟、10 秒超时)都是示例取值,需要根据服务的实际负载和后端能力调整。

下一步

读完这篇,可以接着看同一系列的测试和工作流设计章节,把这里的原则落实到测试用例上。若服务器会执行有副作用的操作,优先去读关于安全重试的配套课程[1]。

本文改编自 Microsoft 开源课程《MCP for Beginners》的开发最佳实践一课[1],原文以 MIT 许可证发布。

来源

  1. 1
    MCP Development Best PracticesMicrosoft mcp-for-beginners (MIT)
  2. 2
    Security Best PracticesModel Context Protocol

引用本页: MCP 服务器开发最佳实践. TellWorld, https://www.tellworld.net/zh/practice/practice-mcp-best-practices. 更新于 2026-11-14

相关动态