
在实际 AI 应用开发中我们经常需要让大语言模型LLM与外部工具、数据源或服务进行交互。Model Context ProtocolMCP正是为此而生的一种标准化协议它定义了 LLM 与外部资源之间的通信规范。上一篇文章介绍了 MCP 的基本概念和协议结构本文将继续深入通过一个完整的实例展示如何调用一个公开的 MCP Server。如果你正在开发基于 Claude、Cursor 或其他支持 MCP 的 AI 助手并希望为其扩展文件操作、数据库查询、API 调用等能力那么理解如何正确调用 MCP Server 是关键一步。本文将带你从环境准备开始逐步完成 MCP Client 的配置、连接建立、工具调用和结果处理的全流程。1. 理解 MCP 协议中的角色分工在开始具体实现前需要先明确 MCP 协议中的三个核心角色及其职责这对后续的调试和问题排查非常重要。1.1 MCP Server 的作用与能力MCP Server 是能力的提供方它封装了具体的功能实现。一个 MCP Server 通常会提供以下几类资源工具Tools可供调用的函数如文件读写、数据库查询、API 调用等资源Resources可访问的数据源如数据库表、配置文件、知识库等提示词Prompts预定义的对话模板或指令数据源Data sources流式或静态的数据提供接口例如一个公开的天气查询 MCP Server 可能提供一个get_weather工具接收城市名作为参数返回天气信息。1.2 MCP Client 的职责与行为MCP Client 是能力的消费方通常是 AI 助手或应用程序。它的主要职责包括发现并连接可用的 MCP Server获取 Server 提供的工具列表和资源列表根据用户需求调用合适的工具处理调用结果和可能的错误管理会话状态和上下文在实际项目中Claude Desktop、Cursor 等都是 MCP Client 的具体实现。1.3 传输层的重要性与选择MCP 协议本身不限定传输方式常见的实现包括stdio标准输入输出适合本地集成稳定性高SSEServer-Sent Events适合 Web 环境支持服务端推送WebSocket适合需要双向实时通信的场景选择哪种传输方式取决于你的部署环境和服务需求。对于初学者建议从 stdio 开始因为它配置简单排除干扰因素少。2. 环境准备与依赖配置在开始编码前需要确保开发环境就绪。以下配置基于 Node.js 环境但思路同样适用于其他语言。2.1 开发环境要求确保你的系统满足以下基本要求# 检查 Node.js 版本需要 16.0 或更高 node --version # 检查 npm 版本 npm --version # 检查是否已安装 MCP 相关工具 npm list -g | grep mcp如果尚未安装 Node.js可以从官网下载 LTS 版本。对于生产环境建议使用 Node.js 18 以获得更好的性能和稳定性。2.2 创建项目并安装依赖创建一个新的项目目录并初始化# 创建项目目录 mkdir mcp-client-demo cd mcp-client-demo # 初始化 package.json npm init -y # 安装 MCP 相关依赖 npm install modelcontextprotocol/sdk axios关键依赖说明modelcontextprotocol/sdk官方提供的 MCP SDK包含 Client 和 Server 的基础实现axios用于 HTTP 请求在调用某些基于 HTTP 的 MCP Server 时会用到2.3 配置 MCP Client 定义文件MCP Client 需要知道如何连接和认证 MCP Server。创建一个配置文件mcp-client.json{ mcpServers: { weather-server: { command: node, args: [/path/to/weather-server/index.js], env: { API_KEY: your-weather-api-key } }, file-operations: { command: python, args: [-m, mcp_file_server], cwd: /path/to/server/files } } }这个配置文件定义了多个 MCP Server每个 Server 的配置包括command启动 Server 的命令args命令参数env环境变量cwd工作目录在实际项目中你需要根据具体的 MCP Server 文档调整这些配置。3. 实现基础的 MCP Client现在开始编写 MCP Client 的核心代码。我们将创建一个能够连接、发现工具并执行调用的完整客户端。3.1 建立 MCP Client 类框架首先创建MCPClient.js文件定义基本的客户端结构const { Client } require(modelcontextprotocol/sdk/client/index.js); const { StdioClientTransport } require(modelcontextprotocol/sdk/client/stdio.js); class MCPClient { constructor(serverConfig) { this.serverConfig serverConfig; this.client new Client( { name: demo-mcp-client, version: 1.0.0, }, { capabilities: { roots: {}, sampling: {}, }, } ); this.transport null; this.tools new Map(); this.resources new Map(); } // 连接方法将在下一节实现 async connect() { // 连接逻辑 } // 工具调用方法 async callTool(name, arguments) { // 调用逻辑 } // 资源访问方法 async readResource(resourceId) { // 资源读取逻辑 } } module.exports MCPClient;这个类框架定义了 MCP Client 的基本结构包括连接管理、工具调用和资源访问等核心方法。3.2 实现连接与初始化流程连接建立是 MCP Client 最关键的一步需要正确处理各种异常情况async connect() { try { // 创建传输层实例 this.transport new StdioClientTransport({ command: this.serverConfig.command, args: this.serverConfig.args, env: this.serverConfig.env }); // 建立连接 await this.client.connect(this.transport); console.log(MCP Server 连接成功); // 初始化获取可用工具和资源 await this.initialize(); } catch (error) { console.error(连接 MCP Server 失败:, error); throw new Error(无法连接 MCP Server: ${error.message}); } } async initialize() { try { // 获取工具列表 const toolsResponse await this.client.listTools(); if (toolsResponse.tools) { toolsResponse.tools.forEach(tool { this.tools.set(tool.name, tool); }); console.log(发现 ${this.tools.size} 个可用工具); } // 获取资源列表 const resourcesResponse await this.client.listResources(); if (resourcesResponse.resources) { resourcesResponse.resources.forEach(resource { this.resources.set(resource.uri, resource); }); console.log(发现 ${this.resources.size} 个可用资源); } } catch (error) { console.error(初始化 MCP Client 失败:, error); throw error; } }连接过程中需要特别注意错误处理因为传输层的问题如命令不存在、权限不足等都可能导致连接失败。3.3 实现工具调用与结果处理工具调用是 MCP Client 的核心功能需要正确处理参数传递和结果解析async callTool(name, arguments) { // 检查工具是否存在 if (!this.tools.has(name)) { throw new Error(工具 ${name} 不存在); } try { const tool this.tools.get(name); // 验证参数简化版本实际需要更严格的校验 if (tool.inputSchema tool.inputSchema.required) { for (const requiredParam of tool.inputSchema.required) { if (arguments[requiredParam] undefined) { throw new Error(缺少必需参数: ${requiredParam}); } } } // 调用工具 const result await this.client.callTool({ name: name, arguments: arguments }); // 处理调用结果 if (result.content) { return this.parseToolResult(result.content); } else { throw new Error(工具调用返回空结果); } } catch (error) { console.error(调用工具 ${name} 失败:, error); throw new Error(工具调用错误: ${error.message}); } } parseToolResult(content) { // MCP 协议中结果可能是文本、图像或其他类型 if (Array.isArray(content)) { return content.map(item { if (item.type text) { return item.text; } else if (item.type image) { return { type: image, data: item.data }; } return item; }); } return content; }工具调用的错误处理需要区分几种情况工具不存在、参数错误、执行错误、网络超时等每种情况都需要不同的处理策略。4. 调用公开 MCP Server 实战现在让我们通过一个具体的例子演示如何调用一个公开的天气查询 MCP Server。4.1 准备示例 MCP Server为了演示我们先创建一个简单的天气查询 MCP Server。创建demo-weather-server.jsconst { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const server new Server( { name: demo-weather-server, version: 1.0.0, }, { capabilities: { tools: {}, }, } ); // 注册天气查询工具 server.setRequestHandler(tools/list, async () ({ tools: [ { name: get_weather, description: 获取指定城市的天气信息, inputSchema: { type: object, properties: { city: { type: string, description: 城市名称 }, unit: { type: string, enum: [celsius, fahrenheit], default: celsius, description: 温度单位 } }, required: [city] } } ] })); // 处理工具调用 server.setRequestHandler(tools/call, async (request) { if (request.params.name get_weather) { const { city, unit celsius } request.params.arguments; // 模拟天气数据查询 const weatherData { 北京: { temperature: 25, condition: 晴朗 }, 上海: { temperature: 28, condition: 多云 }, 深圳: { temperature: 30, condition: 晴朗 } }; const data weatherData[city] || { temperature: 20, condition: 未知 }; let temperature data.temperature; if (unit fahrenheit) { temperature (temperature * 9/5) 32; } return { content: [ { type: text, text: 城市: ${city}\n温度: ${temperature}°${unit celsius ? C : F}\n天气状况: ${data.condition} } ] }; } throw new Error(未知工具: ${request.params.name}); }); // 启动服务器 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Demo Weather MCP Server 已启动); } main().catch(console.error);这个示例 Server 提供了一个get_weather工具接收城市名和温度单位参数返回模拟的天气数据。4.2 配置并连接天气 Server更新客户端配置添加对天气 Server 的支持// 在 mcp-client.json 中添加 { mcpServers: { demo-weather: { command: node, args: [./demo-weather-server.js] } } }然后创建测试脚本test-weather.jsconst MCPClient require(./MCPClient.js); async function testWeatherQuery() { const client new MCPClient({ command: node, args: [./demo-weather-server.js] }); try { // 连接服务器 await client.connect(); // 调用天气查询工具 const result await client.callTool(get_weather, { city: 北京, unit: celsius }); console.log(天气查询结果:); console.log(result); } catch (error) { console.error(测试失败:, error); } finally { // 清理资源 await client.disconnect(); } } testWeatherQuery();运行这个测试脚本你应该能看到类似以下的输出MCP Server 连接成功 发现 1 个可用工具 发现 0 个可用资源 天气查询结果: 城市: 北京 温度: 25°C 天气状况: 晴朗4.3 处理复杂参数和错误情况实际项目中的工具调用往往涉及更复杂的参数验证和错误处理。让我们扩展天气查询工具添加更健壮的错误处理// 在 MCPClient 类中添加参数验证方法 validateArguments(toolName, providedArgs) { const tool this.tools.get(toolName); if (!tool || !tool.inputSchema) { return; // 无模式定义跳过验证 } const schema tool.inputSchema; const errors []; // 检查必需参数 if (schema.required) { for (const requiredParam of schema.required) { if (providedArgs[requiredParam] undefined) { errors.push(缺少必需参数: ${requiredParam}); } } } // 检查参数类型和枚举值 if (schema.properties) { for (const [paramName, paramValue] of Object.entries(providedArgs)) { const paramSchema schema.properties[paramName]; if (!paramSchema) { errors.push(未知参数: ${paramName}); continue; } // 类型检查 if (paramSchema.type typeof paramValue ! paramSchema.type) { errors.push(参数 ${paramName} 应为 ${paramSchema.type} 类型); } // 枚举值检查 if (paramSchema.enum !paramSchema.enum.includes(paramValue)) { errors.push(参数 ${paramName} 的值必须在: ${paramSchema.enum.join(, )} 中); } } } if (errors.length 0) { throw new Error(参数验证失败:\n${errors.join(\n)}); } } // 更新 callTool 方法在调用前验证参数 async callTool(name, arguments) { if (!this.tools.has(name)) { throw new Error(工具 ${name} 不存在); } // 参数验证 this.validateArguments(name, arguments); // 其余调用逻辑保持不变... }这样当调用工具时如果参数不符合要求客户端会在调用前就给出明确的错误信息而不是等到 Server 返回错误。5. 常见问题排查与解决方案在实际使用 MCP Client 时会遇到各种问题。以下是常见问题的排查指南。5.1 连接失败问题排查连接失败是最常见的问题通常有以下几种原因问题现象可能原因检查方式解决方案命令不存在错误命令路径错误或未安装检查 command 和 args 配置使用绝对路径或确保命令在 PATH 中权限被拒绝执行权限不足检查文件权限添加执行权限或使用有权限的用户连接超时Server 启动慢或卡住查看 Server 日志增加超时时间或检查 Server 代码协议版本不匹配Client 和 Server 版本不兼容检查双方版本使用兼容的版本或更新 SDK排查连接问题的基本命令# 检查命令是否可用 which node node --version # 检查文件权限 ls -la demo-weather-server.js chmod x demo-weather-server.js # 直接测试 Server 启动 node demo-weather-server.js5.2 工具调用失败排查工具调用失败通常与参数或网络相关// 添加详细的调试信息 async callToolWithDebug(name, arguments) { console.log(调用工具: ${name}); console.log(参数:, JSON.stringify(arguments, null, 2)); try { const toolInfo this.tools.get(name); console.log(工具定义:, JSON.stringify(toolInfo, null, 2)); const result await this.callTool(name, arguments); console.log(调用成功:, result); return result; } catch (error) { console.error(调用详细错误:, error); // 检查传输层状态 console.log(传输层状态:, this.transport ? 已连接 : 未连接); throw error; } }5.3 性能问题优化当处理大量工具调用时需要考虑性能优化class OptimizedMCPClient extends MCPClient { constructor(serverConfig) { super(serverConfig); this.pendingRequests new Map(); this.requestTimeout 30000; // 30秒超时 } async callToolWithTimeout(name, arguments, timeoutMs this.requestTimeout) { return Promise.race([ this.callTool(name, arguments), new Promise((_, reject) setTimeout(() reject(new Error(请求超时)), timeoutMs) ) ]); } // 批量调用工具 async callToolsInBatch(toolCalls) { const results []; const batchSize 5; // 控制并发数 for (let i 0; i toolCalls.length; i batchSize) { const batch toolCalls.slice(i, i batchSize); const batchResults await Promise.all( batch.map(({name, args}) this.callTool(name, args)) ); results.push(...batchResults); // 添加延迟避免过度负载 if (i batchSize toolCalls.length) { await new Promise(resolve setTimeout(resolve, 100)); } } return results; } }6. 生产环境最佳实践将 MCP Client 用于生产环境时需要考虑更多因素。6.1 安全考虑MCP Client 可能处理敏感数据需要确保安全性class SecureMCPClient extends MCPClient { constructor(serverConfig) { super(serverConfig); this.sensitiveParams new Set([api_key, password, token]); } // 过滤日志中的敏感参数 logSafeCall(name, arguments) { const safeArgs {...arguments}; for (const key of Object.keys(safeArgs)) { if (this.sensitiveParams.has(key.toLowerCase())) { safeArgs[key] ***; } } console.log(调用工具: ${name}, safeArgs); } // 验证 Server 身份简化示例 async verifyServerIdentity() { // 生产环境中应该验证 Server 的证书或签名 // 这里只是示例框架 const serverInfo await this.client.getServerInfo(); if (!this.trustedServers.includes(serverInfo.name)) { throw new Error(未受信任的 Server: ${serverInfo.name}); } } }6.2 监控与日志完善的监控和日志对生产系统至关重要// 生产环境日志配置 const winston require(winston); const logger winston.createLogger({ level: info, format: winston.format.json(), transports: [ new winston.transports.File({ filename: mcp-client-error.log, level: error }), new winston.transports.File({ filename: mcp-client-combined.log }) ] }); class ProductionMCPClient extends MCPClient { async callTool(name, arguments) { const startTime Date.now(); try { logger.info(tool_call_start, { tool: name, arguments }); const result await super.callTool(name, arguments); const duration Date.now() - startTime; logger.info(tool_call_success, { tool: name, duration, resultSize: JSON.stringify(result).length }); return result; } catch (error) { const duration Date.now() - startTime; logger.error(tool_call_failed, { tool: name, duration, error: error.message }); throw error; } } }6.3 配置管理生产环境的配置应该外部化支持不同环境// config/production.json { mcpServers: { weather-service: { command: node, args: [/app/servers/weather-prod.js], env: { NODE_ENV: production, API_KEY: ${WEATHER_API_KEY}, LOG_LEVEL: info } } }, timeouts: { connection: 10000, toolCall: 30000 }, retry: { maxAttempts: 3, backoffMs: 1000 } } // 配置加载和变量替换 const config require(./config/production.json); const expandedConfig this.expandEnvVariables(config); expandEnvVariables(config) { const jsonString JSON.stringify(config); const expanded jsonString.replace(/\$\{(\w)\}/g, (match, p1) { return process.env[p1] || match; }); return JSON.parse(expanded); }6.4 健康检查与熔断确保系统的稳定性需要健康检查机制class ResilientMCPClient extends MCPClient { constructor(serverConfig) { super(serverConfig); this.healthStatus unknown; this.failureCount 0; this.circuitBreakerThreshold 5; } async healthCheck() { try { await this.client.listTools(); // 简单的健康检查 this.healthStatus healthy; this.failureCount 0; return true; } catch (error) { this.failureCount; if (this.failureCount this.circuitBreakerThreshold) { this.healthStatus unhealthy; } return false; } } async callTool(name, arguments) { if (this.healthStatus unhealthy) { throw new Error(服务暂时不可用请稍后重试); } try { return await super.callTool(name, arguments); } catch (error) { await this.healthCheck(); // 失败时更新健康状态 throw error; } } }通过本文的实践你应该已经掌握了 MCP Client 的核心开发技能。从基础连接到生产级部署每个环节都需要仔细考虑错误处理、性能优化和安全性。在实际项目中建议先从简单的工具调用开始逐步扩展到复杂的业务场景同时建立完善的监控和告警机制。