MCP Server 写完以后,真正麻烦的往往不是“能不能启动”,而是它暴露出来的 tools、prompts、resources 是否符合客户端预期。来源文章的核心思路很直接:写一个 Python 命令行 MCP Client,通过 stdio 连接本地 MCP Server,然后逐项测试工具、提示词和资源。这比只看日志靠谱,也比直接接入大模型前盲测更可控。
为什么用 stdio 客户端测试
MCP Server 常见的本地开发形态是通过标准输入输出通信。客户端启动 server 进程,把 JSON-RPC 消息写入 stdin,再从 stdout 读取响应。这个模型有几个好处:
- 不需要先部署 HTTP 服务,适合本机开发和 CI 冒烟测试。
- 可以精确控制调用顺序,例如先
list_tools,再call_tool。 - 出错时边界清晰:启动失败、协议握手失败、工具执行失败可以分开定位。
对开发者来说,这类客户端的价值不是替代正式测试框架,而是提供一个“协议层探针”:server 到底暴露了什么,返回了什么,异常长什么样,都能被看见。
一个可改造的 Python MCP Client
下面示例假设你使用 Python MCP SDK,并且已经有一个可以通过命令行启动的 MCP Server。你需要把 SERVER_COMMAND 和 SERVER_ARGS 改成自己的 server 启动方式。
安装依赖:
python -m venv .venv
source .venv/bin/activate
pip install mcp
创建 test_mcp_client.py:
import asyncio
import json
from typing import Any
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
# 改成你的 MCP Server 启动命令。
# 例子:node build/server.js、python server.py、uv run my-mcp-server
SERVER_COMMAND = "python"
SERVER_ARGS = ["server.py"]
def print_json(title: str, value: Any) -> None:
print(f"\n=== {title} ===")
print(json.dumps(value, ensure_ascii=False, indent=2, default=str))
async def main() -> None:
params = StdioServerParameters(
command=SERVER_COMMAND,
args=SERVER_ARGS,
env=None,
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print_json("tools", tools)
prompts = await session.list_prompts()
print_json("prompts", prompts)
resources = await session.list_resources()
print_json("resources", resources)
# 可以这样实践:如果 server 暴露了 echo 工具,就调用它。
# 把 name 和 arguments 改成你的工具名称与参数结构。
tool_names = [tool.name for tool in tools.tools]
if "echo" in tool_names:
result = await session.call_tool(
"echo",
arguments={"text": "hello from python client"},
)
print_json("call_tool: echo", result)
if __name__ == "__main__":
asyncio.run(main())
运行:
python test_mcp_client.py
如果你的 server 不是 python server.py 启动,例如是 Node.js 项目,可以改成:
SERVER_COMMAND = "node"
SERVER_ARGS = ["dist/index.js"]
如果是通过 uv 启动:
SERVER_COMMAND = "uv"
SERVER_ARGS = ["run", "my-mcp-server"]
测 tools、prompts、resources 时看什么
list_tools() 不是只看名字。实际排查时更应该关注三个点:
- 工具名称是否稳定。客户端、Agent workflow、测试脚本通常会硬编码或配置工具名。
- 参数 schema 是否符合预期。字段名、必填项、类型变化都会破坏调用方。
- 错误返回是否可读。工具内部异常不要只表现为“server disconnected”。
Prompts 的测试重点不同。它们更像可参数化模板,要检查参数声明、返回消息结构、是否包含客户端需要的上下文。
Resources 则要看 URI、MIME type 和内容读取行为。一个 server 能列出 resource,不代表 read_resource 一定能稳定返回内容。可以继续把上面的脚本扩展成读取第一个 resource:
# 放在 list_resources() 之后
if resources.resources:
uri = resources.resources[0].uri
content = await session.read_resource(uri)
print_json(f"read_resource: {uri}", content)
把它放进 CI 做轻量回归
命令行客户端最适合做冒烟测试。可以这样实践:写一个只校验关键工具存在、关键调用能成功的脚本,然后在 CI 里跑。
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
REQUIRED_TOOLS = {"echo", "search_docs"}
async def main() -> None:
params = StdioServerParameters(command="python", args=["server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
names = {tool.name for tool in tools.tools}
missing = REQUIRED_TOOLS - names
if missing:
raise SystemExit(f"missing tools: {sorted(missing)}")
result = await session.call_tool("echo", {"text": "ci-check"})
if not result.content:
raise SystemExit("echo returned empty content")
print("MCP smoke test passed")
if __name__ == "__main__":
asyncio.run(main())
对应的 GitHub Actions 可以写成:
name: mcp-smoke-test
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -r requirements.txt
- run: python test_mcp_smoke.py
采用时的边界
这种测试方式很适合验证 MCP 协议层和 server 暴露面,但它不是完整的端到端测试。它不会替你判断大模型是否会正确选择工具,也不能覆盖真实用户工作流里的所有上下文。
建议把它放在三层测试中的第一层:
- 本地开发:快速列出 tools、prompts、resources,确认 server 没跑偏。
- CI 冒烟:检查关键能力没有被重命名、删掉或改坏参数。
- Agent 集成测试:再验证模型选择工具、组合提示词、处理资源的效果。
MCP Server 越像一个小型 API,越值得给它配一个小型客户端。stdio 客户端不复杂,但它能把“协议能不能通”和“工具能不能用”这两件事拆开,让问题更早、更具体地暴露出来。