远程 MCP 实战:让 AI 无缝调用地图、浏览器与文件系统

从零搭建一个能查酒店、开网页、写文件的 Agent,附详细执行流程图解,代码可复现

你有没有遇到过这种场景:想让 AI 帮我查一下北京南站附近的酒店,然后把每个酒店的图片在浏览器里打开,还要自动把标签页标题改成酒店名------结果你发现,大模型再强,也拿不到地图数据,点不了浏览器,更写不了文件。

说白了,大模型缺的不是"脑子",而是"手脚" 。而 MCP(Model Context Protocol)就是那个让 AI 长出手脚的标准化协议。

今天,我们就从零开始,用一个实际案例把多个远程 MCP Server 串联起来,让你的 Agent 同时具备地图查询、浏览器控制和文件读写能力。本文所有代码都基于 LangChain 的 mcp-adapters,并会深入剖析每一步的通信与执行流程,配合图解让你一次搞懂。


一、MCP 的本质:Tool 的"超级包装"

如果你用过 Function Calling,那理解 MCP 就毫无门槛。MCP 本质上还是 Tool ,只不过它给 Tool 包了一层进程,通过 stdioHTTP(SSE) 来访问。

两种通信模式图解

  • 本地 stdio :Agent 进程作为父进程,启动一个子进程(如 node server.mjs),通过子进程的 stdin/stdout 发送 JSON-RPC 消息。适合本地工具(文件系统、浏览器、自定义脚本)。
  • 远程 SSE:Agent 通过 HTTP 向远程服务器发起 SSE 连接,服务端可以主动推送消息,适合云端服务(如地图、数据库)。

最大的好处是解耦 + 复用:任何人都可以按 MCP 协议开发一个 Server,然后全世界的 Agent 都能直接复用。就像高德地图开放了 MCP Server,你无需写任何 HTTP 请求封装,直接配置一个 URL 就能让 AI 调用地图能力。

MCP 让 Tool 从"函数"升级为"服务",一次编写,到处 Agent。**


二、场景驱动:一个真实的"AI 工作流"

我们要实现的需求是这样的:

用户输入: "北京南站附近的酒店,最近的 3 个酒店,拿到酒店图片,打开浏览器,展示每个酒店的图片,每个 tab 一个 url 展示,并且在把那个页面标题改为酒店名"

这个任务天然需要三种能力:

  1. 地理位置查询 → 高德地图 MCP(远程 SSE)
  2. 浏览器自动化 → Chrome DevTools MCP(本地 npx)
  3. 文件系统(备用) → FileSystem MCP(本地 npx)

同时,为了演示自定义 MCP,我们还会再挂一个本地自己写的 my-mcp-server(比如做日志或额外计算)。

你看,一个 Agent 同时挂载 4 个 MCP Server,每个 Server 提供多个 Tool,整个工具集瞬间变得异常丰富。


三、环境准备与依赖

首先创建一个 Node.js 项目,安装必要依赖:

kotlin 复制代码
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 的初始化与工具加载

直接上核心代码,然后逐步分析内部的执行流程。

javascript 复制代码
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:\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-mcp@latest']
        },
        // 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 循环。

ini 复制代码
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 加速)。
  • 错误处理:单个工具失败后,返回错误信息给模型,模型可以决定是否重试或换方案,增强了鲁棒性。

为什么需要这几种判断?

  1. typeof toolResult === 'string'
    很多 MCP Server(如 FileSystem 的 read_file)直接返回文件内容的字符串。模型期望 ToolMessage.content 是文本,所以直接使用即可。
  2. toolResult && toolResult.result
    Chrome DevTools MCP 和高德地图 MCP 在实现时,有些工具会返回一个对象,其中包含一个 result 字段(可能是结构化的 JSON 字符串或对象)。例如高德的 search_places 可能返回 { result: { pois: [...] } }。我们抽取 result 字段,避免把无用的元数据塞给模型。
  3. else { JSON.stringify(toolResult) }
    兜底方案:如果返回值是普通对象(没有 result 字段),我们就将其序列化为 JSON 字符串。这是最保险的做法,保证模型总能拿到文本形式的可读数据。

金句:工具返回值解析,本质是在"保真"和"保读"之间做平衡------既要保留关键信息,又要让大模型一眼看懂。

不同 MCP Server 的返回值差异一览

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 已经覆盖了参数校验失败的情况,并返回友好的错误信息给模型,模型可以在下一轮自行修正。


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个标签页,每个显示对应酒店的图片,标题也已修改完成。"

你会发现,每一步工具的返回值都被转化为字符串,大模型才能"读得懂"。


六、运行与效果:完整任务执行流程拆解

执行:

javascript 复制代码
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_toset_title 打开 3 个标签页,每个加载酒店图片,标题被修改 无工具调用,直接生成最终回复
3(可能) 如果模型发现某个步骤未完成,会继续调用相应工具 ... ...

最终控制台输出成功信息,浏览器弹出多个标签页。

Agent 就像一位多才多艺的实习生,你只需要告诉它目标,它自己会规划工具调用顺序并执行。**


七、踩坑实录与优化建议

我在跑这个例子时遇到几个坑,分享给大家:

  1. 工具返回值太长 :高德地图返回的酒店详情可能很大,模型上下文窗口有限。建议在工具调用后,对返回值做摘要或只提取关键字段。可以在 foundTool.invoke 之后对 contentStr 进行截断。
  2. Chrome DevTools MCP 需要 Chrome 已启动 :默认 chrome-devtools-mcp 会尝试连接已有的 Chrome 实例(需要开启远程调试端口),如果没有,会启动新实例。确保你的 Chrome 版本兼容,或者通过参数指定 --executablePath
  3. 文件系统写入权限@modelcontextprotocol/server-filesystem 要求传入允许操作的目录,如果你给了绝对路径,确保该目录存在且可写。
  4. 工具调用并行 vs 串行 :当前代码是串行执行工具调用,如果模型一次返回多个工具,可以考虑使用 Promise.all 并发执行,提高效率(但要注意某些工具依赖前一个工具的结果,需要按顺序)。
  5. 关闭 MCP 客户端 :记得在最后调用 mcpClient.close(),否则子进程可能不会退出,导致资源泄露。

八、进阶思考:MCP 让 Agent 生态更开放

这个例子只是冰山一角。有了 MCP 协议,你可以:

  • 接入任何第三方服务:只要对方提供 MCP Server(如 GitHub、Slack、数据库等)。
  • 封装企业私有 API:写一个简单的 stdio MCP Server,把内部微服务暴露给 AI。
  • 组合多种能力:比如先查地图、再截图、最后生成报告并保存到云盘,全流程自动化。

你甚至可以把多个 MCP Server 组合成一个"工具超市",让不同 Agent 按需调用。


九、总结与流程回顾

今天我们完成了:

  1. 理解了 MCP 的本质------Tool 的进程级包装,支持 stdio 和 SSE,并给出了通信流程图。
  2. 使用 MultiServerMCPClient 同时挂载高德地图、Chrome DevTools、FileSystem 和自定义 MCP,并剖析了初始化加载流程。
  3. 实现了一个带迭代控制的 Agent 循环,绘制了多轮工具调用的时序图,展示了模型决策与工具执行的完整交互。
  4. 运行了一个复合任务:查酒店 → 取图片 → 打开浏览器并改标题,并分析了每一步的实际执行。

你会发现,MCP 的关键价值在于"标准化" ------它让 AI 能力扩展不再是"写胶水代码",而是"配置即服务"。开发者的角色从"写工具"变成了"选工具",效率提升不止一个数量级。

如果你还没试过,强烈建议今晚就搭一个自己的 MCP Agent。代码都在文章里,复制粘贴就能跑。

最后抛一个问题:如果让你设计一个 MCP Server,你会开放什么能力? 欢迎在评论区留言,我们一起脑洞。

相关推荐
weixin_422329311 小时前
AgentScope Java2.0 版本Builder 本地启动指南
人工智能
studyrunner1 小时前
GPT-5.5 对比 GPT-5.6 Sol、Terra、Luna:官方性能数据与选型分析
大数据·人工智能·gpt
chanalbert1 小时前
Agent 工程化指南(一):概念与认知架构 — 智能体到底是什么、怎么思考
llm·agent
OpenMiniServer1 小时前
大统一逻辑链3.0(GULP3.0):绝对悖论、逻辑链与宇宙演化动力学
人工智能
Asize1 小时前
Agent 入门:从 LLM 与 Agent 的区别到 Function Calling
javascript·人工智能
小林ixn1 小时前
AI Agent的“万能工具箱”:MCP协议实战,让工具调用不再受语言和进程束缚
人工智能·agent·mcp
武子康1 小时前
🔥 GPT-5.6 有限预览全拆解:Sol/Terra/Luna 三档定价 + max/ultra 双推理 + 缓存 1.00×/1.25×/0.10× 三倍
人工智能·chatgpt·openai
CTA终结者1 小时前
近期AI量化工具推荐,围绕最难推进的环节选择
人工智能·python
无糖可可果1 小时前
MCP(Model Context Protocol)入门与实践:让 AI Agent 跨进程调用工具
人工智能