我想知道一個 MCP 伺服器最少要多少東西才跑得起來,就動手做一個。寫程式的是 Claude Code:我給需求,它從零寫、自己測,我拆它交出來的東西。工具就用第 16 篇那個算退款的 orders_calc_refund。
先講結論:Claude Code 83 秒就交出一個能用的 MCP 伺服器,但第一版連 import 都是錯的。 真正費工的是兩件事:描述要寫到模型會用,出錯時要讓模型讀得到原因,也不能讓整個代理卡住。
開一個空資料夾,丟給 Claude Code 這段需求:
claude -p "在這個資料夾用官方 MCP Python SDK 寫一個 stdio MCP 伺服器 refund_server.py。\
提供一個工具 orders_calc_refund(order_id, ratio):用寫死的訂單資料 \
{\"ORD-1024\": 1280, \"ORD-2048\": 499} 算退款金額,ratio 是 0 到 1 的比例。\
工具描述要寫清楚什麼時候用、參數意思、限制。\
寫完自己測一次它能啟動並正確回應 tools/list 與 tools/call,\
最後告訴我怎麼用 claude mcp add 接上。" \
--allowedTools "Write,Edit,Read,Bash"
我跑了一次(2026-10-02,Claude Code 2.1.287,claude-opus-5-5):15 個回合、83 秒,Claude Code 自己估的成本約 0.45 美元。它沒有一次寫對:
FastMCP,翻了裝好的套件才發現 2.x 版改名成 MCPServer。ToolError 的訊息傳給模型,於是改丟 ToolError。ratio 等於 1.5。照它給的指令接上,問「幫我算 ORD-1024 退 30% 是多少錢」,模型開的單是 {"order_id": "ORD-1024", "ratio": 0.3},伺服器回 "refund_amount": 384.0。(各跑一次,是例子不是證據。)整條路是這樣:

左欄是磁碟上的東西,設定在 .mcp.json 或 ~/.claude.json,伺服器就是一個 .py 檔。中欄是 Claude Code 依序做的事,timeout 管的是 tools/call 能等多久。右欄是模型看得到的全部,伺服器的日誌只到標準錯誤。
完整檔案 74 行,核心是這一段:
mcp = MCPServer("refund")
ORDERS: dict[str, int] = {"ORD-1024": 1280, "ORD-2048": 499}
@mcp.tool(
name="orders_calc_refund",
description=(
"Calculate the refund amount for an existing order given a refund ratio. "
"Use this when the user asks how much money would be refunded for a full or "
"partial refund of a specific order (e.g. 'refund 30% of ORD-1024'). "
"This tool ONLY calculates; it does NOT issue the refund, change the order, "
"or contact payment systems. "
"Limits: only orders in the built-in demo dataset are known "
f"({', '.join(ORDERS)}); unknown order_id returns an error. "
"ratio must be a number between 0 and 1 inclusive (0.3 = 30%, 1 = full refund); "
"convert percentages to a fraction before calling. "
"Result is rounded half-up to 2 decimal places in the order's currency."
),
annotations=ToolAnnotations(readOnlyHint=True, idempotentHint=True, openWorldHint=False),
)
def orders_calc_refund(
order_id: Annotated[str, Field(description="Order ID, exact match and case-sensitive, e.g. 'ORD-1024'.")],
ratio: Annotated[float, Field(ge=0, le=1, description="Refund fraction of the order total, 0 to 1 inclusive. 0.5 means 50%.")],
) -> dict:
if order_id not in ORDERS:
raise ToolError(f"Unknown order_id '{order_id}'. Known orders: {', '.join(ORDERS)}.")
total = Decimal(ORDERS[order_id])
refund = (total * Decimal(str(ratio))).quantize(Decimal("0.01"), ROUND_HALF_UP)
return {"order_id": order_id, "order_total": float(total), "ratio": ratio, "refund_amount": float(refund)}
它做對了四件事:
0.3。ratio 有 minimum: 0、maximum: 1。我故意送 ratio: 30,回來的是 isError: true 的驗證錯誤,不會算出退 30 倍的金額。ToolError 回報錯誤,模型讀得到原因,才能決定下一步。readOnlyHint。不過 MCP 規格寫明,除非伺服器可信,用戶端必須把註記當成不可信(2026-10-02 查)。註記只是伺服器的自我宣稱。它也漏了一個地方:描述說金額以訂單幣別計,回傳的結果卻沒有幣別欄位,模型回答時自己補了一句「工具沒有回傳幣別」。描述寫了什麼,輸出就得給得出來。
為了看懂 SDK 做了什麼,我另外寫了一個不靠套件的 44 行版本。主迴圈是這樣:
for line in sys.stdin:
msg = json.loads(line)
if "id" not in msg: # 通知:不回
continue
m = msg["method"]
if m == "initialize":
result = {"protocolVersion": "2025-11-25", "capabilities": {"tools": {}},
"serverInfo": {"name": "refund", "version": "0.1.0"}}
elif m == "tools/list":
result = {"tools": [TOOL]} # 欄位是 inputSchema
elif m == "tools/call":
result = call(msg["params"]["arguments"])
else: # 其他一律 -32601
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"],
"error": {"code": -32601, "message": "Method not found"}}), flush=True)
continue
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": result}, ensure_ascii=False), flush=True)
就是第 17 篇攔到的那幾種訊息:通知跳過、initialize、tools/list、tools/call,其他一律 -32601。Claude Code 的新版試探 server/discover 就在這裡被擋下,然後退回 initialize。SDK 版則會多宣告 prompts 與 resources,所以 Claude Code 多問兩趟 prompts/list、resources/list。
官方從寫、測、接到串,都有現成的工具(2026-10-02 查):
| 階段 | 工具 | 做什麼 |
|---|---|---|
| 寫 | 官方 SDK | TypeScript、Python、C#、Go、Rust、Ruby 是第一級 |
| 寫 | mcp-builder 技能 |
Anthropic 給 Claude 用的 MCP 伺服器開發指南 |
| 測 | MCP Inspector | npx @modelcontextprotocol/inspector,網頁、--cli、--tui 三種介面,需要 Node 22.19 以上 |
| 接 | claude mcp add/list/get 與 /mcp |
Claude Code 內建,加伺服器、看狀態 |
| 找、發佈 | MCP Registry | 官方的公開伺服器目錄,目前是 preview |
| 串(API) | MCP connector | 從 Messages API 直接連遠端 MCP 伺服器,不用自己寫用戶端,beta |
| 串(程式) | Agent SDK 的 mcpServers |
在自己的程式裡把多個 MCP 伺服器交給代理 |
mcp-builder 的說明寫的還是「Python(FastMCP)」,正好是 Claude 第一版踩的坑。我的做法是讓 Claude 照它寫、對著裝好的 SDK 測,接上之前先用 Inspector 的 --cli 跑一次 tools/list。技能是什麼、怎麼載入,下一篇講。
claude mcp add 的 --scope 決定伺服器在哪裡生效(MCP 文件,2026-10-02 查):
| 範圍 | 在哪裡生效 | 跟團隊共用 | 存在哪裡 |
|---|---|---|---|
| local(預設) | 只有這個專案 | 否 | ~/.claude.json |
| project | 只有這個專案 | 是,跟著版本控制 | 專案根目錄的 .mcp.json |
| user | 你所有的專案 | 否 | ~/.claude.json |
工具屬於專案、應該跟著程式碼走,就用 project。別人 clone 下來第一次開 Claude Code 時要先核准,不會自動跑起來。
Claude 寫的檔案開頭有一行註解:這裡永遠不要 print()。官方教學也規定 stdio 伺服器的日誌一律寫到標準錯誤。我拿 44 行版實際試了兩種:
print("server started"):照常運作,Claude Code 看起來略過了那一行。print("debug:", args, end=""):沒有換行,黏進了 JSON 回應。Claude Code 送出單子後一直等,8 分鐘沒有結果,我手動中止。MCP 文件寫明,MCP_TOOL_TIMEOUT 沒設定時預設大約 28 小時。在 .mcp.json 加上 "timeout": 10000 再跑,32 秒就結束:單子回來「timed out after 10s」,Claude 重試一次,最後回答算不出來,也不給猜的數字。
兩個習慣:日誌寫到標準錯誤,.mcp.json 一定設 timeout。
Hasan et al.(2025)分析了 1,899 個開源 MCP 伺服器:66% 有程式碼異味(code smell),14.4% 帶著已知的 bug 樣式,7.2% 有一般的安全漏洞。 八種漏洞裡只有三種跟傳統軟體重疊,其餘是 MCP 特有的,這一層最後一篇再算。
第一,SDK 會改名。 連 Claude 的第一版都用了舊名字,要對著你裝好的版本測過才算數。
第二,描述就是 API 文件。 「30%」會不會變成 0.3、幣別會不會漏掉,全看那幾句話跟實際輸出對不對得上。
第三,你多養了一個行程,預設逾時又太長。 一個 bug 就能讓代理等上一天多。
多了什麼能力:你能讓 Claude Code 寫出一個自己測過的 MCP 伺服器,看得出好的成品長什麼樣,知道 SDK 底下只做四件事、有哪些工具幫你寫、測、接、串,以及 stdio 伺服器最容易踩的坑。
多付了什麼代價:一個會跟著 SDK 改版的相依、一份決定模型會不會用對的描述,以及一個不設定就長達 28 小時的逾時。
這一篇給 Claude 的是一個工具:一個動作,一組參數。如果你要給它的是一整套作法呢?
例如「這個專案怎麼發版」,有步驟、有檢查清單,可能還附幾支腳本。寫成工具太死,寫進系統提示又每一輪都要付錢。下一篇看 Claude 怎麼把這種東西打包成一個資料夾,用得到時才讀進來,以及它跟 MCP 的分工。
MCPServer、stdio 不准寫標準輸出、工具註記須當成不可信。claude mcp add、三種範圍、timeout。