python-sdk(MCP Python SDK)入门指南:从零搭建、运行与测试你的第一个 MCP Server
【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk
本文是 MCP(Model Context Protocol)Python SDK 的官方入门页面(对应仓库 docs/get-started/index.md 及其德语译本)的深度解读。无论你是第一次接触 MCP,还是第一次接触这个 SDK,这篇指南都会带你从零开始:安装 SDK、编写并运行第一个服务器、用 MCP Inspector 调试、再通过内存客户端(in-memory client)为它写测试,最终把服务器接进真实的主机应用。读完本文,你将掌握一套可复制、可运行、有测试保障的 MCP Server 开发闭环。
入门路径总览
这份入门文档将整个上手过程拆成四个连续步骤,每一页都能独立阅读,但按顺序走完就能得到一个“可运行、已测试”的服务器:
- 安装 SDK —— 用
uv或pip安装mcp[cli]; - 编写第一个服务器 —— 用三个装饰器暴露 Tools、Resources、Prompts 三类原语;
- 连接真实主机 —— 把服务器跑进 Claude Desktop 或 IDE 等 LLM 应用;
- 编写测试 —— 用 SDK 自带的内存客户端写测试,不再靠猜。
需要强调的是,本文档的核心承诺是:文档里每一个代码块都是完整、可直接复制运行的文件,并且全部被 SDK 自身的测试套件真实执行过。下面分别展开。
运行示例代码:uv run mcp dev server.py
入门文档强调,所有代码块都可以直接复制使用,它们是完整可运行的文件,而不是残缺的片段。跟练的方式非常简单:把代码块粘贴进一个server.py,然后用 MCP Inspector 打开它:
uv run mcp dev server.pymcp dev是mcp[cli]额外安装项提供的命令行工具之一(连同mcp run、mcp install一起)。运行后它会打印一个 URL,在浏览器中打开即是 MCP Inspector —— 一个图形化的调试面板,按 Tools / Resources / Prompts 分标签页展示服务器暴露的能力,你可以逐个标签页操作验证(详见 docs/get-started/first-steps.md 的 “Try it” 一节)。Inspector 默认通过stdio与服务器通信,这是 MCP Server 能讲的传输方式之一;传输方式的完整讨论在 docs/run/index.md。
入门文档还给出了一条强烈建议:亲自把代码写(或复制)下来、修改、并在本地运行。只有在自己编辑器里跑一遍,你才能真正体会到这个 SDK 的设计意图——需要手写的代码极少、自动补全的流畅、以及类型检查在运行前就能拦截错误。
顺带一提安装细节(来自 docs/get-started/installation.md):SDK 发布在 PyPI 上,包名是mcp,要求Python 3.10+,当前文档描述的是v2稳定版本线。安装命令:
=== "uv"
```bash uv add "mcp[cli]" ```=== "pip"
```bash pip install "mcp[cli]" ```如果你从 v1 迁移过来,需要注意 v2 是包含破坏性变更的主版本,官方 迁移指南 逐条覆盖了这些变更;如果你的包还依赖mcp且暂未准备好迁移,请给版本加上上限<2(例如mcp>=1.28,<2),让未锁定的解析停留在 1.x 线。
“你不需要猜”:每个示例都被测试套件真实执行
这是入门文档最核心的方法论承诺。每个文档示例都是 SDK 仓库中docs_src/目录下的完整文件,并且每一个都被 SDK 的测试套件通过一个**内存客户端(in-memory client)**真实执行过。
所谓内存客户端,就是 SDK 提供的Client类——它既可以连接 URL、也可以启动子进程,同样也可以直接传入你的服务器对象进行进程内通信:
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)直接连接服务器对象。这与 FastAPI 的TestClient是同一个思路(详见 docs/get-started/testing.md)。
这条承诺带来的实际保障是:如果 SDK 的任何改动破坏了这些页面上的某个示例,CI 会先于页面变红。也就是说——你在这里读到的代码,就是真正在跑的代码。你在 Testing 页面会自己用上这个内存客户端,它同样是你测试自己服务器的标准方式。
源码证据:示例确实是被测试的
这个承诺不是空话,仓库里可以直接验证。示例源码位于 docs_src/first_steps/tutorial001.py,是一个注册了工具、资源模板和提示词的完整服务器:
from mcp.server import MCPServer mcp = MCPServer("Demo") @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b @mcp.resource("greeting://{name}") def greeting(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!" @mcp.prompt() def summarize(text: str) -> str: """Summarize a piece of text in one sentence.""" return f"Summarize the following text in one sentence:\n\n{text}"而测试套件 tests/docs_src/test_first_steps.py 正是通过Client(tutorial001.mcp)内存连接来验证页面上每一个论断:工具add的名称、描述(取自 docstring)、输入 schema(取自类型注解a: int, b: int);资源模板greeting://{name}在具体资源列表里不出现、直到提供name才能读取;提示词summarize的返回值渲染成一条 user 消息;以及服务器声明的 capabilities 字典与页面打印出的完全一致。测试测试文件 tests/docs_src/test_testing.py 则直接复刻了 docs/get-started/testing.md 页面上的测试用例。
第一步前必读:Host、Client、Server 与三类原语
虽然入门索引页本身篇幅不长,但它所导向的 first-steps 页面定义了理解整份文档的三个关键角色:
- Host(主机):LLM 应用本身,比如 Claude、IDE、Agent 运行时——用户正在对话的那个东西;
- Client(客户端):寄宿在 Host 内部、负责讲 MCP 的那一半;Host 每连接一个服务器就运行一个对应的 Client;
- Server(服务器):你用这个 SDK 构建的东西,它向 Client 暴露能力,但从不直接与模型对话。
你写的是 Server 那一侧;Host 是别人的产品。SDK 同时给了你Client——正是 Host 用来按 URL 连接服务器或以子进程方式拉起服务器所用的同一个类。
一个 Server 恰好只暴露三种东西,它们的本质区别在于由谁决定使用它们:
| 原语 | 由谁控制 | 是什么 | 示例 |
|---|---|---|---|
| Tools(工具) | 模型 | 模型为采取动作而调用的函数 | 一次 API 调用、一次数据库写入 |
| Resources(资源) | 应用 | 主机加载进模型上下文的数据 | 文件内容、API 响应 |
| Prompts(提示词) | 用户 | 用户按名称调用的可复用消息模板 | 斜杠命令、菜单项 |
“由谁控制”是整个划分的意义所在:工具因模型决定调用而执行;资源因应用决定模型需要而被附加;提示词因用户选中而被运行。如果你构建过 Web API,很容易类比:资源相当于GET(加载数据、不改变任何东西),工具相当于POST(做工作、可能有副作用),提示词没有 HTTP 对应物,更接近用户按名称运行的“已保存查询”。
三种原语各对应一个装饰器,注册的全部工作就这些——名称、描述、参数 schema 全部由 SDK 从函数本身读取(函数名、docstring、类型注解),你不需要单独声明任何一项。值得注意的还有两条导入路径:from mcp import Client和from mcp.server import MCPServer——并不存在from mcp import MCPServer。
能力声明(Capabilities):客户端只问服务器声明过的
当客户端连接时,服务器会声明它的capabilities:它会应答哪些请求族。客户端依据这份声明决定要问什么。你从未手写过它——MCPServer替你声明了。
你可以自己验证。在一个终端用 HTTP 方式运行服务器:
uv run mcp run server.py --transport streamable-http在另一个终端用客户端指向它(docs_src/first_steps/tutorial001_client.py 就是一个完整的最小客户端):
import anyio from mcp import Client async def main() -> None: async with Client("http://localhost:8000/mcp") as client: print(client.server_capabilities.model_dump(exclude_none=True)) if __name__ == "__main__": anyio.run(main)运行python client.py会打印类似这样的字典:
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}这个字典就是服务器声明的capabilities,它是每个连接客户端学习到的第一件事:
| 能力 | 客户端现在可以调用 |
|---|---|
tools | tools/list、tools/call |
resources | resources/list、resources/templates/list、resources/read |
prompts | prompts/list、prompts/get |
MCPServer服务全部三类原语,所以三者总是被声明。注意缺失的东西:completions(资源模板和提示词的参数自动补全)需要你写一个 handler,这个服务器没有,因此该能力缺席,一个守规矩的客户端也就不会去问。这条规则适用于所有可选能力:注册了它,能力就出现(docs/servers/completions.md 可以证明)。
在测试场景里,你可以跳过终端和端口,直接把服务器对象交给Client:Client(mcp)。
为服务器编写测试:内存客户端的完整用法
Testing 页面给出了完整的测试工作流。假设你有这样一个带单个工具的简单服务器(docs_src/testing/tutorial001.py):
from mcp.server import MCPServer mcp = MCPServer("Calculator") @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b运行下面的测试需要两个额外的(开发)依赖:
=== "uv"
```bash uv add --dev pytest inline-snapshot ```=== "pip"
```bash pip install pytest inline-snapshot ```inline-snapshot用于在一行内对完整的返回结果对象做断言——它把测试输出记录为你看到的snapshot(...)字面量;如果你不想用它,去掉该导入、像普通测试那样对关心的字段断言(如result.content[0].text == "3")即可。
然后是测试本身:
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}, ) )- 如果你使用
trio,把返回值改成"trio"即可——整个 SDK 都基于 anyio 编写,因此同时运行在asyncio和trio之上(详见 docs/get-started/installation.md 对 anyio 依赖的说明)。 - 这个 fixture 产出一个已连接的客户端。每个接收
client的测试都会获得一次全新的、连接到同一服务器的内存连接。
为什么测试中要开启raise_exceptions=True
有两种不同的事情可能出错,而这个开关只影响其中一种:
- 发生在你的工具内部的异常不是协议失败。它会变成一个普通结果,携带
is_error=True(如果是ToolError,模型会读到你的消息)。raise_exceptions不会改变这一点——无论开不开,call_tool都返回同样的is_error=True结果。完整讨论见 docs/servers/handling-errors.md。 - 工具体之外的失败则不同。在
Client(mcp)给到你的这条连接上,服务器会在客户端看到它之前把它净化成通用的"Internal server error"——你绝不应该把一次意外崩溃的细节泄露给远程调用方。而在测试里,这恰恰是你不想要的,raise_exceptions=True改变的正是这一点:你的测试看到真实消息而不是净化后的消息。
所以:在测试中保持开启;它在生产代码中没有意义。另外,Client(mcp)进程内连接默认是era-neutral(协议时代中立)的:它会探测服务器并选择合适的协议路径。如果你的测试要验证 legacy 专属语义(sampling/elicitation 推送、message_handler),才需要固定mode="legacy",并在那里去掉raise_exceptions=True——legacy 连接本来就不做净化,开启该标志反而会在服务器任务内部重新抛出失败,而不是在你的测试里。
正因为有了这一行式的内存客户端,文档才有底气承诺示例可用:每个示例文件都被 SDK 自己的测试套件执行,且几乎全部通过同一个客户端——你使用的正是 SDK 用来测试自身的工具。
下一步往哪走
一旦服务器跑起来,文档的其余部分就是一个参考手册,而不是课程——每一页都可以独立阅读,直接跳到你需要的地方:
- 服务器能提供什么(Tools、Resources、Prompts),见 Servers(服务器);
- 在你注册的函数内部可以访问到什么,见 Handlers(处理器内部);
- 如何把它摆到客户端面前(stdio、HTTP、你现有的 FastAPI 应用),见 Running your server(运行服务器);
- 如何构建另一侧——一个使用MCP 服务器的应用,见 Clients(客户端)。
按入门路径的次序,接下来是 连接真实主机(把这个服务器放进 Claude Desktop 或 IDE 里真正跑起来),然后是 编写测试(一页、一个内存客户端,从此不用再猜代码能不能跑)。之后每个原语都有自己的专属页面,从由模型驱动的那个开始:Tools。
小结
- 入门路径四步走:安装(installation)→ 第一个服务器(first-steps)→ 连接真实主机(real-host)→ 测试(testing);
- 快速上手命令是
uv run mcp dev server.py,配合 MCP Inspector 逐标签页验证; - 文档的每个示例都是 docs_src/ 下的完整文件,且全部被 tests/docs_src/ 中的测试套件通过内存客户端真实执行——代码即运行,CI 兜底;
Client(mcp)直接连接服务器对象:无子进程、无端口、无传输层,从第一天起就是你的测试脚手架;测试中记得开启raise_exceptions=True以看到真实异常。
【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考