ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

使用 python-sdk 的内存 Client 编写 MCP 服务器测试:无端口、无子进程的单元测试实战

使用 python-sdk 的内存 Client 编写 MCP 服务器测试:无端口、无子进程的单元测试实战 使用 python-sdk 的内存 Client 编写 MCP 服务器测试无端口、无子进程的单元测试实战【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk本篇指南聚焦 Model Context Protocol 官方 Python SDKpython-sdk中最实用的一条测试路径Client类不仅可以通过 URL 或子进程连接远程 MCP 服务器还可以直接接收一个服务器对象在内存中建立连接。读完本文你将掌握如何为 MCP 服务器编写基于pytest的单元测试、理解raise_exceptionsTrue的精确语义、以及为什么Client(server)默认对协议版本保持中立era-neutral从而让测试既快又稳。内存连接与 FastAPI TestClient 同源的设计思路SDK 的Client类是一个高度抽象的统一入口。从 src/mcp/client/client.py 的类定义可以看到它的第一个位置参数server接受四种不同的对象URL 字符串通过streamable_http_client传输层走 Streamable HTTPStdioServerParameters以子进程方式启动命令通过 stdin/stdout 通信任意Transport实例直接复用自定义传输层Server或MCPServer实例在进程内in-process直接连接也就是本文的核心用法。后一种模式与 FastAPI 的TestClient是同一个思路没有子进程、没有端口、没有网络连接No subprocess. No port. Nothing on a wire.。你只需要把你的服务器对象传给Client客户端就会在同一个进程里直接与它对话。这意味着测试无需启动任何后台服务运行速度接近纯函数调用且天然适合 CI 环境。从源码看这个分派发生在Client.__post_init__中src/mcp/client/client.py当server是MCPServer时先解包为底层Server再交由_connect_inproc建立进程内连接src/mcp/client/client.py。该连接器在legacy模式下通过InMemoryTransport驱动流循环在现代模式下则直接创建一对DirectDispatcher无需 JSON-RPC 帧、无 initialize 握手走逐请求的分发路径。基本用法为单工具服务器编写第一个测试一个简单的服务器假设你有一个只暴露单个工具tool的服务器代码与 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安装测试依赖要运行下面的测试需要两个额外的开发依赖pytest和inline-snapshot。二选一安装 uvbash uv add --dev pytest inline-snapshot pipbash pip install pytest inline-snapshot 说明本文档假设你已经熟悉pytest的基本用法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_exceptionsTrue) 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(typetext, text3)], structured_content{result: 3}, ) )两个 fixture 的要点对应代码中的注释标记anyio_backend告诉 anyio 的 pytest 插件使用哪个异步后端。这里返回asyncio如果你使用trio则返回trio。更细节可参考 anyio 官方测试文档。clientfixture产出的是一个已连接的客户端。每个接收client参数的测试都会获得一条指向同一服务器的全新内存连接测试之间互不污染。pytest.mark.anyio标记让测试函数在所选后端上以异步方式执行call_tool直接返回完整结果对象因此可以像普通值一样与快照比较。这段文档示例并非纸上谈兵——仓库自带的测试 tests/docs_src/test_testing.py 真实运行着同一个测试仅导入路径不同并且额外做了两件事通过pytestmark将MCPDeprecationWarning视为错误以及在比较前调用strip_server_info剥离 2026 时代的serverInfo印记见 tests/docs_src/_helpers.py从而保证文档中展示的断言结果与真实运行完全一致。为什么测试中要开raise_exceptionsTrue这是本文档最值得深究的一个细节。有两类不同的失败可能发生而这个开关只影响其中一类。工具内部的异常不属于协议失败如果你的某个工具函数内部抛出异常这并不构成协议失败。服务端会把它转换成携带is_errorTrue的普通结果返回如果抛出的是ToolError模型还能直接读到你的自定义消息。raise_exceptions对此没有任何影响无论开关与否call_tool都返回同一个is_errorTrue的结果。关于服务器端错误处理的完整讲解见 服务器错误处理。工具之外的失败会被中和成通用错误而工具函数体之外的失败则完全不同。在Client(mcp)建立的内存连接上服务器会把这类意外崩溃先中和sanitise成一个通用的Internal server error客户端看到的永远是这个脱敏消息。这是生产环境的正确行为——你绝不应该向远端调用方泄露一次意外崩溃的内部细节。但在测试中这恰恰是你不想要的你希望看到真实报错来定位问题。raise_exceptionsTrue改变的正是这一点你的测试看到真实消息而不是被中和的版本。从源码可以印证这条中和链路在 src/mcp/server/runner.py 的modern_error_data中MCPError与ValidationError会按共享的异常映射阶梯转换其余任何异常都会被记录到服务端日志并向客户端返回INTERNAL_ERROR消息即Internal server error——handler internals never reach the wire处理函数内部细节永不落到线路上。而InMemoryTransportsrc/mcp/client/_memory.py会把raise_exceptions参数透传给server.run(...)正是这个参数让真实异常得以穿透。结论在测试里保持开启raise_exceptionsTrue它在生产代码中没有意义。默认对协议版本保持中立Era-neutralClient(mcp)的进程内连接默认对协议代际保持中立它会探测服务器server/discover自动选择合适的协议路径。这在Client.mode参数上有明确体现src/mcp/client/client.pyauto默认探测server/discover对传统服务器回退到 initialize 握手对于进程内的Server/MCPServer则直接分发、不经过 JSON-RPC 帧legacy强制走传统的 initialize 握手与 2026 之前的字节级行为一致显式的现代协议版本字符串如2026-07-28直接采用该版本、跳过探测。因此如果你的测试要覆盖传统连接特有的语义——例如采样sampling或引导elicitation的推送、message_handler回调——请把modelegacy固定下来并同时去掉raise_exceptionsTrue传统连接本身从来不做脱敏而该开关在 legacy 模式下会把失败重新抛到服务器任务内部而不是抛到你的测试里。SDK 用它测试它自己为什么文档示例保证可运行文档敢承诺这些示例都能跑其底气正在于这一行代码仓库中的每一个示例文件都被 SDK 自身的测试套件真实执行而其中几乎全部都是通过这个Client内存连接方式驱动的。也就是说你在测试里用的工具就是 SDK 用来测试自己的同一个工具。以本文的教程为例示例服务器源文件docs_src/testing/tutorial001.py对应的真实测试tests/docs_src/test_testing.py其中async with Client(mcp, raise_exceptionsTrue) as client与文档代码完全同构。这种自举dogfooding模式的好处是文档代码、示例与真实测试三者在同一套 CI 中持续验证任何 API 变更导致示例失效都会被立即发现。你可以放心地把Client(server)内存连接模式作为自己测试体系的基础设施再按需扩展覆盖更多场景——比如断言更多工具、测试错误路径、或覆盖资源与提示prompt的读取。从测试走向真实运行到这里你已经拥有了一个可运行、已被测试的 MCP 服务器。接下来将它接入真实应用如 Claude Desktop、IDE阅读 连接到真实主机了解其他所有运行服务器的方式阅读 运行你的服务器。总结Client的内存连接模式是 python-sdk 为服务器开发者准备的一等测试设施——零网络开销、默认协议中立、配合raise_exceptionsTrue直击真实错误。理解这两三个开关背后的语义你的 MCP 服务器测试就能同时做到快速、准确且与 SDK 自身保持同一水准。【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表