主页
握手没了,会话 ID 没了,MCP 从「有状态双向协议」变成「请求/响应无状态协议」。这份文档讲清:为什么要改、改了什么、怎么迁移、哪里有坑。
The handshake is gone, the session ID is gone: MCP has turned from a stateful bidirectional protocol into a stateless request/response protocol. This document covers why it changed, what changed, how to migrate, and where the traps are.
MCP(Model Context Protocol,让 AI 应用接入外部工具和数据的开放协议)在 2026 年 7 月 28 日发布了新版规范。官方数据:Tier 1 SDK 每月下载量已接近 5 亿次,TypeScript 与 Python SDK 累计均破 10 亿次——这已经是 Agent 生态的水电煤。这次升级做了一件核心的事:把协议核心改成无状态(stateless),即服务器不再需要记住「这个客户端之前和我说过什么」,每个请求自带全部上下文。
MCP (Model Context Protocol — the open protocol that connects AI applications to external tools and data) shipped a new specification on July 28, 2026. Official numbers: Tier 1 SDKs see close to half a billion downloads per month, with both the TypeScript and Python SDKs past 1 billion total — this is the plumbing of the agent ecosystem. The release does one central thing: it makes the protocol core stateless — servers no longer need to remember "what this client said to me before"; every request carries its full context.
八个要点:①核心无状态,initialize 握手与 Mcp-Session-Id 移除;②协议版本与客户端能力随每个请求放进 _meta,可选的 server/discover 按需查询服务器能力;③跨调用状态改用「显式 handle」(工具返回一个 ID,后续调用传回来);④新增 HTTP 头 Mcp-Method / Mcp-Name,网关不解析 JSON 就能路由;⑤交互式调用改用 MRTR(多轮请求);⑥扩展框架转正,Tasks、MCP Apps 成为正式扩展;⑦Roots、Sampling、协议 Logging 弃用(至少保留 12 个月);⑧授权加固,DCR 弃用、转向 CIMD。
Eight takeaways: ① stateless core — the initialize handshake and Mcp-Session-Id are removed; ② protocol version and client capabilities travel in _meta on every request, with an optional server/discover call for up-front capability discovery; ③ cross-call state moves to explicit handles (a tool returns an ID; later calls pass it back); ④ new HTTP headers Mcp-Method / Mcp-Name let gateways route without parsing JSON; ⑤ interactive calls use MRTR (Multi Round-Trip Requests); ⑥ a formal extensions framework, with Tasks and MCP Apps as official extensions; ⑦ Roots, Sampling, and protocol Logging are deprecated (kept for at least 12 months); ⑧ authorization hardening — DCR deprecated in favor of CIMD.
MCP 诞生于 2024 年底,最初的典型场景是桌面应用在本机拉起一个子进程,通过标准输入输出(stdio)通信。在这个世界里,「先握手、再基于连接维持一个会话」几乎没有成本——反正就一条连接、一个进程。
MCP was born in late 2024. Its original setting was a desktop app launching a local subprocess and talking over standard input/output (stdio). In that world, "shake hands first, then keep a per-connection session" cost almost nothing — there was one connection and one process anyway.
远程 MCP 改变了经济账。服务器发一个 Mcp-Session-Id,客户端每次请求都带上它;只要服务器往会话里存了任何东西,后续请求就必须落到同一台实例,或者所有实例共享一个会话存储。于是水平扩容意味着:粘性路由(sticky routing)、共享状态、以及一个「懂 MCP」的网关。社区迁移指南把这叫作「为了维持协议会话而交的分布式系统税」。更麻烦的是,各家实现对 session 的语义理解并不一致——有的每个聊天开一个会话,有的一个用户共享一个,有的进程崩溃就直接丢弃——把应用逻辑建在这上面本来就脆弱。
Remote MCP changed the economics. The server would issue an Mcp-Session-Id and the client returned it with every request; the moment a server stored anything against that session, follow-up requests had to reach the same instance — or every instance had to share a session store. Horizontal scaling then meant sticky routing, shared state, and a gateway that "understood MCP." Community migration guides call this the distributed-systems tax paid just to keep protocol sessions alive. Worse, implementations never agreed on session semantics — one client opened a session per chat, another shared one per user, a third dropped it on any crash — so application logic built on sessions was fragile from the start.
旧架构的「会话税」 vs 新架构的普通负载均衡(依据 MCP 官方博客绘制)
The "session tax" of the old architecture vs plain load balancing in the new one (drawn from the official MCP blog)
官方对新方向的概括是「按需付费的复杂度」(pay-as-you-go complexity):核心请求模型保持精简,状态、UI、长任务这些能力只在应用需要时通过扩展加回来。社区已经开始把这次升级俗称为「MCP v2」——官方按日期给规范定版,并没有 v2 这个名字,但这个说法足见变化之大。
The official framing for the new direction is "pay-as-you-go complexity": keep the core request model lean, and add state, UI, or long-running work back through extensions only where an application needs them. Developers have taken to calling this release "MCP v2" — officially the spec is versioned by date and there is no v2, but the nickname says a lot about the size of the change.
旧协议里,一切从 initialize / initialized 握手开始:客户端先自报家门,服务器回协议版本和能力清单,之后的请求都挂在这条「关系」上。新协议里(依据 SEP-2575 / SEP-2567),每个请求都是自包含的:协议版本、客户端身份、客户端能力全部放进请求的 _meta 字段随行;想提前了解服务器能力,调一个新增的 server/discover RPC 即可——但它不是必须的。
In the legacy protocol everything started with the initialize / initialized handshake: the client introduced itself, the server replied with protocol version and capabilities, and all later requests hung off that relationship. In the modern protocol (per SEP-2575 / SEP-2567), every request is self-contained: protocol version, client identity, and client capabilities travel in the request's _meta field. A client that wants capabilities up front can call the new server/discover RPC — but it is optional.
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"search","arguments":{"q":"otters"},
"_meta":{"io.modelcontextprotocol/clientInfo":{"name":"my-app","version":"1.0"}}}}
示例来自 MCP 官方博客(2026-07-28);实际开发请使用 SDK 生成的类型,不要手写这个结构。
Example from the official MCP blog (2026-07-28); in real code, use your SDK's generated types rather than hand-writing this shape.
去掉协议会话不等于你的应用不能有状态。官方推荐的替代模式朴素到有点无聊:工具返回一个 ID,后续调用把它当参数传回来。比如购物车工具第一次调用返回 basket_id: "bsk_a1b2c3",之后的 add_item 显式带上这个 ID。官方博客说,实践中这比藏在传输层里的会话状态效果更好——模型「看得见」这个 handle,能自己在多个工具间穿针引线。
Removing protocol sessions doesn't mean your application can't have state. The officially recommended replacement is deliberately boring: a tool returns an ID, and later calls pass it back as an argument. A cart tool's first call returns basket_id: "bsk_a1b2c3"; subsequent add_item calls carry that ID explicitly. The official blog notes this works better in practice than session state hidden in the transport — the model can see the handle and thread it between tools.
显式状态还更有表达力:设想一个编排 Agent 带三个调研子 Agent,它们该共用同一个购物车、但各自需要独立的浏览器上下文。会话模式下你只能「全共享」或「全隔离」二选一;handle 模式下,编排者可以共享一个 basket_id、同时发三个不同的 browser_id。handle 还能跨对话续用、交给子 Agent、同时保持两份文档打开——这些都是隐藏会话做不到的。
Explicit state is also more expressive. Picture an orchestrator with three research subagents: they should share one shopping cart but each needs an isolated browser context. Under sessions you had to pick between sharing everything and isolating everything; with handles, the orchestrator shares one basket_id while issuing three different browser_id values. Handles can also survive into later conversations, be handed to subagents, or keep two documents active at once — none of which hidden session state allowed.
原来挂在 initialize 上的逻辑(预热缓存、建限流桶、加载租户配置)也要搬家:进程级的放进程启动或首次使用时懒加载;用户级的从认证后的请求上下文来;持久状态给显式 ID。另一个别忽视的点:无状态化之后,幂等性(idempotency,重复执行不产生重复效果)必须自己做——写操作提交了但 HTTP 响应丢了、客户端重试,没有去重就会发布两次。对不可逆的工具,官方社区建议接受 idempotency_key 参数并存储结果去重。
Logic that used to hang off initialize (cache warming, rate-limit buckets, tenant config) needs a new home: process-wide things move to process startup or lazy first use; user context comes from authenticated request state; durable state gets explicit IDs. And don't skip this one: with statelessness, idempotency (repeat execution must not repeat the effect) is your job — if a write commits but the HTTP response is lost and the client retries, you publish twice without deduplication. For irreversible tools, the community guidance is to accept an idempotency_key argument and return the stored result on repeats.
老问题:工具执行到一半需要用户输入(确认、补一个参数)怎么办?旧协议靠一条常开的双向流,让服务器反向发请求(elicitation / sampling)。无状态协议里这条流没了,取而代之的是 MRTR(Multi Round-Trip Requests,多轮往返请求,SEP-2322):服务器返回 resultType: "input_required" 和它需要的问题清单,客户端拿到用户答案后,带着 inputResponses 重发原调用。一次逻辑操作,跨了两个(或多个)独立的 HTTP 请求。
The old question: what if a tool needs user input mid-call (a confirmation, a missing parameter)? Legacy MCP relied on a held-open bidirectional stream so the server could send requests back (elicitation / sampling). That stream is gone. In its place: MRTR (Multi Round-Trip Requests, SEP-2322) — the server returns resultType: "input_required" plus the questions it needs answered, and the client retries the original call with the answers attached in inputResponses. One logical operation now spans two (or more) independent HTTP requests.
MRTR:服务器「要输入」不再靠反向长连接,而是让客户端带答案重试(依据 SEP-2322 绘制)
MRTR: "the server needs input" no longer requires a reverse long-lived connection — the client retries with answers (drawn from SEP-2322)
真实落地的例子:Supabase 在官方博客的合作方评价中说,他们的 MCP 服务器本来就是无状态部署,以前想做 elicitation(执行前和用户确认,比如「新建项目要花这些钱,确认吗」「这个查询会删数据,确认吗」)一直做不了,MRTR 直接解锁了这件事。
A concrete adopter: in the release's partner quotes, Supabase notes its MCP server always ran stateless, which made elicitation (confirming with the user before acting — "this new project will cost X, proceed?", "this query deletes data, confirm?") impossible before; MRTR unlocks exactly that.
这次升级里最讨运维喜欢的三件事,都指向同一个方向:让 MCP 流量变成「普通的 HTTP 流量」,现有的网关、CDN、WAF、追踪系统直接可用。
The three changes operators like most all point the same way: make MCP traffic behave like ordinary HTTP traffic, so existing gateways, CDNs, WAFs, and tracing systems just work.
Mcp-Method(镜像 JSON-RPC 方法名),tools/call / resources/read / prompts/get 还要带 Mcp-Name。网关不解析 JSON 请求体就能按工具路由、按工具限流、按操作记指标。代价是一条新的校验边界:头和体不一致必须拒绝(HTTP 400 + HeaderMismatch 错误码 -32020),否则网关按头授权、后端按体执行,就是一个协议混淆漏洞。tools/list 等响应带上 ttlMs(可缓存多久)和 cacheScope(public / private)。工具目录不再随连接变化,编排器给多个子 Agent 复用同一份目录,省流量也省 token。服务器要按确定性顺序返回工具;用户相关的目录绝不能标 public。_meta 里规范化传递 traceparent / tracestate / baggage,一次 Agent 动作可以进同一条 OpenTelemetry 兼容的分布式追踪。注意 baggage 不是杂物袋:限制大小、白名单键名、不放凭证。Mcp-Method (mirroring the JSON-RPC method); tools/call / resources/read / prompts/get also carry Mcp-Name. Gateways can route per tool, rate-limit per tool, and record operation metrics without parsing JSON bodies. The price is a new validation boundary: header/body disagreement must be rejected (HTTP 400 + HeaderMismatch, -32020) — a gateway that authorizes on the header while the backend executes the body is a protocol-confusion bypass.tools/list and friends carry ttlMs (freshness) and cacheScope (public / private). Tool catalogs no longer vary per connection, so an orchestrator can reuse one catalog across subagents — saving both wire traffic and tokens. Servers should return tools in deterministic order; never mark a user-specific catalog public.traceparent / tracestate / baggage now propagate through _meta in a standardized way, so one agent action can join a single OpenTelemetry-compatible trace. Baggage is not a junk drawer: bound its size, allowlist keys, keep credentials out.MCP 正式确立扩展框架:扩展有命名空间标识、通过能力协商启用、独立演进版本。两个核心官方扩展:MCP Apps(2026 年 1 月 26 日成为首个官方扩展,本次并入正式框架)让服务器提供交互式 HTML 界面,宿主在沙箱 iframe 里渲染;Tasks(io.modelcontextprotocol/tasks)承载长任务——一次 tools/call 可以返回一个 task handle,客户端用 tasks/get 轮询、tasks/update 更新、tasks/cancel 协作取消;tasks/list 已移除。曾用 2025-11-25 实验版 Tasks 的注意:这是「真正的移植」,不是改个 import。长任务还要自己做配额、过期清理、幂等创建——否则「创建便宜、执行昂贵」的任务就是一个现成的 DoS 原语。
MCP now has a formal extensions framework: namespaced identifiers, capability-negotiated activation, independent versioning. Two central official extensions: MCP Apps (the first official extension since January 26, 2026, now folded into the formal framework) lets a server ship an interactive HTML UI that the host renders in a sandboxed iframe; Tasks (io.modelcontextprotocol/tasks) carries long-running work — a tools/call can return a task handle which the client drives via tasks/get (polling), tasks/update, and cooperative tasks/cancel; tasks/list is removed. If you built on the experimental 2025-11-25 Tasks: this is a real port, not an import rename. Long-running work also needs quotas, expiry/cleanup, and idempotent creation — otherwise "cheap to request, expensive to run" tasks are a ready-made DoS primitive.
授权是实现者花时间最多的地方,这次的加固包括:客户端必须校验授权响应的 iss 参数(RFC 9207,堵住授权服务器混淆漏洞,SEP-2468);客户端凭证绑定签发它的 issuer,不得跨授权服务器复用(SEP-2352);DCR(Dynamic Client Registration,动态客户端注册)正式弃用,转向 CIMD(Client ID Metadata Documents:client ID 就是一个指向元数据文档的 URL,授权服务器自己去读,免去逐个注册)——DCR 向后兼容仍可用,但未来版本会移除。
Authorization is where implementers spend the most integration time. This round hardens it: clients must validate the iss parameter in authorization responses (RFC 9207, closing an authorization-server mix-up hole, SEP-2468); client credentials are bound to the issuer that minted them and can't be reused across authorization servers (SEP-2352); and DCR (Dynamic Client Registration) is formally deprecated in favor of CIMD (Client ID Metadata Documents — the client ID is a URL pointing to a metadata document the authorization server reads itself, avoiding per-server registration). DCR keeps working for backward compatibility but will be removed in a future version.
| 弃用项 | 原来的角色 | 推荐去向 |
|---|---|---|
| Roots | 客户端提供会话级操作边界(如工作目录) | 工具参数、资源 URI 或服务器配置显式传递;客户端给的路径不等于授权,服务器仍要校验 |
| Sampling | 服务器「借用」客户端的模型做生成 | 服务器直连模型供应商,或改为显式 MRTR 输入流程——这是最贵的一项弃用:凭证、推理成本、数据合规都转到服务器运营方 |
| 协议 Logging | 服务器向客户端流式发结构化日志 | stdio 用 stderr;生产环境用 OpenTelemetry 与常规日志系统 |
| HTTP+SSE 旧传输 | 早期远程传输 | Streamable HTTP(此项走单独的较短下线窗口) |
| Deprecated | Previous role | Recommended direction |
|---|---|---|
| Roots | Client supplies session-scoped operating boundaries (e.g. a working directory) | Pass scope explicitly via tool arguments, resource URIs, or server config; a client-supplied path is not authorization — validate server-side |
| Sampling | Server "borrows" the client's model for generation | Direct provider integration, or explicit MRTR input flows — the most expensive deprecation: credentials, inference cost, and data compliance shift to the server operator |
| Protocol Logging | Server streams structured logs to the client | stderr for stdio; OpenTelemetry plus normal logging in production |
| Legacy HTTP+SSE transport | The early remote transport | Streamable HTTP (this one has its own, shorter removal window) |
另外几处线上变化容易踩空:资源不存在的错误码从自定义 -32002 改为标准 -32602;协议错误分配了 -32020~-32099 区间(-32020 HeaderMismatch、-32021 MissingRequiredClientCapability、-32022 UnsupportedProtocolVersion);工具 schema 升级到完整 JSON Schema 2020-12(支持 oneOf / $ref 等);独立的 HTTP GET 通知流被 subscriptions/listen 订阅流取代,SSE 断流不可续传(Last-Event-ID 不再提供重投递)——这又回到「写操作要自己做幂等」。
A few wire-level changes are easy to trip on: the resource-not-found error moves from custom -32002 to standard -32602; the -32020…-32099 range is now reserved for MCP errors (-32020 HeaderMismatch, -32021 MissingRequiredClientCapability, -32022 UnsupportedProtocolVersion); tool schemas move to full JSON Schema 2020-12 (oneOf, $ref, etc.); and the standalone HTTP GET notification stream is replaced by a subscriptions/listen subscription stream, with SSE resumability gone (Last-Event-ID no longer redelivers) — which again means mutations need application-level idempotency.
先说结论:没有任何东西在 7 月 28 日当天坏掉。旧客户端和旧服务器可以继续用旧协议对话。断裂只发生在「跨代」:只会新协议的一方遇到只会旧协议的一方。官方版本文档定义三类实现:Legacy(用 initialize 握手,2025-11-25 及之前)、Modern(用逐请求元数据,2026-07-28 起)、Dual-era(两代都支持)。要兼容两代,客户端得主动探测对端年代:stdio 上先探 server/discover,超时或不认识就回退 legacy initialize;HTTP 上看 400 响应体是不是「可识别的 modern 错误」再决定回退——注意 401/403/429 不能当作「对面是旧服务器」的证据。
First, the punchline: nothing broke on July 28. Legacy clients and servers keep talking legacy MCP to each other. The break is cross-era: a modern-only side meeting a legacy-only side. The official versioning docs define three categories: Legacy (initialize handshake, 2025-11-25 and earlier), Modern (per-request metadata, 2026-07-28+), and Dual-era (supports both). To bridge eras, a client must actively detect the other side: over stdio, probe server/discover and fall back to legacy initialize on timeout or unrecognized errors; over HTTP, inspect whether a 400 body is a recognized modern error before falling back — and never treat 401/403/429 as evidence of a legacy server.
rg 'Mcp-Session-Id|sessionIdGenerator|initialize|initialized' .——逐条审查:哪些是协议上下文(协议现在自带),哪些该进请求中间件,哪些要落持久存储。Client.connect() 仍可能走旧握手。迁移期钉死 SDK 版本。Mcp-* 头存活;头名不区分大小写但值区分,会「规范化大小写」的代理能悄悄弄坏它。rg 'Mcp-Session-Id|sessionIdGenerator|initialize|initialized' . — review every hit: some is protocol context (now carried by the protocol), some belongs in request middleware, some must move to durable storage.Client.connect() can still perform the legacy handshake. Pin exact SDK versions during migration.Mcp-* headers survive every hop; header names are case-insensitive but their values are case-sensitive, so case-normalizing proxies can silently corrupt them.| 你是谁 | 紧迫度与动作 |
|---|---|
| 本地 stdio 服务器作者 | 低——从容升级 SDK 即可,很多场景只是版本更新 |
| 远程 HTTP 服务器作者 | 高——立刻审计会话、状态、头、鉴权与部署,这是真正的架构迁移 |
| 客户端 / 宿主作者 | 高——优先做年代探测、discover、MRTR、鉴权与缓存 |
| 网关 / 平台 | 中高——测头校验、路由、追踪与混合版本共存 |
| 用了实验版 Tasks / Sampling | Tasks 当成真移植做;Sampling 要当产品与合规决策来规划 |
| You build… | Urgency & action |
|---|---|
| A local stdio server | Low — upgrade deliberately; often just an SDK bump |
| A remote HTTP server | High — audit sessions, state, headers, auth, deployment now; this is a real architectural migration |
| A client / host | High — prioritize era detection, discovery, MRTR, auth, caching |
| A gateway / platform | Medium-high — test header validation, routing, tracing, mixed versions |
| Experimental Tasks / Sampling users | Treat Tasks as a real port; treat Sampling as a product & compliance decision |
_meta 里客户端自报的身份当认证结果(它是不可信输入);⑤ 硬编码 -32002 的断言没改;⑥ 在应用代码里读 resultType(TS v2 SDK 会在返回前消费掉它,不可移植);⑦ 指望 SSE 断线续传(已移除)。
① Upgraded the SDK but never opted in — you think you're modern, you're not; ② observability counts JSON-RPC IDs, so MRTR doubles metrics and distorts latency; ③ gateway authorizes on headers while the backend executes the body (must validate header/body agreement); ④ trusting client-self-reported identity in _meta as authenticated context (it's untrusted input); ⑤ leftover hard-coded -32002 assertions; ⑥ reading resultType in app code (the TS v2 SDK consumes it before returning — not portable); ⑦ expecting SSE resumability (removed).
这是社区(如 r/mcp 的讨论)对这次发布最尖锐、也最合理的质疑:既然远程 MCP 现在长得像普通 HTTP 服务,为什么不直接暴露普通 API?社区迁移指南给出的诚实回答是:有时候确实应该用普通 API——如果只是一个应用调一个已知服务、不需要动态的 Agent 集成。MCP 的价值在 HTTP 之上:面向 Agent 的工具发现、统一的 tool/resource/prompt schema、宿主-客户端-服务器的通用角色、授权发现、能力与扩展协商、MCP Apps、Tasks,以及一份宿主可以统一套上审批与策略控制的集成契约。把传输层变「无聊」正是目的:协调工作在协议层解决一次,而不是每个集成重新发明。
This is the sharpest — and fairest — community pushback (see the r/mcp discussion): if remote MCP now looks like a conventional HTTP service, why not just expose a normal API? The honest answer from the migration guides: sometimes you should — if one application calls one known service and doesn't need dynamic agent integration. MCP earns its keep above HTTP: agent-oriented tool discovery, uniform tool/resource/prompt schemas, common host/client/server roles, authorization discovery, capability and extension negotiation, MCP Apps, Tasks, and a single integration contract that hosts can wrap in approval and policy controls. Making the transport boring is the point: solve the coordination problem once at the protocol level instead of reinventing it per integration.
另一处真实分歧是 Sampling 弃用:支持者认为服务器直连模型供应商能自主控制模型选择与评测;反对者指出这把凭证、推理账单、数据处理责任和企业模型政策全压到了服务器运营方头上。官方立场是把它当「产品、安全与计费决策」而非机械迁移——若只是需要用户输入,MRTR 往往是更好的替代。
The other live disagreement is the Sampling deprecation: proponents say direct provider integration gives server authors control over model choice and evals; critics note it shifts credentials, inference bills, data-handling duties, and enterprise model policy onto server operators. The official stance: treat it as a product, security, and billing decision, not a mechanical migration — and where you merely need user input, MRTR is often the better replacement.