
1. 这次重写到底动了什么从 Session 到 Stateless 的底层逻辑MCP 把自己推翻重写这件事在圈子里其实没引起太大动静但我身边几个真正在生产环境里跑 MCP 服务的朋友反应都挺一致早该这么干了。原因很简单之前那套基于 Session 的设计在单机 demo 里看着优雅一旦上规模、上容器、上多副本问题就全冒出来了。这次重写的核心就是把Session 这个状态载体彻底拿掉转向Stateless的请求模型同时把Sampling这个曾经被寄予厚望的能力废掉换成了MRTRMulti-Round Tool Resolution多轮工具解析的思路。先说清楚 MCP 是什么。MCP 全称 Model Context Protocol是一套让模型和外部工具、数据源之间建立标准通信的协议。你可以把它理解成模型世界的 USB-C 接口——不管对面是数据库、文件系统、还是某个业务系统只要按 MCP 的规范暴露能力模型就能统一调用。这个定位决定了它必须足够轻、足够稳、足够好扩展而旧版的 Session 机制恰恰在这三点上都拖了后腿。旧版 MCP 的工作方式是这样的客户端先和服务器建立一个 Session服务器在内存里维护这个 Session 的上下文包括已注册的工具列表、认证状态、临时缓存等。后续所有请求都挂在这个 Session 上。听起来没问题但实际部署时会遇到几个硬伤。第一水平扩展困难。你有三个 MCP 服务副本客户端第一次连到副本 A 建立了 Session第二次请求被负载均衡打到副本 BB 根本不认识这个 Session直接报错。要解决就得做 Session 共享引入 Redis 之类的集中存储架构复杂度立刻上一个台阶。第二连接恢复成本高。网络抖动导致连接断开Session 就没了客户端得重新握手、重新拉工具列表用户体验很差。第三调试和排查困难。Session 状态散落在服务端内存里出问题时你很难复现当时的上下文。新版直接把这些全砍了。每个请求都是自包含的该带的认证信息、该带的上下文全部在请求里带齐。服务端不再维护任何跨请求的状态来一个请求处理一个处理完就忘。这就是Stateless的核心含义。带来的好处非常直接任意副本都能处理任意请求负载均衡随便打服务重启不影响客户端因为压根没有需要恢复的会话排查问题时一个请求的完整信息就在那一个请求里抓包就能看清全部。那 Sampling 为什么废了Sampling 原本的设计意图是让 MCP 服务端能够反向请求模型做一些采样或生成比如服务端在处理工具调用时需要模型帮忙判断一下该走哪个分支。这个能力听起来很美好但实际用起来问题一大堆。首先是循环依赖风险服务端调模型模型又调服务端很容易绕进去。其次是责任边界模糊到底谁该为这次采样负责、计费算谁的、超时怎么算全是扯皮的事。最后是实现复杂度爆炸每个客户端都要实现一套反向调用的通道兼容性极差。新版用 MRTR 替代思路变成服务端不主动调模型而是把我还需要一轮信息这个诉求作为响应返回给客户端由客户端决定要不要发起下一轮。控制权回到客户端手里链路清晰责任明确。这里有个关键点很多人没注意到Stateless 不等于无状态设计。服务端当然可以有缓存、有连接池、有内部状态但这些状态不能和某个特定客户端的会话绑定。换句话说状态可以是全局共享的、可重建的但不能是会话私有的、不可恢复的。这个区分很重要搞混了就会写出伪 Stateless的代码——表面上没有 Session 对象实际上在内存里偷偷维护了一个 mapkey 是客户端标识那和旧版没本质区别。我实测下来改成 Stateless 之后最直观的变化是部署脚本简单了一大截。以前要配 Session 亲和性、要挂共享存储、要考虑优雅下线时怎么迁移会话现在这些全删了。K8s 里就是一个无状态 Deployment副本数随便调滚动更新毫无压力。这个收益在生产环境里是实打实的。2. 新旧协议对照你学的教程为什么过期了现在网上能搜到的 MCP 教程绝大多数还停留在旧版模型上。你去照着搭会发现两个典型症状要么代码里到处是session_id的传递要么在纠结 Sampling 怎么配置。这些教程不是错是过期了。我把新旧两版的关键差异整理成一张表你对照着看就明白自己手上的资料是哪一代的。维度旧版Session 模型新版Stateless 模型连接建立先握手建 Session后续复用无握手每请求独立状态维护服务端内存维护会话上下文服务端不维护会话状态工具列表获取建 Session 时一次性拉取每请求可携带或客户端缓存反向调用Sampling服务端主动调模型MRTR服务端返回诉求由客户端决策水平扩展需 Session 共享或亲和性任意副本可处理断线恢复需重新握手无需恢复直接重发请求认证方式常绑定在 Session 上每请求携带凭证调试难度高状态分散低请求自包含看这张表你就明白了为什么很多老教程里的最佳实践现在变成了反模式。比如旧版推荐在 Session 建立时缓存工具列表减少后续请求体积。新版里这个缓存要么放客户端要么每请求带服务端不背这个锅。再比如旧版处理认证很多实现是把 token 和 Session 绑定Session 有效期内不用重复校验。新版必须每请求校验虽然多了点开销但换来的是无状态这笔账划算。MRTR 到底怎么工作这里展开说一下因为这是新版里最容易被误解的部分。假设客户端发起一个工具调用请求服务端处理到一半发现信息不够比如需要用户确认某个参数或者需要模型再判断一次。旧版的做法是服务端通过 Sampling 通道反向请求模型拿到结果继续处理整个过程对客户端是黑盒。新版的做法是服务端直接返回一个特殊响应大意是我需要你再提供 X 信息或者我需要你先完成 Y 步骤。客户端收到后决定是补充信息重发还是终止流程。这个多轮就体现在这里——一次工具解析可能需要客户端和服务端来回几轮但每一轮都是独立的、自包含的请求。这个设计的好处在于可观测性。旧版 Sampling 的反向调用藏在服务端内部你在客户端侧只能看到最终结果中间发生了什么完全不知道。新版每一轮都是显式的请求响应你在客户端就能看到完整的交互链路出问题一眼就能定位是哪一轮、哪个环节。对于生产环境排查问题这个价值太大了。还有个细节新版对工具列表的动态性支持更好。旧版工具列表在 Session 建立时确定中途服务端加了新工具已建立的 Session 看不到。新版每请求都可以重新解析工具列表服务端随时上下线工具客户端下一轮就能感知。这对于工具频繁变动的场景比如插件化系统是刚需。我踩过的一个坑是刚开始迁移时习惯性地在客户端维护了一个当前会话的概念把工具列表缓存在里面结果服务端工具更新了客户端还在用旧的调用报错。后来改成每次请求前根据场景决定是否刷新工具列表或者给缓存加一个较短的 TTL问题才解决。这个经验说明Stateless 要求客户端也调整心态不能再依赖服务端帮你维护上下文。3. 迁移实操把旧代码改成 Stateless 的完整步骤光讲概念没用直接上迁移步骤。我拿一个典型的旧版 MCP 服务端实现来改你可以对照自己的代码同步操作。假设你有一个基于 Python 的 MCP 服务旧版大概长这样启动时初始化一个 SessionManager每个客户端连接进来分配一个 session_id后续请求都带着这个 id 来查上下文。3.1 第一步干掉 SessionManager旧代码里通常有一个全局的 SessionManager负责创建、查询、销毁会话。迁移的第一步就是把这个东西整个删掉。别想着保留它做兼容留着就是隐患早晚有人会去用。删掉之后所有依赖get_session(session_id)的地方都会报错这些报错点就是你需要改的地方正好当检查清单用。删掉之后原来存在 Session 里的东西要重新安排去处。我列一下常见的几类认证信息改成每请求从 header 或请求体中解析解析完即用即弃。工具列表改成每请求动态生成或者由客户端携带。临时缓存如果确实是跨请求需要的改成全局缓存加合理的 key注意 key 不能是 session_id。用户偏好这类个性化数据要么放客户端要么放独立的配置服务不要塞进 MCP 服务端。3.2 第二步请求体补全自包含信息旧版请求可能只带一个 session_id 加少量参数因为大部分上下文服务端都有。新版要求每个请求自包含所以请求体要补全。以工具调用为例新版请求至少应该包含认证凭证、要调用的工具名、工具参数、以及可选的上下文提示。下面是一个请求体的示例结构{ auth: { token: xxxxx, timestamp: 1730000000 }, tool: query_database, params: { sql: select * from users limit 10 }, context: { trace_id: abc-123, client_version: 2.0.0 } }注意trace_id这个字段Stateless 之后排查问题全靠它。每个请求带一个唯一 id服务端日志里打出来出问题时拿这个 id 去日志系统一搜整条链路清清楚楚。这个习惯一定要养成比 Session 时代的调试体验好太多。3.3 第三步把 Sampling 调用改成 MRTR 响应这是改动量最大的一块。旧代码里如果有调用 Sampling 的地方比如# 旧版服务端主动调模型 result sampling_client.sample(prompt这个参数该填什么)新版要改成返回一个 MRTR 响应告诉客户端我需要更多信息# 新版返回 MRTR 响应由客户端决策 return { status: need_more_info, reason: missing_parameter, required: [target_table], hint: 请提供目标表名 }客户端收到这个响应后可以选择补充参数重发请求也可以直接终止。控制权在客户端服务端只负责表达诉求。这个改动看起来简单但需要重新梳理整个交互流程因为原来藏在服务端内部的往返现在都暴露到客户端和服务端之间了。3.4 第四步认证改成无状态校验旧版认证往往和 Session 绑定登录一次Session 有效期内都算已认证。新版每请求都要校验。这里有个性能考量如果每请求都去查数据库或调认证服务开销不小。常见的优化是用自包含的令牌比如 JWT服务端本地就能验签不用外部依赖。令牌里带上必要的声明比如用户 id、权限范围、过期时间服务端验签通过就直接用。注意无状态认证不等于不校验。我见过有人为了省事在 Stateless 改造后直接把认证逻辑删了理由是反正没 Session 了。这是严重的安全漏洞千万别这么干。3.5 第五步压测验证无状态特性改完之后必须压测而且要专门验证无状态特性。具体做法是起多个服务副本用负载均衡随机分发请求看是否有请求因为找不到会话而失败。如果全部成功说明无状态改造到位。再做一个测试压测过程中随机重启某个副本看客户端是否有请求失败。理论上不应该有失败因为请求打到其他副本照样能处理。我当时的压测脚本大概是这样用 Python 的并发库模拟多客户端import concurrent.futures import requests def call_mcp(i): resp requests.post(http://mcp-service/execute, json{ auth: {token: test-token}, tool: echo, params: {msg: freq-{i}} }) return resp.status_code with concurrent.futures.ThreadPoolExecutor(max_workers50) as executor: results list(executor.map(call_mcp, range(1000))) print(f成功: {results.count(200)}, 失败: {len(results) - results.count(200)})跑下来 1000 个请求全成功且压测中途重启副本也没有失败才算过关。4. 常见问题与排查技巧实录迁移过程中遇到的问题我整理成了一张速查表基本都是实际踩过的你对照着排查能省不少时间。问题现象可能原因排查方法解决方案请求随机失败报会话不存在还有残留的 Session 依赖全局搜索 session_id 关键字彻底清除会话相关代码工具列表不更新客户端缓存了旧列表检查客户端缓存逻辑加 TTL 或每请求刷新认证偶发失败令牌过期或时钟偏移检查令牌 exp 和服务器时间同步时钟合理设置有效期MRTR 循环不终止客户端未正确处理 need_more_info打印每轮响应加最大轮次限制压测时性能下降每请求重复初始化重资源检查是否有连接池用全局连接池注意线程安全日志无法关联缺少 trace_id检查请求体强制每请求带 trace_id重点说几个容易忽略的。MRTR 循环不终止这个问题很隐蔽因为客户端如果没正确处理need_more_info响应可能会一直重发同样的请求服务端一直返回同样的诉求死循环。解决办法是在客户端加一个最大轮次限制比如最多 5 轮超过就报错终止。这个限制值根据业务定一般 3 到 5 轮够用了。性能下降也是常见问题。Stateless 之后每请求都要做认证、解析工具列表如果这些操作里有重资源初始化比如每次新建数据库连接性能会明显下降。解决办法是把这些重资源做成全局的、线程安全的池子请求来了从池里取用完还回去。注意池子本身不能和会话绑定否则又回到老路了。还有个坑是工具列表的动态解析开销。如果工具列表很大每请求都重新生成一遍CPU 开销不小。优化思路是加一层缓存但缓存的 key 不能是会话可以是工具版本号或者租户 id这类全局维度。工具没变就复用缓存变了就重建。这样既保证动态性又控制开销。提示迁移期间建议保留旧版接口一段时间做灰度新老并行确认新版稳定后再下线旧版。直接一刀切风险太大。最后分享一个排查技巧Stateless 之后请求就是最好的调试单元。遇到问题先把出问题的那个请求完整抓下来包括 header、body、trace_id然后拿 trace_id 去日志系统搜整条链路一目了然。这个体验比 Session 时代强太多Session 时代你还要先想办法复现当时的会话状态往往复现不出来。所以迁移完之后一定要把 trace_id 机制建起来这是 Stateless 架构下最重要的可观测性基础设施。我个人在实际操作中的体会是这次重写表面上是删功能实际上是把复杂度从服务端转移到了协议层和客户端。服务端变简单了但客户端要承担更多决策责任协议要表达更丰富的信息。这个转移是值得的因为服务端通常是最难扩展、最难运维的部分把它做简单整体收益远大于客户端的额外工作量。至于那些还停在旧版教程上的资料建议直接跳过从新版协议文档重新学起别在过时的东西上浪费时间。