把 MCP 服务器从本机 stdio 子进程迁到团队共享的 HTTP 端点,是 Agent 平台化的常见一步。MCP 规范在 2025-03-26 版本用 Streamable HTTP 取代了早期的 HTTP+SSE 组合:客户端对每个 JSON-RPC 消息发起独立的 HTTP POST,服务器在需要时以 Content-Type: text/event-stream 返回 SSE 流,并可通过 Mcp-Session-Id 维持有状态会话。官方传输文档同时要求服务器校验 Origin 以防 DNS rebinding,并说明断线不应被客户端等同于取消请求。
若在这一层前面再加 nginx、Envoy 或云负载均衡,默认的响应缓冲与空闲超时往往比 LLM 工具链的单次 POST+SSE 交互更短,表现为:Agent 侧长时间无增量事件、中途 502、或初始化后后续请求丢失会话。本文只讨论可在配置中落地的代理参数与头字段传递,不假设某一云厂商的专有行为。
问题背景:为何默认反向代理配置会伤 MCP
与典型 REST JSON API 相比,Streamable HTTP 有三个容易踩坑的差异点:
- 双模态响应:同一 MCP 路径上,服务器对 POST 可能返回
application/json(单包)或text/event-stream(多事件)。代理若一律按「缓冲完整响应体再转发」,SSE 事件会被攒到最后才出现,破坏 Agent 的流式 UX,甚至触发客户端读超时。 - 会话头不在 Cookie 里:有状态服务器在
InitializeResult响应中下发Mcp-Session-Id,客户端必须在后续所有 HTTP 请求中回传该头;规范要求 ID 为可见 ASCII(0x21–0x7E),且缺失时服务器可返回 400。代理若剥离未知头或按路径做无状态轮询,会把多副本 MCP 实例间的会话打乱。 - 长耗时与断线语义:规范写明 SSE 可在任意时刻断开,且不应把断线解释为客户取消;客户端应发
CancelledNotification显式取消,并可用Last-Event-ID尝试续传。代理的proxy_read_timeout/stream_idle_timeout若小于工具执行时间,会在 TCP 层先断流,与 MCP 应用层语义叠加后更难排查。
因此,Agent 网关要把 MCP 端点当作「可能升级为长连接的 HTTP」而不是普通 200ms 内的 CRUD。
可落地实现:头字段、路由与代理参数
1. 必须透传与校验的 HTTP 头
| 头字段 | 方向 | 说明 |
|---|---|---|
Accept | 客户端 → 服务器 | 规范要求 POST 同时声明 application/json 与 text/event-stream |
Mcp-Session-Id | 双向约定 | 初始化后客户端每条请求必须携带;代理应加入 proxy_set_header / Envoy headers_to_add 白名单,禁止剥离 |
Origin | 客户端 → 服务器 | MCP 规范要求服务器校验,防 DNS rebinding;代理不要伪造为 *,应原样转发浏览器或网关来源 |
Last-Event-ID | 客户端 → 服务器 | GET 恢复 SSE 时用于重放;与 HTML SSE 标准一致 |
Content-Type | 服务器 → 客户端 | 区分 JSON 与 SSE;代理勿强制改写 |
多副本部署时,建议对同一 Mcp-Session-Id 做会话粘性(hash 会话头或集中式会话存储),直到 MCP 服务器本身实现无共享状态。规范允许服务器随时终止会话并以 404 响应,客户端必须重新 Initialize。
2. nginx 示例(单上游 MCP 进程)
下列片段针对 location /mcp;数值需按 P95 工具耗时调整。
# 推荐初始值(LLM 工具 P95 ≈ 3–8 分钟时)
# proxy_read_timeout: 600s
# proxy_send_timeout: 60s
# client_body_timeout: 60s
# keepalive: 与客户端长连接复用,减轻 Initialize 开销
location /mcp {
proxy_pass http://mcp_upstream;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 透传 MCP 会话与 SSE 恢复(勿用 underscores_in_headers off 丢弃自定义头)
proxy_pass_request_headers on;
# SSE:关闭响应缓冲与缓存,否则事件会被攒包
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_read_timeout 600s;
proxy_send_timeout 60s;
send_timeout 600s;
}
若 nginx 作为 TLS 终止点,还需确认 HTTP/2 到后端的协议选择:部分团队对 SSE 仍使用 HTTP/1.1 到上游(proxy_http_version 1.1),以避免 HTTP/2 多路复用与个别实现的交互问题;应以压测为准,而非一刀切。
3. Envoy 示例(网关侧关键字段)
# 路由:/mcp 前缀到 streamable_mcp 集群
# stream_idle_timeout: 0 或 ≥ 600s(0 表示不按空闲切断,需结合组织安全基线)
# per_connection_buffer_limit_bytes: 对 SSE 可适当降低,避免大块缓冲
clusters:
- name: streamable_mcp
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
# 有状态 MCP:改为 HEADER_VALUE 并对 Mcp-Session-Id 做 ring_hash
http_filters:
# 确保自定义头进入上游;勿在 lua/wasm 中删除 Mcp-Session-Id
Envoy 的 route.timeout 对整段流式响应可能过短;对 MCP POST+SSE 应使用 stream_idle_timeout(路由或 HCM 级)与集群 common_http_protocol_options 中的 idle_timeout 分开评估。具体字段名以你使用的 Envoy 版本文档为准。
4. 推荐初始参数(10–50 并发 Agent Worker)
| 参数 | 建议起点 | 调参信号 |
|---|---|---|
上游 proxy_read_timeout / 流空闲超时 | 600s | SSE 中途 502/499;日志在工具完成前断流 |
| 客户端(Agent HTTP)读超时 | ≥ 上游超时 + 30s | 代理已放行但客户端先放弃 |
proxy_buffering | off(SSE 路径) | 工具日志「一次性」到达而非逐条 event |
| 会话粘性 | 按 Mcp-Session-Id hash | 无会话头 400 激增、Initialize 频繁重复 |
| 最大 POST body | 1–4 MiB(按 schema 定) | 大 tools/call 参数 413 |
| 空闲 GET SSE(监听通道) | 独立更长 idle 超时 | 规范允许客户端常开 GET 流收通知 |
5. Agent 网关侧的配合
- 对 MCP 客户端 HTTP 库关闭对 SSE 的「整段 body 聚合」;按 event 边界解析 JSON-RPC。
- 收到 HTTP 404 且带已失效的
Mcp-Session-Id时,按规范重新走Initialize,不要无限重试同一 ID。 - 用户取消任务时发 MCP
CancelledNotification,不要仅依赖关闭 TCP(与规范断线语义一致)。 - 在网关访问日志中记录
Mcp-Session-Id哈希前缀与jsonrpc.id,避免把工具参数全文写入日志。
风险与边界
- 安全基线冲突:规范建议本地服务绑定
127.0.0.1并校验Origin。把 MCP 直接暴露在公网反向代理后,必须叠加认证(规范称 SHOULD 对连接做认证),仅调超时无法弥补。 - 缓冲无法靠客户端绕过:即使 Agent 正确解析 SSE,中间 CDN 若强制缓冲
text/event-stream,仍需换路径或关闭 CDN 对该 location 的缓存。 - 多连接与消息唯一性:规范要求服务器每条 JSON-RPC 消息只在一个 SSE 流上发送,不得广播。负载均衡误配可能导致「半条流在 A、半条在 B」的诡异丢包,表现为 JSON-RPC id 永久悬挂。
- 续传非保证:
Last-Event-ID重放是服务器 MAY 行为;代理层断线后不能假设 exactly-once 交付,Agent 仍须工具幂等。 - HTTP/2 / HTTP/3 差异:不同跳数的协议协商会影响 SSE 分帧表现;变更协议时应重复压测长工具调用,而不是只测 Initialize 延迟。
- 观测缺口:若不在 MCP 层打 span,仅看代理 5xx 难以区分「上游工具慢」与「代理超时」;建议在 Agent 运行时对每次 POST 记录 TTFB 与首个 SSE event 时间戳。
验收清单
- 对返回 SSE 的
tools/call,首个event:在代理日志时间戳上早于响应完成数百毫秒至数秒(而非同一毫秒)。 - 初始化后连续 100 次 POST 均携带同一
Mcp-Session-Id且上游返回非 400。 - 人为缩短
proxy_read_timeout到 30s:长任务应出现可预期的断流;客户端显式取消后上游停止副作用(业务层验证)。 - 剥离
Mcp-Session-Id的对照实验应复现 400,确认代理配置未丢头。
参考来源
- MCP 规范(2025-03-26):Transports — Streamable HTTP(含
Mcp-Session-Id、SSE、Origin 要求) - MCP 规范源码:transports.mdx
- HTML 标准:Server-sent events(
Last-Event-ID、事件流格式) - nginx 文档:ngx_http_proxy_module(
proxy_buffering、proxy_read_timeout) - Envoy 文档:HTTP connection manager 与路由超时相关章节(按部署版本查阅)
- JSON-RPC 2.0:Specification(MCP 消息编码基础)