丹尼拉(Dayfing)
返回文章列表
3,393 字12 分钟

MCP 2026-07-28:无状态服务器与安全迁移

2026 年 7 月 28 日 发布 的 Model Context Protocol(MCP)修订版 改变了 远程 MCP 的 运行方式。协议 核心 现在 是 无状态 的,并且 以 独立 请求 为 基础。每个 请求 都 携带 处理 所需 的 信息,因此 负载 均衡器 可以 把 下一次 调用 发送 到 另一台 服务器 实例。这个 版本 还 加入 了 server/discover、Multi Round-Trip Requests(MRTR)、必需 的 路由 请求头、缓存 提示、扩展 框架,以及 更严格 的 授权 规则。本文 的 规范性 细节 来自 官方 发布 文章规范 变更 日志

这次 变化 针对 的 是 协议 状态,而 不是 要求 整个 业务系统 都 变成 无状态。工具 仍然 可以 使用 数据库、队列 或 持久 workflow。真正 消失 的 是 绑定 到 MCP 传输 会话 的 隐藏 状态,这 对 从 单个 进程 扩展 到 多个 副本 尤其 重要。

与 2025 年 协议 相比 有哪些变化

旧的 生命周期 先 发送 initialize 请求,再 发送 notifications/initialized 通知。在 Streamable HTTP 中,服务器 还 可以 返回 Mcp-Session-Id,并 将 后续 消息 关联 到 这条 连接。2026-07-28 删除 了 initialize 交换 和 协议 会话 请求头。每个 请求 都 在 _meta 中 声明 协议 版本 和 客户端 能力。客户端 应该 提供 io.modelcontextprotocol/clientInfo,服务器 则 应该 在 结果 元数据 中 标识 自身。

下面 的 对比表 可用于 制定 迁移 计划:

领域 2025 年 行为 2026-07-28 行为
生命周期 initializenotifications/initialized 没有 协议 握手
会话 HTTP 中 可选 的 Mcp-Session-Id 没有 协议 层 会话
能力 只 协商 一次 每个 请求 都 声明
发现 initialize 后 或 按约定 读取 现代 服务器 必须 提供 server/discover,客户端 可选 调用
服务器 到 客户端 通过 保持 打开 的 通道 发送 请求 MRTR 在 响应 中 返回 输入 请求
HTTP 路由 网关 通常 需要 解析 JSON 使用 Mcp-Method 和 适用 的 Mcp-Name 请求头
列表 与 读取 新鲜度 由 客户端 自行 决定 提供 ttlMscacheScope 提示
恢复 SSE 可以 使用 事件 ID 不再 使用 Last-Event-ID 恢复,需 创建 新请求
客户端 注册 DCR 通常 是 自动 路径 优先 使用 CIMD,DCR 为 兼容性 保留

本次 修订 将 Tasks 移入 io.modelcontextprotocol/tasks 扩展,用 subscriptions/listen 替换 旧的 变更 流,并 将 Roots、Sampling、Logging 和 旧 HTTP+SSE 标记 为 弃用。生命周期 政策 提供 至少 十二个月 的 过渡期,但 新实现 不应 再 采用 这些 功能。

无状态 在 实际 部署 中 意味着 什么

在 现代 Streamable HTTP 中,服务器 暴露 一个 接受 POST 的 MCP 端点。客户端 每次 POST 发送 一个 JSON-RPC 请求 或 通知。响应 可以 是 单个 JSON 对象,也 可以 是 只 属于 该请求 的 SSE 流。服务器 不会 创建 会话 ID,被 中断 的 响应 流 也 没有 可以 恢复 的 事件 历史。在 HTTP 中,关闭 响应 流 就是 取消 信号。

传输 请求头 和 请求体 描述 同一个 操作。一个 最小 的 工具 调用 如下:

POST /mcp HTTP/1.1
Accept: application/json, text/event-stream
Content-Type: application/json
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/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"catalog-app","version":"1.0.0"}}}}

MCP-Protocol-Version 请求头 的 值 必须 与 _meta 中 的 值 一致。现代 服务器 对 缺失 或 不一致 的 必需 请求头 返回 HTTP 400,并 使用 HeaderMismatch 错误 代码 -32020。服务器 在 网关 处理 后 还 必须 再次 校验 请求头。这样 可以 防止 代理 按 一个 工具 名称 路由,而 应用 却 执行 另一个 工具。

无状态 改变 的 是 扩展 方式,不是 业务 语义。如果 workflow 需要 延续,请 从 工具 返回 一个 明确 的 handle,并 要求 下一次 调用 携带 它。把 权威 状态 存放 在 数据库 或 workflow 服务 中,将 handle 与 用户 和 操作 绑定,并 设置 过期 时间。未经 验证 的 handle、requestState 或 工具 参数 都 不能 证明 调用者 有 权限。

server/discover 与 版本 兼容

每个 现代 服务器 都 必须 实现 server/discover RPC。它 的 结果 宣布 支持 的 协议 版本 和 能力,也 可以 包含 可选 的 使用 说明。客户端 可以 先 调用 它 来 选择 版本,也 可以 直接 发送 现代 请求。因此 discovery 很有 用,但 不是 客户端 必须 执行 的 握手。

如果 请求 的 版本 不受 支持,服务器 返回 UnsupportedProtocolVersionError 以及 支持 的 版本 列表。客户端 选择 一个 双方 支持 的 版本 后 重试。支持 两个 时代 的 客户端 必须 谨慎 判断 探测 结果。空的 400 响应,或 不包含 已知 现代 JSON-RPC 错误 的 400 响应,可能 表示 旧的 端点。已识别 的 现代 错误 则 表示 客户端 应 修正 请求 或 重新 协商。授权 错误 和 基础设施 故障 不能 证明 对方 是 旧服务器。

现代 SDK 可以 探测 standard input 连接,并 为 该连接 固定 协议 时代。服务器 可以 在 新客户端 使用 现代 请求 时 保留 legacy 路由。不要 仅凭 成功 的 TCP 连接 或 普通 404 判断 时代。

MRTR 替代 服务器 到 客户端 的 请求

现代 线协议 删除 了 服务器 到 客户端 的 JSON-RPC 请求 通道。需要 确认、缺少 参数 或 模型 协助 步骤 的 工具,不再 保持 流 打开,而是 返回 中间 结果。结果 包含 resultType: "input_required"inputRequests 映射。客户端 完成 这些 输入 后,使用 inputResponses 重试 原来的 方法。重试 是 一个 新请求,可能 到达 另一台 副本。

确认 流程 可以 表示 为:

{
  "resultType": "input_required",
  "inputRequests": {
    "confirm": {
      "type": "elicitation",
      "message": "Delete three files?",
      "schema": {"type": "boolean"}
    }
  },
  "requestState": "signed-opaque-state"
}

客户端 随后 发送 inputResponses.confirm,并 原样 返回 requestState。服务器 必须 像 处理 新请求 一样 再次 进入 handler。让 handler 具备 幂等性,从 已验证 的 状态 推导 当前 workflow 步骤,只 请求 仍然 缺少 的 信息。确认 尚未 验证 前,不要 将 破坏性 操作 标记 为 完成。

requestState 不是 安全 容器。它 会 经过 客户端,因此 必须 当作 攻击者 可控制 的 输入。使用 HMAC 签名 或 认证 加密,绑定 principal、原始 方法、相关 参数 和 过期 时间,并 在 handler 运行 前 拒绝 被 篡改 的 状态。签名 不会 隐藏 内容,所以 不要 放入 秘密。TypeScript SDK 提供 请求 状态 codec 和 验证 hook。它 的 legacy shim 可以 在 仍需 支持 2025 客户端 时,将 同一个 input_required handler 转换 成 旧的 elicitation/createsampling/createMessageroots/list 请求。

长时间 运行 的 工作 应 使用 Tasks 扩展、持久 的 task handle、tasks/get 轮询,以及 需要 客户端 输入 时 的 tasks/update

缓存 提示 与 确定性 目录

现代 的 tools/listprompts/listresources/listresources/templates/listresources/read 结果 都 携带 ttlMscacheScopettlMs 是 以 毫秒 表示 的 非负 新鲜度 提示,语义 类似 HTTP 的 max-age。零 表示 立即 过期。对于 旧服务器,缺失 的 值 应 按 零 处理。正值 告诉 客户端 在 多久 内 可以 避免 再次 获取,但 不保证 数据 在 到期 前 不变。客户端 应 在 需要 数据 时 检查 新鲜度,不要 将 TTL 变成 持续 polling。

缓存 键 必须 包含 方法 以及 影响 结果 的 每个 参数,包括 资源 URI 和 分页 列表 的 cursor。包含 inputResponsesrequestState 的 响应 不应 缓存,因为 简单 的 列表 键 不包含 其 上下文。cacheScope: "public" 允许 在 不同 授权 上下文 之间 共享,所以 只有 完全 不含 用户 或 权限 特定 数据 时 才能 使用。即使 目录 已 缓存,每个 工具 仍需 执行 权限 检查。

服务器 应 按 确定性 顺序 返回 工具。稳定 的 顺序 能 保持 模型 prompt 稳定,并 在 重连 后 提高 缓存 复用率。listChanged 通知 可以 补充 TTL,但 不能 代替 正确 的 缓存 提示。

OAuth 与 安全 变化

MCP 整体 的 授权 是 可选 的。保护 资源 的 HTTP 服务器 应 遵循 2026 规范 中 的 OAuth 2.1 配置。服务器 作为 resource server,必须 按 RFC 9728 发布 OAuth Protected Resource Metadata。401 响应 应 通过 WWW-Authenticate 指向 这些 元数据,并 在 有用 时 给出 scope challenge。客户端 必须 支持 请求头 中 的 元数据 URL,以及 两种 well-known 形式。

元数据 可以 指定 一个 或 多个 authorization server。客户端 必须 支持 RFC 8414 的 OAuth Authorization Server Metadata 和 OpenID Connect Discovery。对于 带有 路径 的 issuer,应 先 尝试 插入 路径 的 形式,再 尝试 OpenID 追加 路径 的 形式。

客户端 注册 现在 优先 使用 Client ID Metadata Documents。预注册 也 有效。Dynamic Client Registration 是 已弃用 的 后备 机制。如果 使用 DCR,要 为 桌面 或 CLI 客户端 发送 正确 的 application_type。验证 PKCE,使用 S256,注册 精确 的 redirect URI,并 使用 HTTPS,允许 的 localhost callback 除外。

客户端 必须 把 已验证 的 issuer 与 PKCE 事务 一起 记录。如果 授权 响应 含有 iss,在 兑换 code 前 比较 它。凭据 绑定 到 签发 它们 的 issuer,不得 在 另一个 authorization server 重用。把 MCP 服务器 的 规范 URI 作为 RFC 8707 的 resource 放入 授权 和 token 请求。服务器 必须 验证 token audience,并 拒绝 发给 其他 资源 的 token。

对 Streamable HTTP 要 验证 Origin,防止 DNS rebinding。本地 服务器 应 只 绑定 localhost。不要 把 bearer token 放在 query string 或 日志 中,在 暴露 私有 资源 或 调用 工具 前 要 求 用户 同意;如果 服务器 不受 信任,要 将 工具 描述 和 annotation 视为 不可信。401 表示 授权 缺失 或 无效,403 表示 权限 不足,并 应 尽可能 携带 insufficient_scope challenge。

TypeScript SDK 迁移

TypeScript SDK v2 将 client、server、core 和 runtime 拆分 为 不同 包。请 阅读 支持 2026-07-28 的 SDK 指南v1 到 v2 迁移 指南。只 更新 SDK 不会 自动 让 现代 字节 出现在 网络 上。v2 客户端 默认 使用 legacy 协商,所以 应 明确 启用 versionNegotiation

支持 两个 协议 时代 的 客户端 可以 使用 文档 中 的 形式:

import { Client } from '@modelcontextprotocol/client';

const client = new Client(
  { name: 'catalog-app', version: '1.0.0' },
  { versionNegotiation: { mode: 'auto' } },
);
await client.connect(transport);

mode: 'auto' 探测 server/discover,只有 在 对端 确实 是 legacy 时 才 回退 到 2025 握手。如果 回退 会 隐藏 不兼容,请 固定 2026-07-28createMcpHandler(factory) 为 每个 现代 HTTP 请求 创建 新服务器,并 可 通过 一个 端点 服务 两个 时代。stdio 需要 按 连接 选择 时代 时,使用 serveStdio(() => buildServer())

将 v1 基于 schema 的 handler 注册 改为 方法 字符串,例如 setRequestHandler('tools/call', handler)。把 读取 ctx.sessionId 的 代码 改成 应用 handle 或 已验证 的 requestState。把 push 式 elicitation 改成 inputRequired(...)。现代 请求 没有 io.modelcontextprotocol/logLevel 时 不会 发出 log 通知。SDK codemod 只能 作为 机械 辅助,不能 代替 兼容性 测试。

验证 时,可以 通过 基于 fetch 的 测试 传输 驱动 createMcpHandler,并 单独 覆盖 legacy 握手。使用 旧客户端 和 现代 客户端 检查 请求头、_meta、重试 幂等性、缓存 范围、audience 和 HTTP 状态。

迁移 清单

  1. 盘点 客户端、服务器、传输、会话 存储、SSE 事件 存储,以及 读取 Mcp-Session-Id 的代码。
  2. 决定 哪个 端点 接收 现代 流量,哪个 端点 在 过渡 期间 保留 legacy。
  3. 升级 SDK,并 在 lockfile 中 固定 实际 包版本。
  4. 添加 server/discover 和 版本 协商 策略。
  5. 让 每个 请求 都 自描述,并 验证 _meta 与 镜像 请求头。
  6. 用 显式 handle,或 签名 且 会 过期 的 requestState,替代 按会话 保存 的 状态。
  7. 用 MRTR 重写 服务器 交互,并 让 重试 具备 安全 的 幂等 行为。
  8. 为 目录 和 资源 结果 添加 ttlMscacheScope 以及 确定性 排序。
  9. Mcp-MethodMcp-Name 更新 网关、WAF、指标 和 链路 跟踪。
  10. 实现 issuer、resource、audience、PKCE、redirect、Origin 和 scopes 检查。
  11. 新代码 不要 采用 HTTP+SSE、Roots、Sampling、Logging 或 DCR。
  12. 在 可观测性 支持 下 分阶段 发布,比较 两个 时代 的 错误率,迁移 消费者 后 再 删除 兼容 代码。

故障 排查

现象 可能 原因 处理
HTTP 400 和 -32020 请求头 缺失 或 与 请求体 不一致 从 同一个 请求 对象 重新 计算 MCP-Protocol-VersionMcp-MethodMcp-Name
HTTP 400 和 版本 错误 对端 不支持 请求 的 修订版 supported 选择 版本,或 使用 legacy 路径
HTTP 404 和 method-not-found 端点 是 现代 的,但 方法 不存在 检查 方法 与 扩展,不要 盲目 发送 initialize
discovery 返回 HTTP 401 或 403 探测 被 授权 层 阻挡 修复 凭据 和 元数据,授权 状态 不能 证明 对端 是 legacy
没有 log 通知 缺少 io.modelcontextprotocol/logLevel 按 请求 启用,或 使用 stderr 和 OpenTelemetry
重连 后 出现 重复 副作用 流 不再 支持 恢复 使用 idempotency key 和 新的 请求 ID
一个 用户 的 数据 出现在 其他 缓存 结果 错误 标为 public 改为 private,并 保留 principal 检查
旧客户端 在 GET 上 收到 405 它 仍 期待 HTTP+SSE 临时 保留 legacy 端点

架构 相关 内容 可 参考 production AI agent architectureMCP server with TypeScript and OAuth

来源

本文 遵循 MCP 2026-07-28 规范变更 与 弃用 记录Streamable HTTP 要求关于 无状态 MCP 的 SEP-25752026-07-28 发布 文章MCP 授权 规范TypeScript SDK 迁移 文档

更多文章