从零理解远程 MCP:串联三个 MCP Server 打造 AI 自动化工作流

MCP 远程调用(HTTP)完全解析:串联多个 MCP Server 打造 AI 工作流

前言

在上一篇中,我们写了本地 MCP Server,Agent 通过 command + args spawn 子进程、走 stdio 管道通信。本文更进一步:用 url 连远程 MCP Server,用 npx 一键启动本地 npm 包,三个 Server 串联,让 Agent 自动完成"查地图 → 打开浏览器 → 写文件"的完整工作流。


一、从 stdio 到 HTTP:同一种协议,不同传输方式

1.1 核心不变

协议内容完全一样:tools/listtools/callresources/read......LLM 和工具之间说的话还是那些。唯一变的是传输介质:

arduino 复制代码
stdio(本地)                    HTTP(远程)

Agent 进程                       Agent 进程
  │                                │
  │ spawn + stdin/stdout 管道       │ POST http://服务器:3000/mcp
  ▼                                ▼
MCP Server 子进程                 MCP Server(独立 HTTP 服务)
  • stdio = 两个人在同一房间直接说话(管道传声)
  • HTTP = 一个在北京,一个在上海打电话(网络传数据)

1.2 Client 配置三种写法

arduino 复制代码
// 方式一:本地文件 --- spawn 子进程
{ command: 'node', args: ['path/to/server.mjs'] }

// 方式二:npx --- spawn 子进程 + 自动下载 npm 包
{ command: 'npx', args: ['-y', '包名'] }

// 方式三:远程 HTTP --- 发网络请求
{ url: 'https://远程地址/mcp' }

不管哪种方式,MultiServerMCPClient 统一管理,getTools() 一把拿回所有工具。


二、npx 是什么

复制代码
npx -y 包名
部分 含义
npx Node 自带命令,运行 npm 包
-y 跳过"是否下载?y/n"确认,直接执行
包名 npm 上的包,如 @modelcontextprotocol/server-filesystem

npm install 的区别: npx 自动下载、运行、用完即走(或缓存)。不需要先全局安装,一行命令搞定。

在 MCP 里的价值:工具开发者把 MCP Server 发布到 npm,你用 npx 一行就能用,不需要自己写任何工具代码。


三、三个 MCP Server 的配置解析

css 复制代码
const mcpClient = new MultiServerMCPClient({
  mcpServers: {
    'amap-server': {
      url: 'https://mcp.amap.com/mcp?key=f58f8879063a5f61df94bbb40a6305cb'
    },
    'file-system': {
      command: 'npx',
      args: ['-y', '@modelcontextprotocol/server-filesystem', '允许的目录']
    },
    'chrome-devtools': {
      command: 'npx',
      args: ['-y', 'chrome-devtools-mcp@latest']
    }
  }
});

3.1 三个 Server 的命名

'amap-server''file-system''chrome-devtools'------全部自己取名。 变量名自由,框架只认后面的配置结构。

3.2 配置结构的固定字段

字段 含义 谁定
url 远程 HTTP 地址 固定字段,值是实际地址
command 用什么启动子进程 固定字段,值填 node/python/npx
args 传给 command 的参数 固定字段,值是一个数组

3.3 amap-server --- 远程 HTTP

css 复制代码
'amap-server': {
  url: 'https://mcp.amap.com/mcp?key=xxx'
}

不启动任何子进程。 Agent 通过 HTTP POST 请求和高德服务器通信。工具跑在远程服务器上。提供的工具:maps_geo(地址→坐标)、maps_around_search(周边搜索)、maps_search_detail(地点详情)、maps_distance(距离计算)等。

3.4 file-system --- 本地 npx,有安全边界

arduino 复制代码
'file-system': {
  command: 'npx',
  args: [
    '-y',
    '@modelcontextprotocol/server-filesystem',
    'C:\...\remote-mcp\src'    // 只允许访问这个目录
  ]
}

提供的工具:read_filewrite_filesearch_filesget_file_infolist_allowed_directories

最后一个参数是安全边界 ------这个 MCP Server 只能操作这个目录,越界就报 Access denied。这是 file-system MCP 内置的安全机制。

3.5 chrome-devtools --- 浏览器控制

bash 复制代码
'chrome-devtools': {
  command: 'npx',
  args: ['-y', 'chrome-devtools-mcp@latest']
}

提供的工具:navigate_pagenew_pageclose_pageclickfillfill_formtake_screenshottake_snapshotlist_pagesevaluate_scriptwait_for 等几十个浏览器操作工具。相当于一个能自动操控浏览器的机器人。

3.6 一句话总结

scss 复制代码
远程用 url,本地用 command + args,npx 省得自己装
一个 MultiServerMCPClient 同时管三种,getTools() 一把全拿回来

四、获取工具

ini 复制代码
const tools = await mcpClient.getTools();

工具代码不在你的文件里:

Server 工具在哪
amap-server 高德服务器上
file-system npx 下载的 @modelcontextprotocol/server-filesystem npm 包里
chrome-devtools npx 下载的 chrome-devtools-mcp npm 包里

你一行工具代码都没写,但拿到了几十个工具。这就是 MCP 的复用价值。


五、SystemMessage --- 告诉 LLM 工作目录

go 复制代码
const messages = [
  new SystemMessage(`当前工作目录:C:\...\remote-mcp\src
所有文件操作(读写保存)必须在这个目录下进行,不要使用其他路径。`),
  new HumanMessage(query),
];

这是踩坑后的经验。 file-system 只允许写指定目录,但 LLM 不知道------它可能编一个 Linux 路径(/home/user/...),结果报 Access denied。在 SystemMessage 里明确告知工作目录,LLM 就不会乱猜了。


六、ReAct 循环

6.1 invoke

ini 复制代码
const response = await modelWithTools.invoke(messages);
messages.push(response);

LLM 在云端完成决策。 返回的 response 可能是直接回答,也可能是 tool_calls

6.2 判断是否调工具

vbscript 复制代码
if (!response.tool_calls || response.tool_calls.length === 0) {
  console.log(chalk.green(`AI回答:${response.content}`));
  return response.content;
}

两个条件满足任意一个就退出:

  • !response.tool_calls --- tool_calls 是 undefined,LLM 没想调工具
  • tool_calls.length === 0 --- LLM 说要调但给了空数组(几乎不会发生,兜底)

6.3 打印工具名

ini 复制代码
console.log(chalk.bgBlue(`AI调用工具:${response.tool_calls.map(t => t.name).join(', ')}`));

从工具调用列表里只取名字,用逗号拼起来,蓝底打印给开发者看。纯调试信息。

6.4 遍历工具调用

ini 复制代码
for (const tool_call of response.tool_calls) {
  const foundTool = tools.find(t => t.name === tool_call.name);

for...of 串行遍历,一个工具执行完再跑下一个。find 按 LLM 给的名字在工具库里匹配真正的工具对象。

6.5 执行工具

ini 复制代码
if (foundTool) {
  const toolResult = await foundTool.invoke(tool_call.args);

tool_call.args 是 LLM 自动从用户自然语言里提取的参数。LLM 对照工具的 schema 描述,自动推理填入,不需要你手动传。

6.6 处理返回值(四层判断)

不同 MCP Server 返回格式不同,需要统一转成字符串才能放进 ToolMessage:

ini 复制代码
let contentStr;

if (typeof toolResult === 'string') {
  contentStr = toolResult;
}
// 情况一:返回的就是裸字符串,直接用

else if (Array.isArray(toolResult.content)) {
  contentStr = toolResult.content.map(c => c.text || '').join('\n');
}
// 情况二:MCP 标准格式 [{type:'text', text:'...'}, ...]
//         取出每项的 .text,换行拼起来。|| '' 兜底

else if (typeof toolResult.content === 'string') {
  contentStr = toolResult.content;
}
// 情况三:content 字段就是字符串,直接取

else {
  contentStr = JSON.stringify(toolResult);
}
// 情况四:兜底,整个对象转 JSON 字符串。LLM 至少能看到内容,不崩

四种判断只为一个目的:让 contentStr 无论怎样都是字符串,安全放进 ToolMessage。

6.7 塞回 messages

less 复制代码
messages.push(new ToolMessage({
  content: contentStr,
  tool_call_id: tool_call.id,
}));

tool_call_id 绝对不能丢。 LLM 可能同时调 3 个工具,每个结果必须标记对应哪个调用,就像给三个人发微信,回复必须标记是对谁说的。

6.8 兜底返回

ini 复制代码
return messages[messages.length - 1].content;

maxIterations(默认 30 轮)到了还没结束,取最后一条消息当结果返回。防止 LLM 死循环。


七、实战工作流

javascript 复制代码
await runAgentWithTools(
  `抚州东华理工大学附近的酒店,最近的 3 个酒店,
   拿到酒店图片,打开浏览器,展示每个酒店的图片,
   每个 tab 一个 url 展示,并且在把那个页面标题改为酒店名`
);

Agent 的实际执行步骤

markdown 复制代码
第 0 轮:maps_geo(地址→坐标)

第 1 轮:maps_around_search(搜附近酒店)

第 2 轮:maps_search_detail × 3(查每个酒店详情+图片)

第 3 轮:new_page(打开酒店图片链接)
         → new_page → new_page → ...(每个酒店一个 tab)
         → select_page → evaluate_script(改页面标题)

第 4 轮:回答用户

三个 MCP Server 被 Agent 自动编排串联,不需要你写任何编排逻辑。


八、常见踩坑

8.1 API 限流

高德免费 key 有 QPS 上限。Agent 并发 8 个 maps_search_detail 直接把额度打爆:

复制代码
CUQPS_HAS_EXCEEDED_THE_LIMIT

换付费 key 或在 prompt 里让 LLM 分批查询。

8.2 参数格式错误

LLM 给 maps_distance 的坐标参数格式不符合高德要求,返回:

复制代码
INVALID_PARAMS

LLM 每次生成的参数不一样,重跑一次通常能解决。或在 SystemMessage 里加参数格式约束。

8.3 路径越权

LLM 编了个 C:\home\user...(Linux 习惯),超出 file-system 允许的目录范围:

lua 复制代码
Access denied - path outside allowed directories

在 SystemMessage 里明确告诉 LLM 工作目录即可解决。


九、完整流程串讲

scss 复制代码
① 创建 model(ChatOpenAI)

② 创建 mcpClient(MultiServerMCPClient),配置 3 个 MCP Server
   - amap:远程 HTTP
   - file-system:npx 本地,指定安全目录
   - chrome-devtools:npx 本地,控制浏览器

③ mcpClient.getTools() 自动拿回所有工具

④ model.bindTools(tools) 绑定工具

⑤ runAgentWithTools(query):
   消息 = [SystemMessage(工作目录约束), HumanMessage(用户任务)]
   → for 循环(最多 30 轮):
       → invoke → 判断是否调工具
       → 不需要 → 结束
       → 需要   → 遍历 tool_calls → find 匹配 → invoke 执行
       → 四层判断处理返回值 → ToolMessage 带 tool_call_id 塞回
   → 到了上限 → 返回最后一条消息

⑥ mcpClient.close() 释放资源

十、两种 MCP 完整对比

本地 stdio(mcp-demo) 远程 HTTP(remote-mcp)
Server 在哪 你的文件 高德服务器 / npx 缓存的包
连法 command + args url / command + args
传输 stdin/stdout 管道 HTTP 网络 / 管道
跨机器 不能 能(url 方式)
工具代码 你自己写的 别人写的,你直接用
Server 数量 1 个 可串联多个

核心不变:McpServer 接口固定,工具注册方式不变,Agent 端 bindTools + ReAct 循环不变。变得只是 Transport 和配置方式。

相关推荐
weedsfly1 小时前
观察者模式 vs 发布-订阅模式:从概念到实战,一次讲清楚
前端·javascript·面试
暗不需求1 小时前
手写 AJAX:从 XMLHttpRequest 到封装一个简易 axios
前端·面试
不负岁月无痕1 小时前
STL -- C++ 红黑树封装 Map 和 Set
java·c语言·开发语言·c++·面试
怕浪猫12 小时前
现在的"AI 原生应用"热潮,恰恰证明了大多数产品需求本身就是个草台班子
面试
TPBoreas15 小时前
MySQL性能优化面试全攻略
mysql·面试·性能优化
用户852495071841 天前
从零搭建自定义 MCP Server:打造你的专属工具服务
面试
黄敬峰1 天前
远程 MCP 实战:LangChain 同时接入高德地图、Chrome、文件系统,让 AI Agent 操控真实世界
面试
KaifuZeng1 天前
通信与接口协议面试问题汇总三
单片机·嵌入式硬件·面试·通信与接口协议
胡萝卜术1 天前
从聊天模型到本地执行助手:Remote MCP 多工具 Agent 实战
面试·架构·github