docs: refresh translations for recent English changes (#3379)

This commit is contained in:
Max 2026-08-24 19:29:54 +01:00 • committed by GitHub
commit c5d7d0b6d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 3149 additions and 1047 deletions

View file

@ -1,6 +1,6 @@
---
translation:
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e]
tool: 1
---
# 底层 Server {#the-low-level-server}
@ -116,6 +116,17 @@ asyncio.run(main())
服务器从不比较这两个字段。本 SDK 的 `Client` 会:返回的 `structured_content` 不满足你声明的 `output_schema` 时,`call_tool` 会抛出一个 `RuntimeError`,开头是 `Invalid structured content returned by tool search_books`,后面引用 `jsonschema` 的失败信息。承诺一个模式很便宜;信守它是你的事。返回类型和模式的完整阶梯详见 **[结构化输出](../servers/structured-output.md)**。
## 方言是 JSON Schema 2020-12 {#the-dialect-is-json-schema-2020-12}
`input_schema` 和 `output_schema` 是 JSON Schema,而 [MCP 规范](https://modelcontextprotocol.io/specification/latest/basic#json-schema-usage) 固定了方言:没有 `$schema` 键的模式就是 **JSON Schema 2020-12**。`MCPServer` 生成的模式依赖这个默认值(Pydantic 写的是 2020-12 并省略该键),手写的 dict 也按同样的标准对待,所以完整的 2020-12 词汇表都可以用:
```python title="server.py" hl_lines="8 14-15"
--8<-- "docs_src/lowlevel/tutorial007.py"
```
* `input_schema` 的根必须是 `"type": "object"`。在它旁边,`oneOf`、`additionalProperties`、`anyOf`、`if`/`then`/`else`、`prefixItems`、带本地 `$ref` 的 `$defs`,以及其余 2020-12 关键字,都会一字不差地到达客户端。
* 不需要 `$schema` 键。只有想选用更早的草案时才加:本 SDK 的 `Client` 会按工具的 `output_schema` 校验 `structured_content`,它根据 `$schema` 选择校验器,没有时就用 2020-12。
## `_meta`:给应用程序,不是给模型 {#\_meta-for-the-application-not-the-model}
`content` 是答案里模型读取的部分。`structured_content` 是同一个答案的类型化数据形式。`_meta` 是第三条通道:随结果一起传递、面向**客户端应用程序**的数据,根本不属于答案的一部分。
@ -166,7 +177,7 @@ asyncio.run(main())
--8<-- "docs_src/lowlevel/tutorial006.py"
```
* 第一个参数是方法字符串。通知有一个对应的 `add_notification_handler`。
* 第一个参数是方法字符串。通知有一个对应的 `add_notification_handler`。它的处理函数在 stdio 和握手时代的 HTTP 连接上触发;在 `2026-07-28` 的 Streamable HTTP 路径上,客户端发来的通知 POST 会得到 `202` 确认但不会分发,因为那个修订版没有定义通过 HTTP 的客户端到服务器通知。
* `params_type` 是传入的 `params` 在处理函数运行**之前**校验所依据的模型,所以自定义方法**确实**得到了工具没有的校验。继承 `RequestParams`,这样 `_meta` 字段的解析方式和其他方法一样。
* 处理函数返回 `BaseModel`、`dict` 或 `None`。SDK 把它序列化进 JSON-RPC 结果。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
sections: [6048b4f308edbb8c, 46056f318ef205e4, c3e565b61acd75c5, c62422b159c6ed09, 420968f514138f43]
tool: 1
---
# 中间件 {#middleware}
@ -42,7 +42,7 @@ tools/call took 0.1 ms
这正是关键所在。中间件包裹**每一条**入站消息:
* 连接建立:`server/discover`,或者旧版会话上的 `initialize` 和 `notifications/initialized`。
* 每一个请求和每一个通知。对于通知,`ctx.request_id is None`,`call_next(ctx)` 返回 `None`,而你返回的任何东西都会被丢弃。
* 每一个到达服务器的请求和通知。对于通知,`ctx.request_id is None`,`call_next(ctx)` 返回 `None`,而你返回的任何东西都会被丢弃。(在 `2026-07-28` 的 Streamable HTTP 路径上,客户端的通知 POST 在传输层就以 `202` 确认,从不分发,所以也到不了中间件;该修订版本没有定义任何经由 HTTP 的客户端到服务器通知。)
* 甚至包括服务器没有处理函数的方法:`call_next` 会抛出 `MCPError(-32601, "Method not found")`,**穿过**你的中间件送往客户端。
## 在中间件里能做什么 {#what-you-can-do-inside-one}
@ -75,7 +75,7 @@ SDK 自带的中间件恰好只有一个,而且已经在你服务器的列表
## 回顾 {#recap}
* 中间件是 `async (ctx, call_next) -> result`,以 `MCPServer(middleware=[...])` 传入(或追加到 `mcp.middleware`),在低层 `Server` 上则追加到 `server.middleware`。
* 它包裹**每一条**入站消息(`server/discover`、`initialize`、请求、通知、未知方法),按从外到内的顺序执行。
* 它包裹**每一条**到达服务器的入站消息(`server/discover`、`initialize`、请求、通知、未知方法),按从外到内的顺序执行。
* 用 `ctx.request_id is None` 区分通知和请求。
* 抛出异常而不调用 `call_next` 即可拒绝一条消息;连接不受影响。
* SDK 自己的 OpenTelemetry 追踪也是一个中间件,已经在列表上了。见 **[OpenTelemetry](../run/opentelemetry.md)**。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
tool: 1
---
# Client {#the-client}
@ -27,9 +27,10 @@ translation:
* `MCPServer`(或低层 `Server`)实例:**进程内**连接。
* URL 字符串(`Client("http://localhost:8000/mcp")`):Streamable HTTP,生产环境的路径。
* **传输**:任何可以 `async with ... as (read, write)` 的对象,比如包装子进程的 `stdio_client(...)`。
* `StdioServerParameters`:要作为**子进程**启动的命令,通过它的 stdin 和 stdout 通信。
* **传输**:任何可以 `async with ... as (read, write)` 的对象,比如用 `streamable_http_client(url, http_client=...)` 包装你自己的 HTTP 客户端。
本页其余内容在这三种方式下完全相同。请求头、子进程、超时以及 `Transport` 协议另有专页:**[客户端传输](transports.md)**。
本页其余内容在这四种方式下完全相同。请求头、子进程、超时以及 `Transport` 协议另有专页:**[客户端传输](transports.md)**。
### 已连接的客户端上有什么 {#whats-on-a-connected-client}
@ -82,7 +83,7 @@ UI 渲染参数表单所需的一切,以及模型生成合法参数所需的
`call_tool(name, arguments)` 运行工具,返回 `CallToolResult`。
```python title="client.py" hl_lines="26-33"
```python title="client.py" hl_lines="27-34"
--8<-- "docs_src/client/tutorial003.py"
```
@ -113,7 +114,7 @@ result.is_error # False
抛出异常的工具**不会**在客户端里抛出异常。它作为一个普通结果返回,带 `is_error=True`。
!!! check
向 `lookup_book` 查询 `"Solaris"`(目录里没有的书名),函数会抛出 `ValueError`。调用仍然正常返回:
向 `lookup_book` 查询 `"Solaris"`(目录里没有的书名),函数会抛出 `ToolError`。调用仍然正常返回:
```python
result.is_error # True
@ -121,7 +122,7 @@ result.is_error # False
result.structured_content # None
```
异常消息落在了 `content` 里,**模型**可以读到它并重试。这是有意为之:工具错误是对话的一部分,不是崩溃。在相信 `structured_content` 之前,务必先看 `is_error`。
`ToolError` 的消息落在了 `content` 里,**模型**可以读到它并重试。这是有意为之:工具错误是对话的一部分,不是崩溃。(如果工具是因为别的异常崩溃的,`content` 里只会写 `Error executing tool lookup_book`。)在相信 `structured_content` 之前,务必先看 `is_error`。
!!! warning
`is_error=True` 涵盖的不只是你自己的 `raise`。请求一个服务器根本没有的工具(`call_tool("does_not_exist", {})`),什么异常都不会抛出。返回的形状相同:`is_error=True`,`content` 里是 `Unknown tool: does_not_exist`。只有当服务器回复的是 JSON-RPC **错误**而不是结果时,`Client` 方法才会抛出 `MCPError`;服务器在什么情况下产生哪一种,见 **[处理错误](../servers/handling-errors.md)**。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c]
tool: 1
---
# 客户端传输 {#client-transports}
@ -79,15 +79,15 @@ translation:
**stdio** 服务器是一个子进程。客户端启动它,向它的 stdin 写 JSON-RPC,从它的 stdout 读 JSON-RPC。桌面宿主就是这样在你的机器上运行服务器的:宿主**就是**这段代码加上一个 UI,而 **[连接到真实宿主](../get-started/real-host.md)** 是从宿主一侧、以配置文件的形式看到的同一种关系。
用 `StdioServerParameters` 描述进程,用 `stdio_client` 把它变成传输,再把**它**交给 `Client`:
用 `StdioServerParameters` 描述进程,再把它交给 `Client`:
```python title="client.py" hl_lines="4-8 12"
```python title="client.py" hl_lines="3-7 11"
--8<-- "docs_src/client_transports/tutorial004.py"
```
`Client` 不接受单独的参数对象。`StdioServerParameters` 是配置;`stdio_client(server)` 才是知道如何据此启动进程的传输。一定要包一层。
进入块时启动进程。离开块时关停子进程:关闭 stdin,等待,如果它迟迟不退出就杀掉。你从来不需要自己清理。
离开 `async with` 块也会关停子进程:关闭 stdin,等待,如果它迟迟不退出就杀掉。你从来不需要自己清理。
子进程的 stderr 会输出到你的 stderr。要把它送到别处,就用 `stdio_client`(来自 `mcp`)自己构建传输,改为传入它:`Client(stdio_client(server, errlog=log_file))`。
!!! warning
子进程**不会**继承你的环境。它只拿到一个最小的允许列表(POSIX 上是 `HOME`、`LOGNAME`、`PATH`、`SHELL`、`TERM` 和 `USER`),这样敏感信息就不会泄漏进一个可能不是你写的进程。
@ -102,16 +102,16 @@ translation:
对 `Client` 来说,上面这些都是同一种东西。
**传输**是任何能产出一对 `(read, write)` 消息流的异步上下文管理器:正式地说,就是 `mcp.client` 中的 `Transport` 协议。`Client` 按类型解析它的参数:服务器对象在进程内连接,`str` 变成 `streamable_http_client(url)`,其他任何东西都直接作为传输进入。正是最后这条规则让 `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=` 关键字参数。
* stdio 是 `Client(stdio_client(StdioServerParameters(...)))`,绝不是单独的参数对象。
* stdio 是 `Client(StdioServerParameters(...))`。只有在需要重定向子进程的 stderr 时,才自己用 `stdio_client(...)` 包一层。
* 子进程拿到的是允许列表里的环境,不是你的环境;`env=` 往里添加。
* 传输就是任何可以 `async with x as (read, write)` 的东西。凡不是服务器对象或 URL 的参数,`Client` 都直接交给这个协议。
* 传输就是任何可以 `async with x as (read, write)` 的东西。凡不是服务器对象、URL 或 `StdioServerParameters` 的参数,`Client` 都直接交给这个协议。
* 构造 `Client` 选定传输方式。`async with` 打开它。
传输打开之后,两边必须就协议版本达成一致。通常根本不用考虑它;需要考虑的时候,去看 **[协议版本](../protocol-versions.md)**。

View file

@ -1,11 +1,11 @@
---
translation:
sections: [20541a40dbdd5980, 01262a123ad9501d, 429db5b574a2ac08, 56b2d49da412cb28, 6a1717123fe4513c]
sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931]
tool: 1
---
# 已弃用的功能 {#deprecated-features}
2026-07-28 规范让五项内容退役。SDK 仍然实现了其中每一项,而且每一项现在都带有**弃用警告**。
2026-07-28 规范让五项内容退役。SDK 仍然实现了其中每一项,而且每一项现在都带有**弃用警告**。另有一个 SDK 辅助函数因自身原因被弃用,列在[本页末尾](#deprecated-sdk-helpers)。
下表列出了每一项已弃用的功能、它为什么要退场,以及应该改用的替代方案。
@ -49,6 +49,55 @@ MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SE
两个信号,按这个顺序。`MCPDeprecationWarning` 在你调用方法的那一刻触发,任何连接上都是如此。错误是 SDK 随后尝试发送时返回的结果。这两个功能只有在 `mode="legacy"` 连接上、且客户端注册了对应回调时,才能端到端地工作。
## 旧版会话上的 `ping` {#ping-on-a-legacy-session}
**ping** 是一个空请求,任何一方都可以发送,用来确认对方仍在应答。2026-07-28 规范移除了它([SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575)):现代客户端发出的每个请求本身就证明服务器在那里,而现代服务器没有通道可以发出 ping。两个 SDK 方法在握手时代的会话上仍然有效。从客户端:
```python
async def main() -> None:
async with Client("http://localhost:8000/mcp", mode="legacy") as client:
await client.send_ping() # warns; returns an EmptyResult
```
从服务器,在任意处理函数内部:
```python
@mcp.tool()
async def check_client(ctx: Context) -> str:
"""A tool that still pings the client mid-call."""
await ctx.session.send_ping() # no warning; an EmptyResult while the client is connected
return "client answered"
```
* `client.send_ping()` 每次调用都会发出 `MCPDeprecationWarning`。在默认(`2026-07-28`)连接上,服务器改为回应 `MCPError: Method not found`。
* `ctx.session.send_ping()` 不带警告。在现代连接上,它和其他任何服务器发起的请求一样,抛出同样的无反向通道(back-channel)错误。
* 双方都不需要注册任何东西来应答 ping。
## 根目录变更通知 {#roots-change-notifications}
声明了根目录能力的 2025 时代客户端,可以发送 `notifications/roots/list_changed` 告诉服务器它的工作区文件夹变了;服务器的回应是再次请求 `roots/list`。2026-07-28 规范把这个通知连同其余推送式的根目录流程一起移除了。在客户端,传入 `list_roots_callback=`(**[客户端回调](client/callbacks.md)**)就等于声明了 `"roots": {"listChanged": true}`,而一次调用就能兑现这个承诺:
```python
async def open_folder(client: Client, uri: str, name: str) -> None:
"""The user opened another folder: expose it through the roots callback, then tell the server."""
workspace.append(Root(uri=FileUrl(uri), name=name))
await client.send_roots_list_changed()
```
在服务器端,接收方的处理函数交给低层 `Server`:
```python
async def roots_changed(ctx: ServerRequestContext, params: NotificationParams | None) -> None:
"""The client's roots changed: ask for the new list."""
roots = (await ctx.session.list_roots()).roots
server = Server("Bookshop", on_roots_list_changed=roots_changed)
```
* `workspace` 是你的 `list_roots_callback` 返回的列表。`client.send_roots_list_changed()` 会发出警告,并且需要 `mode="legacy"` 客户端:在现代连接上,这个通知会被静默丢弃。之后保持会话打开,因为服务器后续的 `roots/list` 请求会从这个会话上到达。
* `MCPServer` 没有针对这个通知的钩子。在低层 `Server` 上,`on_roots_list_changed=` 注册处理函数(它也已弃用,并在构造时发出警告)。通知不带任何载荷,所以处理函数调用 `ctx.session.list_roots()` 获取新列表。
## 屏蔽警告 {#silencing-the-warning}
新代码里不要这样做。
@ -66,21 +115,30 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
整个 API 就这些。没有按方法的开关,你也不需要:只用一个类别的意义就在于,一行代码让它静音,一行代码把它恢复。
!!! check
反过来用这个过滤器,就白得一个回归测试。在 pytest 配置的 `filterwarnings` 设置里加上 `"error::mcp.MCPDeprecationWarning"`,已弃用的调用就会**抛出异常**而不是发出警告。一个名为 `old_log`、仍在调用 `ctx.info()` 的工具不再通过,转而报告:
反过来用这个过滤器,就白得一个回归测试。在 pytest 配置的 `filterwarnings` 设置里加上 `"error::mcp.MCPDeprecationWarning"`,已弃用的调用就会**抛出异常**而不是发出警告。一个名为 `old_log`、仍在调用 `ctx.info()` 的工具不再通过:调用返回 `is_error=True`,附带 `Error executing tool old_log`,而捕获到的服务器日志点出了元凶:
```text
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
mcp.shared.exceptions.MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```
一行 pytest 配置,已弃用的调用就再也不可能悄悄溜回你的代码库而不让测试失败。
## 已弃用的 SDK 辅助函数 {#deprecated-sdk-helpers}
这些不是规范变更,只是有了更好替代的 SDK 内部实现。它们用同一个 `MCPDeprecationWarning` 发出警告,并将在 3.0 中移除。
| 已弃用 | 替代做法 |
|---|---|
| `FuncMetadata.call_fn_with_arg_validation()` | 先调用 `FuncMetadata.validate_arguments()`,再调用 `FuncMetadata.call_fn()`。只有直接驱动 `FuncMetadata` 的代码(比如自定义的 `Tool` 子类)才调用过它。 |
## 回顾 {#recap}
* 2026-07-28 规范弃用了**根目录**、服务器发起的**采样**和协议**日志**(都出自 [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)),把**进度**限制为只能由服务器发往客户端,并移除了 **`ping`**。
* 替代做法那一列为你指明了去处:采样和根目录看 **[多轮往返请求](handlers/multi-round-trip.md)**,日志看 **[日志](handlers/logging.md)**,进度看 **[进度](handlers/progress.md)**。`ping` 什么都不需要。
* 弃用只是建议性的:线路上没有变化,在 2026 之前的会话上一切照常工作,你会看到一条醒目的 `MCPDeprecationWarning`(它是 `UserWarning`,所以默认开启)。
* 采样和根目录还需要一条反向通道(back-channel),而 2026-07-28 会话没有。在现代连接上,它们先警告,然后抛出异常。
* 采样和根目录还需要一条反向通道,而 2026-07-28 会话没有。在现代连接上,它们先警告,然后抛出异常。
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` 让整个类别静音;pytest 中的 `"error::mcp.MCPDeprecationWarning"` 把它变成测试失败。
* 一个 SDK 辅助函数 `FuncMetadata.call_fn_with_arg_validation()` 单独被弃用,将在 3.0 中移除。
* 新代码不应建立在其中任何一项之上。
本文档的其他每一页讲的都是当前的 API。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [3c4f2f06b4e978b6, 22520eecae3d1961, f4e1709db18d635a, 2eb57992049671d9, 1ba83e9af37cc1b4, 4822586344b08d9e, 1c93afef72478992, b6b448f9eddd51dc, fe55370fd931815b]
sections: [3c4f2f06b4e978b6, 51ea5fbcb0e93563, 32d8808606ffdae0, 2eb57992049671d9, 1ba83e9af37cc1b4, 4822586344b08d9e, 1c93afef72478992, b6b448f9eddd51dc, fe55370fd931815b]
tool: 1
---
# 连接到真实的宿主 {#connect-to-a-real-host}
@ -11,7 +11,7 @@ translation:
## 一个服务器,所有宿主 {#one-server-every-host}
```python title="server.py" hl_lines="3 33-34"
```python title="server.py" hl_lines="4 34-35"
--8<-- "docs_src/real_host/tutorial001.py"
```
@ -41,7 +41,7 @@ uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
!!! note "本页讲的是本地场景"
这里的一切都是在宿主所在的那台机器上运行你的服务器:宿主通过 stdio 启动你的文件。对个人工具或单机工具来说,这样做完全合适。要把服务器交给 **没有** 你这个文件的人,给出去的是 **URL** 而不是命令:同一个 `mcp` 对象,通过 Streamable HTTP 提供服务。**[运行服务器](../run/index.md)** 用一张表讲清这个决策,**[部署与扩展](../run/deploy.md)** 则是从那里走到真实主机名的路线。
而且宿主不过是内置了 MCP 客户端的应用程序,所以你自己的 Python 也能扮演宿主的角色:**[客户端传输方式](../client/transports.md)** 用 `stdio_client(...)` 把同一个文件作为子进程启动,**[测试](testing.md)** 则一个进程都不起,直接在内存中连接它。
而且宿主不过是内置了 MCP 客户端的应用程序,所以你自己的 Python 也能扮演宿主的角色:**[客户端传输方式](../client/transports.md)** 用 `Client(StdioServerParameters(...))` 把同一个文件作为子进程启动,**[测试](testing.md)** 则一个进程都不起,直接在内存中连接它。
## Claude Desktop {#claude-desktop}

View file

@ -1,6 +1,6 @@
---
translation:
sections: ['4926721070127497', c52a1de2b6b32f40, 2e410b412c25f314, 627195f7159e24ef]
sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef]
tool: 1
---
# 测试 {#testing}
@ -80,7 +80,7 @@ async def test_call_add_tool(client: Client):
可能出错的情况有两种,而这个标志只管其中一种。
**你的工具**内部抛出的异常不算协议失败。它会变成一个带 `is_error=True` 的普通结果,模型会读到其中的消息。`raise_exceptions` 不会改变这一点:不管有没有它,`call_tool` 返回的都是同一个 `is_error=True` 结果。有一整页专门讲这个:**[处理错误](../servers/handling-errors.md)**。
**你的工具**内部抛出的异常不算协议失败。它会变成一个带 `is_error=True` 的普通结果(如果抛出的是 `ToolError`,模型会读到你写的消息)。`raise_exceptions` 不会改变这一点:不管有没有它,`call_tool` 返回的都是同一个 `is_error=True` 结果。有一整页专门讲这个:**[处理错误](../servers/handling-errors.md)**。
工具函数体**之外**的失败则不同。在 `Client(mcp)` 提供的这条连接上,服务器会先把它脱敏成一条笼统的 `"Internal server error"`,客户端才会看到。意外崩溃的细节绝不应该泄露给远程调用方。但在测试里,这恰恰是你**不**想要的,也正是 `raise_exceptions=True` 所改变的:测试看到的是真实的消息,而不是脱敏后的那条。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [335ca2a0b266f003, d1ad562d3fe87bc0, 0bb1396c86daeba4, d1cb1235bb9ee267, 833179c09d239c83, e5d6dec2d2e655e8]
sections: [335ca2a0b266f003, d1ad562d3fe87bc0, 25b49f89c9e6f9d0, d1cb1235bb9ee267, 833179c09d239c83, e5d6dec2d2e655e8]
tool: 1
---
# 征询 {#elicitation}
@ -83,7 +83,7 @@ translation:
这个模式就是表单。`Field(description=...)` 是标签;默认值会预填输入框,并让该字段变成可选。这和 **[工具](../servers/tools.md)** 里描述的工具参数用的是同一套 Pydantic 转 JSON Schema 的机制。
!!! warning
征询的模式不如工具的输入模式表达力强。只能是扁平的原始类型字段:`str`、`int`、`float`、`bool`,或字符串的 `Literal`(会变成 `enum`)。在模型里再放一个模型,`ctx.elicit` 会在任何东西发给客户端之前抛出异常:
征询的模式不如工具的输入模式表达力强。只能是扁平的原始类型字段:`str`、`int`、`float`、`bool`,或字符串的 `Literal`(会变成 `enum`)。在模型里再放一个模型,`ctx.elicit` 会在任何东西发给客户端之前抛出异常。工具调用以 `Error executing tool <name>` 失败,原因记在你的服务器日志里:
```text
TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition
@ -104,7 +104,7 @@ translation:
拒绝不是错误。由工具决定拒绝意味着什么(这里是不订位),然后正常回答模型。
!!! tip
答案在你的代码看到之前就已按你的模型验证过。一个给 `bool` 字段发来 `"maybe"` 的客户端不会弄坏你的订位:调用以模式不匹配的错误失败,你的 `if` 根本不会执行。
答案在你的代码看到之前就已按你的模型验证过。一个给 `bool` 字段发来 `"maybe"` 的客户端不会弄坏你的订位:`ctx.elicit` 抛出 `ValueError`,调用失败,你的 `if` 根本不会执行。
## 把用户引到一个 URL {#send-the-user-to-a-url}

View file

@ -1,6 +1,6 @@
---
translation:
sections: [c93a3e1aefd77955, 7851abd5ec54393b, f49d1ca2f330f9cd, c03764bd9dfeef7b, 4a0391691a674ae4, 2df5cd279eabf9f5]
sections: [c93a3e1aefd77955, 7851abd5ec54393b, f49d1ca2f330f9cd, 4cc0a00347c3f534, 4a0391691a674ae4, 2df5cd279eabf9f5]
tool: 1
---
# 日志 {#logging}
@ -49,6 +49,8 @@ MCP 在协议层面有一个**日志能力**(logging capability):服务器
`logging.basicConfig()` 永远不会替换已经存在的 handler。如果你在创建服务器之前自己配置了日志,以你的配置为准。
也不需要只为了记录失败而在每个处理函数里都写 `try`/`except`。工具或资源函数抛出异常时,SDK 会替你记录下来。记录了什么、用哪个级别,详见 **[处理错误](../servers/handling-errors.md#any-other-exception)**。
## 试一试 {#try-it}
用 MCP Inspector 运行服务器:

View file

@ -1,6 +1,6 @@
---
translation:
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 7a53ead3e704a7f0, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
tool: 1
---
# 运行服务器 {#running-your-server}
@ -70,7 +70,7 @@ Inspector 做的事和真实宿主完全一样:它把 `server.py` 作为子进
* `streamable_http_path`:MCP 端点所在的路径。默认 `/mcp`。
* `json_response=True`:用单个 JSON 正文回应每个 POST,而不是 SSE 流。这个正文只容得下响应本身,别的什么都放不下,所以在请求中途回调客户端的工具(`ctx.elicit()`、采样(sampling))会在这一段抛出 `NoBackChannelError`;与进行中的调用绑定的通知(`ctx.report_progress()` 的进度、每次调用的日志消息)会被丢弃;独立的 `GET` 流仍然承载与之无关的通知。
* `stateless_http=True`:每个请求一个全新的传输,不跟踪会话。
* `max_request_body_size`:接受的最大 POST 正文大小,单位为字节。默认 4 MiB;更大的请求在解析或创建会话之前就会收到 HTTP 413。只有当合法的 MCP 消息确实超过这个大小时才调高它。
* `max_request_body_size`:接受的最大请求正文大小,单位为字节。默认 4 MiB;更大的请求在解析或创建会话之前就会收到 HTTP 413。只有当合法的 MCP 消息确实超过这个大小时才调高它。
* `event_store`、`retry_interval`、`transport_security`:可恢复性和 DNS 重绑定防护。它们可以先放一放,等部署到 localhost 以外的地方再说;**[部署与扩展](deploy.md)** 介绍了 `transport_security`。
!!! warning

View file

@ -1,13 +1,13 @@
---
translation:
sections: [e33d441f12d50535, 7099694c603e0f5f, c1df4cf9673433e6, c9cd294541422e6e, 6cec073617bfd037, efa92b8f99e908c8, 6a22a29e27fb4601]
sections: [7be05607887e6853, e7375894888d9750, c36f73fc7e3af13b, 2fec2d7e129e62fe, 809b0e0a7c27295a, b4395a04d2a5d906, 1a436007f5f54779, c6b2078ed1e63ba5]
tool: 1
---
# 错误处理 {#handling-errors}
工具失败有两种方式,SDK 对它们的处理截然不同。
工具失败有三种方式,SDK 对每一种的处理都不一样。
抛出普通异常,看到它的是**模型**。抛出 `MCPError`,看到它的是**协议**。
抛出 `ToolError`,看到你消息的是**模型**。抛出 `MCPError`,看到它的是**协议**。抛出其他任何东西就是崩溃:模型只知道调用失败了,traceback 进你的日志。
这一页讲的就是怎么选。
@ -15,11 +15,11 @@ translation:
拿一个查东西的工具来说,让它查不到:
```python title="server.py" hl_lines="11-12"
```python title="server.py" hl_lines="2 12-13"
--8<-- "docs_src/handling_errors/tutorial001.py"
```
这两行没有任何 MCP 特有的东西。`get_author` 抛出一个普通的 `ValueError`,和任何 Python 函数一样。
`ToolError` 来自 `mcp.server.mcpserver.exceptions`,是工具告诉模型出了问题的方式。
用一个书目里没有的书名去调用它,看看结果:
@ -30,19 +30,21 @@ result.structured_content # None
```
* 请求**成功了**。有一个结果;调用方这边什么也没抛出。
* `is_error` 为 `True`,你的异常消息(前面加了工具名)就在 `content` 里,正是模型读取的位置。
* `is_error` 为 `True`,你的消息(前面加了工具名)就在 `content` 里,正是模型读取的位置。
* `structured_content` 为 `None`。失败的调用没有返回值可供结构化。
这就是**工具错误**,也是工具抛出的**任何**异常的默认归宿。而且它几乎总是你想要的效果。
这就是**工具错误**,而且它几乎总是你想要的效果。
调用工具的是模型,参数也是它挑的。所以工具错误就是对话里的一个回合:模型读到“No book titled 'Nothing' in the catalog.”,发现自己猜错了书名,就换个更好的再调一次。你只写了一个 `raise`,就得到了一个会自我纠正的智能体。
在服务器上,一个 `ToolError` 就是日志里的一行 `INFO`,没有 traceback。这是你预料之中的,所以没什么可查的。
!!! tip
永远不要从工具里 `return` 错误消息。返回的字符串带的是 `is_error=False`,所以在模型(以及每个客户端 UI)看来,工具运行正常,那个字符串就是答案。要 `raise`。这个标志才是信号。
## 模型纠正不了的错误 {#an-error-the-model-cannot-fix}
现在把 `ValueError` 换成 `MCPError`。
现在把 `ToolError` 换成 `MCPError`。
```python title="server.py" hl_lines="1 3 14"
--8<-- "docs_src/handling_errors/tutorial002.py"
@ -74,16 +76,35 @@ result.structured_content # None
两条路径回答的是两个不同的问题。
* **抛出任意异常**,对应**执行**层面的失败:工具想做的事没做成。调用是模型选的,所以后果也该让模型看到,给它补救的机会。拼错的书名、超时的上游 API、不存在的数据行:全是工具错误。
* **抛出 `ToolError`**,对应**执行**层面的失败:工具想做的事没做成。调用是模型选的,所以后果也该让模型看到,给它补救的机会。拼错的书名、超时的上游 API、不存在的数据行:全是工具错误。
* **抛出 `MCPError`**,对应**请求本身**就该被拒绝的情况:客户端缺少工具所依赖的某项能力,服务器当前的状态没法为任何人服务,调用方跳过了某个必需步骤。这些问题模型怎么重试都修不好,所以把消息交给它没有任何好处。
一个问题就能定夺:**换个更聪明的模型,能避免这个问题吗?** 能 -> 普通异常。不能 -> `MCPError`。
一个问题就能定夺:**换个更聪明的模型,能避免这个问题吗?** 能 -> `ToolError`。不能 -> `MCPError`。
按这个标准,第二版 `get_author` 选错了:换个更好的书名就能解决,所以模型理应看到那条消息。它放在这里是为了让你看清机制,而不是推荐这种写法。
!!! info
`MCPError` 通过 `from mcp import MCPError` 导入,接受 `code`、`message` 和可选的 `data` 载荷。你往里放什么,客户端就收到什么:SDK 会把抛出的 `MCPError` 原样转发,不做任何清理。
## 任何其他异常 {#any-other-exception}
现在把检查去掉,让字典查找自己失败:
```python title="server.py" hl_lines="11"
--8<-- "docs_src/handling_errors/tutorial004.py"
```
`CATALOG[title]` 抛出 `KeyError`。这不在你的计划之内,所以 SDK 把它当作崩溃:
```python
result.is_error # True
result.content # [TextContent(text="Error executing tool get_author")]
```
调用仍然返回 `is_error=True`,所以模型知道它失败了,可以继续往下走。它拿不到的是异常的文本:你代码里的一个 `KeyError`,或者隔着三层库的某个驱动吐出的一堆 SQL,都可能暴露服务器的内部细节,所以它永远不会离开服务器。
拿到它的是你。服务器以 `ERROR` 级别记录这次崩溃,附带完整的 traceback,消息是 `Tool 'get_author' raised an unexpected exception`。所以一个设在 `WARNING` 级别的生产日志,遇到每个 `ToolError` 都保持安静,一旦真有东西坏了就会出声。
## 不存在的资源 {#a-resource-that-doesnt-exist}
资源也划出同样的界线,并为常见情况自带了一个具名异常。
@ -104,7 +125,7 @@ result.structured_content # None
}
```
注意这里没有 `is_error=True` 式的“半个结果”。资源读取要么返回内容,要么失败:资源只有协议这一条路径。模板以及资源的其他方方面面,详见 **[资源](resources.md)**。
注意这里没有 `is_error=True` 式的“半个结果”。资源读取要么返回内容,要么失败:资源只有协议这一条路径。`ResourceError` 是同样的东西,用于不属于“未找到”的失败(`-32603`,带你的消息),两者在日志里都是一行 `INFO`。除 `MCPError` 之外的任何其他异常都是崩溃:客户端收到只写明 URI 的 `-32603`,traceback 以 `ERROR` 级别进你的日志。模板以及资源的其他方方面面,详见 **[资源](resources.md)**。
## 你永远不用抛的错误 {#errors-you-never-raise}
@ -115,16 +136,17 @@ result.structured_content # None
这意味着有一整类 `raise` 语句不用你写:不要重复校验自己的类型注解。
!!! info
这一页上的一切都是**客户端**看到的样子,而你写测试时用的内存中的 `Client` 看到的也一模一样。就连 `raise_exceptions=True` 也不会把工具错误变回 traceback:等那个标志能起作用的时候,你的异常早已是 `is_error=True` 的结果了。对结果做断言。这个模式详见 **[测试](../get-started/testing.md)**。
这一页上**客户端**看到的一切,你写测试时用的内存中的 `Client` 看到的也一模一样。就连 `raise_exceptions=True` 也不会把失败工具的异常交还给调用方:等那个标志能起作用的时候,你的异常早已是 `is_error=True` 的结果了。对结果做断言。如果需要崩溃的 traceback,它在服务器的日志里,pytest 的 `caplog` 能捕获到。这个模式详见 **[测试](../get-started/testing.md)**。
## 回顾 {#recap}
* 在工具里抛出**任意异常** -> 调用返回 `is_error=True`,你的消息在 `content` 里。模型读到后可以重试。这是默认行为。
* 在工具里抛出 **`ToolError`** -> 调用返回 `is_error=True`,你的消息在 `content` 里。模型读到后可以重试。
* 抛出 **`MCPError`** -> 调用本身以 JSON-RPC 错误失败。模型什么也看不到;由宿主处理。`code`、`message` 和 `data` 原封不动地保留。
* 决定性的问题:“换个更聪明的模型,能避免这个问题吗?”能 -> 异常。不能 -> `MCPError`。
* 决定性的问题:“换个更聪明的模型,能避免这个问题吗?”能 -> `ToolError`。不能 -> `MCPError`。
* 任何**其他异常**都是崩溃 -> `is_error=True`,模型只看到 `Error executing tool <name>`,你拿到一条带 traceback 的 `ERROR` 记录。
* 资源处理函数抛出 `ResourceNotFoundError` -> 协议的 `-32602`,URI 在 `data` 里。
* 不合法的参数在你的函数运行之前就会对照模式被拒掉;这些不用你 `raise`。
* `from mcp import MCPError`;错误码常量来自 `mcp.types`。
* 导入:`from mcp import MCPError`、`from mcp.server.mcpserver.exceptions import ToolError, ResourceError, ResourceNotFoundError`,以及来自 `mcp.types` 的错误码常量。
错误处理完毕。服务器**对外暴露**的内容就是这些。每个处理函数在运行期间能读到什么、又能反过来对客户端做什么,是下一部分的内容:**[在处理函数内部](../handlers/index.md)**。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 043f526230dd243d, 6ee3e9bcfd24047a]
sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 864137b5e9c61e91, 043f526230dd243d, db1ef91db7d6b3f3]
tool: 1
---
# 媒体 {#media}
@ -81,6 +81,24 @@ result.structured_content # None
!!! check
用 `data=` 时没有文件名,也就无从推断。漏掉 `format=`,SDK 就会回退到默认值:图片是 `image/png`,音频是 `audio/wav`。照这样用 MP3 字节构建一个 `Audio`,客户端会被告知 `mime_type="audio/wav"`,然后老老实实地解码失败。传 `data=` 时,就要一并传 `format=`。
## 嵌入资源 {#embedding-a-resource}
工具还可以返回一份文档:一段文本或字节,连同它所在的 URI 和一个 MIME 类型。这就是 **`EmbeddedResource`**,另一种内容块。和普通的 `str` 不同,它会告诉客户端内容是什么,客户端因此可以把它作为附件显示,或者认出这是一个它已经知道的资源。
```python title="server.py" hl_lines="7 14 16-18"
--8<-- "docs_src/media/tutorial005.py"
```
* `brand://guidelines` 是一个普通的资源(**[资源](resources.md)** 讲的就是这些)。工具按请求把同一份文档交给模型,直接调用 `guidelines()` 能保持唯一的事实来源。
* `EmbeddedResource` 和 `TextResourceContents` 来自 `mcp.types`。这里没有像图片那样的辅助类型:你构建的块原封不动地放进结果,也没有 `structured_content`。
* 使用资源注册时所用的 URI,这样客户端才能分辨出附件和 `brand://guidelines` 是同一份文档。任何 URI 都合法,不管注册过没有。
```python
result.content # [EmbeddedResource(type="resource", resource=TextResourceContents(uri="brand://guidelines", mime_type="text/markdown", text="# Brand guidelines\n\n..."))]
```
二进制内容用 `BlobResourceContents(uri=..., mime_type=..., blob=...)` 代替 `TextResourceContents`,把字节 base64 编码后放进 `blob`。如果只想发送一个指针,让客户端之后再 `resources/read`,就改为返回 `ResourceLink(name=..., uri=...)`;它同样是一个内容块。
## 图标 {#icons}
`Icon` 是元数据,不是内容。它不携带图片本身,而是用一个 URI 指向图片;客户端可以获取它,并显示在服务器名称、某个工具、资源或提示词旁边。
@ -110,6 +128,7 @@ client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_
* 从工具返回 `Image` 或 `Audio`,客户端就会收到一个 `ImageContent` / `AudioContent` 块:字节经 base64 编码,附带 MIME 类型。
* 可以用 `path=` 构建,让后缀决定 MIME 类型;也可以用内存中的 `data=` 加上显式的 `format=` 构建。
* 返回 `EmbeddedResource` 可以把一份文档(文本或 base64 blob,附带 URI 和 MIME 类型)放进结果;返回 `ResourceLink` 则只发送指针。
* 媒体结果不带 `structured_content`,也没有输出模式。
* `Icon` 是一个指针:一个 `src` URI,加上可选的 `mime_type`、`sizes` 和 `theme`。
* `icons=[...]` 可用于服务器、工具、资源和提示词,客户端在对应的对象上就能找到它们。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, ffe823cb0fedd488, f33651add1b59094]
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, d30d3c20168b88b2, f5ef38dad59d6f76, 6e38a699ba57fbdf, 2b984a3bf37a0ddd]
tool: 1
---
# 提示词 {#prompts}
@ -137,7 +137,52 @@ uv run mcp dev server.py
```
!!! info
如果读过 **[工具](tools.md)**,这一页的内容你其实都已经会了。装饰器一样,用 docstring 作描述一样,`Annotated`/`Field` 也一样。变的只有两点:由谁触发(用户),以及结果去哪儿(进入对话)。
如果读过 **[工具](tools.md)**,到这里为止的内容你其实都已经会了。装饰器一样,用 docstring 作描述一样,`Annotated`/`Field` 也一样。变的只有两点:由谁触发(用户),以及结果去哪儿(进入对话)。
## 不止文本 {#more-than-text}
`UserMessage` 和 `AssistantMessage` 在接受 `str` 的地方,也接受内容块,或者 `Image` / `Audio` 辅助类。提示词里常见两种情况:附上一份文档,和附上一张图片。
### 嵌入文件 {#embedding-a-file}
```python title="server.py" hl_lines="5 12 21 23"
--8<-- "docs_src/prompts/tutorial004.py"
```
* 风格指南是位于 `style://python` 的资源(**[资源](resources.md)** 会介绍这类东西),从 `server.py` 旁边的 `style-guide.md` 读取。在那里放任意一个 Markdown 文件即可。
* `EmbeddedResource(resource=TextResourceContents(...))` 两者都来自 `mcp.types`,它把文件连同 URI 和 MIME 类型一起作为第一条消息携带;引用它的请求以纯文本形式跟在后面。
* 用嵌入,而不是把指南直接贴进 f-string,客户端就能把它显示为附件,之后还能重新打开 `style://python`,模型收到的也是原封不动的文件。二进制文件用 `BlobResourceContents`,带一个 base64 的 `blob`。
渲染后,第一条消息的 `content` 是一个 `resource` 块:
```json
{"type": "resource", "resource": {"uri": "style://python", "mimeType": "text/markdown", "text": "* Prefer early returns.\n..."}}
```
### 附上图片 {#attaching-an-image}
```python title="server.py" hl_lines="4 15"
--8<-- "docs_src/prompts/tutorial005.py"
```
* `Image` 是 **[图片、音频和图标](media.md)** 里的辅助类。提示词渲染时,`UserMessage` 把它转换成一个 `ImageContent` 块(文件经 base64 编码,MIME 类型从 `.png` 推断);`Audio` 同样会变成 `AudioContent`。
* 在 `server.py` 旁边放任意一个名为 `architecture.png` 的 PNG。提示词参数都是字符串,所以图片总是来自服务器;`component` 只提供文字。
```json
{"type": "image", "data": "iVBORw0KGgoAAAANSUhEUg...", "mimeType": "image/png"}
```
## 在运行时修改列表 {#changing-the-list-at-runtime}
客户端连接期间也可以添加提示词,比如让用户把一条指令保存成自己的菜单项。先注册提示词,再发通知:
```python title="server.py" hl_lines="5 23-27"
--8<-- "docs_src/prompts/tutorial006.py"
```
* `mcp.add_prompt(Prompt.from_function(fn, name=..., description=...))` 注册函数的方式和 `@mcp.prompt()` 完全一样,`mcp.remove_prompt(name)` 则相反。`add_prompt` 遇到同名的现有条目会保留它而不是覆盖,所以这个工具先删掉旧条目,让保存变成替换。`prompts/list` 立即反映这一变化。
* `await ctx.notify_prompts_changed()` 向每个在 `subscriptions/listen` 流上监听的 `2026-07-28` 客户端发送 `notifications/prompts/list_changed`(**[订阅](../handlers/subscriptions.md)**)。当发起调用的客户端是 2026 之前的版本时,`await ctx.session.send_prompt_list_changed()` 把它发给这个客户端(**[服务旧版客户端](../run/legacy-clients.md)**)。两个都调用;没有人可通知时,它们各自什么也不做。
* 收到通知的客户端会再次调用 `prompts/list`。在 Python `Client` 里就是 `async with client.listen(prompts_list_changed=True) as sub:`,它会产出一个 `PromptsListChanged` 事件。
## 回顾 {#recap}
@ -147,5 +192,7 @@ uv run mcp dev server.py
* 返回 `str`,它就变成一条用户消息。返回 `UserMessage` / `AssistantMessage` 的列表,可以为多轮对话铺好开头。
* `title=` 和 `Field(description=...)` 是客户端放进 UI 里的内容。
* 缺少必填参数会让整个请求失败。没有针对单个提示词的错误结果。
* 把 `EmbeddedResource` 或 `Image` 包进 `UserMessage`,就能附上文档或图片。
* 运行时用 `mcp.add_prompt(...)` / `mcp.remove_prompt(...)` 添加或移除提示词,然后 `await ctx.notify_prompts_changed()` 和 `await ctx.session.send_prompt_list_changed()`。
要在服务器端为提示词(或资源模板)的参数提供自动补全,见 **[补全](completions.md)**。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, e866c192e11d1c14, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c6f7d2a148aa49f4, c851964bb3301907, d715db6f8dccc9cc, ef86634aa70498a7]
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4]
tool: 1
---
# 结构化输出 {#structured-output}
@ -103,7 +103,7 @@ result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions
--8<-- "docs_src/structured_output/tutorial003.py"
```
`TypedDict` 在运行时就是普通的 `dict`,所以构建并返回的也就是它。模式、校验和 `structured_content` 都与 `BaseModel` 版本完全相同(只是少了描述,`TypedDict` 里没有地方写)。
`TypedDict` 在运行时就是普通的 `dict`,所以构建并返回的也就是它。模式、校验和 `structured_content` 遵循与 `BaseModel` 版本相同的规则:加上类的 docstring 或 `Annotated[..., Field(description=...)]`,它们就成为描述;dict 里省略不写的 `NotRequired` 键也不会出现在 `structured_content` 中。
## dataclass {#a-dataclass}
@ -185,16 +185,16 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
注解承诺的是 `WeatherData`,上游响应却不再发送 `humidity` 了。
!!! check
调用 `get_weather`,它不会悄悄把一个缺了一半的对象递给客户端。调用会失败,错误的头几行直接点名那个字段:
调用 `get_weather`,它不会悄悄把一个缺了一半的对象递给客户端。调用会失败:客户端收到 `is_error=True` 和 `Error executing tool get_weather`,于是模型知道调用失败了,而不会信心十足地去读根本不存在的天气数据。字段名是留给你看的,记录在服务器日志的 `ERROR` 级别:
```text
Error executing tool get_weather: 1 validation error for WeatherData
Tool 'get_weather' raised an unexpected exception
...
pydantic_core._pydantic_core.ValidationError: 1 validation error for WeatherData
humidity
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
```
这段文本作为工具结果返回,并带着 `is_error=True`,于是模型知道调用失败了,而不会信心十足地去读根本不存在的天气数据。
顺带一提,从 `-> WeatherData` 的工具里返回普通 `dict` 完全没问题。`json.loads` 产出的正是它。校验针对的是值,而不是 Python 类型。
## 选择退出 {#opting-out}
@ -209,6 +209,10 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
反过来,`structured_output=True` 会把自动检测变成硬性要求:返回类型产不出模式的工具会在导入时直接抛错,而不是退回到纯文本。
## 内容块与媒体 {#content-blocks-and-media}
内容块与媒体(`TextContent`、`EmbeddedResource`、`Image`、`Audio` 等等,无论是单独返回、作为 `list`、`tuple` 或 `Sequence` 的元素,还是作为联合类型的分支)已经替你退出了:它们是给模型读的,所以自动检测不会从中推导出模式(`Image` 和 `Audio` 详见 **[图像、音频与图标](media.md)**)。对内容块类,`structured_output=True` 仍然会强制生成一个模式。
## 没有类型提示的类 {#a-class-without-type-hints}
有一种情况,你没有要求也会落得非结构化:返回一个**类体上没有任何注解**的类。
@ -237,6 +241,6 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
* 标量、列表、元组和联合类型会被包装进 `{"result": ...}`。模型、`TypedDict`、dataclass、带注解的类以及 `dict[str, ...]` 本身已是对象,保持原样。
* 每个结果都同时带有 `content`(文本,给模型)**和** `structured_content`(数据,给应用程序)。
* 返回的内容会对照模式校验。不匹配就是工具错误,而不是一个损坏的结果。
* `structured_output=False` 让工具退出结构化输出。没有类型提示的类会悄无声息地退出;要当心。
* `structured_output=False` 让工具退出结构化输出。内容块、`Image` 和 `Audio` 默认退出;没有类型提示的类会悄无声息地退出,要当心。
至此,工具能回传的一切都由你掌控。接下来是第二种原语:**[资源](resources.md)**。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [e4cc390d56573409, 8566e2b68594e9ad, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363]
sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363]
tool: 1
---
# 工具 {#tools}
@ -39,6 +39,8 @@ SDK 根据这些类型提示生成一份 JSON Schema,并在 `tools/list` 时
两个参数都在 `required` 里,因为都没有默认值。这一点马上就会改。(`title` 键是 Pydantic 附带生成的;属性、属性的类型和 `required` 才是契约。)
也没有 `$schema` 键:不带这个键的模式,MCP 一律按 **JSON Schema 2020-12** 处理,而 Pydantic 生成的正是它,所以在你到 **[底层 Server](../advanced/low-level-server.md#the-dialect-is-json-schema-2020-12)** 上手写模式之前,没有什么需要选择的。
!!! tip
类型提示在这里不是文档,而是**契约**。如果客户端发来 `"limit": "ten"`,SDK 会在你的函数运行之前就把它拒掉。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 101ef9d14bf4ec46, 4b6c4a845438abc7, f98b46bafbee4acd]
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 317f4256a650cab6, 4b6c4a845438abc7, f98b46bafbee4acd]
tool: 1
---
# URI 模板与路径安全 {#uri-templates-and-path-safety}
@ -98,7 +98,7 @@ translation:
内置检查能拦住常见情况,但无从知道你的沙箱边界在哪。访问文件系统时,用 `safe_join` 解析路径并确认它仍在基础目录之内:
```python title="server.py" hl_lines="4 14"
```python title="server.py" hl_lines="5 15"
--8<-- "docs_src/uri_templates/tutorial002.py"
```
@ -124,10 +124,10 @@ translation:
| `reject_null_bytes` | `True` | 拒绝包含 `\x00` 的值 |
| `exempt_params` | 空 | 要跳过检查的参数名 |
这些检查只是启发式的预过滤;访问文件系统时,`safe_join` 仍然是真正的隔离边界。
这些检查只是启发式的预过滤;访问文件系统时,`safe_join` 仍然是隔离边界。
!!! tip
如果处理函数无法完成请求(文件不存在、id 未知),就抛出异常。SDK 会把它变成错误响应。协议错误和工具错误的区别见 **[处理错误](handling-errors.md)**。
如果处理函数无法完成请求(文件不存在、id 未知),就像上面的 `read_manual` 那样抛出 `ResourceNotFoundError`。客户端会收到 `-32602`,附带你的消息和 URI。意料之外的异常则会变成通用的 `-32603`。见 **[处理错误](handling-errors.md#a-resource-that-doesnt-exist)**。
## 底层 Server 上的资源 {#resources-on-the-low-level-server}

View file

@ -1,6 +1,6 @@
---
translation:
sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 4835ed1772f1d113, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, 954dc21efdb532a3]
sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e]
tool: 1
---
# 故障排查 {#troubleshooting}
@ -79,11 +79,11 @@ async def main() -> None:
`__aexit__` 就是断开连接,所以不存在会忘记调用的 `client.close()`。**[测试](get-started/testing.md)** 正是建立在这个模式之上。
## `Error executing tool <name>: <message>` 和 `Unknown tool: <name>` {#error-executing-tool-name-message-and-unknown-tool-name}
## `Error executing tool <name>: <message>`、`Error executing tool <name>` 和 `Unknown tool: <name>` {#error-executing-tool-name-message-error-executing-tool-name-and-unknown-tool-name}
你看到的是一个**结果**,不是异常。`call_tool` 没有抛异常,而且对于失败的工具它永远不会抛。
用服务器不认识的城市调用 `forecast`,它抛出的异常会随着一个标记为**成功**的请求返回:
用服务器不认识的城市调用 `forecast`,它抛出的 `ToolError` 会随着一个标记为**成功**的请求返回:
```python
result.is_error # True
@ -95,6 +95,8 @@ result.structured_content # None
修复在客户端:**检查 `result.is_error`**。包在 `call_tool` 外面的 `try/except` 一个也抓不到,因为根本没有东西可抓。这是有意为之,也是本页最值得记住的一点:调用是**模型**选的,所以消息交给模型,让它有机会重试。详见 **[处理错误](servers/handling-errors.md)**,包括**确实**会抛异常的 `MCPError` 路径。
不带消息的裸形式 `Error executing tool <name>` 表示工具**崩溃了**:一个它没有预料到的异常逃逸了出来(或者返回值没有通过输出模式校验),而那个异常的文本不会放到线路上。traceback 在**服务器日志**里,级别是 `ERROR`,内容是 `Tool '<name>' raised an unexpected exception`。
## `TypeError: The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool` {#typeerror-the-tool-decorator-was-used-incorrectly-did-you-forget-to-call-it-use-tool-instead-of-tool}
你写的是 `@mcp.tool` 而不是 `@mcp.tool()`。`tool()` 是一个装饰器**工厂**:没有括号的话,Python 会把你的函数传给它的 `name=` 参数。
@ -393,7 +395,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key
## 回顾 {#recap}
* `ExceptionGroup: unhandled errors in a TaskGroup` 从来都不是真正的错误。读**最后一行**;在 `async with Client(...)` 块**内部**捕获 `MCPError` 可以完全跳过这层包装。
* `call_tool` 不会因为工具失败而抛异常。`Error executing tool ...` 和 `Unknown tool: ...` 是结果:检查 `result.is_error`。
* `call_tool` 不会因为工具失败而抛异常。`Error executing tool ...` 和 `Unknown tool: ...` 是结果:检查 `result.is_error`。工具名后面没有消息表示它崩溃了,traceback 在服务器日志里。
* `Client must be used within an async context manager` -> 用 `async with`。`Use @tool() instead of @tool` -> 加上括号。
* 服务器日志里的 `Tool already exists:` 是两个同名工具合并成一个的唯一迹象。
* 一个 421,三种写法:`Server returned an error response`(python `Client`)、`421 Misdirected Request` / `Invalid Host header`(其他所有地方)、`Invalid Host header: <host>`(服务器日志)。修复:`transport_security=TransportSecuritySettings(allowed_hosts=[...])`。

View file

@ -1,6 +1,6 @@
---
translation:
sections: [cfe01c0c5863dfa2, 11d93f1fa09eadf5, a7392996acf1ad8f, 875eb2889263424e]
sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e]
tool: 1
---
# v2 的新变化 {#whats-new-in-v2}
@ -41,9 +41,9 @@ v1 交给你的是三层嵌套:一个产出原始流的传输上下文管理
--8<-- "docs_src/client/tutorial001.py"
```
`Client` 接受一个服务器对象(内存直连,没有传输:这就是测试的做法)、一个 URL(Streamable HTTP),或者任意传输上下文管理器,比如 `stdio_client(...)`。进入 `async with` 就会建立连接并协商协议版本,不管服务器讲的是哪一代协议;之后 `client.server_capabilities` 和 `client.protocol_version` 直接就在那里,服务器表明身份时 `client.server_info` 也一样(它现在是 `Implementation | None`,因为 2026 版的身份信息是可选的)。你在 v1 注册的采样和征询回调仍然能用(它们的函数体会看到和本页其他地方一样的 snake_case 属性重命名),现在还会回答 2026 风格的、嵌在结果里的请求(见下文),并且是并发运行而不是一次一个。想要底层接口的人仍然可以用底下的 `ClientSession`,`client.session` 会把它交给你;它也变了(运行在新的调度器引擎上,自身的一些签名也改了),所以下探之前先读 **[迁移指南](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**。
`Client` 接受一个服务器对象(内存直连,没有传输:这就是测试的做法)、一个 URL(Streamable HTTP)、一个 `StdioServerParameters`(stdio 子进程),或者其他任意传输上下文管理器,比如 `sse_client(...)`。进入 `async with` 就会建立连接并协商协议版本,不管服务器讲的是哪一代协议;之后 `client.server_capabilities` 和 `client.protocol_version` 直接就在那里,服务器表明身份时 `client.server_info` 也一样(它现在是 `Implementation | None`,因为 2026 版的身份信息是可选的)。你在 v1 注册的采样和征询回调仍然能用(它们的函数体会看到和本页其他地方一样的 snake_case 属性重命名),现在还会回答 2026 风格的、嵌在结果里的请求(见下文),并且是并发运行而不是一次一个。想要底层接口的人仍然可以用底下的 `ClientSession`,`client.session` 会把它交给你;它也变了(运行在新的调度器引擎上,自身的一些签名也改了),所以下探之前先读 **[迁移指南](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**。
**[Client](client/index.md)** 介绍它,**[客户端传输](client/transports.md)** 讲三种连接形式,**[客户端回调](client/callbacks.md)** 讲回调本身,**[测试](get-started/testing.md)** 展示取代 v1 `create_connected_server_and_client_session()` 辅助函数的内存模式。
**[Client](client/index.md)** 介绍它,**[客户端传输](client/transports.md)** 讲四种连接形式,**[客户端回调](client/callbacks.md)** 讲回调本身,**[测试](get-started/testing.md)** 展示取代 v1 `create_connected_server_and_client_session()` 辅助函数的内存模式。
### 底层 `Server` 是重建,不是改名 {#the-low-level-server-was-rebuilt-not-renamed}
@ -129,7 +129,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentB
重命名会自己跳出来提醒你。下面这些不会:
* **同步函数在工作线程上运行。** `def` 定义的工具(或资源、提示词、解析器)不再阻塞事件循环;代价是它的函数体不再 **在** 事件循环线程上运行,这对有线程亲和性的代码有影响。`async def` 处理函数不受影响。**[迁移指南](migration.md#sync-handler-functions-now-run-on-a-worker-thread)**。
* **在工具内部抛出的 `MCPError`(v1 的 `McpError`)现在是协议错误。** 模型永远看不到它。其他所有异常仍然会变成模型能读到并做出反应的 `is_error=True` 结果。两者的划分见 **[错误处理](servers/handling-errors.md)**。
* **在工具内部抛出的 `MCPError`(v1 的 `McpError`)现在是协议错误。** 模型永远看不到它。其他所有异常仍然会变成 `is_error=True` 结果,但只有 `ToolError` 的消息能到达模型:其他任何异常现在显示为 `Error executing tool <name>`,traceback 则写进你的服务器日志。两者的划分见 **[错误处理](servers/handling-errors.md)**。
* **结果在发出之前会被校验。** 手工构建的 `Tool` 如果 `input_schema` 是 `{}`,现在会让 `tools/list` 失败(规范要求 `"type": "object"`)。基于 `@mcp.tool()` 构建的服务器永远不会遇到这个;它们的模式是 SDK 写的。
* **你的客户端会校验收到的东西。** `list_tools()` 和 `call_tool()` 会按协商好的协议版本检查服务器的答复,所以 v1 宽松解析能容忍的不太合规的服务器,现在会抛 `pydantic.ValidationError`。如果你连接的是自己不控制的服务器,要做好由你来发现它们的准备;细节见 **[迁移指南](migration.md#client-validates-inbound-traffic-against-the-protocol-schema)**。
* **URI 模板现在是真正的 RFC 6570。** `{+path}`、`{?query}` 之类都能用,匹配是精确的而不是正则式的宽松匹配,提取出的值里的路径穿越默认会被拒绝。更严格的模板在装饰时就失败,而不是等到第一个请求。**[URI 模板](servers/uri-templates.md)**。