使用说明
🧪 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社区