MCP 服务器测试框架:in-memory 传输、协议断言与端到端测试

系统讲解 MCP 服务器测试:in-memory 传输的单元测试、工具/资源/提示词行为的协议级断言、客户端模拟与请求驱动、端到端测试(stdio/HTTP 传输)、错误与超时测试,以及测试金字塔与 CI 集成。

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 服务器才能放心上生产。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「AI工程」更多文章

  1. MCP 资源模板与订阅:URI 模板、ListChanged 通知与上下文注入
  2. MCP 的 OAuth 鉴权与会话:动态注册、PKCE 与令牌轮换
  3. MCP 客户端 SDK 深入:TypeScript 客户端 API、传输状态机与错误处理