为什么要测试 MCP Server
MCP Server 直接与 AI 模型交互,如果工具返回错误的结果,AI 会基于错误信息做出错误的判断。更糟糕的是,对于数据库或文件操作类的 Server,Bug 可能导致数据丢失或安全问题。
所以,测试不是可选的,而是必须的。
测试策略
MCP Server 的测试分为三个层次:
单元测试 → 集成测试 → 端到端测试
↓ ↓ ↓
业务逻辑 MCP 协议 真实环境
| 测试类型 | 测试对象 | 工具 |
|---|---|---|
| 单元测试 | 解析器、工具函数 | Vitest / Jest |
| 集成测试 | MCP Server 工具调用 | MCP SDK Client |
| 端到端测试 | 完整的 Client-Server 交互 | MCP Inspector |
项目配置
以我们之前开发的数据库 MCP Server 为例,添加测试配置:
npm install -D vitest @vitest/coverage-v8
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
coverage: {
provider: "v8",
reporter: ["text", "html"],
include: ["src/**/*.ts"],
exclude: ["src/index.ts"],
},
testTimeout: 10000,
},
});
// package.json 添加脚本
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}
测试文件结构:
tests/
├── unit/
│ ├── database.test.ts
│ └── tools.test.ts
├── integration/
│ └── server.test.ts
└── helpers/
└── mock-db.ts
单元测试
测试数据库操作
// tests/unit/database.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { DatabaseConnection } from "../../src/database.js";
// 使用测试数据库
const TEST_DB_URL = process.env.TEST_DATABASE_URL
|| "postgresql://test:test@localhost:5432/test_mcp";
describe("DatabaseConnection", () => {
let db: DatabaseConnection;
beforeAll(async () => {
db = new DatabaseConnection(TEST_DB_URL);
// 创建测试表
// 注意:实际项目中应该用 migration 工具
});
afterAll(async () => {
await db.close();
});
describe("listTables", () => {
it("should return an array of tables", async () => {
const tables = await db.listTables();
expect(Array.isArray(tables)).toBe(true);
tables.forEach((table) => {
expect(table).toHaveProperty("name");
expect(table).toHaveProperty("schema");
expect(table).toHaveProperty("rowCount");
});
});
});
describe("describeTable", () => {
it("should return column information", async () => {
const columns = await db.describeTable("users");
expect(columns.length).toBeGreaterThan(0);
columns.forEach((col) => {
expect(col).toHaveProperty("name");
expect(col).toHaveProperty("type");
expect(col).toHaveProperty("nullable");
expect(col).toHaveProperty("isPrimaryKey");
});
});
it("should reject invalid table names", async () => {
await expect(
db.describeTable("users; DROP TABLE users;")
).rejects.toThrow("Invalid table name");
});
});
describe("query", () => {
it("should execute SELECT queries", async () => {
const result = await db.query("SELECT 1 as num");
expect(result.columns).toContain("num");
expect(result.rows).toHaveLength(1);
expect(result.rows[0].num).toBe(1);
});
it("should reject non-SELECT queries", async () => {
await expect(
db.query("DELETE FROM users")
).rejects.toThrow("Only SELECT");
});
it("should reject queries with forbidden keywords", async () => {
await expect(
db.query("SELECT * FROM users; DROP TABLE users")
).rejects.toThrow("Forbidden keyword");
});
it("should respect the limit parameter", async () => {
const result = await db.query("SELECT * FROM users", 5);
expect(result.rows.length).toBeLessThanOrEqual(5);
});
});
});
测试工具函数
// tests/unit/tools.test.ts
import { describe, it, expect } from "vitest";
describe("SQL Safety", () => {
function isSafeQuery(sql: string): boolean {
const trimmed = sql.trim().toUpperCase();
if (!trimmed.startsWith("SELECT") && !trimmed.startsWith("WITH")) {
return false;
}
const forbidden = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "TRUNCATE"];
return !forbidden.some((kw) => trimmed.includes(kw));
}
it("should allow SELECT queries", () => {
expect(isSafeQuery("SELECT * FROM users")).toBe(true);
expect(isSafeQuery("SELECT COUNT(*) FROM orders")).toBe(true);
});
it("should allow WITH (CTE) queries", () => {
expect(
isSafeQuery("WITH cte AS (SELECT 1) SELECT * FROM cte")
).toBe(true);
});
it("should reject INSERT queries", () => {
expect(isSafeQuery("INSERT INTO users VALUES (1)")).toBe(false);
});
it("should reject UPDATE queries", () => {
expect(isSafeQuery("UPDATE users SET name = 'x'")).toBe(false);
});
it("should reject DROP queries", () => {
expect(isSafeQuery("DROP TABLE users")).toBe(false);
});
it("should reject queries with embedded dangerous keywords", () => {
expect(
isSafeQuery("SELECT * FROM users; DROP TABLE users")
).toBe(false);
});
});
集成测试
集成测试验证 MCP Server 的工具调用是否正确工作。我们使用 MCP SDK 的 Client 来模拟真实的调用:
// tests/integration/server.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createServer } from "../../src/server.js";
import { DatabaseConnection } from "../../src/database.js";
describe("MCP Server Integration", () => {
let client: Client;
let db: DatabaseConnection;
beforeAll(async () => {
db = new DatabaseConnection(
process.env.TEST_DATABASE_URL ||
"postgresql://test:test@localhost:5432/test_mcp"
);
const server = createServer(db);
// 使用内存传输进行测试
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
client = new Client(
{ name: "test-client", version: "1.0.0" },
{ capabilities: {} }
);
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
});
afterAll(async () => {
await client.close();
await db.close();
});
describe("tools/list", () => {
it("should list all available tools", async () => {
const result = await client.listTools();
expect(result.tools).toHaveLength(4);
const toolNames = result.tools.map((t) => t.name);
expect(toolNames).toContain("list_tables");
expect(toolNames).toContain("describe_table");
expect(toolNames).toContain("query");
expect(toolNames).toContain("table_stats");
});
it("should have proper descriptions for each tool", async () => {
const result = await client.listTools();
result.tools.forEach((tool) => {
expect(tool.description).toBeTruthy();
expect(tool.description.length).toBeGreaterThan(10);
});
});
});
describe("tools/call - list_tables", () => {
it("should return table list", async () => {
const result = await client.callTool({
name: "list_tables",
arguments: {},
});
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe("text");
expect((result.content[0] as any).text).toContain("张表");
});
});
describe("tools/call - query", () => {
it("should execute valid queries", async () => {
const result = await client.callTool({
name: "query",
arguments: { sql: "SELECT 1 as test_value" },
});
expect(result.isError).toBeFalsy();
expect((result.content[0] as any).text).toContain("test_value");
});
it("should reject dangerous queries", async () => {
const result = await client.callTool({
name: "query",
arguments: { sql: "DROP TABLE users" },
});
expect(result.isError).toBe(true);
});
});
});
使用 MCP Inspector 测试
MCP Inspector 是官方提供的可视化测试工具:
# 安装并启动 Inspector
npx @modelcontextprotocol/inspector node dist/index.js \
"postgresql://user:pass@localhost:5432/mydb"
Inspector 提供了一个 Web 界面,可以:
- 查看 Server 信息和能力声明
- 浏览所有注册的工具、资源和提示
- 手动调用工具并查看返回结果
- 查看完整的 JSON-RPC 通信日志
Inspector 测试清单
| 检查项 | 说明 |
|---|---|
| Server 信息 | 名称和版本是否正确 |
| 工具列表 | 所有工具是否正确注册 |
| 工具描述 | 描述是否清晰准确 |
| 参数定义 | 必填/可选参数是否正确 |
| 正常调用 | 工具是否返回预期结果 |
| 错误处理 | 错误参数是否返回友好的错误信息 |
| 边界情况 | 空输入、超大输入等 |
调试技巧
1. 日志输出
在开发阶段,添加详细的日志:
// 使用 stderr 输出日志(stdout 被 MCP 协议占用)
function log(message: string, data?: unknown): void {
const timestamp = new Date().toISOString();
const logLine = data
? `[${timestamp}] ${message}: ${JSON.stringify(data)}`
: `[${timestamp}] ${message}`;
process.stderr.write(logLine + "\n");
}
// 在工具处理函数中使用
server.tool("query", "...", schema, async (args) => {
log("query called", args);
try {
const result = await db.query(args.sql);
log("query result", { rowCount: result.rowCount });
return { content: [{ type: "text", text: "..." }] };
} catch (error) {
log("query error", { error: (error as Error).message });
throw error;
}
});
2. 环境变量调试
# 设置 MCP SDK 的调试模式
DEBUG=mcp:* node dist/index.js
3. 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| Server 无响应 | stdout 被其他输出占用 | 确保日志输出到 stderr |
| 工具不显示 | 注册时机不对 | 确保在 connect 之前注册 |
| 参数验证失败 | Schema 定义不匹配 | 检查 Zod schema |
| 连接超时 | Server 启动太慢 | 优化初始化逻辑 |
发布到 npm
1. 准备 package.json
{
"name": "mcp-database-server",
"version": "1.0.0",
"description": "MCP Server for database queries",
"type": "module",
"main": "dist/index.js",
"bin": {
"mcp-database-server": "dist/index.js"
},
"files": [
"dist/**/*",
"README.md",
"LICENSE"
],
"keywords": [
"mcp",
"model-context-protocol",
"database",
"postgresql",
"ai"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourname/mcp-database-server"
},
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"build": "tsc",
"test": "vitest run",
"prepublishOnly": "npm run build && npm test"
}
}
2. 确保入口文件有 shebang
// src/index.ts 的第一行
#!/usr/bin/env node
构建后需要确保 dist/index.js 也有这行,并且有执行权限:
// package.json scripts
{
"scripts": {
"build": "tsc && chmod +x dist/index.js"
}
}
3. 编写 README
README 对于 MCP Server 来说尤其重要,因为用户需要知道如何配置:
# mcp-database-server
MCP Server for querying PostgreSQL databases.
## Installation
npx mcp-database-server <connection-string>
## Configuration
Add to your Claude Code MCP settings:
{
"mcpServers": {
"database": {
"command": "npx",
"args": ["-y", "mcp-database-server", "postgresql://..."]
}
}
}
## Available Tools
- list_tables - List all database tables
- describe_table - Show table structure
- query - Execute read-only SQL queries
- table_stats - Get table statistics
4. 发布流程
# 1. 确保已登录 npm
npm login
# 2. 检查包名是否可用
npm view mcp-database-server
# 3. 构建和测试
npm run build
npm test
# 4. 试运行发布(不实际发布)
npm publish --dry-run
# 5. 正式发布
npm publish
# 6. 验证发布结果
npx mcp-database-server --help
5. 版本管理
遵循语义化版本(Semantic Versioning):
# 修复 Bug
npm version patch # 1.0.0 → 1.0.1
# 新增功能(向后兼容)
npm version minor # 1.0.1 → 1.1.0
# 破坏性变更
npm version major # 1.1.0 → 2.0.0
发布检查清单
发布前确认以下事项:
| 检查项 | 状态 |
|---|---|
| 所有测试通过 | |
| TypeScript 编译无错误 | |
| README 包含安装和配置说明 | |
| package.json 的 bin 字段正确 | |
| 入口文件有 shebang 和执行权限 | |
| files 字段只包含必要文件 | |
| 没有包含敏感信息(.env, tokens) | |
| LICENSE 文件存在 | |
| keywords 包含 “mcp” | |
| engines 指定了 Node.js 最低版本 |
CI/CD 自动化
使用 GitHub Actions 自动化测试和发布:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npm test
publish:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && startsWith(github.event.head_commit.message, 'release:')
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
总结
MCP Server 的测试和发布流程并不复杂,但每一步都很重要:
- 单元测试确保业务逻辑正确
- 集成测试确保 MCP 协议交互正常
- MCP Inspector 提供可视化的端到端验证
- npm 发布让你的 Server 能被全世界使用
整个 MCP 开发实战系列到这里就结束了。从协议理解到 Server 开发,从数据库到 API 到文件处理,再到测试和发布,我们走完了一个完整的开发周期。
写代码是第一步,测试是第二步,发布是第三步。每一步都不能省略,因为你的 MCP Server 会被 AI 信任和使用。
相关文章
评论
加载中...
评论
加载中...