1. 为什么 MCP 服务器需要专门的测试
MCP 服务器暴露的不是「函数」而是「协议能力」——工具、资源、提示词都要通过 JSON-RPC 被外部调用。如果只测内部函数,就测不到「协议这一层」:参数校验、错误码、能力声明、消息往返。
一句话:MCP 测试的关键层是「协议边界」——不仅测逻辑对错,还要测「客户端看到的交互是否正确」。
1.1 测试层级
单元测试:工具 handler 内部逻辑(纯函数)
协议测试:经 in-memory 传输,模拟客户端完整调用
集成测试:真实传输(stdio/HTTP)+ 真实依赖
端到端测试:真实客户端 + 真实服务器 + 外部系统
2. in-memory 传输:协议测试的利器
SDK 提供 InMemoryTransport,让服务器与测试客户端在同一进程内互连,无需起进程/开端口——速度快、可控性强。
2.1 建立 in-memory 会话
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
async function createTestPair(server: Server) {
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: "test-client", version: "1.0.0" });
await client.connect(clientTransport);
await server.connect(serverTransport); // 服务器挂到另一侧
return { client, server };
}
2.2 测试工具调用
describe("search tool", () => {
it("返回搜索结果", async () => {
const { client } = await setup();
const res = await client.callTool({ name: "search", arguments: { query: "mcp" } });
expect(res.isError).toBe(false);
expect(res.content[0].text).toContain("结果");
});
it("参数缺失时返回错误", async () => {
const { client } = await setup();
const res = await client.callTool({ name: "search", arguments: {} });
expect(res.isError).toBe(true);
});
});
一句话:in-memory 传输让「协议级测试」快得像单元测试——同一进程、无网络、可断言到每一次 JSON-RPC 交互。
3. 协议级断言
3.1 工具列表断言
it("声明了正确的工具与 Schema", async () => {
const { client } = await setup();
const { tools } = await client.listTools();
expect(tools.map(t => t.name)).toEqual(["search", "summarize"]);
// 校验 inputSchema 有 required 字段
expect(tools[0].inputSchema.required).toContain("query");
});
3.2 资源与提示词断言
it("暴露资源模板", async () => {
const { client } = await setup();
const list = await client.listResources();
// 服务器在 capabilities 里声明了 resources
});
it("读取资源内容", async () => {
const { client } = await setup();
const res = await client.readResource({ uri: "config://app/settings" });
expect(res.contents[0].mimeType).toBe("application/json");
});
it("列出提示词", async () => {
const { client } = await setup();
const { prompts } = await client.listPrompts();
expect(prompts.length).toBeGreaterThan(0);
});
3.3 能力声明断言
// 校验服务器正确声明能力
const capabilities = server.getCapabilities();
expect(capabilities.tools).toBeDefined();
expect(capabilities.resources).toBeDefined();
一句话:协议断言测的是「契约」——工具名、Schema、资源模板、能力声明,客户端能看到的每一面都要可验证。
4. 请求驱动测试
有些测试要「主动向服务器发特定请求」验证行为,而不是用高层 API。
4.1 直接发 JSON-RPC
const res = await client.request(
{ method: "tools/call", params: { name: "search", arguments: { query: "x" } } },
CallToolResultSchema
);
4.2 测试通知
// 服务器推送 listChanged 后,客户端收到通知
const notified = new Promise(resolve => {
client.setRequestHandler(ResourceListChangedNotificationSchema, () => {
resolve(true);
});
});
server.sendNotification(ResourceListChangedNotificationSchema, {});
await expect(notified).resolves.toBe(true);
4.3 测试错误路径
// 未注册的方法
await expect(client.request({ method: "unknown/method", params: {} }))
.rejects.toMatchObject({ code: -32601 }); // Method not found
// 无效参数
await expect(client.callTool({ name: "search", arguments: "bad" }))
.rejects.toThrow();
一句话:请求驱动测试直接瞄准「协议行为」——错误码、通知、原始请求响应,把协议的每一个分支都钉死。
5. 端到端测试
5.1 stdio 端到端
import { spawn } from "child_process";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
async function spawnServer() {
const transport = new StdioClientTransport({
command: "node",
args: ["dist/server.js"],
stderr: "pipe",
});
const client = new Client({ name: "e2e", version: "1.0.0" });
await client.connect(transport);
return client;
}
it("真实进程可调用工具", async () => {
const client = await spawnServer();
const res = await client.callTool({ name: "search", arguments: { query: "x" } });
expect(res.isError).toBe(false);
});
5.2 外部依赖打桩
- 服务器调外部 API → 用 mock 服务器 / 录放(VCR)
- 数据库 → 测试库 / 事务回滚
- LLM(sampling)→ mock 客户端返回固定补全
5.3 端到端检查清单
- 真实启动流程(入口、配置加载)
- 鉴权(远程时 OAuth 流程)
- 超时与断连
- 环境变量/秘钥注入
一句话:端到端测试验证「真实入口 + 真实传输 + 真实依赖」的整条链路——单元测逻辑、协议测契约、端到端测真相。
6. 测试金字塔与 CI
6.1 金字塔
少量 端到端测试(真实进程/依赖)
中量 协议测试(in-memory + 模拟客户端)
大量 单元测试(工具 handler 内部逻辑)
6.2 CI 集成
# .github/workflows/mcp-test.yml
name: MCP Server Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm run build
- run: npm test # 单元 + 协议
- run: npm run test:e2e # 端到端
6.3 覆盖率关注点
- 工具 handler 分支覆盖
- 错误/超时路径
- 能力声明与模板匹配
- 通知触发路径
一句话:测试金字塔 + CI 让 MCP 服务器「每次提交都可回归」——协议测试量最大、端到端保底线、CI 守门。
7. 常见陷阱
| 陷阱 | 症状 | 解决 |
|---|---|---|
| 只测内部函数 | 协议错误漏测 | 加 in-memory 协议测试 |
| in-memory 未连接 | 调用失败 | connect 后再断言 |
| 依赖真实 LLM | 测试慢/不稳 | mock sampling 客户端 |
| 忽略错误路径 | 错误码不对 | 断言 -32601 等 |
| 端到端连外部服务 | flaky | 打桩 / 录放 |
| 无 CI | 回归无人管 | 集成测试到 CI |
8. 总结
MCP 服务器测试的要点可以概括为「协议先行、传输可换、CI 守门」:
| 层面 | 要点 |
|---|---|
| 单元测试 | 工具 handler 内部逻辑 |
| 协议测试 | in-memory 传输 + 协议级断言 |
| 请求驱动 | 直接断言错误码、通知、原始消息 |
| 端到端 | 真实进程 + 打桩外部依赖 |
| 金字塔 | 单元多、协议中、端到端少 |
| CI | 每次提交回归 + 覆盖率 |
MCP 服务器是「对外暴露的协议面」,测试的焦点必须从「函数正确」上升到「协议正确」——in-memory 传输让协议测试轻量,请求驱动让每个分支可验证,端到端保真实。把这三层测试织密,MCP 服务器才能放心上生产。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。