把 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-Id 与 Last-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/json与text/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 续传:id 与 Last-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 层重复展示;这与工具幂等层是不同维度,但应共用同一
jsonrpcid或业务tool_call_id做关联。
HTML 标准中 Last-Event-ID 的语义见 WHATWG Server-Sent Events;MCP 将其限定为 per-stream cursor,而非全局消息日志。
4. 安全与本地部署
规范在 Streamable HTTP 章节列出三项 ** MUST/SHOULD**:
- 对所有入站连接校验
Origin,缓解 DNS rebinding。 - 本地监听时应绑定
127.0.0.1,而非0.0.0.0。 - 应对所有连接实施适当鉴权(OAuth 2.1、mTLS、内网 IP 允许列表等,部署相关)。
网关层可落地规则:
# 仅示例:生产环境应配合 OAuth/mTLS,而非单独依赖 Origin
if ($http_origin !~* ^https://(app\.example\.com|localhost:3000)$) {
return 403;
}
CORS 预检须允许 Mcp-Session-Id、MCP-Protocol-Version、Accept 等自定义头,否则浏览器型 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 并依赖代理重置;否则服务端可能继续执行长耗时工具。
参考来源
- Model Context Protocol — Transports(2025-06-18) — Streamable HTTP、会话头、SSE 续传、安全警告与向后兼容
- MCP 规范源码:
transports.mdx— MUST/SHOULD 原文与序列图 - WHATWG HTML — Server-sent events —
Last-Event-ID与事件id字段语义 - Model Context Protocol — Lifecycle(2025-06-18) — 初始化阶段与版本协商
- MCP 2024-11-05 — HTTP with SSE(已弃用传输) — 旧客户端回退探测行为
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。