測驗:Resources、Prompts 與 Context
共 5 題,點選答案後會立即顯示結果
1. 依文章的分類,MCP 三種原語中「Resource」最像下列哪一個?
2. 關於 @mcp.resource("data://config") 的敘述,何者正確?
3. 下面這個資源模板,client 讀 users://42 時會發生什麼?
4. 關於 ctx: Context 參數,下列何者正確?
5. 依文章的實務判斷,「新增一筆訂單」這個需求應該用哪種原語?
【MCP 與 FastMCP 實戰】系列 #03(共 4 篇)|難度:L2-進階
前兩篇我們搞懂了 MCP 的原語(Tool / Resource / Prompt),也用 FastMCP 寫出了第一個 tool。 到這裡你會發現:AI 幫你生的 MCP server 幾乎清一色都是 @mcp.tool——因為 tool 最好懂、最萬用。
但一個「好用」的 server,通常會混用三種原語: Tool 做事、Resource 給資料、Prompt 給範本。 再加上一個常被忽略、但一看到就代表這個 server 有在認真做事的東西——Context 物件。
這篇的重點不是教你從零寫,而是教你看懂 AI 交出來的 server 裡,這三種東西各自在幹嘛、有沒有用對地方、有沒有踩雷。
因為這裡最經典的坑是:AI 把所有東西都寫成 tool,能跑、也有輸出,但把「純讀資料」硬做成 tool、把「該給使用者選的範本」也塞進 tool,結果 server 又肥又難維護。你要能一眼看出「這個其實該是 resource / prompt」。
版本提醒:FastMCP 目前主流是 v2.x,本文範例以此為準。v4 是 beta,有些新寫法(例如
CurrentContext())本文會標註「認得就好」。AI 生出來的 code 兩種混著都可能出現。
一句話說明
| 原語 | 一句話 | 像什麼 | 誰在控制 |
|---|---|---|---|
| Tool | 「幫我做一件事」(可能有副作用) | POST/執行一個動作 | 模型決定何時呼叫 |
| Resource | 「給我看一份資料」(唯讀) | GET/讀一個檔案 | 應用程式決定載入 |
| Prompt | 「照這個範本幫我開場」 | 一段可重用的提示語 | 使用者主動挑選 |
記住這張表,你就抓到八成了。看 code 時心裡一直問:這段到底是「做事」、「給資料」還是「給範本」?
Resource:唯讀資料,長這樣
AI 寫的 resource 最小長這樣:
from fastmcp import FastMCP
mcp = FastMCP(name="DataServer")
@mcp.resource("data://config") # 這是這份資料的「網址」,client 用它來讀
def get_config() -> str: # 有人讀 data://config 時才會執行這個函式
"""提供應用程式設定。"""
return '{"theme": "dark", "version": "1.2.0"}'
Code language: PHP (php)逐行翻譯:
@mcp.resource("data://config"):宣告一個唯讀資源,它的 URI(唯一網址)是data://config。def get_config() -> str:函式名會變成資源名稱,docstring 會變成描述。- 重點:這個函式不是一啟動就跑,而是有人來讀
data://config時才跑(叫 lazy loading,延遲載入)。
「這在幹嘛」:把一份資料掛在一個網址上,client 想看的時候來拿,跟後端的
GET /config是同一個味道。
回傳值 AI 常見有幾種寫法:
@mcp.resource("data://config")
def get_config() -> dict: # 回傳 dict,FastMCP 會自動轉成 JSON
return {"theme": "dark", "version": "1.2.0"}
Code language: PHP (php)回 str 就是純文字,回 dict / list 會自動被序列化成 JSON。你不用自己 json.dumps(雖然 AI 有時會多此一舉自己轉,能跑但沒必要)。
資源模板:帶參數的 URI(重點)
上面那個是「固定一份資料」。更常見的是同一種資料、依 id 不同給不同內容——這就是資源模板(Resource Template):
@mcp.resource("users://{user_id}") # URI 裡有 {user_id},就變成「模板」
def get_user(user_id: str) -> dict: # 函式參數名要跟 {user_id} 一模一樣
"""依 user_id 回傳使用者資料。"""
return {"id": user_id, "name": f"User {user_id}"}
Code language: PHP (php)逐行翻譯:
- URI 裡出現
{user_id}這種大括號 → FastMCP 認得這是模板,不是固定資源。 - client 讀
users://42時,user_id就會被填成"42"傳進函式。 - 關鍵驗收點:
{大括號裡的名字}一定要跟函式參數名對得上,對不上就爆。
「這在幹嘛」:等於後端的
GET /users/{id}。URI 是路由,大括號是路徑參數。
認得就好:兩種括號的差別
@mcp.resource("files://{filename}") # 單段:只吃一段,不跨 /
def get_file(filename: str) -> str: ...
@mcp.resource("path://{filepath*}") # 帶星號 = 萬用,可以吃含 / 的多段路徑
def get_path(filepath: str) -> str: ...
Code language: PHP (php){filename}:只匹配一段,files://a/b不會匹配。{filepath*}(多一個*):可以匹配path://docs/server/x.md這種多層路徑。
看到 * 不用慌,就是「這個參數可以含斜線、吃多段」。
✅ 必看懂:{param} 要對應同名函式參數。 🕰️ 認得就好:{param*} 萬用參數(處理檔案路徑時才會出現)。
Prompt:可重用的提示範本
Prompt 是給使用者挑的對話範本。跟 tool 最大的差別:tool 是模型自己決定要不要呼叫;prompt 是使用者從選單點下去、拿來當開場白的。
from fastmcp import FastMCP
mcp = FastMCP(name="PromptServer")
@mcp.prompt # 注意:沒有括號、沒有 URI
def ask_about_topic(topic: str) -> str:
"""產生一句請 AI 解釋某主題的提示。"""
return f"可以請你解釋『{topic}』這個概念嗎?"
Code language: PHP (php)逐行翻譯:
@mcp.prompt:宣告一個提示範本(跟 resource 不同,不需要 URI,用函式名當識別)。topic: str:這個範本的參數。使用者選這個 prompt 時,client 會問他要填什麼topic。- 回傳的字串,就是最後餵給 AI 的訊息。
多輪訊息版本
有時候 AI 會回傳一串訊息(模擬一段對話開場):
from fastmcp.prompts import Message
@mcp.prompt
def code_request(language: str, task: str) -> list[Message]:
"""產生一段請 AI 寫 code 的對話。"""
return [
Message(f"請用 {language} 寫一個函式,功能是:{task}"),
Message("好的,我來幫你寫。", role="assistant"), # 預設 role 是 user
]
Code language: CSS (css)Message(...)預設role="user"(使用者說的)。- 加
role="assistant"表示「這句假裝是 AI 已經回的」,用來鋪陳語氣。
「這在幹嘛」:把「每次都要打的那串開場白」存成一個範本,使用者點一下、填幾個空,就自動展開成完整提示。像 IDE 的 code snippet。
Context:一看到就代表 server 有在認真做事
Context 是你在 tool / resource / prompt 裡多接一個參數,就能拿到的「跟 client 溝通的管道」。它能做:記 log、回報進度、讀其他 resource、跟使用者要輸入。
from fastmcp import FastMCP, Context
mcp = FastMCP(name="Demo")
@mcp.tool
async def process_file(file_uri: str, ctx: Context) -> str:
# ^^^^^^^^^^^^ 多接一個 Context,FastMCP 自動塞進來
await ctx.info(f"開始處理 {file_uri}") # 送一則 log 回 client,讓人看得到進度
return "處理完成"
Code language: PHP (php)逐行翻譯:
ctx: Context:靠型別標註認人,參數叫什麼名字不重要,只要型別是Context,FastMCP 就會自動把 context 塞進來。- client 看不到這個參數:它不會出現在給 AI 的工具說明裡,純粹是 server 內部用的。
await ctx.info(...):Context 的方法幾乎都是 async,所以函式通常要寫成async def、呼叫要加await。
✅ **必看懂**:
ctx: Context是型別注入,靠型別不靠名字;client 看不到它。
Context 最常見的四招
# 1) 記 log(四個等級)
await ctx.debug("除錯訊息")
await ctx.info("一般訊息")
await ctx.warning("警告")
await ctx.error("出錯了")
# 2) 回報進度(跑很久的工作,讓 client 顯示進度條)
await ctx.report_progress(progress=50, total=100) # 做到一半了
# 3) 在 tool 裡讀「別的 resource」
result = await ctx.read_resource("data://config")
content = result.contents[0].content # 拿到內容
# 4) 跟使用者要輸入(互動)
answer = await ctx.elicit("請輸入你的名字:", response_type=str)
if answer.action == "accept":
name = answer.data
Code language: PHP (php)你不用背這些方法,但看到它們你要知道意義:
ctx.report_progress→ 這是個跑得久的操作(AI 願意寫這個,通常代表它有想到 UX)。ctx.read_resource→ 這個 tool 會去讀別的資料源,資料流不只有參數進來這一條。ctx.elicit→ 這個 tool 執行到一半會停下來問使用者(比較新的功能)。
🕰️ 認得就好(v4 新寫法):新版有時會寫成 ctx: Context = CurrentContext(),意思一樣,都是拿 context,只是換了注入寫法。看到別以為是新東西。
什麼時候該用哪個?(實務判斷)
這是驗收 AI 產出時最值得看的一點。給你一個決策口訣:
它會「改變狀態」或「執行動作」嗎?(寫入 DB、寄信、呼叫 API 做事)
→ 是 → Tool
它只是「把一份資料讀出來給看」嗎?(設定、檔案、查詢結果,唯讀)
→ 是 → Resource(固定就用 data://x,要帶 id 就用 data://{id})
它是「一段要重複使用、給使用者挑的開場白/範本」嗎?
→ 是 → Prompt
Code language: JavaScript (javascript)對照一下就很清楚:
| 需求 | 該用 | 為什麼 |
|---|---|---|
| 「新增一筆訂單」 | Tool | 有副作用、在做事 |
| 「讀取系統設定」 | Resource | 唯讀、像 GET |
| 「依 ID 拿使用者資料」 | Resource 模板 | 唯讀 + 帶參數 |
| 「幫我把 bug report 整理成標準格式」 | Prompt | 可重用的範本 |
🚩 紅旗:看到這些要警覺
紅旗 1:什麼都做成 tool
@mcp.tool
def get_config() -> dict: # 🚩 這根本沒做事,只是讀設定 → 該是 resource
return load_config()
Code language: CSS (css)純唯讀、沒有副作用的「拿資料」被寫成 tool。能跑,但語意錯了,也讓模型的工具清單無謂變長。 跟 AI 說:「這個只是唯讀取資料,改寫成 @mcp.resource 比較合適嗎?」
紅旗 2:資源模板的參數名對不上
@mcp.resource("users://{user_id}")
def get_user(id: str) -> dict: # 🚩 URI 是 {user_id},參數卻叫 id → 對不上會爆
...
Code language: PHP (php)大括號名字必須等於函式參數名。這種錯 AI 偶爾會犯,尤其是它改了其中一邊忘了改另一邊。
紅旗 3:用了 Context 卻沒 async / await
@mcp.tool
def process(ctx: Context) -> str: # 🚩 沒 async
ctx.info("start") # 🚩 沒 await → 這行等於沒真的送出
return "done"
Code language: CSS (css)Context 的方法是 async 的。少了 async def 或少了 await,log 和進度回報會默默失效——不會報錯,但 client 什麼都收不到。這種「靜默失效」最難抓。
紅旗 4:把敏感資料塞進 resource 直接吐
@mcp.resource("data://secrets")
def get_secrets() -> dict:
return {"api_key": os.environ["API_KEY"]} # 🚩 金鑰被當成資源公開
Code language: PHP (php)Resource 是給 client / LLM 讀的。把金鑰、密碼放進去,等於主動遞出去。看到 resource 在回傳環境變數、token、密碼,要立刻警覺。
Vibe Coder 驗收檢查點
拿到 AI 生的 MCP server,你可以這樣驗收:
- 分類數一數:打開檔案,數
@mcp.tool/@mcp.resource/@mcp.prompt各幾個。如果全部都是 tool,八成有該拆成 resource 的東西 → 問 AI。 - 模板參數對名字:每個
@mcp.resource("...{x}..."),確認函式裡有同名參數x。用編輯器搜大括號最快。 - Context 一定 async:搜
ctx.,確認每一處前面都有await、所在函式是async def。 - 實際跑一次(FastMCP 內建開發工具):
- 問 AI 檢查一輪(直接複製):
看不懂就這樣問 AI
- 「用白話一句話解釋這段
@mcp.resource("data://{id}")在幹嘛,我是初學者。」 - 「這個 server 為什麼要用 Context?拿掉它會怎樣?」
- 「幫我判斷這幾個函式,各自應該是 tool、resource 還是 prompt,並說明理由。」
小結
- Tool 做事、Resource 給資料(唯讀)、Prompt 給範本——先分類,再看細節。
- 資源模板
data://{id}:URI 大括號名字 = 函式參數名,等於後端的路徑參數。 - Context(
ctx: Context):記 log、回報進度、讀別的 resource、跟使用者互動;方法都是 async,少了await會靜默失效。 - 驗收重點:別讓所有東西都變成 tool,並盯緊模板參數名與 async/await。
下一篇(#04)我們會把這個 server 真的「上線」——部署與跟 client 串接。
進階測驗:Resources、Prompts 與 Context
共 5 題,包含情境題與錯誤診斷題。