docs: refresh translations, and translate pages in parallel (#3458)
This commit is contained in:
parent
08a3bc8eaf
commit
fd66270926
23 changed files with 4821 additions and 2277 deletions
|
|
@ -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` 并显示结果。真正的应用请在
|
|||
|
||||
模型读的是 `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
|
||||
```
|
||||
|
||||
返回的是富结果。从 `Client` 调用里去掉 `extensions=[APPS_SUPPORT]`,同一个程序就会改为打印 `The time is 2026-06-26T12:00:00Z.`,这也是纯文本客户端所能看到的全部。
|
||||
|
||||
!!! warning
|
||||
绝不要把 `"[Rendered UI]"` 这样的占位符当作唯一的内容返回。如果回退文本没用,这个工具对所有纯文本客户端乃至模型本身就都没用。把那句话写出来。
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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` 返回的是**你**敲进去的模式,一字不差。高层版本在每个属性上都有 `"title": "Query"`,根上还有一个 `"title": "search_booksArguments"`:Pydantic 的产物。在这一层,线路上有什么,都是你放上去的。
|
||||
|
||||
在测试里可以跳过 uvicorn 和端口:`Client(server)` 在进程内接收底层 `Server` 的方式和接收 `MCPServer` 完全一样,**[测试](../get-started/testing.md)** 讲的就是这个模式。
|
||||
|
||||
## 没有替你做任何检查 {#nothing-is-checked-for-you}
|
||||
|
||||
`MCPServer` 会在你的函数运行之前拒绝错误的参数,按它生成的模式校验调用(**[工具](../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)**。
|
||||
|
|
|
|||
|
|
@ -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`,循环只跑一次。
|
||||
|
||||
|
|
|
|||
|
|
@ -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)**。
|
||||
|
||||
|
|
|
|||
|
|
@ -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)** 涵盖了每一种。
|
||||
|
|
|
|||
|
|
@ -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`。抛异常的工具是一个结果,不是异常。
|
||||
|
|
|
|||
|
|
@ -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)**。
|
||||
|
||||
|
|
|
|||
|
|
@ -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 的键,线路上的名称不变。
|
||||
|
|
|
|||
|
|
@ -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` 打开它。
|
||||
|
||||
|
|
|
|||
|
|
@ -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` 表示你的验证器自己检查令牌的受众(见 **[授权](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。
|
||||
|
|
|
|||
|
|
@ -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` 的模式。
|
||||
* 请求处理函数。`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()`。名称、描述和模式都来自函数本身。
|
||||
* 带 `{param}` 的 URI 生成的是资源 **模板**,与具体资源分开列出。
|
||||
* 服务器的 **能力** 会替你声明好,而客户端只会请求服务器声明过的内容。
|
||||
* `Client(mcp)` 在内存中连接服务器对象:从第一天起,它就是你的测试工具。
|
||||
* `Client("http://localhost:8000/mcp")` 与正在运行的服务器对话。改为把服务器对象交给它,即 `Client(mcp)`,它从第一天起就是你的测试工具。
|
||||
|
||||
接下来是 **[连接到真实宿主](real-host.md)**:把这个服务器真正放进 Claude Desktop 或 IDE 里。然后是 **[测试](testing.md)**:一页内容,一个内存客户端,从此不用再猜它到底能不能用。再之后,每种原语各有自己的一页,从模型驱动的那一种开始:**[工具](../servers/tools.md)**。
|
||||
|
|
|
|||
|
|
@ -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)` 在进程内连接,默认**不区分协议时代**:它会先探测服务器,再选择合适的协议路径。如果测试要验证旧版(legacy)特有的语义(采样(sampling)或征询(elicitation)的推送、`message_handler`),就固定使用 `mode="legacy"`,并在这种情况下去掉 `raise_exceptions=True`:旧版连接本来就不做脱敏,而这个标志会让失败在服务器任务内部重新抛出,而不是抛到你的测试里。
|
||||
|
|
|
|||
|
|
@ -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)请求 {#multi-round-trip-requests}
|
||||
|
|
@ -162,7 +162,7 @@ TTL、主体绑定和请求绑定**不是** codec 的工作:对每个 codec,
|
|||
|
||||
## 一个 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"` 连接上从处理函数返回一个,运行器无法把它序列化到协商好的版本;客户端收到的是 `-32603`“Handler returned an invalid result”错误。同时服务两个时代的服务器在用它之前必须检查 `ctx.protocol_version`。
|
||||
|
|
|
|||
|
|
@ -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:
|
|||
|
||||
客户端**按调用**选择接收,方法是给 `call_tool` 传 `progress_callback=`:
|
||||
|
||||
```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` 的是什么——像这里这样的 URL、`StdioServerParameters`,还是测试里的服务器对象——`progress_callback` 都是同一个参数。不过,走真实传输方式时要留意时序。每条通知都是单独送达的,和响应各走各的,所以 `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`。没有对应的构造函数参数,因为不同的调用想要不同的回调:这一次驱动下载进度条,下一次是一行日志。
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
|||
|
||||
有效的 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)**。
|
||||
|
|
|
|||
|
|
@ -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`:用单个 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
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue