MCP服务器测试框架

专为Model Context Protocol (MCP)服务器设计的类似Jest的测试框架,提供强大的模拟工具和断言功能。

  • by anyrxo
  • TypeScript
  • 本地部署
  • 更新于
在 GitHub 查看 anyrxo/mcp-test

使用说明

🧪 MCP测试-模型上下文协议服务器的测试框架

专为MCP服务器设计的类似Jest的测试框架

📖 概述

MCP测试 是一个用于模型上下文协议(MCP)服务器的全面测试框架。它提供了一个熟悉的Jest-like API,提供了专门为测试MCP工具、资源和提示而设计的强大的模拟实用程序。

为什么选择MCP测试?

虽然MCP检查员用于手动/目视测试,但 无程序化测试框架 对于MCP服务器。开发人员在以下方面遇到了困难:

  • ❌ MCP没有类似Jest的测试库
  • ❌ 以编程方式测试stdio/SSE传输存在困难
  • ❌ 没有内置的工具/资源/提示模拟
  • ❌ 集成测试的复杂设置
  • ❌ 有限的单元测试能力

MCP测试解决了所有这些问题 使用熟悉、功能强大的API。

主要特点

✅ 像耶稣一样的API - describe(), test(), expect(), beforeEach() ✅ 有力的断言 -20+匹配器,包括异步支持 ✅ 模拟实用程序 -轻松模拟工具、资源和提示 ✅ 间谍功能 -跟踪调用、参数、返回值 ✅ 异步/等待支持 -一流的承诺测试 ✅ 生命周期挂钩 - beforeEach, afterEach, beforeAll, afterAll ✅ 自定义实现 -完全控制模拟行为 ✅ 错误处理 -易于测试错误条件 ✅ 零配置 -开箱即用

🚀 快速开始

安装

# Clone repository
git clone <repository-url>
cd mcp-test

# Install dependencies
npm install

# Build TypeScript
npm run build

基本用法

创建测试文件 my-server.test.js:

import { describe, test, expect, mockTool, createMockServer } from 'mcp-test';

describe('My MCP Server', () => {
  test('read-file tool works', async () => {
    const readFile = mockTool('read-file', {
      returnValue: { content: 'Hello World', size: 11 }
    });

    const result = await readFile.execute({ path: '/tmp/test.txt' });

    expect(result.content).toBe('Hello World');
    expect(readFile).toHaveBeenCalledTimes(1);
    expect(readFile).toHaveBeenCalledWith({ path: '/tmp/test.txt' });
  });

  test('handles errors gracefully', async () => {
    const failingTool = mockTool('failing-tool', {
      throwError: 'File not found'
    });

    await expect(failingTool.execute()).rejects.toThrow('File not found');
  });
});

运行测试:

node dist/cli.js run my-server.test.js

📚 API 参考

测试结构

describe(name, fn)

将相关测试分组在一起:

describe('File operations', () => {
  test('reads files', () => { /* ... */ });
  test('writes files', () => { /* ... */ });
});

test(name, fn) 或 it(name, fn)

定义测试:

test('should return correct result', () => {
  expect(2 + 2).toBe(4);
});

// Async tests
test('async operation works', async () => {
  const result = await asyncFunction();
  expect(result).toBe('success');
});

test.skip() 和 test.only()

跳过或隔离测试:

test.skip('not ready yet', () => {
  // This test will be skipped
});

test.only('run only this test', () => {
  // Only this test will run
});

生命周期挂钩

describe('Lifecycle example', () => {
  beforeAll(() => {
    // Runs once before all tests
  });

  beforeEach(() => {
    // Runs before each test
  });

  afterEach(() => {
    // Runs after each test
  });

  afterAll(() => {
    // Runs once after all tests
  });

  test('example', () => { /* ... */ });
});

断言

基本配对

expect(value).toBe(expected);           // Strict equality (===)
expect(value).toEqual(expected);        // Deep equality
expect(value).toBeNull();               // value === null
expect(value).toBeUndefined();          // value === undefined
expect(value).toBeDefined();            // value !== undefined
expect(value).toBeTruthy();             // !!value === true
expect(value).toBeFalsy();              // !!value === false

否定

expect(value).not.toBe(unexpected);
expect(value).not.toEqual(unexpected);

集合

expect([1, 2, 3]).toContain(2);         // Array contains item
expect('hello').toContain('ell');       // String contains substring
expect([1, 2, 3]).toHaveLength(3);      // Array/string length

对象

expect(obj).toMatchObject({ a: 1, b: 2 });  // Partial object match

错误

expect(() => {
  throw new Error('boom');
}).toThrow();

expect(() => {
  throw new Error('boom');
}).toThrow('boom');

expect(() => {
  throw new Error('boom');
}).toThrow(/boom/);

异步/承诺

// Promise resolves
await expect(promise).resolves.toBe('success');
await expect(promise).resolves.toEqual({ data: 'ok' });

// Promise rejects
await expect(promise).rejects.toThrow('error');
await expect(promise).rejects.toThrow(/error/);

模拟配对

expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(3);
expect(mockFn).toHaveBeenCalledWith(arg1, arg2);

模拟实用程序

创建模拟工具

import { mockTool } from 'mcp-test';

const readFile = mockTool('read-file', {
  returnValue: { content: 'Hello', size: 5 }
});

// Execute the mock
const result = await readFile.execute({ path: '/tmp/test.txt' });

// Check calls
expect(readFile).toHaveBeenCalled();
expect(readFile.calls).toHaveLength(1);

模拟工具选项

// Return a value
mockTool('tool-name', {
  returnValue: { data: 'value' }
});

// Custom implementation
mockTool('tool-name', {
  implementation: (params) => {
    return { result: params.input.toUpperCase() };
  }
});

// Throw an error
mockTool('tool-name', {
  throwError: 'Something went wrong'
});

// Or throw a custom error
mockTool('tool-name', {
  throwError: new TypeError('Invalid input')
});

模拟方法

const tool = mockTool('test-tool');

// Change return value
tool.mockReturnValue('new value');
tool.mockReturnValueOnce('one time value');

// Async values
tool.mockResolvedValue({ data: 'async' });
tool.mockRejectedValue('async error');

// Custom implementation
tool.mockImplementation((params) => {
  return { processed: params.data };
});

// Clear call history
tool.mockClear();

// Reset mock (clears + removes implementation)
tool.mockReset();

创建模拟服务器

import { createMockServer } from 'mcp-test';

const { server, mocks } = createMockServer({
  tools: {
    'read-file': { returnValue: { content: 'test' } },
    'write-file': { returnValue: { success: true } }
  },
  resources: {
    'file:///config.json': { returnValue: '{"version": "1.0.0"}' }
  },
  prompts: {
    'code-review': { returnValue: 'Review complete' }
  }
});

// Use the server
await server.tools['read-file']({ path: '/tmp/test.txt' });

// Access mocks
expect(mocks.tools.get('read-file')).toHaveBeenCalled();

间谍功能

import { spyOn, restoreSpy } from 'mcp-test';

const obj = {
  method: () => 'original'
};

const spy = spyOn(obj, 'method');

obj.method();  // Calls original + tracks call

expect(spy).toHaveBeenCalled();

// Restore original
restoreSpy(spy);

🎯 真实世界的例子

示例1:测试文件工具

describe('File tools', () => {
  let fileSystem;

  beforeEach(() => {
    const { server, mocks } = createMockServer({
      tools: {
        'read-file': {
          implementation: ({ path }) => {
            if (path === '/tmp/exists.txt') {
              return { content: 'File content', size: 12 };
            }
            throw new Error('File not found');
          }
        },
        'write-file': { returnValue: { success: true } }
      }
    });

    fileSystem = { server, mocks };
  });

  test('reads existing file', async () => {
    const result = await fileSystem.server.tools['read-file']({
      path: '/tmp/exists.txt'
    });

    expect(result.content).toBe('File content');
    expect(result.size).toBe(12);
  });

  test('throws error for non-existent file', async () => {
    await expect(
      fileSystem.server.tools['read-file']({ path: '/tmp/missing.txt' })
    ).rejects.toThrow('File not found');
  });

  test('writes file successfully', async () => {
    const result = await fileSystem.server.tools['write-file']({
      path: '/tmp/new.txt',
      content: 'Hello'
    });

    expect(result.success).toBe(true);
    expect(fileSystem.mocks.tools.get('write-file')).toHaveBeenCalledWith({
      path: '/tmp/new.txt',
      content: 'Hello'
    });
  });
});

示例2:测试资源

describe('Configuration resource', () => {
  test('returns configuration', async () => {
    const { server } = createMockServer({
      resources: {
        'config://app.json': {
          returnValue: JSON.stringify({
            version: '1.0.0',
            env: 'production'
          })
        }
      }
    });

    const config = await server.resources['config://app.json']();
    const parsed = JSON.parse(config);

    expect(parsed.version).toBe('1.0.0');
    expect(parsed.env).toBe('production');
  });
});

示例3:测试提示

describe('Code review prompt', () => {
  test('reviews code and provides feedback', async () => {
    const codeReview = mockPrompt('code-review', {
      implementation: ({ file, code }) => {
        const issues = code.includes('console.log') ? 1 : 0;
        return {
          file,
          issues,
          feedback: issues > 0 ? 'Remove console.log statements' : 'Looks good'
        };
      }
    });

    const result = await codeReview.execute({
      file: 'test.js',
      code: 'console.log("debug");'
    });

    expect(result.issues).toBe(1);
    expect(result.feedback).toContain('Remove console.log');
  });
});

示例4:错误处理

describe('Error handling', () => {
  test('handles network errors', async () => {
    const apiTool = mockTool('fetch-data', {
      throwError: new Error('Network timeout')
    });

    await expect(apiTool.execute()).rejects.toThrow('Network timeout');

    expect(apiTool.errors).toHaveLength(1);
    expect(apiTool.errors[0].message).toBe('Network timeout');
  });

  test('retries on failure', async () => {
    let attempts = 0;
    const retryTool = mockTool('retry-tool');

    retryTool.mockImplementation(() => {
      attempts++;
      if (attempts < 3) {
        throw new Error('Temporary failure');
      }
      return { success: true };
    });

    // Implement retry logic
    let result;
    for (let i = 0; i < 3; i++) {
      try {
        result = await retryTool.execute();
        break;
      } catch (e) {
        if (i === 2) throw e;
      }
    }

    expect(result.success).toBe(true);
    expect(retryTool).toHaveBeenCalledTimes(3);
  });
});

🧪 测试框架

运行自检套件:

npm test

这运行了31个测试,涵盖了所有框架功能。

📊 测试结果

📊 Test Summary:
   Total: 31
   ✓ Passed: 31
   Pass Rate: 100%

🔧 CLI使用情况

# Run test files
mcp-test run tests/**/*.test.js

# Run specific file
mcp-test run my-server.test.js

# Verbose output
mcp-test run --verbose

# Bail on first failure
mcp-test run --bail

# Initialize example test
mcp-test init

🎓 与其他工具的比较

功能 MCP测试 MCP检查器 Jest Mocha
MCP特定 ✅ ✅ ❌ ❌
程序化 ✅ ❌ ✅ ✅
模拟实用程序 ✅ ❌ ✅ ❌
异步/等待 ✅ 不适用 ✅ ✅
工具模拟 ✅ ✅ ❌ ❌
资源模拟 ✅ ✅ ❌ ❌
即时嘲讽 ✅ ✅ ❌ ❌
目视检测 ❌ ✅ ❌ ❌

MCP测试 是唯一专门为MCP服务器设计的程序化测试框架。

🤝 贡献

欢迎投稿!请阅读CONTRIBUTING.md了解指南。

📄 许可证

MIT许可证-有关详细信息,请参阅许可证文件

🔗 资源


内置于❤️ 对于MCP社区