新版 RC 砍掉建连握手和会话 ID,协议层变为无状态。拆解 Streamable HTTP 替代 SSE 的工程动机、代码对比、部署变化及 6 步迁移。
5 月 21 日,大模型上下文协议发布了新版 Release Candidate。核心变更:握手阶段砍掉,会话 ID 砍掉,SSE 长连接被 Multi Round-Trip 替代,协议层变无状态。对跑过生产环境的人来说,sticky 路由和共享状态存储终于可以从架构图里删了。如果你还不太确定这套协议在 AI 工具调用生态中的位置,可以先看从 Function Calling 到 MCP 的架构演进。
从工程部署角度看,这次变更解决的核心问题是:旧版状态模型与现代 HTTP 基础设施不兼容。
2025 年 9 月,Jonathan Hefner 等人在 SEP-1442 里写了:一个普通的无状态负载均衡器无法使用,因为请求可能被路由到不同后端。这句话就是整个重构的起点。
本次 RC 包含 21+ 个提案,聚类为五个支柱:
| 支柱 | 涉及提案 | 实际效果 |
|---|---|---|
| 无状态核心 | 握手移除 + 会话 ID 移除 | 建连握手和会话标识头砍掉,每请求独立 |
| 可运维层 | 强制路由头 + 缓存元数据 + 追踪 | 网关按操作名路由、ttlMs 缓存、W3C 追踪贯通 |
| 扩展框架 | UI 渲染 + 长任务 | 对端渲染界面、异步长任务支持 |
| 鉴权加固 | 6 个提案 | 对齐 OAuth 2.1 和 OIDC,强制 iss 校验 |
| 废弃策略 | 生命周期规范 | 最少 12 个月缓冲期,书面化规则 |
对大多数团队,前两个支柱——无状态核心和可运维层——是立刻需要关注的。
旧版调远端能力,先建连再发请求:
# 旧版:先握手,拿回会话 ID
POST /mcp HTTP/1.1
{"jsonrpc":"2.0","id":1,"method":"initialize",...}
# 后端返回 Mcp-Session-Id: 1868a90c
# 之后每个调用必须带这个 ID——被钉在特定实例上了
POST /mcp HTTP/1.1
Mcp-Session-Id: 1868a90c-3a3f-4f5b
{"jsonrpc":"2.0","id":2,"method":"tools/call",...}
新版同一个调用变成自包含的单次请求:
# 新版:无需事先建连
POST /mcp HTTP/1.1
Mcp-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Authorization: Bearer <token>
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"search",...,
"_meta":{".../clientInfo":
{"name":"my-app","version":"1.0"}}}}
三个关键变化:
_meta,跟每个请求走。想知道对端能力?调 server/discover,幂等、可缓存。旧架构的水平扩展依赖 hash $http_mcp_session_id consistent 做粘性路由——把同一会话标识的所有调用固定打到同一个后端实例。三个痛点:单实例挂掉则上面所有状态全丢;弹性伸缩时旧状态不会自动迁移,需 Redis/DB 做外部共享;网关还得深度包检测才能路由,SSE 流的首请求才含握手信息。
RC 版彻底改变了这个局面。连接标识从传输层消失了,任何调用可以落到任意实例。运维配置从需要 sticky hash 的复杂拓扑退化到最朴素的 round-robin。你甚至可以按操作类型做分流——列表类打到读池、执行类打到算力池。
官方博客在 RC 公告里直接写了:先前需要粘性路由、共享状态存储、网关深度包检测的远端服务,现在可以跑在普通 round-robin 后面,按操作路由头做分发,客户端按 ttlMs 缓存列表响应。
这不是理论优化——部署拓扑从"需要一个有状态中间件层"降级到"标准 HTTP 服务"。
旧版里对端主动向客户端发消息——比如弹个"确定要删 3 个文件吗?"——需维持一条 SSE 长连接,运维最头疼的场景。
新版中,处理方不再持有连接,而是返回一个携带交互指令的结果对象。客户端收集用户输入后重新发起同一个调用,附带应答数据和一段不透明上下文。任何实例都能处理这次重试——所需信息全在 payload 里。
一开始觉得这种"回合制"让交互变慢,实际相反:显式 handle 模式比隐藏的连接状态更强。模型可以跨工具组合这些 handle、对它们做推理、在步骤间传递。藏在传输元数据里的状态,模型根本看不见。
# 模型调 create_basket → 拿 id
# 模型调 add_item(b_id="basket-42", ...)
# 模型调 checkout(b_id="basket-42")
# b_id 是显式参数,模型能推理、传递
这就是 HTTP 世界用了二十年的套路——应用层有状态,协议层无状态。
| 能力 | 机制 | 为什么重要 |
|---|---|---|
| 按操作路由 | 两个新的强制请求头 | 网关按操作类型做路由、限流、审计,不解析 body |
| 响应缓存 | 返回体中的 ttlMs + cacheScope | 列表读取结果带缓存元数据,客户端知道有效时长、是否可跨用户共享 |
| 链路追踪 | traceparent + tracestate 传播 | W3C 标准——从宿主应用发起的 trace 穿过网关、后端,进 OpenTelemetry |
三个东西加一起,这套流量从路由、缓存到追踪,跟普通 HTTP API 没区别。运维团队不用学新工具。
Auth0 的工程师在一篇分析里说得很直白:旧 SSE 模式下,标准浏览器 API 很难在初始握手时传安全 Header,开发者被逼着把 Token 塞进 URL——?token=xyz——等同于把家门钥匙贴在门口。
Streamable HTTP 下,每次请求都是标准 POST,可以带 Authorization: Bearer 头。安全中间件每次都校验,而非只查一次然后"门一直开着"。
还有一个实战建议:用 JWT 做连接标识,别用随机 UUID。
# 别这样
import uuid
sid = str(uuid.uuid4()) # 随机数 = 访客牌,捡到就能用
# 建议这样
import jwt
s = jwt.encode({
"sub": uid,
"sid": sid,
"scp": ["read", "exec"],
"exp": datetime.utcnow() + timedelta(hours=1)
}, secret, algorithm="HS256")
每次请求到了,同时检查 Access Token(授权范围)和连接令牌(身份绑定)——两者里的用户标识对不上,直接拒绝。比"记一个 UUID 然后相信后续都来自同一客户端"严谨得多。
如果你在用 FastMCP(日下载量百万级,覆盖约 70% 的实现[1]),迁移路径相对清晰。关于企业级部署的更多细节,可参考我们之前的FastMCP 2.x 生产级部署指南。
uv pip install --upgrade fastmcp。该团队与 spec 同步。_meta,能力发现用 server/discover。当前是 RC,最终 spec 7 月 28 日发。现在做兼容性测试,不建议直接切生产。官方给了 12 个月废弃缓冲——旧功能不会立刻移除。
问:不维护连接状态了,应用状态放哪?
答:HTTP 老套路——显式 handle。创建资源返回 id,后续调用把 id 传回来。状态从"协议层帮你管"变成"应用层自己管"——模型看得见、能推理。
问:握手砍了,客户端怎么知道对端能力?
答:调 server/discover,幂等、可缓存(带 ttlMs),任意实例都能处理。
问:旧 SSE transport 立刻不能用?
答:不会。废弃策略保证从标记到移除至少 12 个月。但 Streamable HTTP 已是推荐方式,SSE 不再收新功能。
问:stdio(本地子进程)受影响吗?
答:不受影响。子进程通信天然 1:1,无负载均衡问题,握手阶段保留。
问:JWT 做连接标识会增加延迟吗?
答:编解码在微秒级(HS256 约 10-50μs,RS256 约 500-2000μs),对端到端延迟(百毫秒到秒级)可忽略。