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

View file

@ -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]"` 这样的占位符当作唯一的内容返回。如果回退文本没用,这个工具对所有纯文本客户端乃至模型本身就都没用。把那句话写出来。

View file

@ -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}

View file

@ -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)**。

View file

@ -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`,循环只跑一次。