docs: refresh translations, and translate pages in parallel (#3458)

This commit is contained in:
Max 2026-09-07 13:35:53 +01:00 • committed by GitHub
commit fd66270926
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 4821 additions and 2277 deletions

View file

@ -1,6 +1,6 @@
---
translation:
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
tool: 1
---
# MCP Apps {#mcp-apps}
@ -18,7 +18,7 @@ SDK 以內建的 `Apps` 擴充功能(`io.modelcontextprotocol/ui`)提供這
## 有錶面的時鐘 {#a-clock-with-a-face}
```python title="server.py" hl_lines="19 22 30 32"
```python title="server.py" hl_lines="17 20 28 30"
--8<-- "docs_src/apps/tutorial001.py"
```
@ -39,11 +39,31 @@ HTML 本身會監聽主機的 `postMessage` 並顯示結果。真正的 app 請
模型讀的是 `content`;iframe 是給人看的。支援 UI 的主機照樣會把文字結果餵給模型,而純文字用戶端**只**會拿到那個。所以標準做法是一個工具,兩種答案。再看一次 `get_time`:
```python title="server.py" hl_lines="23-27"
```python title="server.py" hl_lines="21-25"
--8<-- "docs_src/apps/tutorial001.py"
```
只有當用戶端宣告了 `io.modelcontextprotocol/ui` 擴充功能,**而且**在它的 `mimeTypes` 設定裡列出 `text/html;profile=mcp-app` 時,`client_supports_apps(ctx)` 才會是 `True`。這個欄位是必填的,所以省略它的用戶端不算數。同一個檔案裡的 `main()` 宣告的正是這些:協商的用戶端那一半,於是回來的是豐富版的答案。
只有當用戶端宣告了 `io.modelcontextprotocol/ui` 擴充功能,**而且**在它的 `mimeTypes` 設定裡列出 `text/html;profile=mcp-app` 時,`client_supports_apps(ctx)` 才會是 `True`。這個欄位是必填的,所以省略它的用戶端不算數。以下是協商中用戶端的那一半:
```python title="client.py" hl_lines="8 12"
--8<-- "docs_src/apps/tutorial001_client.py"
```
透過 HTTP 提供 `server.py`,然後在第二個終端機執行用戶端:
```console
uv run mcp run server.py --transport streamable-http
```
```console
python client.py
```
```text
2026-06-26T12:00:00Z
```
回來的是豐富版的答案。把 `extensions=[APPS_SUPPORT]` 從 `Client` 呼叫裡拿掉,同一個程式就會改印出 `The time is 2026-06-26T12:00:00Z.`,而這就是純文字用戶端所能看到的全部。
!!! warning
絕對不要把 `"[Rendered UI]"` 這類佔位文字當成唯一的內容回傳。如果後備文字沒有用,這個工具對每個純文字用戶端、對模型本身就都沒有用。好好寫那句話。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff]
tool: 1
---
# 擴充功能 {#extensions}
@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
最小的有用擴充功能是一個工具加上一個設定對映表:
```python title="server.py" hl_lines="17 19-20 22-23 26"
```python title="server.py" hl_lines="16 18-19 21-22 25"
--8<-- "docs_src/extensions/tutorial003.py"
```
@ -57,17 +57,23 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
* `settings()` 是在 `capabilities.extensions["com.example/stamps"]` 宣告的值。回傳 `{}`(預設值)表示宣告這個擴充功能但不帶任何設定。
* 擴充功能永遠不會拿到伺服器。它以資料的形式宣告貢獻,由 `MCPServer` 取用。沒有 `self.server` 可以修改。
而 `main()` 就是證明,一個記憶體內用戶端直接連上 `mcp`:
透過 HTTP 提供服務,用戶端就是證明:
```python title="server.py" hl_lines="29-34"
--8<-- "docs_src/extensions/tutorial003.py"
```console
uv run mcp run server.py --transport streamable-http
```
```python title="client.py" hl_lines="7-11"
--8<-- "docs_src/extensions/tutorial003_client.py"
```
本頁每個 `server.py` 都用這個指令提供服務,每個 `client.py` 則在第二個終端機用 `python client.py` 與它並行執行。
### 提供自己的方法 {#serving-your-own-methods}
擴充功能可以註冊**新的請求方法**:屬於它自己的動詞,和規格定義的方法並列提供:
```python title="server.py" hl_lines="16-22 31 40-48"
```python title="server.py" hl_lines="14-20 24 33-41"
--8<-- "docs_src/extensions/tutorial004.py"
```
@ -83,14 +89,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
### 用戶端這一側 {#the-client-side}
同一個檔案的 `main()` 就是完整的用戶端故事,兩半都在裡面:
用戶端是獨立的程式,用戶端故事的兩半都在裡面:
```python title="server.py" hl_lines="54-58"
--8<-- "docs_src/extensions/tutorial004.py"
```python title="client.py" hl_lines="21-23 27-30"
--8<-- "docs_src/extensions/tutorial004_client.py"
```
* `Client(..., extensions=[advertise(EXTENSION_ID)])` 宣告這個擴充功能。這些宣告會變成 `ClientCapabilities.extensions`:在 2026-07-28 連線上,這個對映表隨著每個請求的 `_meta` 信封傳送,所以伺服器在**每一個**請求上都看得到它;在舊版連線上,它則搭著 `initialize` 交握傳送。伺服器程式碼不用在意是哪一種:`require_client_extension(ctx, ...)` 和 `ctx.session.check_client_capability(...)` 在兩條路徑上都會讀取正確的來源。
* 廠商方法要往下一層用 `client.session.send_request(...)`;`Client` 只會為規格動詞長出一級方法。`send_request` 接受任何 `Request` 子類別,所以廠商請求原樣傳遞即可。
* `SearchRequest` 和它攜帶的兩個模型是這個擴充功能的線路契約,所以用戶端自己宣告一份。正式發布的擴充功能會把它們放進一個套件,讓兩端都匯入。
### 攔截 `tools/call` {#intercepting-toolscall}
@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
## 使用用戶端擴充功能 {#using-a-client-extension}
**用戶端擴充功能**是從使用端看的同一份契約:掛在單一識別碼之下的一組用戶端行為。把實例傳給 `Client(extensions=[...])`,然後照常呼叫工具:
**用戶端擴充功能**是從使用端看的同一份契約:掛在單一識別碼之下的一組用戶端行為。這裡的伺服器回應 `buy` 時給的不是貨品,而是一張待兌換的收據,而且只對宣告了這個擴充功能的用戶端這樣做:
```python title="client.py" hl_lines="66-68"
```python title="server.py" hl_lines="22-25"
--8<-- "docs_src/extensions/tutorial006.py"
```
在用戶端,把實例傳給 `Client(extensions=[...])`,然後照常呼叫工具:
```python title="client.py" hl_lines="33-35"
--8<-- "docs_src/extensions/tutorial006_client.py"
```
`call_tool("buy", ...)` 回傳普通的 `CallToolResult`,和其他所有呼叫一樣。擴充功能改變的是:伺服器現在可以用 `receipt` **結果形狀**來回應 `buy`,而不是最終結果,而 `Receipts` 會在 `call_tool` 回傳之前把它完成(這裡是透過後續呼叫兌換收據)。呼叫端的程式碼完全不用動。
拿掉這個擴充功能,這一切就不存在:伺服器的關卡會拒絕沒有宣告它的用戶端(錯誤 -32021),而來自跳過關卡的伺服器的認領形狀會驗證失敗,完全符合規格對無法辨識的 `resultType` 的要求。預設關閉,線路的兩端都是。
@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
```python
from mcp.client import advertise
client = Client(mcp, extensions=[advertise("com.example/search")])
client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")])
```
## 撰寫用戶端擴充功能 {#writing-a-client-extension}
繼承 `ClientExtension`,只覆寫需要的部分。三種貢獻類型,各有預設實作:`settings()`、`claims()` 和 `notifications()`。
```python title="client.py" hl_lines="17-18 43-44 46-47"
--8<-- "docs_src/extensions/tutorial006.py"
```python title="client.py" hl_lines="16-17 25-26 28-29"
--8<-- "docs_src/extensions/tutorial006_client.py"
```
* 識別碼遵循和伺服器相同的語法,在類別定義時驗證。
@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]:
### 擴充功能動詞 {#extension-verbs}
擴充功能自己的請求方法不需要用戶端註冊。廠商請求型別繼承 `mcp.types.Request`,並透過 `client.session.send_request` 送出,如[提供自己的方法](#serving-your-own-methods)所示。多一件事:當某個參數鍵必須搭上 `Mcp-Name` 標頭時(像 tasks 這類擴充功能規格對它們的動詞有此要求),請求型別要宣告 `name_param`:
擴充功能自己的請求方法不需要用戶端註冊。廠商請求型別繼承 `mcp.types.Request`,並透過 `client.session.send_request` 送出,如[提供自己的方法](#serving-your-own-methods)所示。以一個伺服器為例,它的擴充功能提供一個和具名工作有關的動詞:
```python title="client.py" hl_lines="22-25 46-47"
```python title="server.py" hl_lines="12-13 30"
--8<-- "docs_src/extensions/tutorial007.py"
```
用戶端多一件事:當某個參數鍵必須搭上 `Mcp-Name` 標頭時(像 tasks 這類擴充功能規格對它們的動詞有此要求),請求型別要宣告 `name_param`:
```python title="client.py" hl_lines="20-23 28-29"
--8<-- "docs_src/extensions/tutorial007_client.py"
```
工作階段會在每一條送出路徑上把 `params["jobId"]` 鏡射到 `Mcp-Name`,而缺少值時會明確失敗,而不是默默省略必要的標頭。
## 擴充功能不能做的事 {#what-an-extension-cannot-do}

View file

@ -1,6 +1,6 @@
---
translation:
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e]
sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a]
tool: 1
---
# 低階 Server {#the-low-level-server}
@ -36,18 +36,22 @@ translation:
### 試試看 {#try-it}
這個沒有 Inspector 可用:`mcp dev` 和 `mcp run` 只接受 `MCPServer`。記憶體內的 `Client` 則不在乎;它接收低階 `Server` 的方式和接收 `MCPServer` 完全一樣:
`mcp dev` 和 `mcp run` 只接受 `MCPServer`,所以這個得自己提供服務。`server.py` 的最後一行用它建立一個普通的 ASGI 應用程式,再交給 uvicorn 執行:
```python title="main.py"
```console
uvicorn server:app --port 8000
```
把 Inspector 或任何用戶端指向 `http://localhost:8000/mcp`:
```python title="client.py"
import asyncio
from mcp import Client
from server import server
async def main() -> None:
async with Client(server) as client:
async with Client("http://localhost:8000/mcp") as client:
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
print(result.content)
@ -64,6 +68,8 @@ asyncio.run(main())
* `result.structured_content` 是 `None`。高階伺服器會幫你把 `-> str` 包成 `{"result": ...}`;在這裡,你沒建的東西,沒有人會替你建。
* `list_tools` 回傳的是**你**打出來的 schema,一字不差。高階版本每個屬性上都有 `"title": "Query"`,根部還有一個 `"title": "search_booksArguments"`:那是 Pydantic 的產物。在這一層,線路上有的東西,都是你放上去的。
在測試中可以跳過 uvicorn 和連接埠:`Client(server)` 在處理程序內接收低階 `Server` 的方式和接收 `MCPServer` 完全一樣,**[測試](../get-started/testing.md)** 講的就是這個模式。
## 沒有人替你檢查 {#nothing-is-checked-for-you}
`MCPServer` 會在函式執行之前就拒絕錯誤的引數,依照它產生的 schema 驗證這次呼叫(**[工具](../servers/tools.md)**)。
@ -214,4 +220,4 @@ use Server.middleware to observe or wrap initialization
* `add_request_handler(method, params_type, handler)` 可以服務任何方法。`initialize` 被保留。
* `Server` 公告的能力,由你註冊了哪些處理函式推導而來。
`Client(server)` 對兩種伺服器一視同仁,因為它們**就是**同一個協定,這正是重點所在。再往下一層根本不是類別:是 **[中介軟體](middleware.md)**。
用戶端對兩種伺服器一視同仁,因為它們**就是**同一個協定,這正是重點所在。再往下一層根本不是類別:是 **[中介軟體](middleware.md)**。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9]
tool: 1
---
# 分頁 {#pagination}
@ -29,9 +29,13 @@ translation:
### 試試看 {#try-it}
`Client(server)` 在記憶體內連線到低階 `Server` 的方式,和連到 `MCPServer` 完全一樣。
`mcp run` 只接受 `MCPServer`,所以這個得自己提供服務。`server.py` 的最後一行從 `Server` 建出一個普通的 ASGI 應用程式,交給 uvicorn 執行:
不帶引數呼叫 `list_resources()`。會拿到十個資源,`book-1` 到 `book-10`,而 `next_cursor` 是字串 `"10"`。
```console
uvicorn server:app --port 8000
```
把任何用戶端(**[用戶端](../client/index.md)**,或 Inspector)指向 `http://localhost:8000/mcp`,不帶引數呼叫 `list_resources()`。會拿到十個資源,`book-1` 到 `book-10`,而 `next_cursor` 是字串 `"10"`。
用 `list_resources(cursor="10")` 把它交回去,第一個資源就是 `book-11`,新的 `next_cursor` 是 `"20"`。
@ -41,7 +45,7 @@ translation:
`Client` 上的每個 `list_*` 方法(`list_tools`、`list_resources`、`list_resource_templates`、`list_prompts`)都接受 `cursor=` 關鍵字引數。把分頁清單抓完只要一個 `while True`:
```python title="client.py" hl_lines="26-32"
```python title="client.py" hl_lines="9-15"
--8<-- "docs_src/pagination/tutorial002.py"
```
@ -49,7 +53,7 @@ translation:
* 先 extend,**再**看 `next_cursor`:最後一頁也有資源。
* `next_cursor is None` 就是出口。其他任何值都原封不動直接放回 `cursor=`。
執行它的 `main()`,會印出 `100 resources`:十頁、每頁十個,由一個從頭到尾不知道有十頁的迴圈接起來。
讓 uvicorn 繼續提供 `server.py`,在第二個終端機執行 `python client.py`。會印出 `100 resources`:十頁、每頁十個,由一個從頭到尾不知道有十頁的迴圈接起來。
這和 **[用戶端](../client/index.md)** 為每個 `list_*` 動詞示範的迴圈是同一個,而且對不分頁的伺服器也沒有任何代價:第一個回應的 `next_cursor` 就是 `None`,迴圈只執行一次。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800]
tool: 1
---
# 快取提示 {#caching-hints}
@ -27,7 +27,7 @@ translation:
在低階的 `Server` 上,處理函式自己手動組出結果,而 `ttl_ms` / `cache_scope` 只是結果模型上的欄位。明確設定這些欄位的處理函式,永遠勝過建構子的對應表,而且是逐欄位比較:
```python title="server.py" hl_lines="10 16"
```python title="server.py" hl_lines="11 17"
--8<-- "docs_src/caching/tutorial002.py"
```
@ -41,10 +41,24 @@ translation:
在 2026-07-28 的工作階段(session)上,`Client` 會替你遵守這些提示:它內建一個回應快取,預設開啟。帶著 `ttlMs` 抵達的結果會被存起來,在 TTL 內完全相同的呼叫會直接由快取提供,不需要往返。**沒有**帶提示的結果不會被快取:沒有提示的結果會套用 `CacheConfig.default_ttl_ms`,它預設為 `0`(立刻過期),所以什麼都沒宣告的伺服器,看到的流量和以往一模一樣,一次呼叫就一次請求。
```python title="client.py" hl_lines="33 35 38"
要親眼看看這個過程,就用 uvicorn 提供前一節的 `server.py`(它的最後一行會建立 ASGI 應用程式)。處理函式每次真正執行時都會印出一行:
```console
uvicorn server:app --port 8000
```
```python title="client.py" hl_lines="20 23 28"
--8<-- "docs_src/caching/tutorial003.py"
```
在第二個終端機執行 `python client.py`。它會印出第一個結果帶著的提示,處理函式的 `ttlMs` 和對應表的 `cacheScope` 並排:
```text
1000 public
```
伺服器的終端機交代了剩下的部分:在 uvicorn 的請求記錄之間,`tools/list served` 出現了三次。
四次呼叫,三次抓取。第二次呼叫找到新鮮的項目,根本沒送到伺服器;把(注入的)時鐘撥過 TTL 之後,第三次又重新抓取;第四次則指定了 `cache_mode="refresh"`。這個關鍵字引數存在於五個會快取的動詞上(`list_tools`、`list_prompts`、`list_resources`、`list_resource_templates`、`read_resource`):
* `"use"`(預設)如果有新鮮的項目就直接提供,沒有的話就抓取並存起來。
@ -53,7 +67,7 @@ translation:
有一條規則凌駕於 `"use"` 之上:**帶有 `meta` 的呼叫一定會送到伺服器。**設定了 `meta` 的請求(進度 token、追蹤欄位)期待的是一個實際送上線路的請求,所以在 `cache_mode="use"` 下會被當成 `"refresh"` 處理:跳過快取讀取,而抓取回來的結果仍然會取代快取中的項目。`"bypass"` 和明確指定的 `"refresh"` 行為照舊。
要完全關掉快取,就用 `Client(server, cache=None)` 建構:每次呼叫又都變回一次往返,而 `cache_mode` 雖然仍可接受,但不會有任何作用。
要完全關掉快取,就在建構 `Client` 時傳入 `cache=None`:每次呼叫又都變回一次往返,而 `cache_mode` 雖然仍可接受,但不會有任何作用。
範圍也會自動遵守:`"private"` 項目綁定在快取的**分區(partition)**上(見下文),而 `"public"` 項目則可以選擇更廣的共用。此外,對通知點名的那些項目來說,**通知勝過 TTL**:`list_changed` 通知會逐出對應的快取清單,`resources/updated` 則會逐出恰好存在該 URI 下的快取讀取結果,不管它們有多新鮮。在 2026-07-28 連線上,這些通知是透過你用 `client.listen(...)` 開啟的 `subscriptions/listen` 串流送達的,而且逐出會在你的監看程式看到事件之前完成;詳情請見 **[訂閱](subscriptions.md)**。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3]
tool: 1
---
# 用戶端回呼 {#client-callbacks}
@ -58,7 +58,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')
你送出一個 `tools/call`,伺服器回送一個 `elicitation/create`,由你的函式回答,全都發生在同一次工具呼叫之內。
!!! info
`Client(...)` 呼叫上的 `mode="legacy"` 是真的有作用。預設情況下 `Client(...)` 會協商出現代的協定路徑,而這條路徑沒有讓伺服器向用戶端發請求的反向通道(back-channel):`ctx.elicit` 在你的回呼有機會執行之前就失敗了。決定這件事的不是傳輸方式,而是協商出來的協定,記憶體內和透過 URL 連線都一樣。只要用戶端必須回應這類請求,就固定用 `mode="legacy"`;這一頁背後的每個測試都是這樣做的。完整說明請見 **[協定版本](../protocol-versions.md)**。
`Client(...)` 呼叫上的 `mode="legacy"` 是真的有作用。預設情況下 `Client(...)` 會協商出現代的協定路徑,而這條路徑沒有讓伺服器向用戶端發請求的反向通道(back-channel):`ctx.elicit` 在你的回呼有機會執行之前就失敗了。決定這件事的不是傳輸方式,而是協商出來的協定。只要用戶端必須回應這類請求,就固定用 `mode="legacy"`;這一頁背後的每個測試都是這樣做的。完整說明請見 **[協定版本](../protocol-versions.md)**。
在 2026-07-28 的工作階段(session)上,回呼並沒有失效,只是餵給它的方式不同:當工具回傳帶有 `ElicitRequest` 的 `InputRequiredResult` 時,`Client` 會把那個項目分派給同一個 `elicitation_callback`,並替你重試這次呼叫。這個流程就是 **[多輪往返(multi-round-trip)請求](../handlers/multi-round-trip.md)**。
@ -139,4 +139,4 @@ result.structured_content # {'result': ['elicitation']}
* `sampling_callback` 和 `list_roots_callback` 的運作方式相同,但服務的是已棄用的功能;現代的伺服器改用多輪往返請求。
* `logging_callback` 和 `message_handler` 接收通知。它們不宣告任何東西。
`Client(...)` 的第一個引數是一個傳輸物件。**[用戶端傳輸方式](transports.md)** 涵蓋了每一種。
`Client(...)` 的第一個引數決定傳輸方式。**[用戶端傳輸方式](transports.md)** 涵蓋了每一種。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7]
tool: 1
---
# 用戶端 {#the-client}
@ -11,13 +11,23 @@ Python 程式要和 MCP 伺服器對話,靠的就是 **`Client`**。
## 你的第一個用戶端 {#your-first-client}
```python title="client.py" hl_lines="14-18"
用戶端需要有伺服器可以對話。這一頁的每段程式碼連的都是這間 Bookshop。把它存成 `server.py`,讓它透過 HTTP 持續執行:
```python title="server.py"
--8<-- "docs_src/client/tutorial001.py"
```
最上面的伺服器只是讓你有東西可以連而已。用戶端就是標示出來的那五行。
```console
uv run mcp run server.py --transport streamable-http
```
* `Client(mcp)` 拿到的是**伺服器物件本身**。這就是記憶體內傳輸:沒有子處理程序、沒有連接埠、沒有 HTTP。這一頁的每個範例,以及你寫的每個測試,都是這樣連線的。
這樣伺服器就在 `http://localhost:8000/mcp` 提供服務。用戶端是另一個獨立的程式。把它存成 `client.py`,在第二個終端機執行 `python client.py`:
```python title="client.py" hl_lines="7-11"
--8<-- "docs_src/client/tutorial001_client.py"
```
* `Client("http://localhost:8000/mcp")` 拿到的是一個 **URL**,所以它透過 Streamable HTTP 連到你剛啟動的伺服器。
* `async with` 就是**生命週期**。進入時連線並協商;離開時斷線。沒有 `connect()` / `close()` 這種成對的方法,而且區塊結束後 `Client` 不能再重複使用。
* 在區塊內,連線的各項資訊已經以普通屬性的形式準備好了。
@ -25,10 +35,10 @@ Python 程式要和 MCP 伺服器對話,靠的就是 **`Client`**。
`Client` 接受一個位置引數,並依它的型別決定傳輸方式:
* `MCPServer`(或低階的 `Server`)實例:在**同一個處理程序內**連線。
* URL 字串(`Client("http://localhost:8000/mcp")`):Streamable HTTP,也就是正式環境的路徑。
* `StdioServerParameters`:要當作**子處理程序**啟動的命令,透過它的 stdin 和 stdout 溝通。
* URL 字串(`Client("http://localhost:8000/mcp")`):Streamable HTTP,也就是部署時使用的傳輸方式。
* `StdioServerParameters`:要當作本機**子處理程序**啟動的命令,透過它的 stdin 和 stdout 溝通。
* 一個**傳輸**:任何可以 `async with ... as (read, write)` 的東西,例如用 `streamable_http_client(url, http_client=...)` 包住你自己的 HTTP 用戶端。
* `MCPServer`(或低階的 `Server`)實例:在**同一個處理程序內**連線,沒有子處理程序,也沒有連接埠。這一種是給測試用的,**[測試](../get-started/testing.md)** 就是以它為基礎。
這一頁其餘的內容在這四種情況下完全相同。標頭、子處理程序、逾時,以及 `Transport` 協定另外有專屬的頁面:**[用戶端傳輸方式](transports.md)**。
@ -48,11 +58,11 @@ Python 程式要和 MCP 伺服器對話,靠的就是 **`Client`**。
## 列出工具 {#listing-tools}
```python title="client.py" hl_lines="15-20"
```python title="client.py" hl_lines="8-13"
--8<-- "docs_src/client/tutorial002.py"
```
`list_tools()` 回傳一個 `ListToolsResult`;工具在 `.tools` 裡。每一個都是 MCP 主機(host)會交給模型的完整定義:
`list_tools()` 回傳一個 `ListToolsResult`;工具在 `.tools` 裡。每一個都是 MCP 主機(host)會交給模型的完整定義。第一個是這樣:
```python
tool.name # 'search_books'
@ -76,6 +86,8 @@ tool.description # 'Search the catalog by title or author.'
這份 schema 就是 UI 算繪引數表單所需的一切,也是模型產生合法引數所需的一切。
第二個工具 `lookup_book` 註冊時沒有給 `title=`,所以它的 `tool.title` 是 `None`。
!!! tip
`title` 是選填的,所以把工具顯示給人看的 UI 得自己挑:有的話就用 `title`,沒有就用 `name`。`from mcp.shared.metadata_utils import get_display_name` 做的正是這件事,適用於工具、資源、資源範本和提示詞。
@ -83,7 +95,7 @@ tool.description # 'Search the catalog by title or author.'
`call_tool(name, arguments)` 會執行工具,並回傳一個 `CallToolResult`。
```python title="client.py" hl_lines="27-34"
```python title="client.py" hl_lines="9-16"
--8<-- "docs_src/client/tutorial003.py"
```
@ -131,7 +143,7 @@ result.is_error # False
資源的動作是成組的:兩種列出的方式,一種讀取的方式。
```python title="client.py" hl_lines="22-31"
```python title="client.py" hl_lines="9-18"
--8<-- "docs_src/client/tutorial004.py"
```
@ -145,7 +157,7 @@ result.is_error # False
## 提示詞 {#prompts}
```python title="client.py" hl_lines="15-20"
```python title="client.py" hl_lines="8-13"
--8<-- "docs_src/client/tutorial005.py"
```
@ -170,7 +182,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from
有自動完成處理函式的伺服器,可以在使用者輸入時自動完成提示詞和資源範本的引數。
```python title="client.py" hl_lines="27-31"
```python title="client.py" hl_lines="9-13"
--8<-- "docs_src/client/tutorial006.py"
```
@ -183,21 +195,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from
每個 `list_*` 方法都接受 `cursor=` 關鍵字引數,每個結果都帶有 `next_cursor`。當 `next_cursor` 是 `None`,表示全部拿到了。
```python title="client.py" hl_lines="22-30"
```python title="client.py" hl_lines="7-15"
--8<-- "docs_src/client/tutorial007.py"
```
這個迴圈對任何伺服器都正確。`MCPServer` 會一頁回傳全部,所以 `next_cursor` 是 `None`,迴圈只跑一次,這也是為什麼大部分程式碼從來不寫它。真正會分頁的伺服器,以及游標遵守的規則,請見 **[分頁](../advanced/pagination.md)**。
`list_all_tools` 對任何伺服器都正確。`MCPServer` 會一頁回傳全部,所以 `next_cursor` 是 `None`,迴圈只跑一次,這也是為什麼大部分程式碼從來不寫它。真正會分頁的伺服器,以及游標遵守的規則,請見 **[分頁](../advanced/pagination.md)**。
## 在測試中 {#in-tests}
不需要處理程序、不需要連接埠的 `Client(mcp)`,本身就已經是伺服器的測試工具了。
這一頁的每個 `client.py` 都是透過 HTTP 連到 `server.py`。在測試裡可以跳過網路,直接把伺服器物件本身交給 `Client`:`from server import mcp`,然後 `Client(mcp)`。沒有處理程序、沒有連接埠,而上面的每個方法用起來都一樣。
有一個建構子旗標是專為此設計的:`Client(mcp, raise_exceptions=True)`。它只對記憶體內連線有作用,而 **[測試](../get-started/testing.md)** 頁面會解釋它,並圍繞它建立整套模式。
有一個建構子旗標是專為此設計的:`Client(mcp, raise_exceptions=True)`。它只對同一處理程序內的連線有作用,而 **[測試](../get-started/testing.md)** 頁面會解釋它,並圍繞它建立整套模式。
## 重點回顧 {#recap}
* `Client(x)` 對伺服器物件以記憶體內方式連線,對 URL 字串透過 Streamable HTTP 連線,其他情況則透過傳輸連線。
* `Client(x)` 對 URL 字串透過 Streamable HTTP 連線,對 `StdioServerParameters` 啟動子處理程序,對傳輸則直接進入,在測試中則接受伺服器物件本身。
* `async with` 就是整個生命週期。在裡面,`server_capabilities` 和 `protocol_version` 已經填好;伺服器有提供時,`server_info` 和 `instructions` 也是。
* `list_tools()` 給你每個工具的 `name`、`title`、`description` 和 `input_schema`。
* `call_tool()` 回傳給模型的 `content`、給程式碼的 `structured_content`,以及 `is_error`。會引發例外的工具是一個結果,不是例外。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b]
tool: 1
---
# OAuth 用戶端 {#oauth-clients}
@ -76,18 +76,20 @@ translation:
`Client` 第一次送出請求時,伺服器回應 `401`。provider 接手:
1. **探索。** 讀取 `WWW-Authenticate` 標頭,從 `/.well-known/oauth-protected-resource` 抓取伺服器的 Protected Resource Metadata,得知是哪個授權伺服器在保護這個資源,再去抓取**那個**伺服器的中繼資料。
1. **探索。** 讀取 `WWW-Authenticate` 標頭,從 `/.well-known/oauth-protected-resource` 抓取伺服器的 Protected Resource Metadata,得知是哪個授權伺服器在保護這個資源,再去抓取**那個**伺服器的中繼資料。(較舊、沒有發布資源中繼資料的伺服器,則改向它自己的 origin 索取授權伺服器中繼資料。)無論哪種情況,中繼資料列出的 `issuer` 都必須就是它所屬的那個伺服器;不是的話一律拒絕。
2. **註冊。** 儲存庫裡什麼都沒有?它會用你的 `OAuthClientMetadata` 動態註冊,並把結果存起來。
3. **授權。** 產生 PKCE 配對和一個 `state`,組出授權 URL,await 你的 `redirect_handler`,接著 await 你的 `callback_handler` 取得授權碼。
4. **交換。** 拿授權碼換得 `OAuthToken`,存起來,然後帶著 `Authorization: Bearer ...` 重送你原本的請求。
之後它就很安靜。權杖從儲存庫拿出來用,過期的存取權杖用重新整理權杖更新,只有這些都行不通時才會重跑整個流程。
有一條傳輸規則適用於所有這些請求:和它們所在的那個 MCP 請求一樣,重新導向只有在留在同一個 origin 且保持相同方法時才會跟隨(例如補上結尾斜線的 307/308),其他任何重新導向都視為那個 URL 沒有回應。
這些你一行都沒寫。還剩兩個關鍵字引數(`client_metadata_url` 和 `validate_resource_url`),這個檔案兩個都用不到。值得認識的是 `client_metadata_url`,下面有它專屬的一節。
### 試試看 {#try-it}
這份文件裡的大多數範例都能用記憶體內的 `Client(server)` 檢驗。這個不行:整個流程的重點就是一個 HTTP `401`,而記憶體內的用戶端和它的伺服器之間根本沒有 HTTP。
測試裡用的記憶體內 `Client(server)` 在這裡幫不上忙:整個流程的重點就是一個 HTTP `401`,而記憶體內的用戶端和它的伺服器之間根本沒有 HTTP。
儲存庫裡附有實際運作的版本。`examples/servers/simple-auth/` 會執行一個獨立的授權伺服器和一個受保護的 MCP 伺服器;`examples/clients/simple-auth-client/` 則是這一頁的用戶端長成的一個小型 CLI。它的 README 有那兩個指令:啟動伺服器、對著它們執行用戶端,就能看著上面四個步驟依序發生。
@ -105,13 +107,14 @@ URL 必須是 HTTPS 且路徑不能是根路徑;否則在建構時就會引發
`ClientCredentialsOAuthProvider` 是同一個 `httpx2.Auth`,只是少了人:
```python title="client.py" hl_lines="4 27-33"
```python title="client.py" hl_lines="4 27-34"
--8<-- "docs_src/oauth_clients/tutorial002.py"
```
改變的地方:
* 沒有 `OAuthClientMetadata`,沒有處理函式。傳入 `client_id` 和 `client_secret`;provider 會圍繞它們建出一筆最精簡的 `client_credentials` 註冊,並完全跳過動態註冊。
* `issuer` 指明發出這組憑證的授權伺服器;用它的 `/.well-known/oauth-authorization-server` 文件回傳的 `issuer` 值。探索仍照上面那樣執行,但權杖請求永遠只會用**那個** issuer 的中繼資料來組裝;如果 MCP 伺服器指向別的地方,流程會停下並引發 `OAuthFlowError`。省略它已棄用,3.0 起會變成必填(見 **[已棄用的功能](../deprecated.md#deprecated-sdk-helpers)**);在那之前,provider 會發出警告,並使用探索找到的那個授權伺服器。
* `scope` 是以空格分隔的字串,也就是 OAuth 的線路格式。
* 下游的一切完全相同:同樣的 `TokenStorage`、同樣的 `httpx2.AsyncClient(auth=...)`、同樣的 `streamable_http_client`。
@ -121,7 +124,7 @@ URL 必須是 HTTPS 且路徑不能是根路徑;否則在建構時就會引發
`client_secret` 要從環境變數或祕密管理工具讀取,絕對不要放進版本控制。
!!! info
`mcp.client.auth.extensions.client_credentials` 裡還有一個 provider:**`PrivateKeyJWTOAuthProvider`**,給用 JWT 而非共用 secret 來驗證的用戶端使用(`private_key_jwt`,也就是金鑰對與工作負載身分那一類)。它遵循同樣的模式:建構一個,放到 `auth=` 上。同一個模組還附了 `SignedJWTParameters` 和 `static_assertion_provider`,兩個用來建出其 assertion 的輔助工具。
`mcp.client.auth.extensions.client_credentials` 裡還有一個 provider:**`PrivateKeyJWTOAuthProvider`**,給用 JWT 而非共用 secret 來驗證的用戶端使用(`private_key_jwt`,也就是金鑰對與工作負載身分那一類)。它遵循同樣的模式:建構一個(它接受同樣選用的 `issuer`),放到 `auth=` 上。同一個模組還附了 `SignedJWTParameters` 和 `static_assertion_provider`,兩個用來建出其 assertion 的輔助工具。
還有一種無人介入的情境:用戶端屬於某個企業,由企業的身分提供者(而非使用者)決定它可以連到哪些 MCP 伺服器。那是另一種授權類型,有自己的信任模型,也有自己的頁面:**[身分斷言](identity-assertion.md)**。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056]
tool: 1
---
# 工作階段群組 {#session-groups}
@ -76,7 +76,7 @@ translation:
## 重點回顧 {#recap}
* `ClientSessionGroup` 握有多條伺服器連線,並把它們的工具、資源和提示詞各自合併成一個 `dict`。
* 每台伺服器呼叫一次 `connect_to_server(params)`。它接受傳輸參數,絕不是 `Client` 接受的伺服器物件或 URL。
* 每台伺服器呼叫一次 `connect_to_server(params)`。它接受傳輸參數,絕不是 `Client` 接受的 URL 或 `Transport`。
* `group.call_tool(name, arguments)` 會替你轉送到擁有該工具的伺服器。
* 名稱在整個群組內必須唯一;兩台都有 `search` 工具的伺服器無法原樣共存。
* `component_name_hook=` 會改寫每個登記的名稱。dict 的鍵會變,線路上的名稱不變。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c]
sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302]
tool: 1
---
# 用戶端傳輸方式 {#client-transports}
@ -11,30 +11,15 @@ translation:
每種傳輸方式的**伺服器**端(`mcp.run()` 做的事,以及你部署的東西)請見 **[執行伺服器](../run/index.md)**。
## 記憶體內 {#in-memory}
直接傳入伺服器物件本身:
```python title="client.py" hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
沒有子處理程序,沒有連接埠,線路上也沒有任何位元組。用戶端和伺服器是同一個處理程序裡的兩個物件,但呼叫仍然會經過真正的協定層:`search_books` 被列出、驗證、呼叫的方式,和透過 HTTP 時完全一樣。
所以它同時是兩樣東西:
* **測試工具。** 這份說明文件裡的每個範例都是這樣跑過的,而 **[測試](../get-started/testing.md)** 那一頁整個模式就是圍繞它建立的。
* **嵌入用的 API。** 自己建立伺服器的應用程式,不需要繞一圈網路就能呼叫它的工具。
## Streamable HTTP {#streamable-http}
傳入一個 URL 字串,得到的就是 **Streamable HTTP**,也就是部署時使用的那種傳輸方式:
傳入一個 URL 字串,得到的就是 **Streamable HTTP**,也就是部署時使用、也該優先選用的傳輸方式:
```python title="client.py" hl_lines="5"
--8<-- "docs_src/client_transports/tutorial002.py"
```
這就是完整的正式環境用戶端。`Client` 會替你把 URL 包進 `streamable_http_client(...)`,底下是一個依 MCP 需求設定好的 `httpx2.AsyncClient`:`follow_redirects=True`、connect/write/pool 的逾時為 30 秒,read 逾時則是 300 秒,因為伺服器可能會讓回應串流一直開著。
這就是完整的正式環境用戶端。`Client` 會替你把 URL 包進 `streamable_http_client(...)`,底下是一個依 MCP 需求設定好的 `httpx2.AsyncClient`:connect/write/pool 的逾時為 30 秒,read 逾時則是 300 秒,因為伺服器可能會讓回應串流一直開著。
!!! check
建立好的 `Client` **還沒有**連線。建立只是選定傳輸方式;真正開啟它的是 `async with`。在進入之前就去拿連線,SDK 會直接告訴你:
@ -49,7 +34,7 @@ translation:
一旦需要 `Authorization` 標頭、cookie、proxy、mTLS,或不同的逾時,就自己建立 `httpx2.AsyncClient`,再交給 `streamable_http_client`:
```python title="client.py" hl_lines="8-14"
```python title="client.py" hl_lines="8-13"
--8<-- "docs_src/client_transports/tutorial003.py"
```
@ -70,7 +55,25 @@ translation:
所有跟 HTTP 有關的設定,現在都放在你傳入的那一個 `httpx2.AsyncClient` 上。
!!! info
`httpx2` 保留了熟悉的 `httpx` API,所以只要會用 `httpx`,就已經知道這裡的驗證、proxy、事件掛鉤、重試和連線數限制該怎麼做。SDK 沒有在上面加任何東西,也沒有拿掉任何東西。OAuth 也是從這裡接上的:`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。整個流程請見 **[OAuth 用戶端](oauth-clients.md)**。
`httpx2` 保留了熟悉的 `httpx` API,所以只要會用 `httpx`,就已經知道這裡的驗證、proxy、事件掛鉤、重試和連線數限制該怎麼做。SDK 沒有在上面加任何東西,也沒有拿掉任何東西,[重新導向的處理](#redirects)除外。OAuth 也是從這裡接上的:`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。整個流程請見 **[OAuth 用戶端](oauth-clients.md)**。
### 重新導向 {#redirects}
傳輸只會連到你給它的那個 URL,而且只限那個來源(origin)。
* 留在同一個 scheme、主機和連接埠上的 `307`/`308` 重新導向會跟隨,同一台主機上的 `http://` → `https://` 也會。這涵蓋了常見的 `/mcp` → `/mcp/` 結尾斜線重新導向。
* 導向其他任何地方的重新導向則**不會**跟隨。呼叫會失敗,並出現:
```text
MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server
```
如果那個 URL 就是你要的伺服器,把它寫進設定裡。如果不是,那就是伺服器或它前面的 proxy 設定有誤。
不管傳入哪個 `httpx2.AsyncClient` 都一樣:MCP 請求不會參考它的 `follow_redirects` 設定,不論設成哪個方向。SDK 的 OAuth provider 對自己發出的請求也套用同一條規則。
!!! tip
`Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` 表示伺服器前面有一個它不知道的、負責終結 TLS 的 proxy,所以它發出的是 `http://` 重新導向。這要在伺服器端修正(**[部署與擴展](../run/deploy.md#behind-a-tls-terminating-proxy)**),或者改用訊息裡建議的那個確切的 `https://…/` URL。
## stdio {#stdio}
@ -91,6 +94,18 @@ translation:
需要 API 金鑰的伺服器在那裡是找不到的。請用 `env=` 明確傳入;這些變數會疊加在允許清單之上。上面的 `BOOKSHOP_API_KEY` 做的就是這件事。
## 記憶體內 {#in-memory}
在測試裡沒有東西要部署,也沒有東西要啟動。直接傳入伺服器物件本身:
```python hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
沒有子處理程序,沒有連接埠,線路上也沒有任何位元組。用戶端和伺服器是同一個處理程序裡的兩個物件,但呼叫仍然會經過真正的協定層:`search_books` 被列出、驗證、呼叫的方式,和透過 HTTP 時完全一樣。**[測試](../get-started/testing.md)** 那一頁整個模式就是圍繞它建立的。
同樣的寫法也可以當成嵌入用的 API:自己建立伺服器的應用程式,不需要繞一圈網路就能呼叫它的工具。
## SSE {#sse}
`mcp.client.sse` 裡的 `sse_client(url)` 是被 Streamable HTTP 取代的那個 HTTP 傳輸。要和還在講它的伺服器溝通,用同樣的方式包起來即可:`Client(sse_client("http://localhost:8000/sse"))`,但不要在它上面蓋任何新東西。
@ -99,15 +114,16 @@ translation:
對 `Client` 來說,上面這些全都是同一種東西。
**傳輸**是任何會產出一對 `(read, write)` 訊息串流的非同步 context manager:正式地說,就是 `mcp.client` 裡的 `Transport` 協定。`Client` 依型別解析它的引數:伺服器物件就在處理程序內連線,`str` 會變成 `streamable_http_client(url)`,`StdioServerParameters` 會變成 `stdio_client(params)`,其他任何東西則直接當成傳輸進入。最後這條規則就是為什麼 `stdio_client(...)`、`streamable_http_client(...)` 和 `sse_client(...)` 都能放進同一個位置,也是為什麼你可以自己寫一個。
**傳輸**是任何會產出一對 `(read, write)` 訊息串流的非同步 context manager:正式地說,就是 `mcp.client` 裡的 `Transport` 協定。`Client` 依型別解析它的引數:`str` 會變成 `streamable_http_client(url)`,`StdioServerParameters` 會變成 `stdio_client(params)`,伺服器物件就在處理程序內連線,其他任何東西則直接當成傳輸進入。最後這條規則就是為什麼 `stdio_client(...)`、`streamable_http_client(...)` 和 `sse_client(...)` 都能放進同一個位置,也是為什麼你可以自己寫一個。
## 重點回顧 {#recap}
* `Client(mcp)`(伺服器物件)在記憶體內連線。用在測試和嵌入。
* `Client("http://.../mcp")`(URL)透過 Streamable HTTP 連線,也就是正式環境用的傳輸方式。
* 標頭、驗證、proxy 和逾時都放在你傳給 `streamable_http_client(url, http_client=...)` 的 `httpx2.AsyncClient` 上。沒有 `headers=` 這個關鍵字引數。
* 重新導向只在 URL 自己的來源內跟隨(結尾斜線的 `307`/`308`),外加同一台主機上的 `http`→`https`。其他的一律失敗並出現 `Redirect to … not followed`;把最終的 URL 寫進設定即可。
* stdio 是 `Client(StdioServerParameters(...))`。只有要把子處理程序的 stderr 導到別處時,才需要自己用 `stdio_client(...)` 包起來。
* 子處理程序拿到的是允許清單上的環境,不是你的環境;`env=` 會往上加。
* `Client(mcp)`(伺服器物件)在記憶體內連線。用在測試裡,或是把伺服器嵌入建立它的那個應用程式。
* 傳輸就是任何可以 `async with x as (read, write)` 的東西。只要不是伺服器物件、URL 或 `StdioServerParameters`,`Client` 就會直接交給那個協定處理。
* 建立 `Client` 是選定傳輸方式。`async with` 才是開啟它。

View file

@ -1,11 +1,11 @@
---
translation:
sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931]
sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6]
tool: 1
---
# 已棄用的功能 {#deprecated-features}
2026-07-28 規格讓五樣東西退場。SDK 仍然實作了其中每一項,而每一項現在都帶有**棄用警告**。另外有一個 SDK 輔助函式是因為自身的原因棄用,列在[最後](#deprecated-sdk-helpers)。
2026-07-28 規格讓五樣東西退場。SDK 仍然實作了其中每一項,而每一項現在都帶有**棄用警告**。另外有幾項 SDK 層級的棄用是出於自身的原因,列在[最後](#deprecated-sdk-helpers)。
下表列出每一項已棄用的功能、它為什麼要退場,以及應該改用的替代做法。
@ -125,11 +125,13 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
## 已棄用的 SDK 輔助函式 {#deprecated-sdk-helpers}
這些不是規格變更,只是有了更好替代做法的 SDK 內部實作。它們用同樣的 `MCPDeprecationWarning` 發出警告,並會在 3.0 移除。
這些不是規格變更,只是有了更好替代做法的 SDK 用法。它們用同樣的 `MCPDeprecationWarning` 發出警告,舊的寫法會在 3.0 移除。
| 已棄用項目 | 替代做法 |
|---|---|
| `FuncMetadata.call_fn_with_arg_validation()` | 先 `FuncMetadata.validate_arguments()`,再 `FuncMetadata.call_fn()`。只有直接操作 `FuncMetadata` 的程式碼(例如自訂的 `Tool` 子類別)才會呼叫過它。 |
| 沒有設定 `validate_token_resource=` 的 `AuthSettings(resource_server_url=...)` | 把它設好:`True` 會讓伺服器拒絕驗證器沒有回報為核發給 `resource_server_url` 的 bearer 權杖;`False` 表示你的驗證器會自己檢查權杖的 audience(請見 **[授權](run/authorization.md#a-token-verifier)**)。不設定時的行為等同 `False`;3.0 起,只要設了 `resource_server_url`,預設就是 `True`。 |
| 沒有傳入 `issuer=` 的 `ClientCredentialsOAuthProvider(...)` 或 `PrivateKeyJWTOAuthProvider(...)` | 傳入 `issuer=`,指明核發這組憑證的授權伺服器(請見 **[撰寫 OAuth 用戶端](client/oauth-clients.md#machine-to-machine)**)。少了它,就變成由 MCP 伺服器決定哪個授權伺服器會收到這組憑證;3.0 會把這個關鍵字引數改為必填。 |
## 重點回顧 {#recap}
@ -138,7 +140,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
* 棄用只是勸告性質:線路沒有變更,一切在 2026 之前的工作階段上都能繼續運作,而且你會看到明顯的 `MCPDeprecationWarning`(它是 `UserWarning`,所以預設就會顯示)。
* 取樣和根目錄還額外需要一條反向通道,而 2026-07-28 的工作階段沒有。在現代連線上,它們會先警告,再引發例外。
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` 會讓整個類別靜音;pytest 裡的 `"error::mcp.MCPDeprecationWarning"` 則把它變成測試失敗。
* 有一個 SDK 輔助函式 `FuncMetadata.call_fn_with_arg_validation()` 另外單獨棄用,預計在 3.0 移除。
* [SDK 層級的棄用](#deprecated-sdk-helpers)遵循同樣的規則:現在發出警告,3.0 移除舊的寫法。
* 新程式碼不應該建立在這些東西之上。
這份說明文件的其他每一頁教的都是目前的 API。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245]
tool: 1
---
# 第一步 {#first-steps}
@ -17,7 +17,7 @@ translation:
* **用戶端**位於主機內部,負責講 MCP。主機每連上一個伺服器,就執行一個用戶端。
* **伺服器**是你用這個 SDK 打造的東西。它把東西公開給用戶端,從不直接和模型溝通。
伺服器由你來寫,主機是別人的產品。SDK 也提供一個 `Client`,用來測試你的伺服器,這一頁稍後就會出現。
伺服器由你來寫,主機是別人的產品。SDK 也提供一個 `Client`,主機要透過 URL 連上伺服器、或把它當成子處理程序啟動,用的就是同一個類別。它在這一頁稍後就會出現,也是你測試伺服器的方式。
## 三種基本元件 {#the-three-primitives}
@ -79,22 +79,20 @@ Inspector 透過 **stdio** 執行你的伺服器,這是 MCP 伺服器能使用
用戶端連線時,伺服器會宣告自己的**能力**:它會回應哪幾類請求。用戶端據此決定究竟該要求什麼。這份宣告不是你寫的,`MCPServer` 替你宣告好了。
自己看看吧。SDK 的 `Client` 直接接受伺服器物件,並在**記憶體內**與它連線(沒有子處理程序,沒有連接埠):
自己看看吧。在一個終端機裡讓 `server.py` 透過 HTTP 持續執行:
```python
import asyncio
```console
uv run mcp run server.py --transport streamable-http
```
from mcp import Client
再從另一個終端機把用戶端指向它:
from server import mcp
```python title="client.py" hl_lines="7-8"
--8<-- "docs_src/first_steps/tutorial001_client.py"
```
async def main() -> None:
async with Client(mcp) as client:
print(client.server_capabilities.model_dump(exclude_none=True))
asyncio.run(main())
```console
python client.py
```
```text
@ -114,7 +112,7 @@ asyncio.run(main())
注意少了什麼。`completions`(資源範本和提示詞的引數自動完成)需要你寫一個處理函式,這個伺服器沒有,所以這項能力不存在,守規矩的用戶端也不會去問。所有選用的東西都照這條規則:註冊了,能力就出現;**[自動完成](../servers/completions.md)** 會證明這一點。
!!! info
`Client(mcp)` 就是這份文件裡每個範例測試時用的同一個記憶體內用戶端,你也會用它來測試自己的。它有專屬的一整頁:**[測試](testing.md)**。
那個 `client.py` 是一個完整的 MCP 用戶端,它的專屬頁面是 **[用戶端](../client/index.md)**。測試時可以跳過終端機和連接埠,把伺服器物件本身直接交給 `Client`,也就是 `Client(mcp)`。那也有專屬的一整頁:**[測試](testing.md)**。
## 你沒寫的東西 {#what-you-did-not-write}
@ -123,7 +121,7 @@ asyncio.run(main())
* JSON Schema。`a: int, b: int` **就是** `add` 的 schema。
* 請求處理函式。`tools/list`、`resources/read`、`prompts/get`:全都替你處理好了。
* 能力宣告。`MCPServer` 替你做了。
* 任何一行協定。版本協商、JSON-RPC 訊框、能力交換:全都發生在 `mcp dev` 和 `Client(mcp)` 裡面,你完全沒看到。
* 任何一行協定。版本協商、JSON-RPC 訊框、能力交換:全都發生在 `mcp dev` 和 `client.py` 裡面,你完全沒看到。
這個比例正是 SDK 的意義所在。
@ -134,6 +132,6 @@ asyncio.run(main())
* 每種基本元件一個裝飾器:`@mcp.tool()`、`@mcp.resource(uri)`、`@mcp.prompt()`。名稱、描述和 schema 都來自函式。
* 帶 `{param}` 的 URI 會產生資源**範本**,和具體資源分開列出。
* 伺服器的**能力**會替你宣告好,而用戶端只會要求伺服器宣告過的東西。
* `Client(mcp)` 在記憶體內連上伺服器物件:從第一天起就是你的測試工具。
* `Client("http://localhost:8000/mcp")` 會和執行中的伺服器對話。改成把伺服器物件交給它,也就是 `Client(mcp)`,從第一天起它就是你的測試工具。
接下來是 **[連接真正的主機](real-host.md)**:把這個伺服器真的放進 Claude Desktop 或 IDE 裡。然後是 **[測試](testing.md)**:一頁、一個記憶體內用戶端,從此不用猜它到底能不能動。再之後,每種基本元件各有自己的一頁,從模型主導的那個開始:**[工具](../servers/tools.md)**。

View file

@ -1,13 +1,13 @@
---
translation:
sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef]
sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f]
tool: 1
---
# 測試 {#testing}
Python SDK 附帶一個 `Client` 類別,內建**記憶體內傳輸**:把伺服器物件傳給它,它就會直接連上去。
SDK 的 `Client` 類別,也就是連到 URL 或啟動子處理程序的同一個類別,也能在**記憶體內**連線:把伺服器物件傳給它,它就會直接和伺服器對話。
沒有子處理程序,沒有連接埠,根本沒有傳輸層。概念和 FastAPI 的 `TestClient` 一樣。
沒有子處理程序,沒有連接埠,線路上什麼都沒有。概念和 FastAPI 的 `TestClient` 一樣。
## 基本用法 {#basic-usage}
@ -86,7 +86,7 @@ async def test_call_add_tool(client: Client):
測試裡就開著它。在正式環境的程式碼裡它沒有任何意義。
## 預設為處理程序內連線 {#in-process-by-default}
## 預設為世代中立 {#era-neutral-by-default}
!!! note
`Client(mcp)` 以處理程序內的方式連線,而且預設是**世代中立**的:它會探測伺服器,選出合適的協定路徑。如果測試要驗證舊版特有的語意(取樣(sampling)或徵詢(elicitation)的推送、`message_handler`),就固定用 `mode="legacy"`,並且在那裡拿掉 `raise_exceptions=True`:舊版連線本來就不會淨化,而這個旗標會讓失敗在伺服器任務裡重新引發,而不是在你的測試裡。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b]
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b]
tool: 1
---
# 多輪往返請求 {#multi-round-trip-requests}
@ -162,7 +162,7 @@ TTL、主體綁定和請求綁定都**不是** codec 的工作:不論哪個 co
## 2026-07-28 的結果型別 {#a-2026-07-28-result}
`InputRequiredResult` 只存在於協定版本 **2026-07-28**。記憶體內的 `Client(server)` 會替你協商;走線路時,`mode="auto"` 會探知它。連線之後,`client.protocol_version` 會告訴你拿到的是什麼。
`InputRequiredResult` 只存在於協定版本 **2026-07-28**。`Client` 預設的 `mode="auto"` 在任何連線上都會探知它。連線之後,`client.protocol_version` 會告訴你拿到的是什麼。
!!! warning
2026 之前的工作階段沒有地方放 `InputRequiredResult`。在 `mode="legacy"` 的連線上從處理函式回傳一個,runner 無法把它序列化成協商好的版本;用戶端會拿回 `-32603`「Handler returned an invalid result」錯誤。同時服務兩個世代的伺服器,必須先檢查 `ctx.protocol_version` 再動用它。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007]
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007]
tool: 1
---
# 進度 {#progress}
@ -29,19 +29,17 @@ translation:
用戶端是**逐次呼叫**選擇加入的,做法是把 `progress_callback=` 傳給 `call_tool`:
```python title="client.py" hl_lines="7 16"
```python title="client.py" hl_lines="5 14"
import anyio
from mcp import Client
from server import mcp
async def show(progress: float, total: float | None, message: str | None) -> None:
print(f"{message} ({progress}/{total})")
async def main() -> None:
async with Client(mcp) as client:
async with Client("http://localhost:8000/mcp") as client:
result = await client.call_tool(
"import_catalog",
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
@ -56,23 +54,27 @@ anyio.run(main)
回呼是一個 `async` 函式,接收的正是伺服器回報的內容:`progress`、`total`、`message`。
!!! info
`Client(mcp)` 在記憶體內直接連上伺服器物件,和 **[測試](../get-started/testing.md)** 那一頁用的是同一個用戶端。不管 `Client` 用哪種傳輸方式,`progress_callback` 都是同一個參數;接下來看到的**時序**則是記憶體內連線的。它會就地執行回呼,所以每一筆回報都會在 `call_tool` 回傳之前送達。換成真正的傳輸方式,通知會和結果競速,一個慢的回呼在 `call_tool` 回傳之後可能還在執行。
不管交給 `Client` 的是什麼,`progress_callback` 都是同一個參數:像這裡的 URL、一個 `StdioServerParameters`,或測試裡的伺服器物件。不過走真正的傳輸方式時要留意時序。每個通知都是在回應之外單獨送達的,所以一個慢的回呼在 `call_tool` 回傳之後可能還在執行。只有處理程序內的測試連線會就地執行回呼,並保證每一筆回報都先送達。
### 試試看 {#try-it}
把 `client.py` 放在 `server.py` 旁邊,然後執行:
用 HTTP 提供 `server.py`,然後在第二個終端機執行用戶端:
```console
uv run mcp run server.py --transport streamable-http
```
```console
python client.py
```
```text
Imported https://example.com/a.json (1/2)
Imported https://example.com/b.json (2/2)
Imported https://example.com/a.json (1.0/2.0)
Imported https://example.com/b.json (2.0/2.0)
{'result': 'Imported 2 records.'}
```
伺服器上的每一個 `await ctx.report_progress(...)` 都變成用戶端上對 `show` 的一次呼叫,依序發生,而且兩行都在 `call_tool` 回傳**之前**印出。進度不會打包進結果裡;它在工具還在執行時就持續串流過來。
伺服器上的每一個 `await ctx.report_progress(...)` 都變成用戶端上對 `show` 的一次呼叫,依序發生。進度不會打包進結果裡;它在工具還在執行時就持續串流過來。
!!! warning
`progress_callback` 屬於那一次**呼叫**,不屬於 `Client`。沒有對應的建構子引數,因為不同的呼叫想要不同的回呼:這一次驅動下載進度條,下一次是一行記錄。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870]
sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870]
tool: 1
---
# 協定版本 {#protocol-versions}
@ -11,9 +11,17 @@ MCP 有兩個世代。
你幾乎不需要在意這件事,因為 `Client` 會替你協商。這一頁談的是控制這件事的那一個建構子引數 `mode=`,以及需要改動它的三種情況。
這一頁的每段程式碼都是一個 `client.py`,連線到 **[用戶端](client/index.md)** 那頁的 Bookshop `server.py`。先在一個終端機裡啟動那個伺服器:
```console
uv run mcp run server.py --transport streamable-http
```
接著在第二個終端機裡用 `python client.py` 執行每一段程式碼。
## `mode="auto"` {#modeauto}
```python title="client.py" hl_lines="14-15"
```python title="client.py" hl_lines="7-8"
--8<-- "docs_src/protocol_versions/tutorial001.py"
```
@ -31,11 +39,11 @@ MCP 有兩個世代。
整個功能就這樣。一個 `Client`,任何世代的伺服器,程式碼裡不用分支。
!!! info
`MCPServer` 在每一種傳輸方式上都會回答 `server/discover`(記憶體內、stdio、Streamable HTTP),所以對你自己的伺服器,`auto` 永遠會落在 `2026-07-28`。退回機制只會在面對真正的 2026 之前的伺服器時觸發,而那正是你希望它觸發的時候。
`MCPServer` 在每一種傳輸方式上都會回答 `server/discover`(Streamable HTTP、stdio,以及測試時用的處理程序內連線),所以對你自己的伺服器,`auto` 永遠會落在 `2026-07-28`。退回機制只會在面對真正的 2026 之前的伺服器時觸發,而那正是你希望它觸發的時候。
## `mode="legacy"` {#modelegacy}
```python title="client.py" hl_lines="14"
```python title="client.py" hl_lines="7"
--8<-- "docs_src/protocol_versions/tutorial002.py"
```
@ -59,7 +67,7 @@ MCP 有兩個世代。
`mode` 也接受新世代的協定版本字串。目前這個集合剛好就是 `["2026-07-28"]`。
```python title="client.py" hl_lines="14"
```python title="client.py" hl_lines="7"
--8<-- "docs_src/protocol_versions/tutorial003.py"
```
@ -90,7 +98,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0
所以把它留下來。`auto` 連線之後,`client.session.discover_result` 保存著伺服器送來的那份 `DiscoverResult`:它的 `supported_versions`、`capabilities`、`instructions`,以及伺服器蓋進結果 `_meta` 裡的身分。下次把它作為 `prior_discover=` 交回去:
```python title="client.py" hl_lines="15 17"
```python title="client.py" hl_lines="8 10"
--8<-- "docs_src/protocol_versions/tutorial004.py"
```

View file

@ -1,6 +1,6 @@
---
translation:
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3]
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9]
tool: 1
---
# 加到現有的應用程式中 {#add-to-an-existing-app}
@ -86,7 +86,7 @@ Starlette 的 `Host` 路由用法相同:把 `Mount("/", ...)` 換成 `Host("mc
--8<-- "docs_src/asgi/tutorial004.py"
```
現在用戶端連到 `/notes`,而不是 `/notes/mcp`。
現在用戶端連到 `/notes/`,而不是 `/notes/mcp`。
## 給瀏覽器用戶端的 CORS {#cors-for-browser-clients}
@ -127,4 +127,4 @@ Starlette 的 `Host` 路由用法相同:把 `Mount("/", ...)` 換成 `Host("mc
* 瀏覽器用戶端需要 CORS:`allow_headers` 給 `Mcp-*` 請求標頭用,`expose_headers=["Mcp-Session-Id"]` 給回應用。
* `@mcp.custom_route()` 在 `/mcp` 旁邊加上普通、不經驗證的 HTTP 端點。
一旦伺服器能透過真正的 URL 連到,**[用戶端](../client/index.md)** 就會用那個 URL 而不是伺服器物件來連線。
一旦伺服器能透過真正的 URL 連到,**[用戶端](../client/index.md)** 就會用那個 URL 來連線。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31]
sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6]
tool: 1
---
# 授權 {#authorization}
@ -23,12 +23,12 @@ translation:
有效的權杖長什麼樣子,SDK 沒有任何預設立場。由你來告訴它,方法是實作 **`TokenVerifier`**:
```python title="server.py" hl_lines="12-14 19-24"
```python title="server.py" hl_lines="14-16 21-27"
--8<-- "docs_src/authorization/tutorial001.py"
```
* `TokenVerifier` 是只有一個非同步方法的 protocol。`verify_token` 會拿到 `Authorization` 標頭裡的原始權杖,有效就回傳 **`AccessToken`**,無效就回傳 `None`。沒有別的需要實作。
* 這個範例是在一張表裡查權杖。真實的實作會驗證 JWT 簽章,或呼叫授權伺服器的權杖內省(token introspection)端點。那段程式碼是你的,SDK 只負責呼叫它。
* 這個範例是在一張表裡查權杖;每一筆都記錄了權杖是為哪個資源發出的。真實的實作會驗證 JWT 簽章,或呼叫授權伺服器的權杖內省(token introspection)端點,並在 `AccessToken.resource` 裡回報權杖是發給誰的(它的 `aud`)。那段程式碼是你的,SDK 只負責呼叫它。
* `token_verifier=` 和 `auth=` 永遠成對出現。只傳其中一個,`MCPServer(...)` 在服務任何請求之前就會引發 `ValueError`。
`AuthSettings` 是資源伺服器對外的門面:
@ -36,6 +36,10 @@ translation:
* `issuer_url`:發出權杖的授權伺服器。
* `resource_server_url`:這個 MCP 端點的公開 URL。它指明權杖是給**哪一個**資源用的,也是探索文件所在的位置。
* `required_scopes`:每個權杖都必須帶有全部這些 scope。
* `validate_token_resource`:拒絕任何 `AccessToken.resource` 不等於 `resource_server_url` 的權杖。設定了 `resource_server_url` 卻沒設定它,會發出警告(`MCPDeprecationWarning`)並視同 `False`;3.0 起,資源伺服器的預設值會改為 `True`。
* 如果你的授權伺服器會把權杖綁定到用戶端請求的 `resource`(MCP 用戶端一定會送),就把它打開。讓 `resource_server_url` 與用戶端實際連線的 URL 完全一致。
* 如果你的授權伺服器使用自己的 audience 識別碼(Auth0 的 API identifier、Entra 的應用程式 ID),就保持關閉,改在驗證器裡檢查 `aud`,遇到不是給這個伺服器的權杖就回傳 `None`。
* 如果 `aud` 是一個清單,把等於 `resource_server_url` 的那一項放進 `resource`。
!!! tip
SDK 儲存庫裡的 `examples/servers/simple-auth/` 有一個 `IntrospectionTokenVerifier`,會呼叫真實授權伺服器的 [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) 端點。大多數正式環境的驗證器都是這個樣子。
@ -85,7 +89,7 @@ translation:
在任何處理函式內,**`get_access_token()`** 就是驗證器為目前請求回傳的那個 `AccessToken`:
```python title="server.py" hl_lines="4 32-35"
```python title="server.py" hl_lines="4 35-38"
--8<-- "docs_src/authorization/tutorial002.py"
```
@ -117,6 +121,6 @@ SDK 給你的是資源伺服器這一半:驗證、公告、拒絕。它不提
* `token_verifier=` 和 `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` 永遠成對出現。
* SDK 會在 `/.well-known/oauth-protected-resource/...` 發布 [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata,並以 401 回應未驗證的請求,其 `WWW-Authenticate` 標頭會指向這份文件。整個探索機制就這樣。
* 在任何處理函式裡,`get_access_token()` 就是誰在呼叫。
* 授權是 HTTP 層的事。`stdio` 和記憶體內用戶端永遠看不到它。
* 授權是 HTTP 層的事。`stdio` 和記憶體內測試用戶端永遠看不到它。
用戶端那一半(探索你的授權伺服器並替你取得權杖)請見 **[OAuth 用戶端](../client/oauth-clients.md)**。至於不問使用者、而是直接**斷言**身分的用戶端,請見 **[身分斷言](../client/identity-assertion.md)**。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a]
sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d]
tool: 1
---
# 部署與擴展 {#deploy-scale}
@ -42,6 +42,22 @@ translation:
在用戶端找不到這幾個字。`421` 是純文字的 HTTP 回應,不是 JSON-RPC 錯誤,所以 MCP 用戶端只會引發一個籠統的傳輸錯誤;它不喜歡的主機名稱只會出現在**伺服器**的記錄裡,就一則警告。剛部署好卻拒絕所有連線的伺服器,在證明是別的原因之前,就是 Host 允許清單的問題。**[疑難排解](../troubleshooting.md)** 也從這裡開始。
## 在終止 TLS 的代理後面 {#behind-a-tls-terminating-proxy}
如果 TLS 在代理(ingress、負載平衡器、Caddy、nginx)就結束,uvicorn 在它後面提供純 HTTP,就告訴 uvicorn 信任代理的 `X-Forwarded-*` 標頭:
```console
uvicorn server:app --proxy-headers --forwarded-allow-ips='<proxy address>'
```
不這麼做的話,應用程式會以為自己是透過 `http://` 提供服務,它發出的任何重新導向(常見的是 `/mcp` → `/mcp/`)都會指向 `http://…`。Python 用戶端拒絕從 HTTPS 端點跟到純 HTTP,而且會明說:
```text
MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP.
```
用戶端的權宜之計是設定伺服器實際提供服務的精確 URL(`https://mcp.example.com/mcp/`,含結尾斜線),讓重新導向根本不發生。正解是上面那個旗標。`FORWARDED_ALLOW_IPS` 是環境變數的寫法;`*` 會信任每一跳,只有在除了代理之外沒有東西碰得到 uvicorn 時才正確。
## Worker,以及誰需要黏性 {#workers-and-who-has-to-be-sticky}
主機名稱能回應之後,就在後面放不只一個 worker。SDK 沒有這方面的設定;擴展 Starlette 應用程式的方式跟擴展任何 ASGI 應用程式一樣,把物件交給懂得 fork 的東西:
@ -154,6 +170,7 @@ python -c "import secrets; print(secrets.token_hex(32))"
## 重點回顧 {#recap}
* 預設情況下,這個應用程式只回應送往 localhost 的請求。`transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` 是上線的關卡:在傳入它之前,真正主機名稱後面的每個請求都是 `421`,原因只在伺服器記錄裡。
* 在終止 TLS 的代理後面,用 `--proxy-headers --forwarded-allow-ips=...` 執行 uvicorn,否則它的重新導向會指向 `http://`,用戶端會拒絕跟隨。
* 在 2026-07-28 上沒有工作階段,負載平衡器也沒有東西可黏。`stateless_http=True` 是只給舊版用的開關,因為現代請求在那個旗標被讀到之前就已經分流並回應了。
* 預設的 `requestState` 金鑰是 `os.urandom(32)`,每個處理程序各自鑄造。送到不同 worker 的多輪往返重試會以 `-32602`「Invalid or expired requestState」失敗。
* 解法是 `RequestStateSecurity(keys=[...])` **加上**每個執行個體相同的伺服器名稱。名稱是權杖預設的 audience 宣告。相同的金鑰,相同的名稱。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
tool: 1
---
# 執行伺服器 {#running-your-server}
@ -71,6 +71,8 @@ Inspector 做的事和真正的主機一模一樣:把 `server.py` 當成子處
* `json_response=True`:每個 POST 都用單一 JSON 本體回應,而不是 SSE 串流。那個本體只裝得下回應本身,別的都沒有,所以在請求中途回頭呼叫用戶端的工具(`ctx.elicit()`、取樣(sampling))在這一段會引發 `NoBackChannelError`,而綁在進行中呼叫上的通知(`ctx.report_progress()` 的進度、每次呼叫的記錄訊息)會被丟棄;獨立的 `GET` 串流仍會承載不相關的那些。
* `stateless_http=True`:每個請求一個全新的傳輸,不追蹤工作階段(session)。
* `max_request_body_size`:可接受的最大請求本體,以位元組計。預設為 4 MiB;更大的請求在解析或建立工作階段之前就會收到 HTTP 413。只有在合法的 MCP 訊息超過這個大小時才調高它。
* `session_idle_timeout`:舊版工作階段在沒有任何進行中的請求時可以閒置的秒數,超過後伺服器就把它關掉。預設為 1800。`None` 會停用它。請見 [工作階段存留時間與限制](legacy-clients.md#session-lifetime-and-limits)。
* `max_sessions`:一個處理程序同時能持有多少個舊版工作階段。預設為 10 000。`None` 會移除這個限制。在同一節有說明。
* `event_store`、`retry_interval`、`transport_security`:可續傳性與 DNS 重新綁定防護。這些可以先放著,等到部署到 localhost 以外的地方再說;`transport_security` 在 **[部署與擴展](deploy.md)** 有說明。
!!! warning

Some files were not shown because too many files have changed in this diff Show more