iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Claude AI

盡信 Claude,不如無 Code — 心法與全端實戰系列 第 14 篇

Day 14 MCP 支援 X?規格、SDK、你手上這支,三個答案要打一次才知道

  • 分享至 

  • xImage
  •  

前 13 天,工具都裝在自己這邊。從今天開始要把它們接到別人的東西 —— 而 MCP 就是那個接頭。

問題是這個接頭一直在變。MCP 規格 20 個月出了五版(2024-11-05、2025-03-26、2025-06-18、2025-11-25、2026-07-28),SDK 跟著改,教學文停在其中某一版,平台又是另一版。所以「MCP 支援 X」這句話現在有三個答案:規格支援、SDK 支援、你手上這支 server 支援 —— 三個常常不一樣。文件追不上實作的時候,翻更多文件沒有用,因為文件一直在變。

能回答的只有 lab。而這種 lab 便宜到過分:把設定檔拿掉,自己手打一次協定,一支 server 三行就問完。本文之後的每一次探針,都是我一句話讓 Claude 去打、把回應貼回來 —— 一個下午可以跑十幾個。今天就是這樣的一份 lab 紀錄。


練習:三行 JSON 看完整個協商

MCP 的 stdio 傳輸就是 JSON-RPC over 標準輸入輸出。意思是你可以用 echo 跟它講話。

這一段可以直接照抄,不需要任何設定檔:

printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"manual","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| npx -y @modelcontextprotocol/server-filesystem /tmp

三行 JSON 就是完整的握手:

訊息 做什麼
initialize 我是誰、我支援哪個版本、我有什麼能力
notifications/initialized 握手完成的通知(沒有 id,不需要回應)
tools/list 你有哪些工具?

第二行最容易被忽略 —— 它沒有 id,因為它是通知不是請求。規格(2025-11-25 Lifecycle)寫的是:初始化必須是第一個互動,握手成功後 client 必須送 initialized。我故意試了兩次違規:不送這行、以及把 tools/list 排在 initialize 前面 —— 這支 server 兩次都照答 14 支工具,一個錯都沒報。規格說 MUST,它不檢查。 別把這當成可以省:規格允許 server 在收到 initialized 之前不理你,換一支照規格嚴格實作的,同樣的違規就可能卡住,而你不會知道是哪一行的問題。

我這次跑回來的第一行是:

{"result":{"protocolVersion":"2025-06-18",
 "capabilities":{"tools":{"listChanged":true}},
 "serverInfo":{"name":"secure-filesystem-server","version":"0.2.0"}},
 "jsonrpc":"2.0","id":1}

tools/list 回了 14 支工具:read_file、read_text_file、read_media_file、read_multiple_files、write_file、edit_file、create_directory、list_directory、list_directory_with_sizes、directory_tree、move_file、search_files、get_file_info、list_allowed_directories。

這個數字是量出來的,不是從文件抄的。 這個習慣明天就會用到 —— 接自己的資料庫,第一步也是先數它到底給了幾支。

協商是真的嗎?送三個版本試試

initialize 裡那個 protocolVersion 看起來很像形式主義。所以我送了三個不同的值:

我送的版本 它回的版本 這代表什麼
2025-06-18 2025-06-18 接受
2024-11-05(舊版) 2024-11-05 它也支援這版,照規格回同一版
2999-01-01(不存在) 2025-11-25 不支援 → 回它自己支援的一版,規格說 SHOULD 是最新版

三個結果讓我確認了兩件事:

第一,協商是真的,不是照抄。 如果它只是把我送的值回給我,第三列應該回 2999-01-01。它沒有 —— 它認出這個版本它不認識,然後回報自己實際支援的最上限。這兩種協商結果都是規格明文允許的:支援就回同一版(MUST),不支援就回另一版而且應該是最新版(SHOULD);規格另外還允許直接回一個 Unsupported protocol version 的錯誤,這支 server 選了前者。

第二,2025-11-25 是它的天花板。 這個數字你在 initialize 送對版本的時候永遠看不到,只有送一個它不認識的版本,它才會把底牌亮出來。

這是一個通用的 probe 技巧:想知道對方支援到哪裡,送一個明顯超過的值,看它回退到哪。比讀文件可靠,因為文件寫的是「應該」,回退值是「實際」。

順便測一個規格上有、實作上沒有的方法

規格 2026-07-28 那一版加了一個 server/discover,規定 server 必須實作,用來在任何請求之前先問它支援哪些版本、有哪些能力。我試著呼叫它:

{"jsonrpc":"2.0","id":9,"error":{"code":-32601,"message":"Method not found"}}

-32601 是 JSON-RPC 的標準錯誤碼「方法不存在」。

規格裡有,這個 server 沒實作。 這不是 bug —— 規格演進和實作跟進之間本來就有時間差;上一節探出來的天花板 2025-11-25 剛好就是 2026-07-28 的前一版,兩個數字對得上。但它示範了一件事:「MCP 支援 X」這句話要問清楚是規格支援還是你手上這支 server 支援,而分辨的方法就是打一次看回什麼。

順帶一提,2026-07-28 那版改的不只這個:它把 initialize/notifications/initialized 這組握手整個拿掉了,改成每個請求自己在 _meta 裡帶版本和能力。所以上面那三行 JSON 是 2025-11-25 以前的協定長相 —— 一支只講新版的 server,握手那兩行它會不認得(規格的相容矩陣:Legacy client × Modern server = Fails);兩種都講的則兩種都接。今天示範的握手已經在退場了;但探它到底是哪一版的方法不會。

還有一個小線索藏在 stderr 裡。這支 server 起來時會印一行:

Client does not support MCP Roots, using allowed directories set from server args

「Roots」是 client 告訴 server 它能碰哪些目錄的機制 —— 而它正是 2026-07-28 那版列入廢棄的三個功能之一(Roots、Sampling、Logging)。同一支 server,一邊回報自己支援到 2025-11-25,一邊還在用下一版已經宣告要淘汰的東西。這不矛盾,這就是「規格支援」和「這支 server 支援」之間的時間差長什麼樣。

這件事已經發生在你的電腦裡

2026-07-28 不是遠方的規格。Claude Code 從 2.1.232 起換了一套 MCP client runtime(官方叫 v2,底層是 MCP TypeScript SDK 2.0),它會講新協定。但它對不同的 server 態度不同:HTTP server 它本來就會問對方支不支援新版;claude.ai 的 connector 只在部分 session 問;stdio server 預設不問 —— 環境變數 MCP_PROTOCOL_NEGOTIATION 沒設時是 legacy,照舊走 initialize 握手,設成 auto 才問(connector 也會變成每個 session 都問)。

我拿自己 Day 13 那支檢查器(Python,uv run --with mcp 每次抓最新 SDK)測了兩種設定,看 debug log 裡它跟誰講了哪一版:

設定 我的 stdio server claude.ai 內建的 Gmail/Calendar/Drive
預設(legacy) 2025-11-25,era = legacy 2025-11-25,era = legacy
MCP_PROTOCOL_NEGOTIATION=auto 2026-07-28,era = modern 2025-11-25,era = legacy

三件事值得看。第一,我那支 server 什麼都沒改,就已經是「雙時代」的了。我把 server 用 tee 包一層,抄下 Claude Code 在兩種設定下實際送出的每一句:

預設(legacy) auto
第一句 initialize(protocolVersion: 2025-11-25) server/discover(_meta 帶 2026-07-28、clientInfo、clientCapabilities)
第二句 notifications/initialized subscriptions/listen
接著 tools/list、prompts/list、resources/list 同樣三支,但每一句的 _meta 都自帶版本與身分
server 對第一句的回應 握手結果 DiscoverResult:supportedVersions: ["2026-07-28"]、能力、instructions
tools/list 的回應 工具清單 工具清單,多了 resultType、cacheScope、ttlMs

握手那兩行在 auto 那邊整個消失,換成每個請求自帶身分 —— 這就是「無握手」在線上的長相。同一支 server(Python SDK 2.2.0)兩種都接,我一個字沒改。

第二,我第一次打它的時候判斷錯了。 我手打 server/discover 給它,它回 -32601,我差點寫下「官方 SDK 沒實作這個 MUST」。看了 Claude Code 送的那一句才發現差在哪:我的探針沒帶 _meta。補上去再打三次:

我送的 server/discover 它回的
沒有 _meta -32601 Method not found
_meta 只帶 protocolVersion -32602 params._meta is missing the required envelope key(s): io.modelcontextprotocol/clientCapabilities
_meta 帶齊版本與 clientCapabilities DiscoverResult,supportedVersions: ["2026-07-28"]

規格寫得很清楚:雙時代 server 看你怎麼開場決定用哪個時代 —— 帶 _meta 的請求走新版,initialize 走舊版。一句沒帶 _meta 的 server/discover,在它眼裡是舊時代的請求在叫一個舊時代沒有的方法,回 -32601 完全正確。錯的是探針,不是 SDK。(上一節那支 filesystem server 我也補打了帶 _meta 的版本,仍然 -32601 —— 它是真的舊。)

第三,Anthropic 自家的三個 connector 是它本來就會問的那種,兩種設定下都被問了,也都回舊版 —— 名字還叫 StatelessServer。在我看到的這幾支裡,順序是規格先走、SDK 跟上、平台自己最後。

線路上全變了:用的人沒感覺,寫 server 的人要知道

同一支 server、同一個工具、同一句話,兩種設定各跑三次,要 Claude 把工具回傳的 JSON 原封不動印出來:六份輸出 md5 完全相同,都是 2,282 bytes。.mcp.json 一個字沒改。你唯一看得到差別的地方是 --debug-file 的 log:protocolEra 一個寫 legacy、一個寫 modern;連線時間一個約 290ms、一個約 570ms,三次都穩定 —— auto 多了 server/discover 那一句要等。

所以「使用者端沒什麼改變」是對的,而且是規格刻意的:相容矩陣裡只要有一邊是雙時代就是「Works」,而 Claude Code 從 2.1.232 起就是雙時代 client。唯一要留意的是:預設 legacy 時它對 stdio server 只會送 initialize,哪天有人給你一支只講新版的 server,照規格它會拒絕那句握手 —— 那時候就是該設 auto 的時候(這一格我沒有實測,是照規格推的)。

對兩種人,各一句:

  • 用的人:手上現有的 server 預設都不會壞,因為 client 預設不問。上面那三行手打的握手今天照樣能用。想提前看自己的 server 在新版下長什麼樣,設 auto,然後開 --debug-file 看 log 裡每支 server 的 negotiatedProtocolVersion 和 protocolEra。
  • 寫 server 的人:用 SDK 寫的,升級一次就免費變雙時代;手刻 JSON-RPC 的(像本文這樣),要知道新版 client 的開場有兩種 —— Claude Code 選的是先送 server/discover(上面抄到的第一句),規格也允許直接送一個 _meta 裡帶版本的請求。你兩種都不認得的話:前者你回 -32601,雙時代的 client 會退回舊握手;後者你會當成一個沒握手就來的請求處理掉,行為不定義。而只講新版的 client,哪一種開場都不會退回舊握手,直接放棄。規格的相容矩陣裡「Legacy server × Modern client = Fails」那一格,指的就是這種 server。

所以本文那個「送一個它可能不認得的方法,看它回什麼」的探針,剛好就是新版規格寫進去的相容機制:stdio 上先送 server/discover,回 DiscoverResult(或新版定義的錯誤)的是新 server,回其他任何錯誤 —— 像 -32601 —— 或逾時沒回的,就是舊 server。 我一開始拿它當偵錯技巧,規格拿它當正式的判別方法。

而我自己就示範了這個判別法怎麼用錯:探針沒照新版規格帶 _meta,探出來的結論就是反的。探針也是請求,規格對請求的要求,它一條都不能少。 文件不會提醒你這件事,因為文件假設你會照著做;只有把自己打的跟 client 打的並排看,才看得出差在哪。


這個練習真正的用途

手打 JSON-RPC 平常沒有必要 —— 有設定檔就好。它的價值在出事的時候。

當你的 MCP server 接不上,client 端給你看的錯誤訊息往往只有一句「connection failed」。這時候有兩條路:

  1. 去翻設定檔、翻文件、猜是哪裡錯
  2. 手打三行,看它回什麼

第二條路會直接告訴你:server 有沒有起來、握手停在哪一步、它認為自己有幾支工具。這三個資訊通常就能定位到問題在哪一層。

把本文開過的 lab 對一下帳 —— 左邊是文件說的、或我以為的,右邊是打了之後它說的:

文件說/我以為 lab 說
規格:server 收到其他請求之前 MUST 先收到 initialized server-filesystem 不檢查,順序反了照答 14 支
規格:server MUST 實作 server/discover filesystem 回 -32601,它是真的舊;我自己那支第一次也回 -32601 —— 錯的是我的探針沒帶 _meta
規格 2026-07-28:Roots 列入廢棄 同一支 server 的 stderr 還在用 Roots
Claude Code 文件:2.1.232 起會講新協定 預設對 stdio 不講,設 auto 才講;Anthropic 自家 connector 兩種設定都回舊版
我以為:協定換版,用的人會有感 六次輸出 md5 全同,差別只在 debug log

五列沒有一列是靠讀文件讀出來的。打過一次的好處就在這裡 —— 當 server 回報的東西跟你以為的不一樣時,你會知道。這在後面幾天會變成關鍵。

這一篇留下的心法:

「支援 X」有三個答案:規格說的、SDK 做的、你手上這支回的 —— 最後一個要打一次才知道。文件會變,lab 不會 —— 有疑問就把設定檔拿掉,手打一次。

明天:從頭到尾接一支 MCP server —— 選、掛、驗、問,到它第一次撞牆。


參考資料


上一篇
Day 13 去識別化做成一支檢查工具:一張對照表,和一條不能交給 AI 的檢查
下一篇
Day 15 接一個 MCP server 的完整流程:從 .mcp.json 到它第一次撞牆
系列文
盡信 Claude,不如無 Code — 心法與全端實戰 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言