# MCP 服务器测试指南
## 概述
MCP (Model Context Protocol) 服务器的测试可以通过多种方式进行,从简单的交互测试到完整的单元测试和集成测试。
## 测试方式
### 1. MCP Inspector (推荐用于开发调试)
MCP Inspector 是官方提供的交互式测试工具,提供 Web UI 界面来测试 MCP 服务器。
#### 安装和使用
```bash
# 全局安装
npm install -g @modelcontextprotocol/inspector
# 或者直接使用 npx
npx @modelcontextprotocol/inspector node dist/index.js
```
#### 配置方式
**1. 命令行直接启动**
```bash
# 基本用法
npx @modelcontextprotocol/inspector node dist/index.js
# 带环境变量
npx @modelcontextprotocol/inspector -e ZENDTAO_BASE_URL=http://localhost -e ZENDTAO_TOKEN=your_token node dist/index.js
# 自定义端口
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node dist/index.js
```
**2. 使用配置文件**
```json
{
"mcpServers": {
"zendao-server": {
"type": "stdio",
"command": "node",
"args": ["dist/index.js"],
"env": {
"ZENDTAO_BASE_URL": "http://localhost",
"ZENDTAO_TOKEN": "your_token_here"
}
}
}
}
```
```bash
# 使用配置文件
npx @modelcontextprotocol/inspector --config mcp.json --server zendao-server
```
#### 功能特点
- 📋 查看可用的工具列表
- 🧪 交互式工具调用测试
- 📊 实时请求/响应监控
- 🔍 JSON-RPC 消息查看
- 🌐 Web UI 界面
### 2. 单元测试 (Jest + TypeScript)
#### 测试环境设置
```bash
# 安装测试依赖
npm install --save-dev jest @types/jest ts-jest @types/node
```
#### Jest 配置 (jest.config.js)
```javascript
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
],
};
```
#### 工具处理函数测试示例
```typescript
// src/tests/tools/getProjects.test.ts
import { getProjectsHandler } from '../../tools/getProjects';
import { ZenTaoClient } from '../../client';
import { LRUCache } from '../../utils/cache';
// Mock ZenTaoClient
jest.mock('../../client');
const MockZenTaoClient = ZenTaoClient as jest.MockedClass<typeof ZenTaoClient>;
describe('getProjectsHandler', () => {
let mockClient: jest.Mocked<ZenTaoClient>;
let mockCache: LRUCache<string, any>;
beforeEach(() => {
jest.clearAllMocks();
mockClient = new MockZenTaoClient({} as any) as jest.Mocked<ZenTaoClient>;
mockCache = {
get: jest.fn(),
set: jest.fn(),
size: 0,
maxSize: 1000
} as any;
});
it('should return projects from API when not cached', async () => {
// Arrange
const mockProjects = [
{ id: 1, name: 'Project 1', code: 'P1', status: 'active', begin: '2024-01-01', end: null }
];
mockCache.get.mockReturnValue(null);
mockClient.get.mockResolvedValue(mockProjects);
const params = { page: 1, limit: 50 };
// Act
const result = await getProjectsHandler(mockClient, mockCache, params);
// Assert
expect(mockCache.get).toHaveBeenCalled();
expect(mockClient.get).toHaveBeenCalledWith('/api.php/v1/projects', {
page: 1,
limit: 50
});
expect(mockCache.set).toHaveBeenCalled();
expect(result.structuredContent).toEqual({
projects: mockProjects,
total: 1,
page: 1,
limit: 50
});
});
it('should return cached projects when available', async () => {
// Arrange
const cachedData = {
projects: [{ id: 1, name: 'Project 1', code: 'P1', status: 'active' }],
total: 1,
page: 1,
limit: 50
};
mockCache.get.mockReturnValue(cachedData);
const params = { page: 1, limit: 50 };
// Act
const result = await getProjectsHandler(mockClient, mockCache, params);
// Assert
expect(mockCache.get).toHaveBeenCalled();
expect(mockClient.get).not.toHaveBeenCalled();
expect(result.structuredContent).toEqual(cachedData);
});
it('should throw error when API call fails', async () => {
// Arrange
mockCache.get.mockReturnValue(null);
mockClient.get.mockRejectedValue(new Error('API Error'));
const params = { page: 1, limit: 50 };
// Act & Assert
await expect(getProjectsHandler(mockClient, mockCache, params))
.rejects.toThrow('获取项目列表失败: API Error');
});
});
```
#### 缓存系统测试
```typescript
// src/tests/utils/cache.test.ts
import { LRUCache } from '../../utils/cache';
describe('LRUCache', () => {
let cache: LRUCache<string, string>;
beforeEach(() => {
cache = new LRUCache<string, string>({
maxSize: 3,
defaultTtl: 1000,
enableStats: true
});
});
it('should store and retrieve values', () => {
cache.set('key1', 'value1');
expect(cache.get('key1')).toBe('value1');
});
it('should return null for non-existent keys', () => {
expect(cache.get('nonexistent')).toBeNull();
});
it('should track statistics correctly', () => {
cache.set('key1', 'value1');
// Hit
cache.get('key1');
// Miss
cache.get('nonexistent');
const stats = cache.getStats();
expect(stats.hits).toBe(1);
expect(stats.misses).toBe(1);
expect(stats.hitRate).toBe(0.5);
});
it('should expire items after TTL', (done) => {
cache.set('key1', 'value1', 10); // 10ms TTL
setTimeout(() => {
expect(cache.get('key1')).toBeNull();
done();
}, 20);
});
});
```
### 3. 集成测试 (完整的 MCP 客户端测试)
#### 测试服务器和客户端的完整交互
```typescript
// src/tests/integration/mcp-server.test.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { spawn, ChildProcess } from 'child_process';
describe('MCP Server Integration Tests', () => {
let serverProcess: ChildProcess;
let client: Client;
let transport: StdioClientTransport;
beforeAll(async () => {
// 启动 MCP 服务器进程
serverProcess = spawn('node', ['dist/index.js'], {
env: {
...process.env,
ZENDTAO_BASE_URL: 'http://localhost:3000',
ZENDTAO_TOKEN: 'test-token'
},
stdio: ['pipe', 'pipe', 'inherit']
});
// 创建客户端传输
transport = new StdioClientTransport({
command: 'node',
args: ['dist/index.js'],
env: {
ZENDTAO_BASE_URL: 'http://localhost:3000',
ZENDTAO_TOKEN: 'test-token'
}
});
// 创建客户端
client = new Client({
name: 'test-client',
version: '1.0.0'
});
await transport.start();
await client.connect(transport);
});
afterAll(async () => {
if (client) {
await client.close();
}
if (transport) {
await transport.close();
}
if (serverProcess) {
serverProcess.kill();
}
});
it('should list available tools', async () => {
const response = await client.listTools();
expect(response.tools).toBeDefined();
expect(Array.isArray(response.tools)).toBe(true);
const toolNames = response.tools.map(tool => tool.name);
expect(toolNames).toContain('zendao_list_projects');
expect(toolNames).toContain('zendao_create_project');
expect(toolNames).toContain('zendao_list_tasks');
});
it('should call zendao_list_projects tool successfully', async () => {
const result = await client.callTool({
name: 'zendao_list_projects',
arguments: {
page: 1,
limit: 10
}
});
expect(result).toBeDefined();
expect(result.content).toBeDefined();
expect(Array.isArray(result.content)).toBe(true);
});
it('should handle invalid tool arguments gracefully', async () => {
await expect(client.callTool({
name: 'zendao_list_projects',
arguments: {
page: -1 // 无效参数
}
})).rejects.toThrow();
});
});
```
### 4. API Mock 测试
#### 使用 MSW (Mock Service Worker) 进行 HTTP Mock
```bash
npm install --save-dev msw @types/msw
```
```typescript
// src/tests/mocks/handlers.ts
import { rest } from 'msw';
import { IProject, ITask } from '../../types';
const mockProjects: IProject[] = [
{
id: 1,
name: 'Test Project',
code: 'TP001',
status: 'active',
begin: '2024-01-01',
end: '2024-12-31'
}
];
const mockTasks: ITask[] = [
{
id: 1,
name: 'Test Task',
project: 1,
status: 'wait',
pri: 1,
estimate: 8
}
];
export const handlers = [
// Mock 项目列表 API
rest.get('*/api.php/v1/projects', (req, res, ctx) => {
const page = parseInt(req.url.searchParams.get('page') || '1');
const limit = parseInt(req.url.searchParams.get('limit') || '10');
return res(
ctx.status(200),
ctx.json(mockProjects.slice((page - 1) * limit, page * limit))
);
}),
// Mock 单个项目 API
rest.get('*/api.php/v1/projects/:id', (req, res, ctx) => {
const { id } = req.params;
const project = mockProjects.find(p => p.id === parseInt(id as string));
if (!project) {
return res(ctx.status(404), ctx.json({ error: 'Project not found' }));
}
return res(ctx.status(200), ctx.json(project));
}),
// Mock 任务列表 API
rest.get('*/api.php/v1/tasks', (req, res, ctx) => {
const projectId = req.url.searchParams.get('projectId');
let filteredTasks = mockTasks;
if (projectId) {
filteredTasks = mockTasks.filter(task => task.project === parseInt(projectId));
}
return res(ctx.status(200), ctx.json(filteredTasks));
}),
// Mock 创建项目 API
rest.post('*/api.php/v1/projects', (req, res, ctx) => {
return res(
ctx.status(201),
ctx.json({
id: Math.floor(Math.random() * 1000),
...req.body
})
);
})
];
```
```typescript
// src/tests/setup.ts
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';
// 设置 mock 服务器
export const server = setupServer(...handlers);
// 在所有测试前启动
beforeAll(() => server.listen());
// 每个测试后重置 handlers
afterEach(() => server.resetHandlers());
// 测试完成后关闭
afterAll(() => server.close());
```
### 5. 性能测试
#### 工具响应时间测试
```typescript
// src/tests/performance/tool-performance.test.ts
import { getProjectsHandler } from '../../tools/getProjects';
import { ZenTaoClient } from '../../client';
import { LRUCache } from '../../utils/cache';
describe('Tool Performance Tests', () => {
let mockClient: jest.Mocked<ZenTaoClient>;
let mockCache: LRUCache<string, any>;
beforeEach(() => {
mockClient = new MockZenTaoClient({} as any) as jest.Mocked<ZenTaoClient>;
mockCache = new LRUCache({ enableStats: true });
});
it('should handle large project lists efficiently', async () => {
// Mock 大量数据
const largeProjectList = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `Project ${i + 1}`,
code: `P${i + 1}`,
status: 'active',
begin: '2024-01-01',
end: '2024-12-31'
}));
mockCache.get.mockReturnValue(null);
mockClient.get.mockResolvedValue(largeProjectList);
const startTime = Date.now();
await getProjectsHandler(mockClient, mockCache, { page: 1, limit: 1000 });
const endTime = Date.now();
const duration = endTime - startTime;
// 断言处理时间应该在合理范围内 (< 1秒)
expect(duration).toBeLessThan(1000);
});
});
```
## 测试脚本配置
### package.json 脚本
```json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:integration": "jest --testPathPattern=integration",
"test:performance": "jest --testPathPattern=performance",
"test:ci": "jest --ci --coverage --watchAll=false"
}
}
```
### GitHub Actions CI/CD
```yaml
# .github/workflows/test.yml
name: Test MCP Server
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
```
## 最佳实践
### 1. 测试策略
- **单元测试**: 测试独立的工具处理函数
- **集成测试**: 测试完整的 MCP 客户端-服务器交互
- **性能测试**: 确保响应时间在可接受范围内
- **Mock 测试**: 隔离外部依赖,提高测试稳定性
### 2. 测试覆盖率
- 目标: 80%+ 代码覆盖率
- 重点测试工具处理函数、缓存系统、错误处理
### 3. 持续集成
- 自动运行测试套件
- 性能回归检测
- 代码覆盖率监控
### 4. 调试工具
- 使用 MCP Inspector 进行交互式调试
- 详细的错误日志记录
- 性能监控和分析
这套测试方案将为你的 ZenTao MCP 服务器提供全面的质量保障!