mcpField
M
mcp-best-practices

mcp-best-practices

github.com/tenequm开发4080 次安装5 天前更新
代码仓库

关于 mcp-best-practices

MCP Best Practices 是面向使用 TypeScript SDK 构建生产级 MCP 服务的决策参考,适合已有可运行服务、需要检查实现质量的开发者。内容涵盖协议版本、传输、安全、工具设计及部署注意事项。

主要功能

  1. 01比较协议版本与 SDK 行为
  2. 02指导传输方式和无状态服务设计
  3. 03说明工具注册、命名与参数模式
  4. 04涵盖分页、注释和结果传递
  5. 05列出安全与兼容性注意事项

适用场景

  1. 01升级 TypeScript MCP SDK 前核对兼容性
  2. 02选择远程或本地传输方式
  3. 03检查工具接口与安全设计

MCP 相关

该技能提供构建和维护 MCP 服务的实践参考,说明 MCP 服务如何向客户端提供工具等能力。它本身不是 MCP 服务。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tenequm/skills --skill mcp-best-practices

安装后在 Claude Code 中输入/mcp-best-practices确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

CHANGELOG.md
LICENSE.txt
references/
error-handling.md
extensions-registry.md
mcp-apps.md
sdk-bugs.md
security-auth.md
spec-2026-07-28.md
tool-schema-guide.md
transport-patterns.md
v2-migration.md

详细介绍

MCP 最佳实践

使用 TypeScript SDK 构建生产级 MCP 服务的决策参考。本文不是教程,假设你已经有可运行的服务,需要让它正确、快速且安全。

快速参考

组件当前版本说明
规范(已发布)2026-07-28 (规范)无状态/无会话改造——参见下文“规范 2026-07-28”和 references/spec-2026-07-28.md
规范(仍在部署)2025-11-25仍是已部署软件的主体,也是 TS 客户端的默认版本——但 Claude Code 现在会与支持该版本的 HTTP 服务协商使用 2026-07-28
TS SDK(当前)v2.3.1(2026-10-05):/server、/client、/core 为 2.3.1;/node 为 2.1.1,/express + /hono 为 2.0.2,/fastify 为 2.0.1(不再同步发布)createMcpHandler 默认同时服务两个时代;客户端默认使用 2025 时代的协议,除非另行指定
TS SDK(旧版)v1.32.1(@modelcontextprotocol/sdk)v2 正式发布后至少 6 个月内提供错误修复和安全修复;版本不会更新到 2025-11-25 之后;源代码位于 v1.x 分支
JSON Schema默认 2020-12(自 v2.0.0 起接受 2019-09 / draft-07)-
传输方式Streamable HTTP(远程)、stdio(本地)v2 中已移除 SSE + WebSocket
扩展MCP Apps(稳定版,SEP-1865)、Auth Extensions(官方)、Tasks(ext-tasks)特定领域工作组
注册表预览版,自 2025-10-24 起冻结 v0.1 API(注册表)正式发布待定

v2 导入(当前版本):

import { McpServer } from "@modelcontextprotocol/server";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server";
import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/core";

v1 导入(旧版系列,仍广泛部署):

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
两个时代

2026-07-28 发布后,与决策最相关的一点是:升级 TS SDK 不会让你的客户端切换到新规范,但其他 SDK 和客户端并没有采用相同的默认设置。rmcp(Rust)>= 3.0 开箱即用地声明支持 2026-07-28,因此依赖项升级可能会让服务悄然切换到现代协议——每次升级 SDK 后,都要重新检查你声明支持的修订版本。

从 2024-10-07 到 2025-11-25 的所有修订版都以 initialize 开始,并共享同一种协议行为——SDK 将这一系列称为旧版。2026-07-28 开启了现代时代:不再使用 initialize,改为通过 server/discover 声明支持情况,并在每个请求中加入 _meta 封装。版本选择需明确指定:

versionNegotiation.mode行为
absent / 'legacy'使用 2025 年的 initialize 握手,逐字节保持一致。不进行探测。这是默认设置。
'auto'使用 server/discover 进行探测;若服务仅支持 2025 年版本,则回退到 initialize
{ pin: '2026-07-28' }只使用该修订版,否则不连接——固定版本绝不会回退

服务:同时支持两个时代。 Claude Code 当前的 MCP 运行时会询问 HTTP 服务是否支持 2026-07-28,并在支持时使用该版本(stdio 正在逐步推出),因此,声明支持现代版本的服务如今会从主流客户端接收现代协议流量。v2 的 createMcpHandler 默认会同时服务 2025 时代和现代时代的请求(legacy: 'stateless');请测试两个时代。本文档中的无状态设计指导正是让这件事成本低廉的关键。

工具:SDK 文档(v2);MCP Inspector,它默认以 legacy 模式连接——请传入 --protocol-era modern(参见 references/spec-2026-07-28.md 中的“针对各时代进行测试”);一致性测试套件;以及用于搭建脚手架的 mcp-server-dev 插件。

服务设置

传输方式决策
场景传输方式关键配置
远程,v2(K8s、CF Workers、Node)createMcpHandler(factory)兼容两个时代;每个请求使用无状态模式
远程,v1 / 无状态WebStandardStreamableHTTPServerTransportsessionIdGenerator: undefined,enableJsonResponse: true
远程,有状态(仅 2025 时代)WebStandardStreamableHTTPServerTransportsessionIdGenerator: () => randomUUID()
本地 CLI / Claude DesktopStdioServerTransport / v2 serveStdio(factory)默认
旧版 SSE 客户端v2 中已移除 SSE - 迁移到 Streamable HTTP-
无状态模式(推荐用于远程部署)

每个请求都创建一个全新的服务端是标准模式;自 [email protected] 起,这一规则已得到强制执行:“升级后,使用同一个服务端对象,或同一个无状态传输处理每个 HTTP 请求的应用,会在第二个请求时失败。” 在 v1.26.0 之前,共享实例还会泄露不同客户端之间的数据(GHSA-345p-7cg4-v4c7)。在 v2 中,createMcpHandler 接受一个工厂,并为每个请求调用一次:

import { createMcpHandler, hostHeaderValidationResponse, McpServer, originValidationResponse } from "@modelcontextprotocol/server";

const handler = createMcpHandler(() => {
  const server = new McpServer({ name: "my-server", version: "1.0.0" });
  registerTools(server);   // register tools, resources, prompts inside the factory
  return server;
});

export default {
  async fetch(request: Request): Promise<Response> {
    // createMcpHandler is deliberately validation-free: guard Host/Origin in front of it.
    const rejected =
      hostHeaderValidationResponse(request, ["mcp.example.com"]) ??
      originValidationResponse(request, ["app.example.com"]);
    return rejected ?? handler.fetch(request);
  },
};

在 v1 中,采用相同模式:每个请求都创建一个新的 McpServer 和 WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }),然后调用 connect()、handleRequest(),最后在 finally 中同时调用 close();详见 references/transport-patterns.md。

McpServer 必须按请求创建,但它的常量输入不应如此。提至模块级:Zod 模式、注释对象({ readOnlyHint: true, ... })、工具描述字符串、支付配置、上游 API 客户端。(v2.3.0 还会延迟转换工具模式,因此按请求创建的服务端不再需要在每个请求中重新转换所有工具。)

如果只路由 POST(常见的无状态布局),请为 GET /mcp 明确返回 405 Method Not Allowed:当不提供 SSE 流时,规范要求这样处理;官方 TS 客户端会将 405 视为正常的“无流”信号,而空的 200 响应会让它陷入重连风暴。

关于传输方式、会话、HTTP/2 注意事项和 K8s 部署,请参阅 references/transport-patterns.md

框架集成

handler.fetch 符合 Web 标准,因此 Workers、Deno、Bun 和 Hono 无需适配器。对于 Express,createMcpExpressApp(options) 会返回一个带有 JSON 解析和 DNS 重绑定保护的 express() 应用;使用 @modelcontextprotocol/node 中的 toNodeHandler(handler) 挂载处理程序。createMcpHonoApp(options) 是 Hono 的对应版本。在 Cloudflare Workers 上,请在模块级调用 preloadSchemas();v2 的 workerd 构建会自动执行此操作。示例:references/transport-patterns.md。

工具设计

注册 API

v1(旧版系列) - server.tool(name, description, zodShape, annotations, handler)。位置参数重载存在歧义;字段与下方的 v2 相同,但没有 outputSchema。在 v2 中已完全移除。

v2(当前版本) - 使用配置对象调用 registerTool():

server.registerTool("search_docs", {
  title: "Document Search",
  description: "Search documents by keyword or phrase",
  inputSchema: z.object({
    query: z.string().describe("Search query"),
    max_results: z.number().optional().describe("Max results (default 20)"),
  }),
  outputSchema: z.object({
    results: z.array(z.object({ id: z.string(), text: z.string() })),
    has_more: z.boolean(),
  }),
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
}, async ({ query, max_results }) => {
  const result = await fetchDocs(query, max_results);
  return {
    // Both channels carry IDENTICAL bytes. Divergent payloads = the text block
    // silently vanishes on Claude Code/Codex/Copilot. See "Tool Result Delivery" below.
    structuredContent: result,
    content: [{ type: "text", text: JSON.stringify(result) }],
  };
});
命名

规范 2025-11-25(建议 SHOULD,而非必须 MUST):1-128 个字符,区分大小写,仅限 A-Za-z0-9_-.。推荐:search_docs、get_user_profile、admin.tools.list。不推荐:search(通用名称会与其他服务端冲突)、Search Docs(不允许包含空格)。同时启用多个服务端时,使用服务前缀(github_*、jira_*)——大模型容易混淆通用名称。将前缀直接写入工具名称:规范明确指出,“服务端的 name(来自 serverInfo)不保证在不同服务端之间唯一,不应依赖它来消除歧义”,因此聚合器无法为你推导出安全的前缀。

Schema 规则

每个字段都要使用 .describe()——大模型会用它生成参数。三种结构会在不报错的情况下失效(z.union()、原始 JSON Schema、z.transform()),客户端的 AJV 严格验证也会如此——参见下文的“已知 SDK Bug”。

分页是大多数服务首先会遇到的基础问题:包含 50 项或更多条目的 tools/list 或 resources/list 应该分页。协议中的 cursor 是不透明的——切勿解析或合成它;循环直到没有 nextCursor 为止。它与工具内部的 offset/limit 参数不同。

Zod 到 JSON Schema 的转换规则、outputSchema/structuredContent 模式、非文本内容类型、其他工具定义字段(icons、listChanged、execution.taskSupport),以及其余基础功能(提示词、资源、资源模板、补全、取消):参见 references/tool-schema-guide.md

注解

这些都只是可选提示(根据规范,应将不可信服务提供的提示视为不可信):

注解默认值含义
readOnlyHintfalse工具不会修改其环境
destructiveHinttrue可能执行破坏性更新(仅当 readOnly=false 时)
idempotentHintfalse使用相同参数重复调用不会产生额外影响
openWorldHinttrue与外部实体(API、Web)交互

准确设置这些提示——客户端会据此决定是否提示用户同意,以及是否自动批准。

“致命三要素”:在同一个 Agent 中,同时具备私有数据访问权限、接触不可信内容的机会和对外通信能力,会造成数据窃取风险(已通过恶意日历事件、MCP 日历服务和代码执行工具进行演示)。设计工具集时,应确保没有任何单个 Agent 同时具备这三项能力。

有状态工具

截至 2026-07-28,协议层没有会话机制,跨调用状态通过服务端生成并作为普通工具参数传递的句柄实现:创建工具返回 { basket_id: "bsk_a1b2c3" },后续工具将 basket_id 作为参数,模型会将其传递下去。句柄只是一个名称,并非一种能力——每次调用都要验证调用者是否有权使用该句柄,确保句柄不透明且包含足够的随机熵,并在创建工具的描述中说明其保留策略。对于已过期或未知的句柄,返回工具执行错误,以便模型重新创建状态。完整规则见:references/spec-2026-07-28.md。

工具结果交付:content 与 structuredContent

容易踩坑的地方:当工具同时返回文本 content 块和 structuredContent 时,一些主流客户端(Claude Code、Codex CLI、VS Code Copilot、Goose)会静默丢弃文本块,只将 structuredContent 转发给模型。如果两种载荷内容不同,人类可读的内容就会消失。这是规范未作约束的客户端行为,并非 SDK 转换导致的。不要同时返回两个通道,并期望它们都能传给模型。

经实测验证 - Claude Code 2.1.165(MCP 2025-11-25)

通过 claude -p --output-format=stream-json 测量,读取模型实际收到的 tool_result:

工具返回内容模型收到的内容
一个文本块,没有 structuredContent原样文本
content: [] + structuredContentJSON.stringify(structuredContent) 作为字符串放入内容槽位 - 可正常工作
文本块 + structuredContent文本块被静默丢弃;以 structuredContent 为准
文本 + structuredContent + outputSchema相同 - outputSchema 毫无影响
两个文本块,没有 structuredContent两个文本块均原样保留

在 Claude Code 中,structuredContent 不是传给模型的独立类型化通道 - 它会被序列化为字符串,放入标准 tool_result 内容槽位,因此与等价的 JSON 文本消耗相同的 token。它不会带来更低成本或带外的结构化数据。

这是 Anthropic 维护者有意为之(anthropics/claude-code#9962):Claude Code v2.0.21 加入了对 structuredContent 的支持,并且“我们在两种格式都存在时将 structuredContent 设为默认格式……以优化 Agent 性能。”在互不相关的服务(Laravel、Roblox Studio、YouTube)中均复现了这一情况 - 这是宿主端的优先级处理,并非服务端 Bug。

规范的实际规定(2025-11-25)

规范没有规定优先级 - 规范从未说明当两个字段同时存在时,客户端应优先选择哪个(讨论 #1563),这个缺口是有记录的客户端行为差异的根本原因。唯一相关的规范性条款是一项向后兼容的 SHOULD:“返回结构化内容的工具 SHOULD 同时在 TextContent 块中返回序列化后的 JSON。”官方 TypeScript SDK 会原样传递两个字段;你观察到的任何将其序列化为字符串并放入内容的行为,都来自宿主端运行环境,而非 SDK。

跨客户端行为(上方矩阵仅适用于 Claude Code)
客户端content + structuredContent 同时存在时
Claude Code CLI、OpenAI Codex CLI、VS Code Copilot、Goose遮蔽 - 只有 structuredContent 会传给模型(文本被丢弃)
Cursor、Claude.ai 网页版、ChatGPT MCP connector优先使用 content / 将两者都提供给模型
Google ADK(框架)默认转发两者;仅转发 content 需显式选择

(非 Claude Code 的条目来自问题跟踪器和维护者声明,而非 stream-json 测试环境 - 实际传递行为取决于客户端版本。)

服务作者须知
  • 不要返回内容不同的 content 和 structuredContent(例如,文本中是渲染后的 ASCII 表格,结构化内容中却是另一份 JSON)。在采用影子模式的客户端上,文本会被静默丢弃,只有 JSON 会传给模型。
  • 如果输出 structuredContent,务必将相同字节复制到一个文本块中:content: [{ type: "text", text: JSON.stringify(payload) }]。这是规范建议的向后兼容做法。采用影子模式的客户端会使用结构化副本;其他客户端则会回退到完全相同的文本——无论哪种情况,模型都能获取数据。在采用影子模式的客户端上,复制不会增加 Token 数量(它们会丢弃文本)。
  • 优先为每个工具/每种模式选择一个通道。 若要让面向人类的呈现内容(表格、摘要)传给模型,只返回纯文本,不要包含 structuredContent;或者提供 format: "table" | "json" 参数(table -> 仅文本;json -> 将 JSON 同时复制到两个通道)。这两种方式在 Claude Code 上均经实测有效,并且每次调用只使用一个通道。
  • outputSchema 仅用于客户端验证;它不会让文本块在采用影子模式的客户端上保留下来。

content 块不只有文本类型——image、audio、resource_link 和嵌入式 resource 块都存在,并带有注释(audience、priority、lastModified);关于这些内容,以及图像预览和 URL 模式,请参阅 references/tool-schema-guide.md。

错误处理

两种截然不同的机制,对大模型的可见性也不同:

类型大模型能看到吗?用途
工具错误(CallToolResult 中的 isError: true)能——可用于自我纠正输入验证、API 失败、业务逻辑错误
协议错误(JSON-RPC 错误响应)不一定——客户端可能会显示未知工具、格式错误的请求、服务崩溃

根据 SEP-1303(已并入 2025-11-25 版规范):输入验证错误必须是工具执行错误,而非协议错误。大模型需要看到“日期必须是未来日期”,才能自我纠正。

// DO: Tool execution error - LLM can self-correct
return {
  isError: true,
  content: [{ type: "text", text: "Date must be in the future. Current date: 2026-03-25" }],
};

// DON'T: throw for validation - you lose control of what the LLM sees
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "Invalid date");

抛出异常实际会做什么:在工具处理程序中,SDK 会将每个异常(包括抛出的 ProtocolError/McpError)都转换为 isError: true 结果(UrlElicitationRequiredError 是唯一例外)。错误消息会保留;错误代码和 error.data 会被丢弃,因此其中嵌入的结构化数据永远不会到达客户端。请自行返回 isError 结果。x402/MPP 生态系统正是因此将带有 structuredContent 的 isError: true 结果标准化。

关于完整的错误分类、代码示例、支付错误模式,以及为何 -32042 不能用作“Payment Required”代码,请参阅 references/error-handling.md

资源和说明

在服务构造函数中设置 instructions——这是向大模型说明如何使用服务的系统级提示:

const server = new McpServer({
  name: "docs-api",
  version: "1.0.0",
  instructions: "Knowledge base API. Use search_docs for full-text search, get_doc for retrieval by ID. All tools are read-only.",
});

启用 Claude Code 的工具搜索后,会话开始时只会加载工具名称和服务 instructions——如今,模型决定加载哪些工具之前读取的主要内容就是 instructions。Claude Code 会将每个工具说明和每个服务的说明截断至 2,048 个字符;请将关键信息放在最前面。

使用 docs:// URI 方案(server.resource(...))将指南和结构化数据作为资源提供——请参阅 references/tool-schema-guide.md 中的“其他服务原语”。

性能

缓解令牌膨胀

工具定义会在对话开始前占用上下文窗口。GitHub MCP:80 个工具占用 20,444 个令牌(SEP-1576)。

策略:

  1. 每个服务提供 5-15 个工具——这是社区认可的最佳范围。超过这个数量就拆分。
  2. 面向结果的工具——将多步操作整合为单个工具(例如,使用 track_order(email),而不是 get_user + list_orders + get_status)。
  3. 响应粒度——返回经过筛选的结果,而不是原始 API 数据。例如,返回 800 个令牌的用户对象,而不是 20 个令牌的摘要。
  4. outputSchema + structuredContent——为程序化客户端/PTC 客户端提供类型化输出。注意:对于使用影子模式的客户端,structuredContent 会被字符串化并放入模型上下文,其令牌成本与文本相同——它不是免费的带外通道(参见“工具结果传递”)。
  5. 动态加载工具——根据请求上下文仅注册相关工具子集(例如使用 ?tools=search,fetch 查询参数)。如果工具集在会话期间发生变化,则配合使用 listChanged。每个连接使用不同的集合,不要在对话中途更改集合:工具定义位于提示词前缀中,“在对话中途添加或移除工具定义会使该缓存失效,而由此产生的缓存未命中可能比被移除的定义消耗更多令牌。” 即使尚未到达你公布的 ttlMs,客户端也必须在收到 list_changed 时立即将缓存列表视为过期。
  6. 渐进式工具发现 / 代码模式——大型目录的客户端越来越多地使用 search_tools 元工具和程序化工具调用,此时 structuredContent 会在模型上下文之外被使用(客户端最佳实践)。经过筛选且描述清晰的工具有助于这些流程顺利运行。
结果大小预算(每个客户端的上限)

客户端会静默截断较大的工具结果。请按目标客户端中限制最严格的一方设置预算:

客户端默认上限可配置
Claude Code25,000 个令牌(10k 时发出警告);超过 50,000 个字符的文本结果会保存到文件,并替换为该文件的路径,不受令牌数影响MAX_MCP_OUTPUT_TOKENS 环境变量;每个工具的 _meta["anthropic/maxResultSizeChars"] 可将 50k 持久化阈值提高到最多 500,000 个字符;对于文本结果,该设置会取代令牌上限,而不是受其限制
OpenAI Codex CLI当前所有模型均为 10,000 个令牌(约 40KB);bytes 模式下的 10,000 仅在旧版 gpt-5.2 和未知模型回退方案中生效tool_output_token_limit 配置项
Gemini CLI40,000 个字符(截取开头 20% / 结尾 80%;完整输出保存到文件)设置;设为 0 或负数可禁用

请在服务端自行实施上限——参见 references/tool-schema-guide.md 中的“结果大小预算与截断”。这里有两条值得说明的规则:绝不截断 isError 结果(支付/身份验证质询必须完整保留)——同时也要控制其大小,因为 Claude Code 本身会将超过约 11,000 个字符的错误文本截取为开头和结尾各 5,000 个字符——并将客户端预算视为每个连接的属性:将其作为 URL 查询参数接收(与 ?tools= 一同使用 ?max_chars=),而不是为每个工具架构都增加覆盖参数。

长时间运行的工具

Claude Code 会运行三个独立的计时器,只有其中一个会响应进度:

计时器默认值进度会重置计时器吗?
每个服务的 timeout“每次工具调用的硬性时钟时间上限”(未设置时约为 28h)不会
空闲超时HTTP 为 5 min,stdio 为 30 min(CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT)会——在此时间窗口内既无响应又无进度的调用会中止
首字节计时器(仅限 HTTP)max(60 s, tool timeout, MCP_TIMEOUT)n/a——涵盖每个请求,直到收到首个响应字节

因此,对于耗时较长的调用,应发送 notifications/progress(这会让空闲计时器保持运行),但不要指望它能延长时钟时间上限。启用 enableJsonResponse: true 时,首字节计时器会造成影响:JSON 模式的响应会在工具完成前不发送任何字节,而 SSE 响应则会立即开始流式传输。主对话中的调用如果运行超过两分钟,就会被移至后台任务,因此耗时较长的工具不再会阻塞会话——但仍然必须完成。

若要突破上限:快速返回由服务生成的句柄,让调用方轮询(参见“有状态工具”);或者采用 io.modelcontextprotocol/tasks 扩展。该扩展专为此场景设计,会返回一个由客户端通过 tasks/get 轮询的 CreateTaskResult。Tasks 按请求选择启用——如果客户端未声明 tasks 能力,而服务无法同步处理调用,则必须返回 -32021(缺少必需的客户端能力),并指出该扩展,而不是默默阻塞。

无参数工具

对于没有输入的工具,请使用显式的空模式——不要使用 undefined 或省略:

inputSchema: { type: "object" as const, additionalProperties: false }

安全性

主要威胁(2025—2026 年的真实事件)
攻击示例缓解措施
工具投毒描述中隐藏恶意指令(WhatsApp MCP,2025 年 4 月)审核工具描述;客户端应将其显示出来
供应链攻击恶意 npm 包(Smithery 泄露事件,2025 年 10 月)固定版本,审计依赖项
Stdio 配置注入用户控制的输入未经清理就传入 StdioServerParameters(OX Security,2026-04-15)在客户端代码中清理 stdio 配置;优先使用第一方服务。此问题被视为“设计如此”——SDK 未对此进行修复
跨服务工具名劫持恶意服务覆盖合法的工具名称为工具名称添加服务前缀;验证工具来源
令牌窃取权限过高、作用域宽泛的 PAT使用最小作用域;采用 OAuth 2.1 Resource Indicators (RFC 8707)
令牌透传服务接受或转发并非为其签发的令牌验证受众声明;绝不将客户端令牌转发到上游 API
迷惑代理人通过 DCR 利用代理服务的同意 Cookie转发到第三方身份验证服务前,须取得每个客户端的同意
会话劫持窃取或猜测会话 ID 以冒充他人使用密码学安全的随机 ID,将其绑定到用户身份,绝不将其用于身份验证
跨客户端响应泄露多个客户端重复使用同一个 McpServer/传输实例(CVE-2026-25536,影响 v1.10.0-1.25.3)要求使用 SDK >= v1.26.0;每个请求使用独立的服务和传输实例
受众错误的令牌SDK 的承载令牌身份验证接受了为其他服务签发的令牌(GHSA-rvq5-wwqv-78pq,sdk <= 1.31.0,server <= 2.2.0)升级并设置 expectedResource——默认关闭
跨会话任务访问v1 实验性任务未绑定到其所属会话(GHSA-22jm-h49p-29qw,适用于使用 taskStore 的 sdk 1.24.0-1.31.0)sdk >= 1.32.0;无状态服务上的任务仍会共享——对每个任务请求进行授权
UriTemplate ReDoS恶意 URI 模式(CVE-2026-0621)升级到 v1.25.2+ / v2.0.0-alpha.1+

最低版本:@modelcontextprotocol/sdk >= 1.32.0,@modelcontextprotocol/server >= 2.3.0(如果使用 /express 的身份验证中间件,还需 >= 2.0.2)。同一周还有两项安全公告涉及客户端(向服务器指定的授权服务器发送凭据、跨源重定向)——参见 references/security-auth.md。

通用安全措施仍然适用:在工具边界验证输入,执行按用户划分的访问控制,限制请求速率,清理工具输出,绝不将工具输入插入 Shell 命令中,在出站抓取时阻止私有 IP,将本地服务绑定到 127.0.0.1。使用 new McpServer(info, { maxToolInputElements: 10_000 }) 限制参数大小(sdk 1.32.0 / server 2.3.0,默认关闭):超大调用会在运行架构验证前返回 isError 结果。

服务端要求(规范性要求)
  • 验证 Origin 请求头——但仅在其存在且无效时拒绝:"If the Origin header is present and invalid, servers MUST respond" 并返回 403。有些已发布的客户端完全不会发送 Origin;一律对缺少该请求头的请求返回 403 会将这些客户端拒之门外。
  • **启用检查。**除非你主动添加,否则不会验证 Host/Origin:
    • createMcpHandler 按设计不执行验证。在 Workers/Deno/Bun 上直接使用 export default handler,不会受到保护(#2844)。请在其前面添加 hostHeaderValidationResponse / originValidationResponse。
    • 原始传输的 enableDnsRebindingProtection 默认值为 false,且该选项及 allowedOrigins/allowedHosts 均已标记为 @deprecated,建议改用外部中间件。
    • createMcpExpressApp/createMcpHonoApp 默认仅在绑定到回环地址时验证 Host 和 Origin。绑定到其他任何特定主机(192.168.1.10、mcp.internal)时,两项检查都会被静默跳过(#2843)。请显式传入 allowedHosts。
    • 自 2.3.0 起,allowedOrigins 支持 <scheme>://*(例如 moz-extension://*),以适配浏览器扩展客户端。
  • 现代线上协议中,MCP-Protocol-Version 不可省略。会话无关改造后,这个请求头依然保留:"Every POST request to the MCP endpoint MUST include an MCP-Protocol-Version header",其值必须与请求体 _meta 中的 io.modelcontextprotocol/protocolVersion 匹配,否则服务端必须返回 400 Bad Request 和 HeaderMismatch 错误。版本会同时携带在 _meta 和请求头中,这是有意的冗余设计:中间层根据请求头路由,服务端则根据请求体执行,因此二者必须一致。
  • 对声明的版本范围保持宽容,但不能对是否声明版本保持宽容。在 2025 年前后的线上环境中,应接受一系列已声明的版本,而不是强制要求单一版本——目前仍有客户端声明 2024-11-05;支持早于 2025-06-18 的客户端的服务端,可以将缺少请求头的请求视为 2025-03-26。不支持这些客户端的服务端必须拒绝缺少请求头的请求。
身份验证(OAuth 2.1)

MCP 规范要求使用 OAuth 2.1 (draft-ietf-oauth-v2-1-13),而非 2.0——必须使用 PKCE,隐式授权流已移除。服务端是资源服务器;客户端必须发送资源指示符(RFC 8707),将令牌绑定到你的服务端。

  • 验证受众——拒绝并非为你的服务端签发的令牌(禁止透传)。使用 SDK 的 requireBearerAuth 时,这意味着要设置 expectedResource: new URL("https://mcp.example.com/mcp"),并且使用一个能从令牌的 aud 填充 AuthInfo.resource 的验证器——只设置前者会导致每个请求都返回 401;两者都不设置则永远不会检查受众。使用 PKCE S256、短时效令牌和最小化作用域(按工具提升作用域时使用 scopeChallenge: requireScopes(...),服务端版本 >= 2.1.0)。
  • **可选身份验证会导致客户端行为不一致。**如果同一端点同时服务匿名用户和已登录用户,有些客户端只有在收到 401 后才会启动 OAuth;因此,若服务端对不带凭据的 tools/list 请求返回 200,这些客户端就会以匿名状态保持连接——“已连接,N 个工具”并不能证明用户已通过身份验证。反过来,一概返回 401 的门禁也会破坏那些需要 200 + isError 挑战的支付客户端。请按端点审慎选择。
  • 使用经过测试的验证库(Keycloak、Auth0 等),不要自行实现;切勿记录 Authorization 请求头、令牌或密钥。
  • RFC 9207 iss 互操作陷阱:将 authorization_response_iss_parameter_supported: true 作为声明,会要求严格的客户端必须验证回调中的 iss,但有些客户端会丢弃该字段。将此标志声明为 false,同时仍发送 iss——参见 references/security-auth.md。

有关完整的安全攻击与缓解模式,以及身份验证实现细节,请参见 references/security-auth.md

已知 SDK 缺陷

截至 [email protected] / [email protected] 必须了解的事项:

  • 所有已发布的 v1 版本中,z.union()/z.discriminatedUnion() 都会静默生成空 schema,包括 v1.32.1(#1643,回移植 PR #2017 仍未合并)——请改用扁平的 z.object() + z.enum()。
  • 安全基线为 sdk >= 1.32.0 / server >= 2.3.0——请参阅上方的威胁表。
  • **在 v1 上,必须在 connect() 前注册。**在所有已发布的 v1 版本中,之后注册都会抛出异常(#893;v1 修复已合并,但在 1.32.1 之后尚未发布)。在 v2 上,只要在构造函数的 capabilities 中声明了相应能力,就可以正常使用。
  • 客户端 AJV 严格模式会拒绝未剥离的 structuredContent 额外字段——请先对上游数据调用 .parse(),或对有意保留的额外字段使用 .passthrough()。
  • v1 会为每个工具 schema 添加 "$schema": "http://json-schema.org/draft-07/schema#"(1.32.1 仍然如此),严格的 2020-12 客户端会因此拒绝整个工具,并报错:"JSON Schema declares an unsupported dialect ... The default validator supports JSON Schema 2020-12 only." 对于会丢弃整个 tools/list 的客户端,一个有问题的 schema 可能连带导致服务端的其他工具也无法使用。v2 输出 2020-12。相关问题尚未解决(#2721,权威问题 #2084);@modelcontextprotocol/inspector >= 2.4.0 会帮你标记此问题。
  • 自 [email protected](一个次要版本)起,复用服务端或无状态传输会导致硬错误。createMcpHandler(() => sharedServer) 会以 500 / -32603 响应重叠请求;复用无状态传输则会在第二个请求时抛出异常。经由 Node 包装器时,只会得到一个空白的 500,日志中没有任何信息(#2704)。请传入一个用于创建服务端的工厂函数。

完整表格(状态、zod 3->4 丢弃 additionalProperties、refine/superRefine 从不运行、传输关闭时堆栈溢出、HTTP/2、原始 JSON Schema、z.transform()、ReDoS、2.1-2.3 修复):请参阅 references/sdk-bugs.md

V2 迁移

如需包含所有破坏性变更及迁移前后代码的完整迁移指南,请参阅 references/v2-migration.md

关键破坏性变更:

  1. 拆分软件包:@modelcontextprotocol/sdk -> @modelcontextprotocol/server + /client + /core
  2. 以 ESM 为先(在 beta.2 中恢复了 CJS 构建),Node.js 20+(支持 Bun/Deno)
  3. 必须使用 Zod v4(或任何 Standard Schema 库)
  4. McpError -> ProtocolError(来自 @modelcontextprotocol/core)
  5. extra 参数 -> 结构化的 ctx,其中包含 ctx.mcpReq
  6. server.tool() -> registerTool()(使用配置对象,而非位置参数)
  7. 移除 SSE 服务端传输(客户端仍可连接旧版 SSE 服务端)
  8. createMcpHandler(factory) 取代按请求创建传输并连接 connect() 的接线方式;@modelcontextprotocol/hono/express/node 对其进行适配
  9. 默认启用 localhost 服务端的 DNS 重绑定防护(仅限框架工厂)

v1.x 至少会持续获得修复,直到 2027-01-27,但永远不会实现 2026-07-28——新代码请使用 v2。

规范 2026-07-28(已发布)

发布于 2026-07-28(发布公告、变更日志)——目前是最新修订版。请注意,它在 SDK 中是选择加入的(参见“两个时代”):2025-11-25 仍是大多数已部署软件所使用的版本。

四项会影响你当下决策的重大变化:

  • MCP 是无状态且无会话的。 initialize 握手和 Mcp-Session-Id 已取消(SEP-2575、SEP-2567);每个请求都会在 _meta 中携带其协议版本、客户端身份和能力,跨调用状态则使用句柄(参见“有状态工具”)。不要基于会话亲和性构建新服务。
  • server/discover 是服务的 MUST——它会公布版本、能力和身份;客户端 MAY 跳过此步骤,并在内联处理中应对 UnsupportedProtocolVersionError。
  • Roots、Sampling、Logging 和 HTTP+SSE 传输已弃用,并纳入正式的功能生命周期管理(SEP-2577/SEP-2596)。它们仍可使用;设计新服务时不要依赖它们。HTTP+SSE 自身的时间表比默认的 12 个月更短——大约从 2026-09-03 起就具备移除资格。
  • 将应用自定义错误代码分配在 -32768..-32000 之外——-32020..-32099 为规范保留,-32000..-32019 属于旧版代码,新实现 SHOULD 完全避免使用(PR #2907)。

content 与 structuredContent 双重传递的问题没有变化——尚未确定优先级规则,因此上述指南仍然适用。

其他所有内容——MRTR、subscriptions/listen、_meta 身份键、requestState、Mcp-Method/Mcp-Name、可缓存结果、按请求设置日志级别、身份验证变更、移除项(SSE 可恢复性、ping、execution.taskSupport)、时代测试、工作组:请参阅 references/spec-2026-07-28.md

扩展

可选的、严格增量式的能力,名称格式为 {vendor-prefix}/{extension-name}(官方:io.modelcontextprotocol/*;第三方:反向域名)。在 2025 年时期的协议线上,通过 initialize capabilities 协商;在 2026-07-28,客户端会在 _meta["io.modelcontextprotocol/clientCapabilities"] 中按请求声明支持情况。官方扩展:MCP Apps(/ui,交互式 HTML UI,稳定版,广泛支持;自 2026-09-08 起的 ext-apps 2.0.0 - 仅 TypeScript 侧有破坏性变更,协议线上未变)、OAuth Client Credentials(草案)、Enterprise-Managed Authorization(2026-06-18 稳定版)、Tasks(自 2026-08-19 起为官方扩展)、Skills(io.modelcontextprotocol/skills,SEP-2640 最终版 - 以资源形式提供的技能)- 客户端矩阵。

除工具外的服务端 capabilities(除非另有说明,均采用 2025 年时期的调用方式):

能力用途v2 API
Elicitation在工具调用过程中请求结构化用户输入两个时期均可:返回 inputRequired({ inputRequests: { k: inputRequired.elicit(...) } })。在 2026-07-28 的连接上,ctx.mcpReq.elicitInput() 会抛出错误
Sampling从客户端请求大模型补全ctx.mcpReq.requestSampling()
Tasks具有生命周期管理的长时间运行操作官方扩展(SEP-2663)
Progress在请求中报告增量进度ctx.mcpReq.sendProgress()

在 2026-07-28,服务端完全无法向客户端发送请求:elicitation 和 sampling 通过 MRTR 进行(返回一个 InputRequiredResult,在重试时读取 inputResponses)。使用 inputRequired 风格编写处理程序:SDK 的默认旧版 shim 会将返回的请求转换为面向 2025 年时期客户端的实际 elicitation/create,因此一个处理程序即可同时服务两个时期。Tasks 已从核心移出,成为通过轮询获取的 io.modelcontextprotocol/tasks 扩展(ext-tasks)。

有关 MCP Apps 架构、ext-apps SDK 和构建模式:请参阅 references/mcp-apps.md 有关扩展系统、身份验证扩展、elicitation/sampling/tasks 详情和 MCP Registry:请参阅 references/extensions-registry.md

评论

支持 Markdown