MCP initialize 被移除、Mcp-Session-Id 被删除后怎么办?2026-07-28 Server 迁移指南
本文基于 MCP 2026-07-28 锁定 RC、官方草案、Python mcp==2.0.0b2 和本地真实 HTTP 轮询 Fixture。2026-07-26 重跑线级实验 6/6 通过,并完成 SDK 导入烟雾测试。实验不代表最终规范一致性、生产 Kubernetes 表现、商业客户端兼容性或性能提升。
MCP initialize 被移除、Mcp-Session-Id 被删除后怎么办?2026-07-28 Server 迁移指南
MCP initialize 被移除后怎么办?Mcp-Session-Id 删除后,状态又应该放到哪里?答案先说清楚:MCP 2026-07-28 删除的是协议层 Session,不是业务状态。 新版远程 Server 需要让每个请求自行携带协议版本、Client 信息和路由元数据;购物篮、审批、浏览器会话和长任务则通过 basket_id、approval_id、browser_id、task_id 等显式 Handle,关联数据库或任务系统中的持久化记录。仅仅把 SDK 升到 v2,并不能自动完成这次迁移。
为什么要改?同一个 /mcp 地址,第一条请求返回 200,第二条请求却可能返回 400。不是网络抖动,也不是 Tool Schema 写错了:第一条 initialize 落到 Replica A,A 把 Session 放进自己的内存;第二条 tools/call 被负载均衡到 Replica B,B 从未见过这个 Session,于是返回 Unknown MCP session。
这正是旧版远程 MCP Server 扩成多副本后最容易暴露的问题。单机开发时,Client、HTTP 连接和 Server 进程几乎总能保持在一起,实例内状态看起来没有代价。到了 Kubernetes、Serverless 或普通轮询负载均衡环境,URL 没变,背后的进程却可能每次都不同。MCP 2026-07-28 锁定 RC 因此移除 initialize / initialized 和协议级 Mcp-Session-Id,把协议信息改为随请求携带,目标是解除协议请求对某个进程记忆的依赖。
为了看清这条边界,本文使用两个旧协议副本、两个新版 Fixture、一个真实 localhost 轮询代理和一个外部 SQLite 状态库做了六项测试。旧协议稳定复现了 200/400;新版请求在 draft-a、draft-b 之间交替仍能工作。实验代码和结果都保存在仓库里,不调用模型,也不依赖 API Key。
测试刻意保持很小:没有引入 Kubernetes、Redis 或商业 Client,避免同时改变太多变量。先用两个独立进程证明 Session 与副本绑定,再用相同代理验证自包含请求,最后只把购物篮状态移到外部存储。每一步都只回答一个问题,方便正式版发布后原样重跑,也方便读者检查结论有没有越过实验本身,避免把局部结果误写成通用结论或线上真实的生产保证。
文章在 2026 年 7 月 26 日完成最后一次发布前验证:线级实验 6/6 通过,Python mcp==2.0.0b2 导入烟雾测试确认新模块布局和旧 mcp.server.fastmcp 路径不可用。到了 2026 年 7 月 28 日,官方 GitHub Releases 仍将该修订标记为 RC / pre-release,最终发布里程碑也尚未完全关闭。因此本文以“锁定 RC 实验与迁移预案”公开发布,把官方事实、本地实验和正式版待验证内容分开记录,不把预发布结论写成正式规范实测。MCP 官方 Releases

为什么 MCP Server 一扩成多副本就出现 Unknown MCP session?
因为 Mcp-Session-Id 只指向某个副本内存中的 Session,而负载均衡可能把下一条请求发送到另一个副本。B 无法读取 A 创建的 Session,就会返回 Unknown MCP session。
旧版 Streamable HTTP 的典型流程并不复杂:Client 先发送 initialize,Server 返回能力和服务器信息,同时可以分配 Mcp-Session-Id;后续请求继续携带这个 ID。若远程端点和 Transport 本身还没有稳定,可先核对 MCP Streamable HTTP 从本地 stdio 到远程部署的完整流程。
只要请求一直回到同一个进程,流程没有问题:
POST /mcp initialize
-> Replica A
<- 200
<- Mcp-Session-Id: session_a_123
POST /mcp tools/call
Mcp-Session-Id: session_a_123
-> Replica A
<- 200
多副本把隐藏前提暴露了。轮询代理把第二条请求送到 B 时,B 的本地 sessions Map 中没有 session_a_123:
POST /mcp tools/call
Mcp-Session-Id: session_a_123
-> Replica B
<- 400 Unknown MCP session
这类故障已经出现在真实部署中。2026 年 6 月,一位开发者在 Stack Overflow 描述了 Spring AI MCP MVC 服务部署到 Kubernetes 多副本后的相同现象:initialize 和后续调用落到不同 Pod,团队只能在 Sticky Session、共享 Session Store 和架构重构之间取舍。相关问题
Sticky Session 能止血,但它只是让请求“尽量回到 A”。A 重启、Pod 被摘除或连接迁移后,内存状态仍然不存在。把整份 Session 放进 Redis 可以继续运行,却容易把 Client 能力、身份、业务对象和恢复信息混成一个大 Blob。问题从“状态丢失”变成“状态没人说得清”。
更麻烦的是,上层经常把这类故障误判成模型不稳定。Gateway 看到 400,Client 重新初始化;Agent Runtime 看到 Tool 失败,再规划一次;有副作用的 Tool 可能因此执行两遍。日志里出现多个 Session、多个 Attempt,根因却只是请求换了进程。
所以迁移前最值得做的不是先升级包,而是主动切换副本:初始化后强制下一次调用到另一实例,再重启 Server、断开返回连接、重复提交同一请求。只跑单实例成功路径,永远看不到 Session 到底替系统承担了多少隐藏职责。
排查时还要把 Session 里的内容拆开看。有些数据只是协议信息,例如 Client 名称、版本和能力;有些是身份信息,例如用户、租户和授权范围;还有些已经是业务事实,例如待审批金额、上传到一半的文件、已经创建但尚未返回的工单。三类数据混在一个 Session 中时,团队很容易做出错误修复:为了保住一个可重新发现的工具列表,把整份包含业务秘密的 Session 复制到 Redis;或者为了实现滚动发布,把一个本来应该有数据库主键的审批草稿继续绑定到 Pod。
一个简单的识别方法是问:这个进程现在被杀掉,数据能否从别处重新得到?Client 能力可以由新请求再次提供,Tool 列表可以重新发现,这些属于可重建信息;付款结果、审批决定和任务是否已经执行则无法靠猜,它们要落到明确的权威存储。迁移真正困难的地方并不是删 Header,而是把原来含糊的 Session 内容重新分类,并为每一类数据找到对应的生命周期。
这也解释了为什么共享 Session Store 更适合作为过渡。它解决“B 查不到 A 的记录”,却没有回答“这条记录究竟是什么”。如果 Session 中保存的只是旧协议兼容上下文,可以设置短 TTL 并随旧协议一起退役;如果里面已经有订单和任务,就先迁成领域对象。否则新协议上线后,业务仍然依赖一个没人敢删除的旧 Session 表。

如何复现 MCP 多副本 Session 失效?
实验中的 legacy-a 和 legacy-b 是两个独立 HTTP Server,各自维护自己的 sessions: set[str]。代理严格轮询:第一个请求发给 A,第二个请求发给 B。
先绕过代理,连续访问 legacy-a:
initialize_status = 200
tool_call_status = 200
session_id = session_legacy-a_0748e8f4
served_by = legacy-a
这说明 Fixture 没有故意让旧流程失败。随后切回轮询代理:
initialize_status = 200
initialize_replica = legacy-a
call_status = 400
call_replica = legacy-b
error.code = -32001
error.message = Unknown MCP session
因果关系很直接:A 创建,B 查不到,于是拒绝。统一域名、健康检查和负载均衡器可用率都不会提前告诉你这个问题。
| 场景 | 请求落点 | 结果 | 实际依赖 |
|---|---|---|---|
| 单实例 | initialize → A,call → A | 200 / 200 | A 的内存持续存在 |
| 轮询多副本 | initialize → A,call → B | 200 / 400 | B 不认识 A 的 Session |
| Sticky Session | 请求尽量回到 A | 通常成功 | A 不能丢失 |
| 共享 Session Store | A/B 读取同一存储 | 可兼容 | 仍需维护协议 Session |
| 自包含请求 | 任一请求可到 A/B | 独立成功 | 业务状态另行保存 |
这次实验要证明的并不是“旧规范有 Bug”,而是确认迁移目标:任一副本只看当前请求,就能完成协议判断。 做到这一点后,扩容就不再依赖粘性路由。
实验还记录了代理的 Route History,而不只看最终状态码。这样可以排除一种常见误判:请求表面经过负载均衡,实际上因为连接复用或代理策略始终回到同一个上游。这里能看到 initialize_replica = legacy-a、call_replica = legacy-b,失败与副本切换发生在同一条证据链上。
线上排查也保留类似信息:协议版本、Session 或 Handle 的不可逆哈希、目标副本、Tool 名称和 Attempt。缺少目标副本时,Unknown MCP session 只说明“某处没找到”;有了路由记录,就能判断是 Session 已过期、Store 读失败,还是请求真的换了实例。对于会自动重试的 Agent Runtime,这个差别直接决定下一步是重新协商、读取业务状态,还是停止执行以避免重复副作用。
实验中的 Session 和 Handle 每次运行都会随机生成,因此文章不把某个 ID 当成固定样本。验证脚本检查的是响应状态、副本切换、对象生命周期和最终数据,再把去敏后的 Exchange 与结果摘要写入 JSON。这样既能复跑,也不会为了让截图一致而硬编码一个看似真实、实际上从未变化的 Session。
MCP 2026-07-28 为什么移除 initialize 和 Mcp-Session-Id?
锁定 RC 的主线是 Stateless Core。initialize / initialized 退出核心流程,协议级 Session 不再承担请求关联;server/discover 用于探测支持的协议版本与能力,协议版本、Client 信息和能力随请求提供,HTTP 侧增加 Mcp-Method、Mcp-Name 等路由信息。当前 TypeScript SDK 指南还明确:Server identity 不应继续从旧初始化结果或固定的 Discover body 字段读取,而应按当前实现从响应 _meta['io.modelcontextprotocol/serverInfo'] 获取。Draft Changelog
| 旧流程 | 2026-07-28 RC | 迁移时最容易写错的地方 |
|---|---|---|
先 initialize,再保存协商结果 | 每次请求携带必要协议信息 | 仍在 Handler 中读取旧 Session Context |
Mcp-Session-Id 关联后续请求 | 核心请求不依赖协议 Session | 把业务状态也一起删除 |
| 初始化响应返回 Server 信息 | server/discover 探测版本与能力,Server identity 随响应 _meta | 仍从旧初始化结果或固定 body 字段读取身份 |
| 主要依赖 JSON-RPC Body 路由 | Header 提供 Method/Name 元数据 | 只相信 Header,不核对 Body |
| Tasks 位于旧核心线级词汇中 | 长任务改由独立扩展承接 | 默认所有 Client/Server 都支持 Tasks |
这些变化可以归结为一条逻辑:过去藏在连接和进程记忆里的协议事实,现在尽量变成请求中可见、可验证、可路由的契约。Client、Server、Tools 与 Resources 的职责边界可继续参考 MCP 协议分层指南。
但不要把 SDK v2 和新协议画等号。TypeScript SDK v2 文档明确提醒,升级主版本不会自动把 2026-07-28 放到 Wire 上,仍需显式配置并检查真实请求。TypeScript SDK v2 迁移说明
协议发布日期、SDK 主版本、包发布时间和商业 Client 支持日期是四条时间线。看到 2.0.0b2,只说明拿到了一个预发布包,不足以证明线上已经在说新协议。
从实现角度看,移除初始化握手也改变了错误出现的位置。旧流程中,版本和能力不兼容通常在 initialize 阶段暴露;新流程里,每次请求都可能携带不同的协议和能力信息,Server 要在进入 Tool 之前完成判断。一个长期运行的 Client 如果升级中途改变了请求形态,Gateway 不再沿用几小时前缓存的协商结果。
多租户环境中的差异更明显。过去同一个 Session 可能同时代表“这个 Client 支持什么”和“这个用户是谁”,两者生命周期却完全不同:Client 版本可能在应用升级后变化,用户 Token 可能十分钟后过期,业务对象可能保留数月。把这些信息拆到请求、身份上下文和领域存储后,更新与失效可以各自发生,不再被一个 Session TTL 绑在一起。远程 Server 的 Token、Resource Indicator 与 Tool Scope 设计可继续参考 MCP OAuth 认证实战。
server/discover 也不是新的长期会话入口。它提供支持的协议版本与能力快照,Client 可以根据 ttlMs 等提示决定何时刷新;Server identity 则按当前 SDK 指南从响应 _meta 读取。真正调用 Tool 时仍要使用当下请求和当下授权。发现阶段看到某个 Tool,不代表用户在执行阶段一定仍有权限,也不代表灰度中的另一 Server Pool 具有完全相同的 Registry。
没有 initialize 后,MCP 请求如何在任意副本执行?
新版 Fixture 不建立协议 Session。每次请求都提供协议版本和路由元数据,Server 根据当前请求完成验证和执行。
下面是简化后的 tools/call:
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: add_item
{
"jsonrpc": "2.0",
"id": 13,
"method": "tools/call",
"params": {
"name": "add_item",
"arguments": {
"basket_id": "basket_33be3dcbe25d",
"item": "mcp-book"
},
"_meta": {
"io.modelcontextprotocol/clientInfo": {
"name": "xbstack-fixture",
"version": "1.0"
},
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
}
}
Header 适合让 Gateway 提前做路由、限流和观测,但最终执行仍以经过验证的 JSON-RPC 请求为准。实验故意制造了一次冲突:Header 声称 tools/list 和 wrong-tool,Body 实际调用 create_basket。Server 在 Tool 执行前返回:
{
"code": -32602,
"message": "Routing headers do not match the JSON-RPC body"
}
错误文本是 Fixture 的实现,不是对正式规范错误消息的声明。真正有用的结论是:Gateway 可以利用路由头,但路由头无权绕过 Body、Token 和 Tool Scope 的最终核对。
跨副本测试中,server/discover 落到 draft-a,tools/list 落到 draft-b:
discover_status = 200
discover_replica = draft-a
list_status = 200
list_replica = draft-b
tool_count = 3
ttl_ms = 30000
两条请求没有共享 Session,也不依赖 A 的内存。随后 create_basket 到 A、add_item 到 B 仍然成功。到这里可以把两件事分开:协议请求能跨副本,是因为请求自包含;业务流程能继续,是因为状态有明确 Handle 和外部存储。
自包含并不等于把所有内容都塞进请求。协议版本、Client 信息和本次调用参数适合随请求传递;订单详情、文件内容和审批记录仍应只传引用。否则为了摆脱 Session,把几十 KB 甚至几 MB 的业务上下文重复发送到每个 Tool Call,既增加成本,也会让敏感数据经过更多代理和日志系统。
Gateway 的职责也因此更清楚。它可以在读取完整 Body 前,根据协议版本和方法头选择 Server Pool;完成解析后,再核对 Body、身份与 Tool 权限。两次判断结果不一致时,安全做法是拒绝,而不是“以 Header 为准”或“以 Body 为准”继续猜。尤其在边缘代理会重写 Header 的环境中,日志保留接收值和标准化值,冲突来源才可追溯。
这条验证链还涉及一个顺序问题:幂等记录是在验证前还是验证后创建。若 Tool 有副作用,通常先完成协议和权限校验,再用规范化后的 Tool 名称与参数生成幂等键。否则攻击者可以用不同 Header、相同 Body 制造多个键,或者让一个被拒绝的请求提前占用正常调用的幂等记录。

Mcp-Session-Id 删除后,业务状态应该放哪里?
“移除 Session”最容易被理解成“Server 不能有状态”。这是错的。订单、审批、购物篮和任务进度当然还要保存,只是不再悄悄塞进协议 Session。
实验使用了一个很小的购物篮流程:
create_basket
<- basket_id
add_item(basket_id, "mcp-book")
<- updated basket
create_basket 在 draft-a 创建记录,add_item 在 draft-b 读取并更新同一个 SQLite 数据库:
{
"basket_id": "basket_33be3dcbe25d",
"status": "active",
"items": ["mcp-book"]
}
basket_id 与旧 Mcp-Session-Id 的差别不只是名字。它指向一个明确领域对象,Server 每次使用时都能检查所有者、租户、状态和过期时间。协议 Session 往往只是一个关联标识,实现却可能把 Client 能力、身份、临时文件和业务草稿全塞进去。
本次过期测试先执行 expire_basket,再从另一副本使用同一 ID。Server 返回 -32602 Invalid or expired application handle。这个错误码属于 Fixture 选择,关键是过期对象没有因为 ID 格式正确而被继续信任。
Handle 设计没必要写成一套百科,迁移时先守住三条:
- 所有权明确:查询条件同时包含
handle_id、tenant_id和允许状态,避免先按 ID 取出再补权限判断; - 生命周期明确:Active、Completed、Cancelled、Expired 不是同一个“找不到”;
- 并发明确:审批、草稿等可写对象带版本或 ETag,冲突时重新读取,不做静默覆盖。
有副作用的 Tool 还要配幂等键。请求无状态并不代表每次重试都是新业务动作。付款、发邮件、创建工单或写数据库时,Gateway 和 Worker 可能因为超时重复提交;没有幂等记录,新的协议形态一样会造成两次真实操作。
实际数据模型可以保持很小。Handle 表保存对象类型、租户、所有者、状态、版本、过期时间和真正领域记录的引用即可。模型看到的是随机 ID,Server 根据身份上下文查询权威记录。这样做比把完整对象签进一个超长 Token 更容易撤销和审计:权限变化后,数据库查询立即生效,无需等待旧 Token 自然过期。
这里还有一个常见陷阱:把 Handle 做成全局唯一,就以为权限已经安全。随机性只能降低枚举概率,替代不了授权。日志泄露、提示词注入、浏览器历史或另一个 Tool 都可能把真实 ID 带到错误用户手中。查询语句中直接加入租户和所有者条件,能让泄露的 ID 失去独立价值。Tool Scope、只读账号和审计边界可结合 MCP 安全治理实战 一起检查。
并发冲突也用一次真实测试证明。让两个 Client 同时读取同一个 approval_id,分别提交不同修改;如果后写请求静默覆盖先写结果,说明新的显式状态仍然不可靠。更稳的结果是第二个请求收到版本冲突,重新读取最新记录,并在金额、收款人等高风险字段变化时再次要求人工确认。
对于等待审批的流程,还可以把创建时的关键参数哈希放进记录。继续执行时,Server 重新计算当前参数;哈希不同就拒绝旧审批。这样即使 Agent 在等待期间改了目标账号,也不能拿原来的批准继续执行。这个保护属于业务对象,而不是协议 Session,也不会因为请求换到另一副本而失效。

Python MCP SDK v2 怎么升级,为什么只改版本号不够?
不能只把依赖从 mcp>=1 改成 mcp>=2。正确顺序是隔离安装、验证导入与启动、抓取真实 Wire Protocol、测试目标 Client,再决定是否切换生产依赖。
截至 2026 年 7 月 23 日,PyPI 可见的 Python MCP v2 预发布包是 mcp==2.0.0b2,发布于 7 月 14 日。PyPI: mcp 2.0.0b2
测试没有修改项目全局依赖,而是隔离安装:
python3 -m pip install \
--target /tmp/xbstack-mcp-sdk-b2 \
mcp==2.0.0b2
安装和 mcp、mcp.server 导入成功,但旧示例常见的:
from mcp.server.fastmcp import FastMCP
在本次 Wheel 中返回:
ModuleNotFoundError: No module named 'mcp.server.fastmcp'
这条失败比“成功安装”更有价值。它说明迁移涉及包布局、Server 抽象、启动方式和协议启用路径;单纯把依赖范围改成下面这样并不够:
mcp>=1.x
改成:
mcp>=2
然后继续运行旧入口。
现阶段更合理的依赖方式是:生产分支继续设置 <2 上界;迁移分支固定精确 Beta;正式版出来后,再依据真实导入、启动和互操作结果固定具体 2.x.y。预发布包不适合通过宽泛版本范围自动升级。
测试顺序也不必上来就做完整压测。先确认四件事:包能否安装、最小 Server 能否启动、真实 Wire 是否采用目标协议、目标 Client 能否完成发现和调用。前四步不通,负载测试没有意义。
“能启动”不等于进程没有退出。最小 Server 可以注册一个无副作用 Tool,完成发现、列举、调用和错误返回;随后抓取真实 HTTP 请求,确认版本头、路由头和 _meta 与预期一致。很多迁移失败不是 API 完全不可用,而是新 SDK 仍以兼容模式发送旧协议,或者 Server 启动成功却没有启用目标 Transport。
迁移分支保留一份 Wire Golden File 即可,无需覆盖所有字段。固定一条 server/discover、一条 tools/list、一条成功调用和一条失败调用,Beta 更新到正式版时先比较这些样本,就能快速看出方法、错误码和元数据是否变化,比重新阅读整份 SDK README 更可靠。
包版本之外,运行环境也属于证据。此次 b2 结果来自 Python 3.10.2 和隔离目录,不足以推导另一个 Python 版本、锁文件或操作系统会得到同样模块。正式验证时把 Python 版本、解析后的依赖和 Wheel 哈希写进结果文件,出现差异时才可复现,而不是只说“同样装了 2.0.0”。

旧 MCP Client 能直接连接 2026-07-28 Server 吗?
新旧协议不是“字段大部分相同,所以大概率能用”。旧 Client 会先 initialize 并等待 Session;新 Client 直接发送自包含请求。旧 Server 需要 Session,新 Server 可能根本不接受初始化流程。
最小兼容矩阵如下:
| Client | Server | 直接结果 | 灰度处理 |
|---|---|---|---|
| 2025-11-25 | 2025-11-25 | 通过 | 保留旧 Pool |
| 2025-11-25 | 2026-07-28 | 失败 | 路由到旧 Adapter |
| 2026-07-28 | 2025-11-25 | 失败 | 路由到新版 Server |
| 2026-07-28 | 2026-07-28 | 通过 | 进入小流量验证 |
这不是官方全量兼容声明,只是最小 Wire Contract 的结果。真实环境还要加入具体 SDK、商业 Client、Transport、OAuth 和扩展能力。
灰度期最好在 Gateway 明确分流,而不是让同一个 Handler 猜测所有版本:
MCP-Protocol-Version
├── 2025-11-25 -> legacy adapter / legacy pool
├── 2026-07-28 -> stateless adapter / new pool
└── 缺失或未知 -> 明确错误与升级指引
Adapter 可以把旧初始化结果转换成内部 Client Profile,也可以把请求路由到对应 Server Pool,但它无权伪造 Client 没有声明的能力;有副作用的 Tool 失败后,也不要换协议无条件重试一次。
为了避免业务代码同时理解两套 Wire Shape,Adapter 后面最好接一个统一内部请求。这个对象保留业务真正使用的字段:外部协议版本、原始 Method、规范化 Tool 名称、身份上下文、业务 Handle、幂等键和允许的回退策略。旧协议负责把 Session 中仍然有效的信息转换进来,新协议负责解析 Header 和 _meta;Tool 本身不直接读取旧 Session,也不依赖某个新 Header。
这层隔离有两个好处。协议翻译错误不会悄悄进入领域模型,Gateway 可以在执行前记录“外部请求是什么、内部请求变成什么”;未来再升级协议时,只替换入口 Adapter,无需让付款、文件和工单 Tool 跟着重写。若同一个 Tool 在旧新路径下生成不同幂等键,问题也能在统一模型处暴露,而不是等到出现重复副作用后再排查。
回退策略在执行前确定。只读的 tools/list 可以在新版不可用时转旧 Pool;已经进入付款后端的 tools/call 遇到超时后,不再换协议重试。Gateway 要区分“还没有执行”和“可能已经执行但结果没回来”。后者通过幂等记录或后端操作 ID 查询,而不是再发一次。
兼容层也有退役条件:按 Client 版本和租户统计旧协议流量,对低频调用方主动提醒;观察窗口内旧流量和回退归零后,再删除旧 Adapter。按“SDK 发布一个月”推测所有 Client 都升级,最容易漏掉月末任务和低频管理员工具。

MRTR、Tasks 和业务 Handle 有什么区别?
MRTR 解决一次 Tool 调用缺少补充输入的问题,Tasks 承接长时间运行的执行,业务 Handle 则指向购物篮、审批、浏览器会话等领域对象。三者都不能继续被混成一个通用 Session ID。
移除协议 Session 后,审批、补充输入和长任务并没有消失,只是不能继续依赖某个连接对象。
MRTR(Multi-Round Tool Results)适合一次 Tool 调用中缺少额外输入的情况。Server 返回 InputRequiredResult 和 requestState,Client 收集用户输入后,再把 inputResponses 与原状态一起提交。
execute_payment
-> InputRequiredResult
requestState = opaque_state_abc
用户确认
-> tools/call
requestState = opaque_state_abc
inputResponses = {...}
requestState 仍要检查调用者、原始参数、是否过期和是否已使用。它不是一个拿到字符串就能继续执行的通行证。
长时间运行的工作由独立 io.modelcontextprotocol/tasks 扩展承接。2026 Core Handler 本身不处理 tasks/*;只有 Client 与 Server 都声明并启用 Tasks 扩展后,扩展实现才接受 tasks/get、tasks/update、tasks/cancel,受支持的请求也才可以返回 CreateTaskResult。MCP Tasks 扩展
| 场景 | 合适的状态 |
|---|---|
| 一次调用缺少用户输入 | MRTR requestState |
| 数十秒到数小时的执行 | Task Handle + Queue/Task Store |
| 购物篮、草稿、审批对象 | 领域 Handle + 数据库 |
| 纯查询 Tool | 自包含请求,通常无需会话状态 |
一个可恢复任务要回答三个问题:现在处于什么状态,谁有权推进,上一轮是否已经产生副作用。仅有 running 和 completed 两个状态不够。任务可能正在排队、等待输入、等待审批、执行中、重试中、收到取消请求、已经取消,或者后端已经成功但结果尚未写回。状态太粗,另一个 Worker 接手时只能重新猜测。
Worker 不要永久“拥有”任务。更适合多副本的做法是有限租约:Worker 领取任务后写入到期时间并定期续约;进程崩溃后,其他 Worker 等租约过期再接手。接手前先读取上一次 Attempt 和外部操作 ID,判断邮件、付款或工单是否已经创建。能向第三方查询结果的就先对账,无法查询的则依赖幂等键或人工复核。
取消同样不是删掉一行记录。Client 发出取消时,任务可能还在队列,也可能已经调用第三方 API,只是响应没有回来。系统分别记录“收到取消请求”和“确认执行停止”。界面显示 Cancelled 之前,先确认外部副作用是否真的没有发生;否则用户以为付款已取消,后端实际上已经扣款。
MRTR 与 Task 还可能同时出现。例如一项长任务执行到一半,等待人工补充凭证:Task Handle 代表整项后台工作,requestState 只代表这一次缺失输入。两者可以关联,但生命周期和权限不要混用。凭证提交完成后,requestState 可以失效,Task 仍继续运行。
不要让一个字符串同时代表“等待用户输入”“后台任务”和“业务对象”。那只是把旧万能 Session 换了个字段名。

MCP Server 上线时如何做双协议灰度?
最稳妥的上线方式不是原地替换,而是按 MCP-Protocol-Version 把旧 Client 路由到 Legacy Pool、新 Client 路由到 Stateless Pool,再用低风险 Tool 做小流量验证,并保留数据和语义回滚能力。更完整的 OAuth、多租户隔离、观测和权限门禁可对照 MCP Server 生产化治理清单。
完整生产治理可以写得很长,但这次迁移真正影响切流量的内容可以收敛为四件事。
1. 协议路由能解释
每条请求都记录 Client、协议版本、Method、Tool、目标 Server Pool 和是否回退。未知版本直接返回可识别错误,不让 Handler 猜。
2. 业务状态能跨副本
切换副本、重启进程和断开连接后,Handle 仍能读取;过期、越权和版本冲突都有稳定结果。关键状态不再只存在 Worker 内存或临时目录。
3. 副作用不会因为重试翻倍
付款、邮件、数据库写入和工单创建使用稳定幂等键,日志能区分 Request、Tool Call 和 Attempt。发生超时时,先查上一次是否已完成,不直接再执行一遍。
4. 旧流量可以立即退回
Gateway 保留版本开关,新版数据能被旧路径解释,或者 Canary 限制在可丢弃、可重建的低风险流程。只回滚代码、不处理已经创建的 Handle 和任务,不算可回滚。
灰度顺序可以很朴素:先只读 Tool,再迁移可幂等写入,最后处理付款、权限修改和文件删除。观察成功率之外,还要看权限拒绝、回退、重复副作用和 Handle 失败。回退成功率很高,有时恰恰说明新版一直没工作。
假设第一批只迁移 tools/list 和一个只读查询 Tool。新版返回结果与旧版一致,但权限拒绝率上升,这不一定是回归:可能是新路径终于正确检查了 Audience 或 Tool Scope。相反,如果最终 HTTP 成功率维持 99.9%,但一半请求都回退到旧 Pool,新路径显然没有通过。面板同时显示首次执行结果、回退原因和最终结果,而不是只展示最后一个 200。
写入 Canary 更关注“执行了几次”。每个请求分配内部 Request ID,每次 Tool 调用分配 Tool Call ID,重试再生成 Attempt ID;三者与 traceparent 关联,彼此不替代。出现重复邮件时,运维人员要能区分一个请求重试两次,还是两个独立请求各执行一次。只记录 Trace 或业务 Handle,通常回答不了这个问题。
数据回滚也在切流量前演练。新版可能已经创建新的 Handle、Task 或审批状态,旧路径是否能读?无法回读时,就把首批 Canary 限制在可重建数据,并明确这是前向迁移。数据库备份只恢复记录,撤不回已经发出的邮件、付款和第三方工单,所以副作用回滚始终要靠幂等、对账或人工处理。
观察窗口还要覆盖真实调用周期。月末财务任务、周一批处理和低频管理员 Tool 不会因为三天没有报错就自动通过。可以用合成请求补足低频路径,但最终仍要等至少一个真实业务周期,确认旧协议流量和回退次数确实下降。
| 阶段 | 通过标准 | 回滚方式 |
|---|---|---|
| Fixture | 旧新请求与错误可重复 | 不改生产流量 |
| 双协议 | 旧 Client 零回归 | 全部回旧 Pool |
| 只读 Canary | 结果差异可解释 | 停止新版路由 |
| 写入 Canary | 幂等、Handle、审计稳定 | 按租户或 Tool 回退 |
| 退役旧协议 | 观察窗内旧流量为零 | 延长兼容窗口 |
正式规范发布后,重新运行本文六项实验,再补目标 SDK 和真实 Client 互操作。RC 与 Final 只要字段、错误码或扩展协商发生变化,文章和迁移代码都要一起更新。

这套 MCP 迁移方案已经验证了什么?
本地实验目前给出了几条明确结果:
- 旧 Session 在同一实例上可以工作;
- 轮询到另一副本后稳定返回
Unknown MCP session; - 新版自包含请求可以跨
draft-a、draft-b; basket_id能让业务状态跨副本延续;- 过期 Handle 和 Header/Body 冲突会在执行前被拒绝;
mcp==2.0.0b2可以安装,但旧 FastMCP 导入路径失败。
这些结果没有覆盖真实 Kubernetes Ingress、Service Mesh、SSE、跨区、商业 Client、OAuth 互操作、Tasks 扩展互操作或性能比较。SQLite 只是为了证明状态能离开进程,并非生产多副本共享本地 SQLite 的建议。
真实 Kubernetes 验证还会多出几层变量:Ingress 是否复用上游连接,Service Mesh 是否自动重试,Pod Drain 时流式响应如何结束,滚动发布期间旧新协议是否同时存在。localhost 轮询可以证明实例内 Session 的故障机制,却无法替这些组件做结论。上线前再把同一组 Fixture 放到目标基础设施里,保留副本和代理日志,并比较结果。
性能也无法由“无状态”三个字推断。每次请求携带更多元数据、读取外部 Handle、做 Schema 与权限验证,可能增加延迟;移除 Session 同步和粘性路由又可能降低复杂度。在相同 Payload、并发和后端存储下分别测量后,才知道成本落在协议解析、数据库还是 Tool 后端。本文没有这组数据,所以不会写“新版更快”或“扩容成本下降多少”。
商业 Client 是另一项独立变量。有的 Client 会主动协商兼容版本,有的会固定旧初始化流程,还有的在 UI 中隐藏错误重试。最小矩阵只说明 Wire Shape 不存在默认互通,正式发布前仍要选择实际支持的 Client,记录版本、协议、能力和错误恢复行为。自有 Fixture 通过,也不代表用户手中的桌面端已经可用。
正式规范尚未发布,因此正文中的方法名、字段和 SDK 结论仍属于发布前验证。最终版出来后,重新核对 Changelog、Schema、SDK Tag、错误码和扩展协商,并把实际结果写回文章。若 Final 与 RC 无差异,也要重新运行并记录正式 Tag,而不是把 7 月 23 日的结果改个日期继续使用。
实验目录:
experiments/mcp-2026-07-28-stateless-migration-demo/
运行 Wire Fixture:
python3 run_experiment.py
当前记录结果:
{"passed": 6, "total": 6, "all_passed": true}
哪些 MCP Server 需要现在迁移?
只通过本地 stdio 服务一个开发者、没有远程多租户和多副本的 Server,不必为了版本号立即改生产代码。先固定依赖,建立最小新协议 Fixture,等正式规范和目标 SDK 稳定后再决定发布时间。
已经使用远程 Streamable HTTP、Kubernetes、Serverless、Gateway 或多租户,并把 Session 留在进程内存中的系统,应当现在开始准备。准备的重点不是切流量,而是找出隐式状态、建立版本路由、把业务状态改成 Handle,并跑真实的新旧 Client/Server 组合。
有审批、支付、邮件、数据库写入等副作用的项目,优先检查幂等和状态所有权。新协议能让请求不再依赖某个 Pod,却不会自动阻止重复付款,也不会替 Server 判断一个泄露的 approval_id 属于谁。
判断优先级时,可以直接看当前 Session 是否参与业务决策。如果 Session 只保存协议协商结果,迁移主要是 Client/Server 兼容与路由问题;如果 Tool 会从 Session 读取用户、租户、草稿或任务进度,风险就高一个等级,因为删除握手后这些数据要有新的来源;如果 Session 中还记录“上一次是否已经付款”,先停下来重构业务状态和幂等,协议切换反而排在后面。
团队规模也会影响做法。只有一个内部 Client 时,可以同时升级 Client 和 Server,兼容窗口很短;面向多个桌面端、合作方或低频自动化任务时,双协议路由可能保留数周甚至数月。此时 Adapter 不是失败,而是主动管理升级节奏。真正的问题是没有流量统计和退役日期,让过渡层永久存在。
迁移计划最后要回答一个具体问题:任意杀掉当前副本后,下一条请求会发生什么?理想结果不是“负载均衡器会尽量送回原 Pod”,而是新的副本能够验证协议、读取权威业务状态、识别上一轮 Attempt,并继续或明确拒绝。只要这个问题仍然依赖 Session 粘性,迁移就还没有完成。
最终判断很简单:MCP 2026-07-28 值得迁移,但安装 SDK v2 只是开始。 真正完成的标志是任一副本都能解释当前请求,业务状态不藏在协议 Session 里,旧新版本有明确路由,失败后能知道副作用到底发生了几次。

实验代码与官方来源
自有实验:
experiments/mcp-2026-07-28-stateless-migration-demo/README.mdexperiments/mcp-2026-07-28-stateless-migration-demo/results/verification.jsonexperiments/mcp-2026-07-28-stateless-migration-demo/results/sdk-smoke.jsonexperiments/mcp-2026-07-28-stateless-migration-demo/IMAGE_PLAN.md
官方与问题来源:
- MCP 2026-07-28 Release Candidate
- MCP Draft Changelog
- SEP-2567: Sessionless MCP
- TypeScript SDK:Support for MCP 2026-07-28
- Python SDK 官方仓库
- PyPI: mcp 2.0.0b2
- Stack Overflow:MCP Streamable HTTP 多副本 Session 问题
正文表格为 XBSTACK 自绘,实验数据来自仓库内 Fixture;封面和架构图使用 GPT 生成,不复制官方截图或第三方图表。
继续按 MCP 生产部署路径读,而不是堆 guide / tutorial
MCP 内容统一按协议理解、本地 Server、远程部署、OAuth、安全治理、stdio/JSON-RPC 排障和工具对比来承接,避免站内关键词互相抢。
下一步阅读
返回专题入口 →AI SDK 7 迁移实战:流式中断、Cloudflare 524 边界与 Tool Call 恢复
基于 AI SDK 6/7 隔离实验与真实 localhost HTTP/SSE 断线测试,拆解消息持久化、Tool Call 幂等恢复、请求绑定与后台继续执行,并区分 AI SDK Timeout、客户端断线和 Cloudflare 524 代理边界。
AI 交易智能体实战:行情监控、策略回测、风险控制与人工确认闭环
系统拆解 AI 交易智能体的生产级设计方法,覆盖行情监控、新闻与公告解析、技术信号、策略回测、风险控制、仓位限制、模拟盘验证、人工确认、交易日志和复盘指标,帮助开发者构建可控的交易辅助系统,而不是盲目自动下单。
OpenAI Agents SDK RunState 实战:Tool Approval 如何跨进程恢复?
基于 openai-agents 0.18.3 的可复现实验,完整验证 Tool Approval 暂停、RunState 序列化、跨进程批准/拒绝与 Worker 恢复,并复现重复投递导致工具执行两次、Context 泄漏秘密和状态版本漂移等生产边界。
AI Agent 协议与框架选型:MCP、Function Calling、A2A、LangGraph、AutoGen、CrewAI 怎么选?
系统梳理 AI Agent 开发中的协议与框架选型,覆盖 Function Calling、MCP、A2A、LangGraph、AutoGen、CrewAI、LangChain、自研 Workflow、多智能体协作、工具调用、状态管理和生产化边界,帮助开发者根据场景选择合适技术栈。
小白
Full-Stack AI Engineer
小白,全栈 AI 工程师,持续构建生产级 Agent 系统、产品工具与独立软件资产。
了解小白与 XBSTACK →
参与讨论
问题、验证与勘误
登录后可发表评论。所有新评论先进入审核;审核期间仅评论者本人和管理员可见,通过后才公开。