資源
資源是你公開給應用程式讀取的資料。
分界就在這裡。工具是模型決定要呼叫的東西;資源是應用程式決定要載入的東西(一個設定檔、一筆紀錄、一份文件),再放到模型面前當作上下文。
在一個普通的 Python 函式上加上 @mcp.resource(uri),就宣告了一個資源。
第一個資源
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.resource("config://app")
def get_config() -> str:
"""The active shop configuration."""
return "theme=dark\nlanguage=en"
形狀和工具一樣,只多了一樣東西:URI。資源靠位址定位,而不是靠名稱。用戶端要的是 config://app,從來不是 get_config。
其餘的部分,SDK 照樣從函式讀出來:
- 名稱就是函式名稱:
get_config。 - 用戶端看到的描述是 docstring。
- 內容就是你回傳的東西。
在 resources/list 期間,用戶端會收到:
{
"name": "get_config",
"uri": "config://app",
"description": "The active shop configuration.",
"mimeType": "text/plain"
}
當它讀取 config://app 時,函式會執行,回傳值以文字形式送回:
result.contents # [TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")]
Tip
列出資源的成本很低。函式在 resources/list 期間不會執行,只有在 resources/read 時才會,而且只針對用戶端要求的那個 URI。就算公開了一千個資源,也只需要為有人打開的那幾個付出代價。
試試看
用 MCP Inspector 執行伺服器:
uv run mcp dev server.py
打開它印出的 URL,切到 Resources 分頁。config://app 會連同描述一起出現在清單裡。點一下,Inspector 就會讀取它:那兩行設定就在眼前。
資源範本
一筆紀錄一個 URI 沒辦法擴展。在 URI 裡放一個佔位符,函式上加一個對應的參數:
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.resource("config://app")
def get_config() -> str:
"""The active shop configuration."""
return "theme=dark\nlanguage=en"
@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
"""A customer's profile."""
return f"User {user_id}: 12 orders since 2021."
URI 裡有 {user_id},函式上有 user_id: str。整個約定就這樣。
這樣就成了資源範本,而且會搬家:它離開 resources/list,改出現在 resources/templates/list,以樣式而不是位址的形式呈現:
{
"name": "get_user_profile",
"uriTemplate": "users://{user_id}/profile",
"description": "A customer's profile.",
"mimeType": "text/plain"
}
用戶端填入佔位符,讀取一個具體的 URI:users://42/profile、users://ada/profile。同一個函式回應所有這些 URI,比對到的值會以 user_id 傳入:
result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")]
注意結果裡的 uri。那是用戶端要求的具體 URI,不是範本。
Check
佔位符和參數必須一致。如果把函式參數改名為 user,URI 卻還寫著 {user_id},裝飾器會在匯入時就拒絕,任何用戶端都還來不及靠近:
ValueError: Mismatch between URI parameters {'user_id'} and function parameters {'user'}
不一致只可能是 bug,所以 SDK 讓帶著這種錯誤的伺服器根本啟動不了。
佔位符語法遵循 RFC 6570:{+path} 用於多段的值,{?q,lang} 用於選用的查詢參數,還有更多。SDK 預設也會對擷取出來的值做路徑安全檢查。完整參考請見 URI 範本與路徑安全。
get_user_profile 也可以接受一個註記為 Context 的參數。SDK 會注入它,而且絕不會把它當成 URI 參數;它能提供什麼,Context 頁面有說明。
回傳什麼
不限於 str。替每個資源指定 mime_type,回傳合適的東西即可:
import base64
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.resource("docs://readme", mime_type="text/markdown")
def readme() -> str:
"""How to use this server."""
return "# Bookshop\n\nSearch the catalog with the `search_books` tool."
@mcp.resource("stats://catalog", mime_type="application/json")
def catalog_stats() -> dict[str, int]:
"""Live counts for the catalog."""
return {"books": 1204, "authors": 391}
@mcp.resource("covers://placeholder", mime_type="image/gif")
def placeholder_cover() -> bytes:
"""A 1x1 transparent GIF, shown when a book has no cover."""
return base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")
readme回傳str,所以原樣送出。這是最常見的情況。-
catalog_stats回傳dict,所以 SDK 會替你序列化成 JSON 文字:{ "books": 1204, "authors": 391 } -
placeholder_cover回傳bytes,所以用戶端收到的是BlobResourceContents而不是TextResourceContents,位元組以 base64 編碼後放在blob欄位裡。
同樣的規則適用於其他任何可序列化為 JSON 的東西:list、Pydantic 模型、dataclass。只要不是 str 也不是 bytes,就會變成 JSON。
mime_type 由你宣告,預設為 text/plain。SDK 從不會檢查回傳的內容來猜測它,所以沒標示的 dict 資源仍然會以純文字對外宣告。
Tip
不想從函式推導時,@mcp.resource() 也接受 name=、title= 和 description=。如果根本沒有函式要寫,mcp.server.mcpserver.resources 裡有現成的 Resource 類別(TextResource、BinaryResource、FileResource、HttpResource、DirectoryResource),用 mcp.add_resource(...) 註冊即可。
用戶端也可以訂閱資源,在它變更時收到通知;那是用戶端那一半的事,寫在 用戶端 裡。
重點回顧
- 在函式上加
@mcp.resource(uri),它就成了資源。URI 是位址,回傳值是內容,docstring 是描述。 - URI 裡有
{placeholder}就成了範本:它列在resources/templates/list底下,同一個函式服務所有符合的 URI。 - 佔位符名稱必須等於函式的參數名稱。弄錯的話,匯入時就會知道,不用等到正式環境。
- 函式在資源被讀取時執行,而不是被列出時。
str變成文字,bytes變成 base64 blob,其他的都變成 JSON 文字。用mime_type=來標示。- 工具讓模型採取行動,資源讓應用程式讀取。
第三種基本元件,由人從選單裡挑選的那種,是 提示詞。