◥ AI 互動教室 ‹ 生成式 AI 導論
下載 .py 單獨開啟實驗場 ↗ 留言回報
GENAI 進階補充 · D · MCP × FASTMCP 4

MCP 新版協定與 FastMCP 4:
線路上的真相

主線 AI Agent 與 MCP 那一課說 MCP 是 AI 工具界的 USB。 但這個「USB」兩年內改了五版,而現行的 2026-07-28 版一口氣拿掉了握手、session 和長連線。 下面是同一份 FastMCP 4.0.8 伺服器程式、同一件事(列出工具,再呼叫 add(2, 3)), 用五個年代的協定各做一次的真實 HTTP 封包——切換年代,點任何一列看 header 與 body:

實測側錄:FastMCP 4.0.8+MCP Python SDK 2.2.0,本機 HTTP 伺服器,2026-09-24。session id 是那一次的亂數。

實驗場在你的瀏覽器裡跑,不用安裝任何東西、也不連任何伺服器——封包都是錄好的,圖表與模擬是現場算的。 首次載入約需 30–60 秒,正好夠你讀完第 1 節。

01 · 版本地圖

五個版本:從「打電話」到「寄信」

一句話重點:MCP 的版本號是日期(最後一次不相容改動的那天)。前四版都在同一條有狀態連線上加功能; 2026-07-28 把連線拆掉,改成每一發請求自帶一切。

第一版規格的「基礎協定」清單裡白紙黑字寫著 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" 起的)。

02 · 線路上的真相

新協定的一發請求,身上帶了什麼

一句話重點:沒有握手之後,每一發都要自我介紹——body 的 _meta 帶協定版本與客戶端能力, header 鏡射 method 與工具名,給看不懂 JSON-RPC 的中間設備讀。

這是開場 2026-07-28 那組的第 3 發,內容原封不動、只重新排版(FastMCP Client 送出的):

POST /mcp accept: application/json, text/event-stream content-type: application/json mcp-protocol-version: 2026-07-28 mcp-method: tools/call mcp-name: add {"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "add", "arguments": {"a": 2, "b": 3}, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": {"name": "mcp", "version": "0.1.0"}, "io.modelcontextprotocol/clientCapabilities": {}, "io.modelcontextprotocol/logLevel": "debug", "progressToken": 3}}}

對照握手年代(2025-11-25)同一個呼叫,body 只剩 {"name": "add", "arguments": {...}}, 身分全靠 header 裡那把 mcp-session-id——伺服器得記得「這把 id 是誰、協商了什麼」。 回應也多了東西:新協定的每個結果都有 resultType(complete 或 input_required), 列表類結果帶 ttlMs/cacheScope 快取提示。

一個誠實的細節:無狀態不等於省流量。實測同一個動作,新協定 3 發請求的 body 合計比舊協定 6 發還大 (數字在實驗場 2️⃣ 的帳本裡)——每一發都自帶身分證是要成本的。它換到的是下一節的東西。

03 · 為什麼要無狀態

站在負載平衡器的位置看

一句話重點:session 活在發它的那台伺服器的記憶體裡。副本一多,請求走錯台就是 Session not found; 無狀態協定讓任何一台都接得住任何一發。

我們真的起了兩台副本(A、B)和一個「輪流分派」的負載平衡器,新舊客戶端各做一次「列工具+呼叫 add」:

情境請求落點(實測)結果
輪流 × 新協定A:discover B:tools/list A:tools/call,全部 200成功
輪流 × 舊協定A:initialize 拿到 session,下一發被分到 B → 404MCPError: 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 擴充(投遞 → 輪詢)。

04 · 手刻一發請求

不用 SDK、不用握手,一發 POST 呼叫工具

一句話重點:四樣東西一個都不能少、而且要彼此一致——版本 header、_meta 信封、 Mcp-Method、Mcp-Name。

實驗場 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 …
3header 版本=_meta 版本-32020 mcp-protocol-version header does not match …
4Mcp-Method 要有且等於 body 的 method-32020 mcp-method header does not match …
5Mcp-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 規格分配的。

05 · FASTMCP 4 功能地圖

我遇到這個問題 → 用哪個功能

一句話重點:傳輸無狀態,應用照樣可以有狀態——FastMCP 4 把 session、反問、長任務 改成應用層的顯式零件,一台伺服器同時服務新舊兩個年代。
遇到的問題用這個
已經有 FastAPI 服務/別家 REST API 的 OpenAPI 規格FastMCP.from_fastapi(app)/FastMCP.from_openapi(spec, client)
好幾台 server 想合成一台;要轉手別人的 serverhub.mount(sub, namespace=…)/create_proxy(…)
agent 要同時接很多台(客戶端)ClientGroup(4.0.0b5 新增)
工具幾十個,模型挑不準BM25SearchTransform:模型只看到 search_tools+call_tool
每次呼叫都要記 log、計時、限流Middleware
只有管理員能用某些工具auth=+require_scopes("admin")
gateway 要依租戶分流;客戶端一直重複 list_toolsx-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 環境跑)。

06 · 速查

本課名詞速查卡

協定版本(日期) 最後一次不相容改動的日期;現行 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。
07 · 實戰

換你動手

LEVEL 1

在實驗場 4️⃣ 轉出唯一回 200 的組合。接著把版本 header 和 _meta 都換成 2025-11-25——錯誤訊息變成什麼?伺服器為什麼跟你要 session?

LEVEL 2

在 3️⃣ 把副本拉到 3、策略選「輪流分派」,舊協定大約多少 session 能整段成功?換成「黏著分派」呢?黏著救得了「那台副本重啟」嗎?

LEVEL 3

在 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)實測過;其他平台與雲端筆記本沒有驗證。

08 · 驗收

情境測驗

離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。

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。最可能的原因是?

POST /mcp MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/list Mcp-Name: add {"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "add", "arguments": {"a": 2, "b": 3}, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}} → 400 {"jsonrpc":"2.0","id":1,"error":{"code":-32020, "message":"mcp-method header does not match the request body's method"}}

這段錯誤訊息是本課實測原文:新協定把 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 接上來一切正常;換成新版客戶端呼叫同一個工具,得到下面的錯誤。該怎麼修?

ToolError: elicitation via server-initiated requests is unavailable on 2026-07-28 connections.

錯誤原文(本課在 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 沒有任何可追蹤的把手,使用者問第二次時伺服器也不知道是哪一份報表。

Python 環境載入中(首次約 30–60 秒)…讀完第 1 節它就好了