iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Claude AI

從 LLM 到 Agent:用 Claude 拆解現代 AI 工程的每一層系列 第 17 篇

MCP 在底下說什麼:攔下 Claude Code 跟伺服器之間的對話紀錄

  • 分享至 

  • xImage
  •  

第 16 篇接上一個檔案系統伺服器之後,Claude Code 的工具清單多了 14 個工具,模型開的單也從 Read 變成了 mcp__fs__read_text_file。可是那張單交出去以後走哪條線、用什麼格式送到伺服器手上,終端機裡一行都看不到。所以這次我把那條線剪開,在中間接一個 tee。

先講結論:MCP 的底層是 JSON-RPC 2.0,一行一個 JSON,請求帶 id、通知不帶。 這套協定今年 7 月剛改過規矩,從「先握手再說話」改成「每個請求自己帶版本」,而 Claude Code 兩種都會講。


30 秒實驗

走 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): …"}]}}

(只跑一次,是例子不是證據。)

  1. 先用新版試探:server/discover 宣告 2026-07-28 版,伺服器回 -32601,JSON-RPC 的「找不到這個方法」。
  2. 退回舊版握手:initialize 改用 2025-11-25 版,雙方交換能力(capabilities)。
  3. notifications/initialized 沒有 id,是通知,伺服器不用回。
  4. tools/list 拿到的 14 個工具,就是上一篇清單多出來的那一批。
  5. 伺服器反問 roots/list,Claude Code 回了專案路徑。
  6. tools/call 帶著 toolu_…,正是 API 回應裡 tool_use 區塊的 id。模型開的單,在這裡變成一個 JSON-RPC 請求。

底層是一個 1984 年就有的想法

讓另一個行程的函式用起來像本地呼叫,叫遠端程序呼叫(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 上。 代理的世界兩種都在用:

  • MCP 只有 JSON-RPC。 Google 的工程師 2025 年 8 月提了 SEP-1352,想把 gRPC 加成官方傳輸,理由是省頻寬、好解析,企業內部本來就跑 gRPC。提案人 2026 年 1 月自己關掉了,因為 MCP 宣布官方傳輸維持兩種、改讓自訂傳輸更好接,gRPC 走那條路比強制每個用戶端實作可行。
  • A2A(Agent2Agent)三種都有。 1.0.0 版定義 JSON-RPC 2.0、gRPC、HTTP+JSON 三種綁定,要求功能等價(2026-10-01 查)。

我的看法是,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 <名稱> <網址> 接上。


MCP 不是唯一的代理協定

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),而且是跨團隊、跨廠商的那一種:同一個系統裡的子代理彼此呼叫用不到它們,代理要跟別人家的代理說話時才需要。


7 月那一版改了什麼

現行的 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。


延伸閱讀


上一篇
工具呼叫與 MCP:同一種呼叫請求,不同的執行端
系列文
從 LLM 到 Agent:用 Claude 拆解現代 AI 工程的每一層 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言