MCP的版本号就是日期:2024-11-05、2025-03-26、2025-06-18、2025-11-25,再到2026-07-28。从外面看,它们像是同一类事件。最后一个不是。

打开网易新闻 查看精彩图片

工作组把这次候选版本称为“协议发布以来最大的一次修订”。通常这句话意味着一份很长的变更日志。这次它意味着:连接模型没了。没有initialize,没有会话ID,没有一直挂着的流。一年前如果有人让我用一句话描述MCP,那句话现在已经错了。

今天已经部署的东西不会停止工作,正因如此,这次变化很容易被一眼扫过去。

握手时代是怎么运转的

直到2025-11-25在内的每一个MCP修订版,开场方式都一样。客户端发送initialize,带上协议版本、能力和clientInfo;服务端回复它选定的版本、自己的能力与serverInfo;客户端再发notifications/initialized。只有走完这套,连接才被允许承载有用的东西。

这轮交换建立一个会话,之后的一切都相对这个会话定义。走Streamable HTTP时,服务端可以签发一个Mcp-Session-Id,客户端随后在每个请求里回传;DELETE结束它,Last-Event-ID恢复断掉的SSE流。

如果记得MCP的起点,这套设计完全说得通。2024年11月,它是作为编辑器子进程、跑在stdio上的东西:一个客户端、一个服务端、一根管道,管道的生命周期就是会话。当所有东西都只有一个时,开场协商一次能力不花任何代价。

Streamable HTTP在2025-03-26到来,把这套模型搬到了网络上。在网络上,“连接有记忆”不再免费。于是你只能二选一:粘性路由,或者共享会话存储。同一个问题,不同的账单。每次部署都会丢掉活着的会话,一个副本崩溃会带走对话。而且会话ID不告诉网关请求想要什么,网关必须打开JSON体,才能知道自己在计量的是tools/list还是一次数据库删除。

四个修订版往连接上加东西,然后一个修订版把连接删了。

2026-07-28到底改了什么

握手没了。没有initialize,没有initialized,没有Mcp-Session-Id。过去只协商一次的东西,现在随每个请求走,放在_meta里。

同一次工具调用,前后对比很直观:

  • 之前:2次往返。先POST一个initialize,带上protocolVersion、capabilities、clientInfo;再POST一个tools/call,请求头里带Mcp-Session-Id。
  • 之后:1个请求。请求头带MCP-Protocol-Version、Mcp-Method、Mcp-Name,body里直接是tools/call,clientInfo塞进_meta。

第二个请求是“冷”的。那个客户端从没和这个服务端说过话,事先什么都没建立,事后什么都不保留。三个新请求头的存在,是为了让网关不打开body就能路由和计量。

版本协商不再是一个阶段,而是一个字段。服务端对每个请求单独接受或拒绝;如果它说不了那个版本,就返回UnsupportedProtocolVersionError并列出自己能说的版本,客户端重试。还有一个server/discover方法,一次调用返回支持的版本和能力,服务端必须实现,客户端不必调用。发现变成了便利,不再是前置条件。

服务端到客户端的调用不再需要一条打开的管道。那正是对会话的承重依赖。一个需要向用户提问的工具,过去要在整个调用期间挂着一条流,把调用钉死在一个进程上。按SEP-2322,服务端返回InputRequiredResult然后挂断;客户端稍后用inputResponses和拿到的requestState作答,任何实例都能接手。SEP-2260进一步收紧:服务端只能在正在处理客户端请求时发起请求。两者合起来取代了elicitation/create和sampling/createMessage。

状态被搬到了明面上。规范写得很直白:需要跨调用携带状态的服务端,可以像HTTP API一直做的那样,从一个工具里签发显式句柄(比如basket_id、browser_id),让模型在后续调用中作为普通参数传回来。规范自己的说法是,这让状态“对模型可见,而不是藏起来”。会话是模型从未看见、一旦消失也无法恢复的状态。

基础设施可以不解析内容就读懂流量。请求头与body不一致的请求会被服务端拒绝,这让那些从不看内部的组件可以放心信任请求头。

为什么是2.0而不是1.4

协议改动是你注意到的部分。治理改动决定它能不能活过下一个修订版,一共三条。

扩展成为一等公民:反向DNS标识符,通过extensions映射协商,独立于规范版本,住在自己的ext-*仓库里、有自己的维护者。Tasks从核心毕业,进入io.modelcontextprotocol/tasks。MCP Apps——沙箱iframe里服务端渲染的HTML,通过同一套JSON-RPC回话——也作为一个扩展发布。核心保持小,有意思的部分按自己的速度走。

功能有了生命周期:Active、Deprecated、Removed,有文档化的迁移路径,任何被弃用的功能至少十二个月后才够格移除。三个功能立即进入这个流程,三个都还能用——把政策写下来,意义就在这里。

一致性测试成了闸门。一个Standards Track SEP在对应场景进入一致性套件之前不能到达Final,官方SDK按分层体系对着这套件打分。四个Tier 1 SDK(TypeScript、Python、Go、C#)在规范发布当天就支持2026-07-28,Rust在beta。

剩下的破坏性改动不大,但值得知道:工具schema转向完整的JSON Schema 2020-12,oneOf、$ref和条件语句现在合法,规范明确说不得自动解引用外部$ref URI;structuredContent接受任意JSON值,不再只限对象;资源缺失错误从MCP自定义的-32002改成标准的-32602,任何按这个字面量匹配的代码都会断;每一个2025-11-25的Tasks实现都需要迁移。

有人早就押了这一边

Appwrite的MCP服务端做了十六个月,大部分时间花在授权上。轮到传输层时,托管服务端带着一个标志上线:StreamableHTTPSessionManager(app=server, json_response=False, stateless=True)。没有会话。每个请求带一个bearer token,验证它、用它构建客户端、服务这次调用、忘掉一切。

这不是先见之明。真实原因更平淡:OAuth已经让会话状态失去意义,因为token在每个请求里标识用户,而没人想为了一个用不上的东西去跑Redis。

规范7月28日发布,7月29日就合并了支持,PR #89,从MCP Python SDK v1.28升到v2.0.0。HTTP和OAuth接口一点没动。让它只花一天而不是一个季度的,是v2的会话管理器按协议版本头路由,一次部署同时服务两个时代:握手版本走旧路径,其他走按请求处理的新路径,没有人的客户端断掉。

真正花工夫的是三件小事:

  1. 工具错误。v1把抛出的异常包成CallToolResult(isError=true);v2把未捕获异常变成脱敏的-32603,模型再也看不到调用为什么失败。confirm_write的拒绝只有在模型能读到它时才有价值,所以每个handler现在显式返回is_error=True。
  2. CORS。Mcp-Method和Mcp-Name搭在每个现代请求上,允许列表里不写它们,浏览器客户端会在到达鉴权代码之前就失败。
  3. 指标。现代客户端从不发initialize,握手计数器会在客户端迁移时悄悄归零——那是最不该丢图表的时刻。现在它按活动窗口计数,过渡期的时间序列仍然可比。

这些都不是协议工作,只是一个依赖在你脚下改变形状的日常成本。它只花了一天,是因为十八个月前为一个完全无关的理由做出的决定,恰好对上了。

那种好的无聊

一个2026-07-28的MCP服务端,接收HTTP请求、读几个请求头、返回响应。形状就这么多。你可以把它放在CDN后面,缓存它的列表端点,在别人对话中途重启它而不用告诉任何人。

它比2024年发布的那版更没意思。我认为这正是重点。双向有状态的那版读起来更有趣,运营起来差得多,而我们大多数人实际在做的是运营。