首页 › tutorials › MCP Server质量工程:测试调试到可观测

MCP Server质量工程:测试调试到可观测性全链路实战

概述

MCP生态已突破13000+公开服务器、9700万月SDK下载量,但社区中大量服务器缺少系统化的测试与调试手段,生产环境故障频发。本教程面向已有MCP Server开发经验的工程师,系统讲解从单元测试、集成测试、MCP Inspector调试,到OpenTelemetry可观测性注入和CI/CD自动化管线构建的全链路质量工程方法,助你打造生产级MCP服务器。

适用人群:已掌握MCP Server基础开发(TypeScript/Python)、需要将服务器推向生产环境的后端工程师和平台开发者。

前置条件


一、MCP Server测试金字塔

测试是质量工程的基石。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

1.1 为什么传统HTTP测试工具不适用

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。

1.2 单元测试:隔离Tool Handler

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)

1.3 集成测试:验证MCP协议层

单元测试验证了业务逻辑,但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:可视化调试利器

MCP Inspector是官方推出的交互式调试工具,提供Web界面连接MCP Server,实时查看Tool列表、调用参数、响应结果和协议交互日志。

2.1 启动Inspector

# 方式一: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信息。

2.2 Inspector调试工作流

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[记录测试用例]

2.3 Inspector核心功能

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模板,支持参数化渲染测试。

2.4 Inspector常见问题

问题 原因 解决方案
连接超时 Server启动慢或端口冲突 检查stdio输出日志,增加 -t 30000 超时
Tool不显示 注册顺序或Schema错误 检查 server.tool() 调用是否在 connect() 之前
调用报 Method not found Capability未声明 在Server构造时添加 { capabilities: { tools: {} } }
中文乱码 stdio编码问题 确认环境变量 LANG=en_US.UTF-8 或等效

三、自动化测试框架:mcp-testing-framework

社区已涌现专用MCP测试工具。mcp-testing-framework(npm/PyPI均可用)支持自动生成测试套件、模拟Tool调用、验证协议合规性。

3.1 安装与初始化

npm install --save-dev mcp-testing-framework
# 或
pip install mcp-testing-framework

3.2 自动生成测试用例

// 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);
      }
    }
  });
});

3.3 Mock外部依赖

// 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');
  });
});

四、OpenTelemetry可观测性注入

生产环境的MCP Server需要回答三个问题:谁调用了什么?花了多久?为什么失败? OpenTelemetry提供标准化的Tracing、Metrics和Logging三支柱。

4.1 架构设计

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

4.2 实现OTel Middleware

// 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());

4.3 包裹Tool Handler注入Tracing

// 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();
    }
  });
}

4.4 使用Instrumented Tool

// 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;
}

4.5 可观测性仪表盘关键指标

部署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

五、Streamable HTTP Transport测试

SSE Transport已于2026年4月正式废弃。Streamable HTTP成为远程MCP Server的标准Transport。测试时需要特别关注会话管理和无状态模式。

5.1 Streamable HTTP 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)

5.2 Streamable HTTP集成测试

// 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);
  });
});

六、CI/CD管线自动化

将MCP Server的测试、构建、发布集成到GitHub Actions,实现提交即验证。

6.1 完整GitHub Actions Workflow

# .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 }}

6.2 发布前检查清单

在 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

七、实战案例:带全链路质量的天气MCP服务器

将上述所有技术融合到一个完整项目中——一个调用天气API的MCP Server,具备完整的单元测试、集成测试、OTel可观测性和CI/CD。

7.1 完整项目源码

入口文件 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%');
  });
});

7.2 运行全链路质量验证

# 安装依赖
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详情

7.3 Docker Compose可观测性栈

# 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

八、生产环境最佳实践

8.1 错误处理三层防御

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

8.2 性能基线与回归检测

// 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);
  });
});

8.3 MCP Server发布清单

在发布npm包前,逐项确认:


参考来源

  1. MCP Inspector官方文档 — MCP Inspector使用指南与调试工作流
  2. MCP TypeScript SDK - Server文档 — TypeScript SDK服务端API参考
  3. MCP Testing Strategies: Unit Tests, Integration Tests — 完整的MCP Server测试策略指南
  4. MCP Streamable HTTP Transport: From SSE Migration to Production — Streamable HTTP协议详解与迁移实践
  5. How to Instrument MCP Servers with OpenTelemetry — OTel注入MCP Server的完整指南

[!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 等场景全覆盖。

51学通信知识星球海报-扫码加入