教程
Remote MCP Server 如何部署到生产环境?2026 无状态 MCP + HTTP Load Balancer + JSON-RPC 完整架构指南
协议无状态只在你真的用普通 HTTP 负载均衡时才值钱。Remote MCP 上生产,要解决的是副本怎么扩、SSE 怎么不被代理攒包、滚动发布怎么不掐长流——不是再发明一套 Session。
2026-07-28 之后,Remote MCP 不再靠 initialize 加 Mcp-Session-Id 把客户端钉在一台进程上。每个 JSON-RPC 请求在 _meta 里自带协议版本和客户端能力,Streamable HTTP 再把关键字段镜像到 HTTP 头,让负载均衡和网关不用拆 body 也能路由、限流、打点。生产上真正难的不是会不会写 tools/call,而是:副本怎么扩、均衡策略怎么选、SSE 进度流怎么不被 nginx 缓冲、滚动发布怎么不掐断 subscriptions/listen、应用状态放哪。站内已有 Stateless MCP 讲协议层为什么无状态,OAuth 2.1 讲谁能调;本文只补「怎么挂到 HTTP Load Balancer 后面」。传输原文见 Streamable HTTP。信封规则见 JSON-RPC 2.0。本地 stdio 子进程不要套这套公网拓扑。
生产卡点:会话亲和会把水平扩展退回单点
很多人把「无状态」听成「一台容器就够」。协议无状态的收益,只有在你真的用普通 HTTP 负载均衡——round-robin、least-conn、按 CPU 加权——而不是会话亲和时才会兑现。旧修订依赖连接级 Session:握手成功后,后续 tools/call 必须打回持有 Mcp-Session-Id 的那台机器。副本一多,你就得在 ALB / nginx 上开 cookie 粘滞或 IP 哈希;那台机器一挂,整条 Agent 会话一起死。2026-07-28 拿掉协议会话之后,任意副本应能独立完成任意一次 POST。
Cursor、Claude Desktop、OpenAI 远程 MCP、自建 Agent Host 不会温顺地串行打一个端点。同一秒里可能同时出现 tools/list、tools/call 和一条长寿命的 subscriptions/listen。这三跳不必落在同一实例。如果你仍按客户端 IP 做亲和,等于把水平扩展退回单点,还把限流和金丝雀发布一起绑死。协议入口见 MCP 是什么;Agent 循环本身见 AI Agent 是什么。
对照站内分层:无状态化拿掉的是协议 Session,不是业务数据。发票草稿、购物车、未完成的多轮工具调用,仍然要进 Redis 或数据库,用 arguments 里的 draftId 找回。不要把「副本内存里的 map」当成生产状态。鉴权也不在这层——公网 Remote MCP 的 Bearer 走 HTTP 头,细节见前一篇 OAuth。本文默认你已经分清这三层,只谈拓扑与运维合同。
| 做法 | 负载均衡看到什么 | 生产后果 |
|---|---|---|
| cookie / IP 亲和 + 进程内 Session | 必须把同一客户端粘回同一 upstream | 扩容难、滚动发布必断、单点故障 |
| 无状态副本 + 普通 HTTP LB | 任意副本可接任意 JSON-RPC POST | 可水平扩展、可金丝雀、可按工具限流 |
| Serverless 冷启动 + 单次 POST | 没有长驻进程可「记住」上一次握手 | 适合短 RPC;长 SSE 要单独设超时 |
目标架构:Client → HTTP LB → N 个无状态副本
目标拓扑应当很薄:公网 DNS → TLS 终止(ALB、NLB+sidecar、nginx、Caddy、云负载均衡)→ 一组相同镜像、相同配置的 MCP 副本。副本前面不要再挂「MCP Session Store」来补偿协议。健康检查走独立的 GET /healthz:只回答进程活着、依赖库能连;不要对 /mcp POST 空 body,也不要用一次 tools/list 当探活——那会打到真实工具注册表,还可能触发鉴权 401,把副本误摘掉。
每个副本必须能独立走完一跳:校验 Origin(防 DNS 重绑定,非法则 403)、读 MCP-Protocol-Version / Mcp-Method / Mcp-Name、必要时验 Bearer、解析 JSON-RPC 信封、执行工具、以单份 JSON 或请求级 SSE 返回。规范要求单一 MCP 端点只接受 POST,例如 https://mcp.example.com/mcp。GET 流端点和协议级 Session 已在 2026-07-28 删除,不要为「兼容旧探活」再开一条 GET /sse。
副本之间共享的是应用依赖:数据库、对象存储、第三方 API、可选的 Redis。不要共享「当前有哪些 MCP 连接」。Cloud Run、Cloud Functions、Knative 这类按请求扩缩的平台,正好吃这套模型;你要单独处理的是 SSE 长流的空闲超时,而不是 Session 粘滞。镜像里只放无特权运行用户,绑定在反代后面,不要把 MCP 进程直接听在 0.0.0.0:80 面向公网。
JSON-RPC 2.0 如何穿过负载均衡
MCP 用 JSON-RPC 2.0 编码消息,且必须是 UTF-8。Streamable HTTP 上,客户端发出的每一个请求或通知都是一次新的 HTTP POST;服务端不主动发起 JSON-RPC 请求。负载均衡不需要理解 method 的业务含义,它只转发字节。2026 的传输把 method 镜像成 Mcp-Method,把 tools/call / resources/read / prompts/get 的名字镜像成 Mcp-Name,就是为了让中间件不拆 body 也能按工具限流、按方法拆队列、按版本做金丝雀。
body 仍是真相。头里的 MCP-Protocol-Version 必须与 params._meta.io.modelcontextprotocol/protocolVersion 字节一致,否则服务端必须 400 并回 HeaderMismatch。客户端还必须带 Accept: application/json, text/event-stream。通知类 POST 成功则 202 Accepted、无 body;请求类则返回一份 JSON 对象或一条 SSE。JSON-RPC 的 id 只用于这一跳请求-响应对齐,不要拿它当会话号,也不要在副本之间用 id 去「找回上下文」。
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32600,
"message": "HeaderMismatch: MCP-Protocol-Version does not match params._meta"
}
}
下面是一跳生产形态的 tools/call:鉴权在 HTTP 头,信封在 body,网关能看见的路由键也在头里。把 Access Token 塞进 params 或 _meta 不是 2026 的 Remote MCP。工具参数形状仍要 Schema 校验,见 MCP 与 JSON Schema。
POST /mcp HTTP/1.1
Host: mcp.example.com
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: searchInvoices
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "searchInvoices",
"arguments": {
"startDate": "2026-01-01",
"endDate": "2026-01-31",
"status": "paid"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "jsonvue-demo",
"version": "1.0.0"
}
}
}
}
Streamable HTTP:缓冲、超时与 SSE
短工具调用应回 Content-Type: application/json。长任务可以回 text/event-stream:先推与本请求相关的 notifications/progress,最后一条 JSON-RPC 响应结束流。生产事故多半出在反向代理,而不是 MCP SDK。nginx 默认 proxy_buffering 会把进度事件攒成一块再刷给客户端,Agent 看起来像卡死;规范明确建议响应带 X-Accel-Buffering: no,并指出这是写给反代看的。对照 nginx proxy_buffering。
取消信号在 Streamable HTTP 上是客户端关掉这条 SSE,不是再 POST 一条 notifications/cancelled(那是 stdio 绑定的做法)。因此 LB 必须把后端断连如实传到客户端,也必须把客户端断连如实传到副本,好让 worker 尽快停手。不要在中间加一层会「重试 POST」的网关:JSON-RPC 请求默认不是幂等的,tools/call 可能已经写过库。
subscriptions/listen 是另一类长流:响应流保持打开,推送 tools/list_changed 一类变更,而不是某次调用的 progress。规范鼓励周期性发 SSE 注释行(以冒号开头)做 keep-alive,避免空闲期被中间件掐断。不支持用 Last-Event-ID 续传。所以 LB 的 idle / read timeout 必须长过你的 keep-alive 间隔;Cloudflare、ALB、nginx 默认 60 秒往往不够。下面是一份最小可用的反代示意——不要当安全基线,TLS、限流和 WAF 另配。
upstream mcp_replicas {
least_conn;
server 10.0.1.11:8080;
server 10.0.1.12:8080;
server 10.0.1.13:8080;
}
server {
listen 443 ssl;
server_name mcp.example.com;
location /healthz {
proxy_pass http://mcp_replicas;
proxy_connect_timeout 2s;
proxy_read_timeout 3s;
}
location /mcp {
proxy_pass http://mcp_replicas;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
滚动发布、排空与 subscriptions/listen
无状态副本让滚动发布变简单,但 SSE 仍是进行中的 HTTP 请求。正确顺序:把实例从目标组摘掉 → 停止把新的 /mcp 打过来 → 等待已打开的 JSON 响应与 SSE 结束,或等到你公布的 drain 超时 → 再 SIGTERM worker。不要对正在推 progress 的进程直接 SIGKILL。健康检查变绿,才把实例加回。
新版本必须仍能处理旧请求形状。没有「先升级握手、再切流量」的协议步骤;金丝雀是按百分比把 POST 分到新镜像。头里的 MCP-Protocol-Version 可以做路由键:只让声明 2026-07-28 的客户端进新池,旧客户端留在兼容池。版本对不上时回 400 加 UnsupportedProtocolVersionError,不要默默降级后继续执行工具。
subscriptions/listen 在发布窗口几乎一定会被掐断。客户端应当重开 listen,不要假设事件不丢。服务端也不要在进程内存里堆积「尚未投递的 list_changed」。需要可靠投递,就写进外部队列,listen 只是订阅口。MRTR(多轮输入)同样是多次独立 POST:中间结果进共享存储,下一跳可以落到另一副本。
网关限流、鉴权、观测与 JSONVue
网关看得见 Mcp-Method 和 Mcp-Name,就该按工具设 QPS,而不是只按源 IP。贵重工具(写库、调支付、跑长 SQL)配额单独算;tools/list 可以松一些。公网必须验 Origin,并按 OAuth 2.1 做 Resource Server——401、Protected Resource Metadata、每跳 Bearer,见 MCP OAuth 2.1。鉴权在传输层,限流在网关层,Schema 在业务层,三层不要揉进一个中间件。
| 观测字段 | 从哪来 | 用来干什么 |
|---|---|---|
| MCP-Protocol-Version / Mcp-Method / Mcp-Name | 请求头(与 body _meta 对齐) | 按工具限流、金丝雀、仪表盘 |
| JSON-RPC id | 这一跳信封 | 把客户端重试和副本日志对上 |
| HTTP 状态 + JSON-RPC error.code | 传输层 vs 方法层 | 区分 401 / HeaderMismatch / 业务 error |
访问日志至少留下上面三组。副本日志再加 jsonrpc 是否为 2.0、工具是否执行到副作用之前。头与 body 不一致、缺 Accept、非法 Origin,都应在网关或副本入口被拦下,不要落到工具函数。失败样本各留一条:HeaderMismatch、401、arguments 缺字段、被缓冲的 SSE(客户端只收到最后一块)。
浏览器里即可完成联调:JSON 格式化看信封能不能 parse;JSON Schema 校验核 params.arguments;JSON Diff对比两份失败响应;JWT 解码看公网部署里 Bearer 的 aud。数据不离开本机。
延伸阅读:MCP 是什么、Stateless MCP、OAuth 2.1、MCP 与 JSON Schema、A2A vs MCP。
常见问题 FAQ
生产环境还要不要开 sticky session?
按 2026-07-28,协议层不需要。开 sticky 只会让你以为「还在用 Session」。应用若必须粘到同一区域或同一数据分片,用 Mcp-Param-* 或 arguments 里的租户键做应用层路由,不要用 cookie 亲和去绑 MCP 进程。
能不能用 GET /mcp 做负载均衡健康检查?
不要。现代 MCP 端点只接受 POST;GET 流已删除。对 /mcp 乱发 GET 会拿到 405 或被旧兼容逻辑误判。探活用独立 /healthz,只检查进程与依赖,不执行工具。
SSE 被掐断,客户端要不要用 Last-Event-ID 续上?
规范写明不支持可恢复 SSE。客户端应重开对应请求(listen 就重新 subscriptions/listen,长工具就决定是否按业务幂等重试)。服务端用注释行 keep-alive,反代关闭缓冲,比自己实现 event id 缓存更符合合同。
本地 stdio MCP 也要挂负载均衡吗?
不要。stdio 是客户端拉起的子进程,字节流在标准输入输出上,没有 HTTP hop。负载均衡、Origin 校验、Bearer、X-Accel-Buffering 都是 Streamable HTTP / Remote 的事。同一套工具可以同时提供两种传输,生产拓扑只套在 HTTP 面上。
总结与下一步
Remote MCP 的生产架构可以写成一句:无状态 JSON-RPC 请求穿过普通 HTTP 负载均衡,落到任意相同副本;长流按请求作用域打开,不按连接作用域记住客户端。头给网关看,body 是真相,应用状态进外部存储。
落地顺序:先让任意副本独立完成一次 tools/call,再关缓冲、拉长超时、加 drain,最后才做按工具限流和金丝雀。用 JSONVue 把成功信封、HeaderMismatch、401 样本留在本地。需要协议语义读 Stateless MCP;需要谁能调用读 OAuth 2.1。