远程 MCP 项目实战:LangChain 连接高德地图、Chrome DevTools 与文件系统
远程 MCP 项目实战:LangChain 连接高德地图、Chrome DevTools 与文件系统
前言
大模型本身擅长理解语言、分析问题和生成内容,但它并不天然具备访问外部世界的能力。比如让模型查询附近酒店、打开浏览器、截图页面、读取本地文件、把结果写入 Markdown 文档,这些事情都不是单靠模型参数就能完成的。
要让模型真正“做事”,就需要给它接入外部工具。传统做法是为每一个工具单独写函数,然后把函数描述提供给模型。这样虽然可行,但当工具越来越多时,接入方式、调用协议、参数结构、运行环境都会变得很分散。
MCP 的出现,就是为了解决这个问题。MCP 全称是 Model Context Protocol,可以理解为一套面向大模型应用的工具接入协议。它的本质仍然是 tool,只不过把工具封装成了标准化的服务。大模型应用通过统一方式连接 MCP Server,再由 MCP Server 去访问地图、浏览器、文件系统或者业务接口。
本文通过一个完整项目讲解远程 MCP 的使用方式:使用 LangChain 连接多个 MCP Server,让模型能够调用 高德地图 MCP 查询地点和路线,调用 Chrome DevTools MCP 控制浏览器,调用 FileSystem MCP 写入本地文件,并通过一个简单 Agent 循环把这些工具串成完整 AI 工作流。
1. MCP 基础知识
MCP 可以先从一句话理解:
MCP 是让大模型应用用统一协议调用外部工具的一种标准。
在没有 MCP 的情况下,如果要让模型调用地图,需要写一套地图工具;要让模型操作浏览器,需要再写一套浏览器工具;要让模型写文件,还要继续封装文件系统工具。每一种工具的接入方式都不一样,后期维护成本会越来越高。
有了 MCP 后,外部能力可以统一封装成 MCP Server。大模型应用只需要连接这些 Server,就能获取它们暴露出来的工具列表。
整体关系可以这样理解:
用户问题
↓
大模型应用 / Agent
↓
MCP Client
↓
MCP Server
↓
地图、浏览器、文件系统、数据库、业务 API
MCP Client 通常运行在大模型应用里,负责连接一个或多个 MCP Server,读取工具列表,并把工具转换成模型可以调用的形式。
MCP Server 负责真正提供能力。比如高德地图 MCP Server 提供地点搜索、路线规划;FileSystem MCP Server 提供文件读取和写入;Chrome DevTools MCP Server 提供浏览器控制能力。
模型并不是直接访问外部系统。 模型只是根据工具描述生成工具调用请求,例如“调用地图工具查询北京南站附近酒店”。真正执行调用的是 MCP Client 和 MCP Server。工具返回结果后,结果再被放回对话上下文,让模型继续分析和总结。
从开发角度看,MCP 的工作过程可以拆成几个关键点。
第一,工具发现。 MCP Client 连接 MCP Server 后,会先获取 Server 暴露的工具列表。每个工具通常会包含名称、描述、参数结构等信息。模型正是根据这些描述判断“什么时候该调用哪个工具”。
第二,参数约束。 工具不是随便传字符串调用的,MCP Server 会描述工具需要什么参数。例如地图搜索工具可能需要关键词、城市、经纬度;文件写入工具可能需要文件路径和文件内容。参数结构越清楚,模型调用工具时越稳定。
第三,结果回传。 工具执行完成后,返回结果并不会直接变成最终答案,而是会进入模型上下文。模型会继续阅读工具结果,再决定是继续调用下一个工具,还是把结果整理成自然语言回复。
第四,能力边界。 MCP 让模型能调用外部工具,但也必须控制权限。比如 FileSystem MCP 只能开放指定目录,高德地图 MCP 只能使用授权 Key,Chrome DevTools MCP 只能控制允许连接的浏览器实例。MCP 提供的是连接能力,不代表可以无边界地开放所有系统权限。
因此,MCP 不只是“把函数给模型用”,它更像是给 AI 应用建立了一套标准工具层:工具如何声明、如何连接、如何调用、如何返回结果,都有统一方式。
MCP 常见的连接方式主要有两种。
第一种是 stdio。 这种方式通常用于本地 MCP Server。应用通过命令启动一个本地进程,然后通过标准输入输出通信。例如文件系统 MCP、Chrome DevTools MCP、自定义 Node.js MCP Server,都很适合用这种方式运行。
'filesystem': {
command: 'npx',
args: [
'-y',
'@modelcontextprotocol/server-filesystem',
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
]
}
这里的 command 表示要启动什么命令,args 表示启动命令时传入的参数。上面这段配置的意思是通过 npx 启动官方文件系统 MCP Server,并且只允许它访问指定目录。
第二种是 HTTP / Streamable HTTP。 这种方式通常用于远程 MCP Server。比如高德地图 MCP 不需要在本地启动地图服务,而是通过一个远程地址访问。
{
"mcpServers": {
"amap-maps-http": {
"url": "https://mcp.amap.com/mcp?key=高德地图Key"
}
}
}
这种配置方式非常适合云服务、第三方 API、企业内部平台等场景。只要远程服务按照 MCP 协议暴露工具,大模型应用就可以通过 URL 连接它。
除了在代码中配置 MCP,也可以在支持 MCP 的编辑器或 AI IDE 中配置。比如在 Trae 中,可以进入设置里的 MCP 配置区域,选择手动配置,然后粘贴类似下面的 JSON:
{
"mcpServers": {
"amap-maps-http": {
"url": "https://mcp.amap.com/mcp?key=高德地图Key"
}
}
}
这类 JSON 的核心结构就是 mcpServers。里面每一个字段名都是一个 MCP Server 的自定义名称,对应的配置可以是远程 url,也可以是本地 command + args。在 Trae 里手动配置后,编辑器就能识别这个 MCP Server 暴露的工具,后续 AI 助手就可以在需要时调用这些能力。
MCP 的核心价值不只是“多了几个工具”,而是让工具生态变得标准化。任何人都可以开发符合协议的 MCP Server,其他 AI 应用可以直接复用。地图、浏览器、文件系统、数据库、内部业务平台,都可以通过同一套思路接入 Agent。
2. 项目目标与整体流程
这个项目要完成的任务是:用户输入一句自然语言需求,模型自动判断需要调用哪些工具,查询北京南站附近的 2 个酒店,规划路线,并把路线规划结果生成一个 Markdown 文件保存到本地目录。
示例用户输入如下:
北京南站附近的2个酒店,以及去的路线,路线规划生成文档保存到 当前目录 的一个 md 文件
这句话里面其实包含了多个子任务:
- 需要理解“北京南站”这个地点
- 需要搜索北京南站附近的酒店
- 需要筛选出 2 个酒店
- 需要规划从北京南站到酒店的路线
- 需要把最终内容整理成 Markdown 文档
- 需要调用文件系统工具写入本地文件
如果靠普通代码手写流程,就要依次调用地图接口、处理结果、拼接文档、写入文件。使用 Agent + MCP 后,模型可以根据工具描述自动拆解任务,并在多轮循环中逐步调用工具。
项目整体流程如下:
用户输入任务
↓
LangChain 把问题交给模型
↓
模型判断是否需要调用工具
↓
如果需要工具,生成 tool_calls
↓
代码找到对应 MCP 工具并执行
↓
把工具结果包装成 ToolMessage 放回上下文
↓
模型继续分析工具结果
↓
直到模型不再请求工具调用,输出最终结果
这里最关键的思想是:Agent 不是一次调用模型就结束,而是模型、工具、工具结果之间不断循环,直到任务完成。
3. MCP Server 配置讲解
项目中同时连接多个 MCP Server。每一个 MCP Server 都代表一类外部能力。
下面这段是写在代码中的配置方式。如果是在 Trae 这类工具里使用 MCP,可以把同类型的 JSON 配置放到设置里的 MCP 手动配置中;如果是在自己的 Node.js 项目里开发,就可以像下面这样交给 MultiServerMCPClient 管理。
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'amap-mcp-streamableHTTP': {
url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
},
'my-mcp-server': {
command: 'node',
args: [
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs'
]
},
'filesystem': {
command: 'npx',
args: [
'-y',
'@modelcontextprotocol/server-filesystem',
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
]
},
'chrome-devtools': {
command: 'npx',
args: [
'-y',
'chrome-devtools-mcp@latest',
]
}
}
});
高德地图 MCP 使用的是远程 HTTP 方式:
'amap-mcp-streamableHTTP': {
url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
}
它负责提供地图相关能力,比如地点搜索、周边搜索、路径规划等。对于“北京南站附近酒店”“西安大唐不夜城附近酒店”“从 A 到 B 怎么走”这类问题,模型都可以借助它拿到更真实的地理信息。
这里推荐把高德 Key 放在环境变量中:
AMAP_MAPS_API_KEY=高德地图Key
不要把真实 Key 直接写进代码。代码一旦上传到仓库,密钥就有泄漏风险。
FileSystem MCP 使用的是本地 stdio 方式:
'filesystem': {
command: 'npx',
args: [
'-y',
'@modelcontextprotocol/server-filesystem',
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
]
}
它负责文件读写。最后一个路径参数表示允许访问的目录。这个参数非常重要,因为文件系统工具有真实读写能力,不能随便开放整个电脑目录。实际项目里建议给 AI 单独准备一个工作目录,只允许它在这个目录下创建和修改文件。
Chrome DevTools MCP 也是本地工具:
'chrome-devtools': {
command: 'npx',
args: [
'-y',
'chrome-devtools-mcp@latest',
]
}
它可以让模型通过 DevTools 控制浏览器,例如打开网页、点击元素、读取页面结构、截图等。浏览器自动化、前端页面检查、网页信息采集,都可以基于这个能力扩展。
自定义 MCP Server 代表项目自己的业务工具:
'my-mcp-server': {
command: 'node',
args: [
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs'
]
}
这类 Server 可以封装自己的业务逻辑,比如查询内部系统、访问数据库、调用公司接口、执行固定业务流程。只要它符合 MCP 协议,就能和其他工具一样被 Agent 调用。
4. 完整代码
下面是一份完整示例代码。为了更适合实际项目使用,代码中把模型 Key 和高德 Key 都放到了环境变量中,并修正了关闭客户端时的变量名。
// 读取 .env 文件中的环境变量,例如模型 Key、高德地图 Key
import 'dotenv/config';
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';
import chalk from 'chalk';
import {
HumanMessage,
ToolMessage
} from '@langchain/core/messages';
// 创建模型实例。这里使用 DeepSeek 的 OpenAI 兼容接口,所以可以通过 ChatOpenAI 调用
const model = new ChatOpenAI({
modelName: 'deepseek-v4-pro',
apiKey: process.env.DEEPSEEK_API_KEY,
// 工具调用场景更关注稳定性,temperature 设置为 0 可以减少随机性
temperature: 0,
configuration: {
baseURL: 'https://api.deepseek.com/v1',
},
});
// 创建 MCP Client,并在一个客户端中统一管理多个 MCP Server
const mcpClient = new MultiServerMCPClient({
mcpServers: {
// 远程 MCP:通过 HTTP 连接高德地图 MCP,用于地点查询、路线规划等
'amap-mcp-streamableHTTP': {
url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
},
// 自定义本地 MCP:通过 node 启动自己实现的 MCP Server
'my-mcp-server': {
command: 'node',
args: [
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs'
]
},
// 文件系统 MCP:只允许访问指定目录,用于读取、写入、创建文件
'filesystem': {
command: 'npx',
args: [
'-y',
'@modelcontextprotocol/server-filesystem',
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
]
},
// Chrome DevTools MCP:用于控制浏览器,例如打开页面、点击元素、截图
'chrome-devtools': {
command: 'npx',
args: [
'-y',
'chrome-devtools-mcp@latest',
]
}
}
});
// 从所有 MCP Server 中获取工具列表,并转换成 LangChain 可识别的 tools
const tools = await mcpClient.getTools();
console.log(tools);
// 把工具绑定到模型上。绑定后,模型才可以生成 tool_calls
const modelWithTools = model.bindTools(tools);
// 不同 MCP 工具返回的数据结构可能不同,这里统一转成字符串交给模型继续理解
function stringifyToolResult(toolResult) {
if (typeof toolResult === 'string') {
return toolResult;
}
if (toolResult?.content) {
return JSON.stringify(toolResult.content);
}
return JSON.stringify(toolResult);
}
async function runAgentWithTools(query, maxIterations = 30) {
// messages 保存完整上下文:用户问题、模型回复、工具执行结果都会放进来
const messages = [
new HumanMessage(query)
];
// Agent 循环:模型可以在多轮中反复判断是否需要继续调用工具
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgGreen(`第${i + 1}轮对话`));
// 调用绑定工具后的模型。模型可能直接回答,也可能返回 tool_calls
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 如果没有工具调用,说明模型已经得到最终答案,可以结束循环
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(chalk.bgRed(`AI 回答:${response.content}`));
return response.content;
}
console.log(chalk.bgBlue(
`工具调用: ${response.tool_calls.map(t => t.name).join(', ')}`
));
for (const toolCall of response.tool_calls) {
// 根据模型返回的工具名,在 tools 列表中找到真正可执行的工具
const foundTool = tools.find(t => t.name === toolCall.name);
if (!foundTool) {
continue;
}
// 执行工具。toolCall.args 是模型为该工具生成的入参
const toolResult = await foundTool.invoke(toolCall.args);
const contentStr = stringifyToolResult(toolResult);
// 把工具结果放回上下文,并用 tool_call_id 对应模型刚才的工具调用
messages.push(new ToolMessage({
content: contentStr,
tool_call_id: toolCall.id
}));
}
}
// 如果达到最大轮数仍未结束,返回最后一条消息内容
return messages[messages.length - 1].content;
}
// 启动一次完整任务:查询酒店、规划路线,并让文件系统 MCP 保存 Markdown 文档
await runAgentWithTools(
'北京南站附近的2个酒店,以及去的路线,路线规划生成文档保存到 当前目录 的一个 md 文件'
);
// 任务结束后关闭 MCP Client,释放本地进程或远程连接资源
await mcpClient.close();
5. 核心代码分块讲解
这一部分是整篇文章的重点。理解这段代码,关键不是只看每一行做了什么,而是要理解它如何把“模型推理”和“工具执行”连接起来。
5.1 导入依赖:准备模型、MCP 客户端和消息类型
import 'dotenv/config';
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';
import chalk from 'chalk';
import {
HumanMessage,
ToolMessage
} from '@langchain/core/messages';
import 'dotenv/config' 是一个副作用导入。它不需要手动调用函数,导入后会自动读取项目根目录下的 .env 文件,并把里面的配置加载到 process.env 中。
例如 .env 可以这样写:
DEEPSEEK_API_KEY=DeepSeekKey
AMAP_MAPS_API_KEY=高德地图Key
这样代码里就可以通过 process.env.DEEPSEEK_API_KEY 和 process.env.AMAP_MAPS_API_KEY 读取密钥。对于 AI 项目来说,这一点很重要,因为模型 Key、地图 Key 都属于敏感信息,不应该硬编码在源码里。
-
MultiServerMCPClient来自@langchain/mcp-adapters,它的作用是把 MCP Server 暴露出来的工具转换成 LangChain 能识别的工具。这里用的是MultiServer,意思是可以同时管理多个 MCP Server,而不是只能连接一个工具服务。 -
ChatOpenAI是 LangChain 中用于调用 OpenAI 兼容聊天模型的类。虽然示例里接入的是 DeepSeek,但只要服务接口兼容 OpenAI 格式,就可以通过ChatOpenAI加上自定义baseURL来调用。 -
HumanMessage和ToolMessage是 LangChain 的消息类型。HumanMessage表示用户输入,ToolMessage表示工具执行结果。Agent 的多轮循环本质上就是不断往messages数组里追加这些消息,让模型看到完整上下文。 -
chalk只是用来美化终端日志,不影响 Agent 核心逻辑。
5.2 创建模型:让 LangChain 连接 DeepSeek 接口
const model = new ChatOpenAI({
modelName: 'deepseek-v4-pro',
apiKey: process.env.DEEPSEEK_API_KEY,
temperature: 0,
configuration: {
baseURL: 'https://api.deepseek.com/v1',
},
});
这段代码创建了一个聊天模型对象。后续所有模型推理、工具调用判断,都会通过这个 model 完成。
modelName 表示要调用的模型名称。实际开发中要根据模型服务商支持的模型名填写,如果模型名称写错,接口通常会返回模型不存在或请求失败。
apiKey 是访问模型服务的凭证。这里从环境变量读取,避免密钥泄漏。
temperature 控制模型输出的随机性。值越高,模型回答越发散;值越低,输出越稳定。工具调用类 Agent 通常建议把它设置得低一些,例如 0。原因是这类任务更看重稳定执行,而不是创意表达。
configuration.baseURL 用来指定接口地址。ChatOpenAI 默认会访问 OpenAI 的接口,如果要调用 DeepSeek 这类 OpenAI 兼容接口,就需要把 baseURL 改成对应服务商地址。
这里可以把模型对象理解为一个“推理引擎”。但此时它还不会使用 MCP 工具,因为工具还没有绑定进去。
5.3 创建 MCP Client:同时接入多个工具服务
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'amap-mcp-streamableHTTP': {
url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
},
'my-mcp-server': {
command: 'node',
args: [
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs'
]
},
'filesystem': {
command: 'npx',
args: [
'-y',
'@modelcontextprotocol/server-filesystem',
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
]
},
'chrome-devtools': {
command: 'npx',
args: [
'-y',
'chrome-devtools-mcp@latest',
]
}
}
});
这段代码是项目的工具接入中心。mcpServers 对象里的每一个 key,都是一个 MCP Server 的名字;每一个 value,都是这个 Server 的连接方式。
对于远程 MCP Server,通常使用 url:
'amap-mcp-streamableHTTP': {
url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
}
这个配置告诉 MCP Client:高德地图工具不需要本地启动进程,直接通过这个 HTTP 地址连接即可。模型后面如果需要地图能力,最终会通过这个 Server 获取工具并执行。
对于本地 MCP Server,通常使用 command + args:
'filesystem': {
command: 'npx',
args: [
'-y',
'@modelcontextprotocol/server-filesystem',
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
]
}
这表示 MCP Client 会启动一个本地命令。command 是命令名,args 是命令参数。对文件系统 MCP 来说,最后一个参数就是允许访问的目录。
这里最容易忽略的是权限边界。FileSystem MCP 不是模拟写文件,而是真的有可能在本地创建、修改文件。所以实际使用时,不要把访问目录配置成 /、用户主目录或者包含敏感信息的目录。更推荐创建一个专门的工作目录,给 AI 一个明确的活动范围。
自定义 MCP Server 的配置方式也类似:
'my-mcp-server': {
command: 'node',
args: [
'/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs'
]
}
这表示通过 node 运行一个本地 .mjs 文件。只要这个文件实现了 MCP Server 规范,它暴露出来的工具就可以和高德地图、文件系统、浏览器工具放在一起使用。
这也是 MCP 设计上很灵活的地方:远程服务和本地服务可以同时存在,最终都会被统一转换成工具列表。
5.4 获取工具并绑定模型:让模型知道自己能调用什么
const tools = await mcpClient.getTools();
console.log(tools);
const modelWithTools = model.bindTools(tools);
mcpClient.getTools() 会连接前面配置的所有 MCP Server,读取它们提供的工具,并转换成 LangChain Tool。
这些工具一般会包含几个关键信息:
- 工具名称
- 工具描述
- 参数结构
- 调用方法
模型能不能正确调用工具,很大程度上取决于这些工具描述是否清楚。比如一个地图工具如果描述了“可以根据关键词搜索地点”,模型就知道遇到地点搜索任务时可以使用它;一个文件工具如果描述了“可以写入文件”,模型就知道保存 Markdown 时可以调用它。
model.bindTools(tools) 的作用是把工具列表绑定到模型上。绑定之后,模型在生成回复时就不只是输出普通文本,还可以输出结构化的 tool_calls。
可以这样理解:
绑定前:模型只能回答文本
绑定后:模型可以回答文本,也可以请求调用工具
当用户问“北京南站附近的 2 个酒店,并保存路线规划到 md 文件”时,模型不会直接编造结果,而是可以先请求调用地图工具,再请求调用文件系统工具。
5.5 Agent 循环:让模型和工具多轮协作
async function runAgentWithTools(query, maxIterations = 30) {
const messages = [
new HumanMessage(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.bgRed(`AI 回答:${response.content}`));
return response.content;
}
...
}
}
这是整个项目的核心函数。
函数接收两个参数:query 是用户输入的问题,maxIterations 是最大循环次数。设置最大循环次数是为了防止 Agent 无限调用工具。比如模型一直认为信息不够,不断请求工具调用,如果没有上限,程序就可能一直运行下去。
messages 是对话上下文数组。第一条消息是:
new HumanMessage(query)
它代表用户输入。后面每一轮模型回复、每一次工具结果,都会继续追加到这个数组里。
这行代码会调用绑定工具后的模型:
const response = await modelWithTools.invoke(messages);
此时模型会根据完整上下文做判断。如果它认为可以直接回答,就返回普通文本;如果它认为需要工具,就返回带有 tool_calls 的响应。
判断是否还有工具调用的代码如下:
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(chalk.bgRed(`AI 回答:${response.content}`));
return response.content;
}
这就是 Agent 停止循环的条件。只要模型不再请求工具调用,就说明它认为任务已经可以完成,于是返回最终回答。
5.6 执行工具调用:把模型的 tool_calls 变成真实动作
for (const toolCall of response.tool_calls) {
const foundTool = tools.find(t => t.name === toolCall.name);
if (!foundTool) {
continue;
}
const toolResult = await foundTool.invoke(toolCall.args);
const contentStr = stringifyToolResult(toolResult);
messages.push(new ToolMessage({
content: contentStr,
tool_call_id: toolCall.id
}));
}
模型返回的 tool_calls 不是工具执行结果,而是“需要调用某个工具”的请求。它通常包含:
name:要调用的工具名称args:调用工具时传入的参数id:本次工具调用的唯一标识
这行代码根据工具名找到真正的工具对象:
const foundTool = tools.find(t => t.name === toolCall.name);
如果找不到对应工具,说明模型请求了一个不存在的工具。示例里直接 continue 跳过,实际生产环境可以记录日志,或者把错误信息也包装成 ToolMessage 返回给模型,让模型尝试修正。
真正执行工具的是这一行:
const toolResult = await foundTool.invoke(toolCall.args);
invoke 是 LangChain Tool 的调用方法。这里传入的是模型生成的参数 toolCall.args。比如地图工具可能接收地点关键词、城市、经纬度;文件系统工具可能接收文件路径和文件内容。
执行后,工具会返回结果。这个结果可能是字符串,也可能是对象,所以示例中封装了一个转换函数:
function stringifyToolResult(toolResult) {
if (typeof toolResult === 'string') {
return toolResult;
}
if (toolResult?.content) {
return JSON.stringify(toolResult.content);
}
return JSON.stringify(toolResult);
}
为什么要把工具结果转成字符串?因为 ToolMessage 的 content 需要是可以放进对话上下文的内容。无论工具原始返回什么结构,最终都要变成模型可以继续阅读和分析的信息。
最后创建 ToolMessage:
messages.push(new ToolMessage({
content: contentStr,
tool_call_id: toolCall.id
}));
这里的 tool_call_id 很重要,它用来告诉模型:这条工具结果对应的是前面哪一次工具调用。尤其当模型一次请求多个工具时,tool_call_id 可以保证结果和请求对应起来。
这一步完成后,循环会进入下一轮:
工具结果进入 messages
↓
模型再次读取上下文
↓
模型决定继续调用工具,或者输出最终答案
这就是 Agent 工具调用的基本闭环。
5.7 执行任务并关闭客户端
await runAgentWithTools(
'北京南站附近的2个酒店,以及去的路线,路线规划生成文档保存到 当前目录 的一个 md 文件'
);
await mcpClient.close();
第一段代码启动整个任务。模型会根据这句话自动拆解动作:先找地点和酒店,再规划路线,最后调用文件系统工具写入 Markdown 文件。
第二段代码关闭 MCP Client。因为 MCP Client 可能启动了本地子进程,也可能维持着远程连接,所以任务结束后需要关闭资源。
这里要注意变量名要一致。如果前面创建的是:
const mcpClient = new MultiServerMCPClient(...)
最后就应该写:
await mcpClient.close();
不要写成:
await client.close();
否则会出现 client is not defined 之类的错误。
6. 从一次运行看懂完整链路
把前面的知识串起来,可以用一次任务运行来理解整个项目。
用户输入:
北京南站附近的2个酒店,以及去的路线,路线规划生成文档保存到 当前目录 的一个 md 文件
模型第一轮看到这个问题后,会发现自己没有实时地图数据,也不能凭空确认附近酒店,所以它会倾向于调用高德地图 MCP。高德地图 MCP 返回地点、酒店或路线相关信息后,代码会把这些结果包装成 ToolMessage 放回 messages。
模型第二轮读取上下文时,就能看到真实工具返回的信息。此时它可能继续调用路线规划工具,分别获取北京南站到两个酒店的路线。路线结果再次通过 ToolMessage 返回给模型。
当地图信息足够后,模型会开始整理 Markdown 文档内容。因为用户要求“保存到当前目录的一个 md 文件”,模型还需要调用 FileSystem MCP,把整理好的内容写入指定目录。
文件写入成功后,文件系统工具会返回执行结果。模型再次读取这个结果,如果确认任务已经完成,就不再发起工具调用,而是输出最终回答。
整个过程可以总结为:
自然语言需求
↓
模型判断任务需要哪些工具
↓
调用地图 MCP 获取真实信息
↓
调用文件系统 MCP 写入文档
↓
模型根据工具结果生成最终回复
这也是 MCP + Agent 最有价值的地方:开发者不用把所有步骤写死在业务代码里,而是提供工具能力和执行循环,让模型根据任务动态决定下一步该做什么。
7. 常见问题与优化建议
API Key 建议放到环境变量中。 地图 Key、模型 Key 都不应该直接写进代码。推荐使用 .env 管理,并把 .env 加入 .gitignore。
.env
文件系统 MCP 要严格限制目录。 文件系统工具拥有真实读写能力,只给它开放项目工作目录即可,不要开放系统目录、用户主目录或者包含隐私数据的目录。
工具返回结果要做兼容处理。 不同 MCP Server 返回的数据结构可能不一样,有的返回字符串,有的返回对象。统一转换成字符串后再放入 ToolMessage,可以减少模型处理上下文时的问题。
Agent 循环必须设置上限。 maxIterations = 30 的意义是防止模型反复调用工具。如果做生产级应用,还可以在达到上限时抛出错误,或者返回更清晰的失败信息。
生产环境要补充异常处理。 示例代码为了突出主流程,没有展开大量错误处理。真实项目中建议为模型调用、MCP 连接、工具执行、文件写入都增加 try...catch,并记录日志。
try {
const toolResult = await foundTool.invoke(toolCall.args);
} catch (error) {
console.error('工具调用失败:', error);
}
总结
MCP 本质上是工具调用协议,但它把工具从“项目里的一个函数”提升成了“可复用的标准服务”。通过 MCP,大模型应用可以统一接入地图、浏览器、文件系统、自定义业务服务等能力。
本文项目的核心链路是:
MultiServerMCPClient 连接多个 MCP Server
↓
getTools() 获取工具列表
↓
bindTools() 把工具绑定到模型
↓
模型根据用户问题生成 tool_calls
↓
代码执行对应工具
↓
ToolMessage 把工具结果交还给模型
↓
模型继续推理,直到任务完成
理解这条链路后,就能看懂大多数基于 MCP 的 Agent 项目。高德地图 MCP 负责提供位置和路线能力,Chrome DevTools MCP 负责浏览器控制,FileSystem MCP 负责本地文件读写,自定义 MCP Server 则可以扩展自己的业务能力。
当这些工具被统一接入后,AI 不再只是回答问题,而是可以在真实环境中查询、操作、整理和保存结果。这正是 MCP 在 AI Agent 开发中最重要的意义。
更多推荐



所有评论(0)