Skip to content

MCP 2026-07-28 规范:从有状态连接走向无状态核心 ​

学习目标:读懂 MCP 第 5 版规范的核心变化,判断自己的客户端/服务端需要改什么,并按清单完成迁移

预计时间:40 分钟

难度等级:⭐⭐⭐⭐☆

最后更新:2026 年 10 月

前置阅读:MCP 协议


一句话概括 ​

2026-07-28 把 MCP 从一个「先握手、再对话」的有状态连接协议,改成了「每个请求自带全部上下文」的无状态 RPC 协议。

被删除的东西曾经的作用现在怎么做
initialize / notifications/initialized连接时协商版本与能力每个请求在 _meta 里带版本与能力
Mcp-Session-Id 头把请求绑到同一个服务端会话服务端签发显式 handle,当普通工具参数传
HTTP GET 端点服务端主动推送变更subscriptions/listen 长连接
resources/subscribe / unsubscribe单资源订阅同上,订阅类型写在请求里
ping保活探测无(无状态协议不关心连接存活)
logging/setLevel会话级日志级别每请求 _meta 的 logLevel
SSE Last-Event-ID断线续传、事件重放断流即丢请求,用新 request ID 重发
服务端主动发 sampling/createMessage 等服务端反向提问MRTR:返回 input_required,客户端带答案重试
tasks(核心)长任务查询官方扩展 io.modelcontextprotocol/tasks

变更清单:2026-07-28 changelog | SEP 索引:modelcontextprotocol.io/seps


版本定位 ​

这是 MCP 的第 5 个主要修订,上一版是 2025-11-25,由 Anthropic 于 2026-07-28 发布。

版本关键特征
2024-11-05初版,HTTP+SSE 传输
2025-03-26Streamable HTTP 取代 HTTP+SSE,加入 OAuth 授权框架
2025-06-18授权细化,elicitation 等能力
2025-11-25会话式 initialize 握手成熟的最后一版
2026-07-28无状态核心、MRTR、订阅重构、扩展机制

两件与「规范怎么演进」有关的事同样重要:

  1. SEP 流程正式化:提案改为 seps/ 目录下的 Markdown 文件,编号由 PR 派生,明确 sponsor 职责,状态用 PR label 管理(SEP-1850)。
  2. 特性生命周期政策落地(SEP-2596):定义 Active / Deprecated / Removed 三态,最短 12 个月弃用窗口,并维护弃用特性注册表。这就是为什么 Roots / Sampling / Logging 这次只是「弃用」而非「删除」。

这是破坏性版本

initialize 移除、服务端主动请求移除、SSE 续传移除都是 breaking change。双轨兼容(dual-era)可行,但必须显式实现,见下文兼容矩阵。


状态模型:连接不再是会话 ​

移除协议级会话 ​

旧规范用 Mcp-Session-Id 头把一串请求绑成一个 session,服务端可以在 session 里存「客户端能力」「已授权范围」「上次列出的工具」。新规范把这条路封死:

  • Streamable HTTP 不再有协议级 session,Mcp-Session-Id 头移除。
  • tools/list、resources/list、prompts/list 的结果不再随连接变化。
  • 需要跨调用状态的服务端,必须签发服务端生成的显式 handle,由客户端每次作为普通工具参数传回(SEP-2567)。

代价换来的是可扩展性:列表结果稳定 = 可缓存 = 能放在网关/CDN 后面 = 可以水平扩容多个无状态实例,负载均衡不需要 sticky session。旧的 session 模型在 serverless 和容器编排环境里几乎无法实现。

handle 模式:让客户端携带状态 ​

json
// 1) 建立上下文,服务端返回不透明 handle
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"workspace_open",
  "arguments":{"path":"/repo"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}
{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","content":[{"type":"text","text":"workspace opened"}],
  "structuredContent":{"workspaceHandle":"ws_7f3a91c2"}}}

// 2) 后续调用把 handle 当普通参数传回
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"workspace_grep",
  "arguments":{"workspaceHandle":"ws_7f3a91c2","pattern":"deprecated"}}}

实现要点:handle 必须对客户端不透明;必须能跨实例校验(签名或共享存储),否则扩容后请求落到别的实例就认不出;必须带过期时间并校验归属,防止 A 用户拿 B 用户的 handle。


无状态核心:每个请求自带上下文 ​

_meta 里的字段 ​

请求没有前导握手(SEP-2575),所以每个请求都要说清「我是什么版本」「我有什么能力」「我是谁」:

json
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
  "name":"search_files","arguments":{"query":"mcp"},
  "_meta":{
    "io.modelcontextprotocol/protocolVersion":"2026-07-28",
    "io.modelcontextprotocol/clientCapabilities":{
      "elicitation":{},
      "extensions":{"io.modelcontextprotocol/tasks":{}}},
    "io.modelcontextprotocol/clientInfo":{"name":"ExampleClient","version":"1.0.0"}}}}
键必需性说明
io.modelcontextprotocol/protocolVersion必需请求使用的协议版本
io.modelcontextprotocol/clientCapabilities必需客户端能力,含 extensions
io.modelcontextprotocol/clientInfo客户端 SHOULD名称与版本
io.modelcontextprotocol/serverInfo服务端 SHOULD(结果 _meta)仅供展示/日志,不用于安全决策
io.modelcontextprotocol/logLevel可选该请求的日志级别

版本协商因此从「握手」变成「逐请求判断」:服务端支持请求里的版本就正常处理;不支持就返回 UnsupportedProtocolVersionError,客户端从 supported 列表里选一个共同版本重试。

json
{"jsonrpc":"2.0","id":1,"error":{"code":-32022,"message":"Unsupported protocol version",
  "data":{"supported":["2026-07-28","2025-11-25"],"requested":"1900-01-01"}}}

HTTP 上版本还会镜像到 MCP-Protocol-Version 头,两处必须一致,不一致返回 400 + HeaderMismatch。

server/discover:服务端 MUST 实现 ​

无状态不等于不可发现。服务端必须实现这个 RPC,一次性宣告支持的版本、能力与身份。

json
// 请求:除了标准 _meta 没有参数
{"jsonrpc":"2.0","id":"discover-1","method":"server/discover","params":{"_meta":{
  "io.modelcontextprotocol/protocolVersion":"2026-07-28",
  "io.modelcontextprotocol/clientCapabilities":{},
  "io.modelcontextprotocol/clientInfo":{"name":"ExampleClient","version":"1.0.0"}}}}

// 响应
{"jsonrpc":"2.0","id":"discover-1","result":{"resultType":"complete",
  "supportedVersions":["2026-07-28"],"capabilities":{"tools":{},"resources":{}},
  "instructions":"This server provides weather and resource utilities.",
  "ttlMs":3600000,"cacheScope":"public",
  "_meta":{"io.modelcontextprotocol/serverInfo":{"name":"ExampleServer","version":"1.0.0"}}}}

客户端调用它是可选的,但两个场景下强烈建议:

  1. 界面展示:一次请求拿到身份与能力,不用分别试探 tools/list、prompts/list、resources/list。
  2. STDIO 兼容探测:STDIO 没有 HTTP 状态码可依赖。先发 server/discover,返回 DiscoverResult 说明是现代服务端;返回不认识的其他错误或超时,说明是旧服务端,回退到 initialize。

兼容矩阵 ​

客户端服务端结果
现代现代正常。server/discover 可选,版本不匹配时按错误码重试
现代旧版失败。STDIO 上应先探测,以便确定性地失败
双轨现代正常,探测返回新式结果
双轨旧版正常,探测失败后回退 initialize
旧版现代失败。STDIO 上 initialize 被当未知方法拒绝
旧版双轨正常,服务端按 initialize 走旧语义
旧版旧版按旧版本规范工作

「双轨服务端」的选择逻辑很简单:请求带现代 _meta → 无状态处理;收到 initialize → 进入旧语义。两套语义可以并存于同一进程、同一端点。另外,「服务端是新是旧」是服务端属性,不是请求属性:判断一次后应缓存(STDIO 缓存到进程结束,HTTP 缓存到 origin),直到假设失败再重新探测。


订阅与通知:subscriptions/listen ​

旧模型有两条通知通道:HTTP GET 端点(服务端主动推)与 resources/subscribe(按资源订阅)。新规范合并成一个请求。

json
{"jsonrpc":"2.0","id":1,"method":"subscriptions/listen","params":{
  "_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"},
  "notifications":{"toolsListChanged":true,"promptsListChanged":false,
    "resourcesListChanged":true,"resourceSubscriptions":["file:///project/config.json"]}}}
字段类型订阅内容
toolsListChangedbooleannotifications/tools/list_changed
promptsListChangedbooleannotifications/prompts/list_changed
resourcesListChangedbooleannotifications/resources/list_changed
resourceSubscriptionsstring[]指定 URI 的 notifications/resources/updated

规则链条:

  • 服务端 MUST NOT 发送客户端没显式订阅的类型。
  • 服务端必须先发 notifications/subscriptions/acknowledged,其 filter 是服务端实际同意的子集(不支持的类型被省略),客户端应比对并优雅处理差异。
  • 该流每条通知都在 _meta 里带 io.modelcontextprotocol/subscriptionId,值即 subscriptions/listen 请求的 JSON-RPC id。STDIO 上所有订阅共用一个通道,客户端必须靠它分流。
  • 一个客户端可以同时开多个订阅。
  • 服务端主动收尾时应先回一个 resultType: "complete" 的响应再关流,响应同样带 subscriptionId——收到是「优雅结束」,没收到是异常断开。

注意:notifications/progress、notifications/message 属于「某次调用的进度/日志」,不走这条 listen 流,而是走该请求自己的响应流。塞进 listen 流,客户端就分不清是哪次调用产生的。


移除 ping / logging/setLevel / roots/list_changed ​

这三个东西都依赖「连接有持续状态」这个前提:

被删替代
ping无。请求能否成功本身就是存活信号
logging/setLevel每请求 _meta 的 io.modelcontextprotocol/logLevel
notifications/roots/list_changedRoots 整体弃用(见迁移建议)

日志语义还收紧了一条:服务端 MUST NOT 对没有携带 logLevel 的请求发 notifications/message。日志变成纯 opt-in——客户端不声张,服务端就不该说话(当然仍可写自己的 stderr 或 OTel)。


MRTR:服务端不再主动发请求 ​

对 SDK 结构影响最大的一条(SEP-2322)。旧模型允许服务端在处理 tools/call 中途反向发 JSON-RPC 请求(roots/list、sampling/createMessage、elicitation/create),客户端答完再继续——这要求连接上存在双向的、有状态的请求关联,与无状态核心直接冲突。

json
// 服务端认为信息不足
{"jsonrpc":"2.0","id":1,"result":{
  "resultType":"input_required",
  "inputRequests":{
    "github_login":{"method":"elicitation/create","params":{
      "mode":"form","message":"Please provide your GitHub username",
      "requestedSchema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}}}},
  "requestState":"AEAD-protected blob"}}

// 客户端收集完,用新的 request id 重发原请求
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
  "name":"create_issue","arguments":{"title":"bug"},
  "inputResponses":{"github_login":{"action":"accept","content":{"name":"octocat"}}},
  "requestState":"AEAD-protected blob"}}

要点:

  • 只有 prompts/get、resources/read、tools/call 三个请求可以返回 InputRequiredResult,其他请求返回它属于违规。
  • inputRequests 的 key 由服务端分配、一次请求内唯一;value 必须是 ElicitRequest / CreateMessageRequest / ListRootsRequest 三者之一。
  • 服务端不得请求客户端没在 clientCapabilities 里声明支持的能力。
  • requestState 对客户端不透明,客户端不得解析或修改;首次结果里有就必须原样回传,没有就不得添加。
  • requestState 会经过客户端,属于攻击者可控输入。服务端若用它做授权或访问控制,必须做完整性保护(HMAC/AEAD)并拒绝校验失败的值;建议把「主体身份 + 短 TTL + 原请求标识摘要」打包进去防重放。

所有结果新增 resultType ​

resultType 现在是必填:普通结果 "complete",MRTR 中间结果 "input_required"。扩展可定义新取值,但必须在能力里宣告。对旧服务端的兼容要求是:客户端必须把缺少该字段的结果当作 "complete",认不出的取值视为非法。

这对 SDK 的影响是结构性的:以前「处理服务端请求」是独立 handler(onSampling、onElicit、onListRoots),现在必须变成「工具调用的重试循环」。运行时若把工具调用包在 for 循环里,改动大概 20 行;若它假设了长连接双向通信,那要重新设计。


tasks 移出核心,成为官方扩展 ​

旧核心的 tasks/* 是试验性能力:tasks/result 阻塞等待,tasks/list 枚举。新规范把它整体搬进扩展 io.modelcontextprotocol/tasks(SEP-2663):阻塞的 tasks/result 改为 tasks/get 轮询;新增 tasks/update 让客户端把补充输入写回任务;移除 tasks/list;服务端可不经客户端预先 opt-in 直接返回任务句柄。

json
{"capabilities":{"tools":{},"extensions":{"io.modelcontextprotocol/tasks":{}}}}

扩展协商规则:一方支持、另一方不支持时,支持方必须回退到核心行为或明确拒绝请求;扩展应文档化自己的 fallback 行为。


传输层:Streamable HTTP 的新约束 ​

断线不可续传 ​

Last-Event-ID 头和 SSE 事件 ID 一起被删:

  • 响应流断了,正在进行的那次请求就丢了。
  • 客户端 MUST 用新的 request ID 重发,不能假装「接着上次的流」。
  • 好处是不用实现事件缓冲与去重;坏处是幂等性要客户端自己兜——重放一个已执行的非幂等工具,可能下两次单。

因此 notifications/progress 只告诉你「跑到哪了」,不再暗示「可以续」。凡是可能被重试的写操作,服务端应支持幂等键(放在工具参数里),而不是指望传输层去重。

强制请求头 ​

头来源何时必需
MCP-Protocol-Version_meta.protocolVersion所有 POST
Mcp-MethodJSON-RPC method所有请求
Mcp-Nameparams.name 或 params.uritools/call、resources/read、prompts/get

设计意图是让网关、负载均衡、可观测系统不解析 body 就能路由与打点,例如按 Mcp-Name 做工具级限流。代价是「头与 body 不一致」成了新的错误来源,所以规范定义了 HeaderMismatch(-32020)与 400 的对应关系。工具参数还可以映射成自定义 HTTP 头(x-mcp-header),用于网关级路由(例如按 tenant_id 参数做租户隔离)。

状态码对应关系:未知方法 → 404 + -32601;版本不支持 → 400 + -32022;头与 body 不一致 → 400 + -32020;通知被接受 → 202 无 body。HTTP 上取消 = 关掉响应流,服务端必须当作该请求的取消;本版核心没有定义客户端到服务端的通知(notifications/cancelled 只在 STDIO 使用)。


缓存:ttlMs 与 cacheScope ​

新的 CacheableResult 给这些结果加了两个必填字段:tools/list、prompts/list、resources/list、resources/read、resources/templates/list,以及 server/discover。其中 ttlMs 是新鲜度提示(毫秒),允许客户端缓存、减少轮询;cacheScope 取 "public" 或 "private",决定共享中间层(CDN/网关)能否缓存。

配套一条 SHOULD:tools/list 应当返回确定性顺序。理由很实际——工具列表通常直接拼进系统提示,顺序稳定,上游 LLM 的 prompt cache 才能命中;顺序随机会让缓存前缀每次都变。

ttlMs 不要随手写一个巨大值:工具列表变了但客户端还用旧缓存,会出现「调用了不存在的工具」这类难查问题。listChanged 通知与 ttlMs 是互补关系,前者主动失效,后者兜底过期。


授权相关变化 ​

变化动作
授权响应带 iss(RFC 9207,SEP-2468)授权服务器 SHOULD 返回;客户端 MUST 在兑换 code 前校验 iss 与记录的 issuer 一致
DCR 必须指定 application_type(SEP-837)客户端注册时显式声明,避免 OIDC redirect URI 冲突
凭证按 issuer 绑定(SEP-2352)客户端 MUST 以 issuer 标识为 key 持久化凭证,MUST NOT 跨授权服务器复用,issuer 变更后必须重新注册
DCR(RFC 7591)被弃用转向 Client ID Metadata Documents。DCR 仍保留,用于兼容不支持 CIMD 的授权服务器

这几条指向同一个威胁模型:混流攻击(把 A 授权服务器的 code 拿去 B 兑换)。凭证不按 issuer 隔离,这个防护就不成立。


Schema 放宽 ​

  • inputSchema / outputSchema 允许使用 JSON Schema 2020-12 的任意关键字(SEP-2106),不再限制在原来的子集。
  • structuredContent 可以是任意 JSON 值,不再限定为 object。
  • 新增 $ref 解析要求,以及组合关键字(allOf/anyOf/oneOf)的资源上限,防止构造解析爆炸的 schema。
  • 未声明 $schema 时默认按 2020-12 处理;实现必须至少支持 2020-12。

自己实现 schema 校验的客户端要注意:$ref 与组合关键字会带来递归与展开成本。规范要求支持它们,但同时给了资源上限——实现时要有深度/节点数保护。


错误码重编号 ​

JSON-RPC 的 -32000 ~ -32099 过去被各实现随意占用,新规范把它切开:

区间归属
-32000 ~ -32019遗留(legacy)。新代码不得分配;除 -32002 外不得假设含义
-32020 ~ -32099规范所有。实现只能发出规范定义的码
错误旧码新码
HeaderMismatch-32001-32020
MissingRequiredClientCapability-32003-32021
UnsupportedProtocolVersion-32004-32022

另外两处:资源未找到从 -32002 改为 -32602(Invalid Params),与 JSON-RPC 对齐,客户端 SHOULD 仍接受旧服务端返回的 -32002;-32042(URL elicitation required,仅 2025-11-25 存在)不再发出。新业务错误码应分配在 JSON-RPC 保留区间之外,避免和规范抢位。


弃用清单与迁移建议 ​

弃用 ≠ 删除。这些特性在弃用窗口内仍然可用,但新实现不该再支持(SEP-2577 / SEP-2596):

弃用项状态迁移方向
Roots12 个月窗口目录/文件通过工具参数、资源 URI 或服务端配置传入
Sampling12 个月窗口服务端直接集成模型厂商 API,不要借客户端当代理
Logging12 个月窗口STDIO 写 stderr;HTTP 用 OpenTelemetry
HTTP+SSE 传输自 2025-03-26 起弃用,现正式归类迁移到 Streamable HTTP
includeContext 的 thisServer / allServers自 2025-11-25 软弃用省略该字段或使用 "none"
OAuth 2.0 DCR(RFC 7591)弃用Client ID Metadata Documents

顺带删除的两个细节:notifications/elicitation/complete 通知,以及 URL 模式 elicitation 的 elicitationId 字段。MRTR 下客户端靠「重试原请求」获知带外交互的结果,服务端主动发的完成信号不再有位置;需要跨重试关联的,把标识编码进自己的 requestState。

Sampling 为什么被放弃:它的初衷是「让服务端借用客户端的模型额度」,但它要求双向长连接、要求工具调用中途停下来问用户,正好踩中无状态化的两条红线。给它 12 个月弃用窗口是给已有实现的缓冲,不是给新实现的选项。


迁移检查清单 ​

客户端侧 ​

#动作判据
1去掉 initialize,改为每请求写 _meta(版本/能力/客户端信息)抓包看不到 initialize
2收到 -32022 时从 supported 里选共同版本重试能用旧版本服务端跑通
3STDIO 上先发 server/discover 判定服务端年代,并缓存结论新旧服务端都不会卡死
4去掉 Mcp-Session-Id 存取;handle 当普通参数传递请求头里没有 session id
5onSampling/onElicit/onListRoots 改成 input_required 重试循环收到 inputRequests 能正确回填
6resultType 解析:缺省视为 "complete",未知值报错新旧服务端混跑无异常
7SSE 断流后以新 ID 重发;非幂等工具加幂等键断流测试不产生重复副作用
8按 ttlMs/cacheScope 缓存;订阅改用 subscriptions/listen 并校验 ack不再使用 GET 端点

服务端侧 ​

#动作判据
1实现 server/discover(MUST),返回版本、能力与 serverInfo新客户端首请求即可发现
2校验每请求 _meta,缺失或版本不支持时返回 -32022非法请求被确定性拒绝
3移除连接级状态:列表与连接无关,跨调用状态改用签名 handle多实例轮询负载均衡下功能正常
4错误码按新编号调整(-32020/-32021/-32022)错误码表与规范一致
5删除 ping/logging/setLevel/roots/list_changed,日志读请求 _meta.logLevel无 logLevel 的请求不再收到 message
6实现 subscriptions/listen:先 ack、filter 取交集、通知带 subscriptionId客户端能正确分流多订阅
7主动请求改为 InputRequiredResult;requestState 做完整性保护 + TTL + 归属校验篡改 requestState 被拒
8tasks 迁到扩展(tasks/get 轮询 + tasks/update)不再依赖 tasks/result/tasks/list

顺序建议就是上表:先做服务端双轨(同时支持现代 _meta 与 initialize),再做客户端切现代,最后删旧路径。反过来做,你会在旧客户端还没升级的那段时间里同时维护两套半成品。


本节小结 ​

✅ 2026-07-28 是 MCP 第 5 个主要修订,核心是无状态:删除 initialize 握手与 Mcp-Session-Id,每个请求自带版本、能力、身份,结果带 serverInfo

✅ 版本协商变成逐请求判断,不匹配返回 UnsupportedProtocolVersionError(-32022);server/discover 是服务端 MUST,兼作 STDIO 年代探测

✅ 跨调用状态改为服务端签发的显式 handle,当普通工具参数传递(SEP-2567);列表接口结果与连接无关,因此可缓存

✅ subscriptions/listen 取代 GET 端点与 resources/subscribe,客户端按类型 opt-in,通知带 subscriptionId;progress/message 仍走请求自身响应流

✅ MRTR(SEP-2322)用 resultType: "input_required" + inputRequests/inputResponses + requestState 取代服务端主动请求,所有结果新增必填 resultType

✅ tasks 移出核心成为官方扩展;SSE 断线不可续传;强制 Mcp-Method/Mcp-Name 头;CacheableResult 引入 ttlMs/cacheScope

✅ Roots / Sampling / Logging 进入 12 个月弃用窗口,HTTP+SSE 与 DCR 也被弃用——按迁移检查清单逐项推进,别按旧流程实现


延伸阅读:MCP 协议基础 | 官方 2026-07-28 changelog


← 上一节:A2UI 实战 | 继续学习:返回模块目录 →

最近更新

基于 MIT LICENSE 许可发布