MCP 客户端集成与调用模式

本文详解如何在自定义 AI 应用中集成 MCP 客户端。涵盖客户端初始化和服务器发现、工具调用链、多服务器协调、上下文管理和结果后处理等核心模式。

1. MCP 客户端的角色

在 MCP 架构中,客户端是连接 AI 宿主(Host)和 MCP 服务器的桥梁。它负责:

  • 服务器发现与连接:启动外部进程或建立网络连接
  • 能力同步:获取服务器声明的工具、资源和提示词
  • 工具调用的编排:将 AI 的工具调用意图转换为 MCP 请求
  • 结果处理与格式化:将服务器返回的结果展示给 AI 或用户
  • 生命周期管理:监控连接健康、处理重连和优雅关闭

与服务器端相比,客户端开发面临更多复杂场景:需要同时管理多个服务器的连接,处理跨服务器的工具依赖,以及在大上下文窗口中高效组织工具结果。

2. 客户端初始化

2.1 stdio 客户端

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

async function createStdioClient() {
  const client = new Client(
    {
      name: "my-ai-app",
      version: "1.0.0",
    },
    {
      capabilities: {
        tools: {},
        resources: {},
        prompts: {},
      },
    }
  );

  const transport = new StdioClientTransport({
    command: "npx",
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/docs"],
    env: process.env as Record<string, string>,
  });

  await client.connect(transport);

  // 初始化握手完成后获取服务器能力
  const tools = await client.listTools();
  const resources = await client.listResources();

  return { client, tools, resources };
}

2.2 HTTP 客户端

import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";

async function createHttpClient() {
  const client = new Client({ name: "web-app", version: "1.0" });

  const transport = new SSEClientTransport(
    new URL("http://localhost:3000/sse")
  );

  await client.connect(transport);
  return client;
}

3. 工具调用链

3.1 基本调用流程

async function callToolWithRetry(
  client: Client,
  toolName: string,
  args: Record<string, unknown>,
  maxRetries: number = 3
): Promise<string> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const result = await client.callTool({
        name: toolName,
        arguments: args,
      });

      // 检查工具返回的错误
      if (result.isError) {
        const errorText = result.content
          .filter((c) => c.type === "text")
          .map((c) => c.text)
          .join("\n");
        throw new Error(`工具执行失败: ${errorText}`);
      }

      return result.content
        .filter((c) => c.type === "text")
        .map((c) => c.text)
        .join("\n");
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));
      if (attempt < maxRetries - 1) {
        const delay = Math.pow(2, attempt) * 1000; // 指数退避
        await new Promise((r) => setTimeout(r, delay));
      }
    }
  }

  throw lastError ?? new Error("工具调用失败");
}

3.2 多步骤工具链

许多 AI 任务需要顺序调用多个工具,后一个工具的输入依赖前一个工具的输出:

interface ToolChainStep {
  tool: string;
  args: (prevResults: Record<string, unknown>) => Record<string, unknown>;
}

async function executeToolChain(
  client: Client,
  steps: ToolChainStep[]
): Promise<Record<string, unknown>> {
  const results: Record<string, unknown> = {};

  for (let i = 0; i < steps.length; i++) {
    const step = steps[i];
    const args = step.args(results);

    console.log(`步骤 ${i + 1}/${steps.length}: 调用 ${step.tool}`);

    const result = await callToolWithRetry(client, step.tool, args);
    results[step.tool] = result;
  }

  return results;
}

// 使用示例:读取文件 -> 分析内容 -> 生成报告
const analysisChain: ToolChainStep[] = [
  {
    tool: "read_file",
    args: () => ({ path: "/path/to/data.json" }),
  },
  {
    tool: "analyze_data",
    args: (prev) => ({
      data: prev["read_file"],
      metrics: ["mean", "median", "stddev"],
    }),
  },
  {
    tool: "generate_report",
    args: (prev) => ({
      analysis: prev["analyze_data"],
      format: "markdown",
    }),
  },
];

4. 多服务器协调

4.1 服务器注册表

在复杂的 AI 应用中,可能需要同时连接数十个 MCP 服务器。设计一个中央注册表来管理它们:

interface ServerConfig {
  name: string;
  transport: "stdio" | "sse";
  command?: string;
  args?: string[];
  url?: string;
  env?: Record<string, string>;
}

class MCPClientRegistry {
  private clients: Map<string, Client> = new Map();
  private toolsIndex: Map<string, { server: string; tool: Tool }> = new Map();

  async addServer(config: ServerConfig): Promise<void> {
    const client = new Client(
      { name: "registry-client", version: "1.0" },
      { capabilities: { tools: {}, resources: {} } }
    );

    let transport;
    if (config.transport === "stdio") {
      transport = new StdioClientTransport({
        command: config.command!,
        args: config.args ?? [],
        env: config.env ?? {},
      });
    } else {
      transport = new SSEClientTransport(new URL(config.url!));
    }

    await client.connect(transport);

    // 建立工具索引
    const tools = await client.listTools();
    for (const tool of tools.tools) {
      this.toolsIndex.set(tool.name, { server: config.name, tool });
    }

    this.clients.set(config.name, client);
  }

  async callTool(toolName: string, args: Record<string, unknown>): Promise<string> {
    const entry = this.toolsIndex.get(toolName);
    if (!entry) {
      throw new Error(`未找到工具: ${toolName}`);
    }

    const client = this.clients.get(entry.server);
    if (!client) {
      throw new Error(`服务器未连接: ${entry.server}`);
    }

    const result = await client.callTool({
      name: toolName,
      arguments: args,
    });

    if (result.isError) {
      throw new Error(`工具调用失败: ${toolName}`);
    }

    return result.content
      .filter((c) => c.type === "text")
      .map((c) => c.text)
      .join("\n");
  }

  getAvailableTools(): string[] {
    return Array.from(this.toolsIndex.keys());
  }

  async closeAll(): Promise<void> {
    for (const [name, client] of this.clients) {
      await client.close();
      console.log(`已关闭服务器连接: ${name}`);
    }
    this.clients.clear();
    this.toolsIndex.clear();
  }
}

4.2 工具路由

当多个服务器提供同名工具时,需要路由策略:

class ToolRouter {
  private routes: Map<string, string> = new Map(); // toolName -> serverName
  private fallbackRules: Array<(toolName: string) => string | null> = [];

  register(toolName: string, serverName: string): void {
    this.routes.set(toolName, serverName);
  }

  addFallbackRule(rule: (toolName: string) => string | null): void {
    this.fallbackRules.push(rule);
  }

  resolve(toolName: string): string | null {
    if (this.routes.has(toolName)) {
      return this.routes.get(toolName)!;
    }

    for (const rule of this.fallbackRules) {
      const result = rule(toolName);
      if (result) return result;
    }

    return null;
  }
}

5. 上下文管理

5.1 工具结果截断

大量工具调用结果可能超出模型的上下文窗口限制。需要智能截断策略:

class ResultCompressor {
  private maxTokens: number;

  constructor(maxTokens: number = 8000) {
    this.maxTokens = maxTokens;
  }

  estimateTokens(text: string): number {
    // 粗略估计:英文约 1 token / 4 chars,中文约 1 token / 1.5 chars
    let tokens = 0;
    for (const char of text) {
      tokens += char.charCodeAt(0) > 127 ? 0.7 : 0.3;
    }
    return Math.ceil(tokens);
  }

  compress(result: string, priority: "head" | "tail" | "both" = "head"): string {
    if (this.estimateTokens(result) <= this.maxTokens) {
      return result;
    }

    const charsBudget = Math.floor(this.maxTokens * 3.5);

    if (priority === "head") {
      return result.slice(0, charsBudget) + "\n... [内容已截断]";
    } else if (priority === "tail") {
      return "[内容前部已截断] ...\n" + result.slice(-charsBudget);
    } else {
      const half = Math.floor(charsBudget / 2);
      return (
        result.slice(0, half) +
        "\n... [中间内容已截断] ...\n" +
        result.slice(-half)
      );
    }
  }
}

5.2 结果缓存

对于幂等性工具调用,添加客户端缓存减少重复请求:

class ToolResultCache {
  private cache: Map<string, { result: string; expiry: number }> = new Map();
  private defaultTTL: number;

  constructor(defaultTTLMs: number = 60000) {
    this.defaultTTL = defaultTTLMs;
  }

  private makeKey(toolName: string, args: Record<string, unknown>): string {
    return `${toolName}:${JSON.stringify(args)}`;
  }

  get(toolName: string, args: Record<string, unknown>): string | null {
    const key = this.makeKey(toolName, args);
    const entry = this.cache.get(key);

    if (!entry) return null;
    if (Date.now() > entry.expiry) {
      this.cache.delete(key);
      return null;
    }

    return entry.result;
  }

  set(
    toolName: string,
    args: Record<string, unknown>,
    result: string,
    ttlMs?: number
  ): void {
    const key = this.makeKey(toolName, args);
    this.cache.set(key, {
      result,
      expiry: Date.now() + (ttlMs ?? this.defaultTTL),
    });
  }

  invalidate(toolName: string): void {
    for (const key of this.cache.keys()) {
      if (key.startsWith(`${toolName}:`)) {
        this.cache.delete(key);
      }
    }
  }
}

6. 与 LLM 集成

6.1 Function Calling 桥接

将 LLM 的 Function Calling 输出桥接到 MCP 工具调用:

interface LLMFunctionCall {
  name: string;
  arguments: string; // JSON string
}

class MCPFunctionBridge {
  constructor(private registry: MCPClientRegistry) {}

  async executeLLMFunctionCall(call: LLMFunctionCall): Promise<string> {
    const args = JSON.parse(call.arguments);
    return this.registry.callTool(call.name, args);
  }

  // 将 MCP 工具列表转换为 LLM Function Calling 格式
  convertToolsToOpenAIFormat(tools: Tool[]): Array<{
    type: "function";
    function: {
      name: string;
      description: string;
      parameters: object;
    };
  }> {
    return tools.map((tool) => ({
      type: "function" as const,
      function: {
        name: tool.name,
        description: tool.description ?? "",
        parameters: tool.inputSchema,
      },
    }));
  }
}

6.2 完整对话流程

async function runConversation(
  llm: OpenAI,
  registry: MCPClientRegistry,
  userMessage: string
): Promise<string> {
  const messages: Array<{
    role: "user" | "assistant" | "function";
    content?: string;
    name?: string;
  }> = [{ role: "user", content: userMessage }];

  // 获取可用工具
  const tools = await registry.getAllTools();
  const openaiTools = registry.convertToOpenAIFormat(tools);

  let response = await llm.chat.completions.create({
    model: "gpt-4",
    messages,
    tools: openaiTools,
    tool_choice: "auto",
  });

  // 处理可能的工具调用循环
  while (response.choices[0].message.tool_calls) {
    const assistantMessage = response.choices[0].message;
    messages.push({
      role: "assistant",
      content: assistantMessage.content ?? "",
    });

    for (const toolCall of assistantMessage.tool_calls) {
      const functionCall = toolCall.function;
      console.log(`LLM 请求调用: ${functionCall.name}`);

      const result = await registry.callTool(
        functionCall.name,
        JSON.parse(functionCall.arguments)
      );

      messages.push({
        role: "function" as const,
        name: functionCall.name,
        content: result,
      });
    }

    response = await llm.chat.completions.create({
      model: "gpt-4",
      messages,
      tools: openaiTools,
    });
  }

  return response.choices[0].message.content ?? "";
}

7. 监控与日志

class MCPClientMetrics {
  private callCounts: Map<string, number> = new Map();
  private callLatencies: Map<string, number[]> = new Map();
  private errors: Map<string, number> = new Map();

  recordCall(toolName: string, latencyMs: number, success: boolean): void {
    const count = this.callCounts.get(toolName) ?? 0;
    this.callCounts.set(toolName, count + 1);

    const latencies = this.callLatencies.get(toolName) ?? [];
    latencies.push(latencyMs);
    this.callLatencies.set(toolName, latencies);

    if (!success) {
      const errCount = this.errors.get(toolName) ?? 0;
      this.errors.set(toolName, errCount + 1);
    }
  }

  getReport(): string {
    const lines = ["=== MCP 客户端调用报告 ==="];
    for (const [tool, count] of this.callCounts) {
      const latencies = this.callLatencies.get(tool) ?? [];
      const avgLatency =
        latencies.reduce((a, b) => a + b, 0) / latencies.length;
      const errCount = this.errors.get(tool) ?? 0;
      lines.push(
        `${tool}: ${count} 次调用, 平均 ${avgLatency.toFixed(0)}ms, 错误 ${errCount} 次`
      );
    }
    return lines.join("\n");
  }
}

7.2 异常检测告警

class MCPAnomalyDetector {
  private latencyHistory: Map<string, number[]> = new Map();
  private errorRateHistory: Map<string, { total: number; errors: number }> = new Map();
  private windowSize: number = 60; // 最近 60 次调用

  record(toolName: string, latencyMs: number, success: boolean): void {
    // 记录延迟历史
    let latencies = this.latencyHistory.get(toolName) ?? [];
    latencies.push(latencyMs);
    if (latencies.length > this.windowSize) {
      latencies = latencies.slice(-this.windowSize);
    }
    this.latencyHistory.set(toolName, latencies);

    // 记录错误率
    let stats = this.errorRateHistory.get(toolName) ?? { total: 0, errors: 0 };
    stats.total++;
    if (!success) stats.errors++;
    this.errorRateHistory.set(toolName, stats);
  }

  // 基于统计的异常检测
  checkAnomalies(toolName: string): Array<{ type: string; severity: string; message: string }> {
    const alerts: Array<{ type: string; severity: string; message: string }> = [];

    // 延迟异常
    const latencies = this.latencyHistory.get(toolName) ?? [];
    if (latencies.length >= 10) {
      const mean = latencies.reduce((a, b) => a + b, 0) / latencies.length;
      const recent = latencies.slice(-5);
      const recentMean = recent.reduce((a, b) => a + b, 0) / recent.length;
      if (recentMean > mean * 3) {
        alerts.push({
          type: "latency_spike",
          severity: "warning",
          message: `${toolName} 最近延迟 ${recentMean.toFixed(0)}ms,远高于平均值 ${mean.toFixed(0)}ms`,
        });
      }
    }

    // 错误率异常
    const stats = this.errorRateHistory.get(toolName) ?? { total: 0, errors: 0 };
    if (stats.total >= 20) {
      const errorRate = stats.errors / stats.total;
      if (errorRate > 0.2) {
        alerts.push({
          type: "high_error_rate",
          severity: "critical",
          message: `${toolName} 错误率高达 ${(errorRate * 100).toFixed(1)}%,需要立即排查`,
        });
      }
    }

    return alerts;
  }
}

7.3 链路追踪

class MCPTracer {
  private spans: Map<string, Span> = new Map();

  startSpan(operation: string, parentId?: string): string {
    const id = crypto.randomUUID();
    this.spans.set(id, {
      id,
      parentId,
      operation,
      startTime: Date.now(),
      tags: {},
    });
    return id;
  }

  addTag(spanId: string, key: string, value: string): void {
    const span = this.spans.get(spanId);
    if (span) span.tags[key] = value;
  }

  finishSpan(spanId: string, error?: Error): void {
    const span = this.spans.get(spanId);
    if (!span) return;

    span.endTime = Date.now();
    span.duration = span.endTime - span.startTime;
    span.error = error?.message;

    // 发送到追踪系统(如 Jaeger、Zipkin)
    this.exportSpan(span);
  }

  private exportSpan(span: Span): void {
    console.log(`[TRACE] ${span.operation} - ${span.duration}ms`, span.tags);
  }
}

// 使用
const tracer = new MCPTracer();

async function tracedToolCall(registry: MCPClientRegistry, toolName: string, args: any) {
  const spanId = tracer.startSpan(`tool_call:${toolName}`);
  try {
    tracer.addTag(spanId, "tool", toolName);
    const result = await registry.callTool(toolName, args);
    tracer.finishSpan(spanId);
    return result;
  } catch (error) {
    tracer.finishSpan(spanId, error as Error);
    throw error;
  }
}

7.3 分布式追踪集成

// OpenTelemetry 集成
import { trace, context, propagation } from "@opentelemetry/api";

class OpenTelemetryMCPWrapper {
  private tracer = trace.getTracer("mcp-client");

  async callToolWithTrace(
    registry: MCPClientRegistry,
    toolName: string,
    args: any,
    parentContext?: any
  ): Promise<string> {
    const span = this.tracer.startSpan(`mcp.tool.${toolName}`, {
      attributes: {
        "mcp.tool.name": toolName,
        "mcp.tool.args": JSON.stringify(args),
      },
    }, parentContext);

    try {
      const result = await context.with(
        trace.setSpan(context.active(), span),
        () => registry.callTool(toolName, args)
      );
      span.setStatus({ code: 1 }); // OK
      return result;
    } catch (error) {
      span.recordException(error as Error);
      span.setStatus({ code: 2, message: (error as Error).message }); // ERROR
      throw error;
    } finally {
      span.end();
    }
  }
}

8. 总结

MCP 客户端的开发核心是将 “AI 模型的工具调用意图” 转化为 “服务器的实际执行”。关键要点包括:

  1. 连接池管理:同时维护多个服务器连接,按需路由工具调用
  2. 容错设计:重试、退避、优雅降级确保服务可用性
  3. 上下文控制:结果截断、缓存和摘要避免超出模型上下文限制
  4. 监控可见性:记录调用频率、延迟和错误率,及时发现异常
  5. 桥接标准化:将兼容 MCP 的服务器无缝接入任何支持 Function Calling 的模型

MCP 客户端的错误处理模式

MCP 服务器可能随时不可用或返回异常,客户端需实现健壮的错误处理:

// 带退避重试的工具调用
func callWithRetry(ctx context.Context, client *mcp.Client, tool string, args map[string]any, maxRetries int) (*mcp.CallResult, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        result, err := client.CallTool(ctx, tool, args)
        if err == nil {
            return result, nil
        }
        lastErr = err
        // 指数退避
        time.Sleep(time.Duration(i*i) * 100 * time.Millisecond)
    }
    return nil, fmt.Errorf("max retries exceeded: %w", lastErr)
}
错误类型处理方式示例
网络超时退避重试context.DeadlineExceeded
服务未就绪连接池健康检查定期 ping initialize
参数无效向前端返回校验错误InvalidParams
工具调用失败降级到默认行为跳过该工具,继续对话

MCP 的开放工具生态意味着不可信服务器的风险。客户端应始终校验返回数据的 schema,防止恶意服务器注入伪造结果。

在下一篇文章中,我们将探讨 MCP 的安全实践——如何在开放工具生态的同时确保数据和系统安全。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. 排序学习与搜索召回排序系统
  2. 数据版本控制与血缘:DVC 与 LakeFS
  3. 模型可解释性:SHAP、LIME 与注意力归因