远程 MCP 实战:让 AI 无缝调用地图、浏览器与文件系统
从零搭建一个能查酒店、开网页、写文件的 Agent附详细执行流程图解代码可复现你有没有遇到过这种场景想让 AI 帮我查一下北京南站附近的酒店然后把每个酒店的图片在浏览器里打开还要自动把标签页标题改成酒店名——结果你发现大模型再强也拿不到地图数据点不了浏览器更写不了文件。说白了大模型缺的不是“脑子”而是“手脚”。而 MCPModel Context Protocol就是那个让 AI 长出手脚的标准化协议。今天我们就从零开始用一个实际案例把多个远程 MCP Server 串联起来让你的 Agent 同时具备地图查询、浏览器控制和文件读写能力。本文所有代码都基于 LangChain 的mcp-adapters并会深入剖析每一步的通信与执行流程配合图解让你一次搞懂。一、MCP 的本质Tool 的“超级包装”如果你用过 Function Calling那理解 MCP 就毫无门槛。MCP 本质上还是 Tool只不过它给 Tool 包了一层进程通过stdio或HTTPSSE来访问。两种通信模式图解本地 stdioAgent 进程作为父进程启动一个子进程如node server.mjs通过子进程的 stdin/stdout 发送 JSON-RPC 消息。适合本地工具文件系统、浏览器、自定义脚本。远程 SSEAgent 通过 HTTP 向远程服务器发起 SSE 连接服务端可以主动推送消息适合云端服务如地图、数据库。最大的好处是解耦 复用任何人都可以按 MCP 协议开发一个 Server然后全世界的 Agent 都能直接复用。就像高德地图开放了 MCP Server你无需写任何 HTTP 请求封装直接配置一个 URL 就能让 AI 调用地图能力。MCP 让 Tool 从“函数”升级为“服务”一次编写到处 Agent。**二、场景驱动一个真实的“AI 工作流”我们要实现的需求是这样的用户输入“北京南站附近的酒店最近的 3 个酒店拿到酒店图片打开浏览器展示每个酒店的图片每个 tab 一个 url 展示并且在把那个页面标题改为酒店名”这个任务天然需要三种能力地理位置查询→ 高德地图 MCP远程 SSE浏览器自动化→ Chrome DevTools MCP本地 npx文件系统备用→ FileSystem MCP本地 npx同时为了演示自定义 MCP我们还会再挂一个本地自己写的my-mcp-server比如做日志或额外计算。你看一个 Agent 同时挂载 4 个 MCP Server每个 Server 提供多个 Tool整个工具集瞬间变得异常丰富。三、环境准备与依赖首先创建一个 Node.js 项目安装必要依赖npm init -y npm install langchain/mcp-adapters langchain/openai dotenv chalk因为 Chrome DevTools MCP 和 FileSystem MCP 都通过npx调用无需额外安装包运行时自动下载。确保你有 DeepSeek 或 OpenAI 的 API Key本文使用 DeepSeek兼容 OpenAI 接口。四、核心流程拆解一MultiServerMCPClient 的初始化与工具加载直接上核心代码然后逐步分析内部的执行流程。import dotenv/config; import { MultiServerMCPClient } from langchain/mcp-adapters; import { ChatOpenAI } from langchain/openai; import chalk from chalk; import { HumanMessage, SystemMessage, ToolMessage } from langchain/core/messages; const model new ChatOpenAI({ modelName: deepseek-v4-flash, apiKey: process.env.DEEPSEEK_API_KEY, configuration: { baseURL: https://api.deepseek.com/v1, } }); const mcpClient new MultiServerMCPClient({ mcpServers: { // 1. 高德地图 MCP远程 SSE amap-server: { url: https://mcp.amap.com/sse?keybaec4e904d7460f61e4c85a571e793de }, // 2. 自定义本地 MCPstdio my-mcp-server: { transport: stdio, command: node, args: [ E:\workspace\lgl_ai\ai\agent_in_action\mcp-demo\src\my-mcp-server.mjs ] }, // 3. Chrome DevTools MCP chrome-devtools: { command: npx, args: [-y, chrome-devtools-mcplatest] }, // 4. FileSystem MCP filesystem: { command: npx, args: [ -y, modelcontextprotocol/server-filesystem, E:\workspace\lgl_ai\ai\agent_in_action\remote-mcp // 允许操作的根目录 ] } } }); const tools await mcpClient.getTools();初始化流程分析当执行new MultiServerMCPClient(config)时内部会依次对每个配置项做如下操作多 Server 并行初始化远程 SSEMultiServerMCPClient内部使用EventSource连接到url按照 MCP 协议进行握手获取服务端声明的工具列表工具名、参数 JSON Schema 等。本地 stdio对于每个command argsspawn一个子进程然后通过子进程的 stdin 发送initialize请求从 stdout 读取响应解析出工具列表。所有工具被包装成DynamicStructuredTool实例并统一放入一个数组返回给调用方。注意mcp-adapters会自动给工具名加上 Server 名前缀如amap-server__search_place避免冲突。五、Agent 循环多轮对话与工具调用的完整流程拿到 Tools 后我们绑定到模型然后实现一个支持多轮工具调用的 Agent 循环。const modelWithTools model.bindTools(tools); async function runAgentWithTools(query, maxIterations 30) { const messages [new HumanMessage({ content: query })]; for (let i 0; i maxIterations; i) { console.log(chalk.bgGreen(第${i 1}轮迭代)); const response await modelWithTools.invoke(messages); messages.push(response); // 没有工具调用直接返回最终回答 if (!response.tool_calls || response.tool_calls.length 0) { console.log(chalk.green(AI 回答: ${response.content})); return response.content; } console.log(chalk.bgBlue(工具调用: ${response.tool_calls.map(t t.name).join(\n)})); // 执行每个工具调用 for (const toolCall of response.tool_calls) { const foundTool tools.find(t t.name toolCall.name); if (foundTool) { let contentStr; try { const toolResult await foundTool.invoke(toolCall.args); // 处理不同格式的返回值 if (typeof toolResult string) { contentStr toolResult; } else if (toolResult toolResult.result) { contentStr toolResult.result; } else { contentStr JSON.stringify(toolResult); } } catch (err) { contentStr 工具调用失败: ${err.message}; console.error(chalk.red(工具 ${toolCall.name} 调用出错: ${err.message})); } messages.push(new ToolMessage({ content: contentStr, tool_call_id: toolCall.id })); } } } // 达到最大迭代次数返回最后一条消息 return messages[messages.length - 1].content; }多轮迭代的完整交互图下面的时序图展示了从用户输入到最终输出的全链路包含模型调用、工具执行和消息回填关键点每一轮模型都会收到完整的消息历史包含之前的工具调用结果因此可以基于结果进行下一步推理。工具调用可以是串行如我们的代码也可以是并行如果工具间无依赖可用Promise.all加速。错误处理单个工具失败后返回错误信息给模型模型可以决定是否重试或换方案增强了鲁棒性。为什么需要这几种判断typeof toolResult string很多 MCP Server如 FileSystem 的read_file直接返回文件内容的字符串。模型期望ToolMessage.content是文本所以直接使用即可。toolResult toolResult.resultChrome DevTools MCP 和高德地图 MCP 在实现时有些工具会返回一个对象其中包含一个result字段可能是结构化的 JSON 字符串或对象。例如高德的search_places可能返回{ result: { pois: [...] } }。我们抽取result字段避免把无用的元数据塞给模型。else { JSON.stringify(toolResult) }兜底方案如果返回值是普通对象没有result字段我们就将其序列化为 JSON 字符串。这是最保险的做法保证模型总能拿到文本形式的可读数据。金句工具返回值解析本质是在“保真”和“保读”之间做平衡——既要保留关键信息又要让大模型一眼看懂。不同 MCP Server 的返回值差异一览MCP Server示例工具返回值类型是否包含result字段高德地图 (SSE)search_places{ result: {...} }或直接{ pois: [...] }多数有但不保证Chrome DevToolsnew_tab{ result: { tabId: 123 } }有FileSystemread_file文件内容的string无自定义 MCP自定义任意类型取决于实现所以我们的解析逻辑必须覆盖以上所有情况。参数解析与校验在工具调用时toolCall.args已经是模型根据工具定义的 JSON Schema 生成的结构化对象无需额外解析。但有一处细节需要注意模型可能漏填必填参数虽然大模型通常很聪明但偶尔会出错。可以在foundTool.invoke之前添加参数校验或者依赖 LangChain 自带的zod校验。mcp-adapters已经将 MCP 工具的inputSchema转换成 Zod 对象invoke时会自动校验非法参数会直接抛出异常从而进入catch分支。因此我们的错误处理catch已经覆盖了参数校验失败的情况并返回友好的错误信息给模型模型可以在下一轮自行修正。5.3 完整循环中的消息结构示例为了让你更直观地理解整个对话流程下面展示一次成功任务的实际消息历史简化版第 1 轮User: “北京南站附近的酒店最近的3个酒店拿到酒店图片打开浏览器展示……”Assistant (tool_calls):[ { name: amap-server__search_places, args: { keywords: 酒店, location: 116.385,39.905, ... } } ]Tool (amap-server__search_places):{pois:[{name:如家,photos:[url1]}, ...]}第 2 轮模型看到工具结果后Assistant (tool_calls):[ { name: chrome-devtools__new_tab, args: {} }, { name: chrome-devtools__navigate_to, args: { url: url1 } }, ... ]Tool (chrome-devtools__new_tab):{result:{tabId:3}}Tool (chrome-devtools__navigate_to):{result:{success:true}}第 3 轮Assistant (no tool_calls): “已成功打开3个标签页每个显示对应酒店的图片标题也已修改完成。”你会发现每一步工具的返回值都被转化为字符串大模型才能“读得懂”。六、运行与效果完整任务执行流程拆解执行await runAgentWithTools(北京南站附近的酒店最近的 3 个酒店 拿到酒店图片打开浏览器展示每个酒店的图片每个 tab 一个 url 展示 并且在把那个页面标题改为酒店名); // 另一备选任务路线规划生成文档 // await runAgentWithTools(北京南站附近的2个酒店以及去的路线路线规划生成文档保存到当前目录的一个 md 文件); await mcpClient.close();任务执行的真实步骤分析以第一个任务为例实际执行过程大致如下每轮迭代都可能对应一次模型决策轮次模型决策工具调用执行结果下一轮输入1调用amap-server__search_places参数keywords酒店, location北京南站, radius...返回 3 个酒店的列表名称、坐标、图片 URL 等将结果作为 ToolMessage 追加2模型从结果中提取出图片 URL然后调用chrome-devtools__new_tab创建 3 个标签页再调用navigate_to和set_title打开 3 个标签页每个加载酒店图片标题被修改无工具调用直接生成最终回复3可能如果模型发现某个步骤未完成会继续调用相应工具......最终控制台输出成功信息浏览器弹出多个标签页。Agent 就像一位多才多艺的实习生你只需要告诉它目标它自己会规划工具调用顺序并执行。七、踩坑实录与优化建议我在跑这个例子时遇到几个坑分享给大家工具返回值太长高德地图返回的酒店详情可能很大模型上下文窗口有限。建议在工具调用后对返回值做摘要或只提取关键字段。可以在foundTool.invoke之后对contentStr进行截断。Chrome DevTools MCP 需要 Chrome 已启动默认chrome-devtools-mcp会尝试连接已有的 Chrome 实例需要开启远程调试端口如果没有会启动新实例。确保你的 Chrome 版本兼容或者通过参数指定--executablePath。文件系统写入权限modelcontextprotocol/server-filesystem要求传入允许操作的目录如果你给了绝对路径确保该目录存在且可写。工具调用并行 vs 串行当前代码是串行执行工具调用如果模型一次返回多个工具可以考虑使用Promise.all并发执行提高效率但要注意某些工具依赖前一个工具的结果需要按顺序。关闭 MCP 客户端记得在最后调用mcpClient.close()否则子进程可能不会退出导致资源泄露。八、进阶思考MCP 让 Agent 生态更开放这个例子只是冰山一角。有了 MCP 协议你可以接入任何第三方服务只要对方提供 MCP Server如 GitHub、Slack、数据库等。封装企业私有 API写一个简单的 stdio MCP Server把内部微服务暴露给 AI。组合多种能力比如先查地图、再截图、最后生成报告并保存到云盘全流程自动化。你甚至可以把多个 MCP Server 组合成一个“工具超市”让不同 Agent 按需调用。九、总结与流程回顾今天我们完成了理解了 MCP 的本质——Tool 的进程级包装支持 stdio 和 SSE并给出了通信流程图。使用MultiServerMCPClient同时挂载高德地图、Chrome DevTools、FileSystem 和自定义 MCP并剖析了初始化加载流程。实现了一个带迭代控制的 Agent 循环绘制了多轮工具调用的时序图展示了模型决策与工具执行的完整交互。运行了一个复合任务查酒店 → 取图片 → 打开浏览器并改标题并分析了每一步的实际执行。你会发现MCP 的关键价值在于“标准化”——它让 AI 能力扩展不再是“写胶水代码”而是“配置即服务”。开发者的角色从“写工具”变成了“选工具”效率提升不止一个数量级。如果你还没试过强烈建议今晚就搭一个自己的 MCP Agent。代码都在文章里复制粘贴就能跑。最后抛一个问题如果让你设计一个 MCP Server你会开放什么能力欢迎在评论区留言我们一起脑洞。