MCP 转向无状态:把服务端迁移到 2026-07-28 规范

·阅读约11分钟·Evergreen Tools Team

2026-07-28 版 Model Context Protocol 规范已于 7 月 28 日发布,在协议层把 MCP 变为无状态。initialize 握手与会话头被移除,elicitation 变成带 requestState 的多轮往返请求,路由改由 Mcp-Method 头承载,列表结果可缓存,一级 SDK 同步更新。本文给出迁移路径,附前后对比代码。

全球网络

协议层的无状态化

一、头条:无状态的协议内核

MCP 最初是双向、有状态的协议:客户端初始化会话、协商能力,并在多次调用之间保持该会话存活。2026-07-28 规范把它变成请求/响应式协议。维护者将其描述为开发者呼声最高的一项改动——人们想要更好的可靠性与可扩展性——并且特意选择了一次干净的分割,让未来的修订无需重写传输层或生命周期代码即可演进。发布说明还引入了正式的弃用策略与扩展框架。实际效果是:水平扩展不再需要粘性路由与共享会话存储,而这两者恰是 MCP 部署中最常见的隐蔽生产事故来源之一。

// BEFORE 2026-07-28: a stateful initialize handshake
const session = await client.initialize({
  protocolVersion: "2025-06-18",
  clientInfo: { name: "my-agent", version: "1.0.0" },
  capabilities: { elicitation: {} },
});
// client and server capabilities live on `session`
const tools = await session.request("tools/list", {});

二、握手没了

initialize 握手被移除。客户端能力改为在每次请求中通过 _meta 与请求头声明,服务端能力则通过 server/discover 调用获取,而不再在连接时一次性协商。这是服务端作者最大的一处代码改动,而且是机械性的:你不再把协商状态存在会话对象上,而是从每次请求的元数据里读取协议版本与客户端信息。代码示例 1 是旧形态,代码示例 2 是新形态。回报是:同一客户端的两个请求可以落到不同实例而不出问题,因为已经没有会话可丢。如果你的服务端此前在调用之间保留了什么内存状态,现在它应该放进请求里、放进以请求为键的缓存里,或者放进外部存储。

// AFTER 2026-07-28: no handshake, per-request metadata
const meta = {
  "protocol-version": "2026-07-28",
  "client-info": { name: "my-agent", version: "1.0.0" },
  "client-capabilities": { elicitation: {} },
};
const server = await client.request("server/discover", {}, { meta });

三、Elicitation 变成多轮往返

交互式提示过去默认存在一个活跃会话。新规范用多轮往返请求流取代 elicitation。服务端不再保持会话开启,而是返回一个 InputRequiredResult 和一个唯一的 requestState 标识;客户端收集到用户输入后,发送一个携带原请求 id 与所收 requestState 的新请求,服务端据此重建自己当时在做什么。代码示例 3 勾勒了这一交互。它比实时提示多了一点仪式感,却稳健得多:这次交互能挺过服务端重启、滚动发布,或者负载均衡把后续请求送到另一个实例——这些都是有状态提示挺不过去的。

// Elicitation is now a multi round-trip request, not a live session.
const first = await client.request("tools/call", {
  name: "book_flight",
  arguments: { from: "SFO" },
});
if (first.InputRequiredResult) {
  const { requestState, prompt } = first.InputRequiredResult;
  const answer = await askUser(prompt);
  const done = await client.request("tools/call", {
    name: "book_flight",
    arguments: { from: "SFO", returnDate: answer },
    requestState,             // server reconstructs its own state
    requestId: first.id,
  });
}

四、请求头路由与可缓存列表

有两个特性悄悄改变了部署方式。第一,请求携带 Mcp-Method 头,并可通过 Mcp-Name 做基于名称的路由,网关因此无需解析请求体即可按方法转发;你的服务端应当接受并校验这些头。第二,列表结果现在可缓存:在 tools/list 与资源读取上输出 ttlMs 与 cacheScope,客户端与网关即可缓存。代码示例 4 给出了形态。两者结合,让一队完全相同的无状态实例可以坐在普通负载均衡之后,并把大部分发现类流量从缓存中服务出去。这正是这个版本真正关乎的运维差异:MCP 服务端现在长得像你其余的网络基础设施,而不再像一个只有某位工程师才看得懂的特殊长生命周期进程。

// Emit ttlMs and cacheScope so gateways and clients can cache discovery.
return {
  tools: TOOLS,
  ttlMs: 60_000,
  cacheScope: "public",
};

五、迁移你的服务端

升级分四步。第一,更新到会说 2026-07-28 的 SDK 版本;TypeScript、Python、Go、C# 的一级 SDK 已随规范同步更新。第二,删除握手与会话处理,改为每次请求从 _meta 读取协议版本与客户端信息。第三,在列表操作上输出 ttlMs 与 cacheScope,并校验 Mcp-Method 与 Mcp-Name。第四,把任何内存中的会话状态替换为请求作用域数据或外部缓存。代码示例 5 把整个服务端压缩为一个无状态处理器。破坏性变更是真实的:请把它当成「准备就绪」而非「今天全部拆掉」,在过渡窗口内同时运行两个协议版本,并借助新的弃用策略规划其余部分。授权加固也一并落地,所以顺手把 OAuth 流程也复查一遍。

// A whole MCP server in a Worker: no session store, no sticky routing.
export default {
  async fetch(req, env) {
    const method = req.headers.get("Mcp-Method");
    const name = req.headers.get("Mcp-Name");
    const meta = JSON.parse(req.headers.get("Mcp-Meta") || "{}");
    const body = await req.json();
    const handler = ROUTES[method];
    if (!handler) return new Response("unknown method", { status: 404 });
    return Response.json(await handler({ name, meta, body, env }));
  },
};

六、为什么这不只关乎 MCP

无状态是一种成熟度信号。一个需要粘性会话的协议,是你在边缘跑不了、无法廉价扩缩、出事时也说不清楚的协议。MCP 用了约十八个月成为 AI 与工具集成的事实标准,而这个版本正是标准长大成人后要做的事:删掉那些不能扩展的聪明设计。同一直觉也适用于你自己的 Agent 基础设施:如果技术栈中某个组件必须在调用之间记住什么,先问它是否真的应该。2026 年 7 月 28 日之后,答案越来越多是「不应该」。服务端渲染界面与长时间运行 Tasks 也作为正式扩展到来,这意味着 MCP 服务端真正有趣的表面已经变成用户界面与持久化工作,而不再是连接管理。

服务器机架

负载均衡后平滑扩缩

开发者编码

握手已成历史

📌 常见问题 FAQ

2026-07-28 版 MCP 规范是什么?

自发布以来最大的一次修订:在协议层把 MCP 变为无状态,移除 initialize 握手,新增多轮往返请求、基于请求头的路由、可缓存列表结果、授权加固、正式扩展框架与弃用策略。

它是破坏性变更吗?

是。规范于 2026 年 7 月 28 日发布且包含破坏性变更,因此早期文章应视为「准备就绪」的指引,而非要求你今天就把一切拆掉。

什么取代了 elicitation?

多轮往返请求流:服务端返回带唯一 requestState 标识的 InputRequiredResult,客户端再发送携带原请求 id 与该状态的后继请求,服务端据此重建自己的工作。

客户端现在如何声明能力?

通过在每次请求中携带 _meta 与请求头,而非一次性握手;服务端能力则通过 server/discover 调用获取。

哪些 SDK 已更新?

TypeScript、Python、Go、C# 的一级 SDK 已随规范发布;MCP 服务端现在可以只跑在一个 Cloudflare Worker 里,无需任何有状态基础设施。