MCP服务器测试工具包

为Model Context Protocol (MCP)服务器提供全面的测试、调试和日志记录工具,包括智能快照测试、自定义匹配器和安全的调试日志记录。

  • by gnana997
  • TypeScript
  • 本地部署
  • 1 星标
  • 更新于
在 GitHub 查看 gnana997/mcp-dev-kit

使用说明

mcp开发工具包

模型上下文协议(MCP)服务器的完整测试和调试工具包

使用全面的测试工具、智能快照测试和stdio安全调试日志构建可靠的MCP服务器。

为什么选择mcp开发工具包?

开发MCP服务器面临着独特的挑战:

  • ❌ 测试很难 -MCP服务器没有内置测试实用程序
  • ❌ 快照中断 -每次运行时,时间戳和ID都会发生变化
  • ❌ 日志记录中断stdio - console.log() 破坏JSON-RPC通信
  • ❌ 手动断言 -用于常见检查的重复样板

mcp-dev-kit解决了所有这些问题:

  • ✅ MCPTestClient -MCP服务器的全功能测试客户端
  • ✅ 智能快照 -自动排除动态字段(时间戳、ID)
  • ✅ 定制火柴 -工具、资源、提示的可读断言
  • ✅ 安全记录 -调试时不破坏JSON-RPC协议

快速开始

安装

npm install --save-dev mcp-dev-kit vitest

基本测试设置

1.创建 vitest.setup.ts:

import { installMCPMatchers } from 'mcp-dev-kit/matchers';

installMCPMatchers();

2.配置 vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    setupFiles: ['./vitest.setup.ts'],
  },
});

3.编写测试(server.test.ts):

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { MCPTestClient } from 'mcp-dev-kit/client';

describe('My MCP Server', () => {
  let client: MCPTestClient;

  beforeAll(async () => {
    client = new MCPTestClient({
      command: 'node',
      args: ['./my-server.js'],
    });
    await client.connect();
  });

  afterAll(async () => {
    await client.disconnect();
  });

  it('should list available tools', async () => {
    const tools = await client.listTools();
    expect(tools).toHaveLength(2);
    expect(tools[0]).toHaveToolProperty('name', 'echo');
  });

  it('should execute tools successfully', async () => {
    const result = await client.callTool('echo', { message: 'hello' });
    await expect(result).toReturnToolResult('hello');
  });

  it('should have stable response structure', async () => {
    const result = await client.callTool('list_files', { path: '/' });
    expect(result).toMatchToolResponseSnapshot();
  });
});

4.将调试日志添加到服务器:

// At the top of your MCP server file
import 'mcp-dev-kit/logger';

// Now console.log works without breaking JSON-RPC!
console.log('Server started');
console.error('Connection error', error);

5.运行测试:

npx vitest

特性

MCP测试客户端

通过stdio生成和测试MCP服务器的综合测试客户端。

import { MCPTestClient } from 'mcp-dev-kit/client';

const client = new MCPTestClient({
  command: 'node',
  args: ['./my-server.js'],
  env: { DEBUG: 'true' },
  timeout: 30000,
});

await client.connect();

// Test server capabilities
const serverInfo = client.getServerInfo();
const capabilities = client.getServerCapabilities();

// List and call tools
const tools = await client.listTools();
const result = await client.callTool('my-tool', { param: 'value' });

// List and read resources
const resources = await client.listResources();
const content = await client.readResource('file://config.json');

// List and get prompts
const prompts = await client.listPrompts();
const prompt = await client.getPrompt('greeting', { name: 'Alice' });

// Helper methods for common patterns
const toolResult = await client.expectToolCallSuccess('my-tool', { input: 'test' });
const error = await client.expectToolCallError('bad-tool', {});

await client.disconnect();

主要特点:

  • 自动流程生命周期管理
  • 请求/响应与超时匹配
  • 服务器通知处理
  • 全面的错误处理
  • TypeScript优先,具有完全类型安全性

定制Vitest配对器

用于MCP特定测试的可读、富有表现力的断言。

import { installMCPMatchers } from 'mcp-dev-kit/matchers';

installMCPMatchers();

可用匹配器:

// Tool assertions
await expect(client).toHaveTool('echo');
const tools = await client.listTools();
expect(tools[0]).toHaveToolProperty('description', 'Echoes back the message');
expect(tools[0]).toMatchToolSchema({
  type: 'object',
  required: ['message']
});

// Resource assertions
await expect(client).toHaveResource('config://app.json');
const resources = await client.listResources();
expect(resources[0]).toHaveProperty('uri', 'config://app.json');

// Prompt assertions
await expect(client).toHavePrompt('greeting');
const prompts = await client.listPrompts();
expect(prompts[0]).toHaveProperty('name', 'greeting');

// Tool result assertions
const result = await client.callTool('echo', { message: 'test' });
await expect(result).toReturnToolResult('test');
await expect(client.callTool('unknown', {})).toThrowToolError();

优点:

  • 清晰、自文档化的测试代码
  • 测试失败时更好的错误消息
  • 减少测试文件中的样板
  • 使用TypeScript进行类型安全

快照测试

MCP感知快照测试,具有智能现场排除功能。

为什么要对MCP进行快照测试?

MCP服务器响应通常包含每次运行时都会发生变化的动态数据:

  • 时间戳(2024-11-03T10:30:00.000Z)
  • 请求ID(abc123)
  • 执行时间(42.5ms)
  • 自动递增ID、文件索引节点、git SHA

每次运行时,常规快照测试都会失败。 mcp-devkit会自动排除这些字段 同时捕获稳定的响应结构。

快速示例

import { installMCPMatchers } from 'mcp-dev-kit/matchers';

installMCPMatchers();

describe('File System Server', () => {
  it('should return consistent file listing structure', async () => {
    const result = await client.callTool('list_files', { path: '/project' });

    // Timestamps, IDs, and dynamic fields automatically excluded!
    expect(result).toMatchToolResponseSnapshot();
  });

  it('should have stable tool definitions', async () => {
    const tools = await client.listTools();

    // Captures tool schemas for regression detection
    expect(tools).toMatchToolListSnapshot();
  });

  it('should snapshot custom data structures', async () => {
    const data = {
      users: [...],
      timestamp: new Date().toISOString(), // Auto-excluded
      requestId: 'abc123',                   // Auto-excluded
    };

    expect(data).toMatchMCPSnapshot();
  });
});

可用的快照匹配器

toMatchMCPSnapshot(options?) -任何MCP数据的通用快照匹配器

expect(serverResponse).toMatchMCPSnapshot();
expect(data).toMatchMCPSnapshot({ exclude: ['user.id', 'files.*.size'] });

toMatchToolResponseSnapshot(options?) -工具调用结果

const result = await client.callTool('query_database', { query: 'SELECT * FROM orders' });
expect(result).toMatchToolResponseSnapshot();

toMatchToolListSnapshot(options?) -关于工具定义

const tools = await client.listTools();
expect(tools).toMatchToolListSnapshot();

toMatchResourceListSnapshot(options?) -关于资源列表

const resources = await client.listResources();
expect(resources).toMatchResourceListSnapshot();

toMatchPromptListSnapshot(options?) -用于快速定义

const prompts = await client.listPrompts();
expect(prompts).toMatchPromptListSnapshot();

智能默认值

这些字段是 自动排除 从所有快照中:

  • timestamp
  • requestId
  • executionTime
  • cacheKey
  • _meta.timestamp
  • serverInfo.startedAt
  • serverInfo.uptime

例子:

// Original response
{
  "users": [...],
  "timestamp": "2024-11-03T10:30:00.000Z",  // ❌ Excluded
  "requestId": "abc123",                     // ❌ Excluded
  "executionTime": 42.5                      // ❌ Excluded
}

// Snapshot (only stable data)
{
  "users": [...]  // ✅ Captured
}

自定义排除

使用glob模式排除其他字段:

// Exclude file system-specific fields
expect(result).toMatchToolResponseSnapshot({
  exclude: ['files.*.size', 'files.*.inode', 'files.*.modified']
});

// Exclude all auto-increment IDs
expect(data).toMatchMCPSnapshot({
  exclude: ['*.id', '*.userId', 'rows.*.orderId']
});

// Exclude nested timestamps with custom names
expect(response).toMatchMCPSnapshot({
  exclude: ['data.users.*.createdAt', 'metadata.generatedAt']
});

模式语法:

  • field -不包括顶级字段
  • nested.field -排除嵌套字段
  • array.*.field -从所有数组项中排除字段
  • data.users.*.createdAt -不包括 createdAt 来自所有用户 data.users

演出

带有属性排除的快照测试增加的开销可以忽略不计:

数据大小 标准化开销 备注
10-100个项目 <50微秒 典型的MCP响应
1000个项目 <50微秒 响应量大
5000个项目 <50微秒 超大响应
深度嵌套(10+级) ~1-2毫秒 实践中很少

性能特点:

  • 与数据大小配合良好 -更多项目≈类似的开销
  • 随着嵌套深度的增加而退化 -结构越深=速度越慢
  • 生产准备就绪 -典型MCP响应的开销<1%

关于基准的说明: 我们的基准测试衡量预热后JIT优化的代码。现实世界中的“冷启动”性能可能略有不同。所有测量值均不包括快照文件I/O(由Vitest处理)。

欢迎反馈! 我正在积极测试此功能,希望得到您的反馈!如果您遇到性能问题或有建议,请 在GitHub上打开一个问题.

更新快照

当您有意更改服务器的响应格式时:

# Review what changed
npm test

# Update snapshots after verifying changes are correct
npm test -- -u

最佳实践

✅ 做:

  • 用于捕获回归的快照服务器响应结构
  • 对常见动态字段使用智能默认值
  • 快照中小型数据集(10-1000个项目)
  • 将快照与关键属性的显式断言相结合
  • 在接受更改之前查看快照差异
  • 将大型响应拆分为集中的较小快照

❌ 不要:

  • 不排除动态字段(时间戳、ID等)的快照
  • 创建数兆字节的快照(拆分为更小的测试)
  • 可能经常更改的快照实施细节
  • 使用以下命令盲目更新快照 -u 未审核的标志
  • 使用快照替代显式断言

示例:组合方法

it('should return valid user list', async () => {
  const result = await client.callTool('list_users', {});
  const parsed = JSON.parse(result.content[0]?.text || '{}');

  // Explicit assertions for critical properties
  expect(parsed.users).toHaveLength(50);
  expect(parsed.users[0]).toHaveProperty('name');
  expect(parsed.users[0]).toHaveProperty('email');

  // Snapshot for structure regression detection
  expect(result).toMatchToolResponseSnapshot();
});

看 示例/快照示例/ 查看带有基准的完整工作示例。

调试日志记录

不会中断JSON-RPC stdio通信的安全调试日志记录。

问题

MCP服务器通过stdio上的JSON-RPC进行通信。每条消息必须是stdout上的一行JSON:

{"jsonrpc":"2.0","method":"tools/list","params":{...}}\n

如果你在stdout中写任何其他东西(比如 console.log()),它腐蚀了溪流:

Server starting...  ← Breaks protocol!
{"jsonrpc":"2.0","method":"tools/list","params":{...}}\n

结果: SyntaxError: Unexpected token 'S'

解决方案

mcp-devkit将所有控制台输出重定向到stderr,保持stdout干净:

  • 标准输出 =纯JSON-RPC(协议)
  • 标准错误 =所有日志(调试)

自动修补程序(推荐)

// At the top of your MCP server file
import 'mcp-dev-kit/logger';

// Now console.log works without breaking JSON-RPC!
console.log('Server started', { port: 3000 });
console.info('Configuration loaded');
console.warn('Deprecated feature used');
console.error('Connection failed', error);

选择退出:

MCP_DEV_KIT_NO_AUTO_PATCH=true node server.js

手动记录仪

为了获得更多控制,请创建一个自定义记录器实例:

import { createLogger } from 'mcp-dev-kit';

const logger = createLogger({
  timestamps: true,
  colors: true,
  level: 'info', // Only show info and above
  logFile: './server.log', // Optional file output
});

logger.info('Server starting...');
logger.warn('Configuration may need updating');
logger.error('Connection failed', { reason: 'timeout' });

// Cleanup when done
await logger.close();

记录器功能

  • 自动修补 -只需导入并整合.log即可
  • 彩色输出 -颜色编码日志级别(自动检测TTY)
  • 时间戳 -所有日志上的ISO8601时间戳
  • 对象格式化 -漂亮的打印对象 util.inspect()
  • 文件日志记录 -可选异步文件输出
  • 清理 -对原始控制台的优雅修复
  • 零开销 -轻量级,使用微微色(7 KB)

配置

日志级别:

const logger = createLogger({ level: 'warn' });

logger.debug('Not shown');
logger.info('Not shown');
logger.warn('Shown');     // ✓
logger.error('Shown');    // ✓

颜色:

createLogger({ colors: false }); // Force disable
createLogger({ colors: true });  // Force enable
// Auto-detected by default based on process.stderr.isTTY

时间戳:

createLogger({ timestamps: false }); // Disable
// ISO8601 format: 2024-11-03T12:34:56.789Z

文件日志记录:

const logger = createLogger({
  logFile: './server.log',
});

logger.info('This goes to both stderr and server.log');

// Flush pending writes
await logger.close();

测试指南

测试结构

按MCP功能组织测试:

describe('My MCP Server', () => {
  let client: MCPTestClient;

  beforeAll(async () => {
    client = new MCPTestClient({ command: 'node', args: ['./server.js'] });
    await client.connect();
  });

  afterAll(async () => {
    await client.disconnect();
  });

  describe('Server Initialization', () => {
    it('should expose correct server info', () => {
      const info = client.getServerInfo();
      expect(info.name).toBe('my-server');
      expect(info.version).toBe('1.0.0');
    });

    it('should declare required capabilities', () => {
      const caps = client.getServerCapabilities();
      expect(caps.tools).toBeDefined();
    });
  });

  describe('Tools', () => {
    it('should list all available tools', async () => {
      const tools = await client.listTools();
      expect(tools).toHaveLength(3);
      expect(tools.map(t => t.name)).toEqual(['echo', 'calculate', 'search']);
    });

    it('should execute tools successfully', async () => {
      const result = await client.callTool('echo', { message: 'test' });
      expect(result.content[0]?.text).toBe('test');
    });

    it('should handle tool errors gracefully', async () => {
      const error = await client.expectToolCallError('calculate', { invalid: 'params' });
      expect(error.message).toContain('Invalid parameters');
    });

    it('should have stable tool schemas', async () => {
      const tools = await client.listTools();
      expect(tools).toMatchToolListSnapshot();
    });
  });

  describe('Resources', () => {
    it('should list available resources', async () => {
      await expect(client).toHaveResource('config://app.json');
    });

    it('should read resource content', async () => {
      const content = await client.readResource('config://app.json');
      expect(content.contents[0]?.text).toContain('version');
    });
  });

  describe('Prompts', () => {
    it('should provide defined prompts', async () => {
      await expect(client).toHavePrompt('greeting');
    });

    it('should render prompts with arguments', async () => {
      const prompt = await client.getPrompt('greeting', { name: 'Alice' });
      expect(prompt.messages[0]?.content.text).toContain('Alice');
    });
  });
});

测试最佳实践

✅ 做:

  1. 测试所有MCP功能 -工具、资源、提示
  2. 使用描述性测试名称 -明确说明正在测试的内容
  3. 结合匹配和快照 -显式断言+结构验证
  4. 测试错误案例 -不要只测试快乐的道路
  5. 清理资源 -始终断开客户端连接 afterAll
  6. 适当使用超时 -为慢速操作设置合理的超时
  7. 测试服务器生命周期 -测试初始化和关闭

❌ 不要:

  1. 不共享客户端状态 -每个测试套件都应该有自己的客户端
  2. 不要跳过错误测试 -错误处理至关重要
  3. 不测试实现细节 -仅测试公共API
  4. 不要创建不稳定的测试 -避免依赖时间的断言
  5. 不要忽略快照 -仔细查看快照更改
  6. 不要硬编码系统特定的路径 -使用相对路径或环境变量

错误测试

始终测试错误条件:

it('should validate tool parameters', async () => {
  const error = await client.expectToolCallError('calculate', {
    // Missing required parameter
  });
  expect(error.code).toBe(-32602); // Invalid params
  expect(error.message).toContain('Required parameter');
});

it('should handle resource not found', async () => {
  await expect(
    client.readResource('nonexistent://resource')
  ).rejects.toThrow('Resource not found');
});

it('should reject unknown tools', async () => {
  await expect(
    client.callTool('unknown-tool', {})
  ).rejects.toThrow();
});

性能测试

关键操作的测试响应时间:

it('should respond quickly to tool calls', async () => {
  const start = Date.now();
  await client.callTool('quick-operation', {});
  const duration = Date.now() - start;

  expect(duration).toBeLessThan(1000); // < 1 second
});

集成测试

测试真实世界的工作流程:

it('should handle complete user workflow', async () => {
  // 1. List available tools
  const tools = await client.listTools();
  expect(tools.length).toBeGreaterThan(0);

  // 2. Get resource for context
  const config = await client.readResource('config://app.json');
  const settings = JSON.parse(config.contents[0]?.text || '{}');

  // 3. Execute tool with context
  const result = await client.callTool('process', {
    mode: settings.defaultMode,
  });
  expect(result.content[0]?.text).toBeTruthy();

  // 4. Verify result structure
  expect(result).toMatchToolResponseSnapshot();
});

例子

看 示例/ 完整示例目录:

运行示例:

npm install -g tsx
tsx examples/logger/basic-usage.ts

API 参考

MCPTestClient

class MCPTestClient {
  constructor(options: {
    command: string;
    args?: string[];
    env?: Record<string, string>;
    timeout?: number;
  });

  // Connection management
  connect(): Promise<void>;
  disconnect(): Promise<void>;

  // Server info
  getServerInfo(): ServerInfo;
  getServerCapabilities(): ServerCapabilities;

  // Tools
  listTools(): Promise<Tool[]>;
  callTool(name: string, args: unknown): Promise<CallToolResult>;
  expectToolCallSuccess(name: string, args: unknown): Promise<string>;
  expectToolCallError(name: string, args: unknown): Promise<Error>;

  // Resources
  listResources(): Promise<Resource[]>;
  readResource(uri: string): Promise<ReadResourceResult>;

  // Prompts
  listPrompts(): Promise<Prompt[]>;
  getPrompt(name: string, args?: unknown): Promise<GetPromptResult>;
}

日志记录器

interface LoggerOptions {
  enabled?: boolean;        // Enable/disable logger (default: true)
  timestamps?: boolean;     // Show timestamps (default: true)
  colors?: boolean;         // Force colors on/off (default: auto-detect)
  level?: 'debug'|'info'|'warn'|'error'; // Min level (default: 'debug')
  stream?: WritableStream;  // Custom output (default: process.stderr)
  logFile?: string;         // Optional file output
}

function createLogger(options?: LoggerOptions): DebugLogger;
function patchConsole(options?: LoggerOptions): void;
function unpatchConsole(): void;

匹配器

// Installation
function installMCPMatchers(): void;

// Tool matchers
expect(client).toHaveTool(name: string);
expect(tool).toHaveToolProperty(property: string, value?: any);
expect(tool).toMatchToolSchema(schema: object);
expect(result).toReturnToolResult(expected: any);
expect(promise).toThrowToolError();

// Resource matchers
expect(client).toHaveResource(uri: string);

// Prompt matchers
expect(client).toHavePrompt(name: string);

// Snapshot matchers
expect(data).toMatchMCPSnapshot(options?: { exclude?: string[] });
expect(result).toMatchToolResponseSnapshot(options?: { exclude?: string[] });
expect(tools).toMatchToolListSnapshot(options?: { exclude?: string[] });
expect(resources).toMatchResourceListSnapshot(options?: { exclude?: string[] });
expect(prompts).toMatchPromptListSnapshot(options?: { exclude?: string[] });

故障排除

测试挂起还是超时?

增加超时时间:

const client = new MCPTestClient({
  command: 'node',
  args: ['./server.js'],
  timeout: 60000, // 60 seconds
});

快照意外失败?

检查是否排除了足够的动态字段:

expect(result).toMatchToolResponseSnapshot({
  exclude: [
    'timestamp',
    'requestId',
    'files.*.modified',
    'data.*.generatedAt',
  ]
});

日志未显示?

检查您的日志级别:

createLogger({ level: 'debug' }); // Show everything

颜色不起作用?

颜色仅在stderr为TTY时有效。强制启用/禁用:

createLogger({ colors: true });  // Always color
createLogger({ colors: false }); // Never color

客户端未连接?

验证您的服务器是否正在使用stdio传输并响应初始化:

// Server must respond to initialize request
server.setRequestHandler(InitializeRequestSchema, async (request) => {
  return {
    protocolVersion: '2024-11-05',
    capabilities: { tools: {} },
    serverInfo: { name: 'my-server', version: '1.0.0' },
  };
});

需求

  • Node.js>=18.0.0
  • TypeScript>=5.0.0(如果使用TypeScript)
  • Vitest>=1.0.0(用于测试功能)

贡献

看 贡献.md 用于开发设置和指南。

许可证

MIT© 格纳997

相关项目


内置❤️ 对于MCP社区

觉得这个有用吗? 把它放在GitHub上 ⭐