MCP协议 高级 MCP 客户端开发 最佳实践 架构设计

MCP客户端最佳实践:构建可靠的AI工具集成层

AIEng Hub
阅读约 18 分钟

引言

MCP客户端是AI模型与外部工具之间的关键中介层。一个设计良好的客户端不仅需要正确的API调用,更需要考虑安全、性能、可维护性等生产级要求。

客户端架构

推荐的分层架构

┌─────────────────────────────────────────────────────────┐
│               AI Model (LLM)                             │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌─────────────────────────────────────────────────┐  │
│  │           Integration Layer                      │  │
│  │    LangChain / LlamaIndex / 自定义Agent框架      │  │
│  └────────────────────┬────────────────────────────┘  │
│                       │                                │
│  ┌────────────────────▼────────────────────────────┐  │
│  │           MCP Client Core                        │  │
│  ├─────────────────────────────────────────────────┤  │
│  │  Connection Mgr │ Tool Router │ Cache Layer     │  │
│  ├─────────────────────────────────────────────────┤  │
│  │  Error Handler  │ Retry Logic │ Degradation     │  │
│  └────────────────────┬────────────────────────────┘  │
│                       │                                │
│  ┌────────────────────▼────────────────────────────┐  │
│  │           MCP Server Transport                   │  │
│  │    (stdio / SSE / WebSocket)                     │  │
│  └─────────────────────────────────────────────────┘  │
│                                                         │
└─────────────────────────────────────────────────────────┘

核心模块职责

模块职责关键实现
Connection Manager连接生命周期、心跳、重连状态机 + 指数退避
Tool Router工具发现、缓存、路由多Server工具注册表
Cache Layer工具列表、资源缓存TTL + LRU
Error Handler错误分类、重试、降级策略模式
Security Layer输入验证、权限检查白名单 + Schema校验

安全最佳实践

输入验证

// 所有用户输入必须经过验证
import { z } from "zod";

// 工具参数白名单
const allowedTools = new Set([
  "search_docs",
  "read_file",
  "calculate",
]);

// 参数大小限制
const maxArgSize = 1024 * 10;  // 10KB

function validateToolCall(toolName: string, args: any): void {
  // 1. 工具名白名单
  if (!allowedTools.has(toolName)) {
    throw new Error(`禁止调用的工具: ${toolName}`);
  }

  // 2. 参数大小限制
  const argSize = JSON.stringify(args).length;
  if (argSize > maxArgSize) {
    throw new Error(`参数过大: ${argSize} bytes (限制: ${maxArgSize})`);
  }

  // 3. 参数类型校验
  if (typeof args !== "object" || args === null) {
    throw new Error("参数必须是对象");
  }
}

输出净化

// 敏感信息过滤
const sensitivePatterns = [
  /api[_\-]?key[=\s:]+["']?[^"'&\s]+/gi,
  /token[=\s:]+["']?[^"'&\s]+/gi,
  /password[=\s:]+["']?[^"'&\s]+/gi,
  /secret[=\s:]+["']?[^"'&\s]+/gi,
  /Bearer\s+[a-zA-Z0-9\-_.]+/g,
  /\b[A-Za-z0-9]{32,}\b/g,  // 疑似API Key的32+字符字符串
];

function sanitizeOutput(text: string): string {
  let safe = text;
  for (const pattern of sensitivePatterns) {
    safe = safe.replace(pattern, "[REDACTED]");
  }
  return safe;
}

// 在工具结果返回前过滤
function processToolResult(result: ToolResult): ToolResult {
  return {
    ...result,
    content: result.content.map(content => ({
      ...content,
      text: content.type === "text"
        ? sanitizeOutput(content.text)
        : content.text,
    })),
  };
}

性能优化

工具列表缓存

class ToolListCache {
  private cache: {
    tools: Tool[];
    timestamp: number;
  } | null = null;
  private ttl: number;

  constructor(ttlMs: number = 30000) {
    this.ttl = ttlMs;
  }

  async getTools(fetcher: () => Promise<Tool[]>): Promise<Tool[]> {
    if (this.cache && Date.now() - this.cache.timestamp < this.ttl) {
      return this.cache.tools;
    }

    const tools = await fetcher();
    this.cache = { tools, timestamp: Date.now() };
    return tools;
  }

  invalidate(): void {
    this.cache = null;
  }
}

批量调用优化

// 批量获取资源的工具
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "batch_read_resources") {
    const uris: string[] = args.uris;
    const results = await Promise.all(
      uris.map(uri => readResource(uri))
    );
    return {
      content: [{ type: "text", text: JSON.stringify(results) }],
      isError: false,
    };
  }
});

测试策略

模拟Server测试

// 测试用的Mock Server
class MockMCPServer {
  private tools: Tool[];
  private handlers: Map<string, Function>;

  constructor() {
    this.tools = [
      {
        name: "greet",
        description: "打招呼",
        inputSchema: {
          type: "object",
          properties: {
            name: { type: "string" },
          },
          required: ["name"],
        },
      },
    ];
    this.handlers = new Map();
    this.handlers.set("greet", (args: any) => ({
      content: [{ type: "text", text: `你好,${args.name}!` }],
      isError: false,
    }));
  }

  async handleRequest(request: any): Promise<any> {
    if (request.method === "tools/list") {
      return { tools: this.tools };
    }

    if (request.method === "tools/call") {
      const handler = this.handlers.get(request.params.name);
      if (handler) {
        return handler(request.params.arguments);
      }
      return {
        error: { code: -32601, message: "工具未找到" },
      };
    }

    if (request.method === "initialize") {
      return {
        protocolVersion: "2024-11-05",
        capabilities: { tools: {} },
        serverInfo: { name: "mock", version: "1.0.0" },
      };
    }

    return {};
  }
}

// 测试
import { describe, it, expect } from "vitest";

describe("MCP Client", () => {
  it("should list tools from server", async () => {
    const mockServer = new MockMCPServer();
    const response = await mockServer.handleRequest({
      method: "tools/list",
    });
    expect(response.tools).toHaveLength(1);
    expect(response.tools[0].name).toBe("greet");
  });

  it("should call tool and get result", async () => {
    const mockServer = new MockMCPServer();
    const response = await mockServer.handleRequest({
      method: "tools/call",
      params: {
        name: "greet",
        arguments: { name: "小明" },
      },
    });
    expect(response.content[0].text).toContain("小明");
  });

  it("should handle unknown tools", async () => {
    const mockServer = new MockMCPServer();
    const response = await mockServer.handleRequest({
      method: "tools/call",
      params: { name: "unknown_tool", arguments: {} },
    });
    expect(response.error).toBeDefined();
  });
});

端到端测试

import { describe, it, expect, beforeAll, afterAll } from "vitest";

describe("MCP Client Integration", () => {
  let client: Client;

  beforeAll(async () => {
    const transport = new StdioClientTransport({
      command: "node",
      args: ["test/mock-server.js"],
    });
    client = new Client(
      { name: "test", version: "1.0.0" },
      { capabilities: {} }
    );
    await client.connect(transport);
  });

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

  it("should succeed on normal tool call", async () => {
    const result = await client.callTool({
      name: "greet",
      arguments: { name: "测试" },
    });
    expect(result.content[0].text).toBe("你好,测试!");
    expect(result.isError).toBe(false);
  });

  it("should handle server errors gracefully", async () => {
    const result = await client.callTool({
      name: "error_tool",
      arguments: {},
    });
    expect(result.isError).toBe(true);
    expect(result.content[0].text).toBeDefined();
  });

  it("should respect timeout", async () => {
    const startTime = Date.now();
    try {
      await client.callTool({
        name: "slow_tool",
        arguments: {},
      }, { timeout: 1000 });
      expect.fail("应该超时");
    } catch (error) {
      const elapsed = Date.now() - startTime;
      expect(elapsed).toBeLessThan(5000);
    }
  });
});

常见陷阱

1. 忽略初始化

// ❌ 错误:跳过initialize直接调用
const result = await client.callTool({...});

// ✅ 正确:必须先initialize
await client.connect(transport);
// 或
await session.initialize();

2. 不处理错误

// ❌ 错误:未捕获异常
const result = await client.callTool({ name: "search", arguments: {} });
console.log(result);

// ✅ 正确:完整错误处理
try {
  const result = await client.callTool({ name: "search", arguments: {} });
  if (result.isError) {
    console.error("工具返回错误:", result.content[0].text);
    return null;
  }
  return result;
} catch (error) {
  if (error instanceof TransportError) {
    await handleReconnect();
  } else {
    console.error("工具调用异常:", error);
  }
}

3. 无限重试

// ❌ 错误:无限重试
while (true) {
  try {
    result = await callTool();
    break;
  } catch {
    // 无限循环!
  }
}

// ✅ 正确:有限次重试
for (let i = 0; i < 3; i++) {
  try {
    result = await callTool();
    break;
  } catch (error) {
    if (i === 2 || !isRetryable(error)) throw error;
    await sleep(1000 * Math.pow(2, i));
  }
}

4. 阻塞主线程

// ❌ 错误:同步阻塞
function callToolSync() {
  const result = client.callTool({...});  // 阻塞!
  return result;
}

// ✅ 正确:异步处理
async function callToolAsync() {
  const result = await client.callTool({...});
  return result;
}

配置管理

推荐配置模板

{
  "mcp_clients": {
    "default": {
      "connection": {
        "reconnect": {
          "enabled": true,
          "maxRetries": 5,
          "baseDelayMs": 1000,
          "maxDelayMs": 30000
        },
        "heartbeat": {
          "enabled": true,
          "intervalMs": 30000,
          "maxFailures": 3
        }
      },
      "performance": {
        "toolCacheTTLMs": 30000,
        "timeoutMs": 30000,
        "maxConcurrentCalls": 10
      },
      "security": {
        "maxArgSize": 10240,
        "sanitizeOutput": true,
        "allowedTools": ["search", "read", "calculate"]
      },
      "logging": {
        "level": "info",
        "traceRequests": true
      }
    }
  }
}

监控指标体系

指标说明告警阈值
mcp.tool.calls.total工具调用总数-
mcp.tool.calls.errors调用错误数> 5%
mcp.tool.calls.latency调用延迟P99> 10s
mcp.connection.state连接状态disconnected
mcp.heartbeat.failures心跳失败次数> 3
mcp.cache.hit_rate工具列表缓存命中率< 50%

总结

构建生产级MCP客户端的关键原则:

原则说明优先级
安全第一输入验证、输出净化P0
健壮性重试、降级、容错P0
可观测性日志、指标、追踪P1
性能缓存、并发控制P1
可测试性Mock Server、集成测试P1

下一步学习建议:


本文最后更新于 2024-07-18。