关于 mcp-best-practices
MCP Best Practices 是面向使用 TypeScript SDK 构建生产级 MCP 服务的决策参考,适合已有可运行服务、需要检查实现质量的开发者。内容涵盖协议版本、传输、安全、工具设计及部署注意事项。
主要功能
- 01比较协议版本与 SDK 行为
- 02指导传输方式和无状态服务设计
- 03说明工具注册、命名与参数模式
- 04涵盖分页、注释和结果传递
- 05列出安全与兼容性注意事项
适用场景
- 01升级 TypeScript MCP SDK 前核对兼容性
- 02选择远程或本地传输方式
- 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.mdLICENSE.txtreferences/error-handling.mdextensions-registry.mdmcp-apps.mdsdk-bugs.mdsecurity-auth.mdspec-2026-07-28.mdtool-schema-guide.mdtransport-patterns.mdv2-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 / 无状态 | WebStandardStreamableHTTPServerTransport | sessionIdGenerator: undefined,enableJsonResponse: true |
| 远程,有状态(仅 2025 时代) | WebStandardStreamableHTTPServerTransport | sessionIdGenerator: () => randomUUID() |
| 本地 CLI / Claude Desktop | StdioServerTransport / 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
注解
这些都只是可选提示(根据规范,应将不可信服务提供的提示视为不可信):
| 注解 | 默认值 | 含义 |
|---|---|---|
readOnlyHint | false | 工具不会修改其环境 |
destructiveHint | true | 可能执行破坏性更新(仅当 readOnly=false 时) |
idempotentHint | false | 使用相同参数重复调用不会产生额外影响 |
openWorldHint | true | 与外部实体(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: [] + structuredContent | JSON.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)。
策略:
- 每个服务提供 5-15 个工具——这是社区认可的最佳范围。超过这个数量就拆分。
- 面向结果的工具——将多步操作整合为单个工具(例如,使用
track_order(email),而不是get_user+list_orders+get_status)。 - 响应粒度——返回经过筛选的结果,而不是原始 API 数据。例如,返回 800 个令牌的用户对象,而不是 20 个令牌的摘要。
outputSchema+structuredContent——为程序化客户端/PTC 客户端提供类型化输出。注意:对于使用影子模式的客户端,structuredContent会被字符串化并放入模型上下文,其令牌成本与文本相同——它不是免费的带外通道(参见“工具结果传递”)。- 动态加载工具——根据请求上下文仅注册相关工具子集(例如使用
?tools=search,fetch查询参数)。如果工具集在会话期间发生变化,则配合使用listChanged。每个连接使用不同的集合,不要在对话中途更改集合:工具定义位于提示词前缀中,“在对话中途添加或移除工具定义会使该缓存失效,而由此产生的缓存未命中可能比被移除的定义消耗更多令牌。” 即使尚未到达你公布的ttlMs,客户端也必须在收到list_changed时立即将缓存列表视为过期。 - 渐进式工具发现 / 代码模式——大型目录的客户端越来越多地使用
search_tools元工具和程序化工具调用,此时structuredContent会在模型上下文之外被使用(客户端最佳实践)。经过筛选且描述清晰的工具有助于这些流程顺利运行。
结果大小预算(每个客户端的上限)
客户端会静默截断较大的工具结果。请按目标客户端中限制最严格的一方设置预算:
| 客户端 | 默认上限 | 可配置 |
|---|---|---|
| Claude Code | 25,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 CLI | 40,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 theOriginheader 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 anMCP-Protocol-Versionheader",其值必须与请求体_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;两者都不设置则永远不会检查受众。使用 PKCES256、短时效令牌和最小化作用域(按工具提升作用域时使用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
关键破坏性变更:
- 拆分软件包:
@modelcontextprotocol/sdk->@modelcontextprotocol/server+/client+/core - 以 ESM 为先(在 beta.2 中恢复了 CJS 构建),Node.js 20+(支持 Bun/Deno)
- 必须使用 Zod v4(或任何 Standard Schema 库)
McpError->ProtocolError(来自@modelcontextprotocol/core)extra参数 -> 结构化的ctx,其中包含ctx.mcpReqserver.tool()->registerTool()(使用配置对象,而非位置参数)- 移除 SSE 服务端传输(客户端仍可连接旧版 SSE 服务端)
createMcpHandler(factory)取代按请求创建传输并连接connect()的接线方式;@modelcontextprotocol/hono/express/node对其进行适配- 默认启用 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