第 9 章 · MCP 与工具扩展协议

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_SERVERS JSON 数组,启动时加载
  • 权限一致:MCP 工具同样受 PermissionGuard 管控
  • 生命周期管理:退出时自动断开所有 MCP 连接

agent 从第 8 章的"封闭工具箱"进化为"可扩展平台"。下一章会实现 SubAgent 协作,用多个 agent 分工完成复杂任务。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page