本教程面向已掌握MCP Server基础开发的工程师,聚焦从调试验证到生产监控的完整链路。你将学会MCP协议生命周期五阶段调试、Inspector可视化测试、结构化日志与OpenTelemetry集成、错误分级处理模式,以及生产级健康检查与告警配置。学完后,你的MCP Server将从"能跑"升级为"可信"。
适用人群:已有TypeScript/Python MCP开发经验,需要将MCP服务推向生产环境的开发者。
MCP协议不是简单的请求-响应模型。它定义了严格的五阶段生命周期:Initialize → Capability Negotiation → Operation → Shutdown → Error Recovery。90%的MCP调试问题都出在前两个阶段。
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)
在开发中,最常见的问题是初始化握手失败。让我们手动模拟这个过程来定位问题。
创建一个最小化的调试用MCP Server:
// 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);
启动server后,通过stdin直接发送JSON-RPC消息来调试握手:
# 启动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会返回自身能力):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "debug-server",
"version": "0.1.0"
}
}
}
| 错误现象 | 根因 | 解决方案 |
|---|---|---|
Protocol version not supported |
client和server版本不匹配 | 检查protocolVersion字段,确保双方使用同一版本 |
| Server无响应 | stdio transport未正确连接 | 确认transport创建在server.connect()之前 |
Method not found |
SDK版本过旧 | 升级@modelcontextprotocol/sdk到最新 |
| 双初始化导致挂起 | 重复发送initialize |
initialize只能发一次,之后发initialized通知 |
MCP Inspector是官方提供的可视化调试工具,但它不是代理——它自己就是一个MCP Client,直接连接你的Server。
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
# 方式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的三个核心面板:
假设你的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",输入参数:
{
"message": "test debug",
"delay": 100
}
观察raw request和raw response,确认:
- 请求JSON是否完整正确
- 响应content类型是否为text或image
- 是否有隐藏的isError: true标记
Inspector有几个关键限制需要了解:
| 局限 | 影响 | 替代方案 |
|---|---|---|
| 不能代理真实Client流量 | 无法观察Claude Code实际调用 | 使用STDIO日志捕获(见第三章) |
| 不支持streaming响应调试 | 无法测试长耗时工具的进度通知 | 自定义日志中间件 |
| 只支持单个Server | 无法调试多Server编排 | 分别调试后做集成测试 |
MCP协议本身提供了一个日志通道:notifications/message。这是最轻量的日志方式,无需额外依赖。
// 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);
Claude Code通过STDIO与MCP Server通信。你可以用一个包装脚本来捕获所有消息:
#!/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"
有了JSONL格式的日志后,可以用jq快速分析:
# 统计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' | \
# 配对计算时间差...
MCP使用JSON-RPC 2.0标准错误码,并扩展了自定义范围:
// 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" },
};
设计一个生产级MCP Server需要三级错误处理:
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
// 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);
}
})
);
}
在Server中埋入错误统计,便于后续分析:
// 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),
};
}
生产环境中的MCP Server需要完整的可观测性。MCP协议的每次tool调用都可以映射到OpenTelemetry的三个信号:
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
// 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);
});
配置好OpenTelemetry Collector后,在Grafana中创建以下Dashboard面板:
面板1:Tool调用速率
rate(mcp_tool_calls_total[5m])
面板2:Tool P99延迟
histogram_quantile(0.99, rate(mcp_tool_duration_bucket[5m]))
面板3:错误率
sum(rate(mcp_tool_calls_total{status="error"}[5m]))
/
sum(rate(mcp_tool_calls_total[5m]))
面板4:活跃调用数
mcp_tool_active
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
// __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();
});
});
});
// __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);
});
});
对于需要可视化验证的场景,可以用Playwright自动化Inspector测试:
// 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-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
// 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"));
// 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, "\\$&");
}
在项目的.claude/settings.json或全局配置中注册这个MCP Server:
{
"mcpServers": {
"file-search": {
"command": "npx",
"args": ["tsx", "/path/to/mcp-file-search/src/index.ts"],
"env": {
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317"
}
}
}
}
# 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
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
在Claude Code中配置MCP Server时,可以添加健康检查脚本:
{
"mcpServers": {
"file-search": {
"command": "npx",
"args": ["tsx", "src/index.ts"],
"env": {
"MCP_HEALTH_CHECK_INTERVAL": "30000"
}
}
}
}
在Server中添加一个隐藏的健康检查工具:
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,
}),
}],
})
);
| 症状 | 可能原因 | 排查命令 |
|---|---|---|
| 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错误处理 |
| 维度 | 评估 |
|---|---|
| 适合训练营 | 进阶营(Day 3-4),学员已有MCP基础开发经验 |
| 预计教学时长 | 4-5小时(含动手实验) |
| 实验环境要求 | Node.js 20+, Docker (可选), Chrome浏览器 |
| 最佳搭配课程 | Day 1-2: MCP Server基础开发 → Day 3: 本课程 → Day 4: MCP远程部署 |
| 难度评级 | ★★★☆☆(中等,概念不难但实践细节多) |
[!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 等场景全覆盖。
