第 16 篇接上一個檔案系統伺服器之後,Claude Code 的工具清單多了 14 個工具,模型開的單也從 Read 變成了 mcp__fs__read_text_file。可是那張單交出去以後走哪條線、用什麼格式送到伺服器手上,終端機裡一行都看不到。所以這次我把那條線剪開,在中間接一個 tee。
先講結論:MCP 的底層是 JSON-RPC 2.0,一行一個 JSON,請求帶 id、通知不帶。 這套協定今年 7 月剛改過規矩,從「先握手再說話」改成「每個請求自己帶版本」,而 Claude Code 兩種都會講。
走 stdio 的 MCP 伺服器,就是 Claude Code 啟動的一個子行程。在中間插一個 tee,每一行都看得到:
#!/bin/bash
# wrap.sh:把兩個方向的訊息原樣記下來
tee -a in.log | npx -y @modelcontextprotocol/server-filesystem "$PWD" | tee -a out.log
把 MCP 設定的 command 指向 wrap.sh,請 Claude Code 只用這個伺服器讀一次 order.py。我跑了一次(2026-10-01,Claude Code 2.1.286),依時間排起來是這樣:
Claude: {"id":"server-discover-probe-1","method":"server/discover",
"params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28", …}}}
server: {"id":"server-discover-probe-1","error":{"code":-32601,"message":"Method not found"}}
Claude: {"id":0,"method":"initialize","params":{"protocolVersion":"2025-11-25",
"capabilities":{"roots":{"listChanged":true},"elicitation":{"form":{},"url":{}}},
"clientInfo":{"name":"claude-code","version":"2.1.286", …}}}
server: {"id":0,"result":{"protocolVersion":"2025-11-25",
"capabilities":{"tools":{"listChanged":true}},
"serverInfo":{"name":"secure-filesystem-server","version":"0.2.0"}}}
Claude: {"method":"notifications/initialized"}
Claude: {"id":1,"method":"tools/list"}
server: {"id":0,"method":"roots/list"}
Claude: {"id":0,"result":{"roots":[{"uri":"file:///<repo>"}]}}
server: {"id":1,"result":{"tools":[ … 14 個工具 … ]}}
Claude: {"id":2,"method":"tools/call","params":{"name":"read_text_file",
"arguments":{"path":"<repo>/order.py"},
"_meta":{"claudecode/toolUseId":"toolu_01PUnK5Cyati3AboQhZkzWsG", …}}}
server: {"id":2,"result":{"content":[{"type":"text","text":"def calcRefund(o, pct): …"}]}}
(只跑一次,是例子不是證據。)
server/discover 宣告 2026-07-28 版,伺服器回 -32601,JSON-RPC 的「找不到這個方法」。initialize 改用 2025-11-25 版,雙方交換能力(capabilities)。notifications/initialized 沒有 id,是通知,伺服器不用回。tools/list 拿到的 14 個工具,就是上一篇清單多出來的那一批。roots/list,Claude Code 回了專案路徑。tools/call 帶著 toolu_…,正是 API 回應裡 tool_use 區塊的 id。模型開的單,在這裡變成一個 JSON-RPC 請求。讓另一個行程的函式用起來像本地呼叫,叫遠端程序呼叫(remote procedure call,RPC),經典的實作論文是 Birrell 與 Nelson 1984 年的〈Implementing Remote Procedure Calls〉。MCP 用的 JSON-RPC 2.0 自稱「無狀態、輕量的 RPC 協定」,實驗裡的規則都來自它:回應用同一個 id 對上請求,沒有 id 的是通知、不准回,-32601 是方法不存在。
今天最大的 RPC 實作是 gRPC,名字是遞迴縮寫:gRPC Remote Procedure Calls。它源自 Google,以 Protocol Buffers 為主要格式、支援全雙工串流(官方 FAQ,2026-10-01 查)。跟 JSON-RPC 比,訊息是二進位、型別先用 .proto 定好、跑在 HTTP/2 上。 代理的世界兩種都在用:
我的看法是,JSON-RPC 犧牲了一點效能,換到你能用 tee 直接讀懂每一行。換成 gRPC,得先拿到 .proto 才解得開。
MCP 在 JSON-RPC 之上分了角色。主機(host,這裡是 Claude Code)管多個用戶端(client),每個用戶端只連一個伺服器。規格的第一條設計原則是伺服器要極度容易寫,複雜的協調留給主機(2026-10-01 查)。
| 原語 | 誰決定什麼時候用 | 方法 |
|---|---|---|
| 工具(tools) | 模型 | tools/list、tools/call |
| 資源(resources) | 應用程式 | resources/list、resources/read |
| 提示詞(prompts) | 使用者 | prompts/list、prompts/get |
提示詞在 Claude Code 裡會變成 /伺服器名:提示詞名 的斜線指令,你打了才執行(MCP 文件,2026-10-01 查)。反方向,伺服器也能透過主機要東西:實驗裡的 roots/list 問的是能碰哪些目錄,elicitation 是向使用者要資料,而規格明文規定敏感資訊不准用表單要,必須導到外部網址。
傳輸只有兩種,訊息的意思完全一樣(傳輸頁):stdio 是子行程、一行一個 JSON,實驗用的就是它。Streamable HTTP 是每則訊息一個 POST,回應是 JSON 或 SSE 串流,遠端伺服器走這條,用 claude mcp add --transport http <名稱> <網址> 接上。
Ehtesham et al.(2025)比較了四種代理互通協定:
| 協定 | 管什麼 | 怎麼傳 | 怎麼找到對方 |
|---|---|---|---|
| MCP | 代理呼叫工具 | JSON-RPC | 主機設定檔寫好 |
| ACP(Agent Communication Protocol) | 代理之間的通用訊息 | RESTful HTTP | 線上、離線探索 |
| A2A(Agent2Agent) | 代理之間委派任務 | JSON-RPC、gRPC、HTTP+JSON | Agent Card 宣告能力 |
| ANP(Agent Network Protocol) | 開放網路上的代理協作 | W3C 去中心化識別碼與 JSON-LD | 可驗證的身分,不靠中央目錄 |
MCP 管代理往下接工具,另外三種管代理跟代理之間。 這張表寫於 2025 年 5 月,之後 ACP 官網宣布已併入 Linux 基金會底下的 A2A(2026-10-01 查)。你在 Claude Code 每天碰到的只有 MCP。另外三種屬於多代理(multi-agent),而且是跨團隊、跨廠商的那一種:同一個系統裡的子代理彼此呼叫用不到它們,代理要跟別人家的代理說話時才需要。
現行的 2026-07-28 版把 MCP 改成無狀態:沒有握手,每個請求都在 _meta 裡自己帶版本與能力,伺服器也不再主動發請求。舊版(2025-11-25 以前)是先 initialize 建立工作階段,還允許伺服器反問,就像那句 roots/list(版本頁,2026-10-01 查)。
規格的相容性表寫明:兩版都會的用戶端碰到舊版伺服器,在 stdio 上先送 server/discover 試探,收到不認得的錯誤就退回 initialize。 實驗的前四行就是這一格。規格給的理由是每個請求要能單獨處理,不准依賴同一條連線上之前的請求。我的理解是,這對遠端伺服器最有感,不用替每條連線記工作階段,前面放負載平衡器也單純。
第一,兩個版本要一起養。 那一趟失敗的 server/discover 就是相容的成本。
第二,伺服器會反過來問。 一句 roots/list,專案路徑就交出去了。換成別人寫的伺服器,上一篇那句「工具代表任意程式碼的執行」要記著。
第三,stdio 伺服器是一個用你的權限在跑的行程。 它能讀的,不只是那 14 個工具會碰的東西。
多了什麼能力:你看得懂 MCP 的每一行,知道一張
tool_use怎麼變成 JSON-RPC 請求、伺服器能給的三種東西、兩種傳輸,以及 MCP 在四種代理協定裡站在哪裡。多付了什麼代價:一個要相容兩個版本的協定、一個會反問的伺服器,以及一個用你的權限在跑的子行程。
一個 MCP 伺服器,說穿了就是讀一行 JSON、回一行 JSON 的程式。自己寫一個要多少行?
下一篇把第 16 篇那個算退款的工具做成 MCP 伺服器,接進 Claude Code。