AI 互動教室 ‹ 學 LLM 應用開發
下載 .py 開啟實戰 notebook ↗ 留言回報
FASTMCP 4.0 · SESSIONLESS PROTOCOL

FastMCP 4:把函式變成 AI 工具,
一發請求不用握手

上一課你手寫工具說明書、手組兩回合。MCP 把這件事標準化:你蓋一台工具伺服器, Claude Desktop、Claude Code、Cursor 都能直接接上來用你的函式;FastMCP 讓這台伺服器只剩一個裝飾器。 而 4.0 最大的改版是協定變無狀態——同樣「呼叫一次 add」,舊協定要握手、發 session id、綁死同一台伺服器; 新協定每個請求自帶一切。按下去比較:

舊協定(握手時代)

新協定(4.0 預設)

這堂課的 notebook 不連任何外部服務——它會在 molab 裡真的起一台 HTTP 伺服器並側錄每個請求, 上面這張圖的每一列都是實測結果。本課用 fastmcp==4.0.0b1

01 · 一個裝飾器

說明書自動生成、參數自動把關

from fastmcp import FastMCP mcp = FastMCP("茶飲店") @mcp.tool def search_menu(keyword: str, limit: int = 3) -> list[dict]: """依關鍵字搜尋菜單,回傳最多 limit 筆品項(含價格)。""" return [m for m in MENU if keyword in m["name"]][:limit]

沒有寫任何 schema。FastMCP 從型別提示、預設值與 docstring 生成說明書: limitdefault: 3keywordrequired。 用 Client(mcp) 直接連同一個 process 裡的伺服器(不開網路)就能 list_tools()call_tool();故意把 a 給成 "two", pydantic 在進你的函式之前就擋下,回一個 ToolError 說明哪個欄位錯。

工具之外還有兩種東西:resource(給 AI 讀的資料,URI 定址如 menu://today, URI 放 {name} 就是 template)與 prompt(帶參數的話術範本)。 tool 是模型主動呼叫、resource 是應用決定要不要餵、prompt 是使用者挑選。

到 notebook 的 1️⃣–2️⃣ 節:蓋伺服器、連上去、加 resources 與 prompts
02 · 無狀態協定

同一台伺服器、兩種協定,側錄給你看

notebook 用 mcp.http_app()(標準 ASGI app)在背景起一台真的 HTTP 伺服器,前面塞一個只側錄不干擾的中介層。 Client(網址) 自動協商最新協定,mode="legacy" 強制舊的。兩邊各呼叫一次 add

舊協定 2025-11-25新協定 2026-07-28
HTTP 請求數6(initialize → initialized → GET 長連線 → tools/call → tools/list → DELETE)3(server/discover → tools/call → tools/list)
session idinitialize 後伺服器發 mcp-session-id,之後每發都帶
請求自帶什麼只有 JSON-RPC bodyheader mcp-methodmcp-name+body 的 _meta 信封(協定版本、客戶端能力)
可以落到哪台副本只能回到發 session 的那台任何一台

最有感的證明:不用 SDK、不握手,一發 httpx.post 就能呼叫工具——

meta = {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}} r = httpx.post("http://127.0.0.1:8765/mcp", json={"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "add", "arguments": {"a": 40, "b": 2}, "_meta": meta}}, headers={"Accept": "application/json, text/event-stream", "MCP-Protocol-Version": "2026-07-28", "mcp-method": "tools/call", "mcp-name": "add"}) # → 200 {"result": 42} # 同一個 body 不宣告新協定 → 400 "Bad Request: Missing session ID"
到 notebook 的 3️⃣ 節:起伺服器、側錄表、裸 POST
03 · 有狀態應用

傳輸無狀態,應用照樣記得你:SessionId

購物車、多步驟流程怎麼辦?4.0 的答案是把狀態綁在一把鑰匙上而不是連線上: 裝上 SessionProvider(),伺服器自動多出 create_session()(發一把猜不到的 uuid) 與 end_session();你的工具宣告 session_id: SessionId,用 get_session(session_id) 拿到可 getset 的小儲存格。

mcp.add_provider(SessionProvider()) @mcp.tool async def add_to_cart(session_id: SessionId, item: str) -> list[str]: s = await get_session(session_id) items = await s.get("items", default=[]) items.append(item) await s.set("items", items) return items

實測:拿鑰匙加兩樣東西 → 換一條全新連線帶同一把鑰匙 → 購物車還在;亂猜一把 → Invalid or unknown session。 FastMCP 還會在 session_id 的 schema 裡自動寫一段給 AI 看的說明(先建 session、之後每次帶著)。 另一個選項 session: UserSession 自動注入、不進 schema,但要有認證身分——它把狀態綁在登入的使用者上。

到 notebook 的 4️⃣ 節:購物車、換連線、亂猜鑰匙
04 · 上線

接給真的 AI 客戶端

# server.py 結尾 if __name__ == "__main__": mcp.run(transport="http", host="0.0.0.0", port=8000) # 終端機 uv run --with "fastmcp==4.0.0b1" --with "fastmcp-slim==4.0.0b1" python server.py claude mcp add --transport http tea http://localhost:8000/mcp

接上之後對 AI 說「幫我找有茶的飲料」,它會自己呼叫 search_menu。 4.0 還拿掉了 ctx.sample()ctx.list_roots() 這類需要活連線的反向呼叫、 新增多回合互動工具與背景任務——細節在 notebook 末節,知道有就好。

到 notebook 的 5️⃣ 節:部署方式與 4.0 其他改動
05 · 實戰

換你動手

LEVEL 1

加一個 place_order(item: str, qty: int = 1) 工具,重跑 list_tools() 確認說明書自動更新。

LEVEL 2

購物車加 checkout(session_id):算總價、清空、回傳收據。

LEVEL 3

改裸 POST 去呼叫 tools/listresources/read(讀 menu://today)——查 MCP 規格找出 params 格式。

卡住了?每一題在 notebook 末節都有折疊解答——先自己做,再打開對照。

06 · 驗收

情境測驗

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

Q1 情境題

你的茶飲店伺服器已經有 search_menu 工具。現在想把「今日完整菜單」這份資料也提供出去——由接上來的應用程式決定要不要餵進模型的上下文。應該怎麼註冊?

三者的分工就是答案:tool 是模型主動呼叫、resource 是應用程式決定要不要餵、prompt 是使用者挑選——「給 AI 讀的資料、由應用決定餵不餵」正是 resource 的定義,URI 裡放 {name} 還能升級成 template(menu://item/{name})。A 能動,但把資料做成動作,主導權從應用移到模型,模型沒想到呼叫就沒有菜單;C 是給使用者一鍵套用的範本,不是資料通道;D 會污染工具說明書——docstring 是給模型看的使用說明,菜單一改還得改程式碼。

Q2 錯誤診斷

你想試「一發 POST 呼叫工具」,發出下面的請求卻拿到 400。最可能的原因是?

r = httpx.post("http://127.0.0.1:8765/mcp", json={"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "add", "arguments": {"a": 40, "b": 2}, "_meta": meta}}, headers={"Accept": "application/json, text/event-stream"}) # → 400 "Bad Request: Missing session ID"

新舊協定走同一個端點,伺服器靠 header 分流:補上 MCP-Protocol-Version: 2026-07-28mcp-methodmcp-name,同一個 body 立刻 200 拿到 42;不宣告就被當舊協定客戶端,而舊協定的規矩是先 initialize 握手拿 mcp-session-id——所以才回 Missing session ID。A 不對:400 是伺服器親口回的 HTTP 狀態,連不上根本不會有回應;B 正好說反——無握手一發就中正是新協定的賣點;C 位置沒錯,params._meta 就是規格寫的位置。

Q3 情境題

你要在 4.0 無狀態協定上做購物車:同一位顧客分多次呼叫加品項、中途可能換連線,而且不同顧客不能看到彼此的購物車。應該怎麼做?

4.0 的口訣是「stateless transport, stateful application」——狀態綁在鑰匙上,不綁在連線上。實測:換一條全新連線帶同一把鑰匙,購物車還在;亂猜一把拿到 Invalid or unknown session,顧客之間自然隔離。B 所有顧客共用同一個 list,甲客加的珍奶會出現在乙客的購物車;C 能動但走回頭路——狀態綁死連線與那台副本,換連線就掉,正是 4.0 要擺脫的;D 部分正確,但 UserSession 把狀態綁在「登入的使用者」上,要有認證身分才能用,本課的伺服器沒有登入機制。

HANDS-ON · MOLAB

實作在 molab 跑(免費)

molab 的登入狀態進不了內嵌框架(瀏覽器的跨站 cookie 保護), 所以 notebook 要在新分頁執行——把它跟本頁並排開,左邊教學照樣對照。

  1. 登入 molab(GitHub / Google)
  2. 開啟課程 notebook,Fork 成自己的副本即可編輯
  3. 從第一格往下全部執行(首次安裝套件約 1 分鐘)——免費 CPU 環境即可,不需要 GPU

不想用 molab?下載 fastmcp4_ext.py 後在自己電腦 uvx marimo edit --sandbox fastmcp4_ext.py,依賴會自動安裝。