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
6 changed files with 4821 additions and 2277 deletions

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` 才是開啟它。