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

03 · MCP 工具桥接

用 wrapMcpTool 把 MCP 工具定义转换为内部 Tool 接口——模型不需要知道一个工具是内置的还是外部的。

统一的工具接口

第 3 章定义了 Tool 接口:

interface Tool {
  name: string;
  description: string;
  parameters: { type: "object"; properties: Record<string, unknown>; required: string[] };
  execute: (args: Record<string, unknown>, state: AgentState) => Promise<string>;
}

MCP server 返回的工具定义:

interface McpToolDefinition {
  name: string;
  description: string;
  inputSchema: {
    type: "object";
    properties?: Record<string, unknown>;
    required?: string[];
  };
}

两者高度相似:都有 name、description 和 JSON Schema 参数。唯一不同的是参数字段名(parameters vs inputSchema)和可选性(propertiesrequired 在 MCP 中是可选的)。

wrapMcpTool 桥接函数

src/mcp/client.ts 中:

export function wrapMcpTool(
  client: McpClient,
  definition: McpToolDefinition,
): Tool {
  return {
    name: definition.name,
    description: definition.description,
    parameters: {
      type: "object",
      properties: definition.inputSchema.properties ?? {},
      required: definition.inputSchema.required ?? [],
    },
    execute: async (args: Record<string, unknown>, _state: AgentState) => {
      const result = await client.callTool(definition.name, args);
      return result.content.map((c) => c.text).join("\n");
    },
  };
}

这个函数做了三件事:

1. 字段映射。 inputSchemaparameters,可选字段用 ?? 提供默认值。

2. 执行桥接。 execute 调用 client.callTool(),把 MCP 的响应格式转换为简单的字符串。MCP 工具可能返回多段内容(content 数组),拼接为一个字符串返回。

3. 错误透传。 MCP 工具执行失败时,isError: true,错误信息作为字符串返回给模型。模型能看到"外部工具执行失败"并调整策略。

模型视角:统一的工具列表

桥接后,从模型的视角看,所有工具都是一样的:

内置工具:
  search → Tool { name, description, parameters, execute }
  read_file → Tool { name, description, parameters, execute }

MCP 工具(通过 wrapMcpTool 桥接):
  search_docs → Tool { name, description, parameters, execute }
  query_issues → Tool { name, description, parameters, execute }

模型不需要知道一个工具是本地执行的还是远程调用的。它只看到统一的工具列表,按需调用。这是接口统一的威力——第 3 章设计的 Tool 接口恰好和 MCP 的工具定义高度兼容,桥接成本极低。

在 main.ts 中的集成

main.ts 中,MCP 工具在启动时加载并合并到工具列表:

let allTools: Tool[] = [...tools];  // 内置工具

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();
    const mcpTools = await client.listTools();
    const wrapped = mcpTools.map((def) => wrapMcpTool(client, def));
    allTools = [...allTools, ...wrapped];
    mcpClients.push(client);
  }
}

关键步骤:

  1. MCP_SERVERS 环境变量读取配置
  2. 为每个 server 创建 McpClient,连接并发现工具
  3. wrapMcpTool 把每个 MCP 工具包装为内部 Tool
  4. 合并到 allTools 数组

然后 runAgent(state, model, allTools, ...) 使用合并后的工具列表。agent 循环不关心工具的来源——它只调用 tool.execute()

清理

agent 退出时需要关闭 MCP 子进程:

rl.on("close", () => {
  for (const client of mcpClients) {
    client.disconnect();
  }
  // ...
});

disconnect() 调用 process.kill() 关闭子进程。不留孤儿进程。

向后兼容

如果 MCP_SERVERS 环境变量不存在,allTools 就是内置工具,行为和第 8 章完全一样。MCP 是完全可选的扩展——不需要就不配置,零影响。

下一节讨论 MCP 的边界和设计取舍。

登录以继续阅读

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

立即登录

On this page