MCP协议最大一次修订,移除了session/initialize握手、服务端主动请求等机制,并给出每个错误的具体修复方案;12个月后legacy功能将完全废弃。
Model Context Protocol 于 2026 年 7 月 28 日发布了 2026-07-28 修订版,这是该协议自发布以来最大的一次变更。会话(Session)没了。初始化握手没了。服务器再也無法主动发起请求了。
如果你在运行一个 MCP 服务器,首先需要知道的是:今天不会有什么东西自动坏掉。一个使用旧版修订的服务器可以继续与使用旧版修订的客户端协同工作,Anthropic 已表示支持正在逐步推广到各 Claude 产品,但没有公布通用的上线时间或各产品的具体日期。破坏性影响会在客户端在你下方升级时到来,而且一旦到来就是双向的彻底破坏——因为现代客户端无法与旧版服务器通信,而旧版客户端根本没有向前兼容到现代版本的能力。
本次修订中还移除的内容包括:ping、logging/setLevel、SSE 流可恢复性、resources/subscribe、resources/unsubscribe、tasks/list 和 tasks/result。Roots、sampling、logging 和动态客户端注册(Dynamic Client Registration)已被废弃,相关倒计时为十二个月。
以下是完整的差异列表、升级时你会遇到的每个错误及其修复方法,以及唯一一个规范与发布博客给出不同答案的截止日期——这件事很关键,因为大多数人会读的是发布博客。
本次发布了八项变更,与其把它们理解为八个独立的功能,不如理解为一个决策及其七个后果,这样更容易掌握。
一切都可以追溯到一个决策:SEP-2575 和 SEP-2567,两者共同使协议变成了无状态的。不存在握手,也不存在会话标识符。每个请求都在 _meta 中携带自己的协议版本和客户端能力,一个新的 server/discover 方法取代了以往在连接时发生的能力交换。服务器必须实现 server/discover;客户端可以完全跳过它,转而处理错误。
其他所有变更都源于此。由于服务器无法再保持连接打开,因此也无法再主动发起请求,所以多轮请求(Multi Round-Trip Requests,SEP-2322)取代了 sampling/createMessage、elicitation/create 和 roots/list,新模式是:服务器返回 resultType: "input_required",然后客户端在答案中附上原始调用并重新发送。由于操作不再能从会话中推断,它被移到了 HTTP 头部,所以 Mcp-Method 和 Mcp-Name(SEP-2243)现在是必需的,负载均衡器可以在不解析 body 的情况下根据它们路由。由于列表结果不再因连接而异,它们变得可缓存了,所以 ttlMs 和 cacheScope(SEP-2549)现在是六个结果类型上的必填字段。
还有四个变更结构上不那么核心,但也值得了解。授权在六个 SEP 中得到了加固,RFC 9207 issuer 验证现在在客户端是强制性的,动态客户端注册正式废弃,取而代之的是客户端 ID 元数据文档(Client ID Metadata Documents)。Tasks 从核心协议移到了一个可选扩展(SEP-2663),并重新设计了轮询 API。一个正式的扩展框架(SEP-2133)让任何人都可以在不需要规范变更的情况下,以反向 DNS 标识符发布协议功能。还有一个正式的废弃策略(SEP-2596)到来,规定最少十二个月的窗口期、已发布的注册表,以及 SDK 层面标记废弃接口的义务。
以下是一张图展示的完整修订。
initialize 加 notifications/initialized
无。按请求的 _meta
Mcp-Session-Id 头部,404 时重新初始化,DELETE 时结束
移除。显式的应用层处理
每个连接协商一次
每个请求声明一次
server/discover,服务器必须实现,客户端可以调用
POST 加 GET SSE,通过 Last-Event-ID 可恢复
仅 POST。现代仅限服务器应在 GET 和 DELETE 上返回 405。不可恢复
操作在 JSON body 中。MCP-Protocol-Version 头部自 2025-06-18 起为必填
两个新的必填头部,每个请求上要有 Mcp-Method,其中三个还要有 Mcp-Name
服务器向客户端的请求
sampling/createMessage、elicitation/create、roots/list 在活动流上
MRTR:resultType: "input_required" 加 requestState,客户端用新的 JSON-RPC id 重试
resources/subscribe 加 GET SSE 流
subscriptions/listen,带显式通知过滤器
六个结果类型上的 ttlMs 和 cacheScope(发布博客说四个 changelog 说五个;schema 和缓存页面说六个)
Tasks,实验性,在核心中。阻塞的 tasks/result,加 tasks/list
io.modelcontextprotocol/tasks 扩展。轮询 tasks/get,加 tasks/update。无 tasks/list
DCR(MAY)加 CIMD(SHOULD)
DCR 废弃。CIMD 是正确路径。application_type 现在必须是
RFC 9207 iss 验证,必须,包括在错误响应中
logging 能力,logging/setLevel,notifications/message
废弃。_meta 中每个请求的日志级别,stderr,OpenTelemetry
-32042(需要 URL 引出)
三个新码,-32020 到 -32022,在保留的 -32020 到 -32099 块中。-32042 退役
JSON Schema 2020-12 作为默认方言
完整的 2020-12 关键字集。默认禁止网络 $ref 解析。需要 DoS 边界
可能因连接而异
不得因连接而异。应该具有确定性顺序
实验性能力包
正式扩展框架,反向 DNS 标识符,可选,独立版本控制
正式策略。十二个月底线,已发布注册表,SDK 义务,九十天安全例外
每个结果上必填 resultType
兼容性在规范本身中有发布,那个矩阵有七行而不是大多数报道在复制的四行。被通常丢弃的两行是具有可操作性的行。
失败。旧版客户端没有前向兼容机制
按旧版修订正常工作
现代意味着 2026-07-28 及之后,带每个请求的元数据。旧版意味着 2025-11-25 及之前,带初始化握手。双时代意味着同时支持两个时代的实现,这是如果你有真实用户的唯一安全姿态。
Python SDK v2 默认给你这个,值得理解其机制而不是假设它。Server.run 驱动一个双时代循环,其中客户端的第一个请求决定连接的年代,仅此一次。携带 2026-07-28 每请求 _meta envelope 的请求打开一个现代连接;其他任何请求(包括初始化握手)则打开一个旧版连接。随后来自另一个时代的声明会被拒绝:现代连接上的 initialize 返回 UNSUPPORTED_PROTOCOL_VERSION 并列出所服务的版本,旧版连接上的 envelope 请求返回 INVALID_REQUEST。因此 v2 服务器按连接(而非按请求)服务两个时代。一个不在发布说明中、而我在源码中找到的注意事项:HTTP 时代路由当前是基于头部的,带 body-primary 分类的作为后续跟进。
有二十项会断裂。以下按严重程度排列,total 意味着实现完全停止工作而非降级。
initialize 和 notifications/initialized 移除
每个实现了握手的客户端和服务器
Mcp-Session-Id 和会话移除
任何持有每个会话状态的服务器,任何恢复会话的客户端
每个请求的 _meta 加 protocolVersion 和 clientCapabilities 现在是必填的
任何不带它们的请求返回 -32602 和 HTTP 400
MCP-Protocol-Version 头部必填且必须与 body 匹配
不匹配返回 400 加 -32020
所有请求上 Mcp-Method 必填,其中三个上 Mcp-Name 必填
任何手写的 HTTP 客户端
服务器主动请求被禁止,MRTR 取代它们
每个使用 sampling、elicitation 或 roots 的服务器
GET SSE 流没了,可恢复性也移除了。现代仅限服务器应在 GET 和 DELETE 上返回 405
任何依赖 GET 流或 Last-Event-ID 的客户端
resources/subscribe 和 resources/unsubscribe 移除
任何资源订阅客户端
Tasks 重新设计并移到一个扩展
每个使用实验性 2025-11-25 Tasks API 的人
总计,对 tasks 用户
每个结果上 resultType 必填
客户端未将缺失值默认为 "complete",或未处理 "input_required"
资源未找到从 -32002 移到 -32602,-32042(需要 URL 引出)不得再发出
任何在任一字面代码上做匹配的客户端
notifications/cancelled 仅为 stdio-only。Streamable HTTP 未定义客户端到服务器的通知;关闭 SSE 响应流是取消信号
任何发送它的 HTTP 客户端,以及任何只在收到它时取消的服务器
ping、logging/setLevel 和 notifications/roots/list_changed 移除
健康检查循环和日志级别控制
notifications/elicitation/complete 和 elicitationId 移除
URL 模式引出关联
工具列表不得因连接而异
任何按会话而非按 token 对 tools/list 做个性化的服务器
任何 JSON Schema 2020-12 关键字现在都是允许的,但网络 $ref 解析默认必须关闭,组合关键字需要资源边界
依赖远程 $ref 解析的 Server
无效的 x-mcp-header 强制客户端从 tools/list 中丢弃该工具
工具静默消失
ttlMs 和 cacheScope 在六种结果类型上不可省略
省略它们的 Server 均不符合规范
DCR 已废弃,application_type 现在为必填项
OIDC 严格授权服务器拒绝原生重定向
RFC 9207 iss 验证现在在客户端侧为 MUST
跳过此验证的客户端不符合规范
同样值得陈述的是那些不会造成破坏的内容,因为它比你预期的要多。stdio 消息帧保持不变。工具、资源、提示、补全、分页和进度均保持原有形态。Elicitation 在形式和 URL 模式中均得以保留,只是投递方式不同。而 roots、sampling 和 logging 被废弃而非移除,这意味着它们仅作为注解存在,并将在至少十二个月内继续工作。
在时代检测中有一个陷阱值得单独成段,因为它正是实现会出错的地方,而且规则因传输方式不同而不同。
在 HTTP 上,你尝试一个现代请求并在 400 Bad Request 时回退,但现代服务器对 UnsupportedProtocolVersionError、MissingRequiredClientCapabilityError 以及 header 验证失败也会返回 400,因此规范说你应该在得出任何结论之前先检查 body。在 stdio 上,你用 server/discover 探测,其规则更严格:回退不得以某一个特定错误码为 клю。时代是服务器的属性而非单个请求的属性,因此在 stdio 上将这个判定缓存到服务器进程生命周期,在 HTTP 上缓存到 origin 生命周期。
选择双时代一个容易遗漏的后果:针对 GET 和 DELETE 的 405 指引是针对仅支持此修订版本的服务器而写的。如果你同时服务两个时代,你仍然需要为旧版客户端保留 GET 流,所以不要一刀切地 405 拒绝它。
同样值得直说的是围绕这一切的信息传递中存在一种张力。Glama 的运营者——他运营着最大的开源 MCP server 索引——在 Hacker News 上写道:"新协议在两个方向上都是 wire-incompatible 的。这意味着许多 server/client 将需要重构(仅更新 SDK 是不够的)。这需要时间,而且会很混乱。"同一讨论串中的一位维护者写道:"此协议变更不需要你对现有运行的 MCP 代码做任何改动。"两句话都是真的,只是针对不同情况。对于无人触碰的部署第二句为真。对于线缆另一端任何东西发生变动的时刻第一句为真。
升级到 MCP 2026-07-28 后你会遇到的所有错误
这些大致按你遇到它们的概率排序。
升级后出现不支持的协议版本,错误码 -32022
这是两个时代之间无法通信,是你这周最常看到的东西。错误将有用数据携带在 error.data 中:
{
"jsonrpc": "2.0", "id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": { "supported": ["2026-07-28", "2025-11-25"], "requested": "1900-01-01" }
}
}
如果 data.supported 仅包含旧修订版,说明 server 未升级。如果仅包含 2026-07-28,说明客户端未升级。双向的修复方法都是同时服务两个时代而非二选一,实际路径是升级 SDK 而非手写协商逻辑,因为协商的实际边缘情况比看起来更多。
有一件事你无法从客户端侧修复:旧版客户端打到仅支持现代协议的 server。在旧版修订中不存在前向回退路径,因此该组合会持续失败,直到客户端更新。如果你运营该 server,这是为何要远超必要时机仍保持双时代运行的理由。
400 Bad Request 附带 HeaderMismatch 错误,错误码 -32020
三个 header 现在在每个 Streamable HTTP POST 上都是必填的,且它们的值必须与 body 匹配:
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json
精确说明这些的范围是值得的,因为它容易被过度应用。Mcp-Method 在所有请求上均需要。在核心协议中,Mcp-Name 仅在 tools/call、resources/read 和 prompts/get 上需要,其中它携带 params.name 或 params.uri。在 tools/list 上发送 Mcp-Name 不是规范所要求的。
tasks 扩展随后又增加了三个。SEP-2663 要求 tasks/get、tasks/update 和 tasks/cancel 在 Streamable HTTP 上将 Mcp-Name 设置为 params.taskId,以便"传输中介和负载均衡器[可以]将同一任务的后续请求路由到持有其状态的服务器实例,这通常对正确性是必需的。"值得停下来仔细体会这句话。这在下面会有进一步讨论。
还要注意 MCP-Protocol-Version 并非新物。自 2025-06-18 起它在 HTTP 上就已经是必填的。两个真正新增的强制 header 是 Mcp-Method 和 Mcp-Name,加上你的工具选择加入的任何 Mcp-Param-*。
Header 名称大小写不敏感,header 值大小写敏感,对于整数比较应该是数值比较而非字符串比较,因此 42.0 和 42 匹配。
有一个真正的警告,它在规范落地前一周发布。Christian Posta 在 Solo.io 指出 header 会说谎:攻击者在 header 中发送 Mcp-Name: echo 而 body 调用 printEnv,一个仅根据 header 做 allowlist 的代理就会放行。
存在两种缓解措施。处理 body 的服务器必须用 -32020 拒绝不匹配。而在镜像 header 上强制策略的中介应该验证 MCP-Protocol-Version 表明需要 header-body 验证的修订版,否则应该拒绝而非信任 header。两者都是正确的。第二种也是一个 SHOULD,但它施加在最有动机不遵守的组件上,而且要求中介具有 MCP 版本感知能力,这正是 Posta 正在论证的 MCP 原生数据平面。他的帖子结尾是一个可工作的修复方案而非死胡同。以这种方式阅读,并将这些 header 视为路由提示而非授权事实。
这里有一种与你的代码无关的操作失败。检查你的反向代理、CDN 或 API 网关是否会剥离未知请求 header,因为这些 header 会在到达服务器的途中静默消失,而每个请求都将因从两端都无法察觉的原因而验证失败。
你服务器发起的请求停止工作了
服务器不能再发起请求了。这不是对它们何时可以这么做的限制;schema 中根本不存在 ServerRequest 联合类型。sampling/createMessage、elicitation/create 和 roots/list 作为类型仍然存在,但仅作为结果内部的 payload。
MRTR 是替代方案。你的服务器返回一个命名了它所需内容的不完整结果:
{
"jsonrpc": "2.0", "id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"github_login": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please provide your GitHub username",
"requestedSchema": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
}
}
},
"requestState": "AEAD-protected blob"
}
}
客户端收集输入并重新发送原始请求,附带 inputResponses 和逐字节回显的 requestState,使用新的 JSON-RPC id,因为规范将重试视为一个独立请求。
如果你跳过它们,六条规则会咬你一口。InputRequiredResult 仅允许出现在 tools/call、prompts/get 和 resources/read 上,服务器不得在其他任何内容上发送它。客户端不得检查、解析或修改 requestState,也不得在重试时包含服务器未发送的 requestState。服务器不得发送客户端在该特定请求上未声明的 inputRequest 类型。服务器不得假设客户端会最终满足请求或重试,因此每个 MRTR 交换都需要超时机制和放弃的清晰路径。而 inputRequests 和 requestState 仅适用于那一个请求的重试,不适用于客户端在飞行中并行持有的任何内容。
第六条是会直接导致崩溃而非产生错误报告的那条。inputRequests 是可选的。schema 要求 inputRequests 或 requestState 至少存在一个,它命名了仅存在 state 的情况:负载丢弃。承受压力的服务器可以仅返回 requestState,规范说客户端然后可以立即重试。任何在 result.inputRequests 上写 for key in 的客户端会在该响应上抛出异常,恰好发生在服务器最无法吸收重试风暴的时刻。
MissingRequiredClientCapability,错误码 -32021
现在能力(capability)是每个请求单独声明的,而不是每个连接声明一次,服务器不能依赖客户端在特定请求中没有发送的能力。当服务器需要某个未声明的能力时,会返回 -32021,并在 data.requiredCapabilities 中列出缺失的能力;在 HTTP 传输中返回 400 状态码。
在客户端侧,不要再把能力当作连接设置来对待。它们应该放在每次调用的 _meta 中:
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "elicitation": {} },
"io.modelcontextprotocol/clientInfo": { "name": "my-app", "version": "1.0" }
}
protocolVersion 和 clientCapabilities 是必填的。clientInfo 是可选的,但你应该发送它,而且规范明确说明 clientInfo 和 serverInfo 都不被协议验证,因此不应该基于两者做出任何安全决策。
资源未找到现在返回 -32602,而不是 -32002
错误码布局在所有实现之下都发生了变化。2025-11-25 发版的两个错误码已废弃,三个新的错误码进入新预留区间。
-32002,资源未找到
2025-11-25 及更早版本
已废弃。使用 -32602。客户端应该继续接受来自旧服务器的此错误码。
-32042,需要 URL 提取
已废弃。本修订版的实现不得发出此错误码。
-32020,header 不匹配
-32021,缺少必需的客户端能力
-32022,不支持的协议版本
从 -32020 到 -32099 的错误码现在预留给规范,实现不得发出规范中未定义的此区间内的错误码。-32000 到 -32019 区间是遗留区间,除了 -32002 之外,接收方不得对其中的错误码假设任何特定含义。
有一个澄清值得说明,因为多篇文章都写错了,包括本文的早期版本。这三个新错误码在 2026-07-28 草案周期内曾短暂编号为 -32001、-32003 和 -32004,在发布前重新编号了。它们从未出现在 2025-11-25 中,后者的 schema 只定义了一个非标准错误码 -32042。因此,如果你是从已发版的 2025-11-25 实现迁移,实际上在你手下变化的错误码是 -32002 和 -32042。如果你是基于候选版本(release candidate)构建的,请检查是否还有旧的那三个错误码。
你的工具从 tools/list 中消失了
这是本版本中最隐蔽的真正破坏性行为,因为它不会产生任何 JSON-RPC 错误,而且与传输方式相关,会浪费某人一下午的时间。
x-mcp-header 是一个新的注解,允许服务器标记各个工具参数,使其被镜像到 Mcp-Param-{Name} HTTP header 中,这样网关就可以在不读取 body 的情况下根据参数值进行路由或限流。它对服务器是可选的,对客户端是强制的。其约束非常严格:只能是基本类型(primitive types),数字(number)明确不允许,而整数(integer)、字符串(string)和布尔值(boolean)可以;该属性必须能够通过仅包含属性键的链从 schema 根静态访问,因此不能位于 items、$ref、oneOf、anyOf、allOf、not 或 if/then/else 之后;值在整个 inputSchema 中必须大小写不敏感地保持唯一;annotated 的整数必须位于 JavaScript 安全范围内;而且值必须是有效的 HTTP 字段名 token,不能包含控制字符。
如果违反了以上任何一条,客户端必须将该工具从 tools/list 的结果中排除。没有 JSON-RPC 错误。规范确实要求客户端记录一条警告,指出工具名称和原因,但这是 SHOULD 级别,而且它出现在客户端日志中而不是给模型或用户看,所以工具simply就是不在了,对话中没有任何内容说明原因。
真正会让你花掉一下午的是:这条规则仅绑定于 Streamable HTTP 客户端。其他传输上的客户端可以完全忽略 x-mcp-header。因此,同一个服务器、同样的工具定义,在你的笔记本电脑上通过 stdio 可以暴露该工具,而在生产环境中通过 HTTP 却会隐藏它。
400 且无明显原因,以及缺失的 Accept header
这不算新问题,但手写客户端 constantly 犯错,而且本修订版给了他们更多犯错的机会。客户端必须包含 Accept header,列出 application/json 和 text/event-stream,因为服务器在单个 JSON 响应和 SSE 流之间做选择,而客户端必须对两者都做好准备。
对服务器未实现的方法返回 404 Not Found
这是一个针对网关和所有有重试逻辑的人的陷阱。未实现的 RPC 方法返回 HTTP 404 和 JSON-RPC -32601,这是有意与传输层故障区分开的。任何把 404 视为"服务器没了"并销毁连接或 failover 的行为,在遇到一个完全健康但 simply 没有实现你请求方法的服务器时会产生错误行为。在对 404 下结论之前,先检查 body,正如你已经需要对 400 做的那样。
你的列表结果被提供给了错误的租户
这不是一条错误信息,所以才放在这里。ttlMs 和 cacheScope 现在在六种结果类型上是必填的:server/discover、tools/list、prompts/list、resources/list、resources/templates/list 和 resources/read。值得注意的是,发版博客列出了其中四个 changelog 列出了五个;而 schema 和缓存页面说六个,那才是算数的。
cacheScope 取 "public",意味着任何客户端、网关或缓存代理都可以向任何用户提供响应;或者取 "private",意味着只能在相同的授权上下文中重用,不得跨上下文共享。在认证端点上把这个搞错,你就以协议的背书构建了一个跨租户数据泄漏。规范几乎原话说了:服务器必须意识到"public"响应可能在不同调用者之间共享,即它来自认证端点时也不例外。
两条随之而来的规则。MRTR 重试产生的结果永远不得缓存,因为它们依赖于……