大家好!我是你们的 AI 炼丹师。👋
在前几期的文章里,我们聊了 MCP(Model Context Protocol)的概念,甚至还手搓了一个简单的本地 MCP Server。很多同学在后台私信:“道理我都懂,但每次都要自己写 Server 还是很累啊!有没有现成的轮子可以用?”
有!必须有! 🚗
MCP 的生态正在以爆炸般的速度增长。今天,我们不再纸上谈兵,直接上代码实战!我们要干一件很酷的事情:在一个 Agent 里同时集成高德地图、本地文件系统、Chrome 浏览器控制这三大神级能力,而且——一行工具逻辑的代码都不用我们写!
准备好了吗?硬核实战,Starting Now! 🚀
🏗️ 核心基建:LangChain 的 MCP 适配器
首先,我们的“地基”依然是 LangChain。为了让 LangChain 能听懂 MCP 协议,我们需要引入那个熟悉的“翻译官”。
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
🔍 深度解析:
MultiServerMCPClient 是我们的核心指挥官。它的厉害之处在于“Multi”——它允许我们同时配置多个 MCP Server。不管你是跑在本地的 Node 进程,还是远在天边的 HTTP 服务,它都能统一管理,把它们提供的 Tools 聚合在一起,打包送给 LLM。
🎯 今日挑战:Agent 的“铁人三项”
为了验证 MCP 的强大,我们给 AI 布置了三个层层递进的任务:
- 任务一(问路):查询北京南站附近的酒店及路线。(考验地理信息获取能力)
- 任务二(文秘):规划路线并生成 Markdown 文档保存到本地。(考验文件系统读写能力)
- 任务三(上网):拿到酒店图片,打开浏览器展示,并修改标签页标题。(考验浏览器控制能力)
这要是在以前,我得写三个复杂的 Tool 类,还得调试半天 API。但今天,我们用 MCP 来秒杀它们。
🗺️ 第一关:接入高德地图 (Remote MCP)
先来解决地图问题。我们不需要自己去申请高德 API Key、看文档、写 HTTP 请求。我们直接使用高德提供的 MCP 服务。
1. 配置远程 MCP
"amap-maps": {
"url": `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
},
🔍 硬核讲解: 大家发现了吗?这个配置和我们昨天的本地配置不一样!
- 本地 MCP:需要指定
command(如node) 和args(脚本路径),Client 负责启动和管理进程。 - 远程 MCP:只需要一个
url!Client 会通过 SSE (Server-Sent Events) 等标准协议与远程服务建立连接。
只要配置好这个 URL,你的 Agent 瞬间就拥有了查询 POI(兴趣点)、路径规划、逆地理编码等全套高德地图能力。这就是云端赋能的魅力。☁️
可以看到工具非常丰富。
2. 更加健壮的 ToolResult 处理
接入了高德 MCP 后,Agent 就可以开始工作了。但在处理工具返回结果时,我们需要格外注意。
重点看这里:
const toolResult = await foundTool.invoke(toolCall.args);
let contentStr;
// 🛡️ 防御性编程:解包 ToolResult
if (typeof toolResult === 'string') {
contentStr = toolResult;
} else if (toolResult && toolResult.text) {
contentStr = toolResult.text;
}
🔍 为什么要这么写?
MCP 协议规定,Tool 的返回结果通常是一个包含 content 数组的对象,每一项可能有 type: 'text' 和 text 字段。
- 有的 SDK 封装得好,直接给你返回字符串。
- 有的比较原始,返回的是 JSON 对象
{ content: [...] }或者{ text: "..." }。 所以,为了防止 Agent 因为看不懂结果而报错,我们需要做一个简单的“拆包”处理,把真正的内容拿出来喂给 LLM。
📂 第二关:接入文件系统 (FileSystem MCP)
第二个任务要求把结果保存为文件。以前我们要写 fs.writeFileSync,还要担心 AI 会不会把我的 C 盘格机了。😱
现在,我们使用 MCP 官方提供的 FileSystem Server。
1. 使用 npx 即时加载
// mcp 官网提供
"filesystem": {
"command": "npx", // 🔥 亮点来了
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"C:\\Users\\MR\\Desktop\\workspace\\lesson_jp\\ai\\agent\\mcp_in_action\\mcp-test" // 操作范围
]
},
🔍 硬核讲解:
这里我们用到了 npx。这意味着我们甚至不需要在项目中 npm install 这个包!
command: "npx": 告诉 Client,启动服务的方式是运行npx命令。"-y": 必考题! 它的意思是 "Yes"。当你第一次用npx运行某个包时,它通常会问你“是否安装... (y/n)”。但在 MCP 的自动化流程里,没人去按那个 'y',所以必须加上这个参数自动确认,否则进程会卡死。@modelcontextprotocol/server-filesystem: 这是官方标准包的名字。
2. 安全沙箱机制 🛡️
注意看参数的最后一行:
"C:\\...\\mcp-test"
这非常关键! 这是文件系统的挂载点(Mount Point)。 MCP 的文件系统服务是受限的。我们显式地告诉它:“你只能在这个文件夹里读写文件”。 这样,无论 LLM 怎么发疯,它都绝对无法触碰到这个文件夹以外的任何文件。这就是 MCP 在架构层面提供的安全保障。
3. 成果展示
看看 AI 生成的文档:
# 北京南站附近酒店及路线指南
...
### 1. 米家青年酒店(北京南站店)
- **步行路线**:
- 沿南站幸福路向西步行98米右转...
...
> Note: All coordinates and route information are based on high-precision GPS data from Amap services.
完美!数据来自高德 MCP,写入通过 FileSystem MCP,配合得天衣无缝。✨
🌐 第三关:掌控浏览器 (Chrome DevTools MCP)
任务升级!我们要让 AI 打开浏览器给我们看图片。这需要更高级的交互。我们将引入 Google 官方的 Chrome DevTools MCP。
1. 配置浏览器控制台
"chrome-devtools": {
"command": "npx",
"args": [
"-y",
"chrome-devtools-mcp@latest" // 使用最新版本
]
}
🔍 玩法解析:
这里同样使用了 npx 和 -y。chrome-devtools-mcp 是一个非常强大的工具,它允许 Agent 通过 CDP (Chrome DevTools Protocol) 协议控制你的浏览器。
- 它可以打开 Tab。
- 它可以截图。
- 它可以执行 JS 脚本。
- 它可以读取控制台日志。
2. 执行效果
当我们下达任务:
“北京南站附近的三个酒店,拿到酒店图片,展开浏览器,展示每个酒店的图片...并且把那个页面标题改为酒店名”
AI 会怎么做?
- 调用 高德 MCP 搜索酒店,拿到图片 URL。
- 调用 Chrome MCP 的
open_tab工具打开 URL。 - 调用 Chrome MCP 的
evaluate_javascript工具修改document.title。
你将亲眼看到浏览器窗口一个个弹出,页面自动跳转,标题自动变更。这就是 Agent Native 的体验!🤖🖥️
💡 总结与思考
今天这堂实战课,我们没有写任何具体的 Tool 逻辑代码,仅仅通过配置 JSON,就赋予了 Agent 地图搜索、文件读写、浏览器控制三大神技。
这给我们带来了什么启示?
- Don't Repeat Yourself (DRY): 不要重复造轮子。社区里有现成的 MCP Server,拿来用就是了。
- 组合式创新: 我们的工作重心从“怎么写工具”转移到了“怎么组合工具”。Agent 的能力边界取决于你接入了多少 MCP Server。
- 异构融合: 高德(云端)、文件系统(本地 Node)、浏览器(本地 App),通过 MCP 协议,它们在同一个 Agent 的上下文中和谐共存。
未来已来。不需要等到明天,现在就去挑选几个好用的 MCP Server,给你的 Agent 装备上吧!
如果觉得文章有用,记得点赞收藏,咱们下期见!👋
附:环境依赖
- Node.js 环境
@langchain/mcp-adapters@langchain/openai- 环境变量
AMAP_MAPS_API_KEY(高德地图 Key)- 环境变量
OPENAI_API_KEY
完整代码
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: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.OPENAI_BASE_URL
});
const mcpClient = new MultiServerMCPClient({
mcpServers: {
"amap-maps": {
"url": `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
},
// mcp 官网提供
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"C:\\Users\\MR\\Desktop\\workspace\\lesson_jp\\ai\\agent\\mcp_in_action\\mcp-test" // 操作范围
]
},
"chrome-devtools": {
"command": "npx",
"args": [
"-y",
"chrome-devtools-mcp@latest"
]
}
}
})
const tools = await mcpClient.getTools();
const modelWithTools = model.bindTools(tools);
async function runAgentWithTools(query, maxIterations = 30) {
const messages = [
new HumanMessage(query)
];
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgGreen('⏳正在等待AI思考...'));
const response = await modelWithTools.invoke(messages);
messages.push(response);
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(`\n AI 最终回复:\n ${response.content}\n`);
return response.content;
}
console.log(chalk.bgBlue(`🔍 检测到 ${response.tool_calls.length} 个工具调用`));
console.log(chalk.bgBlue(`🔍 工具调用: ${response.tool_calls.map(t => t.name).join(', ')}`));
for (const toolCall of response.tool_calls) {
const foundTool = tools.find(t => t.name === toolCall.name);
if (foundTool) {
const toolResult = await foundTool.invoke(toolCall.args);
let contentStr;
if (typeof toolResult === 'string') {
contentStr = toolResult;
} else if (toolResult && toolResult.text) {
contentStr = toolResult.text;
}
messages.push(new ToolMessage({
content: contentStr,
tool_call_id: toolCall.id
}));
}
}
}
return messages[messages.length - 1].content;
}
// await runAgentWithTools('北京南站附近的酒店,以及去的路线');
// await runAgentWithTools(`北京南站附近的2个酒店,以及去的路线,
// 路线规划生成文档保存到当前目录的一个 md 文件`);
await runAgentWithTools(`
北京南站附近的三个酒店,拿到酒店图片,展开浏览器,展示每个酒店的图片
每个tap一个url展示,并且把那个页面标题改为酒店名
`)
await mcpClient.close();