docs: refresh translations for recent English changes (#3379)
This commit is contained in:
parent
d8b638304e
commit
c5d7d0b6d0
6 changed files with 3149 additions and 1047 deletions
|
|
@ -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)**。
|
||||
|
||||
|
|
|
|||
|
|
@ -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=[...]` 可用于服务器、工具、资源和提示词,客户端在对应的对象上就能找到它们。
|
||||
|
|
|
|||
|
|
@ -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)**。
|
||||
|
|
|
|||
|
|
@ -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)**。
|
||||
|
|
|
|||
|
|
@ -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 会在你的函数运行之前就把它拒掉。
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue