<!-- title: MCP Server质量工程：测试调试到可观测 -->

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

## 概述

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

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

## 前置条件

- Node.js 20+ / Python 3.11+ 运行环境
- 已安装 `@modelcontextprotocol/sdk`（TypeScript）或 `mcp`（Python）
- 了解 MCP 协议基本概念（Tool、Resource、Prompt、Transport）
- 熟悉 Jest / Vitest（TypeScript）或 pytest（Python）测试框架
- 一个已开发的 MCP Server 项目（本文以 TypeScript 为主）

---

## 一、MCP Server测试金字塔

测试是质量工程的基石。MCP Server的测试策略与传统Web服务有本质差异：它不是HTTP REST API，而是基于JSON-RPC 2.0的协议通信。理解这一点，才能设计出正确的测试分层。

```text
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框架解耦**。

```typescript
// 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) }],
      };
    }
  );
}
```

**单元测试**：

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

运行结果：

```bash
$ 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。

```typescript
// 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**（更快的方案）：

```typescript
// 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

```bash
# 方式一：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调试工作流

```mermaid
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面板**：

```json
// 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 安装与初始化

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

### 3.2 自动生成测试用例

```typescript
// 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外部依赖

```typescript
// 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 架构设计

```mermaid
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

```typescript
// 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

```typescript
// 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

```typescript
// 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原理

```mermaid
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集成测试

```typescript
// 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

```yaml
# .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` 中添加自动化脚本：

```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"
  }
}
```

一条命令跑完所有质量门禁：

```bash
$ 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`**：

```typescript
#!/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`**：

```typescript
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`**：

```typescript
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 运行全链路质量验证

```bash
# 安装依赖
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可观测性栈

```yaml
# 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 错误处理三层防御

```mermaid
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 性能基线与回归检测

```typescript
// 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包前，逐项确认：

- [ ] 所有单元测试通过（覆盖率 > 80%）
- [ ] 集成测试通过（含Transport层验证）
- [ ] MCP Inspector手动验证所有Tool
- [ ] `package.json` 包含 `mcp` 关键词和正确的 `bin` 字段
- [ ] `README.md` 包含一键安装命令
- [ ] TypeScript编译无错误，导出类型正确
- [ ] 环境变量文档完整（无硬编码密钥）
- [ ] OTel集成可选（不强制依赖Collector）
- [ ] 错误消息用户友好（无堆栈泄露）

---

## 参考来源

1. [MCP Inspector官方文档](https://modelcontextprotocol.io/docs/tools/inspector) — MCP Inspector使用指南与调试工作流
2. [MCP TypeScript SDK - Server文档](https://ts.sdk.modelcontextprotocol.io/documents/server.html) — TypeScript SDK服务端API参考
3. [MCP Testing Strategies: Unit Tests, Integration Tests](https://chatforest.com/guides/mcp-testing-strategies/) — 完整的MCP Server测试策略指南
4. [MCP Streamable HTTP Transport: From SSE Migration to Production](https://apigene.ai/blog/mcp-streamable-http) — Streamable HTTP协议详解与迁移实践
5. [How to Instrument MCP Servers with OpenTelemetry](https://oneuptime.com/blog/post/2026-03-26-how-to-instrument-mcp-servers-with-opentelemetry/view) — 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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
