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,13 +27,13 @@ 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"
```
处理函数指定了 `ttl_ms=1_000`,对作用域只字未提。线路上是:`ttlMs: 1000`(处理函数的值,而不是映射里的 `60_000`)和 `cacheScope: "public"`(映射的值,因为处理函数没有设置)。显式优先于配置,配置优先于默认。这条规则按字段生效,所以处理函数可以固定一个字段,把另一个留给服务器范围的策略。
这也是构造函数无法预知的动态情况的出口:一个按用户过滤 `resources/read` 的处理函数,可以在其他方面都是 public 的服务器上为某个 URI 返回 `cache_scope="private"`。
这也是应对构造函数无法预知的动态情况的出口:一个按用户过滤 `resources/read` 的处理函数,可以在其他方面都是 public 的服务器上为某个 URI 返回 `cache_scope="private"`。
关于分页列表有一点要注意:协议要求同一列表的**每一页 `cacheScope` 相同**。构造函数映射天然满足这一点,因为它按方法而不是按页作键。但自行覆盖作用域的处理函数要自己负责这种一致性:在**每一**页都覆盖,绝不要只在有游标时覆盖,否则第一页和第二页会不一致。
@ -41,10 +41,24 @@ translation:
在 2026-07-28 会话上,`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`(进度令牌、追踪字段)的请求期望产生一次线路请求,所以在 `cache_mode="use"` 下它被当作 `"refresh"` 处理:跳过缓存读取,抓取到的结果仍会替换缓存条目。`"bypass"` 和显式的 `"refresh"` 行为照旧。
要完全关闭缓存,用 `Client(server, cache=None)` 构造:每次调用重新变成一次往返,`cache_mode` 虽然仍被接受,但不起作用。
要完全关闭缓存,构造 `Client` 时传入 `cache=None`:每次调用重新变成一次往返,`cache_mode` 虽然仍被接受,但不起作用。
作用域同样自动遵从:`"private"` 条目按缓存的**分区**(见下文)作键,而 `"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 会话上,这个回调并没有失效,只是触发方式不同:当工具返回一个携带 `ElicitRequest` 的 `InputRequiredResult` 时,`Client` 会把该条目分派给同一个 `elicitation_callback`,并替你重试这次调用。那个流程见 **[多轮往返请求](../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
---
# Client {#the-client}
@ -11,13 +11,23 @@ translation:
## 你的第一个客户端 {#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 @@ translation:
`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 @@ translation:
## 列出工具 {#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` 里。每一个都是宿主会交给模型的完整定义:
`list_tools()` 返回 `ListToolsResult`;工具在 `.tools` 里。每一个都是宿主会交给模型的完整定义。下面是第一个:
```python
tool.name # 'search_books'
@ -76,6 +86,8 @@ tool.description # 'Search the catalog by title or author.'
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 @@ UI 渲染参数表单所需的一切,以及模型生成合法参数所需的
`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`。提供者接手:
1. **发现。** 它读取 `WWW-Authenticate` 头,从 `/.well-known/oauth-protected-resource` 获取服务器的受保护资源元数据,得知是哪个授权服务器在保护这个资源,再去获取**那个**服务器的元数据。
1. **发现。** 它读取 `WWW-Authenticate` 头,从 `/.well-known/oauth-protected-resource` 获取服务器的受保护资源元数据,得知是哪个授权服务器在保护这个资源,再去获取**那个**服务器的元数据。(较旧的服务器如果没有发布资源元数据,则改为在它自己的源上请求授权服务器元数据。)无论哪种方式,元数据里的 `issuer` 都必须正是获取它所针对的那个服务器;其他任何值一律拒绝。
2. **注册。** 存储里什么都没有?它用你的 `OAuthClientMetadata` 动态注册,并把结果存起来。
3. **授权。** 它生成 PKCE 对和一个 `state`,构建授权 URL,await 你的 `redirect_handler`,然后 await 你的 `callback_handler` 拿到授权码。
4. **交换。** 它用授权码换来 `OAuthToken`,存起来,再带上 `Authorization: Bearer ...` 重放你最初的请求。
之后它就安静了。令牌从存储里取出,过期的访问令牌用刷新令牌刷新,只有这些都行不通时才会重新跑一遍流程。
有一条传输规则适用于所有这些请求:和它们所嵌套的那个 MCP 请求一样,只有当重定向留在同一个源并且保持请求方法不变时(比如补尾部斜杠的 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 且路径不能是根路径;否则在构造时就是 `Valu
`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`;提供者围绕它们构建一个最小的 `client_credentials` 注册,完全跳过动态注册。
* `issuer` 指明颁发这些凭据的授权服务器;使用它的 `/.well-known/oauth-authorization-server` 文档返回的 `issuer` 值。发现仍按上面的方式进行,但令牌请求只会基于**那个**颁发者的元数据构建;如果 MCP 服务器指向别处,流程会以 `OAuthFlowError` 中止。省略它已弃用,到 3.0 会变为必填(见 **[已弃用的功能](../deprecated.md#deprecated-sdk-helpers)**);在此之前,提供者会发出警告,并使用发现找到的那个授权服务器。
* `scope` 是空格分隔的字符串,即 OAuth 的线路格式。
* 下游的一切完全相同:同样的 `TokenStorage`、同样的 `httpx2.AsyncClient(auth=...)`、同样的 `streamable_http_client`。
@ -121,7 +124,7 @@ URL 必须是 HTTPS 且路径不能是根路径;否则在构造时就是 `Valu
从环境变量或密钥管理器读取 `client_secret`,绝不要从源码版本控制里读。
!!! info
`mcp.client.auth.extensions.client_credentials` 里还有一个提供者:**`PrivateKeyJWTOAuthProvider`**,用于以 JWT 而非共享密钥进行认证的客户端(`private_key_jwt`,即密钥对和工作负载身份那一类)。它遵循同样的模式:构造一个,放到 `auth=` 上。同一个模块还附带 `SignedJWTParameters` 和 `static_assertion_provider`,两个用来构建其断言的辅助工具。
`mcp.client.auth.extensions.client_credentials` 里还有一个提供者:**`PrivateKeyJWTOAuthProvider`**,用于以 JWT 而非共享密钥进行认证的客户端(`private_key_jwt`,即密钥对和工作负载身份那一类)。它遵循同样的模式:构造一个(它接受同样可选的 `issuer`),放到 `auth=` 上。同一个模块还附带 `SignedJWTParameters` 和 `static_assertion_provider`,两个用来构建其断言的辅助工具。
还有一种没有人参与的情形:客户端属于某个企业,由企业的身份提供者而不是用户来决定它可以访问哪些 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 秒,读超时 300 秒,因为服务器可能会一直保持响应流打开。
这就是完整的生产环境客户端。`Client` 替你把 URL 包进 `streamable_http_client(...)`,底层是一个按 MCP 的需要配置好的 `httpx2.AsyncClient`:connect/write/pool 超时 30 秒,读超时 300 秒,因为服务器可能会一直保持响应流打开。
!!! check
构造出来的 `Client` **并未**连接。构造只是选定传输方式;打开它的是 `async with`。在进入之前就去取连接,SDK 会明确告诉你:
@ -49,7 +34,7 @@ translation:
一旦需要 `Authorization` 头、cookie、代理、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"
```
@ -58,10 +43,7 @@ translation:
* `httpx2.AsyncClient` 归你所有,所以由**你**进入和退出它。SDK 从不关闭不是它自己创建的客户端。
* `streamable_http_client(url, http_client=...)` 返回一个传输,`Client(transport)` 像接受其他任何东西一样接受它。
关于 TLS 的一点说明:`httpx2` 依据操作系统的信任库(通过
[`truststore`](https://pypi.org/project/truststore/))校验证书,而不是自带的 CA 列表。在没有可用系统 CA 库的环境(某些精简容器)中,设置标准的 `SSL_CERT_FILE`/`SSL_CERT_DIR`
环境变量,或者给你的 `httpx2.AsyncClient` 显式传入 `verify=ssl_context`(背景见
[`httpx` 和 `httpx-sse` 被 `httpx2` 取代](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2))。
关于 TLS 的一点说明:`httpx2` 依据操作系统的信任库(通过 [`truststore`](https://pypi.org/project/truststore/))校验证书,而不是自带的 CA 列表。在没有可用系统 CA 库的环境(某些精简容器)中,设置标准的 `SSL_CERT_FILE`/`SSL_CERT_DIR` 环境变量,或者给你的 `httpx2.AsyncClient` 显式传入 `verify=ssl_context`(背景见 [`httpx` 和 `httpx-sse` 被 `httpx2` 取代](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2))。
!!! warning
`streamable_http_client` 过去可以直接接受 `headers=` 和 `timeout=`。现在不行了:它只有 `url`、`http_client` 和 `terminate_on_close` 三个参数。习惯性地去用 `headers=`,会得到:
@ -73,7 +55,25 @@ translation:
所有 HTTP 层面的东西现在都放在你传入的那一个 `httpx2.AsyncClient` 上。
!!! info
`httpx2` 保留了熟悉的 `httpx` API,所以只要会 `httpx`,就已经知道在这里怎么做认证、代理、事件钩子、重试和连接限制。SDK 既不在上面加东西,也不拿走什么。OAuth 也是在这里接入的:`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。整个流程见 **[OAuth 客户端](oauth-clients.md)**。
`httpx2` 保留了熟悉的 `httpx` API,所以只要会 `httpx`,就已经知道在这里怎么做认证、代理、事件钩子、重试和连接限制。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 就是你要连的服务器,把它写进配置。如果不是,说明服务器或它前面的代理配置有误。
这对你传入的任何 `httpx2.AsyncClient` 都成立:MCP 请求不会参考它的 `follow_redirects` 设置,无论设成哪一种。SDK 的 OAuth provider 对它们自己的请求也应用同样的规则。
!!! tip
`Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` 的意思是:服务器前面有一个它不知道的 TLS 终止代理,于是它发出的是 `http://` 重定向。这要在服务器端修复(**[部署与扩展](../run/deploy.md#behind-a-tls-terminating-proxy)**),或者直接使用报错信息建议的那个精确的 `https://…/` URL。
## stdio {#stdio}
@ -94,6 +94,18 @@ translation:
需要 API key 的服务器在那里找不到它。用 `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}
`sse_client(url)` 来自 `mcp.client.sse`,是被 Streamable HTTP 取代的那个 HTTP 传输。用同样的方式包一层,`Client(sse_client("http://localhost:8000/sse"))`,就能和仍在使用它的服务器通信;不要在它之上构建任何新东西。
@ -102,15 +114,16 @@ translation:
对 `Client` 来说,上面这些都是同一种东西。
**传输**是任何能产出一对 `(read, write)` 消息流的异步上下文管理器:正式地说,就是 `mcp.client` 中的 `Transport` 协议。`Client` 按类型解析它的参数:服务器对象在进程内连接,`str` 变成 `streamable_http_client(url)`,`StdioServerParameters` 变成 `stdio_client(params)`,其他任何东西都直接作为传输进入。正是最后这条规则让 `stdio_client(...)`、`streamable_http_client(...)` 和 `sse_client(...)` 都能放进同一个位置,也让你可以自己写一个。
**传输**是任何能产出一对 `(read, write)` 消息流的异步上下文管理器:正式地说,就是 `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 连接,即生产环境的传输方式。
* 请求头、认证、代理和超时应放在 `httpx2.AsyncClient` 上,再传给 `streamable_http_client(url, http_client=...)`。没有 `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` 打开它。