docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,122 @@
---
translation:
sections: [72f9c964769076dd, 9a2c14e10935b515, 235299eb78ab12d7, 8aee1e78c8237fb8, 9bd86acd4112138f, 55343cb7f250dc7b]
tool: 1
---
# 自動完成 {#completions}
用戶端如果在你的伺服器之上做一個 UI,會希望在使用者輸入時自動補上引數的值:語言名稱、儲存庫名稱、檔案路徑。
**自動完成**就是伺服器提供這些建議的方式。
## 值得自動完成的東西 {#something-worth-completing}
自動完成只適用於兩樣東西:**提示詞**的引數,以及**資源範本**的參數。所以先準備一個兩者各有一個的伺服器:
```python title="server.py" hl_lines="6 12"
--8<-- "docs_src/completions/tutorial001.py"
```
這裡還沒有任何跟自動完成有關的東西。
* `review_code` 接受一個 `language`。使用者不該得去猜你接受哪些拼法。
* `github_repo` 接受 `owner` 和 `repo`。兩個都放自由輸入的文字框,表單會很難用。
## 自動完成處理函式 {#the-completion-handler}
加上**一個**以 `@mcp.completion()` 裝飾的函式:
```python title="server.py" hl_lines="21-29"
--8<-- "docs_src/completions/tutorial002.py"
```
* 每個伺服器只有一個處理函式。所有自動完成請求都會送到這裡,再依正在完成的對象分支處理。
* 必須是 `async def`:SDK 會 await 它。
* 它會收到三個引數:
* `ref`:**哪一個**提示詞或資源範本,型別是 `PromptReference` 或 `ResourceTemplateReference`。用 `isinstance` 分辨兩者。
* `argument`:`argument.name` 是正在完成的引數,`argument.value` 是使用者目前輸入的內容。
* `context`:已經解析完成的引數。現在先不用管它。
* 回傳 `Completion(values=[...])`,沒有東西可建議時回傳 `None`。
!!! tip
`argument.value` 是使用者已輸入的前綴。SDK **不會**替你過濾:放進 `values` 的是什麼,UI 就顯示什麼。`startswith` 要自己寫。
### 試試看 {#try-it}
用 **[測試](../get-started/testing.md)** 裡的記憶體內 `Client` 來操作。以 `ref=PromptReference(name="review_code")` 和 `argument={"name": "language", "value": "py"}` 呼叫 `client.complete()`:
```python
result.completion.values # ['python']
```
* `ref` 跟處理函式收到的參照型別相同。
* `argument` 是個普通的 dict,剛好兩個鍵:`name` 和 `value`。
送出空的 `value`,就會拿回整份清單。`lang.startswith("")` 對每種語言都成立:
```python
result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript']
```
詢問 `code`(處理函式不認得的引數),它會回傳 `None`,SDK 會把它轉成空清單:
```python
result.completion.values # []
```
`None` 的意思是「沒有建議」,永遠不是錯誤。UI 會退回一般的文字框。
## 一個你從沒宣告過的能力 {#a-capability-you-never-declared}
註冊處理函式本身就是宣告。連上用戶端看看:
```python
client.server_capabilities.completions # CompletionsCapability()
```
你沒有在任何地方列出 `completions`。SDK 看到處理函式,就替你宣告了這項能力。每一項**可選**能力都是這樣運作的:處理函式就是宣告。(三個基本元件不是可選的:不管有沒有處理函式,`MCPServer` 一律會宣告它們。)
!!! check
回到第一個 `server.py`(沒有處理函式的那個),照樣問它一次。呼叫會失敗,得到 JSON-RPC 錯誤:
```text
Method not found
```
而且 `client.server_capabilities.completions` 是 `None`。這正是能力的用意:行為良好的用戶端會先檢查它,絕不會送出你無法回答的請求。
## 相依的引數 {#dependent-arguments}
`github://repos/{owner}/{repo}` 有兩個參數,而 `repo` 的合理值取決於先選了哪個 `owner`。
這就是 `context` 的用途。它帶著使用者**已經解析完成**的引數:
```python title="server.py" hl_lines="8-11 34-38"
--8<-- "docs_src/completions/tutorial003.py"
```
* 新的分支在範本的 `repo` 參數上觸發。
* `context.arguments` 是 `dict[str, str] | None`,存放目前已選的值(這裡是 `owner`)。
* 還沒有 `owner` 就沒有合理的建議,所以處理函式回傳 `None`。
用戶端用 `context_arguments=` 送出那些已解析的值。這次 `ref` 是 `ResourceTemplateReference(uri="github://repos/{owner}/{repo}")`。以空的 `value` 詢問 `repo`,並傳入 `context_arguments={"owner": "modelcontextprotocol"}`:
```python
result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector']
```
拿掉 `context_arguments=`,同樣的呼叫會回傳 `[]`。處理函式在知道 owner 之前,沒辦法知道該建議哪些儲存庫。
!!! info
`Completion` 也接受 `total=` 和 `has_more=`。當 `values` 只是更長清單的一部分時設定它們,UI 就能顯示「還有 200 個」。大多數處理函式用不到。
## 重點回顧 {#recap}
* 自動完成是給**提示詞引數**和**資源範本參數**的建議,僅此而已。
* `@mcp.completion()` 註冊那唯一的處理函式。它是 `async def (ref, argument, context) -> Completion | None`。
* 依 `isinstance(ref, ...)` 和 `argument.name` 分支。自己用 `argument.value` 過濾。
* `None` 會變成空清單,永遠不是錯誤。
* `context.arguments` 存放已解析的值;用戶端以 `context_arguments=` 提供它們。
* 一註冊處理函式,`completions` 能力就會出現。沒有它,請求會得到 `Method not found`。
建議是在使用者還在**填寫**提示詞或範本時幫忙;如果要在工具呼叫**進行到一半**時問使用者問題,要用的是 **[徵詢(elicitation)](../handlers/elicitation.md)**。工具除了文字之外還能回傳的所有東西,請見 **[圖片、音訊與圖示](media.md)**。

View file

@ -0,0 +1,131 @@
---
translation:
sections: [e33d441f12d50535, 7099694c603e0f5f, c1df4cf9673433e6, c9cd294541422e6e, 6cec073617bfd037, efa92b8f99e908c8, 6a22a29e27fb4601]
tool: 1
---
# 處理錯誤 {#handling-errors}
工具失敗的方式有兩種,而 SDK 對待它們的方式截然不同。
引發一般的例外,看到的是**模型**。引發 `MCPError`,看到的是**協定**。
這一頁談的是怎麼選。
## 模型能修正的錯誤 {#an-error-the-model-can-fix}
拿一個查東西的工具來說,讓查詢落空:
```python title="server.py" hl_lines="11-12"
--8<-- "docs_src/handling_errors/tutorial001.py"
```
這兩行跟 MCP 一點關係也沒有。`get_author` 引發的是普通的 `ValueError`,任何 Python 函式都會這麼做。
用一個不在目錄裡的書名呼叫它,看看結果:
```python
result.is_error # True
result.content # [TextContent(text="Error executing tool get_author: No book titled 'Nothing' in the catalog.")]
result.structured_content # None
```
* 請求**成功**了。有結果;呼叫端沒有引發任何東西。
* `is_error` 是 `True`,而例外的訊息(前面加上工具名稱)就在 `content` 裡,正是模型讀取的地方。
* `structured_content` 是 `None`。失敗的呼叫沒有回傳值可以結構化。
這是**工具錯誤**,也是工具引發**任何**例外時的預設行為。而且幾乎總是你想要的。
呼叫工具的是模型,引數也是它選的。所以工具錯誤就是對話中的一個回合:模型讀到「No book titled 'Nothing' in the catalog.」,發現自己猜錯了書名,就換個更好的再呼叫一次。只寫了一個 `raise`,就得到一個會自我修正的 agent。
!!! tip
永遠不要從工具 `return` 錯誤訊息。回傳的字串 `is_error=False`,所以在模型(以及每個用戶端 UI)看來,工具是成功的,那個字串就是答案。要用 `raise`。那個旗標才是訊號。
## 模型無法修正的錯誤 {#an-error-the-model-cannot-fix}
現在把 `ValueError` 換成 `MCPError`。
```python title="server.py" hl_lines="1 3 14"
--8<-- "docs_src/handling_errors/tutorial002.py"
```
`MCPError` 是 SDK 的**協定錯誤**。它是工具包裝層唯一**不會**攔截的例外:它會往外傳播,整個 `tools/call` 請求以 JSON-RPC 錯誤失敗,而不是回傳結果。
```json
{
"code": -32602,
"message": "No book titled 'Nothing' in the catalog."
}
```
* **沒有結果**。沒有 `content`,沒有 `is_error`:模型沒有東西可讀。
* 錯誤改由**主機(host)**應用程式收到,跟工具根本不存在時一模一樣。
* `code`、`message` 和 `data` 原封不動地送達。`INVALID_PARAMS` 是 `-32602`;`mcp.types` 把它和其他 JSON-RPC 錯誤碼(`INVALID_REQUEST`、`INTERNAL_ERROR`……)都匯出成常數,所以永遠不用手打魔術數字。
!!! check
同樣的查詢、同樣落空,但現在呼叫在用戶端**引發**例外,而不是回傳:
```text
mcp.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog.
```
第一個版本交給模型一句它能回應的話。這個版本什麼都沒給。對 `get_author` 來說這絕對更糟,而這正是下一節的重點。
## 該引發哪一個 {#which-one-to-raise}
兩條路徑回答的是兩個不同的問題。
* **引發任何例外**,用於**執行**上的失敗:工具想做的事沒做成。呼叫是模型選的,所以模型應該看到後果,並有機會補救。拼錯的書名、逾時的上游 API、不存在的資料列:都是工具錯誤。
* **引發 `MCPError`**,用於**請求本身**就該被拒絕的情況:用戶端缺少工具所依賴的能力、伺服器處於無法服務任何人的狀態、呼叫端跳過了必要的步驟。模型再怎麼重試也修不好這些,所以把訊息交給它毫無益處。
一個問題就能決定:**更聰明的模型能避開這個錯誤嗎?**能 -> 一般的例外。不能 -> `MCPError`。
照這個標準,第二版的 `get_author` 選錯了:換個更好的書名就能解決,所以模型理應看到訊息。放在那裡是為了示範機制,不是建議這麼做。
!!! info
`MCPError` 位於 `from mcp import MCPError`,接受 `code`、`message` 和選用的 `data` 承載。放進去什麼,用戶端就收到什麼:SDK 會把引發的 `MCPError` 原封不動地轉送,不會加以清理。
## 不存在的資源 {#a-resource-that-doesnt-exist}
資源也畫了同一條線,並為常見情況提供了一個具名的例外。
```python title="server.py" hl_lines="2 13"
--8<-- "docs_src/handling_errors/tutorial003.py"
```
`books://{title}` 是一個**範本**。它能比對**任何**書名,所以「URI 格式正確」和「這本書存在」是兩個不同的問題,而只有你的函式能回答第二個。
回答不了的時候,引發 `ResourceNotFoundError`。SDK 會把它轉成規格指派給缺漏資源的協定錯誤:`-32602`,並把請求的 URI 放在 `data` 裡,讓用戶端知道是**哪一次**讀取失敗。
```json
{
"code": -32602,
"message": "No book titled 'Nothing' in the catalog.",
"data": {"uri": "books://Nothing"}
}
```
注意這裡沒有 `is_error=True` 那種半成品結果。資源讀取要嘛回傳內容,要嘛失敗:資源只有協定這條路。範本以及資源的其他一切都在 **[資源](resources.md)**。
## 永遠不用引發的錯誤 {#errors-you-never-raise}
錯誤的引數永遠到不了你的函式。
傳給 `get_author` 一個不是字串的 `title`,SDK 會在呼叫你**之前**就依輸入 schema 拒絕它,同樣是模型能讀懂並修正的那種 `is_error=True` 工具錯誤。**[工具](tools.md)** 用 `Field(le=50)` 限制示範了同樣的拒絕。
這表示有一整類 `raise` 陳述式不用寫:不要重新驗證自己的型別提示。
!!! info
這一頁的一切都是**用戶端**看到的東西,而寫測試用的記憶體內 `Client` 看到的完全一樣。就算是 `raise_exceptions=True` 也不會把工具錯誤變回 traceback:等到那個旗標能起作用時,你的例外早已是 `is_error=True` 的結果。對結果做斷言。**[測試](../get-started/testing.md)** 說明了這個模式。
## 重點回顧 {#recap}
* 在工具裡引發**任何例外** -> 呼叫回傳 `is_error=True`,訊息在 `content` 裡。模型讀到後可以重試。這是預設行為。
* 引發 **`MCPError`** -> 呼叫本身以 JSON-RPC 錯誤失敗。模型什麼都看不到;由主機處理。`code`、`message` 和 `data` 完整保留。
* 決定性的問題:「更聰明的模型能避開這個錯誤嗎?」能 -> 例外。不能 -> `MCPError`。
* 資源處理函式引發的 `ResourceNotFoundError` -> 協定的 `-32602`,URI 在 `data` 裡。
* 錯誤的引數會在函式執行前依 schema 被拒絕;這些不用 `raise`。
* `from mcp import MCPError`;錯誤碼常數來自 `mcp.types`。
錯誤處理完畢。這就是伺服器**公開**的全部內容。每個處理函式在執行時能讀到什麼、又能反過來對用戶端做什麼,是下一節的主題:**[在處理函式內部](../handlers/index.md)**。
最常碰到的 SDK 錯誤的確切文字、各自的意思,以及每一個的一步修正法,都在 **[疑難排解](../troubleshooting.md)**。

View file

@ -0,0 +1,22 @@
---
translation:
sections: [09defc170a0da89d]
tool: 1
---
# 伺服器 {#servers}
`MCPServer` 向已連線的用戶端公開三種基本元件,差別在於由**誰**決定使用它們:
* **[工具](tools.md)** 是由**模型**挑選並呼叫的動作。這是大多數人最先想看的頁面,而 **[結構化輸出](structured-output.md)** 是它的參考配套頁面:關於工具回傳內容的形狀,全部都在那裡。
* **[資源](resources.md)** 是由**應用程式**選擇讀取的唯讀資料。**[URI 範本](uri-templates.md)** 是它的參考配套頁面:完整的定址語法與路徑安全規則。
* **[提示詞](prompts.md)** 是由**人**依名稱叫用的訊息範本,可以從選單或斜線指令觸發。
在這三種基本元件之外,伺服器還會宣告這些:
* **[自動完成](completions.md)** 是針對提示詞與資源範本引數的伺服器端自動完成功能。
* **[圖片、音訊與圖示](media.md)** 涵蓋工具除了文字之外能回傳的一切,以及用戶端顯示在伺服器旁邊的圖示。
* **[處理錯誤](handling-errors.md)** 說明模型能從中復原的錯誤,與模型絕對不能看到的錯誤,兩者之間的差別。
這裡的每一頁都各自獨立,直接跳到需要的那一頁即可。如果還沒建過伺服器,請先從 **[第一步](../get-started/first-steps.md)** 開始。
至於註冊的函式**內部**會發生什麼事(`Context`、相依性注入、在呼叫途中向使用者要求更多輸入),是下一節 **[在處理函式內部](../handlers/index.md)** 的內容。

View file

@ -0,0 +1,117 @@
---
translation:
sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 043f526230dd243d, 6ee3e9bcfd24047a]
tool: 1
---
# 媒體 {#media}
工具能回傳的不只是文字。
SDK 內建兩個處理二進位結果的輔助工具(**`Image`** 和 **`Audio`**),以及一個 **`Icon`** 型別,讓伺服器、工具、資源和提示詞在用戶端的 UI 裡有張臉。
## 回傳圖片 {#returning-an-image}
把回傳型別註記為 `Image`,指向一個檔案,然後回傳它:
```python title="server.py" hl_lines="8 12 14"
--8<-- "docs_src/media/tutorial001.py"
```
* `Image` 只接受 `path`(要讀取的檔案)或 `data`(原始位元組)其中之一。
* 用戶端看到的 MIME 型別是從副檔名猜出來的:`logo.png` 會宣告為 `image/png`。
* 這裡跟 logo 沒有特別關係。`server.py` 旁邊的任何 PNG 都行:程式碼繪製的圖表、示意圖、照片都可以。
`Image` 是 SDK 提供的便利工具,不是協定型別。實際傳輸時,回傳值會變成一個 **`ImageContent`** 區塊(檔案的位元組經 base64 編碼,加上 MIME 型別):
```python
result.content # [ImageContent(type="image", data="iVBORw0KGgoAAAANSUhEUg...", mime_type="image/png")]
result.structured_content # None
```
有兩點值得注意:
* `data` 是 base64。你完全沒碰過位元組;SDK 讀了檔案並完成編碼。
* `structured_content` 是 `None`。`Image` 是給模型看的內容,不是給應用程式解析的資料:沒有輸出 schema。(對照 **[結構化輸出](structured-output.md)**,那裡的回傳註記**就是** schema。)
!!! info
`ImageContent` 和 `AudioContent` 位於 `mcp.types`,就在普通 `str` 結果會變成的 `TextContent` 旁邊(**[工具](tools.md)**)。工具結果是一串內容區塊的清單;`Image` 和 `Audio` 是產生這兩種二進位區塊最簡短的方式。
### 試試看 {#try-it}
把任何一張 PNG 放在 `server.py` 旁邊,命名為 `logo.png`,然後執行:
```console
uv run mcp dev server.py
```
打開 **Tools** 分頁並呼叫 `logo`。結果不是字串:它是一個 `image` 內容區塊,Inspector 會把圖片顯示出來。從磁碟上的檔案到螢幕上的像素,中間的一切都是 SDK 做的。
## 回傳音訊 {#returning-audio}
`Audio` 的形式一模一樣。`logo.png` 留在原位,再放一個 WAV 在旁邊,命名為 `chime.wav`:
```python title="server.py" hl_lines="18-21"
--8<-- "docs_src/media/tutorial002.py"
```
結果是一個 **`AudioContent`** 區塊:
```python
result.content # [AudioContent(type="audio", data="UklGR...", mime_type="audio/wav")]
result.structured_content # None
```
同樣的道理:磁碟上的檔案進去,base64 和 MIME 型別出來,沒有輸出 schema。
## 位元組或檔案 {#bytes-or-a-file}
兩個輔助工具也都接受 `data=`(原始位元組)來取代 `path=`。這是給那些本來就不是來自檔案的位元組用的模式,例如資料庫欄位、HTTP 回應,或 Pillow 剛畫好的東西:
```python title="server.py" hl_lines="14 15"
--8<-- "docs_src/media/tutorial003.py"
```
用 `path=` 時什麼都不用宣告:建立結果時才讀取檔案,MIME 型別從副檔名猜出來:
* `Image`:`.png`、`.jpg`、`.jpeg`、`.gif`、`.webp`。
* `Audio`:`.wav`、`.mp3`、`.ogg`、`.flac`、`.aac`、`.m4a`。
認不出來的副檔名會退回 `application/octet-stream`。
!!! check
用 `data=` 時沒有檔名,也就沒有東西可以猜。忘了 `format=`,SDK 就會退回預設值:圖片是 `image/png`,音訊是 `audio/wav`。這樣用 MP3 位元組建立 `Audio`,用戶端會被告知 `mime_type="audio/wav"`,然後老老實實地解碼失敗。傳 `data=` 的時候,就一起傳 `format=`。
## 圖示 {#icons}
`Icon` 是中繼資料,不是內容。它不帶圖片本身,而是用一個 URI 指向圖片;用戶端可以去抓取並顯示在伺服器名稱、工具、資源或提示詞旁邊。
```python title="server.py" hl_lines="4-5 7 10 16"
--8<-- "docs_src/media/tutorial004.py"
```
* `src` 是用戶端能解析的 URI:`https:`,或是想把圖示直接內嵌、省掉額外抓取的話,用 `data:` URI。
* `mime_type` 和 `sizes`(`"48x48"`,可縮放格式則用 `"any"`)讓用戶端在你提供多個圖示時挑出合適的那一個。
* `theme="light"` 或 `theme="dark"` 標記圖示適用於哪一種配色。
同樣的 `icons=[...]` 關鍵字在 `MCPServer(...)`、`@mcp.tool()`、`@mcp.resource()` 和 `@mcp.prompt()` 都能用。
### 用戶端在哪裡看到它們 {#where-a-client-sees-them}
圖示會跟著它所裝飾的東西一起傳送。伺服器的圖示在用戶端連線時送達,放在 `client.server_info` 上(在 2026 世代的連線上是選用的,所以先做型別收窄):
```python
assert client.server_info is not None # python-sdk servers identify themselves by default
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]
```
工具的圖示在 `tools/list` 回傳的 `Tool` 物件上,資源的在 `resources/list` 的 `Resource` 上,提示詞的在 `prompts/list` 的 `Prompt` 上。欄位一律叫做 `icons`。
## 重點回顧 {#recap}
* 從工具回傳 `Image` 或 `Audio`,用戶端就會收到一個 `ImageContent`/`AudioContent` 區塊:位元組經 base64 編碼,附上 MIME 型別。
* 可以用 `path=` 建立並讓副檔名決定 MIME 型別,或用記憶體內的 `data=` 加上明確的 `format=`。
* 媒體結果沒有 `structured_content`,也沒有輸出 schema。
* `Icon` 是個指標:一個 `src` URI,加上選用的 `mime_type`、`sizes` 和 `theme`。
* `icons=[...]` 在伺服器、工具、資源和提示詞上都能用,用戶端會在對應的物件上找到它們。
以上就是工具能放**進**結果裡的全部東西。工具**失敗**時會發生什麼事(以及誰該知道),請見 **[處理錯誤](handling-errors.md)**。

View file

@ -0,0 +1,151 @@
---
translation:
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, ffe823cb0fedd488, f33651add1b59094]
tool: 1
---
# 提示詞 {#prompts}
**提示詞**是使用者挑選的訊息範本。
工具是給模型用的。提示詞正好相反:使用者從用戶端的選單(斜線指令、按鈕)裡選一個,填好引數,算繪出來的訊息就會進入對話,就像是使用者自己打的一樣。
宣告的方式是在回傳文字的函式上加 `@mcp.prompt()`。
## 第一個提示詞 {#your-first-prompt}
```python title="server.py" hl_lines="6-9"
--8<-- "docs_src/prompts/tutorial001.py"
```
SDK 讀取的三樣東西和工具一樣:
* **名稱**是函式名稱:`review_code`。
* 用戶端顯示的**描述**是 docstring:`Review a piece of code.`
* **引數**來自參數。`code` 沒有預設值,所以是必填。
這就是用戶端從 `prompts/list` 拿回來的內容:
```json
{
"name": "review_code",
"description": "Review a piece of code.",
"arguments": [
{"name": "code", "required": true}
]
}
```
這裡沒有 JSON Schema。提示詞的引數是一串扁平的**具名字串值**:是給人填的表單,不是給模型組出來的 payload。
### 算繪 {#rendering-it}
用戶端用 `prompts/get` 算繪範本,並傳入引數。函式會執行,回傳的 `str` 變成**一則使用者訊息**:
```json
{
"description": "Review a piece of code.",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please review this code:\n\ndef add(a, b): return a + b"
}
}
],
"resultType": "complete"
}
```
提示詞的一生就這樣:依名稱列出、需要時算繪、丟進聊天裡。
!!! check
`required` 會在函式執行前就強制檢查。算繪 `review_code` 時不給 `code`,請求本身就會以 JSON-RPC 錯誤(錯誤碼 `-32603`)失敗:
```text
mcp.shared.exceptions.MCPError: Internal server error
```
這裡沒有工具那種可以交回給模型的錯誤結果,因為根本沒有模型參與:呼叫會直接引發例外。原因(`Missing required arguments: {'code'}`)會記在伺服器記錄裡。
### 試試看 {#try-it}
用 MCP Inspector 執行伺服器:
```console
uv run mcp dev server.py
```
打開 **Prompts** 分頁並選擇 `review_code`。Inspector 會畫出一個表單,裡面有一個必填的 `code` 欄位。填好、算繪,拿回來的就是上面那則使用者訊息。
## 不只一則訊息 {#more-than-one-message}
程式碼審查是一則訊息。偵錯則是一段對話,而提示詞可以替整段對話起頭。
改成回傳訊息清單,而不是 `str`:
```python title="server.py" hl_lines="2 13-20"
--8<-- "docs_src/prompts/tutorial002.py"
```
* `UserMessage` 和 `AssistantMessage` 來自 `mcp.server.mcpserver.prompts.base`。交給它們一個 `str`,它們會幫你包成 `TextContent`。角色就是類別名稱。
* `Message` 是它們共同的基底類別,用它當作回傳型別註記。
現在算繪 `debug_error` 會依序產生三則訊息:
```json
{
"description": "Start a debugging conversation.",
"messages": [
{"role": "user", "content": {"type": "text", "text": "I'm seeing this error:"}},
{"role": "user", "content": {"type": "text", "text": "TypeError: 'int' object is not iterable"}},
{
"role": "assistant",
"content": {"type": "text", "text": "I'll help debug that. What have you tried so far?"}
}
],
"resultType": "complete"
}
```
注意最後一則。預先填好一輪 `assistant` 的回合,就是引導模型**下一個**回覆的方法,不必讓使用者自己打出引導的話。
## 標題與引數描述 {#titles-and-argument-descriptions}
`review_code` 是函式名稱,不是標籤。給用戶端更適合放在按鈕上的文字,並描述每個引數,讓表單自己說明清楚:
```python title="server.py" hl_lines="10-13"
--8<-- "docs_src/prompts/tutorial003.py"
```
* `title="Code review"` 是給人看的名稱,和工具的 `title` 完全一樣。
* `Annotated[str, Field(description=...)]` 和 **[工具](tools.md)** 用來描述工具參數的寫法相同。在這裡描述會落在引數上,而不是 schema 裡。
* `language` 有預設值,所以不再是必填。
`prompts/list` 的項目現在帶齊了用戶端畫出好表單所需的一切:
```json
{
"name": "review_code",
"title": "Code review",
"description": "Review a piece of code.",
"arguments": [
{"name": "code", "description": "The code to review.", "required": true},
{"name": "language", "description": "The language the code is written in.", "required": false}
]
}
```
!!! info
如果讀過 **[工具](tools.md)**,這一頁的內容你都已經會了。同樣的裝飾器、同樣以 docstring 當描述、同樣的 `Annotated`/`Field`。唯一不同的是由誰觸發(使用者),以及結果去哪裡(進入對話)。
## 重點回顧 {#recap}
* 在函式上加 `@mcp.prompt()`,它就成為提示詞。名稱取自函式,描述取自 docstring。
* 提示詞**由使用者控制**:用戶端列出來,使用者挑一個並填入引數。
* 引數是一串扁平的具名字串(沒有 schema)。有預設值的參數就是選填。
* 回傳 `str` 會變成一則使用者訊息。回傳 `UserMessage`/`AssistantMessage` 的清單,可以替多輪對話起頭。
* `title=` 和 `Field(description=...)` 是用戶端放在 UI 上的內容。
* 缺少必填引數會讓整個請求失敗,沒有個別提示詞的錯誤結果。
伺服器端替提示詞(或資源範本)引數做自動完成,請見 **[自動完成](completions.md)**。

View file

@ -0,0 +1,138 @@
---
translation:
sections: [09df998c2a799f78, 0cf131146d16d4f9, 4e6b91e3f8025346, 8fe4eef576db17ed, 0d0d1ed43e3d0a53]
tool: 1
---
# 資源 {#resources}
**資源**是你公開給應用程式讀取的資料。
分界就在這裡。工具是**模型**決定要呼叫的東西;資源是**應用程式**決定要載入的東西(一個設定檔、一筆紀錄、一份文件),再放到模型面前當作上下文。
在一個普通的 Python 函式上加上 `@mcp.resource(uri)`,就宣告了一個資源。
## 第一個資源 {#your-first-resource}
```python title="server.py" hl_lines="6-8"
--8<-- "docs_src/resources/tutorial001.py"
```
形狀和工具一樣,只多了一樣東西:**URI**。資源靠位址定位,而不是靠名稱。用戶端要的是 `config://app`,從來不是 `get_config`。
其餘的部分,SDK 照樣從函式讀出來:
* **名稱**就是函式名稱:`get_config`。
* 用戶端看到的**描述**是 docstring。
* **內容**就是你回傳的東西。
在 `resources/list` 期間,用戶端會收到:
```json
{
"name": "get_config",
"uri": "config://app",
"description": "The active shop configuration.",
"mimeType": "text/plain"
}
```
當它讀取 `config://app` 時,函式會執行,回傳值以文字形式送回:
```python
result.contents # [TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")]
```
!!! tip
列出資源的成本很低。函式在 `resources/list` 期間**不會**執行,只有在 `resources/read` 時才會,而且只針對用戶端要求的那個 URI。就算公開了一千個資源,也只需要為有人打開的那幾個付出代價。
### 試試看 {#try-it}
用 MCP Inspector 執行伺服器:
```console
uv run mcp dev server.py
```
打開它印出的 URL,切到 **Resources** 分頁。`config://app` 會連同描述一起出現在清單裡。點一下,Inspector 就會讀取它:那兩行設定就在眼前。
## 資源範本 {#resource-templates}
一筆紀錄一個 URI 沒辦法擴展。在 URI 裡放一個**佔位符**,函式上加一個對應的參數:
```python title="server.py" hl_lines="12-13"
--8<-- "docs_src/resources/tutorial002.py"
```
URI 裡有 `{user_id}`,函式上有 `user_id: str`。整個約定就這樣。
這樣就成了**資源範本**,而且會搬家:它離開 `resources/list`,改出現在 `resources/templates/list`,以樣式而不是位址的形式呈現:
```json
{
"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` 傳入:
```python
result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")]
```
注意結果裡的 `uri`。那是用戶端要求的**具體** URI,不是範本。
!!! check
佔位符和參數必須一致。如果把函式參數改名為 `user`,URI 卻還寫著 `{user_id}`,裝飾器會在**匯入時**就拒絕,任何用戶端都還來不及靠近:
```text
ValueError: Mismatch between URI parameters {'user_id'} and function parameters {'user'}
```
不一致只可能是 bug,所以 SDK 讓帶著這種錯誤的伺服器根本啟動不了。
佔位符語法遵循 [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570):`{+path}` 用於多段的值,`{?q,lang}` 用於選用的查詢參數,還有更多。SDK 預設也會對擷取出來的值做路徑安全檢查。完整參考請見 **[URI 範本與路徑安全](uri-templates.md)**。
`get_user_profile` 也可以接受一個註記為 `Context` 的參數。SDK 會注入它,而且絕不會把它當成 URI 參數;它能提供什麼,**[Context](../handlers/context.md)** 頁面有說明。
## 回傳什麼 {#what-you-return}
不限於 `str`。替每個資源指定 `mime_type`,回傳合適的東西即可:
```python title="server.py" hl_lines="8-9 14-15 20-21"
--8<-- "docs_src/resources/tutorial003.py"
```
* `readme` 回傳 `str`,所以原樣送出。這是最常見的情況。
* `catalog_stats` 回傳 `dict`,所以 SDK 會替你序列化成 **JSON 文字**:
```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(...)` 註冊即可。
用戶端也可以**訂閱**資源,在它變更時收到通知;那是用戶端那一半的事,寫在 **[用戶端](../client/index.md)** 裡。
## 重點回顧 {#recap}
* 在函式上加 `@mcp.resource(uri)`,它就成了資源。URI 是位址,回傳值是內容,docstring 是描述。
* URI 裡有 `{placeholder}` 就成了**範本**:它列在 `resources/templates/list` 底下,同一個函式服務所有符合的 URI。
* 佔位符名稱必須等於函式的參數名稱。弄錯的話,匯入時就會知道,不用等到正式環境。
* 函式在資源被**讀取**時執行,而不是被列出時。
* `str` 變成文字,`bytes` 變成 base64 blob,其他的都變成 JSON 文字。用 `mime_type=` 來標示。
* 工具讓模型採取行動,資源讓應用程式讀取。
第三種基本元件,由人從選單裡挑選的那種,是 **[提示詞](prompts.md)**。

View file

@ -0,0 +1,242 @@
---
translation:
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, e866c192e11d1c14, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c6f7d2a148aa49f4, c851964bb3301907, d715db6f8dccc9cc, ef86634aa70498a7]
tool: 1
---
# 結構化輸出 {#structured-output}
回傳普通 `str` 的工具會把結果產生兩份:一份是 `content` 裡的文字,一份是 `structured_content` 裡的 `{"result": "..."}`。
這一頁談的就是第二個通道:它從哪裡來、可以有哪些形狀,以及 SDK 如何確保它名副其實。
簡單說:**回傳型別註記就是輸出 schema**。你早就寫好了。
## 輸出 schema {#the-output-schema}
```python title="server.py" hl_lines="9"
--8<-- "docs_src/structured_output/tutorial001.py"
```
重要的是簽章那一行:`-> int`。
因為有它,SDK 在 `tools/list` 送出的工具,除了從參數建出的輸入 schema(這部分在 **[工具](tools.md)** 說明),旁邊還帶了一個 `output_schema`:
```json
{
"properties": {
"result": {"title": "Result", "type": "integer"}
},
"required": ["result"],
"title": "get_temperatureOutput",
"type": "object"
}
```
單獨一個 `int` 不是 JSON 物件,所以 SDK 會把它**包**進 `{"result": ...}`。呼叫這個工具,兩個通道都會填上:
```python
result.content # [TextContent(text="17")]
result.structured_content # {"result": 17}
```
每種純量都會套上同樣的包裝:`str`、`int`、`float`、`bool`、`bytes`、`None`。
## 兩個通道 {#two-channels}
為什麼同一個值要送兩次?
* `content` 是給**模型**的。語言模型讀的是文字,結果裡它只看得到這個部分。
* `structured_content` 是給模型所在的**應用程式**的:那些程式碼要的是 `17`,不是一句包含「17」的話。
* `output_schema` 是兩者之間的合約,在工具被呼叫之前就已經公布。
你回傳一個 Python 值,SDK 把三者都填好。
## 回傳一個模型 {#return-a-model}
把形狀宣告成 Pydantic `BaseModel`,再回傳一個實例:
```python title="server.py" hl_lines="8-11 15"
--8<-- "docs_src/structured_output/tutorial002.py"
```
現在 `WeatherData` **就是** schema。沒有包裝,也沒有 `result` 鍵:
```json
{
"properties": {
"temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
"humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
"conditions": {"title": "Conditions", "type": "string"}
},
"required": ["temperature", "humidity", "conditions"],
"title": "WeatherData",
"type": "object"
}
```
`structured_content` 就是這個物件,一個欄位都不差:
```python
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
```
模型也沒被冷落。SDK 會把同一個物件序列化成 JSON 文字放進 `content`:
```json
{
"temperature": 16.2,
"humidity": 0.83,
"conditions": "Overcast"
}
```
注意 `temperature` 和 `humidity` 上的 `Field(description=...)` 進到了 schema 裡。用來描述**輸入**的那個 `Field`,同樣可以描述輸出。
!!! info
如果用過 FastAPI 的 `response_model`,這一套你早就認識了:把 Pydantic 模型宣告為回應,序列化和文件都幫你做好。唯一的差別是,這裡的回傳註記就是全部的宣告。
## `TypedDict` {#a-typeddict}
不是每個形狀都值得寫一個類別。`TypedDict` 會產生同樣的 schema:
```python title="server.py" hl_lines="8"
--8<-- "docs_src/structured_output/tutorial003.py"
```
`TypedDict` 在執行時就是普通的 `dict`,所以建立並回傳的就是它。schema、驗證和 `structured_content` 都跟 `BaseModel` 版本一模一樣(少了描述,因為 `TypedDict` 沒有地方放)。
## dataclass {#a-dataclass}
dataclass 也可以,任何屬性帶有型別提示的普通類別也都可以。SDK 會在背後用這些註記建出一個 Pydantic 模型。
```python title="server.py" hl_lines="8-9"
--8<-- "docs_src/structured_output/tutorial004.py"
```
三種寫法,同一個 schema。程式碼庫裡已經用哪一種,就用哪一種。
## 串列 {#lists}
`list[...]` 同樣不是 JSON 物件,所以也會套上 `{"result": ...}` 包裝,元素型別則以 `$defs` 參照的形式放在裡面:
```python title="server.py" hl_lines="15"
--8<-- "docs_src/structured_output/tutorial005.py"
```
```json
{
"$defs": {
"WeatherData": {
"properties": {
"temperature": {"title": "Temperature", "type": "number"},
"humidity": {"title": "Humidity", "type": "number"},
"conditions": {"title": "Conditions", "type": "string"}
},
"required": ["temperature", "humidity", "conditions"],
"title": "WeatherData",
"type": "object"
}
},
"properties": {
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
},
"required": ["result"],
"title": "get_forecastOutput",
"type": "object"
}
```
要兩天的預報,`structured_content` 就是 `{"result": [{...}, {...}]}`。`content` 則變成**兩個** `TextContent` 區塊,每個元素一個:串列會為模型攤平,而不是整個倒成一個字串。
`tuple[...]`、union 和 `Optional[...]` 也用同樣的方式包裝。
## 字典 {#dictionaries}
`dict[str, ...]` 是唯一本身**就是** JSON 物件的泛型,所以不會被包裝:
```python title="server.py" hl_lines="9"
--8<-- "docs_src/structured_output/tutorial006.py"
```
```json
{
"additionalProperties": {"type": "number"},
"title": "get_temperaturesDictOutput",
"type": "object"
}
```
```python
result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
```
鍵必須是 `str`。`dict[int, float]` 沒辦法成為 JSON 物件,所以會退回 `{"result": ...}` 包裝。
## 驗證 {#validation}
`output_schema` 不是寫來看的說明文件。函式回傳的任何東西,在離開伺服器之前都會**拿它來驗證**。
自己手動建值的時候感覺不到:Pydantic 早就確保 `WeatherData` 確實是 `WeatherData`。等到哪天資料來自你無法掌控的地方,就會感覺到了:
```python title="server.py" hl_lines="9 21"
--8<-- "docs_src/structured_output/tutorial007.py"
```
註記承諾的是 `WeatherData`,但上游回應不再送 `humidity` 了。
!!! check
呼叫 `get_weather`,它不會默默把一個半空的物件交給用戶端。呼叫會失敗,錯誤的頭幾行就點名了那個欄位:
```text
Error executing tool get_weather: 1 validation error for WeatherData
humidity
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
```
這段文字會以 `is_error=True` 的工具結果回傳,所以模型知道呼叫失敗了,而不是信心滿滿地讀一份根本不存在的天氣。
順帶一提,從 `-> WeatherData` 的工具回傳普通的 `dict` 沒問題,`json.loads` 產生的正是這個。驗證看的是值,不是 Python 型別。
## 選擇退出 {#opting-out}
有時候回傳註記是寫給型別檢查器看的,不是給協定用的。傳入 `structured_output=False`,工具就只有文字:
```python title="server.py" hl_lines="6"
--8<-- "docs_src/structured_output/tutorial008.py"
```
沒有 `output_schema`、沒有包裝、沒有驗證。`structured_content` 是 `None`,`content` 就是你回傳的字串。
反過來,`structured_output=True` 會把自動偵測變成硬性要求:回傳型別產生不出 schema 的工具,會在匯入時引發例外,而不是退回文字。
## 沒有型別提示的類別 {#a-class-without-type-hints}
有一種情況會在沒有要求的前提下變成非結構化:回傳一個**本體上沒有任何註記**的類別。
```python title="server.py" hl_lines="6-9"
--8<-- "docs_src/structured_output/tutorial009.py"
```
`Station` 在 `__init__` 裡設定了 `name` 和 `online`,但**類別**本身什麼都沒宣告。SDK 讀取類別註記,什麼都沒找到,於是放棄。
!!! warning
而且是**默默**放棄。`output_schema` 是 `None`,`structured_content` 是 `None`,模型讀到的文字是物件的 `repr`:
```text
"<server.Station object at 0x7f539d75b230>"
```
沒有錯誤、沒有警告,只有一個沒用的工具。把註記移到類別本體上,或者傳入 `structured_output=True`,後者會在模組匯入的那一刻把這件事變成硬性錯誤:`Function get_station: return type <class 'server.Station'> is not serializable for structured output`。
!!! tip
需要完全掌控(自己建構 `CallToolResult`,或附上應用程式看得到但模型看不到的 `_meta`)?請見 **[低階 Server](../advanced/low-level-server.md)**。
## 重點回顧 {#recap}
* **回傳型別註記**就是輸出 schema,會在 `tools/list` 裡以 `output_schema` 公布。
* 純量、串列、tuple 和 union 會包進 `{"result": ...}`。模型、`TypedDict`、dataclass、帶註記的類別和 `dict[str, ...]` 本來就是物件,維持原樣。
* 每個結果都帶有 `content`(文字,給模型)**和** `structured_content`(資料,給應用程式)。
* 回傳的東西會拿 schema 驗證。不符合就是工具錯誤,不會是一份壞掉的結果。
* `structured_output=False` 讓工具退出。沒有型別提示的類別會默默退出,要留意。
工具能回覆的一切,現在都掌握在你手上了。接下來是第二個基本元件:**[資源](resources.md)**。

View file

@ -0,0 +1,170 @@
---
translation:
sections: [e4cc390d56573409, 8566e2b68594e9ad, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363]
tool: 1
---
# 工具 {#tools}
**工具**是模型可以呼叫的函式。
在一個普通的 Python 函式上加上 `@mcp.tool()`,就宣告了一個工具。整個 API 就這樣。
## 你的第一個工具 {#your-first-tool}
```python title="server.py" hl_lines="6-8"
--8<-- "docs_src/tools/tutorial001.py"
```
看看剛才寫的東西。沒有 schema、沒有 JSON、沒有協定,就只是一個函式。SDK 從中讀取三樣東西:
* 工具的**名稱**就是函式名稱:`search_books`。
* 模型看到的**描述**就是 docstring:`Search the catalog by title or author.`
* 模型可以傳入的**引數**來自型別提示:`query: str` 和 `limit: int`。
### 輸入 schema {#the-input-schema}
SDK 從這些型別提示產生一份 JSON Schema,並在 `tools/list` 時送給用戶端:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"title": "Limit", "type": "integer"}
},
"required": ["query", "limit"],
"title": "search_booksArguments"
}
```
兩個引數都在 `required` 裡,因為都沒有預設值。等一下就會修正這點。(`title` 鍵是 Pydantic 產生的附帶產物;屬性、它們的型別和 `required` 才是契約。)
!!! tip
這裡的型別提示不是說明文件,而是**契約**。如果用戶端送來 `"limit": "ten"`,SDK 會在函式執行之前就拒絕它。
### 模型收到什麼 {#what-the-model-gets-back}
用 `{"query": "dune", "limit": 5}` 呼叫這個工具,結果有兩個部分:
```python
result.content # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")]
result.structured_content # {'result': "Found 3 books matching 'dune' (showing up to 5)."}
```
`content` 是**模型**讀取的文字。`structured_content` 是給**用戶端應用程式**的型別化資料。它之所以存在,是因為你把回傳型別宣告成 `-> str`。
先不用管 `structured_content`。從工具回傳真正的 Python 物件,該發生的事就會發生;**[結構化輸出](structured-output.md)**那一頁專門講這件事。
### 試試看 {#try-it}
用 MCP Inspector 執行伺服器:
```console
uv run mcp dev server.py
```
打開它印出的 URL,切到 **Tools** 分頁,呼叫 `search_books`。
Inspector 會呈現一個表單,裡面有一個必填的 `query` 文字欄位和一個必填的 `limit` 數字欄位。這個表單是從你的型別提示建出來的。其他每一個 MCP 用戶端也都會這麼做。
## 選填引數 {#optional-arguments}
替參數加上預設值,它就不再是必填。就這樣,就只是 Python 而已。
```python title="server.py" hl_lines="7"
--8<-- "docs_src/tools/tutorial002.py"
```
schema 跟著變:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
`limit` 離開了 `required`,多了 `"default": 10`。省略它的用戶端會得到 `10`,和 Python 的行為一模一樣。
## 用 `Field` 寫出更豐富的 schema {#richer-schemas-with-field}
型別提示已經能做很多事,但有時候會想**描述**一個引數,或是替它加上限制。
把型別包進 `Annotated`,再加上 Pydantic 的 `Field`:
```python title="server.py" hl_lines="12-14"
--8<-- "docs_src/tools/tutorial003.py"
```
三樣新東西,全都在參數上:
* `Field(description=...)`:每個引數各自的描述,模型會和 docstring 一起讀。
* `Field(ge=1, le=50)`:數值範圍。在 schema 裡會變成 `"minimum": 1, "maximum": 50`。
* `Literal["fiction", "non-fiction", "poetry"]`:列舉。模型只能從中挑一個。
!!! check
限制條件不是裝飾。用 `limit=999` 呼叫這個工具,SDK 會**在函式執行之前**就回應一個工具錯誤:
```text
Input should be less than or equal to 50
```
這個錯誤會當作工具結果回到模型手上,模型讀了之後會用合法的值重試。你只寫了一次 `le=50`,就平白得到會自我修正的 agent。
!!! info
如果用過 FastAPI 或 Pydantic,這些你早就會了。同一個 `Field`、同一個 `Annotated`、同一套驗證。這裡沒有任何 MCP 特有的東西要學。
## 以模型作為參數 {#a-model-as-a-parameter}
當工具的引數超過兩三個,就把它們整理成一個 Pydantic 模型:
```python title="server.py" hl_lines="8-11 15"
--8<-- "docs_src/tools/tutorial004.py"
```
`Book` 的 schema 會巢狀放進工具的輸入 schema(以 `$defs` 參照的形式),模型把它填成一個 JSON 物件,而你的函式收到的是一個**真正的 `Book` 實例**,已經驗證完畢,有 `.title`、`.author` 和 `.year` 屬性。
可以自由搭配:一般參數和模型參數並列、巢狀模型、模型的 list。從頭到尾都是 Pydantic。
## `async def` {#async-def}
如果工具會做 I/O(呼叫 API、讀檔案、查資料庫),就宣告成 `async def`,在裡面 `await`。SDK 會 await 它。
一般的 `def` 工具也可以:SDK 會在執行緒裡執行它,所以永遠不會阻塞伺服器。
沒有其他要設定的東西。
## 名稱、標題與 annotations {#names-titles-and-annotations}
SDK 推斷出來的所有東西,都可以在裝飾器裡覆寫:
```python title="server.py" hl_lines="7-10"
--8<-- "docs_src/tools/tutorial005.py"
```
* `title` 是給 UI 用、方便人閱讀的名稱。用戶端會顯示「Search the catalog」而不是 `search_books`。
* `annotations` 是給用戶端的行為**提示**:
* `read_only_hint=True`:這個工具不會改變任何東西。
* `open_world_hint=False`:它操作的是一組封閉的東西(這份目錄),不是開放的網路。
* 另外兩個 `destructive_hint` 和 `idempotent_hint` 描述的是會**寫入**的工具:它可能刪除東西嗎?呼叫兩次和呼叫一次的結果一樣嗎?規格只針對非唯讀的工具定義這兩個,所以放在 `search_books` 上沒有意義。
守規矩的用戶端會用它們來判斷像「執行這個之前需要先問使用者嗎?」這類事情。它們是提示,不是安全機制。永遠不要指望用戶端一定會遵守。
!!! tip
如果不想從函式名稱和 docstring 推導,`@mcp.tool()` 也接受 `name=` 和 `description=`。大多數時候用推導的就好。
## 重點回顧 {#recap}
* 在函式上加 `@mcp.tool()` 就把它變成工具。名稱來自函式,描述來自 docstring。
* 型別提示**就是**輸入 schema。預設值讓引數變成選填。
* `Annotated[..., Field(...)]` 加上描述和限制;`Literal` 加上列舉。
* 要接收結構化的「body」,就用 Pydantic 模型參數。
* 不合法的引數會替你擋下來,並附上模型讀得懂、能據以修正的錯誤。
* I/O 用 `async def`,其他一律用一般的 `def`。
**[結構化輸出](structured-output.md)**講的是你 `return` 的值接下來會發生什麼事。

View file

@ -0,0 +1,167 @@
---
translation:
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 101ef9d14bf4ec46, 4b6c4a845438abc7, f98b46bafbee4acd]
tool: 1
---
# URI 範本與路徑安全 {#uri-templates-and-path-safety}
這一頁是參考文件,涵蓋 [`@mcp.resource`](resources.md) 接受的 URI 範本語法,以及 SDK 套用在擷取值上的路徑安全策略。想先了解資源是什麼、什麼時候該用,請從 **[資源](resources.md)** 開始;這一頁假設你已經能自在地宣告資源,想要的是完整的運算子集合、安全相關的設定選項,或低階的接線方式。
範本語法是 [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570)。SDK 支援其中一個子集,挑選的依據是用來比對傳入的 `resources/read` URI,另外再加上一層安全機制,會拒絕解析後落在預定服務目錄之外的值。協定層級的細節(訊息格式、生命週期、分頁)請見 [MCP 資源規格](https://modelcontextprotocol.io/specification/latest/server/resources)。
## 完整的運算子集合 {#the-full-operator-set}
最基本的佔位符 `{user_id}` 是 **[資源](resources.md)** 介紹過的那一種。另外還有四種運算子形式;下面把它們放在同一個伺服器上,方便並排比較:
```python title="server.py" hl_lines="16-17 22-23 28-29 34-35 40-41"
--8<-- "docs_src/uri_templates/tutorial001.py"
```
每個標示出來的裝飾器都是切分 URI 的不同方式。以下各節從上到下逐一說明。
### 簡單展開:`{name}` {#simple-expansion-name}
`books://{isbn}` 是最平常的基本形式。佔位符對應到 `isbn` 參數,所以用戶端讀取 `books://978-0441172719` 時會呼叫 `get_book("978-0441172719")`。
單純的 `{name}` 遇到第一個 `/` 就停。`books://978/extra` 不會比對成功,因為 `978` 後面的斜線結束了擷取,剩下 `/extra` 沒有去處。
### 型別轉換 {#type-conversion}
擷取出來的值一開始都是字串,但可以宣告更明確的型別,SDK 會幫忙轉換。`orders://{order_id}` 對應的函式參數是 `order_id: int`,所以讀取 `orders://12345` 會呼叫 `get_order(12345)`,而不是 `get_order("12345")`。處理函式直接拿它做算術(`order_id + 1`),不必轉型。
### 多段路徑:`{+name}` {#multi-segment-paths-name}
要擷取含有斜線的值,用 `{+name}`。以 `manuals://{+path}` 為例:
* `manuals://returns.md` 得到 `path = "returns.md"`
* `manuals://printing/setup.md` 得到 `path = "printing/setup.md"`
只要值是階層式的,就用 `{+name}`:檔案系統路徑、巢狀物件的鍵、代理轉發的 URL 路徑。
### 查詢參數:`{?a,b,c}` {#query-parameters-abc}
`reviews://{isbn}{?limit,sort}` 把 `limit` 和 `sort` 放在 `?` 後面。路徑指出是**哪一本**書;查詢則調整**怎麼**讀它。
查詢參數採寬鬆比對:順序無所謂,多出來的會被忽略,省略的參數則落回函式的預設值。所以 `reviews://978-0441172719` 會用 `limit=10, sort="newest"`,而 `reviews://978-0441172719?sort=top` 只覆寫 `sort`。
### 路徑段轉成清單:`{/name*}` {#path-segments-as-a-list-name}
如果希望每個路徑段各自成為清單中的一個項目,而不是一個帶斜線的字串,用 `{/name*}`。以 `shelves://browse{/path*}` 為例,用戶端讀取 `shelves://browse/fiction/sci-fi` 會呼叫 `browse_shelf(["fiction", "sci-fi"])`。
### 範本速查 {#template-reference}
最常見的樣式:
| 樣式 | 範例輸入 | 得到 |
|--------------|-----------------------|-------------------------|
| `{name}` | `alice` | `"alice"` |
| `{name}` | `docs/intro.md` | **不相符**(停在 `/`) |
| `{+path}` | `docs/intro.md` | `"docs/intro.md"` |
| `{.ext}` | `.json` | `"json"` |
| `{/segment}` | `/v2` | `"v2"` |
| `{?key}` | `?key=value` | `"value"` |
| `{?a,b}` | `?a=1&b=2` | `"1"`, `"2"` |
| `{/path*}` | `/a/b/c` | `["a", "b", "c"]` |
### 剖析器會拒絕什麼 {#what-the-parser-rejects}
有幾種範本寫法會在一開始就被擋下來,而不是等到第一個請求才失敗。`@mcp.resource` 在裝飾器執行時就剖析範本,所以這些情況都不會進到執行中的伺服器。
`UriTemplate.parse()` 在下列情況會引發 `InvalidUriTemplate`:
* **兩個變數之間沒有任何東西。** `manuals://{+path}{ext}` 會被拒絕:比對時無法判斷 `path` 在哪裡結束、`ext` 從哪裡開始。在它們之間放一個字面字元(`manuals://{+path}/{ext}`),或改用自帶分隔符號的運算子。`manuals://{+path}{.ext}` 可以接受,因為 `{.ext}` 自己提供了 `.`。
* **超過一個多段變數。** 每個範本最多只能有一個 `{+var}`、`{#var}` 或展開變數(`{/var*}`、`{.var*}`、`{;var*}`)。兩個就先天有歧義:沒有合理的依據決定哪一個該吸收多出來的段。
* **一般的語法錯誤**:沒關上的大括號、重複使用的變數名稱,或 SDK 不支援的 RFC 6570 功能,例如 `{var:3}` 前綴修飾詞或 `{?vars*}` 查詢展開。
除此之外,當處理函式的某個參數綁定到範本尾端 `{?...}`/`{&...}` 區段裡的查詢變數,卻沒有 Python 預設值時,`@mcp.resource` 會引發 `ValueError`。這些變數是寬鬆比對的(用戶端可以省略其中任何一個),所以沒有預設值的參數只會在第一個省略它的請求上,以一個看不出原因的內部錯誤浮現。上面伺服器裡的 `reviews://{isbn}{?limit,sort}` 就是寫對的版本:`limit` 和 `sort` 都有預設值。
## 安全性 {#security}
範本參數來自用戶端。如果未經檢查就流入檔案系統或資料庫操作,像 `../../etc/passwd` 這樣的值可能會解析到預定服務目錄之外。
### SDK 預設檢查什麼 {#what-the-sdk-checks-by-default}
在處理函式執行之前,SDK 會拒絕任何符合下列條件的參數:
* 透過 `..` 元件跳出起始目錄
* 看起來像絕對路徑(`/etc/passwd`、`C:\Windows`)或 Windows 磁碟機相對路徑(`C:foo`)。磁碟機相對路徑的值和 `x:y` 這類帶命名空間的識別碼,從字串上無法區分,所以任何「單一字母加冒號」的值預設都會被拒絕;如果該參數確實會合法地收到這種值,就把它設為豁免
* 含有 null 位元組(`\x00`)
`..` 的檢查是以路徑元件為單位,不是子字串掃描。`v1.0..v2.0` 或 `HEAD~3..HEAD` 這類值會通過,因為其中的 `..` 並不是獨立的路徑段。
這些檢查套用在解碼後的值上,所以不管在 URI 裡怎麼編碼,都抓得到路徑穿越(`../etc`、`..%2Fetc`、`%2E%2E/etc`、`..%5Cetc`、`%00` 全都會被攔下)。
!!! check
從上面的伺服器讀取 `manuals://../etc/passwd`,請求會直接被拒絕:範本比對在第一次失敗時就停止,所以不會退而嘗試後面(可能更寬鬆)的範本。用戶端看到的是和完全不符合任何範本的 URI 一樣的 `-32602`「Unknown resource」錯誤,而 `read_manual` 根本不會執行。
### 檔案系統處理函式:使用 safe_join {#filesystem-handlers-use-safe_join}
內建檢查擋得住常見情況,但無從得知你的沙箱邊界。存取檔案系統時,用 `safe_join` 解析路徑,並確認它仍在基底目錄之內:
```python title="server.py" hl_lines="4 14"
--8<-- "docs_src/uri_templates/tutorial002.py"
```
`safe_join` 抓得到符號連結跳脫、`..` 序列,以及簡單字串檢查會漏掉的絕對路徑伎倆。如果解析後的路徑跳出 `DOCS_ROOT`,它會引發 `PathEscapeError`,在用戶端會以 `ResourceError` 的形式呈現。
### 預設值礙事的時候 {#when-the-defaults-get-in-the-way}
有時候這些檢查會擋掉合法的值。目錄匯入工具可能就是要接收絕對路徑,或者某個參數是像 `../sibling` 這樣的相對參照,處理函式會安全地解讀它而不碰檔案系統。把那個參數設為豁免,或放寬整個伺服器的策略:
```python title="server.py" hl_lines="9 16-19"
--8<-- "docs_src/uri_templates/tutorial003.py"
```
* 裝飾器上的 `security=ResourceSecurity(exempt_params={"source"})` 只對那一個資源的那一個參數跳過檢查。伺服器其餘部分維持預設策略。
* `MCPServer` 建構子上的 `resource_security=` 設定所有資源的預設值。這裡的 `relaxed` 把 `..` 檢查整個關掉。
可設定的檢查:
| 設定 | 預設值 | 作用 |
|-------------------------|---------|-------------------------------------|
| `reject_path_traversal` | `True` | 拒絕跳出起始目錄的 `..` 序列 |
| `reject_absolute_paths` | `True` | 拒絕 `/foo`、`C:\foo`、UNC 路徑和磁碟機相對的 `C:foo`(也會抓到 `x:y`) |
| `reject_null_bytes` | `True` | 拒絕含有 `\x00` 的值 |
| `exempt_params` | 空 | 要跳過檢查的參數名稱 |
這些檢查只是啟發式的前置過濾;存取檔案系統時,`safe_join` 仍然是真正的隔離邊界。
!!! tip
如果處理函式無法完成請求(檔案不存在、id 不認識),就引發例外。SDK 會把它轉成錯誤回應。協定錯誤和工具錯誤的差別請見 **[處理錯誤](handling-errors.md)**。
## 低階 Server 上的資源 {#resources-on-the-low-level-server}
如果是在低階 `Server` 上開發(見 **[低階 Server](../advanced/low-level-server.md)**),就直接為 `resources/list` 和 `resources/read` 這兩個協定方法註冊處理函式。沒有裝飾器;協定型別要自己回傳。
### 靜態資源 {#static-resources}
固定的 URI 就維護一份登錄表,依完全相符來分派:
```python title="server.py" hl_lines="17 21 27"
--8<-- "docs_src/uri_templates/tutorial004.py"
```
list 處理函式告訴用戶端有哪些可用;read 處理函式提供內容。先查登錄表,如果有範本(見下)就接著落到範本,其餘一律引發例外。
### 範本 {#templates}
`MCPServer` 用的範本引擎位於 `mcp.shared.uri_template`,可以獨立使用。剖析和比對完全一樣;路由和安全策略要自己接線。
```python title="server.py" hl_lines="13-16 22-25 29 33 45"
--8<-- "docs_src/uri_templates/tutorial005.py"
```
標示出來的幾行做了三件事:
* **剖析一次,每個請求比對一次。** `UriTemplate.parse()` 建立範本;`template.match(uri)` 以 `dict` 回傳擷取出的變數,URI 不符則回傳 `None`。URL 解碼在 `match()` 內部進行;解碼後的值原樣回傳,不做路徑安全驗證。出來的值都是字串:自己轉換(`int(matched["id"])`、`Path(matched["path"])`)。
* **自己套用安全檢查。** `MCPServer` 預設執行的 `..` 和絕對路徑檢查位於 `mcp.shared.path_security`。`read_manual_safely` 在碰 `MANUALS` 之前會先呼叫它們。如果某個參數不是檔案系統路徑(ISBN、搜尋查詢),就跳過那個值的檢查:策略是逐個處理函式控制,而不是透過設定物件。
* **從同一個來源列出範本。** 用戶端透過 `resources/templates/list` 探索範本。`str(template)` 會還原出原本的範本字串,所以清單和比對器共用同一個事實來源。
## 重點回顧 {#recap}
* `{name}` 比對一段;`{+name}` 保留斜線;`{?a,b}` 從查詢字串取值;`{/name*}` 把各段拆成清單。
* 兩個變數之間沒有任何東西,或出現第二個多段變數,都會在剖析時被拒絕。綁定到尾端 `{?...}`/`{&...}` 查詢變數的參數必須宣告 Python 預設值。
* 替參數加上註記(`order_id: int`),SDK 就會轉換。
* 預設的安全策略會在處理函式執行前拒絕 `..`、絕對路徑和 null 位元組;用 `security=ResourceSecurity(...)` 針對個別資源覆寫,或用 `resource_security=` 套用到整個伺服器。
* 存取檔案系統時,`safe_join` 是隔離邊界。
* 在低階 `Server` 上,用 `UriTemplate.parse()` 剖析、用 `.match()` 比對,並自己套用 `mcp.shared.path_security`。