
我第一次接觸 MCP 的時候,覺得這東西是多餘的。
模型要呼叫工具,OpenAI 的 function calling 早就能做了,每家 API 也都有自己的格式。再定一套協定出來,看起來像是重複造輪子。
改變我想法的是一件小事:我有六個 agent,每個都需要「查詢任務板」這個能力。在沒有 MCP 之前,我為它們寫了六份幾乎一樣的工具描述,塞在六份不同的 prompt 裡。後來任務板加了一個欄位,我要改六個地方。改完之後有一個 agent 行為變得很奇怪,因為我在其中一份描述裡打錯字了。
MCP 解決的就是這件事。它把「工具怎麼描述、怎麼呼叫、怎麼回傳」從 prompt 裡抽出來,變成一個獨立的行程,誰要用就接上去。
在講 MCP 怎麼寫之前,先確定你真的需要它。給模型「做事的能力」有三個層次,成本和能力遞增。
第一層:塞在 prompt 裡,人工執行。
你可以要求執行以下動作,用 [ACTION:名稱 參數] 的格式輸出:
[ACTION:read_file path=...]
然後你自己解析輸出、執行、把結果貼回去。這個做法很土,但在小規模場景下完全夠用,而且零依賴。我到現在還有幾個小腳本是這樣寫的。
缺點是解析很脆,模型偶爾會漏掉格式或多寫一句解釋,你的正則就爆了。
第二層:原生 function calling。
API 層級的工具呼叫,模型直接回傳結構化的呼叫請求。比第一層可靠很多,因為格式由模型端保證。
缺點是工具定義綁在你的程式裡。換一個 agent 要重寫一份,換一個模型供應商要換一套格式。
第三層:MCP。
工具住在一個獨立行程裡,透過標準協定溝通。任何支援 MCP 的客戶端都能接上,換模型不用改工具。
代價是多一個行程要管,多一層要除錯。
我的建議很直接:單一 agent、少於五個工具,用第二層就好。工具要被多個 agent 共用,或者你想換 CLI 而不重寫工具,才值得上 MCP。

往上走一階,複用性變高,要管的東西也變多。不要因為 MCP 比較新就直接跳到第三階。
拆掉所有名詞,MCP 就是三件事:
客戶端(Claude Code) ←─ JSON-RPC 2.0 over stdin/stdout ─→ 你的 server
就這樣。傳輸層是 stdin/stdout,資料格式是 JSON-RPC 2.0,一行一個訊息。
它不是 HTTP,不需要開 port,不需要處理認證(子行程繼承你的權限,這件事有安全意涵,Day 26 會講)。你可以用任何語言寫,只要能讀 stdin、寫 stdout。
協定裡有很多方法,但要能跑起來只需要三個。
initialize — 握手。客戶端問你支援什麼,你回報自己的能力和協定版本。
// 收到
{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2024-11-05","capabilities":{}}}
// 回覆
{"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2024-11-05",
"capabilities":{"tools":{}},
"serverInfo":{"name":"my-server","version":"0.1.0"}}}
tools/list — 宣告你有哪些工具。這份清單會被送進模型的 context,所以描述要精準且簡短。每多一個字都是每次呼叫都要付的錢。
{"jsonrpc":"2.0","id":2,"result":{"tools":[{
"name":"task_list",
"description":"列出待辦任務。可用 status 過濾。",
"inputSchema":{
"type":"object",
"properties":{"status":{"type":"string","enum":["todo","doing","done"]}},
"required":[]
}}]}}
tools/call — 執行。回傳內容是一個 content 陣列,最常用的是 text。
{"jsonrpc":"2.0","id":3,"result":{
"content":[{"type":"text","text":"待辦 3 筆:\n1. ...\n2. ...\n3. ..."}],
"isError":false}}
isError 這個欄位很重要,很多人會忽略。工具執行失敗的時候,你應該回 isError: true 加上錯誤訊息,而不是丟一個 JSON-RPC 層級的 error。
差別在於:JSON-RPC error 代表「協定層面出事了」,客戶端可能直接中斷。isError: true 代表「工具執行失敗,這是一個可以讓模型看到並自己處理的結果」。模型看到錯誤訊息之後常常能自己修正參數重試,這正是你要的行為。
這是最容易被輕忽、但影響最大的部分。工具描述是模型唯一的說明書。
我的三條規則:
一、寫「什麼時候用」,不只寫「做什麼」。
✗ description: "搜尋記憶"
✓ description: "在長期記憶中搜尋。當使用者提到之前討論過的事、或你需要
確認過去的決定時使用。回傳最相關的 5 筆。"
二、參數的 enum 要窮舉。 給 enum 的參數,模型幾乎不會傳錯。給自由字串,它會發明各種變體。能列舉就列舉。
三、描述長度要跟使用頻率成正比。 一個每次對話都會用到的工具,值得寫 200 字說明。一個一個月用一次的工具,寫 20 字就好。我算過,工具描述的總長度直接反映在每一次 spawn 的固定成本上,而那個成本你昨天已經看到了。
很多人以為掛了 MCP,模型就會「自動知道」什麼時候用。
不會。模型看到的只有你寫的那份 schema。如果你的工具叫 get_data,描述寫「取得資料」,那它永遠不會被呼叫,因為模型無從判斷什麼情況算是需要「資料」。
我踩過的實例:有一個工具叫 working_state_get,描述寫「取得工作狀態」。上線兩週,呼叫次數是零。改成「取得目前生效的操作規則與交接事項。在你要做任何決策之前先呼叫這個,特別是當你不確定現在的規則是什麼的時候。」之後,呼叫率立刻上來了。
工具沒被呼叫,八成問題出在說明書。你寫得像 API 文件,模型需要的是使用指南。
MCP 帶來三個新的麻煩,先講清楚。
除錯變難。 你的 server 跑在子行程裡,stdout 被協定佔用了,所以你不能用 print 除錯。所有日誌必須寫 stderr 或寫檔案。我第一次寫的時候在裡面 print 了一行狀態,整個協定當場壞掉,客戶端解析不了那行非 JSON 的輸出。
啟動成本。 每次 spawn 都要啟動 server 行程。如果你的 server 要載入模型或連資料庫,這個啟動時間會加在每一次呼叫上。我的 server 冷啟動大約 200 毫秒,還可以接受;如果你的要三秒,就要考慮改用長駐的 HTTP 模式。
工具膨脹。 這是最真實的問題。MCP 讓加工具變得很容易,於是你會一直加。我的 server 現在有 200 個以上的工具,而昨天已經算過,工具 schema 是固定開銷裡最大的一塊。容易加的東西最後都會變成負擔,Day 10 會講怎麼處理這個。
明天動手寫。四十行內做出一個能跑的 MCP server,然後真的掛到 Claude Code 上叫它用。