服务端阅读 05月28日 06:32
如何对 MCP Server 进行测试?测试策略与最佳实践详解
MCP(Model Context Protocol)作为 AI 应用与外部工具交互的标准协议,在 2026 年已获得广泛采用——SDK 月下载量超过 9700 万次,Anthropic、OpenAI、Google、Microsoft、AWS 等主流厂商均已支持。随着 MCP Server 数量激增,如何系统地测试 MCP Server 成为开发者必须掌握的技能。本文将从测试层次、实战工具、代码示例和最佳实践四个维度,详细介绍 MCP Server 的测试策略。一、MCP 测试的层次结构有效的 MCP 测试应遵循测试金字塔原则,从底层到顶层依次覆盖:| 层次 | 目标 | 速度 | 覆盖范围 ||------|------|------|----------|| 单元测试 | 验证单个 Tool/Resource 函数的逻辑 | 快(毫秒级) | 代码路径 || 协议测试 | 验证 Server 是否正确实现 MCP 协议 | 中 | 协议边界 || 集成测试 | 验证 Client-Server 端到端通信 | 较慢 | 交互链路 || 性能测试 | 验证并发和负载下的稳定性 | 慢 | 性能指标 || 安全测试 | 验证认证、授权和注入防护 | 中 | 安全边界 |关键原则:大量单元测试打基础,适量协议测试守边界,少量端到端测试保信心。二、单元测试:直接测试 Tool 函数单元测试是最快的反馈环。对于 MCP Server,你无需启动真实服务器,直接测试 @mcp.tool() 装饰的函数即可。2.1 安装依赖pip install mcp pytest pytest-asyncio2.2 被测 Server 示例# server.pyfrom mcp.server.fastmcp import FastMCP, ToolErrorfrom typing import Optionalmcp = FastMCP("calculator")@mcp.tool()def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b@mcp.tool()def divide(a: float, b: float) -> float: """Divide a by b. Raises error if b is zero.""" if b == 0: raise ToolError("Division by zero is not allowed") return a / b@mcp.tool()def search_documents(query: str, max_results: Optional[int] = 10) -> list: """Search documents by query string.""" return [{"id": 1, "title": f"Result for {query}", "score": 0.95}]2.3 单元测试编写# test_unit.pyimport pytestfrom server import add, divide, search_documentsdef test_add(): assert add(2, 3) == 5 assert add(-1, 1) == 0def test_add_type_coercion(): with pytest.raises(Exception): add("not_a_number", 1)def test_divide(): assert divide(10, 2) == 5.0def test_divide_by_zero(): from mcp.server.fastmcp import ToolError with pytest.raises(ToolError, match="Division by zero"): divide(1, 0)@pytest.mark.parametrize("query,expected_len", [ ("test", 1), ("MCP protocol", 1),])def test_search_documents(query, expected_len): results = search_documents(query) assert len(results) == expected_len assert "title" in results[0]要点:单元测试直接调用 Python 函数,不走 MCP 协议,速度极快。重点覆盖正常路径、边界条件和错误处理。三、协议测试:使用 FastMCP Client 验证协议边界单元测试无法发现协议层的问题——比如 Tool 注册是否正确、参数 Schema 是否完整、返回格式是否符合规范。这时需要用 FastMCP Client 进行协议级测试。3.1 In-Process 协议测试FastMCP 提供了 FastMCPTransport,允许在不启动真实服务器的情况下,通过内存中的协议层进行测试:# test_protocol.pyimport pytestfrom mcp.server.fastmcp import FastMCPfrom fastmcp.client import Clientfrom fastmcp.client.transports import FastMCPTransportfrom server import mcp@pytest.fixtureasync def mcp_client(): """创建连接到真实 Server 实例的 Client""" async with Client(mcp) as client: yield client@pytest.mark.asyncioasync def test_tool_registration(mcp_client): """验证所有 Tool 都正确注册""" tools = await mcp_client.list_tools() tool_names = [t.name for t in tools] assert "add" in tool_names assert "divide" in tool_names assert "search_documents" in tool_names@pytest.mark.asyncioasync def test_tool_schema(mcp_client): """验证 Tool 的参数 Schema 描述正确""" tools = await mcp_client.list_tools() add_tool = next(t for t in tools if t.name == "add") assert "properties" in add_tool.inputSchema assert "a" in add_tool.inputSchema["properties"] assert "b" in add_tool.inputSchema["properties"]@pytest.mark.asyncioasync def test_call_tool_via_protocol(mcp_client): """通过协议层调用 Tool 并验证返回""" result = await mcp_client.call_tool("add", arguments={"a": 2, "b": 3}) assert result.data is not None@pytest.mark.asyncioasync def test_tool_error_handling(mcp_client): """验证 ToolError 是否正确通过协议传播""" with pytest.raises(Exception): await mcp_client.call_tool("divide", arguments={"a": 1, "b": 0})要点:FastMCPTransport 让你在进程内完成协议级验证,无需启动子进程或网络端口,兼顾了速度和真实性。3.2 Snapshot 测试:锁定 Tool 接口使用 pytest-inline-snapshot 可以锁定 Tool 列表和 Schema,防止意外变更:# test_snapshot.pyfrom inline_snapshot import snapshot@pytest.mark.asyncioasync def test_tool_list_unchanged(mcp_client): """Tool 列表不应发生意外变更""" tools = await mcp_client.list_tools() tool_info = [(t.name, t.description) for t in tools] assert tool_info == snapshot()首次运行时使用 pytest --inline-snapshot=fix,create 生成快照,后续运行会自动比对。四、集成测试:使用 MCP Inspector 交互式验证MCP Inspector 是官方提供的交互式测试工具,适合开发阶段手动验证 Server 行为。4.1 启动 Inspector# 方式一:通过 MCP CLI(推荐)mcp dev server.py# 方式二:直接使用 npxnpx @modelcontextprotocol/inspector@latest python server.pyInspector 会自动打开浏览器界面,提供以下功能:Tools 面板:列出所有注册的 Tool,支持逐个测试调用Resources 面板:查看和读取 Server 暴露的资源Prompts 面板:测试预定义的 Prompt 模板消息日志:实时查看 JSON-RPC 请求和响应4.2 使用配置文件测试对于需要环境变量的 Server,可以创建配置文件:{ "mcpServers": { "my-server": { "command": "python", "args": ["server.py"], "env": { "API_KEY": "test-key-123" } } }}在 Inspector 中加载此配置,即可模拟真实部署环境。五、性能测试:验证并发和负载能力AI Agent 可能会快速连续调用 Tool,你的 Server 需要承受并发压力。5.1 使用 locust 进行负载测试# locustfile.pyfrom locust import HttpUser, task, betweenclass MCPLoadTest(HttpUser): wait_time = between(0.5, 2) host = "http://localhost:3001" @task(3) def call_add_tool(self): """测试 add Tool 的并发调用""" payload = { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "add", "arguments": {"a": 1, "b": 2} }, "id": 1 } self.client.post("/mcp", json=payload) @task(1) def list_tools(self): """测试 Tool 列表查询""" payload = { "jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 2 } self.client.post("/mcp", json=payload)运行方式:locust -f locustfile.py --users 50 --spawn-rate 105.2 关键性能指标| 指标 | 建议阈值 | 说明 ||------|----------|------|| 单次 Tool 调用延迟 | < 500ms | AI Agent 对延迟敏感 || 并发 50 用户 P99 | < 2s | 峰值场景下的兜底 || 错误率 | < 1% | 稳定性基线 || 内存泄漏 | 无 | 长时间运行不增长 |六、安全测试:认证、授权与注入防护MCP 的动态交互特性引入了独特的安全挑战。根据 MCP 安全规范,需关注六大威胁向量。6.1 认证测试# test_security.pyimport pytest@pytest.mark.asyncioasync def test_unauthorized_access(): """未认证请求应被拒绝""" # 使用无效 token 的 Client 调用受保护 Tool # 预期返回 401/403 pass # 根据实际认证中间件实现@pytest.mark.asyncioasync def test_token_validation(): """过期和无效 token 应被正确识别""" pass # 根据实际认证方案实现6.2 输入注入测试@pytest.mark.asyncioasync def test_path_traversal_prevention(mcp_client): """路径遍历攻击应被阻止""" malicious_paths = [ "../../../etc/passwd", "..\\..\\windows\\system32", "/proc/self/environ" ] for path in malicious_paths: result = await mcp_client.call_tool( "read_file", arguments={"filepath": path} ) assert "error" in str(result).lower() or result.isError6.3 速率限制测试@pytest.mark.asyncioasync def test_rate_limiting(): """超出速率限制应返回 429""" # 快速发送大量请求 # 验证在限制阈值后收到 429 Too Many Requests pass # 根据实际限流策略实现七、Mock 外部依赖当 Tool 依赖外部 API 时,使用 Mock 隔离测试:# test_with_mocks.pyimport pytestfrom unittest.mock import AsyncMock, patch@pytest.mark.asyncioasync def test_search_with_mocked_api(mcp_client): """使用 Mock 测试依赖外部 API 的 Tool""" mock_response = [{"id": 1, "title": "Mocked Result"}] with patch("server.httpx.AsyncClient.get") as mock_get: mock_get.return_value.json.return_value = mock_response result = await mcp_client.call_tool( "search_documents", arguments={"query": "test"} ) assert result is not None mock_get.assert_called_once()八、CI/CD 集成将 MCP 测试集成到持续集成流水线中:# .github/workflows/test.ymlname: MCP Server Testson: [push, pull_request]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies run: pip install mcp[cli] pytest pytest-asyncio pytest-cov - name: Run unit tests run: pytest test_unit.py -v --cov=server --cov-report=xml - name: Run protocol tests run: pytest test_protocol.py -v - name: Coverage check run: pytest --cov-fail-under=80九、最佳实践总结遵循测试金字塔:70% 单元测试 + 20% 协议测试 + 10% 端到端测试优先使用 FastMCPTransport:在不启动真实服务器的情况下完成协议验证,速度快且可靠用 MCP Inspector 辅助开发:写自动化测试前先用 Inspector 手动验证行为Snapshot 锁定接口:防止 Tool Schema 意外变更导致 Client 端故障Mock 外部依赖:隔离测试,避免因第三方 API 不稳定导致测试闪烁覆盖安全边界:认证、授权、输入校验、速率限制必须测试CI 集成:每次提交自动运行测试,覆盖率目标 80%性能基线:设定延迟和并发阈值,定期跑负载测试十、测试工具速查表| 工具 | 用途 | 安装命令 ||------|------|----------|| pytest | 单元/协议测试 | pip install pytest pytest-asyncio || FastMCPTransport | 进程内协议测试 | pip install mcp || MCP Inspector | 交互式手动测试 | mcp dev server.py || pytest-cov | 覆盖率报告 | pip install pytest-cov || pytest-inline-snapshot | 接口快照测试 | pip install pytest-inline-snapshot || locust | 负载测试 | pip install locust || mcpmock | Mock MCP Server | pip install mcpmock |通过以上分层测试策略,你可以系统性地保障 MCP Server 的质量、安全性和性能,从开发阶段到生产环境全程覆盖。