MCP 无状态迁移实战指南:2026-07-28 规范

手把手教你将 MCP 服务器从会话模型迁移到全新的无状态 2026-07-28 规范。包含前后对比代码、迁移清单和常见问题解答。

上周我把三个生产环境的 MCP 服务器——两个在 AWS、一个在 Google Cloud Run——从旧的会话模型迁移到了新的无状态 2026-07-28 规范。结果:删除了 400 行会话管理代码、下线了一整套 Redis 集群,终于实现了真正的轮询负载均衡,不再需要粘性会话。每个服务大约花了四个小时完成迁移。这篇指南就是我当初希望手边就有的操作手册。

MCP 无状态迁移架构总览 图 1:迁移前后架构对比——左侧是会话绑定模式,右侧是无状态轮询模式。

2026-07-28 规范的核心变化

MCP 2026-07-28 规范 是自 MCP 发布以来最重大的协议变更。主要区别如下:

维度旧规范 (2025-03-26)新规范 (2026-07-28)
会话建立initialize/initialized 握手无——请求自描述
会话追踪Mcp-Session-Id完全移除
请求元数据在会话建立时协商每个请求内联 _meta 字段
HTTP 头极简Mcp-Protocol-VersionMcp-MethodMcp-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}]}

_meta 字段添加前后的代码差异 图 2:_meta 字段承载了原来在 initialize 握手中协商的上下文信息。

第四步:添加 HTTP 请求头

新规范为 Streamable HTTP 传输层引入了三个必需的 HTTP 头:

请求头用途
Mcp-Protocol-Version2026-07-28声明此请求使用的规范版本
Mcp-Methodtools/call镜像 JSON-RPC 方法,用于路由
Mcp-Nameget_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/sdk2.0.0
Pythonmcp-sdk2.0.0
Gogithub.com/modelcontextprotocol/go-mcp/v2v2.0.0
C#McpSdk2.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-MethodMcp-Name
6从负载均衡器移除粘性会话
7中间件中移除 Mcp-Session-Id 处理
8SDK 依赖升级到 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 大规模实践案例 了解生产级部署方案。