我的 Agent 只会 4 件事:读文件、写文件、列目录、跑命令。直到我发现了 MCP 协议------它让 Agent 的工具箱从一个固定大小的工具箱,变成了一个可以无限插拔的插座。
一、4 个工具的极限
上篇文章里,我搭好了一个能自主编程的 AI Agent。它有一个 ReAct 循环的大脑,还有 4 个工具作为四肢:
read_file → 读取文件内容
write_file → 写入文件(自动创建目录)
list_directory → 列出目录
exec_command → 执行 shell 命令
一开始觉得很够用。创建项目?exec_command 跑 pnpm create vite。写组件?write_file 覆盖 App.tsx。装依赖?exec_command 跑 pnpm install。
直到我想让它做这些事:
- "帮我搜一下项目里所有用到
useState的文件" - "把这次改动提交到 git,写一条规范的 commit message"
- "看看这段代码最后一次是谁改的"
Agent 的回答是:
makefile
我无法搜索文件内容。可用工具: read_file, write_file, list_directory, exec_command
问题暴露了:Agent 的能力天花板就是工具的数量。每想加一个能力,就得写一个新工具文件、定义 Zod schema、注册到 index.js。这跟装软件只能从源代码编译有什么区别?
更难受的是:隔壁 Cursor、Claude Code 里的那些工具------Git 操作、GitHub Issue、数据库查询------人家早就写好了,但我得自己重新实现一遍。
有没有一种方式,能让 Agent 直接接入已有的工具生态?
二、MCP 协议:工具的"USB 接口"
在翻 LangChain 文档时,我看到了一个词:MCP(Model Context Protocol)。
简单说,MCP 定义了一套标准------就像 USB 协议让任何 U 盘都能插进任何电脑一样,MCP 让任何 MCP Server 提供的工具都能被任何 MCP Client(也就是我的 Agent)使用。
架构长这样:
arduino
┌─────────────────────┐ ┌──────────────────────┐
│ 我的 Agent │ │ Filesystem Server │
│ (MCP Client) │ │ (MCP Server) │
│ │ │ │
│ "帮我搜所有 .tsx" │ │ - search_files │
│ │ │ │ - directory_tree │
│ ▼ │ │ - edit_file │
│ tools/list ───────┼─────────►│ - get_file_info │
│ │ │ - move_file │
│ tools/call ───────┼─────────►│ ... 14 个工具 │
│ │ │ │
└─────────────────────┘ └──────────────────────┘
最妙的是:我不用写一行工具代码 。search_files、directory_tree、edit_file 这些功能,Filesystem Server 已经实现好了。我只需要告诉 Agent 去哪找这个 Server。
三、集成:两行配置,14 个工具到手
LangChain 官方有 @langchain/mcp-adapters 这个包,专门做 MCP → LangChain 的桥接。核心 API 就两个:
javascript
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
// 1. 创建客户端,传入 Server 配置
const client = new MultiServerMCPClient({
mcpServers: {
filesystem: {
transport: 'stdio',
command: 'node',
args: ['./node_modules/@modelcontextprotocol/server-filesystem/dist/index.js', '.']
}
}
});
// 2. 初始化连接 → 获取工具
const toolsByServer = await client.initializeConnections();
const mcpTools = await client.getTools();
// → [DynamicStructuredTool × 14]
getTools() 返回的是什么?是 LangChain 原生的 DynamicStructuredTool 对象------跟我的 read_file、write_file 完全是同一种东西。直接 model.bindTools([...localTools, ...mcpTools]) 就完事了,Agent 根本感觉不到区别。
运行效果:
csharp
[MCP] 连接 1 个服务器: filesystem
[MCP] ✓ filesystem: 14 个工具
[MCP] 共加载 14 个 MCP 工具
🔧 可用工具 (15): read_file, write_file, list_directory, exec_command,
search_files, directory_tree, edit_file, move_file,
read_multiple_files, get_file_info, create_directory,
read_text_file, read_media_file,
list_directory_with_sizes, list_allowed_directories
从 4 个到 15 个,我只写了两样东西:一个 JSON 配置文件,一个不到 130 行的 McpClientManager 封装类。
四、三个踩过的坑
坑 1:npx 罢工了
最初配置长这样:
json
{
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
}
一运行就报错:
lua
EBADDEVENGINES Invalid name "pnpm" does not match "npm" for "packageManager"
原因是我项目的 package.json 里写了 "devEngines": { "packageManager": "pnpm" }。npx(npm 家的)检测到这个约束,直接拒绝执行------即使它只是用来跑一个跟项目无关的 MCP Server。
解决方案 :先 pnpm add @modelcontextprotocol/server-filesystem 安装到项目,再用 node ./node_modules/.../dist/index.js 直接启动 Server 进程。绕过了包管理器的纠纷。
坑 2:工具名打架
Filesystem Server 提供了 read_file、write_file、list_directory 三个工具------跟我本地写的完全同名。
如果不处理,bindTools 会绑进去两个 read_file。模型调用时到底走哪个?不知道,行为不确定。
解决方案 :在 McpClientManager.getTools() 里加一层去重:
javascript
const localNames = new Set(localTools.map(t => t.name));
const newMcpTools = mcpTools.filter(t => !localNames.has(t.name));
// 本地工具优先 --- 它们没有路径白名单限制,权限更高
结果:3 个重名 MCP 工具被自动跳过,最终 15 个工具(4 本地 + 11 MCP 新增),无冲突。
坑 3:连接失败不能阻塞 Agent
MCP Server 是一个独立进程。它可能因为各种原因挂掉------网络问题、依赖缺失、进程崩溃。
如果 Server 连不上就直接 throw,那 Agent 就完全没法启动了------即使 4 个本地工具完全正常。
解决方案:降级策略。
javascript
try {
await client.initializeConnections();
} catch (e) {
logger.warn(`[MCP] 连接失败: ${e.message}`);
this.#connected = false; // 仅用本地工具,Agent 照常启动
}
MCP 是锦上添花,不是雪中送炭。它不应该成为 Agent 的单点故障。
五、封装:McpClientManager 的 4 个职责
我把所有 MCP 逻辑收敛到一个 McpClientManager 类里(完整代码),它只做 4 件事:
| 职责 | 实现 |
|---|---|
| 初始化连接 | init({ servers }) → new MultiServerMCPClient() → initializeConnections() |
| 工具合并去重 | getTools(localTools) → 本地优先,过滤 MCP 重名工具 |
| 错误降级 | try/catch 所有 MCP 操作,失败时退化为仅本地工具 |
| 生命周期清理 | close() 关闭所有 MCP 连接,main.js 的 finally 块确保执行 |
用法:
javascript
const mcp = await initMcpClient({ servers: config.mcp.servers });
const tools = await mcp.getTools(localTools);
const modelWithTools = model.bindTools(tools);
const agent = new ReactAgent({ model: modelWithTools, tools, ... });
try {
await agent.run(task);
} finally {
await mcp.close();
}
六、现在我的 Agent 能做什么
加上 Filesystem MCP Server 后,Agent 多了这些能力:
perl
🔍 search_files → 按 glob 模式搜索文件(再也不用 exec_command + grep 了)
📂 directory_tree → 可视化目录树("帮我看看项目结构")
✏️ edit_file → 基于 diff 精确编辑(而不是整个文件覆盖)
📦 read_multiple_files → 批量读取("把 src/ 下所有 .ts 文件读出来")
📁 move_file → 移动/重命名文件
ℹ️ get_file_info → 获取文件大小、修改时间等元信息
而且这只是开始。mcp-servers.json 里加一行配置就能接更多 Server:
json
{
"mcpServers": {
"filesystem": { "command": "node", "args": [...] },
"github": { "command": "node", "args": [...] }, // ← 加这个就有 GitHub Issue/PR 能力
"postgres": { "transport": "streamableHttp", "url": "..." } // ← 加这个就能查数据库
}
}
七、总结:工具系统的思维转变
做这个集成的过程中,我最大的收获不是技术本身,而是一个思维转变:
传统思维 :Agent 需要什么工具 → 我写什么工具 MCP 思维:Agent 需要什么能力 → 找现成的 MCP Server → 配置一行 JSON
前者是个工具箱,你自己买工具往里塞。后者是个插座,你能插上整个世界的电器。
对于个人开发者来说,这意味着你不需要重新实现 Git 操作、GitHub API、数据库查询------社区已经写好了 MCP Server,你的 Agent 直接用就行。
对于团队来说,这意味着你们的工具可以跨项目复用。写一个内部 API 的 MCP Server,所有项目的 Agent 都能调用------不管你用的是 LangChain、Claude Code 还是 Cursor。
这就是 MCP 协议真正的价值:让 AI Agent 的工具系统,从"手工业"进入"工业化"。