你是否也遇到过这种情况:对着 ChatGPT 或 DeepSeek 咆哮了半天"帮我查一下昨天数据库里新增的用户",或者"帮我把这个网页截个图",AI 却只能委屈地敲出一行:"对不起,作为一个语言模型,我无法访问您的本地数据,也无法操控您的浏览器。"
这时候,你恨不得顺着网线过去给它两拳。
传统的 AI 模型就像是一个智商爆表但"断网、断电、无手无脚"的闭关学者 。为了让它能干活,我们以前会给它写 Tools(工具回调),但每个项目的工具写法千奇百怪。
直到 MCP(Model Context Protocol,模型上下文协议) 诞生,游戏规则变了。今天,我们就来扒开 MCP 的神秘面纱,看看如何用它和 LangChain 强强联手,把一个"只会聊天"的 AI 变成能查数据库、能控浏览器、能读写文件的"超级特工(Agent)"。
一、 什么是 MCP?(小白也能听懂的"普通话"解释)
在正式敲代码之前,我们先搞懂什么是 MCP。别慌,我们不用复杂的网络术语。
我们可以把 AI 模型想象成一部智能手机的操作系统(比如 iOS 或 Android)。
- 在没有统一协议的年代:每家硬件厂商(高德地图、你的本地文件、Chrome 浏览器)为了连接手机,都得自己焊一套独特的接口。手机系统想调用它们?对不起,得针对每一个硬件单独写几千行兼容代码。
- 有了 MCP 协议之后 :它就像是 USB-C 接口规范。MCP 协议规定了统一的插口标准。任何厂商(MCP Server)只要做成 USB-C 接口,就能直接插到支持 MCP 的客户端(MCP Client,比如 Cursor 或我们的 LangChain 脚本)上。
核心三要素
-
MCP Client(客户端) :相当于大脑的主控制器。它负责调配 AI 模型,并向各种工具发出指令。
-
MCP Server(服务端) :相当于各种各样的"外设器官"。它守着自己的"一亩三分地"(比如数据库、本地文件系统系统、浏览器驱动),等着 Client 传指令过来,然后执行具体操作。
-
通信通道(Transport) :Client 和 Server 之间怎么说话?通常有两种方式:
- Stdio(标准输入输出) :就像两个进程在同一台电脑里用"悄悄话"管道通信,速度极快,适合本地扩展。
- HTTP/SSE(网络传输) :通过网络请求远端服务,适合跨服务器调用。
有了这个大前提,任何人开发的 MCP 工具,都能无缝接入任何支持 MCP 的 AI 系统中。这就是它的魅力所在。
二、 动手建一个自己的"秘密基地":自定义 MCP Server
光说不练假把式。我们先用 Node.js 亲手撸一个本地的 MCP Server。这个 Server 将扮演一个"档案管理员",手里握着一份假数据,并且提供工具(Tools)和静态资源(Resources) 。
1. 基础骨架与数据准备
首先,我们引入官方的 SDK,并初始化一个 MCP 服务端实例。为了让数据校验更严谨,我们引入 zod 库来做入参检查。
JavaScript
javascript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
// 模拟一个数据库,里面存了几个用户的"绝密档案"
const database = {
users: {
'001': { id: '001', name: '祖豪', email: 'zh@qq.com', role: 'admin' },
'002': { id: '002', name: '光光', email: 'gg@qq.com', role: 'user' },
'003': { id: '003', name: '小红', email: 'xh@qq.com', role: 'user' },
}
};
// 创建 Server 实例,给它起个响亮的名字
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0'
});
2. 注册动态工具:query_user
有了服务,我们得给 AI 提供"抓手"。我们注册一个名为 query_user 的工具。当 AI 发现人类想查用户时,就会把"用户ID"传给这个工具。
JavaScript
javascript
server.registerTool('query_user', {
description: '查询数据库中的用户信息。输入用户ID, 返回该用户的详细信息(姓名、邮箱、角色)',
inputSchema: {
// 限制入参必须是字符串,并用 describe 贴心地告诉 AI 应该传什么格式
userId: z.string().describe('用户ID, 例如:001, 002, 003')
}
}, async ({ userId }) => {
const user = database.users[userId];
// 如果找不到用户,别玩失踪,礼貌地告诉 AI
if (!user) {
return {
content: [{
type: 'text',
text: `用户 ID ${userId} 不存在。可用的ID: 001, 002, 003`
}]
};
}
// 找到了,打包成标准格式扔给 AI
return {
content: [{
type: 'text',
text: `用户 ${user.id} 的信息是:姓名: ${user.name}, 邮箱: ${user.email}, 角色: ${user.role}`
}]
};
});
3. 注册静态资源:使用指南
除了能动态执行代码的"工具",MCP 还支持"资源(Resource)"。资源就像是摆在架子上的书,AI 可以走过去翻阅它。比如,我们给它准备一本《使用指南》。
JavaScript
php
server.registerResource(
'使用指南',
'docs://guide', // 资源唯一的 URI 地址,类似于网址
{
description: 'MCP Server 使用指南',
mimeType: 'text/plain',
},
async () => {
return {
contents: [{
uri: 'docs://guide',
mimeType: 'text/plain',
text: `
MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 Cursor 等 MCP Client 中通过自然语言对话,Cursor 会自动调用相应工具。
`
}]
};
}
);
4. 开启通信管道
最后一步,让这个 Server 跑起来。我们选择最基础、最稳健的 Stdio(标准输入输出)方式。
JavaScript
csharp
// 采用跨进程的标准输入输出通信方式
const transport = new StdioServerTransport();
await server.connect(transport);
至此,我们的本地 MCP Server 就写好了。它就像一个守门人,静静等待着主进程的召唤。
三、 组装"大黄蜂":在 LangChain 中驱动多 MCP 核心
有了服务端,接下来就是重头戏:编写客户端,用 LangChain 驱动 AI,让它自己学会查阅资源、调用工具。
我们要用到 @langchain/mcp-adapters 库中的 MultiServerMCPClient。顾名思义,它能同时粘合多个不同的 MCP 模块!
1. 初始化 AI 模型与 MCP 客户端
我们先配置好我们的大脑(大模型)以及手脚(MCP 客户端)。
JavaScript
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';
// 1. 初始化大语言模型(这里以强大的 DeepSeek 为例)
const model = new ChatOpenAI({
modelName: 'deepseek-v4-pro',
apiKey: process.env.DEEPSEEK_API_KEY,
temperature: 0, // 搞工程,我们要让 AI 稳重,拒绝瞎编,所以创造性设为 0
configuration: {
baseURL: 'https://api.deepseek.com/v1',
},
});
// 2. 组装我们的 MCP 客户端,把刚刚写的本地 Server 配进去
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'my-mcp-server': {
command: 'node', // 用 node 命令启动
args: ['D:/Workspace/xld_ai/ai/agent_in_action/mcp-demo/src/my-mcp-server.mjs'] // 你的文件绝对路径
}
}
});
2. 抽取资源,喂给 AI 当背景知识
还记得我们在 Server 里写的《使用指南》资源吗?在对话开始前,Client 可以先把这些静态资源读出来,塞进 AI 的 SystemMessage(系统提示词)里,让 AI 提前对当前环境有个认知。
JavaScript
ini
// 获取所有注册的工具和资源
const tools = await mcpClient.getTools();
const res = await mcpClient.listResources();
let resourceContent = '';
// 遍历所有服务返回的资源,把它拼成一个大字符串
for (const [serverName, resources] of Object.entries(res)) {
for (const resource of resources) {
const content = await mcpClient.readResource(serverName, resource.uri);
resourceContent += content[0].text;
}
}
console.log(chalk.gray('--- 读取到的组件资源 ---'));
console.log(resourceContent);
// 将工具绑定到大模型上,让模型具备"选择工具"的能力
const modelWithTools = model.bindTools(tools);
3. 核心:编写 Agent 的"思考-执行"循环循环(ReAct 架构)
AI 是怎么用工具的?其实是一个循环过程:
- 思考:用户问了一个问题,我现有的知识答不上来,我应该用哪个工具?
- 决策 :决定调用
query_user工具,参数是002。 - 执行 :代码拦截到 AI 的决策,去跑真正的工具代码,拿到结果(如:"光光,gg@qq.com")。
- 反馈:把结果再喂回给 AI,AI 恍然大悟,最后组织语言回答人类。
我们用一个 for 循环来帮 AI 实现这个自主思考的闭环:
JavaScript
javascript
async function runAgentWithTools(query, maxIterations = 30) {
// 初始化消息队列,把资源指南作为系统背景,加上用户的提问
const messages = [
new SystemMessage(resourceContent),
new HumanMessage(query)
];
for (let i = 0; i < maxIterations; i++) {
console.log(chalk.bgGreen(`正在等待AI思考, 第${i + 1}轮....`));
// 让 AI 来看当前的所有对话记录,并作出回应
const response = await modelWithTools.invoke(messages);
messages.push(response); // 把 AI 的回应先记录下来
// 关键判断:如果 AI 觉得不需要调用工具了,说明它已经得出最终答案了,直接退出
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(chalk.green(`\nAI 最终回复:\n${response.content}`));
return response.content;
}
// 来到这里,说明 AI 喊话了:"我要用工具!"
console.log(chalk.bgBlue(`检测到 ${response.tool_calls.length} 个工具调用`));
console.log(chalk.blue(`工具调用: ${response.tool_calls.map(t => t.name).join(', ')}`));
// 遍历 AI 想要调用的每一个工具
for (const toolCall of response.tool_calls) {
// 从我们客户端维护的 tools 列表里找到对应的那项
const foundTool = tools.find(t => t.name === toolCall.name);
if (foundTool) {
// 真正去执行工具,传入 AI 帮我们分析好的参数
const toolResult = await foundTool.invoke(toolCall.args);
// 【核心注意点】:一定要带上 tool_call_id,这相当于给回复打上标签,AI 才知道这个数据对应的是哪个请求
messages.push(new ToolMessage({
content: typeof toolResult === 'string' ? toolResult : (toolResult.text || JSON.stringify(toolResult)),
tool_call_id: toolCall.id
}));
}
}
}
return messages[messages.length - 1].content;
}
// 狂奔吧,Agent!
await runAgentWithTools('查一下用户002的信息,顺便看看MCP Server的使用指南是什么?');
// 大功告成后,别忘了优雅地关闭客户端,释放子进程资源
await mcpClient.close();
四、 想象力炸裂:多 Server 联动的"乐高式"AI 工作流
如果 MCP 只能查个本地假数据,那真是格局小了。MCP 真正的恐怖之处在于生态复用。
既然它是"标准接口",这意味着我们可以把"高德地图"、"Chrome 浏览器驱动"、"本地文件系统"像搭乐高一样一次性全插在同一个 Client 上。
来看看下面这个疯狂的进阶配置:
JavaScript
arduino
const mcpClient = new MultiServerMCPClient({
// 1. 远程 HTTP 接入高德地图 MCP:查位置、查路线
'amap-maps': {
url: 'https://mcp.amap.com/mcp?key=YOUR_AMAP_KEY'
},
// 2. 操控 Chrome 浏览器:打开网页、点击元素、截图、爬虫
'chrome-devtools': {
command: 'npx',
args: ['-y', 'chrome-devtools-mcp@latest']
},
// 3. 读写本地文件系统:直接写代码、存文件
'filesystem': {
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', 'D:/Workspace/xld_ai']
}
});
当你把上面这堆东西绑好,再丢给 Agent 一句极其复杂的任务:
"帮我拿到东华理工大学抚州校区的地图图片,然后自动打开我的浏览器,展示这张校区图片。"
AI 看到这个任务,脑子里会自动拆解出完美的交响乐章:
- 调用
amap-maps工具去搜索"东华理工大学抚州校区"的地理位置和图片链接。 - 调用
filesystem工具在本地创建一个工作目录,把图片数据落盘。 - 调用
chrome-devtools工具自动拉起一个真的 Chrome 浏览器,跳转到对应的本地文件或者图片地址上展示给你看。
整个过程不需要你为它定制编写一行有关"如何截图、如何开浏览器"的底层业务代码,你只是引入了三个现成的、标准的 MCP 服务。
五、 避坑指南与总结
在玩转 MCP 的路上,有几个底层的技术细节需要刻在DNA里:
tool_call_id绝对不能丢 :在多轮对话中,AI 发出了调用工具的请求,当你把工具结果返回给 AI 时,必须用ToolMessage并在属性里带上tool_call_id。这就跟去饭店吃饭拿小票取餐一样,对不上号 AI 就会陷入逻辑混乱。- 及时
close()释放资源 :使用StdioServerTransport时,客户端实质上是通过 Node.js 的child_process启动了子进程来和 Server 通信的。如果不手动调用await mcpClient.close(),这些子进程和通信通道就会一直挂在后台,导致你的脚本死活不退出,白白榨干你的 CPU 资源。
MCP 协议的出现,划清了"大模型负责思考"与"周边生态负责执行"的界线。作为开发者的我们,不用再重复造轮子去写各种杂乱的 API 适配器了。只要拥抱 MCP,世界上的所有工具,都将在一夜之间成为你 Agent 的不二臣子!