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

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:
有效的 token 长什么样,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` 是一个只有一个异步方法的协议。`verify_token` 接收 `Authorization` 头里的原始 token,有效时返回一个 **`AccessToken`**,无效时返回 `None`。除此之外没有别的要实现。
* 这个例子是在一张表里查找 token。真实的实现会验证 JWT 签名,或者调用授权服务器的 token 自省端点。那部分代码是你的,SDK 只负责调用它。
* 这个例子是在一张表里查找 token,表中每一项都记录了它是为哪个资源签发的。真实的实现会验证 JWT 签名,或者调用授权服务器的 token 自省端点,并在 `AccessToken.resource` 里报告 token 是签发给谁的(它的 `aud`)。那部分代码是你的,SDK 只负责调用它。
* `token_verifier=` 和 `auth=` 永远成对出现。只传其中一个,`MCPServer(...)` 会在处理任何请求之前就抛出 `ValueError`。
`AuthSettings` 是你的资源服务器对外的门面:
@ -36,6 +36,10 @@ translation:
* `issuer_url`:签发你的 token 的授权服务器。
* `resource_server_url`:这个 MCP 端点的公开 URL。它指明 token 是针对**哪一个**资源的,发现文档也位于这里。
* `required_scopes`:每个 token 都必须携带其中全部 scope。
* `validate_token_resource`:拒绝任何 `AccessToken.resource` 不等于 `resource_server_url` 的 token。设置了 `resource_server_url` 却不设置它,会发出警告(`MCPDeprecationWarning`),行为等同于 `False`;3.0 起资源服务器的默认值会变成 `True`。
* 如果你的授权服务器把 token 绑定到客户端请求的 `resource`(MCP 客户端总是会发送它),就打开它。让 `resource_server_url` 与客户端连接的 URL 完全一致。
* 如果你的授权服务器使用自己的受众标识符(Auth0 的 API 标识符、Entra 的应用 ID),就保持关闭,改为在验证器里检查 `aud`,对不属于本服务器的 token 返回 `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` 和内存内测试客户端永远看不到它。
客户端那一半(发现你的授权服务器并替你获取 token)见 **[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`:用单个 JSON 正文回应每个 POST,而不是 SSE 流。这个正文只容得下响应本身,别的什么都放不下,所以在请求中途回调客户端的工具(`ctx.elicit()`、采样(sampling))会在这一段抛出 `NoBackChannelError`;与进行中的调用绑定的通知(`ctx.report_progress()` 的进度、每次调用的日志消息)会被丢弃;独立的 `GET` 流仍然承载与之无关的通知。
* `stateless_http=True`:每个请求一个全新的传输,不跟踪会话。
* `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 以外的地方再说;**[部署与扩展](deploy.md)** 介绍了 `transport_security`。
!!! warning

View file

@ -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-28` 的
## 一个处理函数,两个时代 {#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-28` 的
!!! 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-28` 的
`json_response=True` 不是那个开关,但它在**每一个**旧版会话上都要付一半同样的代价:用一个 JSON 正文回答的 `POST` 没有供请求范围通道使用的流,所以请求中途的 `ctx.elicit()` 会抛出同样的 `NoBackChannelError`,与该请求绑定的通知会被丢弃。会话的独立流不受影响:无关的通知仍然能到达。
!!! check
故意做错一次。`reserve` 就是刚才同时服务两个客户端的那个工具。用 `stateless_http=True` 部署它,通过 HTTP 连上同样的两个客户端,分别调用它。
故意做错一次。`reserve` 就是刚才同时服务两个客户端的那个工具。用 `stateless_http=True` 部署它,连上同样的两个客户端,分别调用它。
现代客户端仍然收到 `Reserved 2 of 'Dune'.`,现代这一路没变。