使用说明
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();
智能默认值
这些字段是 自动排除 从所有快照中:
timestamprequestIdexecutionTimecacheKey_meta.timestampserverInfo.startedAtserverInfo.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');
});
});
});
测试最佳实践
✅ 做:
- 测试所有MCP功能 -工具、资源、提示
- 使用描述性测试名称 -明确说明正在测试的内容
- 结合匹配和快照 -显式断言+结构验证
- 测试错误案例 -不要只测试快乐的道路
- 清理资源 -始终断开客户端连接
afterAll - 适当使用超时 -为慢速操作设置合理的超时
- 测试服务器生命周期 -测试初始化和关闭
❌ 不要:
- 不共享客户端状态 -每个测试套件都应该有自己的客户端
- 不要跳过错误测试 -错误处理至关重要
- 不测试实现细节 -仅测试公共API
- 不要创建不稳定的测试 -避免依赖时间的断言
- 不要忽略快照 -仔细查看快照更改
- 不要硬编码系统特定的路径 -使用相对路径或环境变量
错误测试
始终测试错误条件:
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();
});
例子
看 示例/ 完整示例目录:
- 快照示例/ -使用基准完成快照测试
- 记录器/基本用法.ts -自动补丁控制台
- 记录器/手动设置.ts -自定义记录器实例
- logger/file-loging.ts -记录到文件
- logger/mcp-server-example.ts -真正的MCP服务器
运行示例:
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
相关项目
- @模型上下文协议/sdk -官方MCP SDK
- 模型上下文协议 -协议规范
- Nochen jsonrpc -基于stdio的JSON-RPC 2.0
内置❤️ 对于MCP社区
觉得这个有用吗? 把它放在GitHub上 ⭐
