Agent 與 MCP
MCP 部署實戰:transport、StreamableHTTP 與水平擴展
MCP 部署實戰:transport、StreamableHTTP 與水平擴展
系列最後一篇,我們談部署。MCP client 與 server 交換的 JSON 訊息,實際上是怎麼送到對方手上的?這個通道叫 transport。開發期人人都用 stdio,一到要上線、要擴展,事情就複雜起來:HTTP 天生不讓 server 主動找 client,而 MCP 偏偏是雙向協定。這篇把訊息類型、stdio、StreamableHTTP 與擴展時的取捨一次講完。
你將學到什麼
訊息的兩大類
request 配 result 成對出現,notification 單向不回。
stdio transport
子行程加標準輸入輸出,開發期的理想狀態。
StreamableHTTP
用 SSE 繞過 HTTP 限制,讓 server 能回頭說話。
擴展的代價
stateless_http 換來水平擴展,也砍掉一票功能。
先懂訊息:兩大類、雙向流動
MCP 的訊息類型定義在官方規格庫裡,與各語言的 SDK 分開維護(規格用 TypeScript 描述資料結構,只是描述方便,不是要你跑 TypeScript)。訊息分兩大類:
- request 與 result:成對出現,有問必答。例如 CallToolRequest 對 CallToolResult、InitializeRequest 對 InitializeResult。
- notification:單向通知,不需要回應。例如進度通知、日誌通知、工具清單變更通知。
關鍵在於:MCP 是雙向協定,client 與 server 都可以主動開口。規格甚至直接按「誰發的」分類訊息。server 主動發起的那些訊息,sampling 的 create message 請求、list roots 請求、各種通知,正是後面所有麻煩的來源。
stdio transport:開發期的理想狀態
開發時最常用 stdio transport:client 把 server 當子行程啟動,訊息走標準輸入輸出。client 寫進 server 的 stdin,server 回到 stdout,任何一方隨時能開口,前提是兩者在同一台機器上。你甚至可以直接在終端機啟動 server,手動貼 JSON 訊息進去,立刻看到它的回應。
每一條 MCP 連線都從三步握手開始:
- client 送出 Initialize Request。
- server 回覆 Initialize Result,帶上自己支援的能力。
- client 送出 Initialized Notification 確認,這一步不需要回應。
握手完成後,才能送工具呼叫這些正式請求。stdio 之所以是理想的基準線,在於四種溝通方向全部暢通,而且只靠兩條通道:
- client 對 server 發請求:寫進 stdin。
- server 回應 client:寫到 stdout。
- server 主動對 client 發請求:一樣走 stdout。
- client 回應 server:一樣走 stdin。
先把這個「完整版」放在心上,接下來看 HTTP 為什麼做不到。
HTTP 的先天限制
要讓 server 部署在遠端、開放給所有人連,走 HTTP 最自然。但標準 HTTP 的形狀是:client 知道 server 的網址,隨時能發請求;server 卻沒有 client 的網址,無法主動發起請求。這不是 MCP 的設計缺陷,是 HTTP 模型本身的形狀。
於是 server 發起的那些訊息類型,sampling 請求、list roots、進度與日誌通知,在純 HTTP 底下全部卡住。進度條消失、日誌不動、sampling 直接失敗,症狀就是這麼來的。
StreamableHTTP:用 SSE 繞過去
StreamableHTTP transport 的解法是 Server-Sent Events(SSE)。初始化時 server 的回應會帶一個 mcp-session-id 標頭,之後所有請求都要帶上這個 session id。接著 client 發一個 GET 請求,建立一條長駐的 SSE 連線,server 從此有了一條隨時能對 client 說話的通道。
工具呼叫時還會再開第二條:主要 SSE 連線長駐,負責 server 發起的請求與進度通知;每次工具呼叫另開一條工具專屬的 SSE 連線,日誌與工具結果走這裡,結果送完自動關閉。
除錯時記得腦中有這兩條線:進度通知走主要連線、日誌與結果走工具專屬連線,看到其中一類訊息沒到,先想想是不是對應的那條連線出了狀況。
stateless_http 與 json_response 的取捨
server 紅了、幾千個 client 湧進來,單一機器扛不住,標準解法是水平擴展:多台機器排在負載平衡器後面。問題來了:同一個 client 的 GET SSE 連線與 POST 工具呼叫,可能被分到不同機器上;工具若要用 sampling,處理 POST 的那台就得跟握著 SSE 的那台協調,複雜度爆炸。
設 stateless_http=True 可以消掉協調問題,但代價很硬:沒有 session id、沒有 server 對 client 的請求、沒有 sampling、沒有進度回報、沒有訂閱通知。好處除了可以擴展,還有一個:連初始化握手都免了,client 可以直接發請求。另一個旗標 json_response=True 比較單純:關掉 POST 回應的串流,不再有中途的進度與日誌,只拿最後結果的純 JSON。
| 旗標 | 打開後失去什麼 | 什麼時候該開 |
|---|---|---|
stateless_http=True | session 追蹤、server 發起的請求、sampling、進度回報、訂閱通知 | 要在負載平衡器後面水平擴展,且工具不倚賴這些功能 |
json_response=True | 串流回應:中途的進度與日誌訊息 | 整合端只想收單純的 JSON 最終結果 |
取捨的判準很單純:把上一篇講的 sampling、通知、roots 拿出來對照,你的工具用到其中任何一樣,就別急著開 stateless_http;真的要開,先確認功能上的犧牲你吞得下去。反過來說,純工具型、不需要 server 回頭說話的服務,開了反而整合更簡單。