从零理解远程 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/list、tools/call、resources/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_file、write_file、search_files、get_file_info、list_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_page、new_page、close_page、click、fill、fill_form、take_screenshot、take_snapshot、list_pages、evaluate_script、wait_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 和配置方式。

相关推荐
怕浪猫1 小时前
外包和大厂的前端代码,差在凌晨三点
前端·面试·github
十年Java程序媛8 小时前
Java this 与 super 详解|继承高频踩坑 + 面试题整理
java·spring boot·面试
萧瑟余晖8 小时前
Spring Cloud配置中心与动态刷新详解
spring cloud·面试
白露与泡影9 小时前
一线 Java 后端面试八股(2026 更新版,含完整答案)
java·开发语言·面试
学到老活到老14 小时前
集合换成线程安全的,就不用加锁了吗?
java·面试
萧瑟余晖15 小时前
Spring Cloud Alibaba生态整合详解
spring·spring cloud·面试
Sailing17 小时前
【封闭结束2】Agent 意图识别:从语义理解到稳定路由
人工智能·面试·agent
Sailing17 小时前
【封闭结束1】端到端交付:Agent 一次请求如何走完全链路
人工智能·面试·agent
怕浪猫1 天前
红利窗口开着,程序员别在场边囤课
面试·github·ai编程
lll23526917791 天前
HR如何快速整理面试沟通内容?
面试·职场和发展