文章类型技术长文 所属专栏Agent 前沿技术 预计阅读89 分钟 文档状态已发布
返回

MCP 2026-07-28 深度迁移指南:从会话耦合协议到无状态 Agent 数据平面

深入解析 MCP 2026-07-28 无状态协议核心、MRTR、Tasks、显式状态载体、授权边界与 Dual-era 生产迁移。

开始阅读全文17813 字 · 89 分钟 查看系列目录Agent 前沿技术
关键词 MCPAgentAI CodingMRTRTasksProtocol Engineering
栏目 AI-Coding;专栏 Agent 前沿技术;标签 MCP、Agent、AI Coding、MRTR、Tasks、Protocol Engineering

逐帧解析 per-request _meta、显式状态句柄、MRTR、订阅流、缓存、授权与双时代兼容
比较基线:MCP 2025-11-252026-07-28
事实核验截止:2026-08-09

2026 年 7 月 28 日,Model Context Protocol 发布了自诞生以来最具破坏性的一次协议修订。官方把它概括为“无状态协议核心”:initialize / notifications/initialized 握手被移除,Mcp-Session-Id 退出现代协议,每个请求都携带版本与能力;服务器需要额外输入时,不再反向发送 JSON-RPC 请求,而是通过 Multi Round-Trip Requests(MRTR)结束本轮,再让客户端重提原请求;目录结果开始携带缓存提示;HTTP 网关也终于可以仅根据标准 Header 完成路由与策略判断。

如果只把这次升级理解成“删 Session、取消粘性负载均衡”,就会在迁移中制造更危险的隐式状态:有人会把浏览器实例塞进某个进程内存,有人会把 requestState 当成可信 JWT,有人会把 handle 当作权限凭证,也有人会把 cacheScope: public 误解为“已通过授权”。协议状态确实被拿走了,但业务状态、交互状态、长任务状态和通知状态并没有消失。

这次升级真正发生的是一次状态所有权重分配

Slogical:=Smeta,Shandle,SMRTR,Stask,SsubscriptionS_{\text{logical}} := \langle S_{\text{meta}}, S_{\text{handle}}, S_{\text{MRTR}}, S_{\text{task}}, S_{\text{subscription}} \rangle

这里是职责分解,不是把五类状态做数值相加。

  • 协议版本、客户端能力与日志偏好进入每请求 _meta
  • 跨普通工具调用的业务对象进入显式 handle;
  • 短时补参与确认进入 MRTR 的 inputResponses / requestState
  • 可跨断线、分钟到小时的执行进入 Tasks 扩展或应用级 Job Handle;
  • 目录与资源变更进入 subscriptions/listen 这一条有边界的长请求。

状态从“连接曾经发生过什么”变成“当前消息明确引用什么”。这让 MCP 请求可以被路由、缓存和追踪,也把幂等、完整性、对象级授权、缓存隔离和双时代兼容的责任明确交还给实现者。

本文把“可路由、可缓存、可观测的 MCP 请求层”称为 Agent 数据平面。这是一种工程解释,不是 MCP 规范中的正式术语。全文也始终区分两件事:协议层无状态应用层是否保存状态


1. 先冻结版本边界:哪些是最终规范,哪些只是设计背景#

1.1 权威顺序决定你应当相信哪一份报文#

本文只比较 2025-11-25 与最终发布的 2026-07-28。遇到冲突时,采用如下优先级:

  1. dated specification 与 schema.ts
  2. 最终 Changelog;
  3. 已通过的 SEP,用来解释设计动机;
  4. 官方 SDK 迁移指南,用来说明具体实现入口;
  5. RC 或正式发布博客,只承担摘要作用。

这个顺序不是形式主义。正式发布博客中的精简 tools/call 示例只展示了 clientInfo,但最终 Base Protocol 明确规定:现代请求必须包含 io.modelcontextprotocol/protocolVersionio.modelcontextprotocol/clientCapabilitiesclientInfo 是 SHOULD,而不是 MUST。照抄博客示例会得到一个应被服务器以 -32602 Invalid params 拒绝的不完整请求。

为便于讨论,本文使用三组术语:

  • Legacy:通过 initialize 建立会话的 2025-11-25 及更早版本;
  • Modern:使用每请求元数据的 2026-07-28 及以后版本;
  • Dual-era:同时支持 Legacy 和 Modern 线协议的客户端或服务器。

它们描述的是协议时代,而不是 SDK 大版本。以官方 TypeScript SDK v2 为例,单纯升级依赖并不会自动发送 2026-07-28 报文;手工构造的 Client / Server 默认仍可走 2025 时代行为,现代协议需要显式启用。SDK 版本与 wire revision 必须分开记录。

1.2 删除、替换、扩展、弃用不能混写#

最终 Changelog 中最容易混淆的是 Removed 与 Deprecated:

类别2026-07-28 变化迁移方向
删除initializenotifications/initializedMcp-Session-Idpinglogging/setLevelnotifications/roots/list_changednotifications/elicitation/complete、URL Elicitation 的 elicitationId、SSE 恢复语义每请求 _meta、传输健康检查、请求级日志、MRTR 与显式重试
替换服务器主动 JSON-RPC 请求;HTTP GET 通知通道;resources/subscribe / unsubscribeMRTR;subscriptions/listen
扩展正式 Extensions 框架;Tasks、MCP Apps 等双方通过 capabilities.extensions 显式协商
弃用Roots、Sampling、Logging、HTTP+SSE、Dynamic Client Registration,以及 Sampling 的 includeContext"thisServer""allServers"工具参数/资源、直接模型 API、OTel、Streamable HTTP、CIMD;includeContext 省略或使用 "none"

被弃用的特性在当前修订中仍然存在。Roots 与 Sampling 若被存量实现使用,其服务器到客户端请求必须经 MRTR;Logging 也仍在弃用窗口内,但日志等级已经改为逐请求 _meta,且 logging/setLevel 已删除。相反,ping 已经从现代核心协议中移除,不能再把它当作现代应用层健康检查。

1.3 五个必须先纠正的误区#

  1. 无协议 Session 不等于无业务状态。 数据库草稿、浏览器进程和购物车仍需持久化,只是不再隐含绑定某条 MCP 连接。
  2. 新版没有彻底移除 SSE。 被移除的是独立 GET 通道、SSE 上的服务器主动 JSON-RPC 请求,以及 Last-Event-ID 恢复;单次请求仍可用 SSE 返回进度与最终结果,subscriptions/listen 本身也可形成 SSE 长流。
  3. server/discover 不是新握手。 服务器 MUST 实现,客户端 MAY 调用;客户端也可直接发送业务 RPC,并处理版本错误。
  4. clientInfo 不是身份凭证。 它与响应里的 serverInfo 都是自声明信息,只适合展示、日志和调试。
  5. Tasks 不再属于核心 wire。 它已成为 io.modelcontextprotocol/tasks 扩展,核心 SDK 支持现代协议并不等于具体宿主已启用 Tasks。

2. 旧 Session 模型为什么会成为生产瓶颈#

2.1 旧握手建立了一个隐式前置条件集合#

2025-11-25 的典型 Streamable HTTP 生命周期如下:

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": { "elicitation": {} },
"clientInfo": { "name": "agent-host", "version": "1.8.0" }
}
}

服务器可以返回 Mcp-Session-Id,客户端随后发送 notifications/initialized,再在后续调用中携带该 ID:

POST /mcp HTTP/1.1
Mcp-Session-Id: 7f0d7d2c...
MCP-Protocol-Version: 2025-11-25
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "create_database",
"arguments": { "region": "us-west-2" }
}
}

需要严谨说明的是:旧版 Streamable HTTP 允许服务器采用无会话实现,Mcp-Session-Id 并非所有旧服务器的必需品。问题不在于旧规范强迫所有服务有状态,而在于它给协议级会话、服务器主动请求和可恢复流留下了空间,于是能力、身份、动态目录与业务对象很容易被实现为同一份连接状态。

2.2 Session 把应用语义泄漏给负载均衡器#

假设副本 A 在 initialize 后把如下内容写进内存:协议版本、客户端能力、当前账号、数据库草稿、报价、已选择工具集合。下一次调用若落到副本 B,B 并不知道这些前置事实。工程上只剩两个选择:

  • 使用 sticky routing,让某个 Session 永远回到 A;
  • 建立共享 Session Store,让所有副本同步读取。

前者降低调度自由度:长尾 Session、慢工具与大模型请求会让副本负载倾斜,滚动发布也更难排空。后者则新增一致性、TTL、垃圾回收、网络跳转和故障恢复问题。更棘手的是,Session 究竟代表浏览器标签页、Agent、对话、子任务还是一个业务对象,协议从未替应用作出可靠选择。

例如,一个总控 Agent 派生 20 个子 Agent。它们可能需要共享同一个购物车,却各自使用隔离浏览器;也可能共享服务器工具目录,但不能共享用户确认。单一 Session 只能表达“一包状态一起生、一起灭”,无法表达这些不同生命周期和授权边界。

2.3 短生命周期 Agent 放大目录重取#

SEP-2567 的一个重要动机是:当工具列表与每条连接绑定时,每个临时 Agent 都可能重新获取每台服务器的目录。若 AA 是短生命周期 Agent 数,SS 是服务器数,在部署版本、认证主体和授权范围相同的前提下,目录请求压力近似为:

Clegacy=O(A×S),Cmodern-cache=O(S)C_{\text{legacy}}=O(A\times S), \qquad C_{\text{modern-cache}}=O(S)

这不是无条件的性能定律。如果不同 Agent 使用不同权限,目录本来就不能共享;若服务器频繁发布目录变化,缓存收益也会下降。它表达的是:当结果等价时,目录不再因为连接生命周期不同而被迫失去复用机会。

旧架构的成本因此不只是一次握手:

  • sticky routing 带来的负载不均与发布阻塞;
  • Session Store 的一致性、回收与故障转移;
  • 短 Agent 重取工具目录导致的 MCP 与上游 Prompt Cache 抖动;
  • 连接中断时协议、交互和业务状态一起丢失;
  • 把 Session ID 错当审计主体、速率限制键甚至授权凭证。

2026-07-28 的价值,是迫使这些概念从一枚含义模糊的 ID 中拆出来。

从黏性会话与隐式状态迁移到自描述请求、任意实例处理和持久化业务状态


3. 自描述请求:握手消失后如何协商版本与能力#

3.1 一份完整的现代 tools/call#

现代 HTTP 请求在 Header 与 JSON-RPC Body 中同时携带可供不同层消费的事实:

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
Authorization: Bearer <access-token>
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: create_database
{
"jsonrpc": "2.0",
"id": "call-0192",
"method": "tools/call",
"params": {
"name": "create_database",
"arguments": {
"account_handle": "acct_h_K91...",
"region": "us-west-2",
"size": "db.m7g.large"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": { "form": {} },
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
},
"io.modelcontextprotocol/clientInfo": {
"name": "agent-host",
"version": "1.8.0"
},
"io.modelcontextprotocol/logLevel": "info",
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
}
}

其中只有两项 per-request 协议字段是必填:

_meta规范等级作用
io.modelcontextprotocol/protocolVersionMUST声明本请求使用的协议修订
io.modelcontextprotocol/clientCapabilitiesMUST只声明本请求可用的客户端能力与扩展
io.modelcontextprotocol/clientInfoSHOULD展示、日志与调试,不参与认证
io.modelcontextprotocol/logLevelMAY请求级日志下限;缺失时服务器不得为该请求发 notifications/message
progressTokenMAY将请求与 notifications/progress 关联,可为字符串或整数
traceparent / tracestate / baggageMAYW3C Trace Context 与 Baggage 传播;存在时 MUST 符合对应 W3C 格式

缺少必填字段属于参数非法:JSON-RPC -32602,HTTP 上同时是 400 Bad Request。如果字段存在,但请求需要的能力未声明,则是 -32021 MissingRequiredClientCapability;HTTP 状态必须为 400 Bad Requesterror.data.requiredCapabilities 携带所需的 ClientCapabilities 结构。两者不要混成“能力协商失败”。

3.2 resultType 把成功响应变成可判别联合#

现代成功结果必须包含 resultType

  • "complete":操作完成,结果是最终结果;
  • "input_required":当前操作未完成,需要通过 MRTR 提供输入;
  • 扩展可以引入新值,例如 Tasks 的 "task"

客户端必须把任何无法识别的 resultType 视为无效;可识别集合由核心值与双方已声明支持的 Extensions 共同构成。为了读取旧服务器,来自早期协议服务器且缺失 resultType 的结果必须按 "complete" 处理。这个兼容规则只允许“旧结果没有判别字段”,并不允许客户端无视一个明确但未知的新值。

3.3 server/discover 是可缓存发现,不是初始化#

服务器必须实现 server/discover,但客户端不必先调用:

{
"jsonrpc": "2.0",
"id": "discover-1",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "agent-host",
"version": "1.8.0"
}
}
}
}
{
"jsonrpc": "2.0",
"id": "discover-1",
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": { "listChanged": true },
"resources": {},
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
},
"instructions": "Database provisioning tools.",
"ttlMs": 3600000,
"cacheScope": "public",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "database-mcp",
"version": "4.2.0"
}
}
}
}

serverInfo 位于响应 _meta,同样是自声明信息。字段在 Schema 中可选,但服务器 SHOULD 在每个响应中携带,除非明确配置为不披露;客户端 SHOULD NOT 据此改变行为或作安全判断。ttlMs / cacheScope 说明发现结果本身可以缓存。客户端如果已经知道首选版本,也可以直接发 tools/listtools/call;当服务器不支持该版本时,返回:

{
"jsonrpc": "2.0",
"id": "call-0192",
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28"],
"requested": "2027-01-01"
}
}
}

客户端应从 supported 中选择共同版本,以新请求重试;没有共同版本才向用户报错。

3.4 HTTP 与 stdio 的时代探测不能共用一个粗糙规则#

Dual-era 客户端必须区分“对方是 Legacy”与“现代服务正在报错”:

情况正确解释是否回退 Legacy
-32022-32021-32020 等可识别现代错误对方是 Modern,修正版本、能力或 Header
HTTP 401 / 403认证或授权失败,不是时代证据
HTTP 5xx、网络错误、HTTP 探测超时基础设施失败
HTTP 400 Bad Request 且响应体为空或不是可识别现代错误时代不确定仅在受控 auto 策略下尝试
stdio server/discover 返回非现代错误、退出或超时可能是 Legacy

400 的空白或未知响应只能说明时代不确定,不足以证明 Legacy。本文建议只有在 auto 明确配置了 Legacy candidate、该 origin 从未成功建立 Modern、且本地安全策略允许降级时才尝试 Legacy;否则拒绝并告警。404/405 可能参与从 Streamable HTTP 向更老 HTTP+SSE transport 的专门探测,但不能推广为“任意 4xx 都证明 Legacy”。

TypeScript SDK 对浏览器 opaque CORS/preflight TypeError 有一项兼容回退例外,这是 SDK 策略,不是协议时代证据;高安全环境应 pin 版本。在 stdio 上,某些旧服务器收到初始化前的未知方法会直接退出。官方 SDK 的精确 StdioClientTransport 因此用 disposable sibling process 探测;其子类与自定义 stdio-shaped transport 则原地探测。时代判断适合按 stdio 进程配置或 HTTP origin 缓存;规范要求客户端 SHOULD 缓存,并在假设后来失败时重新探测,否则服务器升级后仍可能被永久当成 Legacy。

TypeScript SDK v2 的关键点不是“升级依赖”,而是显式选择:

const client = new Client(
{ name: "agent-host", version: "1.8.0" },
{ versionNegotiation: { mode: "auto" } },
);
await client.connect(transport);
console.log(client.getProtocolEra()); // "modern" | "legacy"

mode: "legacy" 或不配置仍走旧握手;auto 探测并在有旧版本候选时回退;{ pin: "2026-07-28" } 则拒绝 Legacy。生产环境应把“实际 era、回退原因、协商版本”写入指标,而不是只记录 SDK 版本。

3.5 发现结果可以缓存,但请求能力不能省略#

server/discover 返回的是服务器支持什么,当前请求 _meta 声明的是客户端在这一轮愿意并能够处理什么,两者方向不同。客户端即使缓存了发现结果,也必须在每个请求中发送自己的 clientCapabilities;服务器也不能因为“同一连接上一轮支持 elicitation”就省略本轮校验。

这使能力变成 request-scoped contract。一个 Host 可以让普通自动化请求只声明基础能力,而在具有 UI 与人工审核通道的请求中声明 Elicitation;也可以只在具备持久 Task Store 的调用路径上启用 Tasks 扩展。服务器必须按当前 envelope 的交集决定结果形态,不能按客户端软件名称猜测。

同理,缓存的 server/discover 只是优化,不是永久事实。TTL 到期、部署版本变化、授权上下文改变或收到相应变化信号后,客户端应重新获取。若首个业务请求直接返回 -32022,应使用错误中的 supported 版本重新协商,而不是因为旧 discover cache 声称兼容就不断重试同一报文。

这一区分还影响故障定位:

  • -32022 表示请求修订不被服务端实现;
  • -32021 表示本轮客户端没有声明服务器完成操作所需的能力;
  • -32602 表示请求结构本身缺失或非法;
  • -32020 表示 HTTP 镜像 Header 与 Body 的事实不一致。

把四者压成一个“握手失败”指标,会让版本回退掩盖能力错误,也会让安全网关不一致被误诊为客户端兼容问题。


4. 状态没有消失:从隐式 Session 迁移到显式载体#

4.1 先按生命周期与信任边界选载体#

状态需求推荐载体典型生命周期主要风险
单次请求所需版本、能力、日志、Trace每请求 _meta一次 RPC自声明字段被误作安全身份
多次普通工具调用共享业务对象服务器签发的显式 handle秒到小时或业务自定义越权、泄露、回收失控
短时补参、确认、多轮交互MRTR requestState数秒到数分钟篡改、重放、过期
跨断线长执行Tasks 扩展或 Job Handle分钟到天终态一致性、取消、持久化
目录和资源变化subscriptions/listen 请求一条长请求断流、重复订阅、缓存失效
敏感或体积很大的状态服务器存储 + 不透明 ID业务自定义存储可用性、对象级授权

所谓“任何请求可以落到任意副本”,并不是要求每个请求只靠一份巨大 Token 完成全部工作。它要求副本不依赖前一请求曾落在本地。副本完全可以读取共享数据库、对象存储或队列;必须被消除的是连接历史这一隐式前置条件。

按生命周期选择单次请求、跨请求回传与耐久任务的状态载体

4.2 显式 handle 是工具设计模式,不是新的 MCP RPC#

一个浏览器类服务器可以把原来的 Session 内对象改成:

create_browser(profile_handle) -> browser_id
navigate(browser_id, url)
click(browser_id, selector)
close_browser(browser_id)

browser_id 只是普通工具参数。规范没有 handles/createhandles/get 这类通用方法,文章与实现都不应虚构。哪些字段由客户端完整携带、哪些只返回引用,取决于体积、敏感性、并发更新和撤销需求:

  • 小、非敏感、不可变、可验证的数据可以放入签名载荷;
  • 大、敏感、需要并发更新或立即撤销的状态应留在服务器,只返回高熵 ID;
  • 涉及外部系统的对象通常要在持久层保存真实外部 ID、所有者、租户、scope、TTL 与状态版本。

4.3 handle 不是 bearer authorization#

模型看见 browser_id,不代表它获得了浏览器所有权。服务端每次调用至少应重新验证:

authenticated principal
∧ tenant match
∧ object owner/delegation match
∧ requested operation ∈ granted scope
∧ object not expired/revoked
∧ quota and policy allow

作为工程加固,handle 应尽量不可预测,但这不是核心协议对所有业务 handle 的统一规范要求,而且“高熵”只减少猜测,不替代授权。对有认证的服务器,安全性来自每次 (handle, auth_context) 对象级校验;对无认证、把 handle 本身当 bearer capability 的服务器,SEP-2567 才建议使用至少 128 bit 的 CSPRNG 熵。把租户、用户和权限直接编码在可读 handle 中也不等于安全绑定;除非整体经过完整性保护,客户端可以篡改。长期密钥更不应进入模型上下文。模型还可能在上下文压缩后丢失 ID、复制错字符或把一个对象 ID 用到另一种工具,因此服务端必须做类型、存在性与所有权校验,并返回可恢复的结构化错误。

4.4 工具目录不能再依赖连接历史#

现代规范要求 tools/listresources/listprompts/list 不因同一连接上的先前调用而变化。目录仍可以因部署版本、认证主体、授权范围、外部配置或真实目录更新而变化;不能再因“此连接刚调用过 connect_database”而变化。

旧模式:

connect_database(credentials)
-> Session 内出现 query / update / close 三个新工具

现代模式应改为固定目录:

connect_database(credentials) -> database_handle
query(database_handle, sql)
update(database_handle, statement)
close_database(database_handle)

如果目录确实由外部授权或配置改变,则通过新一轮 tools/list 和相应变化通知表达,而不是让网关、缓存和客户端猜测连接里曾发生什么。

4.5 状态放客户端还是服务器,取决于四个问题#

显式化不等于“把所有状态序列化进请求”。每类状态至少要回答:

  1. 客户端能否看到? 若包含内部风控规则、凭据或未公开报价逻辑,即使签名也不能明文携带;HMAC 不提供保密性。
  2. 是否需要立即撤销或并发更新? 需要撤销、锁、版本冲突或多人修改的对象更适合服务器记录;自包含 Token 很难在不访问存储的情况下获得实时撤销。
  3. 体积与频率如何? 大状态反复进入模型上下文会增加 Token 成本,并可能被模型复制、摘要或泄漏;客户端只需持有短引用。
  4. 丢失后能否重建? 可由当前参数重算的派生状态可以短暂缓存;不可重放的副作用结果必须持久化。

因此一个生产 handle 记录往往至少包含:

handle_id, object_type, tenant_id, owner_principal,
delegated_scopes, external_object_id, state_version,
created_at, expires_at, revoked_at

调用方传回 handle_id,服务器以当前认证上下文读取并做乐观锁或事务更新。state_version 还能防止模型根据旧上下文对已变化对象执行操作:若预期版本与当前版本不一致,返回冲突并要求重新读取,而不是盲目覆盖。

状态的显式引用也改善审计。日志可以分别记录“谁调用”“操作哪个对象”“从哪个 MRTR phase 继续”“关联哪个 Task”,而不是把所有事实压进 Session ID。但任何模型可见 ID 都要做日志脱敏与最小披露;可观测性不能反过来成为 handle 和 taskId 的泄漏通道。


5. MRTR:无服务器主动请求后,如何完成确认与补参#

5.1 从服务器 Push 改成客户端重提原请求#

旧协议允许服务器在持有的 SSE 流上主动发出 elicitation/createsampling/createMessageroots/list。假设工具调用由副本 A 执行,反向请求由 A 发出,而客户端响应经另一条通路抵达副本 B,系统就必须共享相关性状态,或保证整段交互始终回到 A。

现代 MRTR 取消了这条服务器到客户端的独立 JSON-RPC request channel。服务器需要额外输入时,先用 InputRequiredResult 结束当前请求;客户端完成用户确认、模型采样或 Roots 获取后,以新的 JSON-RPC ID 重发原方法。

MRTR 通过新请求 ID、requestState 与 inputResponses 完成跨实例五步闭环

云数据库案例的第一轮响应可以是:

{
"jsonrpc": "2.0",
"id": "call-0192",
"result": {
"resultType": "input_required",
"inputRequests": {
"confirm_quote": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "该规格预计每月 127 美元,是否继续创建?",
"requestedSchema": {
"type": "object",
"properties": {
"confirmed": { "type": "boolean" }
},
"required": ["confirmed"]
}
}
}
},
"requestState": "eyJ2ZXJzaW9uIjoxLCJraWQiOiJrMjAyNi0wOCJ9..."
}
}

inputRequests 是由服务器分配字符串 key 的 map,key 在本次结果内必须唯一;值只能是当前协议定义的 Elicitation、Sampling 或 Roots 请求。InputRequiredResult 仅能出现在 tools/callprompts/getresources/read,而且至少包含 inputRequestsrequestState 之一。服务器不得请求客户端没有在当前请求能力中声明的特性;若完成操作必须依赖该能力,应返回 -32021,HTTP 状态必须为 400,并在 data.requiredCapabilities 中说明缺失能力。

客户端确认后,用新 ID 重试同一个方法,并原样带回状态:

{
"jsonrpc": "2.0",
"id": "call-0193",
"method": "tools/call",
"params": {
"name": "create_database",
"arguments": {
"account_handle": "acct_h_K91...",
"region": "us-west-2",
"size": "db.m7g.large"
},
"inputResponses": {
"confirm_quote": {
"action": "accept",
"content": { "confirmed": true }
}
},
"requestState": "eyJ2ZXJzaW9uIjoxLCJraWQiOiJrMjAyNi0wOCJ9...",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": { "form": {} },
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
},
"io.modelcontextprotocol/clientInfo": {
"name": "agent-host",
"version": "1.8.0"
}
}
}
}

这不是原 HTTP 请求恢复,而是独立的新请求。JSON-RPC ID 必须改变;收到 inputRequests 时,客户端必须先构造对应输入再重试;requestState 必须逐字节回传,客户端不得解析、修改或拼接。服务器未返回 requestState 时,客户端不得自行添加;这两个字段只属于原请求的当前重试,不能附到并行的其他请求。服务器也不得假定客户端一定会完成输入或再次调用。第二轮可以落到副本 C,只要 C 能验证自包含状态,或根据其中的显式引用访问共享状态。

多轮 MRTR 应被实现为明确状态机,而不是根据“这次出现了哪些回答”猜阶段:

START
-> awaiting_region
-> awaiting_quote_confirmation
-> ready_to_submit
-> COMPLETE

官方 TypeScript SDK 的 modern driver 与 legacy shiminputResponses 采用 replace-not-accumulate:每轮只携带当前 inputRequests 的回答;此前已经接受的 region、报价版本等由受保护的 requestState 或服务器记录承接。还要设置最大轮数,避免恶意服务器或错误业务逻辑把客户端困在无限交互中。

5.2 requestState 是攻击者可控输入,不是可信 Session#

“客户端不得读取”只规定客户端行为,不提供密码学保密。明文 JSON 做 base64url 后仍能被恶意客户端解码。规范要求:只要 requestState 会影响授权、资源访问或业务逻辑,服务器就必须保护完整性并拒绝验签失败的状态;只有篡改最坏也只会导致该请求失败时,完整性保护才可以省略。规范同时建议绑定 authenticated principal、短 TTL、原方法与关键参数摘要;若要求状态最多消费一次,服务器必须保存并执行单次消费约束。

一个可审计的参考载荷是:

payload = {
state_schema_version,
phase,
state_id,
principal_id,
tenant_id,
method,
salient_args_digest,
input_request_keys,
operation_intent_id,
idempotency_ref,
quote_id,
quote_version,
issued_at,
expires_at
}
protected_segment = b64url(canonical({
token_format_version: 1,
kid
}))
body_segment = b64url(canonical(payload))
tag = HMAC-SHA-256(
K[kid],
"mcp-request-state\0" ||
protected_segment || "." || body_segment
)
requestState = protected_segment || "."
|| body_segment || "."
|| b64url(tag)

下面是本文推荐的生产级 Token Profile,不是 MCP wire schema,也不是七条额外的协议 MUST:

  1. 验证端先限长解析 protected segment,只为从本地白名单选择 kid;随后对传输中的精确 protected/body segment 重算 MAC,并做常量时间比较,验签通过后才解析业务 payload。不能让 Token 自由指定任意算法或密钥来源。
  2. HMAC 只保护完整性,不保护报价、内部策略或 PII 的机密性;敏感内容应改用 AEAD,或只放服务器状态记录 ID。
  3. salient_args_digest 必须基于确定性编码,例如 RFC 8785 JCS,并明确缺字段与 null、数值、Unicode、数组顺序及被排除 _meta 的规则。
  4. 当前请求仍要重新执行实时授权。签名只能证明“服务器过去签发过这份状态”,不能证明权限没有被撤销。
  5. 改用 AEAD 时,protected segment 应进入 AAD;多副本共享 AEAD 密钥时,密码学 IV/nonce 必须在同一密钥下保持唯一。它只保证加密安全,不阻止业务请求重放。
  6. state_id / jti 才是业务重放标识;若要求严格单次消费,必须在服务端持久层原子记录已消费状态,并至少保留到 expires_at + clock_skew。纯无状态 Token 无法保证 one-time use,提前清理消费记录会重新打开 replay 窗口。
  7. 验证端还要限制 Token 长度、解码深度和处理时间。正常轮换时只用新 key 签发,旧 key 保留验证到 last_issued_at + max_ttl + clock_skew 后销毁;若 key 疑似泄露,或算法、验证路径失陷,则必须立即撤销、让关联状态 fail closed,并要求用户重新发起流程,不能继续等 TTL。requestState MAC、AEAD、幂等键与缓存 Token 指纹必须使用独立密钥,或通过 HKDF 做用途域分离。

5.3 MRTR 没有 exactly-once,副作用必须另做幂等#

客户端在这些场景都会重试:响应流中断、确认结果已提交但最终响应丢失、网关超时、调用方主动恢复。JSON-RPC ID 不能作为幂等键,因为每个 MRTR 回合必须换 ID。

幂等键需要同时绑定稳定业务事实与“一次逻辑操作”的意图边界。本文建议用无歧义的确定性编码,并做用途域分离:

I = HMAC(
K_idem,
"mcp-idempotency\0" ||
canonical({
version: 1,
operation_intent_id,
principal,
tenant,
method,
salient_args,
plan_id,
quote_version
})
)

operation_intent_id 由服务器为一次逻辑副作用生成,并受保护地携带在 requestState 中;同一次重试保持不变,用户明确发起第二次参数完全相同的操作时则生成新值。否则,一个“只看参数”的幂等键会把两次合法创建永久误合并。K_idem 是幂等用途的专用密钥,不能与 requestState MAC、AEAD 或缓存指纹密钥复用。

生成 II 后应立即把它物化为持久记录键,并将受保护的 idempotency_ref 放进 requestState;重试解析该引用,而不是用“当前” K_idem 重新计算。若实现确实选择重算,则状态必须携带 idem_kid,旧 K_idem 与幂等记录都要保留到约定的重试和对账窗口结束。否则确认与重试之间的一次密钥轮换就可能生成新 II,绕过唯一约束。

但“算出同一个字符串”还不构成幂等。持久层必须对 II 建唯一约束,原子创建 PENDING 记录并选出唯一执行者;副作用与记录需要处于同一事务,或通过 transactional outbox 衔接;下游云 API 若支持幂等键应继续透传。重复到达的请求返回同一 task/result,而不是再次创建数据库。

对于“云资源已创建、网络响应丢失”这类不确定结果,系统必须按幂等键或外部 operation ID 查询恢复。更准确的承诺是:在受控系统边界内实现 effective-once 或 at-most-once,而不是声称跨任意第三方 API 获得端到端 exactly-once。

5.4 decline、cancel、缺参和多轮上限都属于协议正常路径#

MRTR 不是只有 accept。Elicitation 结果可能表示拒绝或取消;客户端也可能根本不重试。服务器在发出 input_required 前不应提交依赖确认的不可逆副作用,并且必须允许临时状态自然过期。用户拒绝费用时,逻辑状态应进入明确的 DECLINED 或结束分支,而不是再次返回相同确认,制造循环诱导。

客户端提交的 inputResponses 仍是不可信输入。服务器应校验 map 结构、每个结果类型及接受内容的 Schema;服务器 SHOULD 忽略不认识或不需要的额外 key,缺少完成操作所必需的回答时 SHOULD 返回新的 InputRequiredResult,而不是报错或把缺失值默认为同意。多轮流程还应同时限制:

  • 最大回合数;
  • 每轮 inputRequests 数量与 Schema 复杂度;
  • 单轮与总交互时长;
  • requestState 最大字节数;
  • 同一主体的并发未完成流程数。

这些限制不改变 wire,却决定服务器会不会被恶意客户端用大量半完成确认拖垮。对于模型自动满足 Sampling 或 Elicitation 的 Host,也应保留策略层:高风险工具不能因为“客户端声明支持 elicitation”就绕过真实用户授权。Capability 只说明技术上会处理该请求,不等于安全政策允许自动批准。

滚动升级时,requestState.version 应由显式 decoder 分派。新副本至少在最大状态 TTL 内读取旧版本;无法安全迁移时返回可操作的“状态已过期,请重新发起”错误,而不是用宽松解析猜字段。这样副本 A 签发的旧状态才能在部署期间由副本 C 验证,同时避免永久保留旧密钥和旧业务规则。


6. subscriptions/listen:通知流被约束为一条有边界的长请求#

6.1 长连接仍然存在,但不再承载全局 Session#

现代 MCP 有三种消息模式:普通 request/response、MRTR、subscribe/notify。subscriptions/listen 属于第三种:客户端发送一条长请求,服务器在该请求的响应流中推送客户端明确订阅的通知。状态属于这条 request,不属于底层 TCP 连接,更不等于 Agent 对话。

{
"jsonrpc": "2.0",
"id": "sub-7",
"method": "subscriptions/listen",
"params": {
"notifications": {
"toolsListChanged": true,
"promptsListChanged": false,
"resourcesListChanged": true,
"resourceSubscriptions": [
"db://accounts/acct_h_K91/databases"
]
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "agent-host",
"version": "1.8.0"
}
}
}
}

服务器对该 subscription ID 发送的第一条消息必须是确认;它可以只接受过滤器的一个子集:

{
"jsonrpc": "2.0",
"method": "notifications/subscriptions/acknowledged",
"params": {
"_meta": {
"io.modelcontextprotocol/subscriptionId": "sub-7"
},
"notifications": {
"toolsListChanged": true,
"resourcesListChanged": true,
"resourceSubscriptions": [
"db://accounts/acct_h_K91/databases"
]
}
}
}

后续通知都携带相同 subscriptionId,它就是打开 listen 请求的 JSON-RPC ID。stdio 上多条订阅共用一条通道,因此“ack 必须最先”是每个 subscription ID 内的顺序,不是全通道的第一条消息;其他订阅的消息可以交错。

客户端 SHOULD 将 acknowledged filter 与自己请求的 filter 比较;服务器未接受的通知类型必须按不支持处理,不能继续等待一个永远不会到来的事件。

6.2 请求进度与目录变化必须分流#

  • notifications/progress 与请求相关的 notifications/message 只跟随原请求响应流;
  • notifications/tools/list_changednotifications/prompts/list_changednotifications/resources/list_changed 和被订阅资源的 notifications/resources/updated 进入 listen 流;
  • 服务器不得发送过滤器中未被客户端请求的通知类型。

把某个 tools/call 的进度塞入全局 listen 流会破坏相关性;反过来,把目录变化塞入任意业务调用的响应流,会让未发起该调用的缓存消费者永远收不到失效信号。

6.3 断流后重新订阅,不再做事件续传#

现代 Streamable HTTP 不支持 SSE event ID 与 Last-Event-ID。业务请求响应流断开后,以新 JSON-RPC ID 重新请求,并依赖应用幂等。HTTP listen 异常断开时,客户端 MAY 将其视为重连触发;若仍需订阅,就发起新的 subscriptions/listen。stdio 连接重建后,客户端 MUST 重发 subscriptions/listen。不存在“从事件 183 自动补齐”的协议保证。

因此,对安全敏感或授权相关的目录,重连后应主动全量刷新,而不是只等未来通知。反向代理需要关闭响应缓冲、设置合理 keep-alive 与空闲超时;服务器可以发送 SSE comment 作为保活,但客户端必须忽略它。

HTTP 客户端关闭 SSE 响应流即表示取消该请求,服务器应尽快停止工作且不得继续发送消息;stdio 则通过 notifications/cancelled 指向原请求 ID。服务器主动优雅结束订阅时,应返回原请求的空 complete 结果,然后关闭流:

{
"jsonrpc": "2.0",
"id": "sub-7",
"result": {
"resultType": "complete",
"_meta": {
"io.modelcontextprotocol/subscriptionId": "sub-7"
}
}
}

客户端可据此区分“正常关闭”与“传输异常”。后续若仍需通知,需要创建新的订阅请求;旧请求不会跨连接复活。

6.4 通知是失效提示,不应成为唯一事实来源#

没有 Last-Event-ID 后,本文建议生产客户端采用 state reconciliation,而不是 event replay 思维。这是工程恢复策略,不是额外的 MCP wire requirement。通知的正确含义通常是“你缓存的某类状态可能变了,请在需要时重新读取”,而不是“只要逐条消费事件就能在本地重建服务器真相”。

这对目录缓存尤其重要。收到 notifications/tools/list_changed 时,先把相关 entry 标记 stale;下一次真正需要目录时调用 tools/list。若订阅在通知与断线之间丢失,重连后的主动刷新会重新收敛。若客户端仅应用“新增工具 X”这类本地增量,而协议通知本身又没有可恢复 offset,就可能永久漏删或漏改。

多副本服务器也需要统一事件来源。副本 C 修改目录,却只向本地内存订阅者广播,连接在副本 A 的客户端不会收到通知。实现可以使用 Event Bus、数据库变更流或平台 Pub/Sub,但这属于应用基础设施;MCP 只规定 listen 请求、过滤器、关联与关闭语义,并不提供跨副本消息总线。

对权限撤销,通知更不能替代执行阶段检查。即使客户端仍缓存着旧工具目录,服务器也必须在 tools/call 时用当前 Token 与策略拒绝操作。目录决定“模型看到什么”,授权决定“服务器允许什么”,两层不可合并。


7. MCP 为什么开始具备数据平面的工程属性#

7.1 Mcp-MethodMcp-Name 把策略事实暴露给网关#

每个现代 Streamable HTTP JSON-RPC request POST 都必须有 MCP-Protocol-VersionMcp-Methodtools/callresources/readprompts/get 还必须有 Mcp-Name,其来源分别是 params.nameparams.uri。本修订没有为 notification POST 定义同一套 Header 要求。网关不解析 JSON 就能按方法与工具做路由、限流、WAF 和指标聚合。

但 Header 只是 Body 的镜像,不是第二套真相。后端必须比较二者;缺失、格式错误或不一致时返回 HTTP 400 与 JSON-RPC -32020 HeaderMismatch

{
"jsonrpc": "2.0",
"id": "call-0192",
"error": {
"code": -32020,
"message": "Header mismatch: Mcp-Name does not match params.name"
}
}

否则攻击者可以让网关按 read_database 放行,后端却按 Body 执行 delete_database。Header 名大小写不敏感,Header 值大小写敏感。非 ASCII、首尾空格或匹配 sentinel 的 Mcp-Name 必须采用规范规定的 =?base64?...?= 编码。

工具 inputSchema 还可以用 x-mcp-header 把选定的原始参数镜像成 Mcp-Param-*,用于区域、租户路由等场景。例如 "x-mcp-header": "Region" 使对应参数进入 Mcp-Param-Region。该注解值必须是非空 HTTP header token,并在同一工具内按大小写不敏感方式保持唯一;它只能标注沿静态 properties 路径可达、最终类型为 stringintegerboolean 的字段,不能用于 number、数组、组合关键字或经 $ref 才能到达的路径。对 Streamable HTTP 而言,注解非法的工具必须从客户端可用的 tools/list 中排除。

任何处理 Body 的服务仍必须解码并验证 Header 与 Body 一致。中间件也只应在 MCP-Protocol-Version 表明该版本强制后端校验时信任这些镜像;旧版本或缺版本的流量应拒绝或走独立安全路径。

7.2 缓存的是明确结果,不是“某个 Session 看过的目录”#

Caching 规范 要求六类 complete 结果携带 ttlMscacheScope

  1. server/discover
  2. tools/list
  3. prompts/list
  4. resources/list
  5. resources/templates/list
  6. resources/read

最小缓存键是 method 加所有影响结果的 params。生产实现还应加入 endpoint、部署修订与协议版本;私有缓存必须绑定授权上下文:

K_public = H(canonical({
version: 1,
scope: "public",
endpoint,
deployment_revision,
protocol_version,
method,
params
}))
auth_context_fingerprint =
HMAC(K_cache_local, exact_access_token_bytes)
K_private = H(canonical({
version: 1,
scope: "private",
public_key: K_public,
issuer,
resource_audience,
auth_context_fingerprint
}))

不要把原始 bearer token 放入 key 或日志。K_cache_local 是缓存实现自己的专用密钥,不是 MCP Server 的 requestState 密钥;缓存位于客户端、网关或服务端时都应各自管理这一用途密钥。承担授权执行职责的共享 private cache,其有效期不得超过已知 Token expiry;一旦检测到撤销或 issuer、audience、scope 变化,就立即失效。客户端本地缓存可以采用独立保留策略,但不得跨 authorization context 复用;重新验证收到 401/403 时也不能用 stale private entry 掩盖授权失败。

新鲜度判断为:

fresh    tnow<treceived+ttlMs\text{fresh} \iff t_{\text{now}} < t_{\text{received}}+\texttt{ttlMs}

ttlMs 是 freshness hint,不是数据不变性保证,更不是自动轮询周期。现代服务器 MUST 提供 ttlMs >= 0;客户端对 0、旧结果中的缺失值或异常负数都 SHOULD 按立即 stale 处理。客户端不应仅因 TTL 到期就自动轮询;实现若选择轮询,MUST 使用指数退避与抖动:

dn=min(dmax,d02n)+U(0,j)d_n=\min(d_{\max},d_0 2^n)+U(0,j)

cacheScope: public 意味着即使原请求带认证,结果也可能跨 token、跨用户复用;只有真正与用户无关的目录才可这样标记。private 结果不得跨 authorization context,共享缓存至少要把不同 access token 隔离。无论哪种 scope,执行工具与读取对象时仍需真正授权,cacheScope 从来不是 ACL。

此外还有四条常被漏掉的规则:

  • 携带 inputResponsesrequestState 的 MRTR 重试结果不得缓存;
  • 收到相关变化通知后,即使 TTL 未到也 SHOULD 立即标记 stale;
  • 分页每页独立缓存,不保证跨页快照一致,所有页面必须使用相同 cacheScope
  • cursor 失效时 SHOULD 丢弃该列表全部缓存页,从首页重新获取。

7.3 确定性目录同时改善 MCP Cache 与 LLM Prompt Cache#

现代规范要求 tools/list SHOULD 使用确定顺序。即使工具集合没有变化,随机排序也会改变上游模型看到的工具定义前缀,破坏 Prompt Cache 命中并让评测产生无意义波动。确定性排序并不能保证某个模型提供商的缓存一定命中,但它消除了一个完全可避免的字节级不稳定源。

这种优化必须服从隔离:不能为了复用 Prompt Cache,把不同用户的私有工具描述合并成同一公共前缀。先保证目录内容与权限正确,再讨论顺序和缓存。

7.4 Trace Context 取代 Session ID 成为观测主线#

_metatraceparenttracestatebaggage 保留了 W3C 兼容键;这些字段存在时 MUST 符合相应 W3C Trace Context / Baggage 格式。一个 Trace 可以贯穿 Host → MCP Client → Gateway → MCP Server → 云 API,而 handle、taskId、MRTR phase 作为 Span 属性或关联字段。Span 命名、拓扑、属性和采样策略属于实现设计,并非 MCP 规范强制。

这比“所有日志都按 Session ID 聚合”更准确:一次用户任务可能跨多个服务器与业务对象;一条 MCP 连接也可能交错多个任务。Trace 表达因果调用,业务 ID 表达对象,二者都不应被当作授权凭证。尤其要限制 baggage:它会跨服务传播,不能塞 access token、完整 prompt、PII 或未经审核的模型内容。

7.5 无状态核心减少的是协调面,不是所有持久化#

可以把现代 MCP 部署拆成两条路径:

Request data path:
Client -> Gateway -> any MCP replica -> downstream API
Durable coordination path:
handle store / idempotency table / task store / event bus

协议元数据、路由 Header 和一次调用参数都在 data path 上自包含,因此网关不必维护 MCP Session,副本也不必读取“本连接初始化结果”。但浏览器对象、数据库创建记录、幂等状态和 Task 本来就是业务真相,它们仍然进入 durable coordination path。

这一区分解释了为什么官方所说的 plain round-robin 是能力上限,而不是部署承诺:

  • 纯读、无业务状态的工具确实可以直接 round-robin;
  • handle-backed 工具需要所有副本访问一致对象存储,或按 handle 路由;
  • MRTR 若采用完全自包含状态,可由任意副本验签;若状态只含记录 ID,副本仍要访问共享存储;
  • Tasks 需要 durable Task Store 或 taskId-aware routing;
  • Subscription 的网络流固定在一个处理节点上,但目录变化事件必须能到达那个节点。

所以迁移后的容量评估应分别测量无 Session 带来的调度收益,以及 durable store/Event Bus 新增的延迟与可用性。不能把协议简化后的吞吐提升写成固定倍数;真实结果取决于工具延迟、状态访问、缓存命中、授权服务和代理配置。

“Agent 数据平面”的价值也正在这里:消息暴露了路由和观测所需事实,但业务一致性仍由明确的持久化组件承担。协议不再偷偷充当分布式事务协调器。


8. Extensions、Tasks 与 Schema:核心变薄后的边界#

8.1 扩展必须由当前请求双方显式协商#

扩展使用带命名空间的标识符,并在客户端与服务器 capability 的 extensions map 中出现。客户端在每个请求的 io.modelcontextprotocol/clientCapabilities 中声明,服务器通过 server/discover 的 capabilities 声明。只有交集内扩展才能使用;若一方不支持,另一方必须回退核心行为或明确失败,不能静默发送新结果类型。

这条规则的直接后果是:曾在上一请求声明 Tasks,不代表下一请求仍然声明;某个 SDK package 包含扩展类型,也不代表 Host 具备展示、持久化和恢复能力。

8.2 Tasks 解决持久执行,但不属于核心协议#

Tasks 作为 io.modelcontextprotocol/tasks 官方扩展,适合 CI、批处理、云资源创建和人工审批等长任务。当前官方设计只增强 tools/call:服务器在真正持久化 Task 后,才可返回 resultType: "task"taskId、状态、保留 TTL 和建议轮询间隔。客户端随后使用 tasks/get,在 input_required 时用 tasks/update 提交输入,并可用 tasks/cancel 请求协作式取消。

Tasks 扩展的耐久创建、断线恢复、轮询更新与协作式取消状态机

working -> input_required -> working
input_required -> completed | failed | cancelled
working -> completed
working -> failed
working -> cancelled

completedfailedcancelled 是不可逆终态。取消响应只说明服务器接受了取消意图,不保证工作一定停止;并发竞态下任务仍可能进入 completedfailedcompleted 也可以包含 isError: true 的工具结果,因为那仍是一次协议成功的工具返回;failed 指执行中的 JSON-RPC 错误。

Tasks 与普通业务 handle 的安全边界也不同:扩展要求 taskId 具备足够熵;SEP-2663 的安全要求还要求对每个 Task 相关请求重新认证和授权,这包括 tasks/get / update / cancel,也包括携带 taskIdssubscriptions/listen。服务器只能确认并发送当前调用者获权访问的 Task,不能通过错误文本或 ack 泄露其他 Task 是否存在。Task 中的 inputRequests 也不是更高信任通道:Host 必须沿用普通 MRTR 的信任策略。同一个尚未完成的 input request key 可以在连续轮询中再次出现,客户端 SHOULD 去重;服务器 MUST NOT 把同一 key 用于不同请求,也不得在该 key 已获得响应后重新使用。

按当前 Tasks draft,Streamable HTTP 的 Task RPC 使用 Mcp-Name = taskId,Header 与 Body 仍需一致;这属于扩展草案,不是核心 Header 表新增的第四类方法。

“任何请求落到任意副本”对 Tasks 不是免费属性。MRTR 可以靠自包含、受保护的 requestState 跨副本;Task 本身是持久状态,必须放进共享 Task Store,或按 taskId 做确定性路由。服务器发出 CreateTaskResult 前,Task 必须已经持久到任意合法的 tasks/get 能解析,否则响应一丢就会产生幽灵任务。

还要注意当前实现成熟度:MCP 官网把 Tasks 列为 official extension,但其链接的 ext-tasks README 仍标注 Experimental / not official,规范与 Schema 路径仍位于 draft;当前 draft 的缺能力错误仍写旧值 -32003,而最终核心协议已经使用 -32021。官方 TypeScript SDK v2 的 modern typed maps 不包含新的 tasks/*,现代连接收到这些入站方法会得到 -32601。因此不能因为核心 SDK 已支持 2026-07-28,就假定 Tasks 已开箱可用。上线时必须固定扩展实现版本,核对 Schema、错误码和客户端支持矩阵,并把扩展测试从核心 conformance 中单独列出。

8.3 MRTR、Tasks 与应用级 Job Handle 如何选#

条件MRTRTasks应用级 Job Handle
数秒内补参数或确认
当前调用可以结束后重提
跨断线、客户端重启继续
分钟到小时执行
标准化状态、轮询、中途输入
对方不支持 Tasks 扩展

MRTR 的目标是把一次逻辑调用拆成若干独立回合;Tasks 的目标是给持续存在的执行建立 durable handle。两者可以组合:tools/call 经 MRTR 完成费用确认,再返回 Task;Task 之后也可以进入 input_required。但每层都有自己的 ID、状态机和授权,不能拿 requestState 代替长期 Task Store。

8.4 Schema 能力增强,也扩大了验证器攻击面#

现代 MCP 将 JSON Schema 2020-12 作为无显式 $schema 时的默认方言,工具 inputSchema / outputSchema 可使用相应关键字,structuredContent 也可承载任意 JSON。实现方必须支持 2020-12,并对不支持的显式方言返回清楚错误;Tool inputSchema 的根仍必须是 type: "object"

同时:Base Protocol 规定实现可以提供联网 $ref resolver,但 MUST 默认关闭;选择性开启时,SHOULD 使用主机 allowlist、拒绝私网/loopback/link-local、设置超时与大小限制并记录解析 URI。无法解析的 external ref SHOULD 被拒绝,而不是静默当成宽松 Schema。anyOfoneOfallOfif/then/else 和深层 $defs 还可能制造 Schema Bomb,因此验证器 SHOULD 限制最大深度、子 Schema 数量和单次验证时间。

旧特性的 Removed / Deprecated 边界与替代方向已集中列在 1.2 节;这里不再重复。迁移时尤其不要因为 Tasks 和 Schema 是第 8 章主题,就把它们与仍在弃用窗口内的 Roots、Sampling、Logging 混成同一成熟度。


9. 授权强化:无状态不等于没有安全上下文#

9.1 认证主体必须来自可验证凭据,而不是连接或自报字段#

MCP Authorization 是 HTTP 实现可采用的组件,不是所有 MCP 部署的强制层。支持授权的 HTTP 客户端与服务器应遵循 MCP 的 OAuth 要求;stdio 不应套用同一套网络授权流程,通常从受控环境获取凭据。无论哪种传输,核心原则相同:移除初始化后,不能把“这条连接以前认证过”或“clientInfo.name 看起来可信”当作当前调用的授权依据。

每个受保护请求都要验证:

token validity
∧ intended audience/resource
∧ issuer and authorization transaction
∧ scope
∧ tenant
∧ object-level permission
∧ current policy and revocation state

显式 handle 和 requestState 只负责引用或携带状态。服务器仍需把 handle 与当前 authenticated principal、tenant 和 scope 联合查询;MRTR 状态通过验签后仍需检查权限是否在确认期间被撤销。

9.2 Token audience 与上游 API Token 必须分离#

Authorization Security Considerations 要求客户端在授权请求和 Token 请求中使用 RFC 8707 resource 参数,MCP 服务器必须验证 access token 确实为自己签发。收到一个可被其他 API 接受的 Token 并不等于拥有转发权。

当 MCP Server 再调用云厂商 API 时,它扮演另一个 OAuth client,应为上游获得独立 Token;不能把 MCP Client 给它的 Token 原样 passthrough。否则一个权限较宽的 MCP 入口会变成 confused deputy,也会绕开资源级 audience 限制。

9.3 RFC 9207 iss 阻断授权服务器 mix-up#

一个客户端可能同时连接多个 MCP Resource Server,也可能被引导到多个 Authorization Server。攻击者控制其中一个 AS 时,会尝试让客户端把诚实 AS 签发的 authorization code 送到恶意 AS。RFC 9207 的 iss 参数用于把授权响应绑定到发起事务时记录的 issuer。

客户端应把经过验证的 issuer 与 PKCE verifier、state 一起存入授权事务,并在兑换 code 之前做 simple string comparison:

AS Metadata响应 iss客户端行为
声明支持 iss存在且匹配继续
声明支持 iss缺失拒绝
未声明支持存在且匹配继续
未声明支持存在但不匹配拒绝
未声明支持缺失按兼容规则继续,并依赖其他保护

比较时不要自行做 host 大小写折叠、默认端口消除、尾斜杠修剪或百分号解码归一化。iss 不匹配时,即使响应携带 OAuth error,也不应继续处理来自错误 issuer 的字段。

9.4 Credential Store 必须按 issuer 分区,CIMD 是例外路径#

预注册凭据和通过 Dynamic Client Registration 获得的 client credentials 都绑定签发它们的 Authorization Server。授权服务器变化后,客户端不得复用旧凭据,应重新注册或报错。因此仅按 MCP endpoint 缓存 {client_id, client_secret, token} 不够,至少要纳入 issuer、resource audience、client identity 和授权主体。

Client ID Metadata Document(CIMD)不同:client_id 本身是客户端托管的 HTTPS Metadata URL,由不同 AS 按需获取,因此 issuer 改变时无需重新注册同一个 CIMD client ID。可迁移的是这枚 URL,不可跨 issuer 混用的是 token、PKCE/state 授权事务,以及 pre-registration / DCR 凭据。

Client Registration 给出的优先级是:

  1. 已有预注册信息;
  2. AS 声明支持时使用 CIMD;
  3. 仅为兼容回退到 DCR;
  4. 都不可用时让用户提供配置。

DCR 已弃用,但存量客户端仍必须正确设置 application_type:桌面、CLI、本地应用通常为 native,远程 Web 应用为 web,否则 OIDC 服务器可能因 localhost redirect 约束拒绝注册。

CIMD 的抓取防护位于授权服务器一侧,不是 MCP 客户端。Client Identifier URL 必须使用 HTTPS、包含 path,且不得含 userinfo、fragment 或 . / .. path segment;非 200 响应一律视为错误,抓取 Metadata 时不得自动跟随 HTTP redirect。Metadata 必须是合法 JSON,且至少包含 client_idclient_nameredirect_uris;其中 client_id 必须与抓取 URL 精确匹配,redirect URI 也必须精确匹配。生产环境不得抓取解析到 special-use IP 的 Metadata URL 或文档内引用 URL,还要防 DNS rebinding,并限制响应大小与读取时间;错误响应以及无效或畸形文档不得缓存。CIMD 不得包含 client_secret、私钥或任何依赖共享对称密钥的客户端认证方式,只能发布公钥材料。

9.5 把新攻击面放回正确的信任边界#

资产 / 边界主要攻击规范底线生产加固
Header / Body网关按 Header 放行、后端按 Body 执行一致性校验,失败 400/-32020网关只信任要求校验的现代版本,后端仍按真实参数授权
业务 handleIDOR、枚举、跨租户复用跨请求状态使用显式 ID(tenant, principal, handle) 对象授权、TTL、吊销、审计
requestState篡改、跨主体/方法重放、过期审批影响业务时完整性保护AEAD、短 TTL、实时再授权、state_id 消费表
taskId枚举、越权 get/update/cancel/subscribe扩展要求足够熵并对 Task 相关请求授权对象授权、订阅过滤、日志脱敏、Task ACL、共享 Store 或确定性路由
Cachepublic 误标、private 跨 Token 命中private 不得跨授权上下文完整 Key、Token 指纹、scope/issuer 变化失效
OAuthissuer mix-up、audience 混淆、Token passthroughissresource、audience 校验issuer 复合存储键、PKCE/state 事务绑定
Subscription断流漏通知,缓存长期陈旧不承诺事件恢复重连后主动刷新关键目录
Era fallback故障或攻击诱导降级识别现代错误后不回退成功 Modern 后锁定/告警,关键环境显式 pin

这里最重要的判断是:协议把状态显式化,不会自动把状态变可信。 恰恰因为状态现在出现在参数、Header、Token 和 handle 中,每个边界都更容易被观测,也必须更明确地验证。


10. 贯穿案例:把云数据库创建迁移为跨副本可恢复流程#

10.1 旧实现的问题不是“用了内存”,而是生命周期全部混在一起#

案例约束如下:create_database 需要账号、区域和规格;执行前必须确认月度报价;真实创建耗时数分钟;客户端可能断线;服务端有多个副本。

旧实现把 account、配置草稿、报价和用户确认状态放进初始化 Session,工具列表在账号选定后动态变化,副本 A 通过 SSE 主动发 elicitation/create。后续请求靠 Mcp-Session-Id 与 sticky routing 回到 A。若 A 在确认后、提交前退出,客户端很难判断应重建 Session、重发确认还是查询已有资源。

10.2 现代设计先拆出四种真实对象#

  1. account_handle:长期业务对象,服务器存储并逐调用授权;
  2. requestState:短时确认状态,绑定账号、参数摘要、报价版本、主体与过期时间;
  3. idempotency_key:提交副作用的稳定业务键,在持久层唯一;
  4. taskIdprovisioning_job_id:长任务句柄,独立于 MRTR 回合。

工具目录始终包含 create_databaseget_provision_status 等固定工具,不再因连接内“是否选择账号”变化。认证主体来自当前 access token;account_handle 只选择对象,不证明权限。

10.3 第一次调用落到副本 A#

客户端可先使用已缓存的 server/discover / tools/list,再发送完整现代 tools/call。Gateway 根据 Mcp-Method: tools/callMcp-Name: create_database 做限流和初筛,后端再次验证 Header/Body、Token audience、scope 及 account 所有权。

副本 A 查询实时价格,生成 quote_idquote_version,但不创建数据库。它返回 input_required 与“预计每月 127 美元,是否继续”的 Elicitation,同时签发短时 requestState。此时 A 即使立即退出,交互状态也没有只留在 A 的内存中。

10.4 确认重试落到副本 C#

客户端以新 JSON-RPC ID 重发原 tools/callinputResponses 表明接受报价,requestState 字节不变。副本 C 执行:

  1. 限长、解析 protected header,拒绝未知 kid
  2. 验证 HMAC/AEAD、有效期、principal、tenant、method 与参数摘要;
  3. 重新检查 account 权限和当前策略;
  4. 检查报价是否仍有效;
  5. 计算稳定幂等键并原子写入 PENDING
  6. 只有唯一执行者向云 API 提交创建。

如果相同确认同时到达 C、D,唯一约束保证只有一个提交者。若云 API 成功而客户端未收到响应,重试会查询同一幂等记录与外部 operation ID,而不是创建第二个数据库。

10.5 长任务用 Tasks,兼容客户端用 Job Handle#

若双方显式协商并实际安装兼容的 Tasks 扩展,服务器在 Task 已持久化后返回 resultType: "task";客户端持久保存 taskId,按 pollIntervalMs 调用 tasks/get,也可订阅任务通知。Task Store 必须可由合法副本读取,或基础设施按 taskId 路由,不能再依赖创建 Task 的 C 一直存活。

若客户端不支持 Tasks,核心兼容路径是返回普通 provisioning_job_id,并提供 get_provision_status(job_id) / cancel_provision(job_id) 工具。它少了标准化 Task 状态,但仍满足无协议 Session 与跨断线恢复。

资源目录变化后,服务器在 subscriptions/listen 上发送相应通知,客户端让 resources/list 或相关 read 缓存失效。初始调用、MRTR 重试、Task 轮询与下游云 API 通过 Trace Context 关联,但审计仍记录 principal、account_handle、quote_id、idempotency_key 与 taskId,各司其职。

10.6 必须证明的不是 happy path,而是这些失败路径#

  • 用户 decline/cancel 后,不得继续提交或循环索要确认;
  • requestState 过期、篡改、跨用户或跨参数重放必须失败;
  • 确认后权限被撤销,实时再授权必须阻止创建;
  • 副本 A 返回 input_required 后退出,副本 C 仍可继续;
  • 同一 state_id 并发提交到 C/D,只发生一次副作用;
  • 外部创建成功、最终 HTTP 响应丢失,重试返回同一任务;
  • Tasks 未协商或实现版本不兼容,正确回退 Job Handle;
  • 订阅断流不影响 Task 继续,重连后通过全量读取恢复真实状态。

只有这些路径通过,才能说系统从“连接耦合”迁移到了“显式、可恢复状态”,而不只是删除了一个 Header。


11. 分阶段迁移 Playbook:先改内部状态,再切 wire#

11.1 第一步是依赖盘点,不是升级 SDK#

在代码、网关、测试和文档中搜索并分类:

Terminal window
rg -n 'initialize|notifications/initialized|Mcp-Session-Id|Last-Event-ID'
rg -n 'resources/(subscribe|unsubscribe)|logging/setLevel|ping'
rg -n 'elicitation/create|sampling/createMessage|roots/list'
rg -n 'session(Id|Store|Map)|sticky|affinity'
rg -n 'tasks/(result|list)|tasks/get|tasks/update|tasks/cancel'
rg -n 'client_secret|registration_endpoint|issuer|token_cache'

搜索结果分为四类:

  • SDK 机械变化:类型、入口、方法名;
  • 业务建模变化:Session-keyed state、动态目录、对象 handle;
  • 基础设施变化:sticky LB、GET SSE、Event Bus、缓存;
  • 安全变化:Header 校验、对象授权、状态完整性、issuer-bound Store。

Codemod 只能处理第一类。官方 TypeScript v1→v2 codemod 也明确不自动完成 requestState、MRTR、modern handler 与版本探测这些架构迁移。

11.2 正确顺序是先让业务处理器与连接历史解耦#

建议顺序:

  1. 把 Session-keyed 业务对象迁到 handle-keyed durable store;
  2. 为副作用建立幂等记录、唯一约束和恢复路径;
  3. 把服务器主动请求重构为显式 MRTR 状态机;
  4. requestState 加版本化、完整性/机密性保护与轮换;
  5. 将目录/资源变化接入可跨副本的 Event Bus;
  6. 稳定工具排序,标注正确 TTL 与 cache scope;
  7. 让所有 handler 从当前请求获取身份、能力和 Trace;
  8. 最后才解除 sticky routing,逐步下线 Session Store。

如果先关闭 Session,再重构业务状态,最常见结果不是“请求自然无状态”,而是对象丢失、重复扣费与跨副本空指针。

11.3 Dual-era 应在入口分流,不能在一次请求中混语义#

同一 endpoint 或进程可以同时服务两种 era:现代请求根据每请求 _meta 进入 stateless path;initialize 选择 Legacy path。传输适配层可以为两者复用同一套已解耦业务 handler,但一个请求一旦分类,就不能一半依赖 Session、一半依赖 modern state。

Dual-era 入口的时代识别、共同版本选择、Modern 灰度扩展、新流量回滚与既有 Tasks 排空

TypeScript SDK v2 的现代 HTTP 入口是 createMcpHandler(factory),stdio 则是 serveStdio(() => buildServer());直接将手工 Server 连接到旧 StdioServerTransport 不会因依赖升级而自动变成 Modern。生产发布必须把 wire 抓包与 SDK 配置一起验收。

客户端通常提供三种策略:

  • auto:先探测现代,按严格条件回退;
  • pin 2026-07-28:现代专用环境、CI 与高安全部署;
  • legacy:存量路径,明确记录技术债。

auto 不能把 401/4035xx、真实网络中断、HTTP timeout 或已识别现代错误当作 Legacy 证据;浏览器 opaque CORS/preflight TypeError 仅按 3.4 所述作为 SDK 兼容例外,高安全环境仍应 pin 版本。一个 origin 曾成功使用 Modern 后,突然出现无法识别的 400,应告警并调查代理/攻击,而不是静默降级。

11.4 迁移状态机与退出门槛#

Legacy-only
-> Dual-era
-> Modern-default
-> Modern-only

每一步都保留回滚到上一已知安全应用版本的能力。回滚不是自动切 Legacy wire;如果问题属于越权、缓存串租户或状态验签失败,应 fail closed、关闭受影响操作或回滚代码,不能用旧协议绕过安全检查。

回滚版本还必须能前向读取仍存活的 requestState 格式及验证密钥,以及已有 idempotency、outbox 和 Task 记录。做不到时,应先停止接收新流程,并排空或显式失效在途工作,再回滚;不得删除 state_id 消费记录、PENDING / 外部 operation ID 或 Task 状态。正常轮换的旧验证 key 保留到 last_issued_at + max_ttl + clock_skew,但不得继续用于签发;疑似泄露或验证路径失陷时则立即撤销,并让关联流程 fail closed。否则旧版本可能把“已执行但响应丢失”误判为“尚未执行”,重新触发副作用,或重新打开 replay 窗口。

移除 Legacy 基础设施前至少满足:

  • Legacy 流量低于已定义门槛并有客户端名单;
  • 2026-07-28 核心 conformance 通过;
  • MRTR 跨副本、重复提交与滚动密钥测试通过;
  • Tasks 或 Job Handle 的恢复测试通过;
  • 没有 Session-keyed 业务状态和动态连接目录;
  • public/private 缓存隔离与通知重连验证完成;
  • sticky routing 依赖指标为零;
  • 能在有限时间内回滚应用版本,而不恢复不安全的状态模型。

12. 验证矩阵:上线判据必须覆盖协议、状态与安全#

12.1 Era × Transport 矩阵#

ClientServer期望
Modern-onlyModern成功;不支持版本时按 -32022 协商
Modern-onlyLegacy明确失败,不伪装成功
Dual-eraModern保持 Modern,不执行初始化
Dual-eraLegacy合法探测后走 initialize
LegacyDual-era使用 Legacy path
LegacyModern-only返回可操作诊断,不自动“向前兼容”
Dual-eraDual-era两套 wire 独立通过,不共享隐式状态

HTTP 与 stdio 必须分别执行。HTTP 的状态码、SSE 取消和 Header 校验无法由 stdio 测试覆盖;stdio 的进程探测、同通道多订阅解复用也无法由 HTTP 结果代表。

12.2 核心报文断言#

  • protocolVersionclientCapabilities → HTTP 400 / -32602
  • 缺当前操作必需能力 → HTTP 400 / -32021
  • 不支持版本 → HTTP 400 / -32022,携带 supportedrequested
  • Header/Body 缺失或不一致 → HTTP 400 / -32020
  • 未知现代 RPC → HTTP 404 / -32601
  • 现代请求携带旧 Mcp-Session-Id → 服务器忽略且不回传;
  • 现代 GET/DELETE MCP endpoint → 405;
  • 旧成功结果无 resultType → 客户端按 complete
  • 未协商扩展却出现未知 resultType → 拒绝,而非猜结构。

12.3 MRTR、幂等与跨副本 Chaos Test#

  • 强制第一轮与重试落到不同副本;
  • 篡改 requestState 任意一字节,测试未知 kid、截断 tag、超长 Token;
  • 跨 principal、tenant、method、args 重放与过期重放;
  • 状态签发后撤销权限,再提交确认;
  • 密钥滚动升级:旧 key 在窗口内可验证,窗口后拒绝;
  • 在确认与重试之间轮换 K_idem,重试仍命中原幂等记录;
  • 多副本并发签发 AEAD 状态,断言同一 key 下密码学 nonce 不重复;
  • 测试 Token/Header 版本冲突、非规范 JSON、重复 key、非法 base64url 与用途密钥误配;
  • 同一 state_id 向两个副本并发提交,断言只有一个副作用;
  • 消费记录不得早于 expires_at + clock_skew 清理;模拟提前清理后重放,测试必须暴露该缺陷;
  • 副作用成功但最终响应丢失,重试返回同一任务;
  • 分别在写入 PENDING 后、下游已接受但 operation ID 尚未持久化时、Task 已持久但响应尚未发送时杀进程;重放 outbox 后仍不得产生第二次副作用;
  • inputResponses 缺失、额外、未知 key,以及用户 decline/cancel;
  • 超过最大 MRTR 轮数,客户端可控地终止。

期望不是“每次都返回 200”,而是无越权、无本地 Session 前置条件、受控系统边界内重复副作用为零。

12.4 Cache、Subscription 与 Task 专项#

  • public 必须由数据流和授权策略证明与调用者无关;两个用户结果逐字段相同只能作为测试样本,不能单独证明可跨用户共享;
  • 不同 access token 的 private 结果不能命中同一缓存;
  • Token 过期或撤销、issuer/audience/scope 变化以及 401/403 时,private cache 不得返回 stale;
  • 参数缺失与 null、Unicode、数值、cursor 不产生 Key 碰撞;
  • 相关 list-changed 到达时,fresh entry 立即失效;
  • 多页 cacheScope 不一致被识别为服务端违规;
  • ack 是其 subscription ID 内第一条消息,服务端只接受过滤器子集时客户端能降级;
  • 订阅异常断流后重连并主动刷新,不假定遗漏事件补发;
  • 当前用户不能读取、更新或取消其他用户的 taskId;
  • 未授权 taskIds 订阅不能通过 ack、错误或通知泄露 Task 存在性;
  • 同一未完成的 Task input request key 跨轮询重复出现时客户端去重;同一 key 被用于不同请求或在响应后复用时判定服务器违规;
  • Task cancel 与完成竞态、Task Store 延迟复制、TTL 清理都进入测试;
  • Task RPC 的 Mcp-Name 与 Body taskId 不一致时拒绝;
  • 带存活 requestState、幂等记录、outbox 与 Task 的跨版本回滚保持可读且不重复执行。

12.5 OAuth/CIMD 安全矩阵#

  • RFC 9207 的 iss 存在/缺失/匹配/不匹配组合;
  • issuer 仅大小写、端口或尾斜杠不同也不视为相同;
  • well-known Metadata 中 issuer 与查询 issuer 不一致;
  • 一个 Resource Server 发布多个 AS,Token 与 DCR Store 不串 issuer;
  • DCR issuer 改变后重新注册,CIMD client ID 可跨 AS;
  • Token audience 错误与上游 Token passthrough 被拒绝;
  • CIMD URL 指向 loopback、link-local、云 Metadata、DNS rebinding、302、超大响应或错误 client_id
  • CIMD 返回 200 但 JSON 畸形,或缺少 client_idclient_nameredirect_uris 任一必填字段;
  • PKCE 能力缺失、state 不匹配、redirect URI 不精确匹配时拒绝。

12.6 Conformance 是下限,不是生产证明#

官方框架可以列出并执行特定修订的核心要求:

Terminal window
npx @modelcontextprotocol/conformance list \
--requirements 2026-07-28
npx @modelcontextprotocol/conformance server \
--url http://localhost:3000/mcp \
--requirements 2026-07-28

Dual-era 服务还应分别执行 2025-11-252026-07-28 requirement set,因为相同场景在两个 wire 上的握手和字段不同。

但通过核心 requirement set 只回答“核心必需场景是否合规”。Tasks 等扩展可能被报告为 not_scored: extension,Authorization Server 本身也不由核心 Resource Server 测试完整覆盖。还要单独执行 Tasks 生命周期/扩展套件、真实 AS 集成、跨租户隔离、幂等与 Chaos Test。

12.7 上线指标与回滚#

至少按 era 分开观测:

  • 协商成功率、回退率与回退原因;
  • -32020/-32021/-32022/-32602 错误率;
  • MRTR 平均/最大轮数、过期率、验签失败与重复副作用;
  • 跨副本连续处理成功率;
  • 工具目录缓存命中率、失效原因与 Prompt Cache 变化;
  • Subscription 异常关闭、重建与重连后全量刷新率;
  • Task 轮询延迟、终态分布、取消竞态与孤儿任务;
  • sticky affinity 实际依赖;
  • Modern 与 Legacy 各自的 P50/P95/P99。

以下情况应 fail closed 或回滚应用版本:跨租户缓存命中、对象级越权、requestState 验证绕过、重复收费/创建、Header/Body 策略不一致。它们都不应触发“自动改走 Legacy”,因为降级 wire 无法修复业务安全错误,还可能扩大攻击面。


结语:真正被移除的是隐式状态,不是状态本身#

MCP 2026-07-28 最重要的变化,不是少了一次 initialize,也不是少维护一张 Session 表,而是协议不再允许实现者用“这条连接以前发生过什么”解释当前请求。

完成迁移后的系统应当能回答五个清楚的问题:

  1. 当前协议事实来自哪个请求字段?——_meta
  2. 当前业务对象由什么引用、谁有权操作?——handle 与对象级授权;
  3. 当前交互回合如何继续、如何防篡改与重放?——MRTR 与受保护 requestState
  4. 长任务如何跨断线存在?——Tasks 或明确的 Job Handle;
  5. 目录变化如何通知、缓存如何失效?——subscriptions/listen、TTL 与 scope。

当这些答案都不依赖某个副本的连接记忆时,普通负载均衡、共享缓存和分布式追踪才真正可用。相应地,幂等、授权、密钥、任务持久化与事件恢复也必须被设计成一等公民。

所以这次迁移的验收标准不应是“Mcp-Session-Id 已从抓包消失”,而应是:任意合法副本都能仅根据当前请求和被显式引用的持久状态,安全、可重复验证地推进同一项工作。


一手资料与延伸阅读#

MCP 2026-07-28 深度迁移指南:从会话耦合协议到无状态 Agent 数据平面
https://jupiter-ws.cn/posts/ai-coding/mcp-2026-stateless-agent-data-plane/
作者
Jupiter
发布于
2026-08-13
许可协议
CC BY-NC-SA 4.0