ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MCP协议实战指南:构建标准化AI Agent工具调用系统

MCP协议实战指南:构建标准化AI Agent工具调用系统 1. 从“玩具”到“生产力”为什么我们需要MCP协议如果你最近在折腾AI Agent尤其是那些能调用外部工具、帮你查天气、订机票、写代码的智能体那你大概率已经遇到了一个核心瓶颈工具连接。你可能会用LangChain的Toolkit或者直接调用某个API但很快就会发现当你想让Agent同时连接数据库、搜索引擎、代码执行环境和内部业务系统时事情变得一团糟。每个工具都有不同的认证方式、输入输出格式、错误处理逻辑你写的胶水代码越来越多Agent的核心逻辑反而被淹没在繁琐的集成细节里。这就像你想造一辆能适应各种地形的全能车结果大部分时间都在为不同的轮胎、不同的发动机接口而头疼。MCPModel Context Protocol协议的出现就是为了解决这个“接口标准化”的问题。它不是一个具体的工具或框架而是一套通信协议和规范旨在为AI模型特别是大型语言模型提供一个统一、标准化的方式来发现、描述和调用外部工具或称为“资源”。简单来说MCP想让AI模型像我们使用USB接口一样使用外部能力插上就能识别驱动自动安装即插即用。这篇指南就是带你从零开始亲手搭建一个基于MCP协议的AI Agent让它能稳定、可靠地连接并使用外部工具。无论你是想做一个个人效率助手还是为企业构建一个复杂的自动化流程理解并实践MCP都能让你从“拼接怪”式的开发升级到“架构师”式的设计。2. 拆解MCP协议核心三要素与工作流全景在动手写代码之前我们必须先理解MCP协议到底规定了什么。它不是魔法而是一套清晰的“游戏规则”。我们可以将其核心拆解为三个角色和它们之间的交互流程。2.1 核心角色定义一个完整的MCP生态涉及三个关键角色客户端Client通常是AI应用或Agent本身。它负责发起请求是工具的“使用者”。在我们的场景中这就是我们构建的AI Agent大脑。服务器Server工具或资源的提供者。它封装了具体的功能实现比如数据库查询、代码执行、调用第三方API等。一个Server可以提供一个或多个工具。协议Protocol定义Client和Server之间通信的语言JSON-RPC over stdio/HTTP/SSE和消息格式。这是MCP协议本身。2.2 标准工作流一次完整的工具调用是如何发生的理解角色后我们来看一次标准的交互流程。这就像一次精心编排的对话初始化与握手InitializeClient启动连接到Server。双方交换初始化信息包括各自的能力声明。Server会告诉Client“我这里有哪些工具可用。”工具列表获取ListToolsClient向Server请求可用的工具列表。Server返回一个数组其中每个工具都有唯一的name、清晰的description以及详细的inputSchemaJSON Schema格式。这个description至关重要它是LLM决定是否及如何调用该工具的主要依据。工具调用CallToolLLM在Client内部根据用户请求和上下文决定调用哪个工具并生成符合inputSchema的参数。Client将这个调用请求发送给Server。执行与返回ResultServer执行实际的操作如查询数据库、调用API然后将结果以结构化文本、图片、JSON等或非结构化的形式返回给Client。结果中还可以包含isError标志来指示调用是否成功。资源推送可选Resources除了被动的工具调用MCP还支持Server主动向Client推送“资源”如动态更新的文档、实时日志流。Client可以订阅Subscribe这些资源Server会在资源变化时通知NotifyClient。这个流程的核心优势在于解耦。Client你的Agent不需要知道工具是用Python、Go还是Rust写的也不需要关心它部署在本地还是云端。它只需要按照协议发送JSON-RPC消息。同样Server开发者可以专注于实现业务逻辑而无需为每个AI框架做适配。注意MCP协议目前有多种传输方式最常用的是stdio标准输入输出适用于本地进程和HTTP适用于远程服务。对于初学者和大多数集成场景从stdio开始是最简单直接的选择。3. 实战第一步构建你的第一个MCP服务器工具提供方理论清晰后我们开始动手。我们将使用官方推荐的TypeScript/JavaScript SDK来构建一个Server因为它生态成熟文档丰富。我们将创建一个提供“天气查询”和“计算器”两个简单工具的Server。3.1 环境准备与项目初始化首先确保你的环境有Node.js建议18和npm。然后创建一个新项目并安装核心依赖。# 创建一个新的项目目录 mkdir my-mcp-server cd my-mcp-server # 初始化项目 npm init -y # 安装MCP服务器SDK和类型定义 npm install modelcontextprotocol/sdk npm install --save-dev typescript types/node # 初始化TypeScript配置 npx tsc --init修改生成的tsconfig.json确保设置正确例如{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true }, include: [src/**/*], exclude: [node_modules] }3.2 实现核心服务器逻辑在src目录下创建index.ts开始编写Server。import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema, } from modelcontextprotocol/sdk/types.js; // 1. 创建Server实例 const server new Server( { name: my-first-mcp-server, version: 0.1.0, }, { capabilities: { tools: {}, // 声明本服务器提供工具能力 }, } ); // 2. 定义工具列表 const tools [ { name: get_weather, description: 获取指定城市的当前天气情况。, inputSchema: { type: object, properties: { city: { type: string, description: 城市名称例如Beijing, Shanghai, New York, }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位默认为摄氏度celsius, default: celsius, }, }, required: [city], }, }, { name: calculate, description: 执行简单的数学计算。, inputSchema: { type: object, properties: { expression: { type: string, description: 数学表达式例如(3 4) * 2 / 5。支持加减乘除和括号。, }, }, required: [expression], }, }, ]; // 3. 处理ListTools请求当Client询问有什么工具时返回这个列表 server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: tools, }; }); // 4. 处理CallTool请求当Client调用具体工具时执行相应逻辑 server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; if (name get_weather) { // 模拟天气查询逻辑 const city (args as any).city; const unit (args as any).unit || celsius; // 在实际应用中这里会调用真实的天气API const temp Math.floor(Math.random() * 30) 10; // 模拟温度 const conditions [晴朗, 多云, 小雨, 阴天]; const condition conditions[Math.floor(Math.random() * conditions.length)]; let displayTemp temp; if (unit fahrenheit) { displayTemp Math.round((temp * 9) / 5 32); } return { content: [ { type: text, text: 城市【${city}】的当前天气为${condition}温度 ${displayTemp}°${unit celsius ? C : F}。, }, ], }; } else if (name calculate) { // 执行计算 const expression (args as any).expression; try { // 警告在生产环境中直接使用eval是极其危险的容易导致代码注入 // 这里仅用于演示。实际应用应使用安全的数学表达式解析库如math.js const result eval(expression); return { content: [ { type: text, text: 计算表达式【${expression}】的结果是${result}, }, ], }; } catch (error) { return { content: [ { type: text, text: 计算表达式【${expression}】时出错${error}, }, ], isError: true, }; } } // 如果工具名未找到 return { content: [ { type: text, text: 未知工具${name}, }, ], isError: true, }; }); // 5. 启动服务器使用stdio传输 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP Server (my-first-mcp-server) 已启动并等待连接...); } main().catch((error) { console.error(服务器启动失败:, error); process.exit(1); });代码解读与避坑点工具描述description是灵魂get_weather工具的description字段写得非常具体。LLM会阅读这个描述来决定是否调用它。模糊的描述会导致LLM错误调用或忽略该工具。输入模式inputSchema是契约它严格定义了Client必须传入的参数格式和类型。使用JSON Schema可以确保类型安全并给LLM清晰的提示。安全警告计算器工具中使用了eval这在实际项目中是绝对禁止的因为它会执行任意字符串代码带来严重的安全风险。此处仅作最简单演示。真实场景务必使用像math.js或expr-eval这样的安全库来解析数学表达式。错误处理在catch块和未知工具返回中我们都设置了isError: true。这有助于Client和背后的LLM识别调用失败从而可能尝试其他策略或向用户报错。3.3 构建与运行编译并运行这个服务器。# 编译TypeScript npx tsc # 运行服务器 node dist/index.js运行后程序会阻塞等待Client通过标准输入输出进行连接。这意味着我们的Server已经就绪。4. 实战第二步构建MCP客户端AI Agent大脑现在我们有了工具提供方Server需要一个使用者Client。我们将构建一个简单的命令行AI Agent它使用OpenAI的GPT模型作为大脑并通过MCP协议调用我们刚写的Server里的工具。4.1 客户端项目初始化新建一个客户端项目目录。mkdir my-mcp-client cd my-mcp-client npm init -y npm install modelcontextprotocol/sdk openai dotenv npm install --save-dev typescript types/node npx tsc --init # 同样配置好tsconfig.json创建.env文件存放你的OpenAI API密钥OPENAI_API_KEYsk-your-api-key-here4.2 实现客户端与工具调用逻辑在src目录下创建index.ts。import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; import OpenAI from openai; import * as path from path; import * as child_process from child_process; import * as fs from fs; import dotenv from dotenv; dotenv.config(); async function main() { // 1. 启动MCP Server进程 const serverPath path.resolve(__dirname, ../../my-mcp-server/dist/index.js); // 这里假设server项目在相邻目录请根据实际情况调整路径 if (!fs.existsSync(serverPath)) { console.error(未找到MCP Server可执行文件: ${serverPath}); console.error(请确保已编译并构建了您的MCP服务器项目。); process.exit(1); } const serverProcess child_process.spawn(node, [serverPath], { stdio: [pipe, pipe, inherit], // 将server的stderr继承到当前控制台便于调试 }); // 2. 创建MCP Client并连接 const client new Client( { name: my-mcp-agent, version: 0.1.0, }, { capabilities: {}, } ); const transport new StdioClientTransport({ command: node, args: [serverPath], }); await client.connect(transport); console.log(MCP Client 已连接至服务器。); // 3. 获取服务器提供的工具列表 const toolsResponse await client.listTools(); const availableTools toolsResponse.tools; console.log(从服务器获取到 ${availableTools.length} 个工具, availableTools.map(t t.name)); // 4. 初始化OpenAI客户端 const openai new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); // 5. 为LLM构造工具调用格式的函数定义 const toolDefinitionsForLLM availableTools.map(tool ({ type: function as const, function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, })); // 6. 主对话循环 const readline require(readline).createInterface({ input: process.stdin, output: process.stdout }); const question (prompt: string): Promisestring { return new Promise((resolve) { readline.question(prompt, resolve); }); }; console.log(\n AI助手已就绪集成MCP工具); console.log(你可以询问天气或让我计算。输入 exit 退出。\n); const conversationHistory: ArrayOpenAI.ChatCompletionMessageParam []; while (true) { const userInput await question(\n你: ); if (userInput.toLowerCase() exit) { break; } conversationHistory.push({ role: user, content: userInput }); try { // 调用OpenAI并告知它可用的工具 const completion await openai.chat.completions.create({ model: gpt-4o-mini, // 或使用 gpt-3.5-turbo messages: [ { role: system, content: 你是一个有帮助的助手可以调用工具来获取天气或进行计算。请根据用户需求决定是否调用工具。如果调用请严格遵循工具的参数格式要求。 }, ...conversationHistory, ], tools: toolDefinitionsForLLM, tool_choice: auto, // 让模型自行决定是否调用工具 }); const responseMessage completion.choices[0].message; conversationHistory.push(responseMessage); // 检查LLM是否想要调用工具 const toolCalls responseMessage.tool_calls; let finalResponseText responseMessage.content || ; if (toolCalls toolCalls.length 0) { console.log(助手决定调用工具: ${toolCalls.map(tc tc.function.name).join(, )}); for (const toolCall of toolCalls) { const toolName toolCall.function.name; const toolArgs JSON.parse(toolCall.function.arguments); // 通过MCP Client实际调用工具 const toolResult await client.callTool({ name: toolName, arguments: toolArgs, }); const resultContent toolResult.content?.[0]?.text || 工具未返回文本结果; const isError toolResult.isError; // 将工具调用结果作为新的消息追加到历史让LLM进行总结或下一步决策 conversationHistory.push({ role: tool, tool_call_id: toolCall.id, content: isError ? 工具调用出错: ${resultContent} : resultContent, }); // 如果是错误可能直接输出错误信息 if (isError) { finalResponseText 调用工具【${toolName}】时出错${resultContent}; } else { // 如果不错误我们可能需要LLM根据工具结果生成最终回复 // 这里简化处理直接展示结果 finalResponseText 工具【${toolName}】返回结果${resultContent}; } } // 可选如果希望LLM基于工具结果生成更自然的回复可以在这里再进行一次API调用 // 但为了简化演示我们直接输出工具结果 } console.log(助手: ${finalResponseText}); } catch (error) { console.error(处理请求时发生错误:, error); } } readline.close(); await client.close(); serverProcess.kill(); console.log(会话结束。); } main().catch(console.error);关键实现解析进程管理客户端通过child_process.spawn启动并管理Server进程通过stdio管道进行通信。这是一种常见的本地集成模式。动态工具发现客户端在启动后首先调用client.listTools()从Server获取最新的工具列表和它们的schema。这意味着你更新Server工具后Client无需修改代码即可感知。OpenAI Function Calling 适配我们将MCP工具的描述完美转换成了OpenAI Function Calling所需的格式。这是连接MCP协议与主流LLM的关键桥梁。对话管理我们维护了一个conversationHistory数组包含了用户消息、LLM回复以及工具调用结果。将工具结果以role: tool的消息格式放回历史是让LLM理解上下文并生成最终回答的标准做法。4.3 运行你的AI Agent确保你的MCP Server项目已经编译dist/index.js存在并且Client项目中的路径配置正确。然后在Client目录下运行npx tsc node dist/index.js现在你可以尝试对话你: 上海天气怎么样 助手决定调用工具: get_weather 助手: 工具【get_weather】返回结果城市【上海】的当前天气为多云温度 22°C。 你: 帮我算一下(1527)*3等于多少 助手决定调用工具: calculate 助手: 工具【calculate】返回结果计算表达式【(1527)*3】的结果是126恭喜你已经成功构建了一个基于MCP协议、能动态发现并调用外部工具的AI Agent原型。5. 进阶生产环境部署与架构考量上面的例子是一个本地一体化的演示。但在生产环境中Client、Server和LLM服务往往是分离部署的。下面我们来探讨更实际的架构。5.1 服务器部署模式HTTP Transport对于远程工具服务我们需要使用HTTP传输模式。这需要修改我们的Server。首先安装HTTP传输层依赖cd my-mcp-server npm install modelcontextprotocol/sdk/server/http.js然后修改或新建一个HTTP服务器文件如src/server-http.tsimport { Server } from modelcontextprotocol/sdk/server/index.js; import { HTTPServerTransport } from modelcontextprotocol/sdk/server/http.js; import express from express; // ... 工具定义和请求处理逻辑与之前相同 ... async function main() { const app express(); app.use(express.json()); const server new Server(...); // 初始化Server同上 // 设置请求处理器 server.setRequestHandler(ListToolsRequestSchema, ...); server.setRequestHandler(CallToolRequestSchema, ...); // 创建HTTP Transport并绑定到Express路由 const transport new HTTPServerTransport(app, /mcp); await server.connect(transport); const port process.env.PORT || 3000; app.listen(port, () { console.log(MCP HTTP Server 运行在 http://localhost:${port}/mcp); }); } main();这样你的工具服务器就暴露了一个HTTP端点例如http://your-server:3000/mcp。任何兼容MCP协议的Client都可以通过HTTP连接到它。5.2 客户端连接远程服务器相应地客户端也需要改为使用HTTP连接。// 在客户端项目中 import { Client } from modelcontextprotocol/sdk/client/index.js; import { HTTPClientTransport } from modelcontextprotocol/sdk/client/http.js; async function connectToRemoteServer() { const client new Client(...); const transport new HTTPClientTransport(new URL(http://your-server:3000/mcp)); await client.connect(transport); // ... 后续工具调用逻辑不变 }5.3 多服务器管理与工具编排一个强大的Agent往往需要连接多个工具服务器。MCP Client可以同时连接多个Server。你需要管理多个Client实例并在向LLM提供工具定义时合并来自所有服务器的工具列表同时注意处理工具名冲突建议在Server层面确保工具名全局唯一或添加命名空间前缀。更复杂的场景下你可能需要一个工具路由层或编排引擎。这个层负责负载均衡当多个Server提供相同功能的工具时如不同的天气API根据成本、延迟、可用性进行选择。权限与鉴权管理不同工具对不同用户或请求的访问权限。组合工具将多个工具调用串联起来完成复杂任务如“查天气然后根据天气推荐穿衣”。Fallback策略当主工具调用失败时自动尝试备用工具。这超出了基础MCP协议的范围通常需要在你的AI应用框架如LangChain, LlamaIndex或自定义的Agent逻辑中实现。5.4 安全性、错误处理与监控在生产环境中以下几点至关重要输入验证与净化Server端必须对Client传入的arguments进行严格的验证防止注入攻击。即使有JSON Schema也要在业务逻辑层再次检查。认证与授权HTTP模式下必须实施API密钥、OAuth等认证机制。MCP协议本身不规定认证方式这需要在传输层如HTTPS 头部令牌或应用层实现。限流与配额为工具调用设置速率限制和调用配额防止滥用。全面的错误处理Client端需要处理网络超时、Server无响应、返回格式错误等各种异常给出友好的用户提示或重试策略。日志与监控记录所有工具调用的请求、响应、耗时和错误。这对于调试、优化和成本核算必不可少。考虑使用结构化日志如JSON格式并输出到集中式日志系统。成本控制特别是调用付费API的工具如发送短信、生成图像需要在Server或路由层实施预算控制。6. 生态与工具链加速开发的利器手动编写Server和Client虽然有助于理解原理但对于快速开发利用现有生态工具效率更高。官方与社区Server已经有很多现成的MCP Server实现可以直接使用或作为参考。文件系统提供读写本地文件的能力。Git提供Git仓库的查询和操作。SQL数据库连接并查询MySQL、PostgreSQL等。搜索引擎连接Brave Search、Google Programmable Search等。你可以在MCP协议的GitHub仓库或社区中找到更多。Server开发框架除了TypeScript SDK也有Python、Rust等语言的SDK方便你用熟悉的语言开发工具。Client集成框架Claude Desktop / Anthropic APIAnthropic官方大力推广MCP其Claude桌面应用直接支持通过MCP协议加载本地工具。Cursor IDE一些先进的AI编程IDE也开始内置MCP Client允许AI助手直接调用你配置的工具。LangChain / LlamaIndex这些流行的AI应用框架正在逐步增加对MCP的原生支持你可以用几行代码就将MCP工具接入到现有的Chain或Agent中。调试与测试工具像mcp-cli这样的命令行工具可以让你手动测试MCP Server发送ListTools和CallTool请求而无需编写完整的Client极大方便了Server的开发和调试。拥抱这些工具链能让你从协议细节中解放出来更专注于设计和实现有价值的工具本身。7. 踩坑实录从开发到部署的常见问题在实际项目中我遇到了不少坑这里分享几个典型的问题一工具描述description写得太差导致LLM从不调用或错误调用。现象你写了一个完美的数据库查询工具但LLM总是忽略它或者用错误的参数调用。根因LLM完全依赖description和inputSchema来理解工具。模糊的描述如“查询数据”毫无用处。解决方案描述要具体、包含关键词、说明使用场景和限制。例如“根据用户ID查询其在订单表中的最近10条订单记录返回订单号、日期、金额和状态。用户ID必须是数字。”同时inputSchema中的参数描述也要详尽。问题二Server进程僵尸或资源泄漏。现象Client异常退出后Server进程没有正确关闭占用系统资源。根因Client端没有正确处理断开连接和清理进程的逻辑。解决方案在Client代码中监听SIGINT、SIGTERM等退出信号确保在退出前调用client.close()并killServer进程。使用child_process时考虑使用p-kill等库来确保进程树被彻底清理。问题三工具调用超时或阻塞主线程。现象某个工具如一个慢速网络请求执行时间很长导致整个Agent响应卡住。根因Server同步执行耗时操作阻塞了请求处理循环。解决方案Server端所有工具处理函数都必须是async的。对于可能耗时的操作要设置合理的超时例如使用Promise.race或AbortController并及时向Client返回超时错误避免无限期等待。问题四多工具并发调用时的状态冲突。现象Agent同时调用“写入文件”和“读取文件”工具导致读取到不完整的数据。根因工具Server是无状态的但工具操作的外部资源如文件、数据库存在状态竞争。解决方案这需要在业务逻辑层面解决。可以为相关工具组设计锁机制如使用文件锁、数据库事务或者在工具描述中明确说明其非幂等性和潜在冲突让LLM或上层的编排逻辑进行顺序调度。构建基于MCP的AI Agent协议本身只是解决了“连接”的问题。真正的挑战在于如何设计好用、安全、可靠的工具以及如何让LLM智能、高效地使用这些工具。这需要你同时具备后端开发、API设计以及对LLM能力边界和思维模式的深刻理解。从这个小原型出发不断迭代你的工具集和Agent逻辑你就能打造出真正强大的AI应用。
返回列表