教程
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。