docs: refresh translations, and translate pages in parallel (#3458)
This commit is contained in:
parent
08a3bc8eaf
commit
fd66270926
5 changed files with 4821 additions and 2277 deletions
|
|
@ -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 來連線。
|
||||
|
|
|
|||
|
|
@ -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)**。
|
||||
|
|
|
|||
|
|
@ -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 宣告。相同的金鑰,相同的名稱。
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
|
||||
sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
|
||||
tool: 1
|
||||
---
|
||||
# 服務舊版用戶端 {#serving-legacy-clients}
|
||||
|
|
@ -18,15 +18,25 @@ SDK 依每個請求的 `MCP-Protocol-Version` 標頭來路由。標明 `2026-07-
|
|||
|
||||
## 一個處理函式,兩個世代 {#one-handler-both-eras}
|
||||
|
||||
下面是一個必須問使用者問題的工具,以及兩個世代的用戶端呼叫它:
|
||||
下面是一個必須問使用者問題的工具:
|
||||
|
||||
```python title="server.py" hl_lines="24 37-38"
|
||||
```python title="server.py" hl_lines="21"
|
||||
--8<-- "docs_src/legacy_clients/tutorial001.py"
|
||||
```
|
||||
|
||||
`reserve` 需要一樣模型沒有提供的東西:要幾本。`Annotated[..., Resolve(ask_quantity)]` 就是工具宣告這件事的方式(完整說明請見 **[相依性](../handlers/dependencies.md)**)。`reserve` 裡沒有任何地方指名版本、檢查能力或做分支。
|
||||
|
||||
兩個用戶端**同時**開著,連到同一個 `mcp` 物件。`mode="legacy"` 會執行 `initialize` 交握:正是 2026 之前的用戶端會開啟的那種連線。另一個用預設值,落在 `2026-07-28`。
|
||||
透過 HTTP 提供服務,下面是兩個世代的用戶端呼叫它:
|
||||
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
```python title="client.py" hl_lines="14-15"
|
||||
--8<-- "docs_src/legacy_clients/tutorial001_client.py"
|
||||
```
|
||||
|
||||
兩個用戶端**同時**開著,連到同一個正在執行的伺服器。`mode="legacy"` 會執行 `initialize` 交握:正是 2026 之前的用戶端會開啟的那種連線。另一個用預設值,落在 `2026-07-28`。從第二個終端機執行 `python client.py`:
|
||||
|
||||
```text
|
||||
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
|
||||
|
|
@ -53,6 +63,31 @@ SDK 依每個請求的 `MCP-Protocol-Version` 標頭來路由。標明 `2026-07-
|
|||
!!! warning
|
||||
`event_store=` 看起來像解法,但不是。它是**可恢復性**(把漏掉的 SSE 事件重播給重新連回**同一個**工作階段的用戶端),不是工作階段儲存區。它永遠不會讓工作階段能從另一個處理程序存取到。
|
||||
|
||||
## 工作階段的存活時間與上限 {#session-lifetime-and-limits}
|
||||
|
||||
舊版工作階段不會永遠活著,單一處理程序也不會持有無限多個。有兩個設定控制這件事,兩者都是 `run()`、`streamable_http_app()` 和 `Server.streamable_http_app()` 上的關鍵字引數。現代(`2026-07-28`)連線和 `stateless_http=True` 都沒有工作階段,所以這兩個設定對它們都不適用。
|
||||
|
||||
| 設定 | 預設值 | 作用 | 用戶端看到什麼 | 關閉方式 |
|
||||
|---|---|---|---|---|
|
||||
| `session_idle_timeout` | `1800`(30 分鐘) | 關閉一個已經那麼久沒有任何進行中事項的工作階段。 | `404 Session not found`。必須重新 `initialize`。 | `None` |
|
||||
| `max_sessions` | `10_000` | 超過這個數量就拒絕再開新的工作階段。既有的工作階段不受影響,也不會驅逐任何一個。 | `503 Too many open sessions`,JSON-RPC 錯誤碼 `-32603`。 | `None` |
|
||||
|
||||
什麼算「進行中」:
|
||||
|
||||
* 一條開著的 `GET` 串流。SDK 的用戶端會保持一條開著,所以已連線的用戶端,它的工作階段永遠不會過期。
|
||||
* 還在回應中的請求。執行時間超過逾時的工具呼叫不會被中斷,倒數要等它結束後才開始。
|
||||
* 沒有別的了。請求與請求之間,時鐘照跑。工作階段上的任何請求都會讓它重新起算,`ping` 也算。工作階段一旦過期,沒有任何東西能讓它復活。
|
||||
|
||||
用 `DELETE` 結束工作階段的用戶端會立刻釋放它。開頭請求被拒絕的用戶端也是。
|
||||
|
||||
```python
|
||||
mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000)
|
||||
```
|
||||
|
||||
兩種事件都會出現在伺服器記錄裡。過期是 `INFO` 層級的 `Session <id> idle timeout`。拒絕開啟是 `WARNING` 層級的 `Refusing to open a new session: <n> sessions are already open`。
|
||||
|
||||
上限是以處理程序為單位。四個 worker 時,上限是 `max_sessions` 的四倍,每個 worker 各自讓自己的工作階段過期。
|
||||
|
||||
## 唯一的開關:`stateless_http` {#the-one-knob-stateless_http}
|
||||
|
||||
如果黏性是你不願付的代價,能改的東西剛好只有一樣。
|
||||
|
|
@ -73,7 +108,7 @@ SDK 依每個請求的 `MCP-Protocol-Version` 標頭來路由。標明 `2026-07-
|
|||
`json_response=True` 不是那個開關,但它在**每個**舊版工作階段上都會付出一半同樣的代價:用單一 JSON 本體回應的 `POST` 沒有串流可供請求範圍的通道使用,所以請求途中的 `ctx.elicit()` 會引發同樣的 `NoBackChannelError`,綁在該請求上的通知則被丟掉。工作階段的獨立串流不受影響:不相關的通知照樣送達。
|
||||
|
||||
!!! check
|
||||
故意做錯一次。`reserve` 正是剛剛服務了兩個用戶端的那個工具。用 `stateless_http=True` 部署它,透過 HTTP 連上同樣的兩個用戶端,從各自呼叫它。
|
||||
故意做錯一次。`reserve` 正是剛剛服務了兩個用戶端的那個工具。用 `stateless_http=True` 部署它,連上同樣的兩個用戶端,從各自呼叫它。
|
||||
|
||||
現代用戶端還是得到 `Reserved 2 of 'Dune'.`,現代路徑沒變。
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue