MCP 新版協定與 FastMCP 4:
線路上的真相
主線 AI Agent 與 MCP 那一課說 MCP 是 AI 工具界的 USB。 但這個「USB」兩年內改了五版,而現行的 2026-07-28 版一口氣拿掉了握手、session 和長連線。 下面是同一份 FastMCP 4.0.8 伺服器程式、同一件事(列出工具,再呼叫 add(2, 3)), 用五個年代的協定各做一次的真實 HTTP 封包——切換年代,點任何一列看 header 與 body:
實驗場在你的瀏覽器裡跑,不用安裝任何東西、也不連任何伺服器——封包都是錄好的,圖表與模擬是現場算的。 首次載入約需 30–60 秒,正好夠你讀完第 1 節。
五個版本:從「打電話」到「寄信」
第一版規格的「基礎協定」清單裡白紙黑字寫著 Stateful connections:先握手、協商能力,之後所有對話都在這條線上, 伺服器還能順著線反過來問客戶端(要 LLM 生成、要使用者輸入)。像打電話——線不斷,雙方隨時能開口。 現行版把它改成寄信:每封信都寫齊寄件人、版本與能力,哪個郵局收到都能處理, 代價是伺服器再也不能在你講到一半時插嘴反問。
| 版本 | 一句話 | 重點改動(官方 changelog) |
|---|---|---|
| 2024-11-05 | 誕生 | JSON-RPC+有狀態連線;stdio 與 HTTP+SSE 兩種傳輸;tools/resources/prompts;sampling、roots |
| 2025-03-26 | 上雲 | Streamable HTTP 取代 HTTP+SSE(單一端點+Mcp-Session-Id);OAuth 2.1;tool annotations;JSON-RPC batching |
| 2025-06-18 | 收斂與加固 | 拿掉剛加的 batching;structured output;elicitation;HTTP 請求必帶 MCP-Protocol-Version |
| 2025-11-25 | 企業化 | 實驗性 tasks;URL elicitation;CIMD 用戶端註冊;icons;sampling 可帶 tools |
| 2026-07-28 | 無狀態(現行) | 移除握手、session、GET 長連線、ping;新增 server/discover、_meta 信封、Mcp-Method/Mcp-Name header、多回合請求(MRTR)、快取提示;tasks 改成官方擴充;Roots/Sampling/Logging 棄用 |
來源:modelcontextprotocol.io 各版 changelog 與 deprecated 登記表(2026-09-24 查閱)。棄用的功能至少保留 12 個月才可能移除(SEP-2596)。
同一天(2026-07-28)MCP Python SDK v2 與 FastMCP 4.0.0b1 一起發布;FastMCP 4.0.0 正式版在 2026-08-31 推出, 本課用的 4.0.8 是 2026-09-23 的版本(PyPI)。一台 FastMCP 4 伺服器同時服務新舊年代——開場 2025-03-26 到 2026-07-28 那四組封包是同一台伺服器接的(2024-11-05 的 HTTP+SSE 是同一份程式改用 transport="sse" 起的)。
新協定的一發請求,身上帶了什麼
這是開場 2026-07-28 那組的第 3 發,內容原封不動、只重新排版(FastMCP Client 送出的):
對照握手年代(2025-11-25)同一個呼叫,body 只剩 {"name": "add", "arguments": {...}}, 身分全靠 header 裡那把 mcp-session-id——伺服器得記得「這把 id 是誰、協商了什麼」。 回應也多了東西:新協定的每個結果都有 resultType(complete 或 input_required), 列表類結果帶 ttlMs/cacheScope 快取提示。
一個誠實的細節:無狀態不等於省流量。實測同一個動作,新協定 3 發請求的 body 合計比舊協定 6 發還大 (數字在實驗場 2️⃣ 的帳本裡)——每一發都自帶身分證是要成本的。它換到的是下一節的東西。
站在負載平衡器的位置看
我們真的起了兩台副本(A、B)和一個「輪流分派」的負載平衡器,新舊客戶端各做一次「列工具+呼叫 add」:
| 情境 | 請求落點(實測) | 結果 |
|---|---|---|
| 輪流 × 新協定 | A:discover B:tools/list A:tools/call,全部 200 | 成功 |
| 輪流 × 舊協定 | A:initialize 拿到 session,下一發被分到 B → 404 | MCPError: Session not found |
| 黏著 × 舊協定 | 負載平衡器記住「這把 session 是 A 發的」,6 發全送 A | 成功 |
黏著分派(sticky)救得了路由,救不了「那台不見了」——副本重啟、縮容,上面的 session 全滅。 新協定還順手解了兩件事:
① gateway 不拆 body 也能路由。每發都有 Mcp-Method、Mcp-Name; 工具參數標上 x-mcp-header 還會鏡射成 Mcp-Param-*(實測:租戶參數變成 mcp-param-tenant: acme),按租戶分叢集不用解析 JSON。伺服器則強制檢查 header 與 body 一致, 免得 gateway 看 header、伺服器看 body、兩邊各說各話。
② 伺服器不再「插嘴反問」。舊協定裡工具可以停在半路,順著連線發一個 elicitation/create 給客戶端, 等答案回到同一台、同一條 session。新協定改成多回合請求(MRTR):工具直接回 resultType: "input_required" 結束這一回合,客戶端問完使用者,帶著 inputResponses 重打同一個工具——第二發落到哪台都行。 長時間的連線也一起退場:GET 長連線與斷線續傳(Last-Event-ID)移除,要訂閱變更改用 subscriptions/listen,要跑很久的工作改用 tasks 擴充(投遞 → 輪詢)。
不用 SDK、不用握手,一發 POST 呼叫工具
實驗場 4️⃣ 給你四個旋鈕,每一種組合的回應都是真的打過一次錄下來的(4×3×3×5=180 發)。 從實測回推出 FastMCP 4.0.8 的檢查順序:
| # | 檢查 | 沒過的實測回應 |
|---|---|---|
| 1 | 版本 header 決定走哪個年代(沒帶或帶 2025-11-25 → 當你是舊客戶端) | -32600 Bad Request: Missing session ID |
| 2 | _meta 要有 protocolVersion 與 clientCapabilities | -32602 params._meta … |
| 3 | header 版本=_meta 版本 | -32020 mcp-protocol-version header does not match … |
| 4 | Mcp-Method 要有且等於 body 的 method | -32020 mcp-method header does not match … |
| 5 | Mcp-Name 要有且等於工具名 | -32020 mcp-name header does not match … |
| 6 | 伺服器支援這個版本 | -32022 Unsupported protocol version(附上 supported 清單) |
順序是從實測回推的(FastMCP 4.0.8+mcp 2.2.0),不是規格規定的順序;錯誤碼 -32020/-32022 則是 2026-07-28 規格分配的。
我遇到這個問題 → 用哪個功能
| 遇到的問題 | 用這個 |
|---|---|
| 已經有 FastAPI 服務/別家 REST API 的 OpenAPI 規格 | FastMCP.from_fastapi(app)/FastMCP.from_openapi(spec, client) |
| 好幾台 server 想合成一台;要轉手別人的 server | hub.mount(sub, namespace=…)/create_proxy(…) |
| agent 要同時接很多台(客戶端) | ClientGroup(4.0.0b5 新增) |
| 工具幾十個,模型挑不準 | BM25SearchTransform:模型只看到 search_tools+call_tool |
| 每次呼叫都要記 log、計時、限流 | Middleware |
| 只有管理員能用某些工具 | auth=+require_scopes("admin") |
| gateway 要依租戶分流;客戶端一直重複 list_tools | x-mcp-header;cache_ttl+Client(cache=True) |
| 工具跑到一半要使用者確認 | 回傳 InputRequiredResult(新協定);ctx.elicit() 只剩舊協定能用 |
| 工具要跑好幾分鐘 | @mcp.tool(task=True)+fastmcp-tasks |
| 要記住購物車(跨呼叫、跨連線) | SessionId/UserSession+共用 store |
實驗場 5️⃣ 有 19 張卡,每張附最小程式與在 4.0.8 上實測到的證據(例如 30 個工具的目錄,模型 list_tools 只看到 2 個; 客戶端呼叫 3 次 list_tools,線路上只有 1 發)。
上過 LLM 應用開發系列的 FastMCP 4 課?那幾課用 4.0.0b1,正式版有這些不同
- 線路形狀沒變:本課用 4.0.8 重測,新協定 3 發、舊協定 6 發,與 b1 的紀錄一致;裸 POST 的必要條件也相同。
- 裝法變簡單:正式版直接 fastmcp==4.0.8,不必再同時釘 prerelease 的 fastmcp-slim。
- 新增 ClientGroup(b5)、CallArgument/Depends 注入(b3);4.0.2 起可 from fastmcp import ClientGroup。
- 確定拿掉 ctx.sample()、ctx.list_roots();ctx.elicit() 只在舊協定連線可用(新協定連線上實測:elicitation via server-initiated requests is unavailable on 2026-07-28 connections.)。
- Client("server.py") 用字串指本機檔案改為棄用,請傳 Path(FastMCP 5 移除)。
來源:FastMCP GitHub releases v4.0.0–v4.0.8 與官方 What's New(2026-09-24 查閱)。
想真的動手寫這些 server,本站有外部軌的深入版: FastMCP 4 入門、認證、狀態與加密、 4.0 專屬功能、常見 MCP 服務(LLM 應用開發系列,在 molab 免費 CPU 環境跑)。
本課名詞速查卡
| 協定版本(日期) | 最後一次不相容改動的日期;現行 2026-07-28。每個請求自己宣告版本,伺服器逐發接受或拒絕。 |
| server/discover | 取代 initialize 的「自我介紹」RPC:一發拿到支援版本、能力、伺服器身分。伺服器必須實作,客戶端可以不呼叫。 |
| _meta 信封 | 每個請求 body 裡的 io.modelcontextprotocol/protocolVersion+clientCapabilities(+clientInfo)——握手搬進了每一發。 |
| Mcp-Method/Mcp-Name | 把 method 與工具名鏡射到 HTTP header,讓 gateway 不拆 body 就能路由;與 body 不一致回 -32020。 |
| MRTR(input_required) | 伺服器不再主動發 request;回 input_required 結束回合,客戶端帶 inputResponses 重打。 |
| Mcp-Session-Id | 握手年代的 session 鑰匙,只有發它的那台認得;2026-07-28 移除。跨請求狀態改由應用層發「顯式鑰匙」(FastMCP 的 SessionId)。 |
| FastMCP 4 | 一台伺服器同時服務新舊年代;stateless transport, stateful application。4.0.0 正式版 2026-08-31。 |
換你動手
在實驗場 4️⃣ 轉出唯一回 200 的組合。接著把版本 header 和 _meta 都換成 2025-11-25——錯誤訊息變成什麼?伺服器為什麼跟你要 session?
在 3️⃣ 把副本拉到 3、策略選「輪流分派」,舊協定大約多少 session 能整段成功?換成「黏著分派」呢?黏著救得了「那台副本重啟」嗎?
在 2️⃣ 對照情境 ④ 與 ⑤:兩邊各有幾發 HTTP、伺服器有沒有反過來發 request?說明為什麼新協定的「確認刪除」可以跨副本、跨重啟,舊的不行。
卡住了?三題在實驗場最後一格都有折疊解答——先自己做,再打開對照。
帶回家自己跑(免費)
本課的全部封包出自一支腳本:spike_genai_mcp_fastmcp.py。 裝好 uv 後一行 uv run --script spike_genai_mcp_fastmcp.py 就會在你的電腦起十台小伺服器(只綁 127.0.0.1)、 把五個年代、180 發手刻請求、兩副本實驗全部重錄一遍,約 10 秒。純 CPU、不需要任何 key、除了第一次下載套件不連外。 我們在 Linux(Python 3.12)實測過;其他平台與雲端筆記本沒有驗證。
情境測驗
離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。
Q1 情境題
你的 MCP 伺服器升級成 FastMCP 4,部署成 3 個副本、前面是一般的輪流分派負載平衡器。新版客戶端一切正常,但還沒升級的舊客戶端(只會握手協定)時不時噴 Session not found。最合適的處理是?
問題的根源是舊協定的 session 只活在發它的那一台(本課兩副本實測:initialize 落在 A 拿到 session,下一發被分到 B,B 回 404 Session not found)。只有「帶 session 的請求」需要回到原來那台,所以對它們做黏著分派就夠了(實測黏著後 6 發全落 A、成功);新協定請求沒有 session,繼續輪流、享受無狀態的擴展性。A 走回頭路,把能自由分派的新客戶端也綁死;C 能動但放棄了多副本的意義;D 症狀相似但原因不同——session 不是被清掉,是請求根本送到了不認得它的另一台。
Q2 錯誤診斷
同事想不透過 SDK、用一發 POST 呼叫 add 工具。他的 header 是從上一發 tools/list 請求複製來改的,結果拿到 400。最可能的原因是?
這段錯誤訊息是本課實測原文:新協定把 method 鏡射到 Mcp-Method header 讓 gateway 不拆 body 就能路由,伺服器因此強制檢查 header 與 body 一致——否則 gateway 以為是無害的 tools/list、伺服器卻執行了 tools/call,正是規格要防的「兩邊各信各的」。錯誤碼 -32020 就是 HeaderMismatch。A 不對:不支援版本會回 -32022 並附 supported 清單;B 不對:discover 是選用的,本課 180 發手刻請求都沒打 discover,照樣有 1 發成功;D 不對:clientInfo 是 SHOULD 不是必填,實測成功的那發也沒帶。
Q3 錯誤診斷
你在 FastMCP 4 的刪檔工具裡寫了 await ctx.elicit("確定刪除?", response_type=bool)。舊版 Claude Desktop 接上來一切正常;換成新版客戶端呼叫同一個工具,得到下面的錯誤。該怎麼修?
錯誤原文(本課在 4.0.8 實測)已經講了原因:2026-07-28 連線上沒有伺服器反向發 request 的通道。舊協定的 ctx.elicit() 是讓工具停在半路、順著 session 問客戶端;新協定改成多回合請求——工具回 input_required 結束這一回合,客戶端問完使用者、帶著 inputResponses 重打(實驗場 2️⃣ 情境 ④ 就是這個線路)。A 症狀相似但原因不同:handler 有沒有設都一樣,這條路在新協定上根本不存在;C 說反了,4.0.8 就是最新版,這是設計不是 bug;D 更糟:ctx.sample() 在 FastMCP 4 已經移除(實測 'Context' object has no attribute 'sample'),Sampling 本身在 2026-07-28 也被棄用。
Q4 情境題
公司有一套 40 個端點的訂單系統 REST API(有 OpenAPI 規格),想讓 agent 能用它;你也擔心 40 個工具說明書塞爆上下文、模型挑錯工具。最省力又穩的做法是?
兩個問題各有現成零件:from_openapi 讀規格自動生成工具(實測三個路由 → 三個工具,名稱取自 operationId、參數取自規格),BM25SearchTransform 把大目錄藏在搜尋後面(實測 30 個工具的目錄,模型 list_tools 只看到 search_tools 與 call_tool)。A 能動但 40 份手工包裝要跟著 API 改版一起維護,而且沒解決上下文爆量;C 讓模型直接拼 HTTP,失去 schema 驗證與權限控管,出錯也難追;D 把一個系統拆成 40 台 server,部署與命名都變複雜,而且模型看到的工具數一個也沒少。
Q5 情境題
有個「產生月報」工具要跑 8–10 分鐘。伺服器是多副本部署,使用者的網路偶爾會斷。在 2026-07-28 協定下怎麼設計最穩?
長工作要脫離請求本身:tasks 擴充(2026-07-28 從核心移出、成為官方擴充 io.modelcontextprotocol/tasks)讓 tools/call 立刻回一個 taskId,之後每次 tasks/get 都是獨立的一發(實驗場 2️⃣ 情境 ⑥:投遞後一串輪詢,最後 completed)。多副本時把任務後端放在共用的 Redis,哪一台接到輪詢都查得到。A 很脆弱:新協定下回應串流一斷,這個請求就沒了、只能重打(規格已移除續傳);B 正是 2026-07-28 拿掉的機制,而且又把你綁回 session;D 沒有任何可追蹤的把手,使用者問第二次時伺服器也不知道是哪一份報表。