第6章:MCP Client集成

在前面的章节中,我们学习了如何开发 MCP Server,定义 Tools 和 Resources。本章将学习如何开发 MCP Client——在自己的应用程序中连接和使用 MCP Server 的客户端程序。

一、什么是 MCP Client

MCP Client 是 MCP 架构中的另一重要角色,它的职责是:

  • 连接 Server:与 MCP Server 建立通信连接
  • 发现能力:获取 Server 提供的 Tools、Resources、Prompts
  • 调用工具:根据需要调用 Server 的 Tools
  • 处理结果:接收并处理 Server 返回的结果
类比理解:
• MCP Server = 餐厅(提供菜品/服务)
• MCP Client = 顾客(点菜、用餐)
• MCP 协议 = 菜单和交流语言

Client 与 Server 的关系

┌─────────────────┐         MCP Protocol         ┌─────────────────┐
│   MCP Client    │  ◄────────────────────────►  │   MCP Server    │
│  (你的应用程序)  │                              │ (提供Tools服务)  │
│                 │                              │                 │
│  1. 连接 Server │                              │ 1. 启动并监听    │
│  2. 获取工具列表 │                              │ 2. 注册工具      │
│  3. 调用工具    │                              │ 3. 执行工具逻辑  │
│  4. 处理结果    │                              │ 4. 返回结果      │
└─────────────────┘                              └─────────────────┘

二、Client 使用场景

开发 MCP Client 可以让你的应用程序获得强大的 AI 扩展能力:

场景 说明 示例
智能助手 开发命令行或桌面 AI 助手 可以执行文件操作、查询数据库的 AI 助手
自动化工具 结合 LLM 和工具实现自动化 自动代码审查、文档生成工具
IDE 插件 在编辑器中集成 AI 能力 VS Code 插件,支持代码补全、重构
数据分析 让 AI 直接查询和分析数据 用自然语言查询数据库、生成报表
运维工具 服务器管理和监控 自然语言查询日志、重启服务

三、TypeScript Client 开发

MCP SDK 提供了完整的 Client 支持。以下是 TypeScript Client 的核心开发步骤:

1. 安装依赖

npm install @modelcontextprotocol/sdk

2. 基本 Client 代码结构

// client.js - MCP Client 基础示例
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

/**
 * 创建 MCP Client 并连接到 Server
 */
async function createClient() {
  // 步骤 1: 创建 Client 实例
  // name 和 version 用于标识你的 Client 应用
  const client = new Client({
    name: 'my-mcp-client',
    version: '1.0.0',
  });

  // 步骤 2: 创建传输层(stdio 方式)
  // 这里连接到本地的 server.js
  const transport = new StdioClientTransport({
    command: 'node',
    args: ['server.js'],  // Server 入口文件路径
    env: process.env,     // 传递环境变量
  });

  // 步骤 3: 建立连接
  await client.connect(transport);
  console.log('✅ 已连接到 MCP Server');

  return client;
}
传输层说明:
StdioClientTransport:通过标准输入输出通信,适合本地进程
SseClientTransport:通过 HTTP SSE 通信,适合远程 Server

3. 完整 TypeScript Client 示例

// full-client.js - 完整的 MCP Client 示例
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import readline from 'readline';

class MCPClient {
  constructor() {
    this.client = null;
    this.transport = null;
  }

  /**
   * 连接到 MCP Server
   * @param {string} serverPath - Server 脚本路径
   */
  async connect(serverPath) {
    // 创建 Client 实例
    this.client = new Client({
      name: 'mcp-demo-client',
      version: '1.0.0',
    });

    // 创建 stdio 传输层
    this.transport = new StdioClientTransport({
      command: 'node',
      args: [serverPath],
    });

    // 建立连接
    await this.client.connect(this.transport);
    console.log('✅ 已成功连接到 MCP Server');
  }

  /**
   * 获取 Server 提供的所有 Tools
   * @returns {Array} 工具列表
   */
  async listTools() {
    const response = await this.client.listTools();
    return response.tools || [];
  }

  /**
   * 调用指定的 Tool
   * @param {string} name - 工具名称
   * @param {object} args - 工具参数
   * @returns {object} 调用结果
   */
  async callTool(name, args) {
    const result = await this.client.callTool({
      name: name,
      arguments: args,
    });
    return result;
  }

  /**
   * 关闭连接
   */
  async close() {
    if (this.transport) {
      await this.transport.close();
      console.log('🔌 已断开连接');
    }
  }
}

// ===== 使用示例 =====
async function main() {
  const mcpClient = new MCPClient();

  try {
    // 1. 连接到 Server(假设 Server 在同级目录的 server.js)
    await mcpClient.connect('./server.js');

    // 2. 获取可用的 Tools 列表
    console.log('\n📋 可用工具列表:');
    const tools = await mcpClient.listTools();
    tools.forEach((tool, index) => {
      console.log(`  ${index + 1}. ${tool.name}: ${tool.description}`);
    });

    // 3. 调用 calculator 工具
    console.log('\n🔧 测试调用 calculator 工具:');
    const calcResult = await mcpClient.callTool('calculator', {
      expression: '100 * 2 + 50',
    });
    console.log('结果:', calcResult.content[0].text);

    // 4. 调用时间查询工具
    console.log('\n🔧 测试调用 get_current_time 工具:');
    const timeResult = await mcpClient.callTool('get_current_time', {
      format: 'full',
    });
    console.log('结果:', timeResult.content[0].text);

  } catch (error) {
    console.error('❌ 错误:', error.message);
  } finally {
    // 5. 关闭连接
    await mcpClient.close();
  }
}

// 运行
main();

四、调用 Tools 流程

在实际应用中,与 LLM 集成时的典型调用流程如下:

┌─────────────┐     1. 用户提问      ┌─────────────┐
│    用户     │ ───────────────────► │  Client 应用  │
└─────────────┘                      └──────┬──────┘
                                            │
                                            ▼
                                    ┌─────────────┐
                                    │ 2. 获取工具列表 │
                                    │  listTools()  │
                                    └──────┬──────┘
                                           │
                                           ▼
┌─────────────┐     3. 发送问题和工具列表   ┌─────────────┐
│  Client 应用  │ ───────────────────────► │     LLM     │
│             │                           │  (AI模型)   │
│ 4. 执行工具  │ ◄───────────────────────  │             │
│  callTool() │    返回要调用的工具名称      │  决定调用   │
└──────┬──────┘    和参数                  └─────────────┘
       │
       ▼
┌─────────────┐
│  MCP Server │
│  (执行逻辑)  │
└──────┬──────┘
       │
       ▼
┌─────────────┐     5. 返回工具执行结果    ┌─────────────┐
│  Client 应用  │ ───────────────────────► │     LLM     │
│             │                           │             │
│ 6. 展示最终  │ ◄───────────────────────  │  生成回复   │
│    回答     │     返回给用户看的回答      │             │
└──────┬──────┘                           └─────────────┘
       │
       ▼
┌─────────────┐
│    用户     │
└─────────────┘

核心 API 详解

API 功能 返回值
client.listTools() 获取 Server 的所有可用工具 { tools: [...] }
client.callTool() 调用指定工具 { content: [...], isError?: boolean }
client.listResources() 获取 Server 的所有可用资源 { resources: [...] }
client.readResource() 读取指定资源 { contents: [...] }
client.listPrompts() 获取 Server 的所有提示模板 { prompts: [...] }

五、Python Client 开发

MCP 也提供了 Python SDK,以下是等效的 Python Client 实现:

1. 安装依赖

pip install mcp

2. Python Client 完整代码

#!/usr/bin/env python3
"""
MCP Python Client 示例
演示如何连接 MCP Server 并调用 Tools
"""

import asyncio
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


class MCPPythonClient:
    """MCP Client 封装类"""
    
    def __init__(self):
        self.session = None
        self.client = None
    
    async def connect(self, server_script: str):
        """
        连接到 MCP Server
        
        Args:
            server_script: Server Python 脚本路径
        """
        # 配置 Server 参数
        server_params = StdioServerParameters(
            command="python",
            args=[server_script],
            env=None
        )
        
        # 建立 stdio 连接
        self.client = stdio_client(server_params)
        self.read, self.write = await self.client.__aenter__()
        
        # 创建会话
        self.session = await ClientSession(self.read, self.write).__aenter__()
        
        # 初始化连接
        await self.session.initialize()
        print("✅ 已成功连接到 MCP Server")
    
    async def list_tools(self):
        """获取可用的 Tools 列表"""
        response = await self.session.list_tools()
        return response.tools
    
    async def call_tool(self, name: str, arguments: dict):
        """
        调用指定的 Tool
        
        Args:
            name: 工具名称
            arguments: 工具参数
        
        Returns:
            工具执行结果
        """
        result = await self.session.call_tool(name, arguments)
        return result
    
    async def close(self):
        """关闭连接"""
        if self.session:
            await self.session.__aexit__(None, None, None)
        if self.client:
            await self.client.__aexit__(None, None, None)
        print("🔌 已断开连接")


async def main():
    """主函数 - Client 使用示例"""
    client = MCPPythonClient()
    
    try:
        # 1. 连接到 Server
        await client.connect("server.py")
        
        # 2. 获取工具列表
        print("\n📋 可用工具列表:")
        tools = await client.list_tools()
        for i, tool in enumerate(tools, 1):
            print(f"  {i}. {tool.name}: {tool.description}")
        
        # 3. 调用 calculator 工具
        print("\n🔧 测试调用 calculator 工具:")
        calc_result = await client.call_tool("calculator", {
            "expression": "15 * 8 - 30"
        })
        for content in calc_result.content:
            print(f"结果:{content.text}")
        
        # 4. 调用随机数生成工具
        print("\n🔧 测试调用 generate_random 工具:")
        random_result = await client.call_tool("generate_random", {
            "min": 1,
            "max": 100,
            "count": 5
        })
        for content in random_result.content:
            print(f"结果:{content.text}")
            
    except Exception as e:
        print(f"❌ 错误:{e}")
    finally:
        # 5. 关闭连接
        await client.close()


if __name__ == "__main__":
    # 运行异步主函数
    asyncio.run(main())
Python vs TypeScript:
• Python SDK 使用异步编程(async/await)
• TypeScript SDK 可选择 Promise 或 async/await
• 两者功能等价,根据你的技术栈选择

六、与 LLM 集成

MCP 的真正威力在于与 LLM(大语言模型)的结合。下面展示如何将 Tools 列表传递给 LLM,并让 AI 决定何时调用工具。

集成流程示意

┌─────────────────────────────────────────────────────────────┐
│                    Client 应用程序                           │
│                                                             │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐  │
│  │   用户输入    │───►│   LLM 处理   │───►│  展示结果    │  │
│  └──────────────┘    └──────┬───────┘    └──────────────┘  │
│                             │                               │
│                             ▼                               │
│                      需要调用工具?                          │
│                      /           \                          │
│                    是              否                       │
│                    │              │                         │
│                    ▼              ▼                         │
│            ┌──────────────┐   直接回答                      │
│            │  MCP Client  │                                │
│            │  callTool()  │                                │
│            └──────┬───────┘                                │
│                   │                                         │
│                   ▼                                         │
│            ┌──────────────┐                                │
│            │  MCP Server  │                                │
│            └──────────────┘                                │
└─────────────────────────────────────────────────────────────┘

与 OpenAI API 集成示例

// llm-client.js - 与 LLM 集成的 MCP Client
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import OpenAI from 'openai';
import readline from 'readline';

class LLMMCPClient {
  constructor() {
    this.mcpClient = null;
    this.openai = new OpenAI({
      apiKey: process.env.OPENAI_API_KEY,
    });
    this.tools = [];  // 存储 MCP Tools
  }

  /**
   * 初始化:连接 MCP Server 并获取 Tools
   */
  async initialize(serverPath) {
    // 连接 MCP Server
    this.mcpClient = new Client({
      name: 'llm-mcp-client',
      version: '1.0.0',
    });

    const transport = new StdioClientTransport({
      command: 'node',
      args: [serverPath],
    });

    await this.mcpClient.connect(transport);
    console.log('✅ 已连接到 MCP Server');

    // 获取 Tools 列表并转换为 OpenAI function calling 格式
    const response = await this.mcpClient.listTools();
    this.tools = this.convertToOpenAIFunctions(response.tools);
    console.log(`📋 已加载 ${this.tools.length} 个工具`);
  }

  /**
   * 将 MCP Tools 转换为 OpenAI function 格式
   */
  convertToOpenAIFunctions(mcpTools) {
    return mcpTools.map(tool => ({
      type: 'function',
      function: {
        name: tool.name,
        description: tool.description,
        parameters: tool.inputSchema,
      },
    }));
  }

  /**
   * 处理用户消息,与 LLM 交互
   */
  async processMessage(userMessage) {
    const messages = [
      {
        role: 'system',
        content: '你是一个智能助手。当需要执行计算、查询时间或生成随机数时,请使用提供的工具。'
      },
      {
        role: 'user',
        content: userMessage,
      },
    ];

    // 第一步:发送消息给 LLM,附带可用工具
    const response = await this.openai.chat.completions.create({
      model: 'gpt-4',
      messages: messages,
      tools: this.tools,
      tool_choice: 'auto',  // 让 LLM 自动决定是否调用工具
    });

    const responseMessage = response.choices[0].message;

    // 检查 LLM 是否决定调用工具
    if (responseMessage.tool_calls) {
      console.log('🤖 LLM 决定调用工具');

      // 执行工具调用
      const toolResults = [];
      for (const toolCall of responseMessage.tool_calls) {
        const functionName = toolCall.function.name;
        const functionArgs = JSON.parse(toolCall.function.arguments);

        console.log(`  调用: ${functionName}(${JSON.stringify(functionArgs)})`);

        // 调用 MCP Tool
        const result = await this.mcpClient.callTool({
          name: functionName,
          arguments: functionArgs,
        });

        toolResults.push({
          tool_call_id: toolCall.id,
          role: 'tool',
          content: result.content[0].text,
        });
      }

      // 将工具结果返回给 LLM
      messages.push(responseMessage);  // 添加 LLM 的 tool_calls 消息
      messages.push(...toolResults);    // 添加工具执行结果

      // 第二步:让 LLM 根据工具结果生成最终回答
      const finalResponse = await this.openai.chat.completions.create({
        model: 'gpt-4',
        messages: messages,
      });

      return finalResponse.choices[0].message.content;
    }

    // LLM 直接回答了,没有调用工具
    return responseMessage.content;
  }

  /**
   * 启动交互式对话
   */
  async startChat() {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });

    console.log('\n🤖 LLM MCP Client 已启动');
    console.log('可用命令:');
    console.log('  - 直接输入问题与 AI 对话');
    console.log('  - 输入 "quit" 退出\n');

    const askQuestion = () => {
      rl.question('你: ', async (input) => {
        if (input.toLowerCase() === 'quit') {
          console.log('再见!');
          rl.close();
          return;
        }

        try {
          const response = await this.processMessage(input);
          console.log('\nAI: ', response, '\n');
        } catch (error) {
          console.error('错误:', error.message);
        }

        askQuestion();
      });
    };

    askQuestion();
  }
}

// ===== 运行 =====
async function main() {
  const client = new LLMMCPClient();
  
  try {
    // 初始化并连接 Server
    await client.initialize('./server.js');
    
    // 启动对话
    await client.startChat();
  } catch (error) {
    console.error('启动失败:', error);
  }
}

main();
对话示例:
你: 现在几点了?
🤖 LLM 决定调用工具
  调用: get_current_time({"format": "full"})
AI:  当前时间是 2026年3月27日 星期五 09:30:15 CST

你: 帮我计算 123 乘以 456
🤖 LLM 决定调用工具
  调用: calculator({"expression": "123 * 456"})
AI:  123 乘以 456 等于 56,088

你: 生成一个 1-100 的随机数
🤖 LLM 决定调用工具
  调用: generate_random({"min": 1, "max": 100})
AI:  为您生成的随机数是 42

七、完整示例:命令行助手

现在让我们开发一个完整的命令行助手,整合前面学习的所有知识:

// cli-assistant.js - 完整的命令行 AI 助手
#!/usr/bin/env node

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import readline from 'readline';
import chalk from 'chalk';  // 用于彩色输出

/**
 * 命令行 AI 助手
 * 支持 MCP Server 工具调用、对话历史、错误处理
 */
class CLIAssistant {
  constructor(config = {}) {
    this.config = {
      serverPath: config.serverPath || './server.js',
      serverCommand: config.serverCommand || 'node',
      showToolCalls: config.showToolCalls !== false,  // 默认显示工具调用
      ...config,
    };
    
    this.client = null;
    this.tools = [];
    this.conversationHistory = [];  // 对话历史
    this.stats = {
      toolCalls: 0,
      messages: 0,
    };
  }

  /**
   * 初始化:连接 Server
   */
  async initialize() {
    console.log(chalk.blue('🔌 正在连接到 MCP Server...'));

    try {
      // 创建 Client
      this.client = new Client({
        name: 'cli-assistant',
        version: '1.0.0',
      });

      // 创建传输层
      const transport = new StdioClientTransport({
        command: this.config.serverCommand,
        args: [this.config.serverPath],
      });

      // 连接(带超时)
      const connectPromise = this.client.connect(transport);
      const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('连接超时(10秒)')), 10000);
      });

      await Promise.race([connectPromise, timeoutPromise]);

      // 获取工具列表
      const toolsResponse = await this.client.listTools();
      this.tools = toolsResponse.tools;

      console.log(chalk.green(`✅ 连接成功!已加载 ${this.tools.length} 个工具`));
      this.printTools();

    } catch (error) {
      throw new Error(`初始化失败: ${error.message}`);
    }
  }

  /**
   * 显示可用工具
   */
  printTools() {
    console.log(chalk.cyan('\n📋 可用工具:'));
    this.tools.forEach((tool, i) => {
      console.log(chalk.gray(`  ${i + 1}. ${chalk.white(tool.name)}`));
      console.log(chalk.gray(`     ${tool.description.slice(0, 60)}...`));
    });
    console.log('');
  }

  /**
   * 解析用户输入,判断是否需要调用工具
   * 简化版:使用关键词匹配
   */
  async processInput(input) {
    // 添加到历史
    this.conversationHistory.push({ role: 'user', content: input });
    this.stats.messages++;

    // 特殊命令
    if (input.startsWith('/')) {
      return this.handleCommand(input);
    }

    // 根据关键词判断调用哪个工具
    const toolCall = this.detectToolCall(input);
    
    if (toolCall) {
      return this.executeTool(toolCall);
    }

    // 没有匹配的工具,返回默认回复
    return '我不太确定如何帮助你。你可以尝试询问时间、计算数学表达式或生成随机数。';
  }

  /**
   * 检测用户输入是否匹配某个工具
   */
  detectToolCall(input) {
    const lowerInput = input.toLowerCase();

    // 计算器:包含数字和运算符
    if (/[\d+\-*/].*/.test(input) && 
        (lowerInput.includes('计算') || lowerInput.includes('等于') || lowerInput.includes('多少'))) {
      // 提取数学表达式
      const match = input.match(/([\d+\-*/().\s]+)/);
      if (match) {
        return {
          name: 'calculator',
          args: { expression: match[1].replace(/\s/g, '') },
        };
      }
    }

    // 时间查询
    if (lowerInput.includes('时间') || lowerInput.includes('几点') || lowerInput.includes('日期')) {
      const format = lowerInput.includes('日期') ? 'date' : 
                     lowerInput.includes('时间') && !lowerInput.includes('日期') ? 'time' : 'full';
      return {
        name: 'get_current_time',
        args: { format },
      };
    }

    // 随机数
    if (lowerInput.includes('随机') || lowerInput.includes('随机数')) {
      const rangeMatch = input.match(/(\d+)\s*到\s*(\d+)/);
      if (rangeMatch) {
        return {
          name: 'generate_random',
          args: { min: parseInt(rangeMatch[1]), max: parseInt(rangeMatch[2]) },
        };
      }
      return {
        name: 'generate_random',
        args: { min: 1, max: 100 },
      };
    }

    return null;
  }

  /**
   * 执行工具调用
   */
  async executeTool(toolCall) {
    if (this.config.showToolCalls) {
      console.log(chalk.yellow(`\n🔧 正在调用: ${toolCall.name}`));
      console.log(chalk.gray(`   参数: ${JSON.stringify(toolCall.args)}`));
    }

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

      this.stats.toolCalls++;

      // 提取文本内容
      const text = result.content
        .filter(c => c.type === 'text')
        .map(c => c.text)
        .join('\n');

      return text;

    } catch (error) {
      return `工具调用失败: ${error.message}`;
    }
  }

  /**
   * 处理特殊命令
   */
  handleCommand(input) {
    const parts = input.slice(1).split(' ');
    const cmd = parts[0];

    switch (cmd) {
      case 'help':
        return `可用命令:
  /help     - 显示帮助
  /tools    - 显示工具列表
  /stats    - 显示统计信息
  /clear    - 清空对话历史
  /quit     - 退出程序`;

      case 'tools':
        this.printTools();
        return '';

      case 'stats':
        return `统计信息:
  对话消息: ${this.stats.messages}
  工具调用: ${this.stats.toolCalls}
  可用工具: ${this.tools.length}`;

      case 'clear':
        this.conversationHistory = [];
        return '对话历史已清空';

      case 'quit':
        return null;  // 返回 null 表示退出

      default:
        return `未知命令: ${cmd},输入 /help 查看可用命令`;
    }
  }

  /**
   * 启动交互式会话
   */
  async start() {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
      prompt: chalk.cyan('你> '),
    });

    console.log(chalk.green.bold('\n🤖 MCP CLI 助手已启动!'));
    console.log(chalk.gray('输入 /help 查看命令,输入 /quit 退出\n'));

    rl.prompt();

    rl.on('line', async (input) => {
      const trimmed = input.trim();
      if (!trimmed) {
        rl.prompt();
        return;
      }

      try {
        const response = await this.processInput(trimmed);

        if (response === null) {
          console.log(chalk.yellow('👋 再见!'));
          rl.close();
          return;
        }

        if (response) {
          console.log(chalk.green('AI> ') + response + '\n');
        }
      } catch (error) {
        console.error(chalk.red('错误: ') + error.message);
      }

      rl.prompt();
    });

    return new Promise((resolve) => {
      rl.on('close', resolve);
    });
  }

  /**
   * 关闭连接
   */
  async close() {
    if (this.client) {
      // Client 没有显式的 close 方法,依赖 transport 关闭
      console.log(chalk.gray('\n🔌 断开连接'));
    }
  }
}

// ===== 主程序 =====
async function main() {
  const assistant = new CLIAssistant({
    serverPath: './server.js',
    showToolCalls: true,
  });

  try {
    await assistant.initialize();
    await assistant.start();
  } catch (error) {
    console.error(chalk.red('错误: ') + error.message);
    process.exit(1);
  } finally {
    await assistant.close();
  }
}

main();

运行效果

$ node cli-assistant.js
🔌 正在连接到 MCP Server...
✅ 连接成功!已加载 3 个工具

📋 可用工具:
  1. calculator
     执行数学计算,支持:基础运算、幂运算、三角函数...
  2. get_current_time
     获取当前日期和时间信息...
  3. generate_random
     生成随机数...

🤖 MCP CLI 助手已启动!
输入 /help 查看命令,输入 /quit 退出

你> 现在几点了?
🔧 正在调用: get_current_time
   参数: {"format":"full"}
AI> 当前时间:2026年3月27日 星期五 09:30:15 GMT+8(时区:Asia/Shanghai)

你> 计算 100 + 200 等于多少
🔧 正在调用: calculator
   参数: {"expression":"100+200"}
AI> 计算结果:100+200 = 300

你> 生成 1 到 100 的随机数
🔧 正在调用: generate_random
   参数: {"min":1,"max":100}
AI> 生成的随机数:42

你> /stats
AI> 统计信息:
  对话消息: 4
  工具调用: 3
  可用工具: 3

你> /quit
👋 再见!

八、错误处理与重试

在实际应用中,需要处理各种错误情况。以下是完善的错误处理策略:

常见错误类型

// error-handling.js - 错误处理示例

class ResilientMCPClient {
  constructor(config = {}) {
    this.config = {
      maxRetries: config.maxRetries || 3,
      retryDelay: config.retryDelay || 1000,  // 毫秒
      timeout: config.timeout || 10000,       // 毫秒
      ...config,
    };
    this.client = null;
    this.retryCount = 0;
  }

  /**
   * 带重试的连接
   */
  async connectWithRetry(serverPath) {
    while (this.retryCount < this.config.maxRetries) {
      try {
        await this.connect(serverPath);
        console.log('✅ 连接成功');
        return;
      } catch (error) {
        this.retryCount++;
        console.error(`❌ 连接失败 (${this.retryCount}/${this.config.maxRetries}): ${error.message}`);
        
        if (this.retryCount < this.config.maxRetries) {
          console.log(`⏳ ${this.config.retryDelay}ms 后重试...`);
          await this.sleep(this.config.retryDelay);
        }
      }
    }
    
    throw new Error(`连接失败,已重试 ${this.config.maxRetries} 次`);
  }

  /**
   * 建立连接
   */
  async connect(serverPath) {
    this.client = new Client({
      name: 'resilient-client',
      version: '1.0.0',
    });

    const transport = new StdioClientTransport({
      command: 'node',
      args: [serverPath],
    });

    // 使用 Promise.race 实现超时
    const connectPromise = this.client.connect(transport);
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => {
        reject(new Error(`连接超时 (${this.config.timeout}ms)`));
      }, this.config.timeout);
    });

    await Promise.race([connectPromise, timeoutPromise]);
    this.retryCount = 0;  // 重置重试计数
  }

  /**
   * 安全的工具调用
   */
  async callToolSafely(name, args) {
    // 参数验证
    if (!name || typeof name !== 'string') {
      throw new Error('工具名称无效');
    }

    try {
      const result = await this.client.callTool({ name, 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;

    } catch (error) {
      // 分类错误
      if (error.message.includes('Unknown tool')) {
        throw new Error(`工具 "${name}" 不存在,请检查工具名称`);
      }
      if (error.message.includes('timeout')) {
        throw new Error(`调用超时,请检查 Server 是否响应`);
      }
      throw error;
    }
  }

  /**
   * 健康检查
   */
  async healthCheck() {
    try {
      await this.client.listTools();
      return { status: 'healthy', timestamp: new Date().toISOString() };
    } catch (error) {
      return { 
        status: 'unhealthy', 
        error: error.message,
        timestamp: new Date().toISOString(),
      };
    }
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

错误处理最佳实践

错误类型 处理策略 示例代码
连接失败 重试机制 + 指数退避 最多重试 3 次,间隔递增
连接超时 设置超时限制 + 友好提示 10 秒超时,提示检查 Server
工具不存在 获取工具列表验证 + 错误提示 提示可用的工具名称
参数错误 参数校验 + Schema 验证 调用前检查必需参数
Server 崩溃 异常捕获 + 自动重连 捕获异常,尝试重新连接
生产环境注意:
• 添加详细的日志记录
• 实现监控和告警
• 限制重试次数,避免无限循环
• 为长时间运行的工具设置超时

本章小结

本章全面介绍了 MCP Client 的开发:

  • MCP Client 概念:连接 Server、发现能力、调用工具的角色
  • TypeScript Client:使用 @modelcontextprotocol/sdk 开发
  • Python Client:使用 mcp 包开发
  • 调用流程:listTools() → LLM 决策 → callTool() → 处理结果
  • LLM 集成:将 Tools 转换为 Function Calling 格式
  • 错误处理:重试、超时、参数验证等策略
学习路径建议:
1. 先实现基础的 Client,能够连接 Server 并调用工具
2. 再集成 LLM,让 AI 自主决定何时调用工具
3. 最后添加错误处理和重试机制,提升稳定性

至此,你已经掌握了 MCP Server 和 Client 的完整开发技能。下一章将学习 MCP 的高级特性与最佳实践,包括认证授权、性能优化、安全考虑等进阶话题。