MCP 在 2026-07-28 修订里把传输层拆了重搭:initialize / notifications/initialized 握手删了,Mcp-Session-Id 会话也删了。现在每个请求都是自包含的,自己带协议版本、身份和能力声明,服务器不需要记住这个客户端上一秒说过什么。
影响很实际。以前自托管的远程 MCP server 要水平扩容,会话会把客户端钉死在签发它的那台实例上,要么配 sticky 路由,要么搞共享 session 存储。现在请求落到哪台实例都一样,普通轮询的负载均衡就能跑。官方博客把这次修订称为 remote MCP 上线以来最重要的一次,配了详细的迁移说明。
这篇把新协议完整跑一遍:一个最小的 Python server,加一串 curl 请求。环境是官方 Python SDK 2.x,写这篇时 PyPI 上最新是 2.2.0。
第一个请求就是业务请求#
旧版 Streamable HTTP 的请求序列是两步:先发 initialize 换一个 Mcp-Session-Id,之后每个请求都带上它。新协议里第一步没了,下面这个请求打到刚启动的服务器上,不带任何前置状态:
| |
返回是个普通 JSON(content-type 是 application/json,不想要流的时候服务器不会硬塞一条 SSE 过来),节选如下,工具 schema 的细节省略:
| |
resultType 是所有结果的必填项,普通结果是 complete,另有 input_required(后面细说);ttlMs 和 cacheScope 是缓存提示,tools/list 这类列表结果和 resources/read 现在必须带。ttlMs 是毫秒级新鲜度提示,语义类似 HTTP 的 Cache-Control: max-age,0 表示立即过期;cacheScope 是 public 或 private,决定共享网关能不能缓存这份响应。
响应里没有 Mcp-Session-Id,请求里也没有。顺手做了个对照:把服务器进程杀掉,换个新进程起来,同一个请求再打一次,照样 200。会话时代遗留的状态问题(重启后会话悬空、客户端重新握手)也就没有了。
版本、身份、能力:每个请求自己带#
没有协商握手,协商动作被下放到了每个请求的 _meta 里:
io.modelcontextprotocol/protocolVersion:请求用的协议版本,必填。io.modelcontextprotocol/clientInfo:客户端名字和版本,建议每个请求都带。io.modelcontextprotocol/clientCapabilities:能力声明,服务器据此决定能向客户端要什么。比如客户端没声明elicitation,服务器就不能向它发问询请求。- 服务器侧对称:每个结果的可选
_meta里放io.modelcontextprotocol/serverInfo。
版本对不上时,服务器返回 -32022,data.supported 里列出自己支持的版本。实测把版本写成 1900-01-01(HTTP 400):
| |
客户端从 supported 里挑一个双方都支持的版本重试就行,一次往返解决,不用专门握一轮手。
如果客户端想开局就摸清服务器底细,可以调 server/discover(服务器必须实现,客户端可选)。一个请求拿回支持版本、能力清单和身份:
| |
头和体必须对得上#
新的 Streamable HTTP 把几个 body 字段镜像进了 HTTP 头,网关、限流器、WAF 不解析 body 就能做路由:
MCP-Protocol-Version:所有 POST 必带,值必须与_meta里的 protocolVersion 一致。Mcp-Method:镜像method字段,所有请求必带。Mcp-Name:镜像params.name或params.uri,tools/call、resources/read、prompts/get必带。
规则是:体是唯一事实来源,头只是镜像,服务器必须校验两者一致,对不上就 400 + -32020(HeaderMismatch)。把 Mcp-Name 写成 multiply、body 里却是 add,结果:
| |
这个校验有实际动机:如果头里说调的是 A 工具、体里写的是 B 工具,转发层和服务器就建立在两套事实上,路由和限流可以这样被绕。
另外两个小细节:工具参数可以用 schema 里的 x-mcp-header 声明镜像成 Mcp-Param-{Name} 头(值不是安全 ASCII 时用 =?base64?...?= 哨兵格式包裹),给按租户限流这类场景用;老客户端残留的 Mcp-Session-Id 请求头会被直接忽略,GET / DELETE 打到端点返回 405,Last-Event-ID 也失效了,SSE 流断了不能续传,要换个新 id 重发。
服务器要问用户:多轮往返(MRTR)#
旧协议里,服务器中途要问用户(elicitation/create)、借客户端模型(sampling/createMessage)、要文件系统信息(roots/list),靠的是在常开流上反向发请求。新修订把这条路整个拆了,换成 Multi Round-Trip Requests(MRTR,SEP-2322),官方标注这是 breaking change。
新流程:服务器需要额外输入时,返回 resultType: "input_required",附带 inputRequests(要问的内容)和 requestState(服务器自己的不透明状态)。客户端收集好答案后,换一个新的 JSON-RPC id 重发原请求,带上 inputResponses 和原样回传的 requestState。
调用一个要先确认的 deploy 工具,第一回合的返回(HTTP 200,走正常结果通道,不是错误):
| |
inputRequests 的键是服务器起的名字,requestState 是一段对客户端不透明的加密串(SDK 的实现是 AES-256-GCM,密文带 v1. 前缀)。客户端要做的事只有一件:把答案填进 inputResponses,把 requestState 原样抄回来,换新 id 重发。重试时 params 多了这两个字段,其余不变:
| |
第二回合返回最终结果:
| |
只有 tools/call、resources/read、prompts/get 三种请求允许这种中间结果;重试的 id 必须和首次不同,规范原文要求把它们当两次独立请求处理;requestState 对客户端是不透明字符串,不许解析、修改、假设内容,重试时原样回传。反过来,服务器必须把它当攻击者可控输入对待,只要它影响授权或业务逻辑就必须做完整性保护,并建议绑定主体、加 TTL、绑请求摘要来收窄重放窗口。
跨调用的状态,协议层不再提供归宿,交给应用自己管:服务端发一个显式 handle(比如 basket_id),让模型用普通参数在后续调用里带回来。状态从传输层的隐藏元数据挪进了模型的上下文,官方认为这反而更好:模型能看到、传递、组合这些 handle,而藏在会话里的状态对它完全不可见。
写一个最小的无状态 server#
server 全文,保存为 server.py:
| |
deploy 的第二个参数不是给模型填的,Resolve(ask_confirmation) 把它标成一个解析器参数:SDK 先跑 ask_confirmation,拿回一个 Elicit 请求标记;对 2026-07-28 的连接,框架把它包进 InputRequiredResult 发出去,重试进来时再把答案注入。同一份代码配旧协议客户端,SDK 会退回中途发请求的老行为,传输语义跟着协商出来的版本走。这个取舍在 SDK 源码的 resolve.py 里写得明确:批量打包成 InputRequiredResult,还是逐个中途发送,由协商出的版本决定。
起服务:
| |
服务起来后,用上面的 curl 直接打就行。SDK 客户端这一侧更省事,MRTR 往返被 call_tool 内部消化,应用只要给 elicitation 注册一个回调:
| |
跑出来是这样:
| |
客户端自己完成了 server/discover 探测、版本选择、MRTR 重试这些事,call_tool 返回的就是最终结果。
升级时会被波及的几件事#
Roots、Sampling、Logging 三个功能被一次性标记为废弃(SEP-2577):还能用,窗口至少 12 个月,但新实现就别往里投了。迁移方向:目录和文件信息改走工具参数或资源 URI;Sampling 改成直接对接模型供应商 API;日志写 stderr(stdio)或走 OpenTelemetry。ping 和 logging/setLevel 也删了,日志等级改为按请求在 _meta 里设置。老的 HTTP+SSE 传输正式进了废弃流程(SEP-2596)。
通知机制换了条路:独立 GET 流整个删掉,改成 subscriptions/listen,一条 POST 开出的长 SSE 流,客户端按类型订阅(toolsListChanged、promptsListChanged、resourcesListChanged、resourceSubscriptions),通知带 io.modelcontextprotocol/subscriptionId 标归属;请求级通知(如 notifications/progress)不受影响,仍走各自请求的响应流。
鉴权侧加了一批硬化:OAuth 授权响应建议带 RFC 9207 的 iss,客户端兑换 code 前必须校验;动态客户端注册(DCR)正式让位给客户端元数据文档(CIMD);客户端凭证按授权服务器绑定,不许跨服务器复用。Tasks 从实验性核心拆成了官方扩展(io.modelcontextprotocol/tasks,轮询式 tasks/get)。
兼容旧服务器不难:客户端先照常发一个 modern 请求,撞上 400 时看一眼响应体,是可识别的 modern 错误(比如 -32022)就按新协议继续,否则回退到 initialize 走旧流程。
参考#
- 官方发布博客:https://blog.modelcontextprotocol.io/posts/2026-07-28/
- Key Changes(相对 2025-11-25 的完整变更):https://modelcontextprotocol.io/specification/2026-07-28/changelog
- Streamable HTTP 绑定:https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
- Multi Round-Trip Requests:https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr
- Discovery(server/discover):https://modelcontextprotocol.io/specification/2026-07-28/server/discover
- Versioning and Compatibility:https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning
- Caching:https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching
- Python SDK:https://pypi.org/project/mcp/

