调研日期:2026-07-30
本文目标:把 Tasks 扩展和鉴权变化,转成一份可以排期、灰度和验收的生产迁移清单。

一、先分清:哪些是规范事实,哪些是工程决策

层面

已核验事实

本文的工程建议

传输

2026-07-28 移除了协议层初始化握手与会话 ID;请求可由任一实例处理

网关按 MCP-Protocol-Version 灰度,并在切换期保留旧版兼容路径

状态

协议无状态不等于业务无状态;服务可返回显式句柄供后续工具调用携带

run_idapproval_idtask_id 加上租户、过期时间与调用者绑定

长任务

Tasks 从实验性核心能力转为扩展,生命周期围绕 tasks/gettasks/updatetasks/cancel 调整

把任务状态放进受控存储;不要试图用网关内存模拟全局 tasks/list

鉴权

授权规范加强了 iss 校验、动态客户端注册、issuer 绑定和 scope 升级等要求

身份提供方、MCP Client、Gateway 三处共同做 issuer / audience / scope 校验

可观测性

W3C Trace Context 在 _meta 中有约定的传播键名

外来 traceparent 仅用于关联;安全审计仍以网关签发的调用身份为准


二、真正改变的是“状态放在哪里”

旧实现常见的隐含假设是:一个客户端先 initialize,随后每次工具调用都带上同一个 Mcp-Session-Id;反向代理必须做粘性会话,或让所有实例共用会话存储。

在无状态传输模型下,推荐把调用链拆成下面这样:

Host / Agent
   │  MCP-Protocol-Version + 调用元数据 + 显式业务句柄
   ▼
Agent Gateway(身份、策略、限流、Trace)
   │  Mcp-Method / Mcp-Name 路由
   ▼
任一 MCP Server 实例
   │
   ├─ 立即结果
   └─ { task_id / run_id / approval_id }  ← 需要跨调用保留的状态

例如“导出一份报表”不应依赖某台 Server 内存里的 session,而应由 reports.create 返回一个受权限约束的 report_id;后续 reports.get 带回该 ID。这样横向扩容、重试、故障转移和审计回放都有明确边界。

不要把显式句柄等同于裸 ID。 一个可进入生产的句柄至少应能在服务端关联到:tenantsubjecttoolexpires_attrace_id 和状态版本。客户端只看到不可猜测的标识,服务端才负责真正的授权判断。


三、迁移前先做四张清单

3.1 协议与 SDK 清单

为每个 Host、MCP Client、Server 和 Gateway 记录:支持的协议版本、使用的传输方式、是否还依赖 Mcp-Session-Id、是否使用实验性 Tasks API。这里的目的不是收集版本号,而是找出谁会在切换日把新版请求误发给旧实现。

3.2 状态清单

逐个扫描工具:有没有把购物车、审批、浏览器上下文、分页游标或任务结果藏在进程内存或 session 里?为每类状态确定显式 handle、生命周期、所有者与回收策略。

3.3 网关清单

确认反向代理、WAF、限流器和审计采集器能透传并读取新版关键 Header:

MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: reports.create

网关应校验 Header 与 JSON-RPC 请求体是否一致,不能只相信其中一侧;否则按工具名分流的策略可能被绕过或产生错误审计。

3.4 身份与权限清单

把旧的“一个 MCP Server 一个长期 API Key”列为迁移风险项。至少核验:授权响应的 iss、客户端登记的 application_type、凭据是否只绑定到对应 issuer、scope 升级是否需要重新批准,以及工具句柄是否会跨租户泄漏。


四、可运行示例:先在网关边缘拒绝 Header / Body 不一致

下面是一个 Node.js 20+ 的最小接入检查器。它不是 MCP Server,也不替代 OAuth 验证;用途是在流量回放或灰度环境中,先验证新版请求的协议版本、方法名、工具名和客户端元数据是否自洽。真实网关通过检查后还应把请求转发给后端服务。

// mcp-edge-check.mjs
import http from "node:http";

function reject(res, message) {
  res.writeHead(400, { "content-type": "application/json" });
  res.end(JSON.stringify({ error: message }));
}

async function readJson(req) {
  let raw = "";
  for await (const chunk of req) raw += chunk;
  return JSON.parse(raw || "{}");
}

http.createServer(async (req, res) => {
  if (req.method !== "POST" || req.url !== "/mcp") {
    res.writeHead(404).end();
    return;
  }

  let payload;
  try {
    payload = await readJson(req);
  } catch {
    reject(res, "invalid JSON");
    return;
  }

  const version = req.headers["mcp-protocol-version"];
  const headerMethod = req.headers["mcp-method"];
  const headerName = req.headers["mcp-name"];
  const bodyMethod = payload?.method;
  const bodyName = payload?.params?.name;
  const meta = payload?.params?._meta ?? {};

  if (version !== "2026-07-28") return reject(res, "unexpected protocol version");
  if (payload?.jsonrpc !== "2.0") return reject(res, "JSON-RPC 2.0 required");
  if (!headerMethod || headerMethod !== bodyMethod) {
    return reject(res, "Mcp-Method does not match JSON-RPC method");
  }
  if (bodyMethod === "tools/call" && (!headerName || headerName !== bodyName)) {
    return reject(res, "Mcp-Name does not match params.name");
  }
  if (!meta["io.modelcontextprotocol/clientInfo"]) {
    return reject(res, "clientInfo missing from _meta");
  }

  // 这里只记录关联信息;不要把外来 traceparent 当成认证信息。
  console.log({
    method: bodyMethod,
    tool: bodyName ?? null,
    traceparent: meta.traceparent ?? null,
  });
  res.writeHead(204).end();
}).listen(8181, () => console.log("edge check: http://127.0.0.1:8181/mcp"));

启动后用下面的请求验证一条合规的 tools/call

node mcp-edge-check.mjs

curl -i http://127.0.0.1:8181/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/call" \
  -H "Mcp-Name: search" \
  --data '{
    "jsonrpc":"2.0",
    "id":1,
    "method":"tools/call",
    "params":{
      "name":"search",
      "arguments":{"q":"MCP migration"},
      "_meta":{
        "io.modelcontextprotocol/clientInfo":{"name":"migration-test","version":"1.0"},
        "traceparent":"00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"
      }
    }
  }'

Mcp-Name 改成别的值,应收到 400。这正是灰度期值得自动化的检查:路由依据和审计依据必须指向同一个工具调用。


五、按这个顺序上线,回滚才有抓手

  1. 先双栈观测,再切流量。 Server / Gateway 先接受旧版与 2026-07-28,记录每个版本的成功率、429/4xx、工具耗时和会话依赖。
  2. 把隐式 session 状态改成显式资源。 为每个 handle 增加归属与过期校验,并针对重试设计幂等键。
  3. 迁移 Tasks。 不再假设可以全局枚举 tasks/list;由业务侧保存可见 task 关联,再用 tasks/gettasks/updatetasks/cancel 驱动生命周期。
  4. 把授权单独做回归。 分别测试 issuer 不匹配、缺少 iss、错误 redirect、scope step-up、过期 refresh token 和跨租户 handle;不要只测“能登录”。
  5. 开启路径级策略。 Mcp-Method / Mcp-Name 可用于路由和限流,但 L3/L4 工具仍要回到上一篇的 Gateway 策略和人工审批。
  6. 完成可回滚验收。 一旦新版错误率、授权拒绝率或任务中断率异常,按 Host / Client 开关退回旧协议路径;不要在回滚时删除已生成的业务 handle。

六、三个最容易翻车的点

1)“无状态”后仍偷偷依赖本机内存

单实例压测会掩盖问题;一加第二台实例,task_id、审批上下文或分页游标就丢。对所有跨调用数据使用显式 ID 和服务端受控存储,才是真正的无状态传输。

2)把 Trace 当作身份

traceparent 的价值是串起调用链,而不是证明谁有权限。安全决策仍须来自已验证的工作负载身份、租户和 scope;Trace 最多作为关联字段。

3)只改 Server,不测 Client 与反向代理

新版 Header 被 CDN 丢弃、WAF 不认识 Mcp-Method、旧 Client 仍发送 Session ID,都会让“SDK 单测通过”变成线上失败。验收必须覆盖 Host → Gateway → Server → 下游工具整链。


结语

其最大价值不是让 MCP 少一次握手,而是让协议层不再替业务藏状态。对 Agent 系统而言,这为普通负载均衡、精细网关策略、可关联追踪和可控长任务打下了更干净的边界。

如果你已经完成了 Agent Gateway 的身份、权限和审计设计,下一步不是立刻全量升级,而是拿一个低风险 MCP Server 跑完本文四张清单、边缘一致性检查和双栈回滚演练。把“能调用”升级成“可扩、可审、可退”,协议升级才真正产生生产价值。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐