作者:互联网 时间: 2026-07-12 08:57:53
你有没有遇到过这种场景:想让 AI 帮我查一下北京南站附近的酒店,然后把每个酒店的图片在浏览器里打开,还要自动把标签页标题改成酒店名——结果你发现,大模型再强,也拿不到地图数据,点不了浏览器,更写不了文件。
说白了,大模型缺的不是“脑子”,而是“手脚” 。而 MCP(Model Context Protocol)就是那个让 AI 长出手脚的标准化协议。
今天,我们就从零开始,用一个实际案例把多个远程 MCP Server 串联起来,让你的 Agent 同时具备地图查询、浏览器控制和文件读写能力。本文所有代码都基于 LangChain 的mcp-adapters,并会深入剖析每一步的通信与执行流程,配合图解让你一次搞懂。
如果你用过 Function Calling,那理解 MCP 就毫无门槛。MCP 本质上还是 Tool,只不过它给 Tool 包了一层进程,通过stdio或HTTP(SSE) 来访问。
node server.mjs),通过子进程的 stdin/stdout 发送 JSON-RPC 消息。适合本地工具(文件系统、浏览器、自定义脚本)。最大的好处是解耦 + 复用:任何人都可以按 MCP 协议开发一个 Server,然后全世界的 Agent 都能直接复用。就像高德地图开放了 MCP Server,你无需写任何 HTTP 请求封装,直接配置一个 URL 就能让 AI 调用地图能力。
我们要实现的需求是这样的:
这个任务天然需要三种能力:
同时,为了演示自定义 MCP,我们还会再挂一个本地自己写的my-mcp-server(比如做日志或额外计算)。
你看,一个 Agent 同时挂载 4 个 MCP Server,每个 Server 提供多个 Tool,整个工具集瞬间变得异常丰富。
首先创建一个 Node.js 项目,安装必要依赖:
npm init -ynpm install /mcp-adapters /openai dotenv chalk
因为 Chrome DevTools MCP 和 FileSystem MCP 都通过npx调用,无需额外安装包,运行时自动下载。
确保你有 DeepSeek 或 OpenAI 的 API Key(本文使用 DeepSeek,兼容 OpenAI 接口)。
直接上核心代码,然后逐步分析内部的执行流程。
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?key=baec4e904d7460f61e4c85a571e793de'},// 2. 自定义本地 MCP(stdio)'my-mcp-server': {transport: 'stdio',command: 'node',args: ['E:workspacelgl_aiaiagent_in_actionmcp-demosrcmy-mcp-server.mjs']},// 3. Chrome DevTools MCP'chrome-devtools': {command: 'npx',args: ['-y', 'chrome-devtools-mcp@latest']},// 4. FileSystem MCP'filesystem': {command: 'npx',args: ['-y','@modelcontextprotocol/server-filesystem','E:workspacelgl_aiaiagent_in_actionremote-mcp' // 允许操作的根目录]}}});const tools = await mcpClient.getTools();
当执行new MultiServerMCPClient(config)时,内部会依次对每个配置项做如下操作(多 Server 并行初始化):
MultiServerMCPClient内部使用EventSource连接到url,按照 MCP 协议进行握手,获取服务端声明的工具列表(工具名、参数 JSON Schema 等)。command + args,spawn一个子进程,然后通过子进程的 stdin 发送initialize请求,从 stdout 读取响应,解析出工具列表。DynamicStructuredTool实例,并统一放入一个数组返回给调用方。注意:mcp-adapters会自动给工具名加上 Server 名前缀(如amap-server__search_place),避免冲突。拿到 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'read_file)直接返回文件内容的字符串。模型期望ToolMessage.content是文本,所以直接使用即可。toolResult && toolResult.resultresult字段(可能是结构化的 JSON 字符串或对象)。例如高德的search_places可能返回{ result: { pois: [...] } }。我们抽取result字段,避免把无用的元数据塞给模型。else { JSON.stringify(toolResult) }result字段),我们就将其序列化为 JSON 字符串。这是最保险的做法,保证模型总能拿到文本形式的可读数据。| MCP Server | 示例工具 | 返回值类型 | 是否包含result字段 |
|---|---|---|---|
| 高德地图 (SSE) | search_places | { result: {...} }或直接{ pois: [...] } | 多数有,但不保证 |
| Chrome DevTools | new_tab | { result: { tabId: 123 } } | 有 |
| FileSystem | read_file | 文件内容的string | 无 |
| 自定义 MCP | 自定义 | 任意类型 | 取决于实现 |
所以,我们的解析逻辑必须覆盖以上所有情况。
在工具调用时,toolCall.args已经是模型根据工具定义的 JSON Schema 生成的结构化对象,无需额外解析。但有一处细节需要注意:
foundTool.invoke之前添加参数校验,或者依赖 LangChain 自带的zod校验。mcp-adapters已经将 MCP 工具的inputSchema转换成 Zod 对象,invoke时会自动校验,非法参数会直接抛出异常,从而进入catch分支。因此我们的错误处理catch已经覆盖了参数校验失败的情况,并返回友好的错误信息给模型,模型可以在下一轮自行修正。
为了让你更直观地理解整个对话流程,下面展示一次成功任务的实际消息历史(简化版):
第 1 轮:
[ { name: 'amap-server__search_places', args: { keywords: '酒店', location: '116.385,39.905', ... } } ]"{"pois":[{"name":"如家","photos":["url1"]}, ...]}"第 2 轮(模型看到工具结果后):
[ { name: 'chrome-devtools__new_tab', args: {} }, { name: 'chrome-devtools__navigate_to', args: { url: 'url1' } }, ... ]"{"result":{"tabId":3}}""{"result":{"success":true}}"第 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(可能) | 如果模型发现某个步骤未完成,会继续调用相应工具 | ... | ... |
最终控制台输出成功信息,浏览器弹出多个标签页。
我在跑这个例子时遇到几个坑,分享给大家:
foundTool.invoke之后对contentStr进行截断。chrome-devtools-mcp会尝试连接已有的 Chrome 实例(需要开启远程调试端口),如果没有,会启动新实例。确保你的 Chrome 版本兼容,或者通过参数指定--executablePath。@modelcontextprotocol/server-filesystem要求传入允许操作的目录,如果你给了绝对路径,确保该目录存在且可写。Promise.all并发执行,提高效率(但要注意某些工具依赖前一个工具的结果,需要按顺序)。mcpClient.close(),否则子进程可能不会退出,导致资源泄露。这个例子只是冰山一角。有了 MCP 协议,你可以:
你甚至可以把多个 MCP Server 组合成一个“工具超市”,让不同 Agent 按需调用。
今天我们完成了:
MultiServerMCPClient同时挂载高德地图、Chrome DevTools、FileSystem 和自定义 MCP,并剖析了初始化加载流程。你会发现,MCP 的关键价值在于“标准化” ——它让 AI 能力扩展不再是“写胶水代码”,而是“配置即服务”。开发者的角色从“写工具”变成了“选工具”,效率提升不止一个数量级。
如果你还没试过,强烈建议今晚就搭一个自己的 MCP Agent。代码都在文章里,复制粘贴就能跑。
最后抛一个问题:如果让你设计一个 MCP Server,你会开放什么能力? 欢迎在评论区留言,我们一起脑洞。