iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0

昨天把 MCP 的 JSON-RPC 訊息拆開來看,知道 initializetools/call 這些訊息實際長什麼樣。

但還有一個問題:

這些 JSON-RPC 訊息,到底怎麼從 Client 送到 Server?

MCP 目前主要有兩種傳輸方式:

  • stdio
  • Streamable HTTP

差別不只是「一個走本機、一個走網路」。

真正會影響的是 Server 怎麼部署、狀態放在哪裡,以及多個 Client 要怎麼連它

今天用同一個 counter Tool 跑幾組實驗,直接看差異。

https://ithelp.ithome.com.tw/upload/images/20260922/20161224KeXqaUvrwb.png

一、stdio:行程就是連線

先看最熟悉的 stdio

Host 啟動一個 MCP Server 子行程,往它的 stdin 寫 JSON-RPC,再從 stdout 收回結果。

可以先把它想成:

Host
  │
  ├── stdin  ──→ MCP Server
  │
  └── stdout ←── MCP Server

這次我同時建立兩個 Client,各自啟動一個 Server,再各呼叫兩次 counter

結果:

客戶端 1(server pid=447244)→ count=2 pid=447244
客戶端 2(server pid=447251)→ count=2 pid=447251

兩邊的 count 都是 2,但 PID 不同。

原因很直接:它們根本是兩個不同的 Server 行程。

所以行程裡的記憶體也是各自獨立的。

接著把第一個 Server 的 stdin 關掉:

server 行程 447244 跟著結束了,exit code = 0

對 stdio 來說,Client 和 Server 的生命週期綁得很緊。

連線結束,行程通常也跟著結束,存在行程記憶體裡的狀態自然就消失。

這很適合 Claude Code 這類本機工具:簡單、不需要開 Port,也可以直接沿用本機作業系統的權限。

但如果 Server 要提供給多人共用,或部署成遠端服務,就不會是 stdio 最擅長的場景。

二、Streamable HTTP:Server 可以獨立存在

換成 Streamable HTTP 後,Tool 本身完全不用改:

@mcp.tool()
def counter() -> str:
    _state["count"] += 1
    return f"count={_state['count']} pid={os.getpid()}"

差別只在啟動方式:

mcp.run(
    transport="streamable-http",
    host="127.0.0.1",
    port=8931,
)

這時 Server 不再是某一個 Client 專屬的子行程,而是一個可以獨立運行的 HTTP 服務。

Client 透過:

POST /mcp

送 JSON-RPC 訊息。

也就是昨天看到的:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize"
}

內容沒變,只是今天換了一條路送出去。

三、有狀態 HTTP:用 Session 把請求串起來

先跑有狀態模式。

Client 完成 initialize 後,Server 的回應 Header 會帶回:

Mcp-Session-Id

例如:

mcp-session-id: bde579150edf433984d77f43b74cf315

後續請求要帶著這個 Session ID。

我故意不帶 Session 就直接呼叫 Tool:

HTTP 400

接著帶上同一個 Session,連續呼叫三次:

第 1 次 → count=1 pid=447256
第 2 次 → count=2 pid=447256
第 3 次 → count=3 pid=447256

這裡最重要的不是 count,而是三次請求可以被 Server 視為同一個 Session 的後續操作。

所以有狀態 HTTP 可以把多次 HTTP Request 串成同一段 MCP Session。

如果部署多個 replica,而且 Session state 只存在單一行程裡,就要再處理 sticky session,或把 Session state 放到共享儲存。

四、無狀態 HTTP:沒有 Session,不代表程式沒有狀態

接著把 Server 改成:

stateless_http=True

這次 initialize 不再取得 Mcp-Session-Id,後續 Request 也不需要攜帶 Session。

看起來很合理。

結果連叫三次 counter

第 1 次 → count=1 pid=447264
第 2 次 → count=2 pid=447264
第 3 次 → count=3 pid=447264

等等。

不是無狀態嗎?

為什麼 count 還在累加?

問題出在這裡:

_state = {"count": 0}

_state 是 Python 行程裡的全域變數。

stateless_http=True 拿掉的是 MCP 協定層的 Session,不會幫你清掉程式自己存在記憶體裡的資料。

所以:

協定無狀態 ≠ 程式自動無狀態

這個差別很重要。

五、真正的問題要到多副本才看得出來

單一 Server 行程很容易讓人誤以為「反正 count 還是會保存」。

所以這次直接開兩個無狀態 replica,模擬負載平衡器輪流送 Request:

請求 1 → replica A  count=1 pid=447296
請求 2 → replica B  count=1 pid=447301
請求 3 → replica A  count=2 pid=447296
請求 4 → replica B  count=2 pid=447301

問題馬上出現。

使用者明明送了四次請求,但:

replica A 認為自己收到 2 次
replica B 也認為自己收到 2 次

因為兩台機器根本不知道對方記憶體裡有什麼。

這不是 MCP 的問題,而是應用程式把狀態放錯地方。

如果要做真正可以水平擴充的無狀態服務,跨 Request 需要共享的資料就不能只放在:

_state = {}

而要移到例如:

Redis
Database
其他共享儲存

這樣 Request 打到哪一台 replica,都能看到同一份狀態。

六、stdio 和 HTTP 怎麼選?

整理成一張表:

stdio HTTP 有狀態 HTTP 無狀態
Server 型態 Client 啟動的行程 獨立 HTTP 服務 獨立 HTTP 服務
Session 跟著行程 MCP Session 不保存 MCP Session
多 Client 通常各自一個行程 可以 可以
水平擴充 不是主要使用場景 需處理 Session state 最容易擴充
常見場景 本機工具 有連續互動狀態的服務 API / 高併發服務

我的判斷方式會很簡單。

如果 MCP Server 要操作使用者本機的檔案、Git repository 或開發環境:

→ stdio

如果是遠端服務,而且一次互動需要跨 Request 保留 Session:

→ Streamable HTTP,有狀態

如果希望任何 replica 都能處理任何 Request:

→ Streamable HTTP,無狀態

但這時應用程式自己的共享狀態也要一起外置。

所以選 transport,其實也是在選部署方式。

七、這次踩到的兩個坑

第一個是 Accept Header。

Streamable HTTP Client 要能處理:

Accept: application/json, text/event-stream

如果只寫:

Accept: application/json

這次實測會收到 406

有趣的是,curl 沒手動指定 Accept 時通常會送:

Accept: */*

反而可以通過。

所以會出現一個很怪的情況:

curl 可以,自己寫的 Client 不行。

第二個是 SSE。

如果回應的 Content-Type 是:

text/event-stream

就不能直接:

resp.json()

要先從 SSE 裡取出:

data: {...}

再解析裡面的 JSON。


昨天我們把 MCP 的 JSON-RPC 訊息攤開來看。

今天補上最後一塊:

JSON-RPC 決定「訊息長什麼樣」
Transport 決定「訊息怎麼送過去」

而 stdio 跟 Streamable HTTP 的差異,最後其實都會回到同一件事:

你的 MCP Server 準備跑在哪裡?

明天進入 Day 10。

前面幾天都是把 MCP 一層一層拆開,接下來把它們組回去:用 Python SDK 寫出第一個真正可以掛進 Claude Code 的 MCP Server。


上一篇
Day 8:JSON-RPC 2.0 與 MCP 訊息生命週期,不用 SDK 手寫一次
下一篇
Day 10:實作,用 Python SDK 寫一個 MCP Server
系列文
協定、框架、架構:一條龍搞懂 AI Agent 是怎麼被造出來的10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言