MCP测试策略
3869 字约 13 分钟
AIAgentMCP测试
2026-07-24
MCP 测试是针对 Model Context Protocol 生态中 Server、Client 以及模型工具调用行为的系统性验证方法。它不仅仅是"API 测试的翻版"——MCP 涉及协议层、工具执行层和模型决策层三个独立的正确性维度,每个维度需要不同的测试策略。
一句话解释
MCP 测试要回答三个独立的问题:协议对不对?工具执行对不对?模型选对了没有?
核心问题
传统 API 测试只需要验证"请求-响应"是否正确。MCP 测试的复杂度来自三个独立层面的正确性:
- 协议正确性:JSON-RPC 消息格式、能力协商、生命周期是否符合 MCP 规范
- 工具执行正确性:给定正确的参数,工具是否返回了预期结果
- 模型决策正确性:LLM 是否在该调用工具时调用了正确的工具,并传入了正确的参数
这三者不是同一个测试问题。协议正确是基础设施问题,工具执行是业务逻辑问题,模型决策是行为验证问题。混淆这三者会导致测试覆盖盲区。
三个测试维度
协议正确(Protocol Correctness)
验证 MCP 协议层面的消息格式和交互流程是否合规:
- JSON-RPC 消息结构是否符合规范(
method、params、id字段) - 能力协商(capability negotiation)是否正确完成
- 生命周期(initialize → initialized → shutdown)是否完整
- 请求/响应/通知的区分是否正确
- 错误码和错误格式是否符合标准
# 协议正确性测试示例
def test_initialize_request_format():
msg = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {"tools": {}},
"clientInfo": {"name": "test-client", "version": "1.0"}
}
}
assert validate_jsonrpc(msg) # 消息结构合法
def test_capability_negotiation():
server_caps = {"tools": {"listChanged": True}}
client_caps = {"tools": {}}
negotiated = negotiate(client_caps, server_caps)
assert "tools" in negotiated # 双方都支持 tools工具执行正确(Tool Execution Correctness)
验证工具在给定输入时是否返回正确输出——这和传统函数的单元测试没有本质区别:
- 正常输入是否返回预期结果
- 边界值处理是否正确
- 参数校验是否严格(类型、必填、范围)
- 错误情况是否返回合理的错误信息
- 副作用(文件写入、数据库修改)是否符合预期
模型是否正确选择工具(Model Tool Selection)
验证 LLM 在面对用户请求时,是否选择了正确的工具并构造了合理的参数:
- 模型是否识别出需要调用工具的场景
- 是否选择了语义上最匹配的工具
- 传入的参数是否从用户意图中正确提取
- 在多个工具可用时,选择是否合理
- 是否需要拒绝调用工具(不该调用时不调动)
这个维度的测试天然具有非确定性,需要专门的策略来处理(见下方"模型相关测试")。
测试层次
从底层到顶层:
单元测试(Unit Tests)
测试工具函数的内部逻辑,不依赖 MCP 协议:
# 测试工具的核心逻辑,与 MCP 解耦
def test_search_logic():
results = search_engine("hello", dataset=sample_data)
assert len(results) == 3
assert results[0]["text"] == "hello world"
def test_parameter_validation():
with pytest.raises(ValidationError):
validate_params({"query": 123}) # query 应该是 stringSchema 测试
验证工具参数的 JSON Schema 定义和返回值格式:
- 输入参数是否符合声明的 Schema
- 返回值结构是否稳定、符合预期格式
- Schema 本身的定义是否正确(类型、required、enum 等)
def test_tool_input_schema():
schema = get_tool_schema("search")
assert schema["inputSchema"]["properties"]["query"]["type"] == "string"
assert "query" in schema["inputSchema"]["required"]
def test_tool_output_format():
result = call_tool("search", {"query": "test"})
assert isinstance(result, list)
assert all("type" in item for item in result)协议测试(JSON-RPC 消息格式)
验证 MCP 协议消息的结构和交互流程:
def test_tool_call_jsonrpc_format():
msg = build_tool_call_request("search", {"query": "test"})
assert msg["jsonrpc"] == "2.0"
assert msg["method"] == "tools/call"
assert msg["params"]["name"] == "search"
assert msg["params"]["arguments"] == {"query": "test"}
def test_error_response_format():
error = build_error_response(code=-32602, message="Invalid params")
assert "error" in error
assert error["error"]["code"] == -32602Client 测试
测试 Client 端逻辑——连接管理、能力发现、请求发送、响应处理:
- 连接建立和初始化流程
- 工具列表获取和缓存
- 请求超时和重试逻辑
- 断线重连行为
Server 测试
测试 Server 端逻辑——工具注册、请求处理、响应构造:
- 工具注册是否正确
- 请求路由是否正确
- 并发请求处理
- 资源清理
集成测试
Client 和 Server 联合测试:
async def test_client_server_integration():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
assert any(t.name == "search" for t in tools.tools)
result = await session.call_tool("search", {"query": "test"})
assert len(result.content) > 0端到端测试(E2E)
从用户请求到最终响应的完整链路:
async def test_e2e_search_flow():
user_input = "帮我搜索关于 MCP 的资料"
response = await agent.process(user_input)
assert response.tool_calls[0].name == "search"
assert "MCP" in response.tool_calls[0].arguments["query"]
assert "搜索结果" in response.final_answerMock 策略
Mock Server(测试 Client)
构造一个可控的 Mock Server 来测试 Client 行为:
class MockMCPServer:
def __init__(self):
self.tools = [
{"name": "search", "description": "Search data",
"inputSchema": {"type": "object", "properties": {"query": {"type": "string"}}}}
]
self.call_log = []
async def handle_call(self, name, arguments):
self.call_log.append((name, arguments))
if name == "search":
return {"content": [{"type": "text", "text": "mock results"}]}
raise ValueError(f"Unknown tool: {name}")适用场景:测试 Client 的工具选择逻辑、参数构造、错误处理。
Mock Client(测试 Server)
模拟 Client 发送请求来测试 Server 行为:
async def test_server_with_mock_client():
server = create_test_server()
# 直接调用 server 的处理函数
result = await server.handle_tool_call("search", {"query": "test"})
assert result["content"][0]["text"] == "expected result"适用场景:测试 Server 的工具执行逻辑、错误处理、权限检查。
Mock 外部依赖
工具通常依赖外部服务(数据库、API、文件系统),测试时需要 Mock:
- 数据库连接 → 内存数据库或 Mock 对象
- 外部 API → 固定响应的 Mock HTTP 客户端
- 文件系统 → 临时目录或内存文件系统
合约测试(Contract Testing)
合约测试确保 Client 和 Server 之间的接口约定保持一致。当 Schema 发生变化时,合约测试能立即发现不兼容。
def test_tool_contract():
"""验证 search 工具的输入输出合约"""
# 输入合约
schema = server.get_tool_schema("search")
assert schema["inputSchema"]["required"] == ["query"]
# 输出合约
result = server.call_tool("search", {"query": "test"})
assert_valid_output(result, expected_schema=TOOL_OUTPUT_CONTRACT)合约测试的价值:在 Client 和 Server 独立开发时,防止接口变更导致的运行时错误。建议将合约定义为独立文件,双方都依赖同一份合约。
特殊测试场景
错误注入(Error Injection)
测试系统在异常情况下的行为:
- 工具执行抛出异常时,是否返回正确的 JSON-RPC 错误
- Server 进程崩溃时,Client 是否能检测到并恢复
- 网络中断时,正在进行的请求如何处理
def test_tool_execution_error():
# 模拟工具执行失败
server.register_tool("failing_tool", lambda: raise_exception("boom"))
response = client.call_tool("failing_tool", {})
assert response["error"]["code"] == -32603
assert "boom" in response["error"]["message"]超时测试
- 工具执行超时后是否正确终止
- Client 是否在超时后返回错误而不是无限等待
- 超时配置是否生效
async def test_tool_timeout():
with pytest.raises(TimeoutError):
await session.call_tool("slow_tool", {}, timeout=5.0)并发测试
- 多个 Client 同时调用同一 Server
- 同一 Client 同时发送多个请求
- 共享资源在并发场景下是否存在竞争
权限测试
- 未授权的工具调用是否被拒绝
- 资源访问权限是否正确控制
- 参考 MCP安全边界 中的权限模型
安全测试
- 参数注入(SQL 注入、命令注入)是否被防范
- 敏感信息是否在响应中泄露
- 工具是否被滥用(如路径穿越攻击)
回归测试
- 每次修改后验证已有工具的行为未变
- Schema 变更是否向后兼容
- 历史 bug 是否被修复且不再复发
模型相关测试
模型调用测试
验证模型在不同场景下的工具选择行为:
test_cases = [
{
"user_input": "搜索 MCP 相关资料",
"expected_tool": "search",
"expected_params": {"query": "MCP"}
},
{
"user_input": "今天天气怎么样",
"expected_tool": None, # 不该调用任何工具
"reason": "超出工具能力范围"
}
]
@pytest.mark.parametrize("case", test_cases)
async def test_model_tool_selection(case):
response = await agent.process(case["user_input"])
if case["expected_tool"] is None:
assert len(response.tool_calls) == 0
else:
assert response.tool_calls[0].name == case["expected_tool"]Tool 描述测试
工具的描述质量直接影响模型的选择准确率。测试方法:
- 给定工具描述,让模型判断该工具是否适用于特定场景
- 检查描述是否包含关键信息:功能、参数含义、返回内容
- 对比不同描述版本下的选择准确率
def test_tool_description_clarity():
"""验证工具描述是否足够清晰"""
tool = get_tool("search")
assert len(tool["description"]) > 20 # 描述不能太短
assert "参数" in tool["description"] or "query" in tool["inputSchema"]["properties"]非确定性测试
模型输出具有随机性,单次测试不可靠。应对策略:
- 多次采样:同一输入运行 N 次,统计正确率(如 10 次中至少 8 次正确)
- 温度控制:测试时将 temperature 设为 0 或接近 0
- 种子固定:如果模型支持,固定 random seed
- 断言宽松化:不要求精确匹配,而是检查关键属性
async def test_model_with_sampling():
correct = 0
for _ in range(10):
response = await agent.process("搜索 Python 教程")
if response.tool_calls and response.tool_calls[0].name == "search":
correct += 1
assert correct >= 8, f"准确率 {correct}/10 低于阈值"测试夹具(Test Fixtures)
为测试提供可复用的预设环境:
@pytest.fixture
def mock_server():
"""提供一个预配置的 Mock Server"""
server = MockMCPServer()
server.register_tool("search", mock_search)
server.register_tool("calculate", mock_calculate)
return server
@pytest.fixture
def test_client(mock_server):
"""提供一个连接到 Mock Server 的 Client"""
client = TestClient(mock_server)
yield client
client.close()
@pytest.fixture
def sample_data():
"""提供测试用的样本数据"""
return load_fixture("sample_dataset.json")好的 fixture 设计原则:
- 每个 fixture 职责单一
- fixture 之间可以组合
- 测试结束后自动清理
- 避免 fixture 之间的隐式依赖
CI 集成
MCP 测试在 CI 中的关键考虑:
- Server 生命周期管理:CI 中需要正确启动和关闭 Server 进程
- 环境变量隔离:测试环境和生产环境的配置分离
- 超时控制:CI 环境可能较慢,合理设置超时
- 并行执行:不同测试套件可以并行,但要避免端口冲突
- 模型 API 调用:涉及真实模型调用的测试应标记为可选或单独运行
# GitHub Actions 示例
jobs:
test:
steps:
- name: Run unit and protocol tests
run: pytest tests/unit tests/protocol -v
- name: Run integration tests
run: pytest tests/integration --timeout=60 -v
- name: Run model tests (optional)
if: env.MODEL_API_KEY != ''
run: pytest tests/model --timeout=120 -v测试工具
MCP Inspector
官方提供的调试和测试工具,可以:
- 可视化 Server 的能力(tools、resources、prompts)
- 手动发送请求并查看响应
- 验证 JSON-RPC 消息格式
- 调试连接和通信问题
适合开发阶段的交互式测试,不适合自动化测试流水线。
测试框架选择
- pytest(Python):推荐,生态丰富,支持异步测试
- vitest / jest(TypeScript):适合 TS 生态的 MCP 项目
- pytest-mcp:社区 MCP 测试插件(如可用)
- 自定义 harness:对于模型测试,通常需要自定义的评估框架
完整示例:一个 Tool 的测试用例设计
以 search 工具为例,完整的测试用例覆盖:
| 测试层次 | 测试内容 | 验证目标 |
|---|---|---|
| 单元测试 | search(query, dataset) 函数逻辑 | 搜索结果正确性 |
| Schema 测试 | 参数类型、必填字段 | Schema 定义与实现一致 |
| 协议测试 | tools/call 消息格式 | JSON-RPC 格式合规 |
| Server 测试 | Server 处理 search 请求 | 路由和执行正确 |
| Client 测试 | Client 发送 search 请求 | 参数构造正确 |
| 集成测试 | Client ↔ Server 完整调用 | 端到通信正确 |
| 模型测试 | 用户说"找一下…"时选择 search | 模型选择正确 |
| 错误注入 | 传入非法 query | 错误处理正确 |
| 超时测试 | search 执行超过 5 秒 | 超时处理正确 |
| 安全测试 | 注入 SQL 到 query 参数 | 无注入漏洞 |
设计原则
- 分层测试,独立验证:三个维度的正确性要分别测试,不要混在一起
- 协议与业务分离:协议合规性测试和工具逻辑测试使用不同的测试套件
- Mock 适度:单元测试充分 Mock,集成测试尽量少 Mock,E2E 不用 Mock
- 模型测试宽容化:模型测试允许一定的失败率,用统计方法而非精确断言
- 测试可重复:避免依赖外部状态,使用 fixture 和 seed 保证可重复性
- 快速反馈:单元测试和协议测试必须快,模型测试可以异步运行
常见误区
| 误区 | 正确做法 |
|---|---|
| 只测试工具逻辑,忽略协议合规 | 协议测试独立进行 |
| 用精确匹配断言模型输出 | 用统计方法和宽松断言 |
| E2E 测试中 Mock 太多,失去意义 | E2E 尽量使用真实组件 |
| 模型测试跑一次就下结论 | 多次采样,统计准确率 |
| 工具描述随便写,不测试 | 描述质量直接影响模型表现,需要测试 |
| 所有测试都跑在 CI 上 | 模型测试可选运行,避免 CI 不稳定 |
| 测试数据和生产数据混用 | 使用独立的测试 fixture |
实践检查清单
与其他概念的关系
- MCP基础:协议基础决定了协议测试的标准
- MCP Server设计:Server 设计质量直接影响可测试性
- MCP Client设计:Client 设计决定了 Client 测试的切入点
- MCP Tool能力:工具的 Schema 定义是 Schema 测试的依据
- MCP安全边界:安全测试的范围由安全模型决定
- MCP消息与数据模型:消息格式是协议测试的直接验证对象
- MCP项目实践:实践中的测试经验反哺测试策略
适用边界
- 适用:所有 MCP Server 和 Client 的开发和验证
- 适用:涉及 MCP 工具调用的 Agent 行为验证
- 不适用:纯模型能力评估(如 benchmark),那是模型评测的范畴
- 不适用:非 MCP 协议的 Tool Use 测试(如 OpenAI Function Calling 有自己的测试策略,但思路可参考)
- 局限:模型测试的覆盖率受限于测试用例的设计,无法穷举所有可能的用户输入