Agent 與 MCP

MCP 部署實戰:transport、StreamableHTTP 與水平擴展

MCP 的訊息分成 request 與 result 成對出現的請求,以及單向的 notification,而且 client 與 server 都能主動開口。開發期最適合 stdio transport;要部署成遠端服務就用 StreamableHTTP,它靠 SSE 讓 server 能回頭對 client 說話。需要水平擴展時開 stateless_http,但會犧牲 sampling 與進度通知這些倚賴雙向溝通的功能。
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 連線都從三步握手開始:

  1. client 送出 Initialize Request。
  2. server 回覆 Initialize Result,帶上自己支援的能力。
  3. 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 連線,日誌與工具結果走這裡,結果送完自動關閉。

除錯時記得腦中有這兩條線:進度通知走主要連線、日誌與結果走工具專屬連線,看到其中一類訊息沒到,先想想是不是對應的那條連線出了狀況。

MCP client MCP server POST 初始化,取得 session id GET 主要 SSE 連線(長駐,server 主動訊息) POST 工具呼叫+專屬 SSE(結果送完即關) 之後每個請求都要帶 mcp-session-id
StreamableHTTP 的雙連線模型:長駐 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=Truesession 追蹤、server 發起的請求、sampling、進度回報、訂閱通知要在負載平衡器後面水平擴展,且工具不倚賴這些功能
json_response=True串流回應:中途的進度與日誌訊息整合端只想收單純的 JSON 最終結果

取捨的判準很單純:把上一篇講的 sampling、通知、roots 拿出來對照,你的工具用到其中任何一樣,就別急著開 stateless_http;真的要開,先確認功能上的犧牲你吞得下去。反過來說,純工具型、不需要 server 回頭說話的服務,開了反而整合更簡單。

部署前的一句忠告本機用 stdio 開發、上線用 HTTP 部署的話,請在開發階段就改用正式環境的 transport 測試。stateful 與 stateless 的行為差異很大,問題要在部署前抓到,不是部署後。
參考出處本文取材自 Anthropic 官方 Claude Academy 免費課程「Model Context Protocol: Advanced Topics」,由酒Ann 消化後以自己的視角重新編寫。想看英文原版課程,可到 Claude Academy 修習。
把這篇文章分享給需要的人FacebookLINEThreadsX

常見問答

開發時該用哪種 transport?
stdio 最適合開發與測試:client 把 server 當子行程啟動,訊息走標準輸入輸出,雙向暢通。但若正式環境要走 HTTP,開發階段就該改用同一種 transport 測試。
StreamableHTTP 為什麼需要 session id?
初始化時 server 會在回應標頭給一個 mcp-session-id,之後所有請求都要帶著它,server 才認得這個 client,並把 SSE 訊息送對地方。
開了 stateless_http 會失去什麼?
server 不再追蹤個別 client:沒有 server 發起的請求、沒有 sampling、沒有進度回報與訂閱通知。換來的是可以放進負載平衡器水平擴展。
json_response 跟 stateless_http 差在哪?
json_response 只關掉 POST 回應的串流,改拿單一的最終 JSON 結果;stateless_http 影響更大,直接拿掉 session 與 server 對 client 的整條溝通路徑。