MCP 无状态迁移实战指南:2026-07-28 规范
手把手教你将 MCP 服务器从会话模型迁移到全新的无状态 2026-07-28 规范。包含前后对比代码、迁移清单和常见问题解答。
上周我把三个生产环境的 MCP 服务器——两个在 AWS、一个在 Google Cloud Run——从旧的会话模型迁移到了新的无状态 2026-07-28 规范。结果:删除了 400 行会话管理代码、下线了一整套 Redis 集群,终于实现了真正的轮询负载均衡,不再需要粘性会话。每个服务大约花了四个小时完成迁移。这篇指南就是我当初希望手边就有的操作手册。
图 1:迁移前后架构对比——左侧是会话绑定模式,右侧是无状态轮询模式。
2026-07-28 规范的核心变化
MCP 2026-07-28 规范 是自 MCP 发布以来最重大的协议变更。主要区别如下:
| 维度 | 旧规范 (2025-03-26) | 新规范 (2026-07-28) |
|---|---|---|
| 会话建立 | initialize/initialized 握手 | 无——请求自描述 |
| 会话追踪 | Mcp-Session-Id 头 | 完全移除 |
| 请求元数据 | 在会话建立时协商 | 每个请求内联 _meta 字段 |
| HTTP 头 | 极简 | Mcp-Protocol-Version、Mcp-Method、Mcp-Name |
| 负载均衡 | 必须粘性会话 | 轮询 / 任意策略 |
| 状态存储 | Redis、Memcached 等 | 协议层不再需要 |
如果你对 MCP 还不熟悉,建议先阅读我们的 MCP 协议详解。
前置条件
开始迁移前,请确认:
- 已有基于旧规范 (2025-03-26 或 2025-06-18) 运行的 MCP 服务器
- 已更新 SDK:
@modelcontextprotocol/sdk@^2.0.0(TypeScript)、mcp-sdk>=2.0.0(Python)、go-mcp/v2(Go) 或McpSdk 2.x(C#) - 有负载均衡器的配置权限
- 有可用的预发布环境
分步迁移指南
第一步:移除会话存储
旧规范要求维护会话状态——通常存在 Redis 或内存中。2026-07-28 让每个请求自描述,协议层不再需要这套基础设施。
迁移前 (TypeScript):
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
interface McpSession {
clientCapabilities: ClientCapabilities;
serverCapabilities: ServerCapabilities;
protocolVersion: string;
createdAt: number;
}
async function getSession(sessionId: string): Promise<McpSession | null> {
const data = await redis.get(`mcp:session:${sessionId}`);
return data ? JSON.parse(data) : null;
}
async function createSession(sessionId: string, session: McpSession): Promise<void> {
await redis.set(`mcp:session:${sessionId}`, JSON.stringify(session), 'EX', 3600);
}
迁移后 (TypeScript):
// 会话存储已完全移除。
// 不再依赖 Redis,不再创建会话,不再查找会话。
// 每个请求通过 _meta 字段携带自身的上下文。
删除 Redis/Memcached 连接代码、会话接口定义以及所有 TTL 清理任务。如果你的 Redis 还用于其他用途(缓存工具结果、限流等),那些保留——只删除会话管理逻辑。
第二步:移除 Initialize 处理器
initialize/initialized 握手已不复存在。删除该处理器以及会话开始时的能力协商逻辑。
迁移前 (Python):
from mcp.server import McpServer
server = McpServer()
@server.method("initialize")
async def handle_initialize(params: dict) -> dict:
client_caps = params.get("capabilities", {})
session_id = generate_session_id()
await store_session(session_id, {
"client_capabilities": client_caps,
"protocol_version": params["protocolVersion"],
})
return {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {"listChanged": True},
"resources": {"subscribe": True},
},
"serverInfo": {"name": "my-server", "version": "1.0.0"},
}
@server.method("initialized")
async def handle_initialized(params: dict) -> None:
# 会话激活
pass
迁移后 (Python):
from mcp.server import McpServer
server = McpServer(
name="my-server",
version="1.0.0",
# 能力声明放在服务器配置中,
# 不再按会话协商
capabilities={
"tools": {"listChanged": True},
"resources": {"subscribe": True},
},
)
# 不再需要 initialize/initialized 处理器。
# 服务器随时可以处理工具调用。
第三步:为每个请求添加 _meta 字段
在无状态模型中,每个请求必须包含 _meta 字段,携带原先在初始化阶段协商的协议上下文。
客户端请求 (TypeScript):
// 旧模式:握手后发送裸请求
const oldRequest = {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "get_weather",
arguments: { city: "Tokyo" },
},
};
// 新模式:自描述请求,包含 _meta
const newRequest = {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "get_weather",
arguments: { city: "Tokyo" },
_meta: {
protocolVersion: "2026-07-28",
capabilities: {
tools: { listChanged: true },
},
},
},
};
服务端解析 (Python):
@server.method("tools/call")
async def handle_tool_call(params: dict) -> dict:
meta = params.get("_meta", {})
protocol_version = meta.get("protocolVersion", "2026-07-28")
client_capabilities = meta.get("capabilities", {})
# 直接使用内联的能力信息——无需会话查找
tool_name = params["name"]
arguments = params["arguments"]
result = await execute_tool(tool_name, arguments)
return {"content": [{"type": "text", "text": result}]}
图 2:_meta 字段承载了原来在 initialize 握手中协商的上下文信息。
第四步:添加 HTTP 请求头
新规范为 Streamable HTTP 传输层引入了三个必需的 HTTP 头:
| 请求头 | 值 | 用途 |
|---|---|---|
Mcp-Protocol-Version | 2026-07-28 | 声明此请求使用的规范版本 |
Mcp-Method | 如 tools/call | 镜像 JSON-RPC 方法,用于路由 |
Mcp-Name | 如 get_weather | 工具/资源名称,用于可观测性 |
客户端 (TypeScript):
const response = await fetch("https://mcp.example.com/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Mcp-Protocol-Version": "2026-07-28",
"Mcp-Method": "tools/call",
"Mcp-Name": "get_weather",
},
body: JSON.stringify(request),
});
服务端校验 (TypeScript):
import express from "express";
const app = express();
app.post("/mcp", (req, res) => {
const protocolVersion = req.headers["mcp-protocol-version"];
const method = req.headers["mcp-method"];
if (!protocolVersion) {
return res.status(400).json({
jsonrpc: "2.0",
error: { code: -32020, message: "Missing Mcp-Protocol-Version header" },
id: null,
});
}
if (protocolVersion !== "2026-07-28") {
return res.status(400).json({
jsonrpc: "2.0",
error: { code: -32020, message: `Unsupported protocol version: ${protocolVersion}` },
id: null,
});
}
// 处理请求...
});
第五步:更新负载均衡器配置
会话状态去除后,粘性会话不再必要。将负载均衡策略改为轮询或最少连接。
NGINX — 迁移前:
upstream mcp_servers {
ip_hash; # 粘性会话
server mcp-1:8080;
server mcp-2:8080;
server mcp-3:8080;
}
NGINX — 迁移后:
upstream mcp_servers {
# 轮询(默认)——不再需要粘性会话
server mcp-1:8080;
server mcp-2:8080;
server mcp-3:8080;
}
AWS ALB: 从目标组中移除粘性配置。Terraform 示例:
resource "aws_lb_target_group" "mcp" {
# 完全删除此块:
# stickiness {
# type = "lb_cookie"
# cookie_duration = 3600
# }
health_check {
path = "/health"
}
}
第六步:使用新版 SDK 进行测试
各官方 SDK 均已发布支持 2026-07-28 规范的 2.x 版本:
| SDK | 包名 | 最低版本 |
|---|---|---|
| TypeScript | @modelcontextprotocol/sdk | 2.0.0 |
| Python | mcp-sdk | 2.0.0 |
| Go | github.com/modelcontextprotocol/go-mcp/v2 | v2.0.0 |
| C# | McpSdk | 2.0.0 |
使用新版 SDK 运行现有的集成测试。SDK 在配置完成后会自动处理 _meta 注入和请求头管理:
import { McpClient } from "@modelcontextprotocol/sdk/client";
const client = new McpClient({
transport: "streamable-http",
url: "https://mcp.example.com/mcp",
// 无需会话管理——SDK 自动处理 _meta 和请求头
});
const result = await client.callTool("get_weather", { city: "Tokyo" });
图 3:测试流程——在过渡期间同时用新旧客户端测试已迁移的服务器。
迁移清单
| # | 任务 | 已完成 |
|---|---|---|
| 1 | 移除会话存储(Redis/Memcached)连接 | ☐ |
| 2 | 删除 initialize/initialized 处理器 | ☐ |
| 3 | 所有方法处理器添加 _meta 解析 | ☐ |
| 4 | 校验传入请求的 Mcp-Protocol-Version 头 | ☐ |
| 5 | 发出请求时设置 Mcp-Method 和 Mcp-Name 头 | ☐ |
| 6 | 从负载均衡器移除粘性会话 | ☐ |
| 7 | 中间件中移除 Mcp-Session-Id 处理 | ☐ |
| 8 | SDK 依赖升级到 2.x | ☐ |
| 9 | 使用新版 SDK 客户端运行集成测试 | ☐ |
| 10 | 测试向后兼容(如需同时支持两种版本) | ☐ |
| 11 | 更新监控告警(移除会话数量指标) | ☐ |
| 12 | 部署到预发布环境并验证轮询分发 | ☐ |
向后兼容:同时支持新旧规范
如果现有客户端尚未迁移,可以通过版本检测同时支持新旧规范:
app.post("/mcp", async (req, res) => {
const protocolVersion = req.headers["mcp-protocol-version"];
const sessionId = req.headers["mcp-session-id"];
if (protocolVersion === "2026-07-28") {
// 新的无状态路径
return handleStatelessRequest(req, res);
} else if (sessionId) {
// 旧的会话路径
return handleSessionRequest(req, res, sessionId);
} else {
// 无版本头且无会话——视为 initialize 握手
return handleLegacyInitialize(req, res);
}
});
这种双模式方案允许你先迁移服务器,再让客户端按各自节奏升级。我们在 Google Cloud 大规模 MCP 实践 案例中详细记录了类似策略。
常见踩坑点
1. 忘记移除会话绑定
我见过最常见的错误:服务器代码迁移了,但忘了改负载均衡器。服务器已经是无状态的,但请求仍然被钉到同一个实例。在某个实例宕机、流量没有重新分配之前,你根本不会发现问题。
解决方案: 明确验证请求在实例间均匀分配。测试期间同时查看所有实例的日志。
2. 请求头不匹配错误 (-32020)
错误码 -32020 表示 Mcp-Protocol-Version 头与服务端期望不符,或必需的请求头缺失。常见原因:
- CDN 或反向代理剥离了自定义请求头
- 客户端 SDK 未配置新的传输方式
- 发送了
Mcp-Protocol-Version: 2025-03-26但请求体中使用了_meta
解决方案: 检查代理/CDN 配置,确保 Mcp-* 系列请求头被正确透传。将这些头添加到允许列表中。
3. 破坏现有客户端
如果在没有兼容层的情况下直接移除 initialize 支持,旧客户端会得到没有任何有用信息的连接错误。
解决方案: 部署上文展示的双模式处理器。记录旧客户端的连接日志,以便追踪迁移进度,最终下线旧路径。
推荐部署平台
无状态模型特别适合以下平台:
- Cloudflare Workers — 官方
@modelcontextprotocol/cloudflare-worker模板可在边缘部署完全无状态的 MCP 服务器。无需为会话查找承受冷启动代价。 - Google Cloud Run — 缩容到零、扩容到数千实例。每个请求独立处理。详见我们的详细案例。
- AWS Lambda + Function URL — 简单部署无需 ALB。每次调用处理一个请求。
迁移后的收益
完成迁移后,你会立即看到运维层面的改善:
- 真正的轮询负载均衡 — 不再有因会话绑定导致的热点实例
- 对 Serverless 友好 — 无需维护会话存储即可缩容到零
- 去除 Redis 依赖 — 少运维一套基础设施,少付一份钱
- 透明故障转移 — 实例宕机后,下一个请求自动路由到其他实例
- 简化调试 — 每个请求自包含,无需重建会话历史
- 更低延迟 — 首次连接无需 initialize 往返
常见问题
问:必须立即迁移吗? 答:不必。旧规范仍可正常工作,双模式方案允许渐进式过渡。但新版 SDK 默认使用 2026-07-28,新的集成方会期望你支持新规范。
问:SSE 和流式传输怎么办?
答:SSE 在新规范中依然可用。区别在于每个 SSE 连接是按请求建立的,完整上下文在 _meta 中携带,不再依赖先前建立的会话。
问:迁移后还能在服务端维护应用状态吗? 答:当然可以。协议是无状态的,但你的应用仍然可以使用数据库、缓存和状态存储来实现业务逻辑。这次变更只移除了协议层的会话状态。
问:如果工具调用依赖之前的上下文怎么办? 答:在工具参数中显式传递上下文,或在应用层使用对话/线程 ID。协议不再维护隐式的会话上下文。
问:没有会话后如何做认证? 答:在每个请求中使用标准 HTTP 认证机制(Bearer Token、API Key)。即使在旧规范下,这也是推荐做法。
对迁移有疑问?请参阅我们的 MCP 协议详解 了解基础概念,或查看 Google Cloud 大规模实践案例 了解生产级部署方案。


