1. 这次更新到底改了什么MCP 协议在 2026 年做了一次伤筋动骨的调整核心就一个词Stateless。如果你之前接过 MCP 的 Server 或者写过 Client第一反应大概是——那套initialize握手、session维持、notifications/initialized的流程是不是要全推倒重来答案是握手还在但服务端不再需要为每个会话在内存里挂一份状态了。我先把这个变化翻译成人话。以前的 MCP 更像一通电话客户端拨号initialize双方确认协议版本和能力capabilities然后服务端记住这个连接是谁、它订阅了什么、它刚才问过什么后续所有请求都挂在这条连接上。现在改成什么样了更像寄快递每个请求自带完整的寄件人和订单信息服务端处理完就忘下一个请求来了重新读一遍信封。服务端从记住你变成每次重新认识你。这个改动影响面比想象中大。它直接决定了你的 MCP Server 能不能水平扩容、能不能跑在无状态的边缘函数里、能不能被多个客户端实例共享。以前一个 Server 进程只能服务有限数量的长连接连接一多内存就顶不住现在理论上你可以把请求打到任意一个实例上前面挂个负载均衡就行。对于做工具集成、做 Agent 编排的人来说这意味着部署模型从养一台常驻服务变成了按请求计费。适合谁来读这篇三类人。第一类是自己写 MCP Server 的开发者你需要知道哪些代码要改、哪些设计模式要换。第二类是搭 Agent 平台的工程师你要重新评估 MCP 在你架构里的位置。第三类是刚接触 MCP、还没被旧架构污染的新手恭喜你你学到的直接就是新范式少走一段弯路。下面我会从设计动机、协议细节、实操改造、踩坑排查四个层面拆开讲尽量让没接触过 MCP 的人也能跟上。2. 为什么非要改成无状态2.1 旧架构的三个死结要理解这次更新得先知道旧的有状态架构到底卡在哪。我总结下来是三个死结每一个都在真实生产里咬过人。第一个死结是扩容难。有状态意味着会话粘性。客户端 A 的 initialize 打到了实例 1那它后续所有请求都必须回到实例 1否则实例 2 不认识这个 session。你要扩容就得在负载均衡层做会话保持而会话保持本身就是个脆弱的东西——实例 1 挂了客户端 A 的整个会话就断了得重新握手。我见过有团队为了绕开这个把 MCP Server 做成单实例加队列结果吞吐上不去延迟还高。第二个死结是资源占用。每个活跃会话都要在内存里维护一份上下文协议版本、客户端能力、订阅列表、可能的临时缓存。一个会话占几 KB 到几十 KB 不等听起来不多但 MCP 的场景往往是一个 Agent 拉起几十个 Server 连接连接数一上来内存曲线就很难看。更麻烦的是这些状态的生命周期管理——客户端异常断开没发 close服务端得靠心跳超时来回收回收不及时就是内存泄漏。第三个死结是部署形态受限。有状态服务天然不适合 Serverless。你想把 MCP Server 部署成边缘函数、按请求冷启动旧架构根本做不到因为函数实例随时可能被回收会话状态无处安放。这就把 MCP 锁死在了常驻进程这一种形态上运维成本下不来。2.2 无状态换来了什么改成 Stateless 之后上面三个死结基本都解开了。请求自带上下文任何实例都能独立处理扩容就是加实例不需要会话保持。内存占用从随连接数增长变成随并发请求数增长而并发请求是瞬时的处理完就释放。部署形态也打开了你可以把 MCP Server 塞进任何能跑 HTTP 处理函数的地方。代价是什么每次请求都要多传一些元数据网络开销略微增加。另外那些真正需要跨请求记忆的功能——比如服务端主动推送通知、维护长周期的订阅——需要换一种实现方式。协议给出的方案是把这类状态显式地交给客户端管理或者引入一个独立的、可选的状态存储层。换句话说状态没有消失只是从服务端隐式持有变成了显式传递或外置。这里有个认知误区要澄清Stateless 不等于完全没有状态而是服务端不在请求之间保留状态。状态要么跟着请求走要么存在客户端要么放在一个所有实例都能访问的外部存储里。理解这一点后面的改造思路就顺了。2.3 和行业大趋势的呼应把视角拉高一点MCP 这次转向其实是在跟整个后端架构的演进方向对齐。过去几年从容器编排到边缘计算从函数即服务到各种无状态网关主流思路都是让计算单元变得可替换、可丢弃。有状态的东西被集中到少数几个专门的组件里数据库、缓存、消息队列其余部分尽量做成无状态。MCP 作为连接模型和工具的协议层长期背着会话状态其实是个历史包袱。这次更新相当于把这个包袱卸下来让协议层回归纯消息传递的本分。对生态来说这意味着 MCP Server 的实现门槛降低了——你不需要再操心会话管理、心跳保活、状态回收这些脏活专注把工具逻辑写好就行。3. 协议层面的关键变化3.1 请求信封里多了什么无状态化最直接的体现就是每个请求的信封变厚了。以前 initialize 之后后续请求只需要带一个 session id 就能让服务端认出你。现在不行了每个请求都得自带足够的上下文让任何一个没见过的实例都能独立完成处理。具体来说请求里需要显式携带的信息包括协议版本、客户端标识、本次请求需要的能力声明、以及可能的认证凭据。这些以前是握手时协商一次、服务端记住的现在变成每次都要带。听起来冗余但正是这种冗余换来了实例的可替换性。我实测下来一个典型的工具调用请求信封部分大概增加了 200 到 500 字节不等取决于你带多少元数据。对于本地进程间通信这点开销可以忽略对于跨网络的远程调用如果 QPS 很高累积起来还是值得关注的。优化思路后面会讲。3.2 握手流程的简化旧协议里initialize 是个有副作用的操作——它会在服务端创建会话。新协议里initialize 更像是一次能力探测客户端问你支持什么服务端答我支持这些然后双方各自记住后续请求靠信封里的声明来对齐。服务端不因为这次 initialize 而保留任何东西。这个变化带来的一个实际好处是握手可以省略或延迟。如果你的客户端已经知道服务端支持哪些能力比如配置里写死了完全可以跳过 initialize 直接发请求信封里带上能力声明即可。这在冷启动场景下能省一个往返。当然稳妥起见第一次连接还是建议走一遍探测把能力缓存到客户端本地。3.3 通知机制的重新设计有状态架构下服务端可以主动向客户端推送通知因为它持有连接。无状态之后服务端没有连接这个概念了主动推送就失去了通道。协议对此的处理是把推送改成拉取或者通过一个独立的、有状态的通道来做。具体有两种模式。一种是客户端轮询定期问服务端有没有新东西。这种方式简单但实时性差而且空轮询浪费资源。另一种是客户端在需要订阅时主动建立一个专门的通知通道这个通道可以是有状态的因为它是个独立组件不影响主请求路径的无状态性。我倾向于后者把有状态的部分隔离到一个专门的、数量可控的组件里主路径保持无状态。实操建议如果你的工具本身不需要服务端主动推送大多数工具调用都属于这种那这次改动对你几乎无感。只有做订阅、事件流、长任务的场景才需要认真处理通知机制。4. 服务端改造实操4.1 从会话对象到请求上下文改造的第一步是把代码里所有依赖会话对象的地方找出来。旧代码里通常有个Session或Connection类持有客户端信息、订阅列表、临时状态。新架构下这个类要么删掉要么改造成一个每次请求临时构造、用完即弃的上下文对象。我拿一个典型的工具服务举例。旧代码大概长这样连接建立时创建 session把客户端能力存进 session工具处理函数从 session 里读能力来决定返回格式。改造后能力信息从请求信封里解析出来直接作为参数传给处理函数处理完就丢。函数签名从handle(request)变成handle(request, context)其中 context 是从信封现解析的。这个改动看似机械但有个坑别把临时上下文缓存到全局变量里。我见过有人为了优化性能把解析出来的上下文塞进一个模块级的 Mapkey 用客户端 id。这就等于把无状态又改回了有状态而且比原来更糟——因为现在没有生命周期管理Map 会无限增长。记住无状态的纪律是请求进来构造请求出去销毁中间不落地。4.2 认证与凭据的传递有状态架构下认证通常发生在握手阶段之后靠 session 关联身份。无状态之后每个请求都得自带凭据。这意味着你的认证逻辑要从握手时验一次变成每个请求都验。性能上如果每个请求都去查数据库验 token压力会很大。常规做法是引入无状态的令牌机制比如签名令牌服务端本地就能验证不需要查库。令牌里可以编码客户端身份、权限范围、有效期服务端验签通过就信任。这样认证也变成了无状态的纯计算。注意令牌的有效期要设得合理。太短客户端频繁刷新增加往返太长泄露风险大。我一般设 15 分钟到 1 小时配合刷新机制。另外令牌里别放敏感信息它是可被客户端读取的。4.3 幂等性的处理无状态带来一个副作用重试变得更容易发生。因为请求可能被负载均衡打到不同实例网络抖动导致的重试、客户端超时后的重发都可能让同一个操作执行多次。如果你的工具是有副作用的比如写文件、发消息、扣款必须处理幂等。协议层面客户端可以在信封里带一个幂等键服务端用这个键去重。实现上可以用一个带过期时间的键值存储记录最近处理过的幂等键。同一个键第二次来直接返回上次的结果不重复执行。这个存储是有状态的但它是独立的、可共享的组件不影响主路径的无状态性。我踩过的坑是幂等键的生成要保证全局唯一且稳定。用随机数不行因为重试时客户端得生成同一个键。通常用客户端 id 请求序号或者对请求内容做哈希。后者更稳但要注意请求里如果有时间戳之类的易变字段哈希会不稳定。5. 客户端适配要点5.1 能力声明的管理客户端这边最大的变化是以前握手一次、服务端记住你的能力现在你得每次请求都带上能力声明。为了不每次都重新计算客户端应该在本地缓存一份能力声明连接建立时探测一次之后复用。这里有个细节能力声明可能随场景变化。比如某个工具调用需要额外的权限你得在信封里临时加上这个权限声明。所以缓存的能力声明应该是基础能力 本次请求的增量能力而不是一份写死的静态数据。我一般把基础能力放在客户端配置里增量能力在构造请求时动态拼接。5.2 重试与超时策略无状态架构下重试变得安全了前提是服务端做了幂等所以客户端可以更激进地重试。但要注意重试的前提是请求幂等。对于非幂等的操作重试前要确认服务端是否支持幂等键不支持的话宁可报错让用户决定。超时设置也要重新考虑。旧架构下会话断了就是断了重连成本高。新架构下单个请求超时只影响这个请求重发即可。所以超时可以设得短一些快速失败快速重试整体体验反而更好。我一般把单请求超时设在 5 到 10 秒配合指数退避重试 2 到 3 次。5.3 本地状态的位置无状态化之后很多原本服务端持有的状态转移到了客户端。比如订阅列表、上次请求的游标、临时缓存。这些状态放哪我的建议是能不放就不放必须放就放内存且设上限。订阅列表这种如果客户端进程重启就丢那重启后重新订阅即可没必要持久化。游标这种如果丢了会导致重复处理那就得持久化但要考虑并发写入的问题。临时缓存要设大小上限和过期时间否则又是一个内存泄漏点。总之客户端接管状态后你得自己承担起状态管理的责任别以为无状态就是把麻烦推给别人。6. 常见问题与排查6.1 请求被路由到不同实例后行为不一致这是无状态化后最常见的问题。表现是同一个客户端有时请求成功有时失败失败率大概和实例数量成正比。原因通常是某个实例上残留了本该无状态的状态或者某个实例的配置和其他实例不一致。排查思路先确认所有实例的代码版本和配置完全一致。然后检查代码里有没有模块级的可变状态全局变量、单例里的可变字段。再检查有没有依赖本地文件或本地缓存的地方。最后用同一个请求反复打看是否稳定复现。如果只在特定实例上失败基本就是那个实例有本地状态。6.2 认证令牌验证失败率升高无状态化后每个请求都验令牌如果令牌验证依赖某个外部服务那个服务的压力会陡增可能出现超时导致验证失败。解决办法是把令牌验证做成纯本地计算签名验证或者给验证结果加一层短时效的本地缓存。还有一种情况是时钟偏移。签名令牌通常带有效期如果服务端实例之间的时钟不同步有的实例认为令牌过期了有的认为还有效。部署时确保所有实例走同一个时间源偏移控制在秒级以内。6.3 幂等键冲突导致操作被误去重幂等键设计不当会导致两个不同的操作被当成同一个第二个被错误地去重掉。典型原因是幂等键生成规则太粗比如只用客户端 id那同一个客户端的所有请求都成了同一个键。正确做法是把请求的关键参数纳入哈希确保不同操作有不同的键。排查时可以打印出每个请求的幂等键观察是否有不该相同的键相同了。如果发现冲突检查哈希输入里是不是漏了某个区分参数。问题现象可能原因排查方向解决思路请求随机失败实例间状态不一致对比各实例代码与配置消除模块级可变状态认证失败率升高验证服务压力大或时钟偏移看验证服务负载、查时钟同步本地验签、统一时间源操作被误去重幂等键生成过粗打印幂等键对比哈希纳入关键参数内存持续增长临时上下文被缓存检查全局 Map、缓存请求结束即销毁通知收不到推送通道未重建检查订阅逻辑改用拉取或独立通道6.4 性能不升反降理论上无状态化应该提升吞吐但有人改完发现性能反而降了。常见原因是信封变大导致网络传输增加或者每个请求都重新解析大量元数据。优化方向精简信封只带必要字段把元数据解析结果做短时缓存注意是纯函数式的缓存key 是信封内容不是客户端 id开启连接复用减少握手开销。我实测过一个工具服务改造后单请求延迟从 8ms 涨到 11ms主要是信封解析的开销。后来把能力声明的解析结果按信封哈希缓存延迟回到 9ms 左右。这点开销换来的是水平扩容能力我认为是划算的。7. 我踩过的几个坑第一个坑是把无状态理解成不用管状态。实际上状态只是转移了位置该管的还得管。我一开始把订阅列表直接丢了结果客户端收不到任何通知排查了半天才反应过来——订阅这个动作本身需要被记住只是记住的地方从服务端换到了客户端或独立存储。第二个坑是幂等键用了随机数。当时想的是每次请求生成一个唯一键结果重试时客户端生成了新的随机数服务端认为是新请求重复执行了。后来改成对请求内容做哈希重试时哈希不变才正确去重。第三个坑是忽略了信封大小对移动端的影响。桌面端网络好多几百字节无所谓。移动端在弱网下信封膨胀会明显拖慢请求。后来针对移动端做了信封精简把一些不常用的能力声明改成按需携带。第四个坑是以为所有实例都无状态就万事大吉。实际上负载均衡层、认证服务、幂等存储这些周边组件如果还有状态照样会出问题。无状态是个系统工程主路径无状态只是第一步周边也得跟上。8. 后续可以怎么扩展如果你已经把主路径改成无状态了下一步可以考虑把 MCP Server 部署到更轻量的形态上比如按请求计费的函数计算进一步压低空闲成本。再进一步可以做多区域部署请求就近处理因为无状态意味着任何区域的实例都能独立完成工作不需要跨区域同步会话。另一个方向是把幂等存储和通知通道做成可插拔的组件。不同的部署环境对这两块的要求不一样单机部署可能用本地文件就行分布式部署需要共享存储。把它们抽象成接口按环境注入实现能让你的 Server 适配更多场景。我个人在实际操作中的体会是无状态化最大的价值不是性能而是心智负担的降低。以前写 MCP Server脑子里要一直挂着这个状态放哪、什么时候清、并发怎么办现在这些顾虑大部分消失了代码写起来清爽很多。代价是客户端那边多担了点责任但客户端本来就是状态的天然归属地这个转移是合理的。