Hotdry.

Article

在生产网关上部署 MCP Streamable HTTP:Mcp-Session-Id、SSE 续传与 Origin 校验

依据 MCP 2025-06-18 传输规范,说明 Agent 网关如何把 Streamable HTTP 的会话头、Last-Event-ID 续传、协议版本头与 DNS 重绑定防护落成可运维的反向代理与超时参数。

2026-06-22ai-systems

把 MCP Server 从本机 stdio 子进程迁到可被多 Agent 共享的 HTTP 端点时,协议层已从 2024-11-05 的「独立 SSE + POST」演进为 Streamable HTTP(2025-06-18):同一 MCP 路径同时接受 POST(下发 JSON-RPC)与 GET(可选的长连 SSE)。Agent 编排器若仍按「一次 POST 等于一次完整往返」来配置负载均衡超时,会在 tools/call 触发服务端 SSE 流时过早切断连接;若忽略 Mcp-Session-IdLast-Event-ID,断线后既可能丢失工具结果,也可能在会话已 404 后仍向旧会话重放请求。本文只讨论传输与会话边界,工具副作用的幂等控制需另见执行层设计。

问题背景:传输语义与网关默认行为冲突

Streamable HTTP 下,客户端对每个 JSON-RPC 消息发起新的 HTTP POST(规范要求)。当 POST 体是 request 时,服务端可以:

  • 返回 Content-Type: application/json 的单体响应;或
  • 返回 Content-Type: text/event-stream,在 SSE 流中推送 JSON-RPC response,并可在 response 之前穿插与本次 request 相关的 server request/notification。

因此一次 tools/call 的 wall-clock 时间可能等于「模型推理 + 工具执行 + 多段 SSE 事件」,而不是单个 HTTP 响应体的解析时间。常见踩坑包括:

现象 常见根因
工具已成功、Agent 却报超时 反向代理 proxy_read_timeout 小于 SSE 流持续时间
断线后重复初始化、工具列表抖动 未持久化 Mcp-Session-Id,或未在 HTTP 404 时按规范重新 initialize
本地 MCP 被远程网页调用 未校验 Origin、监听 0.0.0.0 且无鉴权(规范中的 DNS rebinding 警告)
旧客户端连新服务端失败 未实现 2024-11-05 HTTP+SSE 回退探测,或 MCP-Protocol-Version 头缺失导致版本假设错误

规范同时明确:SSE 断开不应被客户端解释为取消;取消应发送 MCP CancelledNotification。网关与 Agent 运行时的重试策略必须与工具幂等层对齐,不能仅凭 TCP 断开就自动重发同一 tools/call

可落地实现:网关、会话与续传参数

1. 端点与协议头

服务端需提供单一 MCP 路径(示例 /mcp),同时支持 POST 与 GET。客户端每次 POST 应携带:

POST /mcp HTTP/1.1
Host: mcp.example.com
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2025-06-18
Mcp-Session-Id: 7f3c9e2a-4b1d-4c8a-9f0e-1a2b3c4d5e6f
Content-Type: application/json

{"jsonrpc":"2.0","id":42,"method":"tools/call","params":{...}}

参数说明:

  • Accept:必须同时列出 application/jsontext/event-stream,否则无法协商 SSE 响应。
  • MCP-Protocol-Version:规范要求客户端在后续 HTTP 请求中携带与初始化阶段协商一致的版本;服务端若无法识别该头,应返回 400。若未收到该头,服务端假定版本为 2025-03-26(向后兼容行为)。
  • Mcp-Session-Id:仅在服务端于 InitializeResult 响应中通过响应头下发后出现;此后客户端必须在所有后续请求中附带。可见 ASCII 字符(0x21–0x7E),宜为 UUID 或等强度随机标识。

初始化响应示例(服务端):

HTTP/1.1 200 OK
Content-Type: application/json
Mcp-Session-Id: 7f3c9e2a-4b1d-4c8a-9f0e-1a2b3c4d5e6f

{"jsonrpc":"2.0","id":1,"result":{...}}

客户端在收到针对带 Mcp-Session-Id 请求的 HTTP 404 时,必须不带会话头发起新的 InitializeRequest(规范强制语义)。

2. 反向代理超时(Nginx 示例)

SSE 场景下,代理应在「已收到完整响应头且为 text/event-stream」时禁用对端的过早超时。参考起点(按工具 P99 调整):

location /mcp {
    proxy_pass http://mcp_upstream;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;

    proxy_connect_timeout 10s;
    proxy_send_timeout 3600s;
    proxy_read_timeout 3600s;

    proxy_set_header MCP-Protocol-Version $http_mcp_protocol_version;
    proxy_set_header Mcp-Session-Id $http_mcp_session_id;
    proxy_set_header Origin $http_origin;
}
参数 建议起点 说明
proxy_buffering off 必开 避免 SSE 事件被缓冲到连接结束才下发
proxy_read_timeout ≥ 最长 tools/call SLA Job 级 deadline 宜与此一致
proxy_connect_timeout 5–15s 仅影响建连,与 SSE 时长无关
上游 keepalive 启用 减少多 POST 短连接的 TLS 开销

notification/response 的 POST 若被服务端接受,规范要求返回 202 Accepted 且无 body;网关不应把 202 当作错误重试。

3. SSE 续传:idLast-Event-ID

为降低断线丢消息概率,服务端可以为 SSE 事件设置 id 字段;若设置,则在该 session(或无 session 时该客户端)范围内全局唯一。客户端恢复连接时,应对 MCP 端点发起 GET,并携带:

GET /mcp HTTP/1.1
Accept: text/event-stream
Last-Event-ID: evt-0001842
Mcp-Session-Id: 7f3c9e2a-4b1d-4c8a-9f0e-1a2b3c4d5e6f

服务端可以据此在断开的那条流上重放 cursor 之后的事件;规范禁止把其它流上的消息重放到当前流。网关实现注意:

  • 不要把 Last-Event-ID 与 HTTP 会话粘性绑死到不同 Pod:若 MCP 状态在本机内存,续传必须路由到持有该流状态的上游,或使用 Redis/DB 存储 per-stream cursor。
  • Agent 侧应把「续传拿到的 JSON-RPC response」与「首次 POST 的 in-flight 请求」做 id 关联,避免 UI 层重复展示;这与工具幂等层是不同维度,但应共用同一 jsonrpc id 或业务 tool_call_id 做关联。

HTML 标准中 Last-Event-ID 的语义见 WHATWG Server-Sent Events;MCP 将其限定为 per-stream cursor,而非全局消息日志。

4. 安全与本地部署

规范在 Streamable HTTP 章节列出三项 ** MUST/SHOULD**:

  1. 对所有入站连接校验 Origin,缓解 DNS rebinding。
  2. 本地监听时绑定 127.0.0.1,而非 0.0.0.0
  3. 对所有连接实施适当鉴权(OAuth 2.1、mTLS、内网 IP 允许列表等,部署相关)。

网关层可落地规则:

# 仅示例:生产环境应配合 OAuth/mTLS,而非单独依赖 Origin
if ($http_origin !~* ^https://(app\.example\.com|localhost:3000)$) {
    return 403;
}

CORS 预检须允许 Mcp-Session-IdMCP-Protocol-VersionAccept 等自定义头,否则浏览器型 MCP 客户端无法完成跨源调用。

5. 与旧版 HTTP+SSE 的共存

需要同时服务 2024-11-05 客户端时,规范建议服务端继续提供旧 SSE/POST 端点,并额外提供 Streamable HTTP 的 MCP 端点;客户端可先对新端点 POST InitializeRequest,若得到 4xx 再 GET 探测旧 endpoint 事件。Agent 平台若统一走 Streamable HTTP,应在配置中显式关闭对旧 URL 的自动探测,避免误连到已下线的 /sse 路径。

6. 会话生命周期与显式终止

客户端在离开应用时对 MCP 端点发送 HTTP DELETE 并附带 Mcp-Session-Id;服务端可以 405 表示不支持。运维侧建议:

建议
会话 TTL 与 Agent 用户会话一致,空闲 30–120min 回收
服务端 404 触发客户端重新 initialize,并清空本地工具缓存
多 GET 流 规范允许客户端保持多条 SSE;服务端不得向多流广播同一消息

风险与边界

传输层重试 ≠ 工具幂等。 规范指出断线不应视为取消;若网关或 HTTP 客户端自动重发同一 POST,可能产生重复 tools/call。只读工具可结合退避重试;写工具必须依赖服务端幂等键或 Agent 执行层去重表,不能仅靠 MCP 注解。

SSE 续传是可选能力。 服务端可以不实现 Last-Event-ID 重放;此时 Agent 只能在断线后依赖应用层重新发起 call,并承担「工具已执行但结果未送达」的不确定性。

会话粘性与水平扩展。 Mcp-Session-Id 使 MCP 成为有状态 HTTP 服务;无共享存储时,滚动发布会导致大量 404 与重新初始化。需要会话存储外置或 graceful drain。

协议版本头与实现漂移。 假定 2025-03-26 的兼容行为可能导致新特性(如 Streamable HTTP 细粒度取消)在旧库上静默降级;应在监控中记录 MCP-Protocol-Version 分布。

stdio 与 HTTP 的安全模型不同。 stdio 子进程由客户端启动,攻击面主要是本机;Streamable HTTP 暴露网络面,Origin 校验不能替代鉴权,尤其在内网穿透或开发隧道场景。

Cancel 语义。 用户中止 Agent 时,应发 CancelledNotification,而不是直接关闭浏览器 tab 并依赖代理重置;否则服务端可能继续执行长耗时工具。

参考来源

ai-systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com