MCP生态已突破13000+公开服务器、9700万月SDK下载量,但社区中大量服务器缺少系统化的测试与调试手段,生产环境故障频发。本教程面向已有MCP Server开发经验的工程师,系统讲解从单元测试、集成测试、MCP Inspector调试,到OpenTelemetry可观测性注入和CI/CD自动化管线构建的全链路质量工程方法,助你打造生产级MCP服务器。
适用人群:已掌握MCP Server基础开发(TypeScript/Python)、需要将服务器推向生产环境的后端工程师和平台开发者。
@modelcontextprotocol/sdk(TypeScript)或 mcp(Python)测试是质量工程的基石。MCP Server的测试策略与传统Web服务有本质差异:它不是HTTP REST API,而是基于JSON-RPC 2.0的协议通信。理解这一点,才能设计出正确的测试分层。
pyramid
title MCP Server测试金字塔
"E2E测试(MCP Inspector + 真实客户端)" : 10
"集成测试(Transport层 + Tool调用链路)" : 30
"单元测试(Tool Handler纯函数 + Schema校验)" : 60
MCP使用三种Transport:
| Transport | 适用场景 | 测试挑战 |
|---|---|---|
stdio |
本地CLI集成 | 子进程通信,无法用HTTP客户端测试 |
SSE(已废弃) |
旧版远程服务器 | 2026年4月已弃用 |
Streamable HTTP |
新版远程服务器 | 非标准REST,需JSON-RPC客户端 |
核心差异:MCP请求格式为 {"jsonrpc":"2.0","method":"tools/call","params":{...},"id":1},不是 POST /api/tools。
MCP Server的核心逻辑是Tool Handler——接收参数、执行操作、返回结果。将Handler设计为纯函数是可测试性的关键。
项目结构:
my-mcp-server/
├── src/
│ ├── index.ts # 入口,注册Transport
│ ├── server.ts # McpServer实例与Tool注册
│ ├── tools/
│ │ ├── calculator.ts # 计算器Tool Handler
│ │ ├── database.ts # 数据库查询Tool Handler
│ │ └── weather.ts # 天气API Tool Handler
│ └── utils/
│ ├── validators.ts # 参数校验
│ └── formatters.ts # 输出格式化
├── tests/
│ ├── unit/
│ │ ├── calculator.test.ts
│ │ ├── database.test.ts
│ │ └── validators.test.ts
│ ├── integration/
│ │ └── server.test.ts
│ └── e2e/
│ └── full-flow.test.ts
├── package.json
└── tsconfig.json
核心技巧:将Tool逻辑与MCP框架解耦。
// src/tools/calculator.ts — 业务逻辑纯函数,不依赖MCP框架
export interface CalcInput {
expression: string;
precision?: number;
}
export interface CalcResult {
value: number;
formatted: string;
}
export function calculate(input: CalcInput): CalcResult {
// 纯逻辑,易于测试
const { expression, precision = 2 } = input;
const sanitized = expression.replace(/[^0-9+\-*/().%\s]/g, '');
if (sanitized !== expression) {
throw new Error(`不安全表达式: ${expression}`);
}
const value = Function(`"use strict"; return (${sanitized})`)();
if (typeof value !== 'number' || !isFinite(value)) {
throw new Error(`计算结果无效: ${value}`);
}
return {
value,
formatted: value.toFixed(precision),
};
}
// 将纯函数包装为MCP Tool Handler
export function registerCalculatorTools(server: McpServer): void {
server.tool(
'calculate',
'执行数学表达式计算',
{
expression: z.string().describe('数学表达式'),
precision: z.number().optional().default(2).describe('小数精度'),
},
async ({ expression, precision }) => {
const result = calculate({ expression, precision });
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
}
);
}
单元测试:
// tests/unit/calculator.test.ts
import { describe, it, expect } from 'vitest';
import { calculate } from '../../src/tools/calculator.js';
describe('calculate', () => {
it('基础四则运算', () => {
expect(calculate({ expression: '2 + 3' }).value).toBe(5);
expect(calculate({ expression: '10 / 3', precision: 4 }).formatted).toBe('3.3333');
});
it('拒绝注入攻击', () => {
expect(() => calculate({ expression: 'require("fs")' })).toThrow('不安全表达式');
expect(() => calculate({ expression: 'process.exit(1)' })).toThrow('不安全表达式');
});
it('边界值处理', () => {
expect(() => calculate({ expression: '1/0' })).toThrow('计算结果无效');
expect(calculate({ expression: '0.1 + 0.2', precision: 1 }).formatted).toBe('0.3');
});
});
运行结果:
$ npx vitest run tests/unit/
✓ calculator.test.ts (3 tests) 5ms
✓ validators.test.ts (8 tests) 12ms
✓ formatters.test.ts (4 tests) 8ms
Test Files 3 passed (3)
Tests 15 passed (15)
单元测试验证了业务逻辑,但Tool注册、Schema校验、Transport通信需要集成测试覆盖。TypeScript SDK提供了 Client 类,可在进程内连接MCP Server。
// tests/integration/server.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { createServer } from '../../src/server.js';
describe('MCP Server 集成测试', () => {
let client: Client;
let transport: StdioClientTransport;
beforeAll(async () => {
// 使用 in-memory Transport,无需启动子进程
const { server: mcpServer, cleanup } = createServer();
// 创建Client直连
client = new Client({
name: 'test-client',
version: '1.0.0',
});
// 通过 stdio pipe 连接
transport = new StdioClientTransport({
command: 'node',
args: ['--experimental-strip-types', 'src/index.ts'],
});
await client.connect(transport);
});
afterAll(async () => {
await client.close();
});
it('列出所有Tools', async () => {
const { tools } = await client.listTools();
expect(tools.length).toBeGreaterThan(0);
const calcTool = tools.find(t => t.name === 'calculate');
expect(calcTool).toBeDefined();
expect(calcTool!.description).toBeTruthy();
});
it('调用calculate Tool', async () => {
const result = await client.callTool({
name: 'calculate',
arguments: { expression: '144 / 12', precision: 0 },
});
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
const parsed = JSON.parse((result.content[0] as any).text);
expect(parsed.value).toBe(12);
expect(parsed.formatted).toBe('12');
});
it('传入无效参数返回错误', async () => {
const result = await client.callTool({
name: 'calculate',
arguments: { expression: 'require("fs")' },
});
expect(result.isError).toBe(true);
});
it('Schema校验:缺少必填参数', async () => {
await expect(
client.callTool({
name: 'calculate',
arguments: {},
})
).rejects.toThrow();
});
});
使用 @modelcontextprotocol/sdk 的 In-Memory Transport(更快的方案):
// tests/integration/memory-transport.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
createInMemoryTransport,
} from '@modelcontextprotocol/sdk/inMemory.js';
import { registerCalculatorTools } from '../../src/tools/calculator.js';
describe('In-Memory Transport 集成测试', () => {
let client: Client;
beforeAll(async () => {
// 创建Server实例
const server = new Server(
{ name: 'test-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
registerCalculatorTools(server as any);
// 创建双向内存Transport
const [clientTransport, serverTransport] =
createInMemoryTransport();
// 同时连接
await Promise.all([
server.connect(serverTransport),
(async () => {
client = new Client({
name: 'test-client',
version: '1.0.0',
});
await client.connect(clientTransport);
})(),
]);
});
it('完整Tool调用链路', async () => {
const result = await client.callTool({
name: 'calculate',
arguments: { expression: '(100 - 37) * 2.5' },
});
const parsed = JSON.parse((result.content[0] as any).text);
expect(parsed.value).toBe(157.5);
});
});
内存Transport的优势:无子进程开销,测试速度提升10-50倍,适合CI环境。
MCP Inspector是官方推出的交互式调试工具,提供Web界面连接MCP Server,实时查看Tool列表、调用参数、响应结果和协议交互日志。
# 方式一:npx直接启动(推荐)
npx @modelcontextprotocol/inspector node dist/index.js
# 方式二:指定环境变量
npx @modelcontextprotocol/inspector \
-e API_KEY=sk-test-123 \
-e DB_URL=postgres://localhost/test \
node dist/index.js
# 方式三:调试远程Streamable HTTP服务器
npx @modelcontextprotocol/inspector --url http://localhost:3000/mcp
启动后,Inspector会打开浏览器(默认 http://localhost:6274),左侧面板显示连接的Server信息。
flowchart LR
A[启动Inspector] --> B[连接Server]
B --> C[浏览Tools列表]
C --> D[选择Tool]
D --> E[填写参数]
E --> F[发送调用]
F --> G{结果正常?}
G -->|是| H[查看响应JSON]
G -->|否| I[查看错误堆栈]
I --> J[定位源码]
J --> K[修复并重启]
K --> B
H --> L[检查性能指标]
L --> M[记录测试用例]
Tools面板:
// Inspector展示的Tool Schema(自动从Server拉取)
{
"name": "calculate",
"description": "执行数学表达式计算",
"inputSchema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "数学表达式"
},
"precision": {
"type": "number",
"default": 2,
"description": "小数精度"
}
},
"required": ["expression"]
}
}
调用结果可视化:
┌──────────────────────────────────────────────┐
│ Tool: calculate │
│ Arguments: { "expression": "sqrt(144)" } │
├──────────────────────────────────────────────┤
│ Response (127ms) │
│ │
│ content[0]: │
│ type: text │
│ text: {"value":12,"formatted":"12.00"} │
├──────────────────────────────────────────────┤
│ Notifications: │
│ → tools/list (5ms) │
│ → tools/call (122ms) │
└──────────────────────────────────────────────┘
Resources面板:查看Server暴露的静态/动态资源,支持预览。
Prompts面板:列出所有Prompt模板,支持参数化渲染测试。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 连接超时 | Server启动慢或端口冲突 | 检查stdio输出日志,增加 -t 30000 超时 |
| Tool不显示 | 注册顺序或Schema错误 | 检查 server.tool() 调用是否在 connect() 之前 |
调用报 Method not found |
Capability未声明 | 在Server构造时添加 { capabilities: { tools: {} } } |
| 中文乱码 | stdio编码问题 | 确认环境变量 LANG=en_US.UTF-8 或等效 |
社区已涌现专用MCP测试工具。mcp-testing-framework(npm/PyPI均可用)支持自动生成测试套件、模拟Tool调用、验证协议合规性。
npm install --save-dev mcp-testing-framework
# 或
pip install mcp-testing-framework
// tests/generated/auto-generated.test.ts
import { MCPTestRunner } from 'mcp-testing-framework';
import { describe, it, expect } from 'vitest';
const runner = new MCPTestRunner({
serverCommand: 'node',
serverArgs: ['dist/index.js'],
timeout: 10000,
});
describe('MCP Server 自动化合规测试', () => {
beforeAll(async () => {
await runner.start();
});
afterAll(async () => {
await runner.stop();
});
it('协议版本协商', async () => {
const info = await runner.getServerInfo();
expect(info.protocolVersion).toMatch(/^2025-/);
expect(info.capabilities).toBeDefined();
});
it('所有Tool均包含描述', async () => {
const tools = await runner.listTools();
for (const tool of tools) {
expect(tool.description).toBeTruthy();
expect(tool.description!.length).toBeGreaterThan(10);
expect(tool.inputSchema).toBeDefined();
expect(tool.inputSchema.type).toBe('object');
}
});
it('Tool调用返回合法content格式', async () => {
const tools = await runner.listTools();
for (const tool of tools) {
// 跳过需要外部依赖的Tool
if (['database_query'].includes(tool.name)) continue;
const result = await runner.callToolWithDefaults(tool.name);
expect(result.content).toBeInstanceOf(Array);
expect(result.content.length).toBeGreaterThan(0);
for (const item of result.content) {
expect(['text', 'image', 'resource']).toContain(item.type);
}
}
});
});
// tests/integration/weather-with-mock.test.ts
import { describe, it, expect, vi } from 'vitest';
import { registerWeatherTools } from '../../src/tools/weather.js';
// Mock fetch API
global.fetch = vi.fn();
describe('Weather Tool with Mock', () => {
it('正常返回天气数据', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
main: { temp: 22.5, humidity: 65 },
weather: [{ description: '晴朗' }],
}),
});
const result = await callTool('get_weather', { city: '上海' });
expect(result.content[0].text).toContain('22.5');
});
it('API超时处理', async () => {
(global.fetch as any).mockRejectedValueOnce(
new Error('Request timeout')
);
const result = await callTool('get_weather', { city: '北京' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('timeout');
});
});
生产环境的MCP Server需要回答三个问题:谁调用了什么?花了多久?为什么失败? OpenTelemetry提供标准化的Tracing、Metrics和Logging三支柱。
flowchart TB
subgraph Claude Code
CC[Claude Code Client]
end
subgraph MCP Server
MW[OTel Middleware]
T1[Tool: calculate]
T2[Tool: database_query]
T3[Tool: get_weather]
MW --> T1
MW --> T2
MW --> T3
end
subgraph Observability Stack
OTEL[OTel Collector]
JAEG[Jaeger UI]
PROM[Prometheus]
LOKI[Loki / ELK]
end
CC -->|JSON-RPC| MW
T1 & T2 & T3 -->|spans| OTEL
OTEL --> JAEG
OTEL --> PROM
OTEL --> LOKI
// src/telemetry/index.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'my-mcp-server',
[ATTR_SERVICE_VERSION]: process.env.npm_package_version || '1.0.0',
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
}),
exportIntervalMillis: 10000,
}),
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown());
// src/telemetry/instrumented-tool.ts
import { trace, metrics, diag } from '@opentelemetry/api';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const tracer = trace.getTracer('mcp-server', '1.0.0');
const meter = metrics.getMeter('mcp-server', '1.0.0');
// 定义Metrics
const toolCallCounter = meter.createCounter('mcp.tool.calls', {
description: 'Tool调用次数',
});
const toolCallDuration = meter.createHistogram('mcp.tool.duration', {
description: 'Tool调用耗时(ms)',
unit: 'ms',
});
const toolErrorCounter = meter.createCounter('mcp.tool.errors', {
description: 'Tool调用错误次数',
});
/**
* 为Tool Handler添加OTel自动埋点
*/
export function instrumentedTool(
server: McpServer,
name: string,
description: string,
schema: Record<string, any>,
handler: (args: any) => Promise<any>
): void {
server.tool(name, description, schema, async (args, extra) => {
const startTime = Date.now();
const span = tracer.startSpan(`tool.${name}`, {
attributes: {
'mcp.tool.name': name,
'mcp.tool.args_count': Object.keys(args).length,
},
});
try {
const result = await handler(args);
// 记录成功Metrics
toolCallCounter.add(1, { tool: name, status: 'success' });
toolCallDuration.record(Date.now() - startTime, { tool: name });
span.setAttribute('mcp.tool.result_type', typeof result);
span.setStatus({ code: 1 }); // OK
return result;
} catch (error: any) {
// 记录失败Metrics
toolCallCounter.add(1, { tool: name, status: 'error' });
toolErrorCounter.add(1, { tool: name, error_type: error.constructor.name });
toolCallDuration.record(Date.now() - startTime, { tool: name, status: 'error' });
span.setStatus({ code: 2, message: error.message }); // ERROR
span.recordException(error);
return {
content: [{ type: 'text' as const, text: `错误: ${error.message}` }],
isError: true,
};
} finally {
span.end();
}
});
}
// src/server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { instrumentedTool } from './telemetry/instrumented-tool.js';
import { calculate } from './tools/calculator.js';
import { z } from 'zod';
import './telemetry/index.js'; // 初始化OTel
export function createServer() {
const server = new McpServer({
name: 'my-instrumented-server',
version: '1.0.0',
});
// 注册带可观测性的Tool
instrumentedTool(
server,
'calculate',
'执行数学表达式计算',
{
expression: z.string().describe('数学表达式'),
precision: z.number().optional().describe('小数精度'),
},
async (args) => {
const result = calculate(args);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
};
}
);
instrumentedTool(
server,
'get_weather',
'查询城市天气',
{
city: z.string().describe('城市名称'),
},
async (args) => {
const resp = await fetch(
`https://api.weather.example/v1?city=${encodeURIComponent(args.city)}`,
{ signal: AbortSignal.timeout(5000) }
);
const data = await resp.json();
return {
content: [{ type: 'text' as const, text: JSON.stringify(data) }],
};
}
);
return server;
}
部署Grafana + Prometheus后,关注以下Dashboard面板:
| 指标名 | 类型 | 含义 | 告警阈值 |
|---|---|---|---|
mcp_tool_calls_total |
Counter | Tool总调用次数 | — |
mcp_tool_duration_ms |
Histogram | Tool执行耗时分布 | P99 > 5s |
mcp_tool_errors_total |
Counter | Tool错误次数 | 5min内 > 10 |
mcp_tool_duration_ms{quantile="0.99"} |
Gauge | P99延迟 | > 3s |
rate(mcp_tool_calls_total[5m]) |
Rate | 5分钟QPS | 突增10x |
SSE Transport已于2026年4月正式废弃。Streamable HTTP成为远程MCP Server的标准Transport。测试时需要特别关注会话管理和无状态模式。
sequenceDiagram
participant Client as MCP Client
participant Server as MCP Server
Client->>Server: POST /mcp (JSON-RPC initialize)
Server-->>Client: 200 OK + Mcp-Session-Id: abc123
Client->>Server: POST /mcp (tools/list) <br/>Header: Mcp-Session-Id: abc123
Server-->>Client: 200 OK (JSON response)
Client->>Server: POST /mcp (tools/call, long-running) <br/>Header: Mcp-Session-Id: abc123
Server-->>Client: 200 OK (SSE stream of progress + result)
Note over Client,Server: 会话超时或Client主动DELETE
Client->>Server: DELETE /mcp <br/>Header: Mcp-Session-Id: abc123
Server-->>Client: 200 OK (Session terminated)
// tests/integration/streamable-http.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import express from 'express';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createServer } from '../../src/server.js';
describe('Streamable HTTP Transport 测试', () => {
let app: express.Application;
let server: any;
beforeAll(async () => {
app = express();
app.use(express.json());
const mcpServer = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
});
await mcpServer.connect(transport);
// MCP协议路由
app.post('/mcp', (req, res) => transport.handleRequest(req, res));
app.delete('/mcp', (req, res) => transport.handleRequest(req, res));
app.get('/mcp', (req, res) => transport.handleRequest(req, res));
server = app.listen(0); // 随机端口
});
afterAll(() => server.close());
function getPort() {
return server.address().port;
}
it('初始化握手', async () => {
const resp = await fetch(`http://localhost:${getPort()}/mcp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: { name: 'test', version: '1.0.0' },
},
id: 1,
}),
});
expect(resp.status).toBe(200);
const sessionId = resp.headers.get('mcp-session-id');
expect(sessionId).toBeTruthy();
const body = await resp.json();
expect(body.result.protocolVersion).toBe('2025-03-26');
});
it('无Session ID调用返回错误', async () => {
const resp = await fetch(`http://localhost:${getPort()}/mcp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/list',
params: {},
id: 2,
}),
});
// 有状态的Transport要求Session ID
expect(resp.status).toBe(400);
});
});
将MCP Server的测试、构建、发布集成到GitHub Actions,实现提交即验证。
# .github/workflows/ci.yml
name: MCP Server CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npx tsc --noEmit
- name: Unit tests
run: npx vitest run tests/unit/ --coverage
env:
NODE_ENV: test
- name: Integration tests
run: npx vitest run tests/integration/
env:
NODE_ENV: test
- name: MCP Protocol compliance
run: |
# 启动Server并用Inspector验证
node dist/index.js &
SERVER_PID=$!
sleep 3
# 运行协议合规性检查
npx mcp-testing-framework check \
--command "node dist/index.js" \
--check-tools \
--check-resources \
--check-prompts \
--timeout 30000
kill $SERVER_PID 2>/dev/null || true
- name: Upload coverage
uses: codecov/codecov-action@v4
if: matrix.node-version == 22
with:
files: ./coverage/lcov.info
publish:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Install and build
run: npm ci && npm run build
- name: Publish to npm
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
在 package.json 中添加自动化脚本:
{
"scripts": {
"prepublishOnly": "npm run lint && npm run test && npm run build",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:integration": "vitest run tests/integration/",
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
"lint": "eslint src/",
"build": "tsc",
"quality": "npm run lint && npm run test:coverage && npm run test:integration"
}
}
一条命令跑完所有质量门禁:
$ npm run quality
> eslint src/ ✓ 0 errors, 0 warnings
> vitest run --coverage ✓ 47 tests passed
> vitest run tests/integration/ ✓ 12 tests passed
Coverage: 89.3% statements, 82.1% branches, 95.0% functions
将上述所有技术融合到一个完整项目中——一个调用天气API的MCP Server,具备完整的单元测试、集成测试、OTel可观测性和CI/CD。
入口文件 src/index.ts:
#!/usr/bin/env node
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createServer } from './server.js';
import './telemetry/index.js';
async function main() {
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
业务逻辑 src/tools/weather.ts:
export interface WeatherInput {
city: string;
units?: 'metric' | 'imperial';
}
export interface WeatherOutput {
city: string;
temperature: number;
humidity: number;
description: string;
units: string;
}
export function formatWeatherResponse(output: WeatherOutput): string {
return `${output.city}: ${output.description}, ` +
`${output.temperature}°${output.units === 'imperial' ? 'F' : 'C'}, ` +
`湿度 ${output.humidity}%`;
}
export async function fetchWeatherData(
input: WeatherInput,
apiClient: (url: string) => Promise<Response> = fetch
): Promise<WeatherOutput> {
const { city, units = 'metric' } = input;
if (!city || city.trim().length === 0) {
throw new Error('城市名称不能为空');
}
const url = `https://api.openweathermap.org/data/2.5/weather?` +
`q=${encodeURIComponent(city)}&units=${units}&appid=${process.env.WEATHER_API_KEY || ''}`;
const response = await apiClient(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`未找到城市: ${city}`);
}
if (response.status === 429) {
throw new Error('API调用频率超限,请稍后重试');
}
throw new Error(`天气API错误: ${response.status}`);
}
const data = await response.json();
return {
city: data.name,
temperature: Math.round(data.main.temp * 10) / 10,
humidity: data.main.humidity,
description: data.weather[0].description,
units,
};
}
单元测试 tests/unit/weather.test.ts:
import { describe, it, expect } from 'vitest';
import { fetchWeatherData, formatWeatherResponse } from '../../src/tools/weather.js';
// 创建Mock fetch
function createMockFetch(data: any, status = 200): (url: string) => Promise<Response> {
return async () =>
new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
describe('fetchWeatherData', () => {
const mockSuccessData = {
name: 'Shanghai',
main: { temp: 28.5, humidity: 72 },
weather: [{ description: '多云' }],
};
it('正常返回天气', async () => {
const result = await fetchWeatherData(
{ city: '上海' },
createMockFetch(mockSuccessData)
);
expect(result.city).toBe('Shanghai');
expect(result.temperature).toBe(28.5);
expect(result.description).toBe('多云');
});
it('城市为空抛出异常', async () => {
await expect(fetchWeatherData({ city: '' })).rejects.toThrow('不能为空');
await expect(fetchWeatherData({ city: ' ' })).rejects.toThrow('不能为空');
});
it('城市不存在返回404错误', async () => {
await expect(
fetchWeatherData({ city: '不存在的城市XYZ' }, createMockFetch({}, 404))
).rejects.toThrow('未找到城市');
});
it('限流返回429错误', async () => {
await expect(
fetchWeatherData({ city: '北京' }, createMockFetch({}, 429))
).rejects.toThrow('频率超限');
});
it('华氏温度转换', async () => {
const result = await fetchWeatherData(
{ city: 'New York', units: 'imperial' },
createMockFetch({ ...mockSuccessData, main: { temp: 83.3, humidity: 50 } })
);
expect(result.units).toBe('imperial');
});
});
describe('formatWeatherResponse', () => {
it('格式化输出', () => {
const result = formatWeatherResponse({
city: '上海',
temperature: 28.5,
humidity: 72,
description: '多云',
units: 'metric',
});
expect(result).toBe('上海: 多云, 28.5°C, 湿度 72%');
});
});
# 安装依赖
npm install
# 运行全部测试
npm run quality
# 启动Inspector手动调试
npm run inspector
# 启动OTel + Jaeger本地栈
docker compose up -d # 包含 otel-collector, jaeger, prometheus, grafana
node dist/index.js # 启动带遥测的Server
# 在Jaeger UI查看Traces
open http://localhost:16686
# 搜索 service = "my-mcp-server"
# 查看 tool.get_weather 的Trace详情
# docker-compose.observability.yml
version: '3.8'
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
ports:
- "4317:4317" # gRPC
- "4318:4318" # HTTP
volumes:
- ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # UI
environment:
- COLLECTOR_OTLP_ENABLED=true
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
flowchart TB
subgraph "第一层:Tool Handler"
A[参数校验] --> B{合法?}
B -->|否| C[返回 isError: true + 友好提示]
B -->|是| D[执行业务逻辑]
end
subgraph "第二层:OTel Middleware"
D --> E[try/catch包裹]
E --> F{异常?}
F -->|是| G[记录Span Error + Metrics]
F -->|否| H[记录Span OK + Duration]
end
subgraph "第三层:Transport层"
G --> I[格式化为MCP错误响应]
H --> I
I --> J[返回给Client]
end
// tests/performance/baseline.test.ts
import { describe, it, expect } from 'vitest';
import { performance } from 'perf_hooks';
describe('性能基线测试', () => {
const BASELINE_MS = {
calculate: 5, // 纯计算,应 < 5ms
get_weather: 2000, // 含网络请求,应 < 2s
database_query: 500, // 含DB查询,应 < 500ms
};
it('calculate Tool响应时间', async () => {
const start = performance.now();
await callTool('calculate', { expression: '1+1' });
const duration = performance.now() - start;
expect(duration).toBeLessThan(BASELINE_MS.calculate);
});
it('并发10次calculate无退化', async () => {
const results = await Promise.all(
Array(10).fill(null).map(() =>
timedCall('calculate', { expression: '2**10' })
)
);
const avgDuration = results.reduce((a, b) => a + b, 0) / results.length;
expect(avgDuration).toBeLessThan(BASELINE_MS.calculate * 3);
});
});
在发布npm包前,逐项确认:
package.json 包含 mcp 关键词和正确的 bin 字段README.md 包含一键安装命令[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
