MCP 2026-07-28:把会话删掉,协议变回无状态
MCP 新规范删掉了 initialize 握手与 Mcp-Session-Id,把跨调用状态改成显式的服务端句柄。这篇梳理这次改动动了什么、为什么,以及对已经写了 MCP 服务端的人意味着什么。
MCP 的新规范(2026-07-28)是一次减法。上一版是 2025-11-25,两者之间最大的变化可以一句话说完:
协议层不再有会话。
具体删掉的东西:
- Streamable HTTP 传输里的 协议级会话与
Mcp-Session-Id头 initialize/notifications/initialized握手ping、logging/setLevel、notifications/roots/list_changed- 原来的 HTTP GET 端点,以及
resources/subscribe/resources/unsubscribe
为什么删
会话是有状态的,而 HTTP 上的有状态协议有个老问题:请求被负载均衡打到另一台机器上就找不回上下文。要么粘性会话,要么共享存储,两条路都很贵,而且都让「水平扩容」变成一句空话。
MCP 之前的做法是给连接发一个 Mcp-Session-Id,之后所有请求都带着它。这在单机开发时毫无问题,一上生产就开始折磨人。
新规范把状态降级成数据:
- 每个请求自带协议版本与客户端能力,放在
_meta里(io.modelcontextprotocol/protocolVersion、io.modelcontextprotocol/clientCapabilities) - 客户端应该在每个请求上表明身份(
io.modelcontextprotocol/clientInfo),服务端应该在每个结果的_meta里表明自己(io.modelcontextprotocol/serverInfo) - 需要跨调用状态的,服务端自己签发一个显式句柄,当成普通工具参数传来传去(SEP-2567)
最后一条是这次改动的精髓:状态没有被消灭,只是从「协议偷偷替你记着」变成「一个你看得见、能存、能过期、能撤销的字符串」。
版本协商也换了位置
老流程是握手时协商一次。新流程里没有握手可协商,所以:
- 版本不匹配时返回
UnsupportedProtocolVersionError(SEP-2575) - 新增
server/discoverRPC,服务端必须实现它,用来公布自己支持的协议版本、能力与身份 - 客户端可以在任何其它请求之前调用它做版本选择,也可以在 STDIO 上把它当成向后兼容探测
订阅改成一个流
原来服务端要推送变更,得靠 HTTP GET 端点(长连接)加上 resources/subscribe。现在统一成一个 subscriptions/listen:一个长活的 POST-response 流,客户端明确订阅自己关心的类型(toolsListChanged、promptsListChanged、resourcesListChanged、resourceSubscriptions),服务端确认并给每条通知打上 io.modelcontextprotocol/subscriptionId。
注意一个细节:notifications/progress、notifications/message 这类请求作用域的通知仍然走它们所属请求的响应流,不走 subscriptions/listen。把这两类混在一起会让「这条进度属于谁」变得没法回答。
日志同理:等级改成每个请求自己带(io.modelcontextprotocol/logLevel),而没带这个字段的请求,服务端不得发 notifications/message —— 这条约束把「静默的客户端被日志淹掉」这件事从规范层面堵住了。
另外:tasks 被移出核心
实验性的 tasks 挪进了一个正式扩展 io.modelcontextprotocol/tasks。这是好事 —— 核心协议里不该长期养着实验特性,扩展可以独立演进、独立弃用。
对已经写了服务端的人意味着什么
如果你按 2025-11-25 实现了服务端:
- 删掉会话存储。这是收益最大的一步,也是这次改版的主要动机。
- 实现
server/discover。它是 MUST,不做的话新客户端没法做版本选择。 - 把跨调用状态改成句柄。任何你原本塞在会话里的东西(用户身份、临时上下文、分页游标)现在都要变成显式的工具参数。
- 检查日志。以前
logging/setLevel是全局开关,现在按请求生效,而且不发就等于「别说话」。
我的判断
这次改版把一个看起来方便的设计换成了一个看起来啰嗦的设计,但换来的东西很实在:任何一台机器都能处理任何一个请求,不需要粘性会话、不需要共享会话存储。
代价是每个请求都要多带几十字节的 _meta。这笔买卖在 HTTP 上几乎总是划算的 —— 无状态是 HTTP 能活到今天的原因,MCP 之前算是站在它肩膀上又给自己加了个轮子。

评论
…