docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
5 changed files with 89737 additions and 54 deletions
139
i18n/zh/pages/get-started/first-steps.md
Normal file
139
i18n/zh/pages/get-started/first-steps.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
|
||||
tool: 1
|
||||
---
|
||||
# 第一步 {#first-steps}
|
||||
|
||||
**[首页](../index.md)** 节奏很快:写一个服务器,运行它,调用一个工具。
|
||||
|
||||
这一页慢慢来:服务器能暴露的三样东西全都讲到,沿途遇到的每个概念也都给出名字。
|
||||
|
||||
## 宿主、客户端和服务器 {#host-client-and-server}
|
||||
|
||||
从这里开始,每一页都会见到这三个词:
|
||||
|
||||
* **宿主** 是 LLM 应用:Claude、IDE、智能体运行时。用户与之对话的就是它。
|
||||
* **客户端** 位于宿主内部,讲 MCP。宿主每连接一个服务器,就运行一个客户端。
|
||||
* **服务器** 是你用这个 SDK 构建的东西。它向客户端暴露内容,从不直接和模型对话。
|
||||
|
||||
你写的是服务器。宿主是别人的产品。SDK 还提供了一个 `Client`,你会用它来测试自己的服务器,本页后面就会用到。
|
||||
|
||||
## 三种原语 {#the-three-primitives}
|
||||
|
||||
服务器暴露的东西恰好有三种。区分它们的标准是 **谁来决定使用它们**:
|
||||
|
||||
| 原语 | 由谁控制 | 是什么 | 示例 |
|
||||
|------------|----------|--------------------------------|---------------------------|
|
||||
| **工具** | 模型 | 模型为执行操作而调用的函数 | 一次 API 调用、一次数据库写入 |
|
||||
| **资源** | 应用 | 宿主加载进模型上下文的数据 | 文件内容、API 响应 |
|
||||
| **提示词** | 用户 | 用户按名称调用的可复用消息模板 | 斜杠命令、菜单项 |
|
||||
|
||||
“由谁控制”正是这样划分的全部意义。工具会运行,是因为 **模型** 决定调用它。资源会被附加进来,是因为 **应用** 认为模型需要它。提示词会运行,是因为 **用户** 选了它。
|
||||
|
||||
!!! info
|
||||
如果你做过 Web API,大部分直觉其实已经有了:**资源** 相当于 `GET`(加载数据,什么都不改),**工具** 相当于 `POST`(干活,可能有副作用)。**提示词** 在 HTTP 里没有对应物,它更接近一个用户按名称运行的已保存查询。
|
||||
|
||||
## 一个服务器,三样俱全 {#one-server-all-three}
|
||||
|
||||
```python title="server.py" hl_lines="6 12 18"
|
||||
--8<-- "docs_src/first_steps/tutorial001.py"
|
||||
```
|
||||
|
||||
三个普通函数,三个装饰器。每个装饰器就是注册的全部:
|
||||
|
||||
* `@mcp.tool()` 把 `add` 变成 **工具**。
|
||||
* `@mcp.resource("greeting://{name}")` 把 `greeting` 变成 **资源模板**:URI 里的 `{name}` 就是函数的参数。
|
||||
* `@mcp.prompt()` 把 `summarize` 变成 **提示词**。它返回的字符串会成为一条用户消息。
|
||||
|
||||
其余的一切(名称、描述、参数模式),SDK 都从函数本身读取:函数名、文档字符串、类型注解。这些你都没有单独声明过。
|
||||
|
||||
!!! tip
|
||||
SDK 的两半各有一条导入路径:`from mcp import Client` 和 `from mcp.server import MCPServer`。不存在 `from mcp import MCPServer` 这种写法。
|
||||
|
||||
### 试一试 {#try-it}
|
||||
|
||||
用 MCP Inspector 运行它:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
打开它打印出来的 URL。Inspector 为每种原语各设一个标签页,按顺序逐个看一遍。
|
||||
|
||||
**工具。** 只有一项:`add`,描述是“Add two numbers.”。表单里有一个必填的整数字段 `a`,另一个是 `b`。填好后调用,结果是 `3`。这张表单是 Inspector 根据 `a: int, b: int` 生成的。其他所有客户端也都这样做。
|
||||
|
||||
**资源。** “Resources”列表是空的。`greeting` 在 **Resource Templates** 下面,因为 `greeting://{name}` 带有参数:在有人给出 `name` 之前,没有哪个具体的资源可以列出。填入 `World` 并读取:
|
||||
|
||||
```text
|
||||
Hello, World!
|
||||
```
|
||||
|
||||
**提示词。** 只有一项:`summarize`,带一个必填参数 `text`。传一段文本去获取它,会收到一条 `role: user` 的消息,内容就是你渲染出的字符串。提示词就是这么回事:一个构建消息的函数。
|
||||
|
||||
Inspector 是通过 **stdio** 运行你的服务器的,这是 MCP 服务器可用的传输方式之一。现在还不用选;**[运行服务器](../run/index.md)** 专门讲这个。
|
||||
|
||||
## 能力 {#capabilities}
|
||||
|
||||
你在 Inspector 里看到了三个标签页。它怎么知道有三个?
|
||||
|
||||
客户端连接时,服务器会声明自己的 **能力**:它会响应哪几类请求。客户端根据这份声明来决定该请求什么。这份声明你从没写过;是 `MCPServer` 替你声明的。
|
||||
|
||||
自己看一下。SDK 的 `Client` 可以直接接受服务器对象,并在 **内存中** 与之连接(没有子进程,没有端口):
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
print(client.server_capabilities.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}
|
||||
```
|
||||
|
||||
这个字典就是你的服务器所声明的 **能力**。每个连接上来的客户端最先得知的就是它:
|
||||
|
||||
| 能力 | 客户端现在可以调用 |
|
||||
|-------------|------------------------------------------------------------|
|
||||
| `tools` | `tools/list`, `tools/call` |
|
||||
| `resources` | `resources/list`, `resources/templates/list`, `resources/read` |
|
||||
| `prompts` | `prompts/list`, `prompts/get` |
|
||||
|
||||
`MCPServer` 三种原语都提供,所以这三项始终都会声明。
|
||||
|
||||
注意这里缺了什么。`completions`(资源模板和提示词的参数自动补全)需要一个由你编写的处理函数,而这个服务器没有,所以这项能力不会出现,行为规范的客户端也就不会去问。所有可选项都遵循这条规则:注册了对应的东西,能力就出现;**[补全](../servers/completions.md)** 会证明这一点。
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` 正是这些文档里每个示例测试时所用的那个内存客户端,你测试自己的服务器也会用它。它有整整一页:**[测试](testing.md)**。
|
||||
|
||||
## 你没有写的东西 {#what-you-did-not-write}
|
||||
|
||||
回头看看这一页。你写了三个小小的 Python 函数。你 **没有** 写:
|
||||
|
||||
* JSON Schema。`a: int, b: int` **就是** `add` 的模式。
|
||||
* 请求处理函数。`tools/list`、`resources/read`、`prompts/get`:全都替你处理好了。
|
||||
* 能力声明。`MCPServer` 替你生成了。
|
||||
* 一行协议代码。版本协商、JSON-RPC 分帧、能力交换:全都发生在 `mcp dev` 和 `Client(mcp)` 内部,你一眼都没见到。
|
||||
|
||||
这个比例,正是这个 SDK 的意义所在。
|
||||
|
||||
## 回顾 {#recap}
|
||||
|
||||
* **宿主** 是 LLM 应用,**客户端** 是它讲 MCP 的那一半,**服务器** 是你构建的东西。
|
||||
* 工具由 **模型** 控制,资源由 **应用** 控制,提示词由 **用户** 控制。
|
||||
* 每种原语一个装饰器:`@mcp.tool()`、`@mcp.resource(uri)`、`@mcp.prompt()`。名称、描述和模式都来自函数本身。
|
||||
* 带 `{param}` 的 URI 生成的是资源 **模板**,与具体资源分开列出。
|
||||
* 服务器的 **能力** 会替你声明好,而客户端只会请求服务器声明过的内容。
|
||||
* `Client(mcp)` 在内存中连接服务器对象:从第一天起,它就是你的测试工具。
|
||||
|
||||
接下来是 **[连接到真实宿主](real-host.md)**:把这个服务器真正放进 Claude Desktop 或 IDE 里。然后是 **[测试](testing.md)**:一页内容,一个内存客户端,从此不用再猜它到底能不能用。再之后,每种原语各有自己的一页,从模型驱动的那一种开始:**[工具](../servers/tools.md)**。
|
||||
53
i18n/zh/pages/get-started/index.md
Normal file
53
i18n/zh/pages/get-started/index.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ed4a756b4c53c585, 97e2fb315b7fe398, 4d04f1c6f4bf6c1d, 577d73078fc62baf]
|
||||
tool: 1
|
||||
---
|
||||
# 快速开始 {#get-started}
|
||||
|
||||
刚接触 MCP,或者刚接触这个 SDK?从这里开始。这几页会带你从零开始,做出一个能用、经过测试的服务器:[安装 SDK](installation.md)、构建[第一个服务器](first-steps.md)、[把它接入真实的宿主](real-host.md),然后用内存客户端[测试它](testing.md)。
|
||||
|
||||
## 运行代码 {#run-the-code}
|
||||
|
||||
所有代码块都可以直接复制使用:它们都是完整、可运行的文件。
|
||||
|
||||
想跟着做,就把某个代码块粘贴到 `server.py` 里,再用 MCP Inspector 打开:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
**强烈建议**亲手写(或者复制)这些代码,改一改,然后在本地运行。只有在自己的编辑器里真正用上,才能体会到关键所在:要写的代码非常少,有自动补全,还没运行,类型检查就已经把错误找出来了。
|
||||
|
||||
## 不用靠猜 {#you-will-not-be-guessing}
|
||||
|
||||
这些文档中的每个示例,都是 SDK 自身仓库 [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) 目录下的完整文件;SDK 的测试套件会通过**内存客户端**把它们逐一运行一遍:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_add() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
assert result.structured_content == {"result": 3}
|
||||
```
|
||||
|
||||
没有子进程,不占端口,也不经过任何传输。`Client(mcp)` 直接连到服务器对象上。
|
||||
|
||||
如果对 SDK 的某次改动弄坏了这些页面上的某个示例,CI 会在页面出问题之前先变红。这里读到的代码,就是实际运行的代码。
|
||||
|
||||
在[测试](testing.md)中你会亲手用到它;测试自己的服务器,用的也是这个方法。
|
||||
|
||||
## 接下来去哪里 {#where-to-go-next}
|
||||
|
||||
服务器跑起来之后,其余文档就是参考手册,而不是课程。每一页都自成一体,需要什么就直接跳过去看:
|
||||
|
||||
* 服务器对外暴露什么(工具、资源、提示词),见 **[服务器](../servers/index.md)**。
|
||||
* 注册的函数内部有什么可用,见 **[在处理函数内部](../handlers/index.md)**。
|
||||
* 怎样把它送到客户端面前(stdio、HTTP、现有的 FastAPI 应用),见 **[运行服务器](../run/index.md)**。
|
||||
* 构建另一端,也就是**使用** MCP 服务器的应用,见 **[客户端](../client/index.md)**。
|
||||
45
i18n/zh/pages/get-started/installation.md
Normal file
45
i18n/zh/pages/get-started/installation.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6e2f9bab94d5ed36, 8cf653388f69e28b, 6fd9ea2f65de0df6]
|
||||
tool: 1
|
||||
---
|
||||
# 安装 {#installation}
|
||||
|
||||
Python SDK 在 PyPI 上的包名是 [`mcp`](https://pypi.org/project/mcp/),需要 **Python 3.10+**。
|
||||
|
||||
本文档描述的是 **v2**,也就是当前的稳定版本系列:
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add "mcp[cli]"
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install "mcp[cli]"
|
||||
```
|
||||
|
||||
!!! note "从 v1 迁移过来?"
|
||||
v2 是包含破坏性变更的主版本,**[迁移指南](../migration.md)** 涵盖了其中每一处。如果你的**包**依赖 `mcp` 且还没准备好迁移,请保留 `<2` 的版本上限(例如 `mcp>=1.28,<2`),这样在未锁定版本的情况下,依赖解析仍会停留在 1.x 系列。
|
||||
|
||||
## 安装了什么 {#what-gets-installed}
|
||||
|
||||
使用 SDK 并不需要了解这些,不过如果你好奇每个依赖是做什么的:
|
||||
|
||||
* `mcp-types`:所有协议类型(请求、结果、内容块)独立成一个包,版本与 SDK 同步发布。依赖 `mcp` 的代码通过 `mcp.types` 这个别名导入它(本文档里每一处 `from mcp.types import ...` 都是这样);只有在安装了 `mcp-types` 却没有安装 SDK 的项目里,才直接导入 `mcp_types`。
|
||||
* [`anyio`](https://anyio.readthedocs.io/):异步运行时。整个 SDK 都基于 anyio 编写,因此既能跑在 `asyncio` 上,也能跑在 `trio` 上。
|
||||
* [`pydantic`](https://docs.pydantic.dev/):每个 `mcp.types` 模型都构建在它之上,所有的模式生成和校验也由它完成。
|
||||
* [`httpx2`](https://pypi.org/project/httpx2/):支撑 Streamable HTTP 和 SSE **客户端**传输方式的 HTTP 客户端,内置对 server-sent events 的支持。
|
||||
* [`starlette`](https://www.starlette.io/)、[`uvicorn`](https://www.uvicorn.org/)、[`sse-starlette`](https://pypi.org/project/sse-starlette/) 和 [`python-multipart`](https://pypi.org/project/python-multipart/):HTTP **服务器端**传输方式。
|
||||
* [`jsonschema`](https://pypi.org/project/jsonschema/):对照工具声明的输出模式,校验工具的结构化输出。
|
||||
* [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/):授权所需的 OAuth 令牌处理。
|
||||
* [`opentelemetry-api`](https://opentelemetry-python.readthedocs.io/):仅包含轻量级的 API,所以除非你自己安装 OpenTelemetry SDK 和导出器,否则 SDK 的追踪中间件不会带来任何开销。
|
||||
* [`typing-extensions`](https://typing-extensions.readthedocs.io/) 和 [`typing-inspection`](https://pypi.org/project/typing-inspection/):在 Python 3.10 上提供现代的类型标注特性。
|
||||
* [`pywin32`](https://pypi.org/project/pywin32/):仅 Windows 需要,用于 `stdio` 子进程管理。
|
||||
|
||||
## 可选附加依赖 {#optional-extras}
|
||||
|
||||
* `mcp[cli]` 会额外安装 [`typer`](https://typer.tiangolo.com/) 和 [`python-dotenv`](https://pypi.org/project/python-dotenv/),供 `mcp` 命令行工具(`mcp dev`、`mcp run`、`mcp install`)使用。开发期间会用到它;部署后的服务器里未必需要。
|
||||
* `mcp[rich]` 会额外安装 [`rich`](https://rich.readthedocs.io/),让服务器日志更美观。
|
||||
168
i18n/zh/pages/get-started/real-host.md
Normal file
168
i18n/zh/pages/get-started/real-host.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
---
|
||||
translation:
|
||||
sections: [3c4f2f06b4e978b6, 22520eecae3d1961, f4e1709db18d635a, 2eb57992049671d9, 1ba83e9af37cc1b4, 4822586344b08d9e, 1c93afef72478992, b6b448f9eddd51dc, fe55370fd931815b]
|
||||
tool: 1
|
||||
---
|
||||
# 连接到真实的宿主 {#connect-to-a-real-host}
|
||||
|
||||
**宿主** 是你的服务器最终所处的应用程序:Claude Desktop、Claude Code、IDE。用户与之对话的是宿主。在宿主内部,一个 MCP **客户端** 把你的服务器作为子进程启动,并通过该进程的 stdin 和 stdout 与它通信。
|
||||
|
||||
也就是说,连接到宿主只需要做一件事:告诉它 **启动服务器的命令**。本页的所有内容(两条 CLI 命令、三个 JSON 文件)都只是放置同一条命令的不同位置。
|
||||
|
||||
## 一个服务器,所有宿主 {#one-server-every-host}
|
||||
|
||||
```python title="server.py" hl_lines="3 33-34"
|
||||
--8<-- "docs_src/real_host/tutorial001.py"
|
||||
```
|
||||
|
||||
两个工具加一个资源,全在一个文件里。关于这个文件,有三点对下面每个宿主都很重要:
|
||||
|
||||
* 不带参数的 `mcp.run()` 启动的是 **stdio** 服务器:它会阻塞,从 stdin 读取协议消息,再把消息写到 stdout。本页每个宿主用的都是这种传输方式。宿主把你的文件作为子进程启动,并掌管这两个管道,所以连接从来都只是“把命令告诉它”这一件事。你永远不用选端口,也没有任何东西在端口上监听。
|
||||
* `run()` 放在 `if __name__ == "__main__":` 之下。下文的所有方式都是 **导入** 这个文件而不是执行它,所以不加这层保护的 `run()` 会在模块被任何东西加载的那一刻就启动服务器。
|
||||
* 服务器对象是一个名为 `mcp` 的模块级全局变量。这是 `mcp run` 要找的名字(`server` 和 `app` 也行)。如果起了别的名字,就得显式指定:`mcp run server.py:bookshop`。
|
||||
|
||||
这是本页最后一行 Python。从这里往下全是宿主配置。
|
||||
|
||||
## 启动命令 {#the-launch-command}
|
||||
|
||||
下面每个宿主拿到的都是同一条命令:
|
||||
|
||||
```bash
|
||||
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
所有宿主共用一条命令,是因为 `uv run --with` 会当场把 SDK 解析进一个全新的环境:在任何目录下都能用,既不需要项目,也不需要激活虚拟环境。这一点在这里比在别处都更要紧,因为宿主是从 **它自己的** 工作目录、带着几乎为空的环境启动你的服务器,而不是从你的 shell。
|
||||
|
||||
它也是 `mcp install` 替你写进 Claude Desktop 配置的那条命令(见下文),所以手敲的和工具生成的是一致的,差别只在工具额外加上的精确版本锁定。
|
||||
|
||||
!!! tip "如果宿主找不到 `uv`"
|
||||
宿主启动你的服务器时只带一个极简的 `PATH`,`uv` 可能不在其中。把不带路径的 `uv` 换成 `which uv`(macOS/Linux)或 `where uv`(Windows)给出的绝对路径。`mcp install` 写入的正是这个。
|
||||
|
||||
!!! note "本页讲的是本地场景"
|
||||
这里的一切都是在宿主所在的那台机器上运行你的服务器:宿主通过 stdio 启动你的文件。对个人工具或单机工具来说,这样做完全合适。要把服务器交给 **没有** 你这个文件的人,给出去的是 **URL** 而不是命令:同一个 `mcp` 对象,通过 Streamable HTTP 提供服务。**[运行服务器](../run/index.md)** 用一张表讲清这个决策,**[部署与扩展](../run/deploy.md)** 则是从那里走到真实主机名的路线。
|
||||
|
||||
而且宿主不过是内置了 MCP 客户端的应用程序,所以你自己的 Python 也能扮演宿主的角色:**[客户端传输方式](../client/transports.md)** 用 `stdio_client(...)` 把同一个文件作为子进程启动,**[测试](testing.md)** 则一个进程都不起,直接在内存中连接它。
|
||||
|
||||
## Claude Desktop {#claude-desktop}
|
||||
|
||||
唯一一个 SDK 能替你配置的宿主:
|
||||
|
||||
```bash
|
||||
uv run mcp install server.py
|
||||
```
|
||||
|
||||
就这样。`mcp install` 导入该文件以读取服务器的名字,找到 Claude Desktop 的配置文件,然后把启动命令写进去。过程中它会把你的路径转换成绝对路径,省得你自己动手。
|
||||
|
||||
这里没有什么玄机。它写入的条目是这样的:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"Bookshop": {
|
||||
"command": "/absolute/path/to/uv",
|
||||
"args": [
|
||||
"run",
|
||||
"--frozen",
|
||||
"--with",
|
||||
"mcp[cli]==2.0.0",
|
||||
"mcp",
|
||||
"run",
|
||||
"/absolute/path/to/server.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这就是上一节的启动命令,外加三样东西:`uv` 的绝对路径、`--frozen`(让 `uv` 永远不会改写它碰巧挨着的锁文件),以及对你已安装的 `mcp` 版本的精确锁定。它最终写进 `claude_desktop_config.json`,该文件位于:
|
||||
|
||||
* **macOS**:`~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
* **Windows**:`%APPDATA%\Claude\claude_desktop_config.json`
|
||||
|
||||
这个文件可以手写。`mcp install` 存在的意义,就是让你手写时不会犯那个经典错误(相对路径)。
|
||||
|
||||
完全退出 Claude Desktop(不只是关掉窗口),再重新打开。
|
||||
|
||||
!!! warning
|
||||
如果 Claude Desktop 的配置 **目录** 还不存在,`mcp install` 会失败并报 `Claude app not found`。安装 Claude Desktop 并运行一次:目录正是这一步创建的。
|
||||
|
||||
!!! tip
|
||||
Claude Desktop 在它自己的进程中启动你的服务器,所以那里没有你 shell 里的环境变量。`uv run mcp install server.py -v API_KEY=abc123`(或 `-f .env`)会把它们记到条目的 `env` 字段里。`--name` 用来覆盖条目名;默认取服务器的 `name`。
|
||||
|
||||
## Claude Code {#claude-code}
|
||||
|
||||
没有文件要编辑。用 `claude` CLI 注册服务器;`--` 之后的所有内容就是启动命令。
|
||||
|
||||
```bash
|
||||
claude mcp add bookshop -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
在 Claude Code 会话中运行 `/mcp`,确认 `bookshop` 已连接,且它的工具已列出。
|
||||
|
||||
## Cursor {#cursor}
|
||||
|
||||
在项目根目录创建 `.cursor/mcp.json`。
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"bookshop": {
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
同样的 `command` 加 `args`,放在 Claude Desktop 也在用的 `mcpServers` 键下。服务器会出现在 Cursor 的 MCP 设置里,两个工具都已列出。
|
||||
|
||||
## VS Code {#vs-code}
|
||||
|
||||
在项目根目录创建 `.vscode/mcp.json`。
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"bookshop": {
|
||||
"type": "stdio",
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
和 Cursor 的文件有两处不同,也仅此两处:外层键是 `servers` 而不是 `mcpServers`,而且每个条目都声明自己的 `type`。确认信任提示之后,在命令面板中执行 **MCP: List Servers**,会看到 `bookshop` 正在运行。
|
||||
|
||||
!!! note
|
||||
需要 VS Code 1.99 或更高版本,安装 **GitHub Copilot** 扩展并登录(Copilot Free 就够),而且 Copilot Chat 必须处于 **Agent** 模式,因为别的模式都不会调用工具。
|
||||
|
||||
## 服务器没有出现 {#it-doesnt-show-up}
|
||||
|
||||
在改动任何宿主配置之前,先自己运行一遍启动命令:
|
||||
|
||||
```bash
|
||||
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
什么都不打印,也不返回。这种沉默是正确的:stdio 服务器正在等宿主先在 stdin 上开口(按 `Ctrl-C` 停止)。出现 traceback 或者立刻退出,那才是真正的 bug;现在可以直接读到它,而不用隔着宿主去猜。
|
||||
|
||||
一旦这条命令能停在那里等待,剩下的问题几乎总是下面三种之一:
|
||||
|
||||
* **相对路径。** 宿主从 **它自己的** 工作目录启动你的服务器,而不是你注册时所在的目录。在需要 `/absolute/path/to/server.py` 的地方写成了 `server.py`,是所有失败里最常见的一个。如果宿主连 `uv` 也找不到,那个路径同样必须是绝对路径。
|
||||
* **宿主还在跑旧配置。** 宿主在启动时读取配置。尤其是 Claude Desktop,必须 **完全退出**(不只是关掉窗口)再重新打开,对 `claude_desktop_config.json` 的修改才会生效。
|
||||
* **有东西在重定向窗口期之外写到了 stdout。** 在 stdio 上,stdout **就是** 协议。SDK 在提供服务期间会把已刷新的杂散输出重定向到 stderr,但在那之前就刷新到 stdout 的输出(包装脚本的回显、无缓冲进程里导入期间的 `print()`),或者直到解释器退出才排空的带缓冲 `print()`,都会递给宿主一条损坏的消息,宿主随即断开连接。用默认的 `logging` 配置记日志,它的 stderr handler 每条记录都会刷新;自定义 handler 同样必须避开 stdout。详见 **[日志](../handlers/logging.md)**。
|
||||
|
||||
Claude Desktop 为每个服务器各留一份日志:`mcp-server-<NAME>.log` 是你服务器的 stderr,和记录连接情况的 `mcp.log` 放在一起,macOS 上在 `~/Library/Logs/Claude` 下,Windows 上在 `%APPDATA%\Claude\logs` 下。
|
||||
|
||||
这三种之外的任何问题,去看 **[故障排查](../troubleshooting.md)**。
|
||||
|
||||
## 回顾 {#recap}
|
||||
|
||||
* **宿主**(Claude Desktop、IDE)运行一个 MCP 客户端,由它通过 stdio 把你的服务器作为子进程启动。连接就是给它一条启动命令。
|
||||
* 这条命令是 `uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py`:无需激活 venv,在任何目录下都能用。
|
||||
* **Claude Desktop** 是唯一一个 `mcp install` 能替你配置的宿主。它把同一条命令(外加 `uv` 的绝对路径、`--frozen`,以及对已安装版本的精确锁定)写进 `claude_desktop_config.json`,你永远不必自己动手。
|
||||
* **Claude Code** 用 `claude mcp add bookshop -- <launch command>`。**Cursor** 用 `.cursor/mcp.json`,放在 `mcpServers` 下。**VS Code** 用 `.vscode/mcp.json`,放在 `servers` 下,每个条目带一个 `type`。
|
||||
* 处处使用绝对路径,改完配置后重启宿主,并且绝不让 SDK 以外的任何东西写入 stdout。
|
||||
|
||||
本页每个宿主都用同一条命令连接到了同一个文件。至于这个文件能 **暴露** 什么,就是这套文档余下的内容:**[工具](../servers/tools.md)**、**[资源](../servers/resources.md)**,以及 **[运行服务器](../run/index.md)** 中 stdio 之外的每一种传输方式。
|
||||
96
i18n/zh/pages/get-started/testing.md
Normal file
96
i18n/zh/pages/get-started/testing.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
translation:
|
||||
sections: ['4926721070127497', c52a1de2b6b32f40, 2e410b412c25f314, 627195f7159e24ef]
|
||||
tool: 1
|
||||
---
|
||||
# 测试 {#testing}
|
||||
|
||||
Python SDK 提供了一个带**内存传输**的 `Client` 类:把服务器对象传给它,它就会直接连接上去。
|
||||
|
||||
不用子进程,不占端口,根本不走任何传输。思路和 FastAPI 的 `TestClient` 一样。
|
||||
|
||||
## 基本用法 {#basic-usage}
|
||||
|
||||
假设有一个简单的服务器,只有一个工具:
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/testing/tutorial001.py"
|
||||
```
|
||||
|
||||
要运行下面的测试,还需要两个额外的(开发)依赖项:
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add --dev pytest inline-snapshot
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install pytest inline-snapshot
|
||||
```
|
||||
|
||||
!!! info
|
||||
本文档假设你已经熟悉 [`pytest`](https://docs.pytest.org/en/stable/)。
|
||||
|
||||
下面的测试用 [`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/) 在一行里对整个结果对象做断言。它会把测试的输出记录成你看到的 `snapshot(...)` 字面量。如果不想用它,去掉这行 import,像其他任何测试一样对关心的字段做断言(`result.content[0].text == "3"`)即可。
|
||||
|
||||
下面是测试:
|
||||
|
||||
```python title="test_server.py"
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp import Client
|
||||
from mcp.types import CallToolResult, TextContent
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend(): # (1)!
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(): # (2)!
|
||||
async with Client(mcp, raise_exceptions=True) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_call_add_tool(client: Client):
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
# Drop the server identity stamp in `_meta`; it is not what this test is about.
|
||||
result.meta = None
|
||||
assert result == snapshot(
|
||||
CallToolResult(
|
||||
content=[TextContent(type="text", text="3")],
|
||||
structured_content={"result": 3},
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
1. 如果用的是 `trio`,就改为返回 `"trio"`。详见 [anyio 文档](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on)。
|
||||
2. 这个 fixture 产出一个已连接的客户端。每个接收 `client` 的测试,都会拿到一条连向同一服务器的全新内存连接。
|
||||
|
||||
这样就行了。接下来可以扩展测试,覆盖更多场景。
|
||||
|
||||
## 为什么用 `raise_exceptions=True`? {#why-raise_exceptionstrue}
|
||||
|
||||
可能出错的情况有两种,而这个标志只管其中一种。
|
||||
|
||||
**你的工具**内部抛出的异常不算协议失败。它会变成一个带 `is_error=True` 的普通结果,模型会读到其中的消息。`raise_exceptions` 不会改变这一点:不管有没有它,`call_tool` 返回的都是同一个 `is_error=True` 结果。有一整页专门讲这个:**[处理错误](../servers/handling-errors.md)**。
|
||||
|
||||
工具函数体**之外**的失败则不同。在 `Client(mcp)` 提供的这条连接上,服务器会先把它脱敏成一条笼统的 `"Internal server error"`,客户端才会看到。意外崩溃的细节绝不应该泄露给远程调用方。但在测试里,这恰恰是你**不**想要的,也正是 `raise_exceptions=True` 所改变的:测试看到的是真实的消息,而不是脱敏后的那条。
|
||||
|
||||
测试里就让它开着。它在生产代码中没有意义。
|
||||
|
||||
## 默认在进程内 {#in-process-by-default}
|
||||
|
||||
!!! note
|
||||
`Client(mcp)` 在进程内连接,默认**不区分协议时代**:它会先探测服务器,再选择合适的协议路径。如果测试要验证旧版(legacy)特有的语义(采样(sampling)或征询(elicitation)的推送、`message_handler`),就固定使用 `mode="legacy"`,并在这种情况下去掉 `raise_exceptions=True`:旧版连接本来就不做脱敏,而这个标志会让失败在服务器任务内部重新抛出,而不是抛到你的测试里。
|
||||
|
||||
也正是因为这一行,本文档才敢保证其中的示例都能跑通:每个示例文件都会在 SDK 自己的测试套件里跑一遍,而且几乎全都正是通过这个客户端。你用的,就是 SDK 用来测试自己的同一个工具。
|
||||
|
||||
现在你有了一个可用且经过测试的服务器。要把它接入真实的应用(Claude Desktop、IDE),见 **[连接到真实宿主](real-host.md)**;以其他任何方式对外提供它,见 **[运行服务器](../run/index.md)**。
|
||||
Loading…
Reference in a new issue