<!-- title: MCP Server调试到生产：全链路可观测性实战 -->

# MCP Server调试到生产：生命周期到可观测性全链路实战

## 概述

本教程面向已掌握MCP Server基础开发的工程师，聚焦从调试验证到生产监控的完整链路。你将学会MCP协议生命周期五阶段调试、Inspector可视化测试、结构化日志与OpenTelemetry集成、错误分级处理模式，以及生产级健康检查与告警配置。学完后，你的MCP Server将从"能跑"升级为"可信"。

**适用人群**：已有TypeScript/Python MCP开发经验，需要将MCP服务推向生产环境的开发者。

## 前置条件

- Node.js 20+ 和 npm/pnpm
- 已安装Claude Code CLI
- 了解MCP协议基础概念（Tool、Resource、Prompt三原语）
- 一台可运行MCP Inspector的开发机
- （可选）Docker用于容器化部署

---

## 第一章：MCP协议生命周期——五阶段深度调试

### 1.1 为什么理解生命周期是调试的起点

MCP协议不是简单的请求-响应模型。它定义了严格的**五阶段生命周期**：Initialize → Capability Negotiation → Operation → Shutdown → Error Recovery。90%的MCP调试问题都出在前两个阶段。

```mermaid
sequenceDiagram
    participant Client as Claude Code (Client)
    participant Server as MCP Server

    Note over Client,Server: Phase 1: Initialize
    Client->>Server: initialize {capabilities, clientInfo}
    Server-->>Client: {capabilities, serverInfo, protocolVersion}

    Note over Client,Server: Phase 2: Capability Negotiation
    Client->>Server: initialized notification
    Note right of Client: 双方确认支持的特性

    Note over Client,Server: Phase 3: Operation
    Client->>Server: tools/list
    Server-->>Client: [tool definitions]
    Client->>Server: tools/call {name, arguments}
    Server-->>Client: {content, isError}

    Note over Client,Server: Phase 4: Shutdown
    Client->>Server: JSON-RPC batch close
    Note over Server: 清理资源

    Note over Client,Server: Phase 5: Error Recovery
    Server-->>Client: notifications/stderr {error}
    Client->>Server: re-initialize (if transport supports)
```

### 1.2 手动模拟Initialize阶段

在开发中，最常见的问题是**初始化握手失败**。让我们手动模拟这个过程来定位问题。

创建一个最小化的调试用MCP Server：

```typescript
// debug-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "debug-server",
  version: "0.1.0",
});

// 注册一个简单的ping工具
server.tool(
  "ping",
  "健康检查工具，返回pong和服务器状态",
  {},
  async () => ({
    content: [
      {
        type: "text",
        text: JSON.stringify({
          status: "pong",
          timestamp: new Date().toISOString(),
          uptime: process.uptime(),
          memory: process.memoryUsage(),
        }),
      },
    ],
  })
);

// 注册一个带参数的工具用于调试
server.tool(
  "echo",
  "回显输入参数，用于调试协议传输",
  {
    message: z.string().describe("要回显的消息"),
    delay: z.number().optional().describe("模拟延迟（毫秒）"),
  },
  async ({ message, delay }) => {
    if (delay) {
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            echo: message,
            receivedAt: new Date().toISOString(),
            delayed: delay ? `${delay}ms` : "none",
          }),
        },
      ],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
```

### 1.3 用原始JSON-RPC调试Initialize

启动server后，通过stdin直接发送JSON-RPC消息来调试握手：

```bash
# 启动server并手动交互
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"debug-client","version":"0.1.0"}}}' | npx tsx debug-server.ts
```

预期输出（server会返回自身能力）：

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "tools": {}
    },
    "serverInfo": {
      "name": "debug-server",
      "version": "0.1.0"
    }
  }
}
```

### 1.4 常见Initialize阶段问题

| 错误现象 | 根因 | 解决方案 |
|----------|------|----------|
| `Protocol version not supported` | client和server版本不匹配 | 检查`protocolVersion`字段，确保双方使用同一版本 |
| Server无响应 | stdio transport未正确连接 | 确认transport创建在`server.connect()`之前 |
| `Method not found` | SDK版本过旧 | 升级`@modelcontextprotocol/sdk`到最新 |
| 双初始化导致挂起 | 重复发送`initialize` | initialize只能发一次，之后发`initialized`通知 |

---

## 第二章：MCP Inspector——可视化调试利器

### 2.1 Inspector架构与工作原理

MCP Inspector是官方提供的可视化调试工具，但它**不是代理**——它自己就是一个MCP Client，直接连接你的Server。

```mermaid
graph LR
    A[MCP Inspector<br/>Browser UI] -->|SSE/HTTP| B[Inspector Server<br/>localhost:6274]
    B -->|STDIO/SSE| C[你的MCP Server]

    D[浏览器 DevTools] -->|WebSocket| B

    style A fill:#4A90D9,color:#fff
    style B fill:#F5A623,color:#fff
    style C fill:#7ED321,color:#fff
```

### 2.2 启动Inspector调试你的Server

```bash
# 方式1：直接启动（自动检测server配置）
npx @modelcontextprotocol/inspector npx tsx debug-server.ts

# 方式2：指定端口
npx @modelcontextprotocol/inspector --port 6274 npx tsx debug-server.ts

# 方式3：调试已运行的HTTP server
npx @modelcontextprotocol/inspector --url http://localhost:3000/mcp
```

启动后打开 `http://localhost:6274`，你会看到Inspector的三个核心面板：

1. **Connection面板**：查看initialize握手结果、双方capabilities
2. **Tools面板**：列出所有已注册的工具，可直接调用并查看原始JSON-RPC请求/响应
3. **Resources面板**（如有）：浏览可用的上下文资源

### 2.3 Inspector调试实战：逐步排查Tool调用失败

假设你的tool调用返回了意外结果，按以下步骤排查：

**Step 1：在Connection面板确认capabilities**

确认Server返回的capabilities中包含`tools: {}`。如果缺少，说明tool注册时机有问题——可能是在`server.connect()`之后才注册的。

**Step 2：在Tools面板检查tool schema**

点击`echo`工具，查看其inputSchema是否正确。常见问题：
- Zod schema未正确转换
- description字段缺失（Claude Code可能无法正确理解工具用途）

**Step 3：手动调用并观察原始消息**

在Inspector中点击"Run Tool"，输入参数：
```json
{
  "message": "test debug",
  "delay": 100
}
```

观察**raw request**和**raw response**，确认：
- 请求JSON是否完整正确
- 响应content类型是否为`text`或`image`
- 是否有隐藏的`isError: true`标记

### 2.4 Inspector的局限性及替代方案

Inspector有几个关键限制需要了解：

| 局限 | 影响 | 替代方案 |
|------|------|----------|
| 不能代理真实Client流量 | 无法观察Claude Code实际调用 | 使用STDIO日志捕获（见第三章） |
| 不支持streaming响应调试 | 无法测试长耗时工具的进度通知 | 自定义日志中间件 |
| 只支持单个Server | 无法调试多Server编排 | 分别调试后做集成测试 |

---

## 第三章：结构化日志——让MCP Server不再是黑盒

### 3.1 MCP协议内置的日志通道

MCP协议本身提供了一个日志通道：`notifications/message`。这是最轻量的日志方式，无需额外依赖。

```typescript
// structured-logging-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "logging-demo",
  version: "0.1.0",
});

// MCP协议内置日志方法
// server.server 对象提供 notification 方法
async function log(level: "debug" | "info" | "warning" | "error", message: string, data?: unknown) {
  const notification = {
    method: "notifications/message",
    params: {
      level,
      logger: "logging-demo",
      data: JSON.stringify({
        message,
        timestamp: new Date().toISOString(),
        ...(data !== undefined ? { data } : {}),
      }),
    },
  };

  // 通过stderr输出（不影响JSON-RPC通信）
  process.stderr.write(JSON.stringify(notification) + "\n");
}

server.tool(
  "search-docs",
  "搜索文档库",
  {
    query: z.string().describe("搜索关键词"),
    limit: z.number().optional().default(10),
  },
  async ({ query, limit }) => {
    log("info", "Tool call received", { tool: "search-docs", query, limit });

    try {
      // 模拟搜索
      log("debug", "Starting search execution");
      const results = await performSearch(query, limit);

      log("info", "Search completed", {
        resultCount: results.length,
        duration: results.duration,
      });

      return {
        content: [{ type: "text", text: JSON.stringify(results) }],
      };
    } catch (error) {
      log("error", "Search failed", {
        error: error instanceof Error ? error.message : String(error),
        query,
      });
      return {
        content: [{ type: "text", text: `搜索失败: ${error}` }],
        isError: true,
      };
    }
  }
);

async function performSearch(query: string, limit: number) {
  // 模拟搜索逻辑
  return {
    query,
    results: Array.from({ length: Math.min(limit, 3) }, (_, i) => ({
      id: i + 1,
      title: `文档 ${i + 1}: 关于${query}的内容`,
      relevance: 0.9 - i * 0.1,
    })),
    duration: "45ms",
    total: 100,
  };
}

const transport = new StdioServerTransport();
await server.connect(transport);
```

### 3.2 捕获Claude Code与MCP Server的完整对话

Claude Code通过STDIO与MCP Server通信。你可以用一个包装脚本来捕获所有消息：

```bash
#!/bin/bash
# mcp-debug-wrapper.sh
# 用法: 在Claude Code配置中将command改为此脚本

LOG_DIR="$HOME/.mcp-debug-logs"
mkdir -p "$LOG_DIR"

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="$LOG_DIR/mcp_trace_${TIMESTAMP}.jsonl"

echo "[MCP Debug Wrapper] Logging to: $LOG_FILE" >&2

# 使用tee捕获双向通信
# stdin -> server（Claude Code发给Server的消息）
# stdout -> client（Server返回给Claude Code的消息）
mkfifo "$LOG_DIR/stdin_pipe_$TIMESTAMP" 2>/dev/null

# 简单方式：用sed在每行前后加标记
# 这里用node来实现更精确的JSON-RPC消息捕获
node -e "
const fs = require('fs');
const logFile = '$LOG_FILE';
let buffer = '';

function logMessage(direction, data) {
  const entry = {
    timestamp: new Date().toISOString(),
    direction,
    message: JSON.parse(data),
  };
  fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
}

// 捕获stdin（client -> server）
process.stdin.on('data', (chunk) => {
  buffer += chunk.toString();
  const lines = buffer.split('\n');
  buffer = lines.pop() || '';
  for (const line of lines) {
    if (line.trim()) {
      try { logMessage('client->server', line); } catch {}
    }
  }
  process.stdout.write(chunk); // 转发给server
});

// 捕获stdout（server -> client）-- 需要包装server进程
" 2>/dev/null

# 实际使用更简单的方式：直接重定向stderr
exec npx tsx "$@" 2>>"$LOG_DIR/server_stderr_${TIMESTAMP}.log"
```

### 3.3 结构化日志分析工具

有了JSONL格式的日志后，可以用`jq`快速分析：

```bash
# 统计tool调用频率
cat ~/.mcp-debug-logs/mcp_trace_*.jsonl | \
  jq -r 'select(.direction=="client->server" and .message.method=="tools/call") | .message.params.name' | \
  sort | uniq -c | sort -rn

# 找出所有错误响应
cat ~/.mcp-debug-logs/mcp_trace_*.jsonl | \
  jq 'select(.direction=="server->client" and .message.result?.isError==true)'

# 计算tool平均响应时间
cat ~/.mcp-debug-logs/mcp_trace_*.jsonl | \
  jq -r 'select(.message.method=="tools/call") | .timestamp' | \
  # 配对计算时间差...
```

---

## 第四章：错误处理模式——从"能报错"到"能自愈"

### 4.1 MCP错误码体系

MCP使用JSON-RPC 2.0标准错误码，并扩展了自定义范围：

```typescript
// mcp-errors.ts
export const MCP_ERRORS = {
  // JSON-RPC标准错误
  PARSE_ERROR: { code: -32700, message: "Parse error" },
  INVALID_REQUEST: { code: -32600, message: "Invalid Request" },
  METHOD_NOT_FOUND: { code: -32601, message: "Method not found" },
  INVALID_PARAMS: { code: -32602, message: "Invalid params" },
  INTERNAL_ERROR: { code: -32603, message: "Internal error" },

  // MCP自定义错误范围 (-32000 到 -32099)
  SERVER_NOT_INITIALIZED: { code: -32002, message: "Server not initialized" },
  UNKNOWN_ERROR: { code: -32001, message: "Unknown error" },
};
```

### 4.2 三级错误处理策略

设计一个生产级MCP Server需要**三级错误处理**：

```mermaid
graph TD
    A[Tool调用] --> B{输入验证}
    B -->|无效| C[Level 1: 输入错误<br/>快速失败, 明确提示]
    B -->|有效| D{业务逻辑}
    D -->|失败| E[Level 2: 业务错误<br/>降级返回, 保留上下文]
    D -->|成功| F{外部依赖}
    F -->|超时/不可用| G[Level 3: 基础设施错误<br/>重试/熔断, 优雅降级]
    F -->|成功| H[正常返回]

    style C fill:#FF6B6B,color:#fff
    style E fill:#FFA726,color:#fff
    style G fill:#EF5350,color:#fff
    style H fill:#66BB6A,color:#fff
```

### 4.3 完整的错误处理工具包装器

```typescript
// error-handler-wrapper.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

// 错误分类枚举
enum ErrorSeverity {
  INPUT = "input",       // 用户输入问题
  BUSINESS = "business", // 业务逻辑问题
  INFRA = "infra",       // 基础设施问题
}

interface ToolError {
  severity: ErrorSeverity;
  message: string;
  suggestion?: string;
  retryable: boolean;
  originalError?: unknown;
}

// 将错误转换为MCP友好的返回格式
function toMcpError(error: ToolError) {
  const prefix = {
    [ErrorSeverity.INPUT]: "❌ 输入错误",
    [ErrorSeverity.BUSINESS]: "⚠️ 处理失败",
    [ErrorSeverity.INFRA]: "🔴 服务暂时不可用",
  }[error.severity];

  let text = `${prefix}: ${error.message}`;
  if (error.suggestion) text += `\n💡 建议: ${error.suggestion}`;
  if (error.retryable) text += "\n🔄 此错误可重试";

  return {
    content: [{ type: "text" as const, text }],
    isError: true,
  };
}

// 通用错误捕获包装器
function wrapToolHandler<TParams>(
  handler: (params: TParams) => Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }>
) {
  return async (params: TParams) => {
    try {
      return await handler(params);
    } catch (error) {
      if (error instanceof z.ZodError) {
        return toMcpError({
          severity: ErrorSeverity.INPUT,
          message: `参数验证失败: ${error.errors.map(e => e.message).join(", ")}`,
          suggestion: "请检查参数格式和取值范围",
          retryable: false,
          originalError: error,
        });
      }

      if (error instanceof TimeoutError) {
        return toMcpError({
          severity: ErrorSeverity.INFRA,
          message: "外部服务响应超时",
          suggestion: "请稍后重试，或检查网络连接",
          retryable: true,
          originalError: error,
        });
      }

      if (error instanceof Error && error.message.includes("ECONNREFUSED")) {
        return toMcpError({
          severity: ErrorSeverity.INFRA,
          message: "无法连接到后端服务",
          suggestion: "确认后端服务是否在运行",
          retryable: true,
          originalError: error,
        });
      }

      // 未知错误
      return toMcpError({
        severity: ErrorSeverity.BUSINESS,
        message: error instanceof Error ? error.message : String(error),
        retryable: false,
        originalError: error,
      });
    }
  };
}

// 使用示例
export function registerTools(server: McpServer) {
  server.tool(
    "query-database",
    "查询数据库",
    {
      sql: z.string().describe("SQL查询语句"),
      maxRows: z.number().optional().default(100),
    },
    wrapToolHandler(async ({ sql, maxRows }) => {
      // Level 1: 输入验证
      const forbiddenPatterns = [/DROP/i, /DELETE/i, /TRUNCATE/i, /ALTER/i];
      for (const pattern of forbiddenPatterns) {
        if (pattern.test(sql)) {
          return toMcpError({
            severity: ErrorSeverity.INPUT,
            message: `SQL包含禁止的操作: ${pattern.source}`,
            suggestion: "此工具仅支持SELECT查询",
            retryable: false,
          });
        }
      }

      // Level 2: 业务逻辑（带超时）
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 5000);

      try {
        const result = await fetch("http://localhost:5432/query", {
          method: "POST",
          body: JSON.stringify({ sql, maxRows }),
          signal: controller.signal,
        });

        clearTimeout(timeout);
        const data = await result.json();
        return {
          content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
        };
      } finally {
        clearTimeout(timeout);
      }
    })
  );
}
```

### 4.4 错误指标统计

在Server中埋入错误统计，便于后续分析：

```typescript
// error-metrics.ts
interface ErrorMetric {
  tool: string;
  severity: string;
  message: string;
  timestamp: string;
}

const metrics: ErrorMetric[] = [];

export function recordError(tool: string, severity: string, message: string) {
  metrics.push({
    tool,
    severity,
    message,
    timestamp: new Date().toISOString(),
  });
}

export function getErrorStats() {
  const byTool = metrics.reduce(
    (acc, m) => ({ ...acc, [m.tool]: (acc[m.tool] || 0) + 1 }),
    {} as Record<string, number>
  );
  const bySeverity = metrics.reduce(
    (acc, m) => ({ ...acc, [m.severity]: (acc[m.severity] || 0) + 1 }),
    {} as Record<string, number>
  );
  return {
    totalErrors: metrics.length,
    byTool,
    bySeverity,
    recentErrors: metrics.slice(-10),
  };
}
```

---

## 第五章：生产级可观测性——OpenTelemetry集成

### 5.1 三大支柱：Logs、Metrics、Traces

生产环境中的MCP Server需要完整的可观测性。MCP协议的每次tool调用都可以映射到OpenTelemetry的三个信号：

```mermaid
graph TB
    subgraph "MCP Tool Call: search-docs"
        A[Span: tools/call<br/>name=search-docs] --> B[Event: params_validated]
        B --> C[Span: db_query<br/>duration=45ms]
        C --> D[Event: results_returned<br/>count=3]
    end

    subgraph "OpenTelemetry Signals"
        E[Traces<br/>调用链路]
        F[Metrics<br/>tool_duration_seconds]
        G[Logs<br/>结构化JSON日志]
    end

    A -.-> E
    C -.-> F
    A -.-> G

    style A fill:#4A90D9,color:#fff
    style C fill:#F5A623,color:#fff
    style E fill:#7ED321,color:#fff
    style F fill:#BD10E0,color:#fff
    style G fill:#50E3C2,color:#fff
```

### 5.2 集成OpenTelemetry的MCP Server

```typescript
// otel-mcp-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// OpenTelemetry imports
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 { trace, metrics, SpanStatusCode } from "@opentelemetry/api";
import { Resource } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";

// 初始化OpenTelemetry
const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: "mcp-search-server",
    "service.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();

// 创建Meter和Tracer
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: "MCP工具调用总数",
});

const toolDurationHistogram = meter.createHistogram("mcp.tool.duration", {
  description: "MCP工具调用耗时",
  unit: "ms",
});

const activeCallsGauge = meter.createUpDownCounter("mcp.tool.active", {
  description: "当前活跃的MCP工具调用数",
});

const server = new McpServer({
  name: "otel-search-server",
  version: "1.0.0",
});

// 包装器：为每个tool调用自动添加OpenTelemetry instrumentation
function instrumentedTool(
  name: string,
  description: string,
  schema: Record<string, z.ZodTypeAny>,
  handler: (params: any, span: any) => Promise<any>
) {
  server.tool(name, description, schema, async (params) => {
    const startTime = Date.now();
    activeCallsGauge.add(1);

    // 创建Span
    const span = tracer.startSpan(`mcp.tool.${name}`, {
      attributes: {
        "mcp.tool.name": name,
        "mcp.tool.params": JSON.stringify(params),
      },
    });

    try {
      const result = await handler(params, span);

      // 记录成功指标
      toolCallCounter.add(1, { tool: name, status: "success" });
      span.setStatus({ code: SpanStatusCode.OK });

      return result;
    } catch (error) {
      // 记录失败指标
      toolCallCounter.add(1, { tool: name, status: "error" });
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: error instanceof Error ? error.message : String(error),
      });

      return {
        content: [{
          type: "text",
          text: `工具调用失败: ${error instanceof Error ? error.message : String(error)}`,
        }],
        isError: true,
      };
    } finally {
      const duration = Date.now() - startTime;
      toolDurationHistogram.record(duration, { tool: name });
      activeCallsGauge.add(-1);
      span.end();
    }
  });
}

// 注册instrumented tools
instrumentedTool(
  "search",
  "全文搜索文档",
  {
    query: z.string(),
    limit: z.number().optional().default(10),
  },
  async (params, span) => {
    span.addEvent("search_started", { query: params.query });

    // 模拟搜索
    const results = await new Promise((resolve) =>
      setTimeout(() => resolve([
        { id: 1, title: `关于${params.query}的文档`, score: 0.95 },
        { id: 2, title: `${params.query}高级指南`, score: 0.87 },
      ]), 100 + Math.random() * 200)
    );

    span.addEvent("search_completed", {
      result_count: results.length,
    });

    return {
      content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

// 优雅关闭
process.on("SIGTERM", async () => {
  await sdk.shutdown();
  process.exit(0);
});
```

### 5.3 用Grafana可视化MCP Server指标

配置好OpenTelemetry Collector后，在Grafana中创建以下Dashboard面板：

**面板1：Tool调用速率**
```promql
rate(mcp_tool_calls_total[5m])
```

**面板2：Tool P99延迟**
```promql
histogram_quantile(0.99, rate(mcp_tool_duration_bucket[5m]))
```

**面板3：错误率**
```promql
sum(rate(mcp_tool_calls_total{status="error"}[5m])) 
/ 
sum(rate(mcp_tool_calls_total[5m]))
```

**面板4：活跃调用数**
```promql
mcp_tool_active
```

---

## 第六章：端到端测试策略

### 6.1 MCP Server测试金字塔

```mermaid
graph BT
    A["单元测试<br/>纯函数/验证逻辑<br/>占比60%"] --> B["集成测试<br/>Tool注册+调用链路<br/>占比25%"]
    B --> C["端到端测试<br/>Inspector自动化<br/>占比10%"]
    C --> D["生产冒烟测试<br/>健康检查+告警<br/>占比5%"]

    style A fill:#66BB6A,color:#fff
    style B fill:#FFA726,color:#fff
    style C fill:#4A90D9,color:#fff
    style D fill:#EF5350,color:#fff
```

### 6.2 单元测试：测试Tool Handler

```typescript
// __tests__/search-tool.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";

// 直接测试handler逻辑，不依赖MCP框架
import { validateSearchParams, performSearch } from "../tools/search";

describe("search tool", () => {
  describe("validateSearchParams", () => {
    it("应接受合法参数", () => {
      const result = validateSearchParams({ query: "test", limit: 10 });
      expect(result.success).toBe(true);
    });

    it("应拒绝空query", () => {
      const result = validateSearchParams({ query: "", limit: 10 });
      expect(result.success).toBe(false);
      expect(result.error).toContain("query不能为空");
    });

    it("应限制limit上限", () => {
      const result = validateSearchParams({ query: "test", limit: 10001 });
      expect(result.success).toBe(false);
      expect(result.error).toContain("limit最大为10000");
    });
  });

  describe("performSearch", () => {
    beforeEach(() => {
      vi.clearAllMocks();
    });

    it("应在超时时抛出TimeoutError", async () => {
      vi.spyOn(global, "fetch").mockImplementation(
        () => new Promise((_, reject) =>
          setTimeout(() => reject(new DOMException("Aborted", "AbortError")), 6000)
        )
      );

      await expect(performSearch("test", 10)).rejects.toThrow();
    });
  });
});
```

### 6.3 集成测试：通过MCP SDK直接调用

```typescript
// __tests__/integration/mcp-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";

describe("MCP Server集成测试", () => {
  let client: Client;
  let transport: StdioClientTransport;

  beforeAll(async () => {
    transport = new StdioClientTransport({
      command: "npx",
      args: ["tsx", "src/server.ts"],
    });

    client = new Client({
      name: "test-client",
      version: "1.0.0",
    });

    await client.connect(transport);
  });

  afterAll(async () => {
    await client.close();
  });

  it("应完成initialize握手", () => {
    // client已连接，说明initialize成功
    expect(client).toBeDefined();
  });

  it("应返回工具列表", async () => {
    const tools = await client.listTools();
    expect(tools.tools).toBeInstanceOf(Array);
    expect(tools.tools.length).toBeGreaterThan(0);

    const searchTool = tools.tools.find((t) => t.name === "search");
    expect(searchTool).toBeDefined();
    expect(searchTool?.description).toBeTruthy();
  });

  it("应成功调用search工具", async () => {
    const result = await client.callTool({
      name: "search",
      arguments: { query: "测试", limit: 5 },
    });

    expect(result.content).toBeInstanceOf(Array);
    expect(result.content[0].type).toBe("text");

    const data = JSON.parse((result.content[0] as any).text);
    expect(data).toBeInstanceOf(Array);
  });

  it("应在参数错误时返回isError", async () => {
    const result = await client.callTool({
      name: "search",
      arguments: { query: "", limit: 5 },
    });

    expect(result.isError).toBe(true);
  });
});
```

### 6.4 Inspector自动化测试（Playwright）

对于需要可视化验证的场景，可以用Playwright自动化Inspector测试：

```typescript
// e2e/inspector.spec.ts
import { test, expect } from "@playwright/test";

test("Inspector应正确显示工具列表", async ({ page }) => {
  // 前提：MCP Server和Inspector已启动
  await page.goto("http://localhost:6274");

  // 等待连接建立
  await page.waitForSelector('[data-testid="connection-status"][data-status="connected"]', {
    timeout: 10000,
  });

  // 导航到Tools面板
  await page.click('[data-testid="tools-tab"]');

  // 验证工具列表
  const tools = await page.$$('[data-testid="tool-item"]');
  expect(tools.length).toBeGreaterThan(0);

  // 验证search工具存在
  const searchTool = await page.$('[data-testid="tool-item"][data-name="search"]');
  expect(searchTool).toBeTruthy();
});
```

---

## 第七章：实战项目——生产级文件搜索MCP Server

将前面所有章节整合为一个完整项目，包含完整的调试、日志、错误处理和可观测性。

### 7.1 项目结构

```
mcp-file-search/
├── src/
│   ├── index.ts              # 入口：启动server
│   ├── tools/
│   │   ├── search.ts         # 文件搜索工具
│   │   └── index.ts          # 工具注册
│   ├── middleware/
│   │   ├── error-handler.ts  # 错误处理中间件
│   │   ├── logger.ts         # 结构化日志
│   │   └── telemetry.ts      # OpenTelemetry
│   └── utils/
│       ├── validators.ts     # 输入验证
│       └── rate-limiter.ts   # 简易限流
├── __tests__/
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── scripts/
│   └── mcp-debug-wrapper.sh  # 调试包装器
├── docker-compose.yml        # OTel Collector + Grafana
├── package.json
├── tsconfig.json
└── vitest.config.ts
```

### 7.2 完整的Server入口

```typescript
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerTools } from "./tools/index.js";
import { initTelemetry } from "./middleware/telemetry.js";
import { structuredLogger as log } from "./middleware/logger.js";
import { RateLimiter } from "./utils/rate-limiter.js";

// 初始化OpenTelemetry（必须在所有import之后第一个执行）
const telemetry = initTelemetry({
  serviceName: "mcp-file-search",
  version: "1.0.0",
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
});

// 创建限流器
const rateLimiter = new RateLimiter({
  maxCalls: 60,
  windowMs: 60000, // 60次/分钟
});

// 创建Server
const server = new McpServer({
  name: "mcp-file-search",
  version: "1.0.0",
});

// 注册所有工具
registerTools(server, { rateLimiter, log, telemetry });

// 启动
const transport = new StdioServerTransport();

try {
  await server.connect(transport);
  log("info", "Server started", {
    pid: process.pid,
    nodeVersion: process.version,
  });
} catch (error) {
  log("error", "Server startup failed", { error });
  process.exit(1);
}

// 优雅关闭
const shutdown = async (signal: string) => {
  log("info", `Received ${signal}, shutting down gracefully`);
  await telemetry.shutdown();
  process.exit(0);
};

process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
```

### 7.3 带完整instrumentation的搜索工具

```typescript
// src/tools/search.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { trace, SpanStatusCode } from "@opentelemetry/api";
import fs from "fs/promises";
import path from "path";

export function registerSearchTool(
  server: McpServer,
  deps: { rateLimiter: any; log: any; telemetry: any }
) {
  const { rateLimiter, log } = deps;
  const tracer = trace.getTracer("mcp-file-search");

  server.tool(
    "file-search",
    "在指定目录中搜索文件内容，支持文本匹配和正则表达式",
    {
      directory: z.string().describe("搜索的根目录路径"),
      pattern: z.string().describe("搜索关键词或正则表达式"),
      maxResults: z.number().optional().default(20).describe("最大结果数"),
      fileTypes: z.array(z.string()).optional().describe("文件类型过滤，如['.ts', '.md']"),
      useRegex: z.boolean().optional().default(false).describe("是否使用正则表达式"),
    },
    async (params) => {
      const span = tracer.startSpan("file-search");

      try {
        // 限流检查
        if (!rateLimiter.check()) {
          span.setStatus({ code: SpanStatusCode.ERROR, message: "Rate limited" });
          return {
            content: [{ type: "text", text: "请求过于频繁，请稍后重试" }],
            isError: true,
          };
        }

        span.setAttributes({
          "search.directory": params.directory,
          "search.pattern": params.pattern,
          "search.useRegex": params.useRegex,
        });

        log("info", "File search started", params);

        // 安全检查：防止路径遍历
        const resolvedPath = path.resolve(params.directory);
        if (!resolvedPath.startsWith(process.cwd()) && 
            !resolvedPath.startsWith("/tmp")) {
          span.setStatus({ code: SpanStatusCode.ERROR, message: "Path traversal blocked" });
          return {
            content: [{ type: "text", text: "安全限制：只能搜索当前目录或/tmp下的文件" }],
            isError: true,
          };
        }

        // 执行搜索
        const results = await searchFiles(resolvedPath, params);

        span.setAttribute("search.results_count", results.length);
        span.setStatus({ code: SpanStatusCode.OK });

        log("info", "File search completed", {
          directory: params.directory,
          resultCount: results.length,
        });

        return {
          content: [{
            type: "text",
            text: JSON.stringify({
              query: params.pattern,
              directory: resolvedPath,
              resultCount: results.length,
              results: results.slice(0, params.maxResults),
            }, null, 2),
          }],
        };
      } catch (error) {
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: error instanceof Error ? error.message : String(error),
        });
        throw error;
      } finally {
        span.end();
      }
    }
  );
}

async function searchFiles(
  rootDir: string,
  params: { pattern: string; fileTypes?: string[]; useRegex?: boolean; maxResults?: number }
): Promise<Array<{ file: string; line: number; content: string; match: string }>> {
  const results: Array<{ file: string; line: number; content: string; match: string }> = [];
  const regex = params.useRegex
    ? new RegExp(params.pattern, "g")
    : new RegExp(escapeRegex(params.pattern), "gi");

  async function walk(dir: string) {
    if (results.length >= (params.maxResults || 20)) return;

    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
      if (results.length >= (params.maxResults || 20)) break;
      const fullPath = path.join(dir, entry.name);

      if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
        await walk(fullPath);
      } else if (entry.isFile()) {
        const ext = path.extname(entry.name);
        if (params.fileTypes && !params.fileTypes.includes(ext)) continue;

        try {
          const content = await fs.readFile(fullPath, "utf-8");
          const lines = content.split("\n");
          for (let i = 0; i < lines.length; i++) {
            regex.lastIndex = 0;
            if (regex.test(lines[i])) {
              results.push({
                file: path.relative(rootDir, fullPath),
                line: i + 1,
                content: lines[i].trim().slice(0, 200),
                match: params.pattern,
              });
              if (results.length >= (params.maxResults || 20)) break;
            }
          }
        } catch {
          // 跳过无法读取的文件（二进制等）
        }
      }
    }
  }

  await walk(rootDir);
  return results;
}

function escapeRegex(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
```

### 7.4 Claude Code配置

在项目的`.claude/settings.json`或全局配置中注册这个MCP Server：

```json
{
  "mcpServers": {
    "file-search": {
      "command": "npx",
      "args": ["tsx", "/path/to/mcp-file-search/src/index.ts"],
      "env": {
        "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317"
      }
    }
  }
}
```

### 7.5 Docker Compose（可选的监控栈）

```yaml
# docker-compose.yml
version: "3.8"
services:
  otel-collector:
    image: otel/opentelemetry-collector:latest
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317"   # gRPC
      - "4318:4318"   # HTTP
      - "8889:8889"   # Prometheus metrics

  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
```

---

## 第八章：生产部署检查清单

### 8.1 部署前自检

```mermaid
graph TD
    A[部署检查清单] --> B[安全性]
    A --> C[可靠性]
    A --> D[可观测性]
    A --> E[性能]

    B --> B1[✓ 路径遍历防护]
    B --> B2[✓ SQL注入防护]
    B --> B3[✓ Rate Limiting]
    B --> B4[✓ 敏感信息不在日志中]

    C --> C1[✓ 超时处理]
    C --> C2[✓ 优雅关闭]
    C --> C3[✓ 重试机制]
    C --> C4[✓ 错误分级]

    D --> D1[✓ 结构化日志]
    D --> D2[✓ OpenTelemetry集成]
    D --> D3[✓ 健康检查端点]
    D --> D4[✓ 告警规则]

    E --> E1[✓ 结果数量限制]
    E --> E2[✓ 内存使用监控]
    E --> E3[✓ 冷启动优化]
    E --> E4[✓ 依赖超时配置]

    style A fill:#4A90D9,color:#fff
    style B fill:#FF6B6B,color:#fff
    style C fill:#FFA726,color:#fff
    style D fill:#7ED321,color:#fff
    style E fill:#BD10E0,color:#fff
```

### 8.2 Claude Code中的MCP Server健康检查

在Claude Code中配置MCP Server时，可以添加健康检查脚本：

```json
{
  "mcpServers": {
    "file-search": {
      "command": "npx",
      "args": ["tsx", "src/index.ts"],
      "env": {
        "MCP_HEALTH_CHECK_INTERVAL": "30000"
      }
    }
  }
}
```

在Server中添加一个隐藏的健康检查工具：

```typescript
server.tool(
  "_health",
  "内部健康检查工具，不要直接使用",
  {},
  async () => ({
    content: [{
      type: "text",
      text: JSON.stringify({
        status: "healthy",
        uptime: process.uptime(),
        memory: {
          rss: `${Math.round(process.memoryUsage().rss / 1024 / 1024)}MB`,
          heapUsed: `${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`,
        },
        activeHandles: process._getActiveHandles().length,
        activeRequests: process._getActiveRequests().length,
      }),
    }],
  })
);
```

### 8.3 常见生产问题速查表

| 症状 | 可能原因 | 排查命令 |
|------|----------|----------|
| Claude Code显示"MCP server disconnected" | Server进程崩溃或OOM | `dmesg \| grep -i killed` |
| Tool调用超时 | 外部依赖阻塞 | 检查trace中的span耗时 |
| 内存持续增长 | 结果缓存未释放 | `process.memoryUsage()`对比 |
| 响应变慢 | 事件循环阻塞 | `node --inspect` + Chrome DevTools |
| 日志中出现`EPIPE` | Client已断开但Server还在写 | 添加`stderr.write`错误处理 |

---

## 延伸阅读

- [MCP Specification - Lifecycle](https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle) — 协议生命周期完整规范
- [MCP Inspector GitHub](https://github.com/modelcontextprotocol/inspector) — Inspector源码与使用说明
- [Microsoft MCP for Beginners](https://github.com/microsoft/mcp-for-beginners) — 微软官方MCP入门课程
- [OpenTelemetry Node.js SDK](https://opentelemetry.io/docs/languages/js/) — OTel Node.js集成文档
- [MCP Best Practices](https://mcp-best-practice.github.io/mcp-best-practice/best-practice/) — 社区最佳实践集合

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| **适合训练营** | 进阶营（Day 3-4），学员已有MCP基础开发经验 |
| **预计教学时长** | 4-5小时（含动手实验） |
| **实验环境要求** | Node.js 20+, Docker (可选), Chrome浏览器 |
| **最佳搭配课程** | Day 1-2: MCP Server基础开发 → Day 3: 本课程 → Day 4: MCP远程部署 |
| **难度评级** | ★★★☆☆（中等，概念不难但实践细节多） |

## 参考来源
1. [MCP Specification 2025-03-26 - Lifecycle](https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle) — MCP协议生命周期五阶段完整规范，initialize到shutdown的JSON-RPC交互细节
2. [MCP Inspector - Visual Testing Tool](https://github.com/modelcontextprotocol/inspector) — 官方可视化调试工具源码，支持STDIO和HTTP传输的交互式测试
3. [Microsoft MCP for Beginners - Best Practices](https://github.com/microsoft/mcp-for-beginners/blob/main/08-BestPractices/README.md) — 微软开源MCP课程中的生产实践章节，涵盖错误处理和测试策略
4. [MCP Error Handling Best Practices](https://www.kaigritun.com/mcp/mcp-error-handling) — 生产环境MCP错误处理模式，包含分级策略和重试机制
5. [How to Instrument MCP Servers with OpenTelemetry](https://oneuptime.com/blog/post/2026-03-26-how-to-instrument-mcp-servers-with-opentelemetry/view) — MCP Server集成OpenTelemetry的实战指南，覆盖Traces/Metrics/Logs三支柱

> [!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)
