把一个 MCP 服务部署到 K8s 集群上,挂了三个节点。然后发现:客户端全断了。不是某个请求失败,是所有请求都失败了。因为每个客户端的「会话」都卡在某一台机器上,那台机器挂了,会话状态跟着一起消失。
2026 年 7 月 28 日,MCP 规范发布了一个大版本,把这个问题从协议层干掉了。
1.x 的问题
MCP 1.x 要求客户端先和服务器握手(initialize / initialized),拿到 Mcp-Session-Id 后,所有请求带着这个 ID,服务器在内存里维护会话上下文。本地跑 stdio 没问题,部署到云端就崩了:节点挂了会话丢失、负载均衡不认识会话只能开 Sticky Sessions、Serverless 根本跑不了。
2.0 怎么解决的
思路很干脆:默认无状态,需要的时候应用层自己管。SEP-2575 把这叫「Pay as you go」

请求自省
2.0 删掉了 initialize / initialized 和 Mcp-Session-Id。每个请求在 _meta 里自带协议版本、客户端身份和能力声明,落到集群任意节点都能独立处理。需要跨请求状态?让模型自己传状态句柄,状态从传输层搬到应用层。版本协商也是内联的:不支持就返回 UNSUPPORTED_PROTOCOL_VERSION(-32022)附带支持列表,客户端挑一个重发。
HTTP 请求头限制
Mcp-Method 和 Mcp-Name 提到 HTTP 标头,网关不解包 JSON 就能限流路由。MCP-Protocol-Version 必须跟 _meta 一致,否则直接 400。
请求轮询
1.x 靠 SSE 长连接让服务端反向问客户端,网络一抖就断。2.0 用 MRTR(多轮次请求) 替代:服务端需要用户输入时结束当前请求,返回 requestState 凭证;客户端收集输入后重发,附带 inputResponses 和原样 requestState。任意节点反序列化就能恢复上下文,每轮都是独立 HTTP 请求。
代码快速上手
用 MCP SDK V2 重写一下内部的一个 release 发布的 MCP Server,验证两件事:
- 2.0 的协议协商能不能正常工作
- 是不是真的「每个请求一个实例」
场景是我日常遇到的:发布前需要检查服务错误率、未关闭故障和变更窗口,决定能不能上线。工具名就叫 check_pig_release。
服务端
核心是用 createMcpHandler。它是 2.0 的 HTTP 入口,已经内置在 @modelcontextprotocol/server v2 里。接收一个 factory 函数,每个 HTTP 请求进来时调用一次,创建新的 McpServer 实例:
1 | const handler = createMcpHandler( |
注意 { legacy: 'reject' }。这表示只接受 2026-07-28 协议,拒绝 1.x 的初始化握手请求。如果你需要同时兼容两个版本,改成 legacy: 'stateless' 就行,同一个 factory 会服务两边。
部署方面,createMcpHandler 返回一个 Web 标准的 { fetch } 对象,可以直接 export default 到 Cloudflare Workers、Deno、Bun。Node.js 环境下用 toNodeHandler 包一层就能挂到 Express、Fastify 或原生 node:http 上。
客户端
客户端这边也很直接,协议版本锁定 2026-07-28:
1 | const client = new Client( |
我分两次调用了同一个工具。一次是安全场景(低错误率、无故障、变更窗口已批准),一次是风险场景(错误率 1.7%、两个未关闭故障)。
跑出来的结果
安全场景:
1 | { |
风险场景:
1 | { |
两个 instanceId 不同。这说明什么?两次工具调用走了两个不同的 McpServer 实例。factory 被调用了两次,每次创建新实例。没有任何跨请求的会话状态被复用。
总结
我试了 MCP 官方 Java SDK,它目前没有适配 2.0 的无状态特性,跑起来直接 500。langchain4j 的相关集成 SDK 也没跟上。相比之下,TypeScript SDK 是四个 Tier 1 SDK 里最先适配 2026-07-28 规范的,createMcpHandler 一行搞定。