docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,122 @@
---
translation:
sections: [72f9c964769076dd, 9a2c14e10935b515, 235299eb78ab12d7, 8aee1e78c8237fb8, 9bd86acd4112138f, 55343cb7f250dc7b]
tool: 1
---
# 补全 {#completions}
在你的服务器之上构建 UI 的客户端,会想在用户输入时自动补全参数值:语言名称、仓库名称、文件路径。
**补全**(completion)就是服务器提供这些建议的方式。
## 值得补全的东西 {#something-worth-completing}
补全只适用于两样东西:**提示词**的参数和**资源模板**的参数。所以先写一个两者各有一个的服务器:
```python title="server.py" hl_lines="6 12"
--8<-- "docs_src/completions/tutorial001.py"
```
这里还没有任何与补全相关的内容。
* `review_code` 接受一个 `language`。用户不该靠猜来知道你接受哪些写法。
* `github_repo` 接受 `owner` 和 `repo`。两个都用自由文本框,这个表单会很难用。
## 补全处理函数 {#the-completion-handler}
添加**一个**用 `@mcp.completion()` 装饰的函数:
```python title="server.py" hl_lines="21-29"
--8<-- "docs_src/completions/tutorial002.py"
```
* 每个服务器只有一个处理函数。所有补全请求都会落到这里,由你根据正在补全的对象分支处理。
* 它必须是 `async def`:SDK 会 await 它。
* 它接收三个参数:
* `ref`:是**哪一个**提示词或资源模板,类型为 `PromptReference` 或 `ResourceTemplateReference`。用 `isinstance` 区分两者。
* `argument`:`argument.name` 是正在补全的参数,`argument.value` 是用户目前已输入的内容。
* `context`:已经确定的参数。暂时忽略它。
* 返回一个 `Completion(values=[...])`;没有可提供的建议时返回 `None`。
!!! tip
`argument.value` 是用户已输入的前缀。SDK **不会**替你过滤:放进 `values` 的是什么,UI 显示的就是什么。`startswith` 得你自己写。
### 试一试 {#try-it}
用 **[测试](../get-started/testing.md)** 中的内存 `Client` 来驱动它。调用 `client.complete()`,传入 `ref=PromptReference(name="review_code")` 和 `argument={"name": "language", "value": "py"}`:
```python
result.completion.values # ['python']
```
* `ref` 与处理函数收到的引用类型相同。
* `argument` 是一个普通的 dict,只有 `name` 和 `value` 两个键。
发送空的 `value`,会拿回整个列表。`lang.startswith("")` 对每种语言都为真:
```python
result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript']
```
询问 `code`(一个处理函数不认识的参数),它会返回 `None`,SDK 会把它变成空列表:
```python
result.completion.values # []
```
`None` 表示“没有建议”,绝不是错误。UI 会退回到普通的文本框。
## 一项你从未声明过的能力 {#a-capability-you-never-declared}
注册处理函数本身就是声明。连接一个客户端看看:
```python
client.server_capabilities.completions # CompletionsCapability()
```
你没有在任何地方列出 `completions`。SDK 看到处理函数,就替你声明了这项能力。每一项**可选**能力都是这样:处理函数就是声明。(三种原语不是可选的:无论有没有处理函数,`MCPServer` 总会声明它们。)
!!! check
回到第一个 `server.py`(没有处理函数的那个),照样向它发请求。调用会失败,并返回一个 JSON-RPC 错误:
```text
Method not found
```
而且 `client.server_capabilities.completions` 是 `None`。这正是能力的意义所在:行为规范的客户端会先检查它,绝不会发出你无法响应的请求。
## 有依赖关系的参数 {#dependent-arguments}
`github://repos/{owner}/{repo}` 有两个参数,而 `repo` 的有用取值取决于先选了哪个 `owner`。
这就是 `context` 的用处。它携带用户**已经确定**的参数:
```python title="server.py" hl_lines="8-11 34-38"
--8<-- "docs_src/completions/tutorial003.py"
```
* 新分支针对模板的 `repo` 参数触发。
* `context.arguments` 是 `dict[str, str] | None`,保存目前已选定的值(这里是 `owner`)。
* 还没有 `owner`,就没有合理的建议可给,所以处理函数返回 `None`。
客户端通过 `context_arguments=` 发送这些已确定的值。这次 `ref` 是 `ResourceTemplateReference(uri="github://repos/{owner}/{repo}")`。用空的 `value` 请求补全 `repo`,并传入 `context_arguments={"owner": "modelcontextprotocol"}`:
```python
result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector']
```
去掉 `context_arguments=`,同样的调用会返回 `[]`。不知道 owner,处理函数就无从知道该提供哪些仓库。
!!! info
`Completion` 还接受 `total=` 和 `has_more=`。当 `values` 只是更长列表中的一段时设置它们,这样 UI 就能显示“另有 200 项”。大多数处理函数用不到它们。
## 回顾 {#recap}
* 补全是针对**提示词参数**和**资源模板参数**的建议。仅此而已。
* `@mcp.completion()` 注册这唯一的处理函数。它的形式是 `async def (ref, argument, context) -> Completion | None`。
* 根据 `isinstance(ref, ...)` 和 `argument.name` 分支。按 `argument.value` 过滤要自己写。
* `None` 会变成空列表。它绝不是错误。
* `context.arguments` 保存已确定的值;客户端通过 `context_arguments=` 提供它们。
* 一注册处理函数,`completions` 能力就会出现。没有它,请求的结果就是 `Method not found`。
建议在用户还在**填写**提示词或模板时有用;想在工具调用**中途**向用户提问,需要的是 **[征询(elicitation)](../handlers/elicitation.md)**。工具除了文本还能返回什么,见 **[图像、音频和图标](media.md)**。

View file

@ -0,0 +1,131 @@
---
translation:
sections: [e33d441f12d50535, 7099694c603e0f5f, c1df4cf9673433e6, c9cd294541422e6e, 6cec073617bfd037, efa92b8f99e908c8, 6a22a29e27fb4601]
tool: 1
---
# 错误处理 {#handling-errors}
工具失败有两种方式,SDK 对它们的处理截然不同。
抛出普通异常,看到它的是**模型**。抛出 `MCPError`,看到它的是**协议**。
这一页讲的就是怎么选。
## 模型能纠正的错误 {#an-error-the-model-can-fix}
拿一个查东西的工具来说,让它查不到:
```python title="server.py" hl_lines="11-12"
--8<-- "docs_src/handling_errors/tutorial001.py"
```
这两行没有任何 MCP 特有的东西。`get_author` 抛出一个普通的 `ValueError`,和任何 Python 函数一样。
用一个书目里没有的书名去调用它,看看结果:
```python
result.is_error # True
result.content # [TextContent(text="Error executing tool get_author: No book titled 'Nothing' in the catalog.")]
result.structured_content # None
```
* 请求**成功了**。有一个结果;调用方这边什么也没抛出。
* `is_error` 为 `True`,你的异常消息(前面加了工具名)就在 `content` 里,正是模型读取的位置。
* `structured_content` 为 `None`。失败的调用没有返回值可供结构化。
这就是**工具错误**,也是工具抛出的**任何**异常的默认归宿。而且它几乎总是你想要的效果。
调用工具的是模型,参数也是它挑的。所以工具错误就是对话里的一个回合:模型读到“No book titled 'Nothing' in the catalog.”,发现自己猜错了书名,就换个更好的再调一次。你只写了一个 `raise`,就得到了一个会自我纠正的智能体。
!!! tip
永远不要从工具里 `return` 错误消息。返回的字符串带的是 `is_error=False`,所以在模型(以及每个客户端 UI)看来,工具运行正常,那个字符串就是答案。要 `raise`。这个标志才是信号。
## 模型纠正不了的错误 {#an-error-the-model-cannot-fix}
现在把 `ValueError` 换成 `MCPError`。
```python title="server.py" hl_lines="1 3 14"
--8<-- "docs_src/handling_errors/tutorial002.py"
```
`MCPError` 是 SDK 的**协议错误**。它是工具包装层唯一**不**捕获的异常:它会向上传播,整个 `tools/call` 请求以一个 JSON-RPC 错误失败,而不是返回结果。
```json
{
"code": -32602,
"message": "No book titled 'Nothing' in the catalog."
}
```
* **没有结果**。没有 `content`,没有 `is_error`:模型没有任何东西可读。
* 收到这个错误的是**宿主**应用,和工具根本不存在时的情形一样。
* `code`、`message` 和 `data` 原封不动地送达。`INVALID_PARAMS` 就是 `-32602`;`mcp.types` 把它和其他 JSON-RPC 错误码(`INVALID_REQUEST`、`INTERNAL_ERROR`……)作为常量导出,这样你永远不用手写魔法数字。
!!! check
同样的查找,同样没查到,但这次调用在客户端一侧**抛出了异常**,而不是返回:
```text
mcp.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog.
```
第一个版本递给模型一句它能据此应对的话。这个版本什么也没给。对 `get_author` 来说这只会更糟,而这正是下一节要讲的重点。
## 该抛哪一个 {#which-one-to-raise}
两条路径回答的是两个不同的问题。
* **抛出任意异常**,对应**执行**层面的失败:工具想做的事没做成。调用是模型选的,所以后果也该让模型看到,给它补救的机会。拼错的书名、超时的上游 API、不存在的数据行:全是工具错误。
* **抛出 `MCPError`**,对应**请求本身**就该被拒绝的情况:客户端缺少工具所依赖的某项能力,服务器当前的状态没法为任何人服务,调用方跳过了某个必需步骤。这些问题模型怎么重试都修不好,所以把消息交给它没有任何好处。
一个问题就能定夺:**换个更聪明的模型,能避免这个问题吗?** 能 -> 普通异常。不能 -> `MCPError`。
按这个标准,第二版 `get_author` 选错了:换个更好的书名就能解决,所以模型理应看到那条消息。它放在这里是为了让你看清机制,而不是推荐这种写法。
!!! info
`MCPError` 通过 `from mcp import MCPError` 导入,接受 `code`、`message` 和可选的 `data` 载荷。你往里放什么,客户端就收到什么:SDK 会把抛出的 `MCPError` 原样转发,不做任何清理。
## 不存在的资源 {#a-resource-that-doesnt-exist}
资源也划出同样的界线,并为常见情况自带了一个具名异常。
```python title="server.py" hl_lines="2 13"
--8<-- "docs_src/handling_errors/tutorial003.py"
```
`books://{title}` 是一个**模板**。它能匹配**任何**书名,所以“URI 格式正确”和“这本书存在”是两个不同的问题,而第二个只有你的函数能回答。
答案为否时,抛出 `ResourceNotFoundError`。SDK 会把它转成规范为缺失资源指定的那个协议错误:`-32602`,请求的 URI 放在 `data` 里,让客户端知道失败的是**哪一次**读取。
```json
{
"code": -32602,
"message": "No book titled 'Nothing' in the catalog.",
"data": {"uri": "books://Nothing"}
}
```
注意这里没有 `is_error=True` 式的“半个结果”。资源读取要么返回内容,要么失败:资源只有协议这一条路径。模板以及资源的其他方方面面,详见 **[资源](resources.md)**。
## 你永远不用抛的错误 {#errors-you-never-raise}
不合法的参数根本到不了你的函数。
给 `get_author` 传一个不是字符串的 `title`,SDK 会在调用你**之前**就对照输入模式把它拒掉,得到的同样是模型能读懂并改正的那种 `is_error=True` 工具错误。**[工具](tools.md)** 用一个 `Field(le=50)` 约束演示了同样的拒绝。
这意味着有一整类 `raise` 语句不用你写:不要重复校验自己的类型注解。
!!! info
这一页上的一切都是**客户端**看到的样子,而你写测试时用的内存中的 `Client` 看到的也一模一样。就连 `raise_exceptions=True` 也不会把工具错误变回 traceback:等那个标志能起作用的时候,你的异常早已是 `is_error=True` 的结果了。对结果做断言。这个模式详见 **[测试](../get-started/testing.md)**。
## 回顾 {#recap}
* 在工具里抛出**任意异常** -> 调用返回 `is_error=True`,你的消息在 `content` 里。模型读到后可以重试。这是默认行为。
* 抛出 **`MCPError`** -> 调用本身以 JSON-RPC 错误失败。模型什么也看不到;由宿主处理。`code`、`message` 和 `data` 原封不动地保留。
* 决定性的问题:“换个更聪明的模型,能避免这个问题吗?”能 -> 异常。不能 -> `MCPError`。
* 资源处理函数抛出 `ResourceNotFoundError` -> 协议的 `-32602`,URI 在 `data` 里。
* 不合法的参数在你的函数运行之前就会对照模式被拒掉;这些不用你 `raise`。
* `from mcp import MCPError`;错误码常量来自 `mcp.types`。
错误处理完毕。服务器**对外暴露**的内容就是这些。每个处理函数在运行期间能读到什么、又能反过来对客户端做什么,是下一部分的内容:**[在处理函数内部](../handlers/index.md)**。
你最有可能碰到的那些 SDK 错误的原文、各自的含义,以及每个错误一步到位的修复方法,详见 **[故障排查](../troubleshooting.md)**。

View file

@ -0,0 +1,22 @@
---
translation:
sections: [09defc170a0da89d]
tool: 1
---
# 服务器 {#servers}
`MCPServer` 向已连接的客户端暴露三种原语。三者的区别在于由谁决定使用它们:
* **[工具](tools.md)** 是由 **模型** 挑选并调用的动作。大多数人最先想看的就是这一页,而 **[结构化输出](structured-output.md)** 是与之配套的参考页:关于工具返回值结构的一切。
* **[资源](resources.md)** 是由 **应用** 选择读取的只读数据。**[URI 模板](uri-templates.md)** 是与之配套的参考页:完整的寻址语法和路径安全规则。
* **[提示词](prompts.md)** 是由 **人** 通过菜单或斜杠命令按名称调用的消息模板。
除了这三种原语,服务器声明的其余内容如下:
* **[补全](completions.md)** 是针对提示词和资源模板参数的服务器端自动补全。
* **[图像、音频与图标](media.md)** 涵盖工具除文本之外能返回的一切,以及客户端显示在你的服务器旁边的图标。
* **[处理错误](handling-errors.md)** 解释两类错误的区别:一类模型能够从中恢复,另一类绝不能让模型看到。
这里的每一页都自成一体,直接跳到需要的那一页即可。如果还没构建过服务器,先从 **[第一步](../get-started/first-steps.md)** 开始。
你注册的函数 **内部** 会发生什么(`Context`、依赖注入、调用中途向用户索要更多输入),是下一节 **[在处理函数内部](../handlers/index.md)** 的内容。

View file

@ -0,0 +1,117 @@
---
translation:
sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 043f526230dd243d, 6ee3e9bcfd24047a]
tool: 1
---
# 媒体 {#media}
工具能返回的不只是文本。
SDK 自带两个用于二进制结果的辅助类型(**`Image`** 和 **`Audio`**),以及一个 **`Icon`** 类型,用来让服务器、工具、资源和提示词在客户端 UI 中有自己的图标。
## 返回图片 {#returning-an-image}
把返回类型标注为 `Image`,让它指向一个文件,然后返回:
```python title="server.py" hl_lines="8 12 14"
--8<-- "docs_src/media/tutorial001.py"
```
* `Image` 接受 `path`(要读取的文件)或 `data`(原始字节),二者只能取其一。
* 客户端看到的 MIME 类型根据后缀推断:`logo.png` 会被声明为 `image/png`。
* logo 在这里并不特殊。`server.py` 旁边的任何 PNG 都可以:代码渲染出的图表、示意图、照片都行。
`Image` 是 SDK 提供的便利类型,不是协议类型。在线路上,返回值会变成一个 **`ImageContent`** 块(文件字节经 base64 编码,再加上 MIME 类型):
```python
result.content # [ImageContent(type="image", data="iVBORw0KGgoAAAANSUhEUg...", mime_type="image/png")]
result.structured_content # None
```
有两点值得注意:
* `data` 是 base64。你完全没碰过字节;文件是 SDK 读的,编码也是 SDK 做的。
* `structured_content` 是 `None`。`Image` 是给模型看的内容,不是给应用解析的数据:没有输出模式。(对比 **[结构化输出](structured-output.md)**,那里的返回标注**就是**模式。)
!!! info
`ImageContent` 和 `AudioContent` 位于 `mcp.types` 中,紧挨着普通 `str` 结果所变成的那个 `TextContent`(**[工具](tools.md)**)。工具结果是一个内容块列表;`Image` 和 `Audio` 是产出这两种二进制内容块的最简方式。
### 试一试 {#try-it}
把任意一张 PNG 放到 `server.py` 旁边,命名为 `logo.png`,然后运行:
```console
uv run mcp dev server.py
```
打开 **Tools** 标签页,调用 `logo`。结果不是字符串:它是一个 `image` 内容块,Inspector 会把图片渲染出来。从磁盘上的文件到屏幕上的像素,中间的一切都是 SDK 做的。
## 返回音频 {#returning-audio}
`Audio` 的用法完全一样。`logo.png` 留在原处,再在旁边放任意一个 WAV 文件,命名为 `chime.wav`:
```python title="server.py" hl_lines="18-21"
--8<-- "docs_src/media/tutorial002.py"
```
结果是一个 **`AudioContent`** 块:
```python
result.content # [AudioContent(type="audio", data="UklGR...", mime_type="audio/wav")]
result.structured_content # None
```
一样的道理:进去的是磁盘上的文件,出来的是 base64 和 MIME 类型,没有输出模式。
## 字节还是文件 {#bytes-or-a-file}
两个辅助类型也都接受 `data=`(原始字节)来代替 `path=`。这种方式适用于本来就不是来自某个文件的字节——数据库的一列、一个 HTTP 响应、Pillow 刚画出来的东西:
```python title="server.py" hl_lines="14 15"
--8<-- "docs_src/media/tutorial003.py"
```
用 `path=` 时什么都不用声明:文件在构建结果时读取,MIME 类型根据后缀推断:
* `Image`:`.png`、`.jpg`、`.jpeg`、`.gif`、`.webp`。
* `Audio`:`.wav`、`.mp3`、`.ogg`、`.flac`、`.aac`、`.m4a`。
识别不了的后缀会回退到 `application/octet-stream`。
!!! check
用 `data=` 时没有文件名,也就无从推断。漏掉 `format=`,SDK 就会回退到默认值:图片是 `image/png`,音频是 `audio/wav`。照这样用 MP3 字节构建一个 `Audio`,客户端会被告知 `mime_type="audio/wav"`,然后老老实实地解码失败。传 `data=` 时,就要一并传 `format=`。
## 图标 {#icons}
`Icon` 是元数据,不是内容。它不携带图片本身,而是用一个 URI 指向图片;客户端可以获取它,并显示在服务器名称、某个工具、资源或提示词旁边。
```python title="server.py" hl_lines="4-5 7 10 16"
--8<-- "docs_src/media/tutorial004.py"
```
* `src` 是客户端能解析的 URI:`https:`,或者如果想把图标内嵌、免去一次额外获取,就用 `data:` URI。
* `mime_type` 和 `sizes`(`"48x48"`,可缩放格式用 `"any"`)让客户端在你提供多个图标时挑出合适的那个。
* `theme="light"` 或 `theme="dark"` 把图标标记为适用于某一种配色方案。
`MCPServer(...)`、`@mcp.tool()`、`@mcp.resource()` 和 `@mcp.prompt()` 都接受同一个 `icons=[...]` 关键字参数。
### 客户端在哪里看到它们 {#where-a-client-sees-them}
图标跟着它们所装饰的对象一起传递。服务器的图标在客户端连接时送达,挂在 `client.server_info` 上(该字段在 2026 版连接上是可选的,所以先收窄类型):
```python
assert client.server_info is not None # python-sdk servers identify themselves by default
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]
```
工具的图标在 `tools/list` 返回的 `Tool` 对象上,资源的在 `resources/list` 返回的 `Resource` 上,提示词的在 `prompts/list` 返回的 `Prompt` 上。字段一律叫 `icons`。
## 回顾 {#recap}
* 从工具返回 `Image` 或 `Audio`,客户端就会收到一个 `ImageContent` / `AudioContent` 块:字节经 base64 编码,附带 MIME 类型。
* 可以用 `path=` 构建,让后缀决定 MIME 类型;也可以用内存中的 `data=` 加上显式的 `format=` 构建。
* 媒体结果不带 `structured_content`,也没有输出模式。
* `Icon` 是一个指针:一个 `src` URI,加上可选的 `mime_type`、`sizes` 和 `theme`。
* `icons=[...]` 可用于服务器、工具、资源和提示词,客户端在对应的对象上就能找到它们。
这就是工具能放**进**结果里的全部内容。工具**失败**时会发生什么(以及该让谁知道),见 **[处理错误](handling-errors.md)**。

View file

@ -0,0 +1,151 @@
---
translation:
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, ffe823cb0fedd488, f33651add1b59094]
tool: 1
---
# 提示词 {#prompts}
**提示词**是由用户挑选的消息模板。
工具是给模型用的。提示词正好相反:用户在客户端的菜单里(比如斜杠命令或按钮)选一个,填好参数,渲染出来的消息就进入对话,就像是用户自己打出来的一样。
在一个返回文本的函数上加 `@mcp.prompt()`,就声明了一个提示词。
## 第一个提示词 {#your-first-prompt}
```python title="server.py" hl_lines="6-9"
--8<-- "docs_src/prompts/tutorial001.py"
```
SDK 从中读取的三样东西和工具一样:
* **名称**就是函数名:`review_code`。
* 客户端显示的**描述**是 docstring:`Review a piece of code.`
* **参数**来自函数的形参。`code` 没有默认值,所以是必填的。
客户端从 `prompts/list` 拿到的就是这些:
```json
{
"name": "review_code",
"description": "Review a piece of code.",
"arguments": [
{"name": "code", "required": true}
]
}
```
这里没有 JSON Schema。提示词的参数是一个扁平的**具名字符串值**列表:是给人填的表单,而不是由模型构造的载荷。
### 渲染 {#rendering-it}
客户端用 `prompts/get` 渲染模板,并传入参数。你的函数运行后,返回的 `str` 会变成**一条用户消息**:
```json
{
"description": "Review a piece of code.",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please review this code:\n\ndef add(a, b): return a + b"
}
}
],
"resultType": "complete"
}
```
提示词的完整流程就是这样:按名称列出,按需渲染,放进对话。
!!! check
`required` 的检查发生在你的函数运行之前。渲染 `review_code` 时不传 `code`,请求本身就会失败,并返回一个 JSON-RPC 错误(错误码 `-32603`):
```text
mcp.shared.exceptions.MCPError: Internal server error
```
这里没有工具那种可以交回给模型的错误结果,因为整个环节里根本没有模型:调用会直接抛出异常。原因(`Missing required arguments: {'code'}`)会记在服务器的日志里。
### 试一试 {#try-it}
用 MCP Inspector 运行服务器:
```console
uv run mcp dev server.py
```
打开 **Prompts** 标签页,选择 `review_code`。Inspector 会画出一个表单,带一个必填的 `code` 字段。填好、渲染,返回的正是上面那条用户消息。
## 不止一条消息 {#more-than-one-message}
代码审查只要一条消息。调试则是一段对话,而提示词可以把整段对话的开头都铺好。
把返回值从 `str` 换成消息列表:
```python title="server.py" hl_lines="2 13-20"
--8<-- "docs_src/prompts/tutorial002.py"
```
* `UserMessage` 和 `AssistantMessage` 来自 `mcp.server.mcpserver.prompts.base`。给它们一个 `str`,它们会替你包装成 `TextContent`。角色由类名决定。
* `Message` 是它们的公共基类。用它作返回值注解。
现在渲染 `debug_error` 会按顺序产生三条消息:
```json
{
"description": "Start a debugging conversation.",
"messages": [
{"role": "user", "content": {"type": "text", "text": "I'm seeing this error:"}},
{"role": "user", "content": {"type": "text", "text": "TypeError: 'int' object is not iterable"}},
{
"role": "assistant",
"content": {"type": "text", "text": "I'll help debug that. What have you tried so far?"}
}
],
"resultType": "complete"
}
```
注意最后一条。预先填入一轮 `assistant` 发言,就能引导模型的**下一条**回复,而不用让用户自己把引导的话敲出来。
## 标题和参数描述 {#titles-and-argument-descriptions}
`review_code` 是函数名,不是标签。给客户端一个更适合放在按钮上的名字,并给每个参数加上描述,让表单一目了然:
```python title="server.py" hl_lines="10-13"
--8<-- "docs_src/prompts/tutorial003.py"
```
* `title="Code review"` 是给人看的名称,和工具的 `title` 一模一样。
* `Annotated[str, Field(description=...)]` 和 **[工具](tools.md)** 用来描述工具参数的是同一种写法。这里描述直接落在参数上,而不是写进模式里。
* `language` 有默认值,所以不再是必填参数。
现在 `prompts/list` 里的这一项包含了客户端画好一个表单所需的全部信息:
```json
{
"name": "review_code",
"title": "Code review",
"description": "Review a piece of code.",
"arguments": [
{"name": "code", "description": "The code to review.", "required": true},
{"name": "language", "description": "The language the code is written in.", "required": false}
]
}
```
!!! info
如果读过 **[工具](tools.md)**,这一页的内容你其实都已经会了。装饰器一样,用 docstring 作描述一样,`Annotated`/`Field` 也一样。变的只有两点:由谁触发(用户),以及结果去哪儿(进入对话)。
## 回顾 {#recap}
* 在函数上加 `@mcp.prompt()`,它就成了提示词。名称取自函数名,描述取自 docstring。
* 提示词由**用户控制**:客户端列出它们,用户选一个并填好参数。
* 参数是一个扁平的具名字符串列表(没有模式)。有默认值的形参是可选的。
* 返回 `str`,它就变成一条用户消息。返回 `UserMessage` / `AssistantMessage` 的列表,可以为多轮对话铺好开头。
* `title=` 和 `Field(description=...)` 是客户端放进 UI 里的内容。
* 缺少必填参数会让整个请求失败。没有针对单个提示词的错误结果。
要在服务器端为提示词(或资源模板)的参数提供自动补全,见 **[补全](completions.md)**。

View file

@ -0,0 +1,138 @@
---
translation:
sections: [09df998c2a799f78, 0cf131146d16d4f9, 4e6b91e3f8025346, 8fe4eef576db17ed, 0d0d1ed43e3d0a53]
tool: 1
---
# 资源 {#resources}
**资源**是你暴露出来、供应用程序读取的数据。
区别就在这里。工具是由**模型**决定调用的东西。资源是由**应用程序**决定加载的东西(一个配置文件、一条记录、一份文档),加载后作为上下文放到模型面前。
在一个普通的 Python 函数上加 `@mcp.resource(uri)`,就声明了一个资源。
## 第一个资源 {#your-first-resource}
```python title="server.py" hl_lines="6-8"
--8<-- "docs_src/resources/tutorial001.py"
```
形式和工具一样,只多了一样东西:**URI**。资源靠地址定位,而不是靠名称。客户端请求的是 `config://app`,从来不是 `get_config`。
其余信息,SDK 照样从函数里读取:
* **名称**是函数名:`get_config`。
* 客户端看到的**描述**是 docstring。
* **内容**就是你返回的东西。
`resources/list` 期间,客户端拿到的是:
```json
{
"name": "get_config",
"uri": "config://app",
"description": "The active shop configuration.",
"mimeType": "text/plain"
}
```
当它读取 `config://app` 时,你的函数运行,返回值以文本形式返回:
```python
result.contents # [TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")]
```
!!! tip
列出资源的开销很小。`resources/list` 期间**不会**调用你的函数,只在 `resources/read` 期间调用,而且只针对被请求的那个 URI。哪怕暴露一千个资源,也只为有人打开的那些付出开销。
### 试一试 {#try-it}
用 MCP Inspector 运行服务器:
```console
uv run mcp dev server.py
```
打开它打印出来的 URL,进入 **Resources** 标签页。`config://app` 连同它的描述就在列表里。点一下,Inspector 就会读取它:你的两行配置就出来了。
## 资源模板 {#resource-templates}
每条记录一个 URI,这种做法扩展不了。在 URI 里放一个**占位符**,再给函数加一个对应的参数:
```python title="server.py" hl_lines="12-13"
--8<-- "docs_src/resources/tutorial002.py"
```
URI 里写 `{user_id}`,函数上写 `user_id: str`。整个约定就这些。
它现在是一个**资源模板**,位置也变了:它离开 `resources/list`,改为出现在 `resources/templates/list` 里,不再是一个地址,而是一个模式:
```json
{
"name": "get_user_profile",
"uriTemplate": "users://{user_id}/profile",
"description": "A customer's profile.",
"mimeType": "text/plain"
}
```
客户端填上占位符,读取一个具体的 URI:`users://42/profile`、`users://ada/profile`。所有这些都由同一个函数应答,匹配到的值作为 `user_id` 传入:
```python
result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")]
```
注意结果里的 `uri`。它是客户端请求的那个**具体** URI,不是模板。
!!! check
占位符和参数必须对得上。把函数参数改名为 `user`,而 URI 里仍写着 `{user_id}`,装饰器就会在**导入时**拒绝,那时还没有任何客户端接触到它:
```text
ValueError: Mismatch between URI parameters {'user_id'} and function parameters {'user'}
```
不匹配只可能是 bug,所以 SDK 让带着这种不匹配的服务器根本无法启动。
占位符语法是 [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570):`{+path}` 表示跨多个分段的值,`{?q,lang}` 表示可选的查询参数,等等。SDK 默认还会对提取出的值做路径安全检查。完整参考见 **[URI 模板与路径安全](uri-templates.md)**。
`get_user_profile` 还可以接收一个注解为 `Context` 的参数。SDK 会把它注入进来,而绝不会把它当成 URI 参数;它能给你什么,见 **[Context](../handlers/context.md)** 页面。
## 返回什么 {#what-you-return}
不只限于 `str`。给每个资源一个 `mime_type`,返回合适的内容即可:
```python title="server.py" hl_lines="8-9 14-15 20-21"
--8<-- "docs_src/resources/tutorial003.py"
```
* `readme` 返回 `str`,因此原样发送。这是最常见的情况。
* `catalog_stats` 返回 `dict`,因此 SDK 替你把它序列化成 **JSON 文本**:
```json
{
"books": 1204,
"authors": 391
}
```
* `placeholder_cover` 返回 `bytes`,因此客户端拿到的是 `BlobResourceContents` 而不是 `TextResourceContents`,你的字节经 base64 编码后放在它的 `blob` 字段里。
同样的规则适用于其他任何可 JSON 序列化的东西:列表、Pydantic 模型、dataclass。只要既不是 `str` 也不是 `bytes`,就变成 JSON。
`mime_type` 由你来声明,默认是 `text/plain`。SDK 从不检查你返回的内容去猜它,所以一个没标注的 `dict` 资源仍然会以纯文本的类型对外宣告。
!!! tip
不想从函数推导时,`@mcp.resource()` 也接受 `name=`、`title=` 和 `description=`。而当根本没有函数可写时,`mcp.server.mcpserver.resources` 里有现成的 `Resource` 类(`TextResource`、`BinaryResource`、`FileResource`、`HttpResource`、`DirectoryResource`),用 `mcp.add_resource(...)` 注册即可。
客户端还可以**订阅**一个资源,在它变化时收到通知;那是客户端那一半的事,详见 **[客户端](../client/index.md)**。
## 回顾 {#recap}
* 在函数上加 `@mcp.resource(uri)`,它就成了资源。URI 是地址,返回值是内容,docstring 是描述。
* URI 里有 `{placeholder}`,它就成了**模板**:列在 `resources/templates/list` 下,一个函数服务所有匹配的 URI。
* 占位符名必须和函数的参数名一致。写错了,导入时就会发现,而不是等到生产环境。
* 你的函数在资源被**读取**时运行,而不是在列出时。
* `str` 变成文本,`bytes` 变成 base64 blob,其他一切变成 JSON 文本。用 `mime_type=` 给它标注类型。
* 工具供模型采取行动。资源供应用程序读取。
第三种原语,也就是由人从菜单里挑选的那一种,是 **[提示词](prompts.md)**。

View file

@ -0,0 +1,242 @@
---
translation:
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, e866c192e11d1c14, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c6f7d2a148aa49f4, c851964bb3301907, d715db6f8dccc9cc, ef86634aa70498a7]
tool: 1
---
# 结构化输出 {#structured-output}
返回普通 `str` 的工具会把结果产出两次:一次是 `content` 里的文本,一次是 `structured_content` 里的 `{"result": "..."}`。
本页讲的就是这第二个通道:它从哪里来、可能有哪些形态,以及 SDK 如何保证它货真价实。
一句话概括:**返回类型注解就是输出模式(output schema)**。你其实已经写好了。
## 输出模式 {#the-output-schema}
```python title="server.py" hl_lines="9"
--8<-- "docs_src/structured_output/tutorial001.py"
```
重要的是签名那一行:`-> int`。
有了它,SDK 在 `tools/list` 时发出的工具除了根据参数构建的输入模式(详见 **[工具](tools.md)**),还会带上一个 `output_schema`:
```json
{
"properties": {
"result": {"title": "Result", "type": "integer"}
},
"required": ["result"],
"title": "get_temperatureOutput",
"type": "object"
}
```
单独一个 `int` 不是 JSON 对象,所以 SDK 把它**包装**进 `{"result": ...}`。调用这个工具,两个通道都有内容:
```python
result.content # [TextContent(text="17")]
result.structured_content # {"result": 17}
```
所有标量都是同样的包装:`str`、`int`、`float`、`bool`、`bytes`、`None`。
## 两个通道 {#two-channels}
为什么同一个值要发两次?
* `content` 是给**模型**看的。语言模型读的是文本;整个结果里它只看得到这一部分。
* `structured_content` 是给模型所在的**应用程序**用的:代码想要的是 `17`,而不是一句含有“17”的话。
* `output_schema` 是二者之间的契约,早在工具被调用之前就已发布。
你只返回一个 Python 值,SDK 把这三样全部填好。
## 返回模型 {#return-a-model}
用 Pydantic `BaseModel` 声明形状,并返回一个实例:
```python title="server.py" hl_lines="8-11 15"
--8<-- "docs_src/structured_output/tutorial002.py"
```
现在 `WeatherData` **就是**模式。没有包装,也没有 `result` 键:
```json
{
"properties": {
"temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
"humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
"conditions": {"title": "Conditions", "type": "string"}
},
"required": ["temperature", "humidity", "conditions"],
"title": "WeatherData",
"type": "object"
}
```
`structured_content` 就是这个对象,字段逐一对应:
```python
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
```
语言模型也没有被落下。SDK 把同一个对象序列化为 JSON 文本,放进 `content`:
```json
{
"temperature": 16.2,
"humidity": 0.83,
"conditions": "Overcast"
}
```
注意,`temperature` 和 `humidity` 上的 `Field(description=...)` 进入了模式。描述**输入**的那个 `Field`,同样描述了输出。
!!! info
如果用过 FastAPI 的 `response_model`,这一套你已经熟悉:把 Pydantic 模型声明为响应,序列化和文档都替你做好。唯一的不同是,在这里返回注解就是全部的声明。
## `TypedDict` {#a-typeddict}
不是每种形状都值得专门写一个类。`TypedDict` 产出的模式完全一样:
```python title="server.py" hl_lines="8"
--8<-- "docs_src/structured_output/tutorial003.py"
```
`TypedDict` 在运行时就是普通的 `dict`,所以构建并返回的也就是它。模式、校验和 `structured_content` 都与 `BaseModel` 版本完全相同(只是少了描述,`TypedDict` 里没有地方写)。
## dataclass {#a-dataclass}
dataclass 也行,任何属性带类型提示的普通类同样可以。SDK 会在幕后根据注解构建出一个 Pydantic 模型。
```python title="server.py" hl_lines="8-9"
--8<-- "docs_src/structured_output/tutorial004.py"
```
三种写法,一个模式。代码库里本来用哪种,就用哪种。
## 列表 {#lists}
`list[...]` 也不是 JSON 对象,所以同样套上 `{"result": ...}` 包装,元素类型以 `$defs` 引用的形式放在里面:
```python title="server.py" hl_lines="15"
--8<-- "docs_src/structured_output/tutorial005.py"
```
```json
{
"$defs": {
"WeatherData": {
"properties": {
"temperature": {"title": "Temperature", "type": "number"},
"humidity": {"title": "Humidity", "type": "number"},
"conditions": {"title": "Conditions", "type": "string"}
},
"required": ["temperature", "humidity", "conditions"],
"title": "WeatherData",
"type": "object"
}
},
"properties": {
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
},
"required": ["result"],
"title": "get_forecastOutput",
"type": "object"
}
```
请求两天的预报,`structured_content` 就是 `{"result": [{...}, {...}]}`。`content` 则变成**两个** `TextContent` 块,每个元素一个:列表会为模型逐项展开,而不是整个转储成一个字符串。
`tuple[...]`、联合类型和 `Optional[...]` 的包装方式相同。
## 字典 {#dictionaries}
`dict[str, ...]` 是唯一一个本身**就是** JSON 对象的泛型,所以不会被包装:
```python title="server.py" hl_lines="9"
--8<-- "docs_src/structured_output/tutorial006.py"
```
```json
{
"additionalProperties": {"type": "number"},
"title": "get_temperaturesDictOutput",
"type": "object"
}
```
```python
result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
```
键必须是 `str`。`dict[int, float]` 成不了 JSON 对象,所以会退回到 `{"result": ...}` 包装。
## 校验 {#validation}
`output_schema` 并非只是文档。函数返回的任何内容,在离开服务器之前都会**对照它校验**。
手工构建值的时候你察觉不到:Pydantic 早已保证你的 `WeatherData` 确实是 `WeatherData`。等到哪天数据来自你控制不了的地方,你就会察觉了:
```python title="server.py" hl_lines="9 21"
--8<-- "docs_src/structured_output/tutorial007.py"
```
注解承诺的是 `WeatherData`,上游响应却不再发送 `humidity` 了。
!!! check
调用 `get_weather`,它不会悄悄把一个缺了一半的对象递给客户端。调用会失败,错误的头几行直接点名那个字段:
```text
Error executing tool get_weather: 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}
有时返回注解是写给类型检查器看的,而不是给协议的。传入 `structured_output=False`,工具就变成纯文本:
```python title="server.py" hl_lines="6"
--8<-- "docs_src/structured_output/tutorial008.py"
```
没有 `output_schema`,没有包装,没有校验。`structured_content` 为 `None`,`content` 就是你返回的字符串。
反过来,`structured_output=True` 会把自动检测变成硬性要求:返回类型产不出模式的工具会在导入时直接抛错,而不是退回到纯文本。
## 没有类型提示的类 {#a-class-without-type-hints}
有一种情况,你没有要求也会落得非结构化:返回一个**类体上没有任何注解**的类。
```python title="server.py" hl_lines="6-9"
--8<-- "docs_src/structured_output/tutorial009.py"
```
`Station` 在 `__init__` 里设置了 `name` 和 `online`,但**类**本身什么都没声明。SDK 去读类注解,一个也没找到,于是放弃。
!!! warning
而且是**悄无声息地**放弃。`output_schema` 是 `None`,`structured_content` 是 `None`,模型读到的文本是这个对象的 `repr`:
```text
"<server.Station object at 0x7f539d75b230>"
```
没有报错,没有警告,只剩一个没用的工具。把注解挪到类体上,或者传入 `structured_output=True`——后者会在模块导入的那一刻就让它直接报错:`Function get_station: return type <class 'server.Station'> is not serializable for structured output`。
!!! tip
需要完全掌控(自己构建 `CallToolResult`,或者附加应用程序看得见、模型看不见的 `_meta`)?详见 **[底层 Server](../advanced/low-level-server.md)**。
## 回顾 {#recap}
* **返回类型注解**就是输出模式,在 `tools/list` 中以 `output_schema` 发布。
* 标量、列表、元组和联合类型会被包装进 `{"result": ...}`。模型、`TypedDict`、dataclass、带注解的类以及 `dict[str, ...]` 本身已是对象,保持原样。
* 每个结果都同时带有 `content`(文本,给模型)**和** `structured_content`(数据,给应用程序)。
* 返回的内容会对照模式校验。不匹配就是工具错误,而不是一个损坏的结果。
* `structured_output=False` 让工具退出结构化输出。没有类型提示的类会悄无声息地退出;要当心。
至此,工具能回传的一切都由你掌控。接下来是第二种原语:**[资源](resources.md)**。

View file

@ -0,0 +1,170 @@
---
translation:
sections: [e4cc390d56573409, 8566e2b68594e9ad, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363]
tool: 1
---
# 工具 {#tools}
**工具**是模型可以调用的函数。
在一个普通的 Python 函数上加上 `@mcp.tool()`,就声明了一个工具。整个 API 就这些。
## 第一个工具 {#your-first-tool}
```python title="server.py" hl_lines="6-8"
--8<-- "docs_src/tools/tutorial001.py"
```
看看刚才写的代码。没有模式、没有 JSON、没有协议,只是一个函数。SDK 从中读出三样东西:
* 工具的**名称**就是函数名:`search_books`。
* 模型看到的**描述**就是文档字符串:`Search the catalog by title or author.`
* 模型可以传入的**参数**来自类型提示:`query: str` 和 `limit: int`。
### 输入模式 {#the-input-schema}
SDK 根据这些类型提示生成一份 JSON Schema,并在 `tools/list` 时发给客户端:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"title": "Limit", "type": "integer"}
},
"required": ["query", "limit"],
"title": "search_booksArguments"
}
```
两个参数都在 `required` 里,因为都没有默认值。这一点马上就会改。(`title` 键是 Pydantic 附带生成的;属性、属性的类型和 `required` 才是契约。)
!!! tip
类型提示在这里不是文档,而是**契约**。如果客户端发来 `"limit": "ten"`,SDK 会在你的函数运行之前就把它拒掉。
### 模型会收到什么 {#what-the-model-gets-back}
用 `{"query": "dune", "limit": 5}` 调用这个工具,结果有两部分:
```python
result.content # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")]
result.structured_content # {'result': "Found 3 books matching 'dune' (showing up to 5)."}
```
`content` 是给**模型**读的文本。`structured_content` 是给**客户端应用**的带类型数据。之所以有它,是因为你把返回类型声明成了 `-> str`。
先不用操心 `structured_content`。从工具里返回真正的 Python 对象,结果自然是对的;**[结构化输出](structured-output.md)** 页面专门讲这件事。
### 试一试 {#try-it}
用 MCP Inspector 运行服务器:
```console
uv run mcp dev server.py
```
打开它打印出来的 URL,切到 **Tools** 标签页,调用 `search_books`。
Inspector 会渲染出一个表单,里面有一个必填的 `query` 文本字段和一个必填的 `limit` 数字字段。这个表单是它根据你的类型提示生成的。其他所有 MCP 客户端也会这样做。
## 可选参数 {#optional-arguments}
给参数设一个默认值,它就不再是必填参数。就这样,只是普通的 Python。
```python title="server.py" hl_lines="7"
--8<-- "docs_src/tools/tutorial002.py"
```
模式也随之改变:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
`limit` 从 `required` 里移了出来,并多了 `"default": 10`。省略它的客户端会拿到 `10`,和 Python 的行为一模一样。
## 用 `Field` 写出更丰富的模式 {#richer-schemas-with-field}
类型提示已经很够用了,但有时还想**描述**某个参数,或者给它加约束。
把类型包进 `Annotated`,再加一个 Pydantic 的 `Field`:
```python title="server.py" hl_lines="12-14"
--8<-- "docs_src/tools/tutorial003.py"
```
新东西有三样,全在参数上:
* `Field(description=...)`:单个参数的描述,模型会把它和文档字符串一起读。
* `Field(ge=1, le=50)`:数值上下界。它们在模式里变成 `"minimum": 1, "maximum": 50`。
* `Literal["fiction", "non-fiction", "poetry"]`:枚举。模型只能从中选一个。
!!! check
约束不是摆设。用 `limit=999` 调用这个工具,SDK 会**在你的函数运行之前**就回复一个工具错误:
```text
Input should be less than or equal to 50
```
这个错误会作为工具结果回到模型那里,模型读到后会换一个合法的值重试。只写了一次 `le=50`,就免费得到了会自我纠错的智能体。
!!! info
如果用过 FastAPI 或 Pydantic,这些你全都已经会了。同一个 `Field`、同一个 `Annotated`、同一套校验。这里没有任何 MCP 特有的东西要学。
## 用模型作参数 {#a-model-as-a-parameter}
当工具的参数不止两三个时,把它们归进一个 Pydantic 模型:
```python title="server.py" hl_lines="8-11 15"
--8<-- "docs_src/tools/tutorial004.py"
```
`Book` 的模式嵌套在工具的输入模式里(以 `$defs` 引用的形式),模型把它当作一个 JSON 对象填写,而你的函数收到的是一个**真正的 `Book` 实例**,已经校验过,带有 `.title`、`.author` 和 `.year` 属性。
可以随意搭配:普通参数和模型参数并列、嵌套模型、模型列表。从里到外都是 Pydantic。
## `async def` {#async-def}
如果工具要做 I/O(调用 API、读文件、查数据库),就把它声明为 `async def`,并在里面 `await`。SDK 会 await 它。
普通的 `def` 工具也可以:SDK 会在线程里运行它,所以它永远不会阻塞服务器。
没有别的需要配置。
## 名称、标题与注解 {#names-titles-and-annotations}
SDK 推断出来的一切,都可以在装饰器里覆盖:
```python title="server.py" hl_lines="7-10"
--8<-- "docs_src/tools/tutorial005.py"
```
* `title` 是给 UI 用的人类可读名称。客户端会显示“Search the catalog”,而不是 `search_books`。
* `annotations` 是给客户端的行为**提示**:
* `read_only_hint=True`:这个工具不会改动任何东西。
* `open_world_hint=False`:它针对的是一个封闭的集合(这份书目),而不是开放的互联网。
* 另外两个,`destructive_hint` 和 `idempotent_hint`,描述的是会**写入**的工具:它会不会删除东西?调用两次和调用一次是不是一样?规范只为非只读工具定义了这两项,所以它们放在 `search_books` 上什么也说明不了。
守规矩的客户端会用它们来决定诸如“运行它之前要不要先问用户?”之类的事。它们是提示,不是安全机制。永远不要指望客户端一定会遵守。
!!! tip
如果不想从函数名和文档字符串推导名称和描述,`@mcp.tool()` 也接受 `name=` 和 `description=`。大多数时候,直接推导就够了。
## 回顾 {#recap}
* 在函数上加 `@mcp.tool()`,它就成了工具。名称来自函数名,描述来自文档字符串。
* 类型提示**就是**输入模式。默认值让参数变为可选。
* `Annotated[..., Field(...)]` 添加描述和约束;`Literal` 添加枚举。
* 要接收结构化的“请求体”,就用 Pydantic 模型参数。
* 错误的参数会替你拒掉,并附带一条模型能读懂、也能据此纠正的错误信息。
* I/O 用 `async def`,其他一律用普通的 `def`。
**[结构化输出](structured-output.md)** 讲的是你 `return` 的值之后会怎样。

View file

@ -0,0 +1,167 @@
---
translation:
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 101ef9d14bf4ec46, 4b6c4a845438abc7, f98b46bafbee4acd]
tool: 1
---
# URI 模板与路径安全 {#uri-templates-and-path-safety}
本页是 [`@mcp.resource`](resources.md) 所接受的 URI 模板语法的参考,也涵盖 SDK 对提取出的值应用的路径安全策略。想了解资源是什么、什么时候该用,请先看 **[资源](resources.md)**;本页假设你已经熟悉如何声明资源,想要的是完整的运算符集合、安全方面的配置项,或者底层的接线方式。
模板语法是 [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570)。SDK 支持其中一个子集,专为匹配传入的 `resources/read` URI 而选,另外还加了一层安全检查,会拒绝那些会解析到你打算提供的目录之外的值。协议层面的细节(消息格式、生命周期、分页)见 [MCP 资源规范](https://modelcontextprotocol.io/specification/latest/server/resources)。
## 完整的运算符集合 {#the-full-operator-set}
普通占位符 `{user_id}` 就是 **[资源](resources.md)** 一页介绍过的那种。除此之外还有四种运算符形式;下面把它们放在同一个服务器上,方便并排对照:
```python title="server.py" hl_lines="16-17 22-23 28-29 34-35 40-41"
--8<-- "docs_src/uri_templates/tutorial001.py"
```
每个高亮的装饰器都是切分 URI 的一种不同方式。下面各节从上到下依次讲解。
### 简单展开:`{name}` {#simple-expansion-name}
`books://{isbn}` 是最普通、最常用的形式。占位符映射到 `isbn` 参数,所以客户端读取 `books://978-0441172719` 时会调用 `get_book("978-0441172719")`。
普通的 `{name}` 在第一个 `/` 处停止。`books://978/extra` 不匹配,因为 `978` 后面的斜杠终止了捕获,`/extra` 就多出来了。
### 类型转换 {#type-conversion}
提取出的值都是字符串,但可以声明更具体的类型,SDK 会负责转换。`orders://{order_id}` 对应的函数参数是 `order_id: int`,所以读取 `orders://12345` 会调用 `get_order(12345)`,而不是 `get_order("12345")`。处理函数直接对它做算术运算(`order_id + 1`),不需要强制转换。
### 多段路径:`{+name}` {#multi-segment-paths-name}
要捕获包含斜杠的值,用 `{+name}`。以 `manuals://{+path}` 为例:
* `manuals://returns.md` 得到 `path = "returns.md"`
* `manuals://printing/setup.md` 得到 `path = "printing/setup.md"`
只要值是层级结构的,就用 `{+name}`:文件系统路径、嵌套对象的键、你正在代理的 URL 路径。
### 查询参数:`{?a,b,c}` {#query-parameters-abc}
`reviews://{isbn}{?limit,sort}` 把 `limit` 和 `sort` 放在 `?` 之后。路径确定读**哪一本**书;查询参数调整**怎么**读它。
查询参数的匹配是宽松的:顺序无所谓,多余的会被忽略,省略的则落到函数的默认值上。所以 `reviews://978-0441172719` 使用 `limit=10, sort="newest"`,而 `reviews://978-0441172719?sort=top` 只覆盖 `sort`。
### 把路径段作为列表:`{/name*}` {#path-segments-as-a-list-name}
如果想让每个路径段成为列表里独立的一项,而不是一个带斜杠的字符串,用 `{/name*}`。以 `shelves://browse{/path*}` 为例,客户端读取 `shelves://browse/fiction/sci-fi` 会调用 `browse_shelf(["fiction", "sci-fi"])`。
### 模板速查 {#template-reference}
最常见的模式:
| 模式 | 示例输入 | 得到的值 |
|--------------|-----------------------|-------------------------|
| `{name}` | `alice` | `"alice"` |
| `{name}` | `docs/intro.md` | **不匹配**(在 `/` 处停止) |
| `{+path}` | `docs/intro.md` | `"docs/intro.md"` |
| `{.ext}` | `.json` | `"json"` |
| `{/segment}` | `/v2` | `"v2"` |
| `{?key}` | `?key=value` | `"value"` |
| `{?a,b}` | `?a=1&b=2` | `"1"`, `"2"` |
| `{/path*}` | `/a/b/c` | `["a", "b", "c"]` |
### 解析器会拒绝什么 {#what-the-parser-rejects}
有几种模板形式会在一开始就被拦下,而不是等到第一个请求时才失败。`@mcp.resource` 在装饰器运行时就解析模板,所以这些问题都不会进入运行中的服务器。
`UriTemplate.parse()` 在以下情况抛出 `InvalidUriTemplate`:
* **两个变量之间什么都没有。** `manuals://{+path}{ext}` 会被拒绝:匹配时无法判断 `path` 在哪里结束、`ext` 从哪里开始。在它们之间放一个字面量(`manuals://{+path}/{ext}`),或者使用自带分隔符的运算符。`manuals://{+path}{.ext}` 可以接受,因为 `{.ext}` 自己提供了 `.`。
* **不止一个多段变量。** 每个模板最多只能有一个 `{+var}`、`{#var}` 或展开变量(`{/var*}`、`{.var*}`、`{;var*}`)。两个就有本质上的歧义:没有合理的办法决定多出来的段该归哪一个。
* **常见的语法错误**:花括号没闭合、变量名重复使用,或者用了 SDK 不支持的 RFC 6570 特性,比如 `{var:3}` 前缀修饰符或 `{?vars*}` 查询展开。
此外,如果处理函数的某个参数绑定到模板末尾 `{?...}`/`{&...}` 段里的查询变量,却没有 Python 默认值,`@mcp.resource` 会抛出 `ValueError`。这些变量的匹配是宽松的(客户端可以省略其中任何一个),所以没有默认值的参数只会在第一个省略它的请求上以一个含糊的内部错误暴露出来。上面服务器里的 `reviews://{isbn}{?limit,sort}` 就是规范的写法:`limit` 和 `sort` 都带默认值。
## 安全 {#security}
模板参数来自客户端。如果不加检查就流入文件系统或数据库操作,像 `../../etc/passwd` 这样的值就可能解析到你原本打算提供的目录之外。
### SDK 默认检查什么 {#what-the-sdk-checks-by-default}
在处理函数运行之前,SDK 会拒绝任何符合以下情况的参数:
* 通过 `..` 路径组件逃出起始目录
* 看起来像绝对路径(`/etc/passwd`、`C:\Windows`)或 Windows 盘符相对路径(`C:foo`)。盘符相对值和 `x:y` 这样带命名空间的标识符作为字符串无法区分,所以任何由单个字母加冒号构成的值默认都会被拒绝;如果该参数确实会合法地收到这类值,就把它设为豁免
* 包含空字节(`\x00`)
`..` 检查是基于路径组件的,不是子串扫描。`v1.0..v2.0` 或 `HEAD~3..HEAD` 这样的值能通过,因为其中的 `..` 并不是独立的路径段。
这些检查作用于解码后的值,所以无论路径穿越在 URI 里是怎么编码的都能抓到(`../etc`、`..%2Fetc`、`%2E%2E/etc`、`..%5Cetc`、`%00` 全都会被拦下)。
!!! check
从上面的服务器读取 `manuals://../etc/passwd`,请求会被直接拒绝:模板匹配在第一次失败时就停止,所以不会再把后面(可能更宽松)的模板当作后备去尝试。客户端看到的是 `-32602` “Unknown resource” 错误,和一个完全不匹配任何模板的 URI 一样,而 `read_manual` 根本不会运行。
### 文件系统处理函数:使用 safe_join {#filesystem-handlers-use-safe_join}
内置检查能拦住常见情况,但无从知道你的沙箱边界在哪。访问文件系统时,用 `safe_join` 解析路径并确认它仍在基础目录之内:
```python title="server.py" hl_lines="4 14"
--8<-- "docs_src/uri_templates/tutorial002.py"
```
`safe_join` 能抓到符号链接逃逸、`..` 序列,以及简单字符串检查会漏掉的绝对路径花招。如果解析后的路径逃出了 `DOCS_ROOT`,它会抛出 `PathEscapeError`,在客户端那边表现为 `ResourceError`。
### 默认设置碍事的时候 {#when-the-defaults-get-in-the-way}
有时这些检查会挡住合法的值。一个书目导入工具可能本来就要接收绝对路径,或者某个参数是 `../sibling` 这样的相对引用,处理函数会在不碰文件系统的前提下安全地解释它。可以豁免那个参数,或者放宽整个服务器的策略:
```python title="server.py" hl_lines="9 16-19"
--8<-- "docs_src/uri_templates/tutorial003.py"
```
* 装饰器上的 `security=ResourceSecurity(exempt_params={"source"})` 只对这一个资源的这一个参数跳过检查。服务器的其余部分保持默认策略。
* `MCPServer` 构造函数上的 `resource_security=` 为每个资源设置默认值。这里的 `relaxed` 把 `..` 检查整个关掉了。
可配置的检查项:
| 设置 | 默认值 | 作用 |
|-------------------------|---------|-------------------------------------|
| `reject_path_traversal` | `True` | 拒绝逃出起始目录的 `..` 序列 |
| `reject_absolute_paths` | `True` | 拒绝 `/foo`、`C:\foo`、UNC 路径和盘符相对的 `C:foo`(也会拦下 `x:y`) |
| `reject_null_bytes` | `True` | 拒绝包含 `\x00` 的值 |
| `exempt_params` | 空 | 要跳过检查的参数名 |
这些检查只是启发式的预过滤;访问文件系统时,`safe_join` 仍然是真正的隔离边界。
!!! tip
如果处理函数无法完成请求(文件不存在、id 未知),就抛出异常。SDK 会把它变成错误响应。协议错误和工具错误的区别见 **[处理错误](handling-errors.md)**。
## 底层 Server 上的资源 {#resources-on-the-low-level-server}
如果你是基于底层 `Server` 构建(见 **[底层 Server](../advanced/low-level-server.md)**),就要直接为 `resources/list` 和 `resources/read` 这两个协议方法注册处理函数。没有装饰器;协议类型由你自己返回。
### 静态资源 {#static-resources}
对于固定的 URI,维护一个注册表,按精确匹配分发:
```python title="server.py" hl_lines="17 21 27"
--8<-- "docs_src/uri_templates/tutorial004.py"
```
列表处理函数告诉客户端有哪些可用;读取处理函数提供内容。先查注册表,如果有模板(见下)就接着落到模板上,其他情况一律抛异常。
### 模板 {#templates}
`MCPServer` 使用的模板引擎位于 `mcp.shared.uri_template`,可以独立使用。解析和匹配完全一样;路由和安全策略由你自己接上。
```python title="server.py" hl_lines="13-16 22-25 29 33 45"
--8<-- "docs_src/uri_templates/tutorial005.py"
```
高亮的几行里发生了三件事:
* **解析一次,每个请求匹配一次。** `UriTemplate.parse()` 构建模板;`template.match(uri)` 以 `dict` 形式返回提取出的变量,URI 不符合时返回 `None`。URL 解码在 `match()` 内部完成;解码后的值原样返回,不做路径安全校验。值都是字符串:自己转换(`int(matched["id"])`、`Path(matched["path"])`)。
* **自己应用安全检查。** `MCPServer` 默认运行的 `..` 和绝对路径检查位于 `mcp.shared.path_security`。`read_manual_safely` 在碰 `MANUALS` 之前调用它们。如果某个参数不是文件系统路径(ISBN、搜索查询),就跳过对该值的检查:策略由你按处理函数逐个控制,而不是通过配置对象。
* **从同一来源列出模板。** 客户端通过 `resources/templates/list` 发现模板。`str(template)` 返回原始模板字符串,所以列表和匹配器共用同一份事实来源。
## 回顾 {#recap}
* `{name}` 匹配一段;`{+name}` 保留斜杠;`{?a,b}` 从查询字符串取值;`{/name*}` 把各段拆成列表。
* 两个变量之间什么都没有,或者出现第二个多段变量,都会在解析时被拒绝。绑定到末尾 `{?...}`/`{&...}` 查询变量的参数必须声明 Python 默认值。
* 给参数加上类型注解(`order_id: int`),SDK 就会转换。
* 默认安全策略在处理函数运行之前拒绝 `..`、绝对路径和空字节;用 `security=ResourceSecurity(...)` 按资源覆盖,或用 `resource_security=` 在整个服务器范围内覆盖。
* 访问文件系统时,`safe_join` 是隔离边界。
* 在底层 `Server` 上,用 `UriTemplate.parse()` 解析,用 `.match()` 匹配,并自己应用 `mcp.shared.path_security`。