05 · 实战:MCP 接入外部工具
展示 MCP 配置解析、工具发现和桥接的完整流程,以及未配置 MCP 时 agent 的行为。
从内置工具到外部工具
前三节实现了 MCP 类型定义、McpClient 和工具桥接。这一节用真实的 agent 运行展示 MCP 的接入效果——有配置和无配置两种场景。
场景一:未配置 MCP_SERVERS
当环境变量 MCP_SERVERS 未设置时,agent 只使用内置工具集:
--- MCP 配置 ---
未配置 MCP_SERVERS 环境变量
MCP 工具不会加载,agent 使用内置工具集
--- 内置工具列表 ---
- search: 在项目目录中搜索包含指定关键词的文件...
- glob: 按文件路径模式列出项目中的文件...
- read_file: 读取指定文件的内容...
- write_file: 创建或覆盖文件...
- patch_file: 对已有文件做局部修改...
- run_command: 在终端中执行命令并返回输出...
- git_status: 查看当前仓库的 git 状态...
- git_diff: 查看当前仓库的代码变更详情...
--- 工具总数: 8 (内置 8 + MCP 0) ---agent 完全正常运行,只是没有外部工具可用。这就是 MCP 的"可选扩展"设计——不配置就不加载,不影响任何内置能力。
场景二:配置 MCP Server 后
MCP server 的配置通过环境变量 MCP_SERVERS 传入,格式是 JSON 数组:
export MCP_SERVERS='[{
"name": "weather",
"command": "npx",
"args": ["-y", "weather-mcp-server"],
"env": { "API_KEY": "xxx" }
}]'main.ts 在启动时读取配置并连接:
const mcpServersJson = process.env.MCP_SERVERS;
if (mcpServersJson) {
const configs: McpServerConfig[] = JSON.parse(mcpServersJson);
for (const config of configs) {
const client = new McpClient(config);
await client.connect(); // 启动子进程,建立 stdio 连接
const mcpTools = await client.listTools(); // 发现可用工具
const wrapped = mcpTools.map(def => wrapMcpTool(client, def));
allTools = [...allTools, ...wrapped]; // 合并到工具列表
mcpClients.push(client);
}
}连接成功后,工具列表从 8 个变成了 8 + N 个。模型在系统提示词中看到 get_weather 工具,直接调用它——agent 的代码没有任何改动。
关键观察: 没有新增工具定义,没有修改系统提示词,没有改 agent.ts。所有变化都来自 MCP server 的接入。这就是"扩展机制"的意义。
MCP 工具同样受权限管控
MCP 工具在 agent 内部就是一个普通的 Tool 对象,它同样经过权限检查。如果 MCP server 提供的工具需要写文件或执行命令,权限系统同样会拦截。MCP 不是"安全后门"——它只是提供了更多工具,工具的使用仍然受权限管控。
退出时清理
当 agent 退出时(Ctrl+C 或任务完成),main.ts 会关闭所有 MCP 连接:
rl.on("close", () => {
for (const client of mcpClients) client.disconnect();
if (!busy) process.exit(0);
});disconnect() 会终止 MCP server 的子进程。MCP server 是无状态的,重启不影响任何数据。
wrapMcpTool 的桥接逻辑
MCP 工具定义和 agent 内部 Tool 接口的桥接:
const wrapped: Tool = {
name: mcpDef.name, // "get_weather"
description: mcpDef.description, // "获取指定城市的当前天气"
parameters: mcpDef.inputSchema, // { type: "object", properties: {...} }
execute: async (args) => {
const result = await client.callTool(mcpDef.name, args);
return JSON.stringify(result.content, null, 2);
}
};工具的执行被代理到 MCP server。agent 不知道也不需要知道工具是在本地执行还是远程执行的——对它来说,MCP 工具和内置工具都是同一个 Tool 接口。
完整的接入时序
main.ts McpClient MCP Server 子进程
───────── ───────── ──────────────────
解析 MCP_SERVERS
│
├─ new McpClient(config)
├─ client.connect() ────> spawn("npx", ["weather-server"])
│ send: {"method": "initialize", ...}
│ <─────────────────── recv: {"result": {"capabilities": ...}}
│
├─ client.listTools() ──> send: {"method": "tools/list"}
│ <─────────────────── recv: {"tools": [{ name: "get_weather", ... }]}
│
├─ wrapMcpTool(client, def)
└─ allTools.push(wrapped)
... agent 运行中 ...
model 返回: { name: "get_weather", arguments: { city: "北京" } }
│
├─ tool.execute(args) ──> send: {"method": "tools/call", ...}
│ <─────────────────── recv: {"content": [{"type": "text", "text": "..."}]}
│
└─ result 追加到对话
... agent 退出 ...
client.disconnect() ────> process.kill()整个流程通过 stdio 管道通信,JSON-RPC 协议。agent 和 MCP server 完全解耦。
这一章做了什么
回顾第 9 章的成果:
- MCP 类型系统:JSON-RPC 消息、工具定义、server 配置
- McpClient:子进程管理、stdio 通信、工具发现和调用
- wrapMcpTool:把 MCP 工具定义桥接为内部 Tool 接口
- 环境变量配置:
MCP_SERVERSJSON 数组,启动时加载 - 权限一致:MCP 工具同样受 PermissionGuard 管控
- 生命周期管理:退出时自动断开所有 MCP 连接
agent 从第 8 章的"封闭工具箱"进化为"可扩展平台"。下一章会实现 SubAgent 协作,用多个 agent 分工完成复杂任务。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。