MCP2026-07-28:无状态核心拆解
2026/8/2 2:09:02 网站建设 项目流程

「告别握手」系列 · 第 2 篇

Agent 调用 MCP 工具服务,需要先确认两边协议版本和能力对得上。在 MCP 的旧版本里,「确认双方能对话」这一步本身,就要在客户端和服务端之间来回好几轮;之后的每一次查询,还要带上服务端发的一张「通行证」。

7 月 28 日发布的 MCP 新规范,把这一整套来回拆掉了。官方称这是 MCP 自远程协议推出以来最重要的一次修订,核心变化在协议的状态管理上,让我们拆开看清楚。

一次调用,为什么要先握手

先看旧版(2025-11-25)在远程模式下,一次工具调用的完整路径。假设 Agent 要调一个搜索工具,流程是三步:

  1. 客户端发initialize,告诉服务端「我用的协议版本是 2025-11-25,我支持工具调用,我是 my-app」。
  2. 服务端核对版本和能力,签发一个Mcp-Session-Id随响应返回;客户端再发一个notifications/initialized,通知「初始化完成」。
  3. 之后每一个真实请求——tools/listtools/call——都带上这张通行证。

报文长这样。先看握手:

POST /mcp HTTP/1.1 Content-Type: application/json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": { "tools": {} }, "clientInfo": { "name": "my-app", "version": "1.0.0" } } }

服务端回一个 200,响应头部带着Mcp-Session-Id: 7f3a…,body 里是它支持的协议版本和能力清单。然后是实际调用:

POST /mcp HTTP/1.1 Mcp-Session-Id: 7f3a… Content-Type: application/json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "search", "arguments": { "q": "订单" } } }

这套「握手加会话」的模式,是从 Web 借鉴来的。HTTP 本身无状态,Web 应用靠 Cookie 和 Session 在应用层补上状态;MCP 在 2025-03-26 引入 Streamable HTTP 传输之后,也采用了同样的思路:initialize相当于登录,Mcp-Session-Id相当于登录后发的那张会话凭证。再往前追溯,2025-03-26 之前 MCP 用的是 HTTP+SSE 传输,服务端还要为每个客户端维持一条持续连接的 SSE 流。

这套设计放在单机场景没毛病。MCP 最早就是为本地进程设计的——AI IDE拉起一个文件系统服务,利用stdio 管道通信,进程本身就是那个会话。问题在于「连接」管理 被耦合成协议设计的一部分:所有请求默认共享一个隐式的连接上下文,服务端必须记得「这个客户端是谁」。这个假设在本地成立,在分布式环境里很快撑不住。

握手在分布式环境里变成负担

生产环境的特点是:服务不止一台机器,而是几十上百个实例;流量不直连,而是经过网关和负载均衡器;实例随时在扩容、缩容、重启。在这种环境里,「连接绑定实例」的设计会带来一系列问题。举个具体场景:采购服务部署了三台实例,一个 Agent 的搜索请求被网关分到实例 A,会话建在 A 上;下一个请求因为粘性路由又被送回 A。只要 A 稳定,一切正常。可一旦 A 要重启、或者扩容时新实例要分担流量,麻烦就来了。

第一,负载均衡必须做粘性路由。同一个会话的请求,必须被路由到当初签发Mcp-Session-Id的那台实例,否则新实例不认识这张通行证。负载均衡器得按会话 ID 把请求固定调度到同一台实例——这种固定调度就是常说的「会话亲和性」,它本身就是一种状态。更麻烦的是,扩缩容时亲和性还得重新计算:新实例上线了,老会话还钉在旧实例上,负载并不均匀。

第二,会话状态需要共享存储。要支持多实例、避免单点,会话数据必须放进 Redis 这类外部存储。每个请求因此多一次存储查询,还得处理过期、续期、存储故障降级。更糟的是,一旦 Redis 出问题,所有会话一起失效,MCP 服务的可用性绑定在这个外挂的存储上了。

第三,实例宕机,会话一起没。一台实例挂了,它持有的所有会话同时失效,这些会话上的客户端必须重新走一遍握手。对 Agent 来说之前几轮调用积累的上下文也随会话一起丢了,任务可能要从头再来。

第四,网关想路由,得解析请求体。方法名tools/call藏在 JSON-RPC 的 body 里,网关要按方法路由或限流,只能解析 JSON。专业网关每次请求都多一次解析开销,轻量入口如 Nginx 干脆做不了,得靠 Lua 或 NJS 扩展补能力。

四个问题背后是同一个根源:协议把状态放在连接里,而生产环境里连接恰恰是最不稳定的一环——实例会重启、流量会重新分配、网络会抖动。

一句话概括:基础设施在为协议「让路」。而协议本该是为基础设施服务的。

新版:每个请求自带上下文

2026-07-28 的改法很直接:把连接级的握手和会话整个拿掉。具体是两件事——移除initialize/initialized握手(SEP-2575),移除Mcp-Session-Id和协议级 session(SEP-2567)。

替代方案是让每个请求自包含。客户端把三样信息塞进请求的_meta字段:协议版本(io.modelcontextprotocol/protocolVersion)、客户端身份(io.modelcontextprotocol/clientInfo)、客户端能力(io.modelcontextprotocol/clientCapabilities)。这些信息不是只在建立连接时交换一次,而是跟着每一个请求走。

新版将三步调用变成一步:

POST /mcp HTTP/1.1 MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: search Content-Type: application/json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "search", "arguments": { "q": "订单" }, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { "name": "my-app", "version": "1.0.0" }, "io.modelcontextprotocol/clientCapabilities": { "elicitation": {} } } } }

注意几个细节,头部多了三样东西:MCP-Protocol-Version声明协议版本;Mcp-MethodMcp-Name让网关不用解析 body 就知道这是哪个方法、哪个工具。body 里的_meta三个字段各司其职:协议版本告诉服务端「我说的是哪版协议」,客户端身份用于排障和统计,客户端能力告诉服务端「我能接收什么」。

这不止是「把握手参数挪了个位置」。旧版里这些信息只在握手时交换一次,服务端默认后续请求都属于同一个会话;新版里每一个请求都带着完整上下文,服务端不需要任何「之前」的记忆。请求之间不再有隐式的先后关系——它们是一群平等的独立个体,而不是串在同一条线上的珠子。

这个请求落到任何一台实例上,都能被独立处理。实例不需要查「这个客户端之前跟我握过手吗」,不需要查会话表,不需要判断 session 有没有过期。对服务端而言,处理成本反而变简单了。要不要状态、怎么表达状态,是业务自己的事,协议不再掺和。

新旧对照,一张表说清楚:

维度2025-11-25(旧)2026-07-28(新)
建立连接必须initialize握手无需握手,直接发请求
会话标识Mcp-Session-Id绑定实例无协议级会话
版本与能力握手时协商一次每个请求携带于_meta
实例要求粘性路由或共享存储任意实例可处理任意请求
状态表达藏在传输层显式句柄参数(见下文)

版本不匹配怎么办

客户端说「我用 2026-07-28」,假如服务端不匹配,只支持 2025-11-25,就返回一个UnsupportedProtocolVersionError(错误码 -32022),并列出它支持的版本。客户端收到后,挑一个双方都支持的版本,重发请求即可。

这和旧版的区别很微妙但很重要。旧版里,版本协商在连接建立时一次完成,之后所有请求都假设已经谈妥;新版里,协商是每个请求各自的事——这个请求谈不拢,重试就好,不影响其他请求,也不需要任何连接级状态。如果配合server/discover的响应缓存,客户端通常可以提前拿到版本信息,连出错重试都省掉。

这个设计还给版本升级带来了便利。假设一个集群里新旧实例并存的窗口期:旧版协议下,会话在握手时锁定版本,期间不能变;新版下,每个请求独立协商,客户端可以根据服务端返回的 supportedVersions 自动选择——请求打到老实例按老版本应答,打到新实例按新版本应答,互不干扰。新旧实例可以共存一段时间,不用在某个时间点一次性全部切换。

server/discover:能力发现,但不是握手

客户端怎么提前知道服务端支持什么?新规范加了一个server/discover方法:服务端必须实现它,客户端可以(但不是必须)在任何其他请求之前调用。它返回服务端支持的协议版本、能力清单和身份信息:

POST /mcp HTTP/1.1 MCP-Protocol-Version: 2026-07-28 Mcp-Method: server/discover Content-Type: application/json { "jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { "name": "my-app", "version": "1.0.0" }, "io.modelcontextprotocol/clientCapabilities": { "elicitation": {} } } } }

响应大概是这样的:

{ "jsonrpc": "2.0", "id": 1, "result": { "resultType": "complete", "supportedVersions": ["2026-07-28"], "capabilities": { "tools": { "listChanged": true }, "resources": {} }, "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "order-server", "version": "1.0.0" } }, "ttlMs": 3600000, "cacheScope": "public" } }

拆开看这个响应。resultType: "complete"表示这是一次完整应答;supportedVersions列出服务端愿意对话的协议版本;capabilities是它的能力清单,这里声明支持 tools 和 resources;serverInfo是服务端身份,用于排障和兼容性统计;最后的ttlMscacheScope告诉客户端这份信息多久有效、能不能放进共享缓存。对一个常驻的企业网关来说,discover 结果拉一次、缓存起来、在内存里复用即可,不需要每个请求都重新询问。

还要分清两个「发现」:server/discover发现的是协议级能力——支持哪些版本、tools、resources 这些;tools/list发现的才是业务级工具——具体有哪些工具、参数长什么样。前者是「这家店卖什么类型的货」,后者是「货架上具体有哪些」。一次 discover 打底,再按需 list,两次请求各司其职。

它和握手的区别在哪?握手的特征是「必须」——不完成握手,任何请求都做不了,会话状态被协议强绑定。discover 的特征是「可选」——不调用它也能直接干活,只是可能走弯路;调用了,能提前拿到版本和能力信息,少一次出错重试。这就像到一个新城市,不查地图也能走,只是可能走错路;查了地图,路线更稳,但无论查不查,都不需要和城市签任何「协议」。它不建立任何连接级状态,响应里带着ttlMs(多久内可复用)和cacheScope(能不能被共享缓存),客户端可以缓存它,不需要每个请求前都问一遍。它甚至在 STDIO 传输下还能当兼容性探针用——先探一下对方支持什么版本,再决定怎么说话。

协议无状态,业务有状态

这是理解这次改版最容易踩的坑:协议无状态,不等于你的服务不能有状态。

官方给的做法很朴素:需要跨调用状态,服务端就在工具结果里返回一个显式标识符,Agent 下一次调用时把这个标识符作为普通参数传回去。还拿采购 Agent 举例。Agent 搜索供应商、创建采购申请,服务端创建成功后在结果里返回一个request_id;Agent 后续要查审批状态、要下单,都把这个request_id作为参数传回去。状态存在于业务系统里,只是不再由协议偷偷记着。

把流程一步步写出来更直观。

第一轮:Agent 调search_suppliers拿到候选列表,挑中一家,调create_request(supplier, amount);服务端落库,返回request_id: R-2026-0001

第二轮:Agent 想确认审批进度,调get_request_status(request_id: R-2026-0001);审批通过后,再调place_order(request_id: R-2026-0001)。三次调用之间,服务端没有在传输层记任何东西,它认的只是这个显式句柄。

对熟悉 Web 的读者来说,这个模式很眼熟——它和 HTTP API 里用资源 ID 表达资源、用 token 表达身份是同一个思路。MCP 只是把这套被验证了几十年的做法,从 HTTP 层搬到了 Agent 的工具调用层。

这种「显式句柄」的做法,比藏在传输层里的隐式会话更好用,原因有三条。

第一,模型看得见。会话 ID 藏在连接元数据里,模型不知道、也操作不了它;而request_id是模型手里的一个普通值,它可以比较、可以传给别的工具、可以在推理里引用。状态从「协议的隐式负担」变成了「模型的可编排资源」——模型甚至能在一个工具返回的句柄和另一个工具之间做编排,这是隐形会话给不了的。

第二,归属清楚。状态在业务系统里,谁负责数据就归谁管,协议不越俎代庖。审计和排查时,跟着request_id走业务链路,比翻传输层日志直观得多——每一笔操作都能追到对应的请求和句柄,而不是一段模糊的连接历史。

第三,重试干净。请求自包含,重试就是再发一遍,不会因为「会话过期了」「会话不在本实例」这种传输层原因失败。

官方发布说明里有句话值得记住:把状态从传输层的元数据里搬出来、变成模型可见的显式参数,往往比藏在 session 里的隐形状态更有用。这是这次设计的一个正面收益。

顺手精简掉的连接级东西

SEP-2575 在无状态化的同时,还移除了其他一批依赖会话的东西。把它们列出来,能更清楚地看到改版的完整边界。

ping被移除了。旧版里它是协议自带的「你还在吗」——客户端定期发、服务端回,用来判断连接是否存活。现在没有连接这个概念了,健康检查回到最朴素的方式:发一个真实请求,能通就是活的。对基础设施来说这反而更省事,探活探的是真实路径,不是一条专门的虚拟路径。

logging/setLevel没了。日志级别改成每个请求在_meta里带io.modelcontextprotocol/logLevel,按请求控制日志,而不是按会话。这在无状态服务里是顺理成章的:请求散落在不同实例上,日志级别只能跟着请求走,绑定到某台实例的某个会话没有意义。服务端对没带这个字段的请求,也不该再主动发日志通知。

列表接口不再随连接变化。tools/listresources/list这类结果不再依赖会话状态,同一个列表对谁返回都一样。

SSE 流的断点续传和消息重发被移除了。旧版响应流断了可以恢复,代价是服务端要记住消息队列和已发送的进度。新版把这些记忆全部删除,换来传输层的纯粹无状态;代价也很明确:流一断,正在处理的请求就作废,客户端必须用一个新请求 ID 重新发起。听起来更脆弱,但这就是无状态的核心交换——协议不再为「万一断流」兜底。

这些改动看起来琐碎,指向的却是同一个原则:协议里不再保留任何连接级的概念,一切都可以按普通 HTTP 的语义来理解、缓存、路由和运维。

这次改动带来的工作量

这次改动直接改的是协议层,工作量首先落在 MCP 客户端、服务端和 SDK 的实现与维护者身上:凡是假设了initializeMcp-Session-Idinitialized生命周期、或者依赖会话 ID 做状态管理的代码,都要改。原本靠会话识别的业务状态,要改成显式标识符。

不过,改动范围有一条界限:移除握手是「新协议版本」的事,不是「7 月 28 日一切失效」。老版本协议照常能用,新旧实现还可以各自按自己支持的版本说话,SDK 也保留了向后兼容——比如 TypeScript SDK 的 v1.x 还会继续接收至少 6 个月的修复与安全更新,Python 的 v1.x 分支也保留关键修复通道。真正要改的,是那些准备切到 2026-07-28 的实现。

另一个需要接受的取舍是:协议变轻,换来的是部署和运维变简单,代价是传输层不再替你兜底状态——流断了就是断了,会话没了就是没了。业务状态必须自己显式管理,这条责任线从协议移到了实现者手里。

而对最终使用者来说,界面不会有任何变化。简单来说,这次改版把连接和 MCP 协议解耦了:协议层面不再需要关心连接如何建立和维护,实现者只需要关注协议本身。

结语

移除握手和会话,本质上是把连接和 MCP 协议解耦了。从 2026-07-28 开始,每个请求都是独立、自包含、可被任意实例处理的普通 HTTP 请求,协议本身不再需要关注连接如何建立和维护。


本文基于 2026-07-28 正式发布的 MCP 规范撰写。

来源:

  • The 2026-07-28 Specification — 官方发布公告
  • MCP 架构总览(2026-07-28)— 官方文档
  • MCP 变更日志(2026-07-28)— 官方文档
  • SEP-2575:无状态化与移除初始化握手
  • SEP-2567:移除协议级 session 与 Mcp-Session-Id
  • Model Context Protocol 官方仓库

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询