Resources、Prompts 與 Context:讓 MCP Server 更好用

測驗:Resources、Prompts 與 Context

共 5 題,點選答案後會立即顯示結果

1. 依文章的分類,MCP 三種原語中「Resource」最像下列哪一個?

  • A. POST/執行一個有副作用的動作
  • B. GET/唯讀地讀取一份資料
  • C. 一段給使用者挑選的對話開場白範本
  • D. 一個背景排程任務

2. 關於 @mcp.resource("data://config") 的敘述,何者正確?

  • A. server 一啟動就會立刻執行該函式並快取結果
  • B. 它會改變狀態,屬於有副作用的操作
  • C. 函式只有在 client 來讀該 URI 時才執行(延遲載入)
  • D. 回傳值一定要自己先 json.dumps 才能用

3. 下面這個資源模板,client 讀 users://42 時會發生什麼?

@mcp.resource(“users://{user_id}”) def get_user(user_id: str) -> dict: return {“id”: user_id, “name”: f”User {user_id}”}
  • A. user_id 被填成 “42” 傳進函式,回傳該使用者資料
  • B. 會報錯,因為模板不能帶參數
  • C. 只會回傳固定的預設值,忽略 42
  • D. 需要另外呼叫一個 tool 才能取得資料

4. 關於 ctx: Context 參數,下列何者正確?

  • A. 參數一定要命名為 ctx,改名就失效
  • B. 它會出現在給 AI 的工具說明裡,供模型填值
  • C. Context 的方法都是同步的,不需要 await
  • D. 靠型別標註注入,client 看不到它,方法多為 async

5. 依文章的實務判斷,「新增一筆訂單」這個需求應該用哪種原語?

  • A. Resource,因為它跟資料有關
  • B. Tool,因為它會改變狀態、有副作用
  • C. Prompt,因為它是可重用的範本
  • D. 資源模板,因為它帶了一個訂單參數

【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,你可以這樣驗收:

  1. 分類數一數:打開檔案,數 @mcp.tool / @mcp.resource / @mcp.prompt 各幾個。如果全部都是 tool,八成有該拆成 resource 的東西 → 問 AI。
  2. 模板參數對名字:每個 @mcp.resource("...{x}..."),確認函式裡有同名參數 x。用編輯器搜大括號最快。
  3. Context 一定 async:搜 ctx.,確認每一處前面都有 await、所在函式是 async def
  4. 實際跑一次(FastMCP 內建開發工具):
  5. 問 AI 檢查一輪(直接複製):

看不懂就這樣問 AI

  • 「用白話一句話解釋這段 @mcp.resource("data://{id}") 在幹嘛,我是初學者。」
  • 「這個 server 為什麼要用 Context?拿掉它會怎樣?」
  • 「幫我判斷這幾個函式,各自應該是 tool、resource 還是 prompt,並說明理由。」

小結

  • Tool 做事、Resource 給資料(唯讀)、Prompt 給範本——先分類,再看細節。
  • 資源模板 data://{id}:URI 大括號名字 = 函式參數名,等於後端的路徑參數。
  • Contextctx: Context):記 log、回報進度、讀別的 resource、跟使用者互動;方法都是 async,少了 await 會靜默失效。
  • 驗收重點:別讓所有東西都變成 tool,並盯緊模板參數名與 async/await。

下一篇(#04)我們會把這個 server 真的「上線」——部署與跟 client 串接。

進階測驗:Resources、Prompts 與 Context

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你在驗收 AI 生的 MCP server,發現下面這段。你該建議怎麼改?情境題

@mcp.tool def get_config() -> dict: return load_config() # 純唯讀,讀出系統設定
  • A. 不用改,能跑就好,tool 最萬用
  • B. 改成 @mcp.resource,因為它是唯讀取資料、沒有副作用
  • C. 改成 @mcp.prompt,因為它回傳結構化資料
  • D. 加上 async/await 就好,語意不用動

2. 你要寫一個「跑很久、想在 client 顯示進度條」的 tool,最適合用 Context 的哪個方法?情境題

  • A. await ctx.info(“50%”)
  • B. await ctx.read_resource(“progress://50”)
  • C. await ctx.report_progress(progress=50, total=100)
  • D. await ctx.elicit(“進度 50%”, response_type=int)

3. 需求是「幫我把 bug report 整理成公司標準格式的提示,讓團隊每個人都能重複用」。最適合的原語是?情境題

  • A. Tool,因為它要處理文字
  • B. Resource,因為它是一份文件
  • C. 資源模板,因為每個 bug 有不同 id
  • D. Prompt,因為它是可重用、給使用者挑的提示範本

4. 這個資源模板註冊或呼叫時會出問題,原因是什麼?錯誤診斷

@mcp.resource(“users://{user_id}”) def get_user(id: str) -> dict: return {“id”: id}
  • A. URI 不能用 users:// 開頭
  • B. URI 的 {user_id} 與函式參數名 id 對不上,兩者必須同名
  • C. 回傳型別不能是 dict
  • D. 資源模板一定要寫成 async def

5. 這個 tool 執行後,client 完全收不到任何 log,也不報錯。最可能的原因是?錯誤診斷

@mcp.tool def process(ctx: Context) -> str: ctx.info(“start”) return “done”
  • A. Context 沒有 info 方法,應該用 print
  • B. tool 不能使用 Context,只有 resource 可以
  • C. 函式不是 async 且 ctx.info 沒 await,非同步呼叫沒被真正送出(靜默失效)
  • D. log 等級 info 預設被過濾,要改用 ctx.error

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *