Code Agent 解剖(23):从零扩展——接入 MCP 外部工具生态

一个问题引入

前两篇讲的扩展方式都是"自己做":自己写工具类、自己写 Skill 文件。

但世界上有很多现成的工具服务:Tavily 可以联网搜索,Context7 可以查最新文档,GitHub CLI 可以管理仓库......如果 agent 能直接调用这些服务,就不需要重复造轮子。

**MCP(Model Context Protocol)**就是解决这个问题的协议标准。它定义了一套规范:工具服务商按这个格式暴露工具,agent 框架按这个格式调用,双方约定好接口,不需要为每家服务写专门的适配器。


结论先说

接入一个 MCP 工具服务,只需要两步:

步骤 做什么
1. 配置 mcp_servers.json 告诉框架 MCP 服务在哪里、怎么启动
2. 启用 MCP --enable-mcp 启动参数,或 ENABLE_MCP=true 环境变量

框架会自动连接服务、发现工具列表、把每个远端工具注册为本地工具------之后的调用链与内置工具完全一致。


一、MCP 的基本模型

先理解 MCP 的工作方式,再来看代码。

MCP 定义了一个"工具服务器"的概念:一个独立的进程(或远端 HTTP 服务),里面注册了若干工具,每个工具有自己的名字、描述和参数 schema。

agent(客户端)与工具服务器通信的流程只有三步:

scss 复制代码
1. 握手:连接服务器
         ↓
2. 发现:list_tools() → 拿到工具列表(名字 + 描述 + inputSchema)
         ↓
3. 调用:call_tool(name, params) → 拿到结果

就这三步,没有更复杂的东西。协议的价值在于"标准化"------任何 MCP 服务器都支持这三步,任何 MCP 客户端都能与任何 MCP 服务器通信。

MyCodeAgent 实现了 MCP 的客户端侧,用 MCPClient 封装这三个操作。


二、配置文件:mcp_servers.json

配置一个 MCP 服务器,只需要在项目根目录创建(或修改)mcp_servers.json

json 复制代码
{
  "mcpServers": {
    "tavily": {
      "command": "uvx",
      "args": ["mcp-server-tavily"],
      "env": {
        "TAVILY_API_KEY": "tvly-xxxxx"
      }
    },
    "context7": {
      "command": "uvx",
      "args": ["--from", "context7-mcp", "context7-mcp"],
      "env": {
        "CTX7_API_KEY": "your-key"
      }
    }
  }
}

两个关键字段:

  • command:启动工具服务器的命令(uvx 是 Python 包运行工具,相当于无需安装直接运行)
  • args:传给 command 的参数
  • env:工具服务器需要的环境变量(API key 等)

还有一种远程 HTTP 服务器的配置方式:

json 复制代码
{
  "mcpServers": {
    "remote-tool": {
      "transport": "http",
      "url": "https://your-mcp-server.com/v1"
    }
  }
}

extensions/mcp/config.py 负责读取这个文件,并兼容 Claude 桌面端的 {"mcpServers": {...}} 包一层写法,让配置文件可以在不同 agent 之间复用。


三、连接和发现:register_mcp_servers 做了什么

当 agent 启动时(带 --enable-mcp),register_mcp_servers() 被调用:

python 复制代码
# extensions/mcp/bootstrap.py
def register_mcp_servers(tool_registry, project_root):
    # 1. 读取 mcp_servers.json
    servers = load_mcp_servers(project_root)
    
    # 2. 为每个 server 创建 MCPClient(还没连接)
    for server_name, spec in servers.items():
        config = _build_client_config(project_root, spec, MCPClientConfig)
        client = MCPClient(config)
        
        # 3. 连接 server,发现工具列表,注册到 ToolRegistry
        tools_meta = register_mcp_tools(tool_registry, client, namespace=server_name)

注意 namespace=server_name 这个参数。它解决了一个实际问题:两家不同的 MCP 服务器可能都有一个叫 search 的工具,加了 namespace 之后,它们分别变成 tavily:searchcontext7:search(冒号会被清理成下划线),不会冲突。


四、MCPToolAdapter:让远端工具变成本地工具

register_mcp_tools() 调用完 list_tools_sync() 之后,为每个远端工具创建一个 MCPToolAdapter

python 复制代码
# extensions/mcp/adapter.py
class MCPToolAdapter(Tool):
    """把 MCP 远端工具伪装成本地 Tool。"""

    def get_parameters(self) -> list[ToolParameter]:
        # 从 MCP 工具的 inputSchema 生成参数定义
        schema = self._schema  # 从 list_tools 拿到的 JSON Schema
        properties = schema.get("properties", {})
        required = set(schema.get("required", []))
        return [
            ToolParameter(name=name, type=spec.get("type", "any"), ...)
            for name, spec in properties.items()
        ]

    def run(self, parameters: dict) -> ToolResult:
        # 1. 校验参数
        invalid = self._validate_params(parameters)
        if invalid:
            return to_protocol_invalid_param(invalid, ...)

        # 2. 调用远端工具
        result = self._mcp_client.call_tool_sync(self._remote_name, parameters)

        # 3. 把结果转成标准信封
        return to_protocol_result(result, ...)

这个 Adapter 做了三件事:

  1. 参数接口对齐 :把 MCP 工具的 JSON Schema 翻译成 ToolParameter 列表,让 get_openai_tools() 能生成正确的 Function Calling schema
  2. 执行转发 :调用 call_tool_sync() 把请求发给远端
  3. 结果归一化 :把 MCP 格式的响应转成 ToolResult,让管道后续步骤不需要关心这个工具是本地还是远端

注册完成后,tavily_searchReadBash 在框架看来没有任何区别------它们都在 ToolRegistry 里,都会出现在 get_openai_tools() 返回的列表里,都经过同样的 Executor 管道执行。


五、完整链路:从配置到模型调用

把上面几步串起来,一个 MCP 工具从配置到被模型调用的完整链路:

scss 复制代码
写 mcp_servers.json,填入 server 配置
    │
    ▼
agent 启动(--enable-mcp)
    │
    ▼
bootstrap.py 调用 register_mcp_servers()
    │
    ├── 读 mcp_servers.json
    ├── 为每个 server 创建 MCPClient
    └── 连接 server,调 list_tools_sync(),拿到工具列表
         │
         ▼
         为每个工具创建 MCPToolAdapter
         MCPToolAdapter 注册到 ToolRegistry
             │
             ▼
             tool_registry.get_openai_tools()
             返回的工具列表里包含了 tavily_search
                  │
                  ▼
                  模型看到工具列表,决定调用 tavily_search
                       │
                       ▼
                       ToolRegistry.execute_tool("tavily_search", params)
                       → ToolExecutor 走四关卡(权限/锁/熔断/run)
                       → MCPToolAdapter.run()
                       → MCPClient.call_tool_sync("search", params)
                       → 结果转 ToolResult → 返回给模型

整条链路里,MCP 的特殊性只在 MCPToolAdapter.run() 这一层------其他所有部分(权限检查、熔断器、字节预算、结果写入历史)完全复用内置工具的代码。


六、两种 transport 的区别

_build_client_config() 根据配置决定用哪种传输方式:

stdio 模式(本地子进程):

json 复制代码
{
  "command": "uvx",
  "args": ["mcp-server-tavily"]
}

框架 fork 一个子进程运行工具服务器,通过 stdin/stdout 管道通信。启动时会产生一个新进程,子进程和 agent 进程同生命周期。

http 模式(远端 HTTP 服务):

json 复制代码
{
  "transport": "http",
  "url": "https://your-mcp-server.com/v1"
}

框架通过 HTTP 与远端服务通信,不需要启动子进程。适合托管在服务器上的 MCP 工具(比如付费 SaaS API)。

两种模式对上层代码完全透明------MCPToolAdapter.run() 不关心底层是子进程还是 HTTP,只管调 call_tool_sync()


七、单个 server 失败不影响整体启动

register_mcp_servers() 里有一个重要的错误隔离:

python 复制代码
for server_name, spec in servers.items():
    try:
        tools_meta = register_mcp_tools(tool_registry, client, namespace=server_name)
        registered_tools.extend(tools_meta)
    except Exception as exc:
        logger.warning("MCP tool registration failed for %s: %s", server_name, exc)
        continue  # 单个失败,继续处理下一个

如果 Tavily 服务器启动失败(比如密钥没填),不会影响 Context7 的注册,也不会导致整个 agent 启动失败。只会在日志里留一条 warning。

这个设计和 CompositeRuntimeEventSink 的思路一样(第 15 篇讲过):可观测性基础设施或外部服务的故障,不能让 agent 主路径停下来。


设计亮点

1. 适配器模式屏蔽了"远端"

MCPToolAdapter 让远端工具在框架内部和本地工具无法区分。权限检查、熔断器、字节预算------所有内置工具享有的保护,MCP 工具自动获得,不需要为 MCP 重写这些逻辑。

2. namespace 避免命名冲突

两个 MCP 服务器都有 search 工具?加了 namespace 就变成了 tavily_searchcontext7_search,模型能清楚区分,不会误调。

3. 配置格式兼容 Claude 桌面端

{"mcpServers": {...}} 这个格式是 Claude 桌面端 app 的标准写法。MyCodeAgent 兼容这个格式,意味着用户可以直接复用已经为 Claude 桌面端配置好的 MCP 配置文件,不需要二次翻译。


小结

设计选择 方案 工程价值
接入方式 JSON 配置文件 + --enable-mcp 零代码接入,改配置即可
工具注册 MCPToolAdapter 实现 Tool 接口 远端工具和本地工具走同一管道
命名空间 namespace:remote_name 前缀 多 server 不冲突
故障隔离 单 server 失败 continue 不影响其他 server 和 agent 启动
传输方式 stdio(本地进程)/ http(远端服务) 覆盖本地和云端两种部署场景

至此,Part 6「从零扩展」全部完成。回顾这四篇:

  • 20:添加新工具------Tool 基类、协议信封、沙箱检查、注册与测试
  • 21:接入新 LLM------表驱动的 provider 路由,加一行 profile 就搞定
  • 22:写 Skill------Markdown 定义专家行为,热加载,$ARGUMENTS 参数注入
  • 23:接入 MCP------配置文件 + 适配器,外部工具生态一键接入

这四个扩展维度,加在一起覆盖了你能想到的几乎所有"让 agent 做新事情"的需求:

  • 需要 agent 执行新的具体动作 → 工具
  • 需要 agent 用新的 LLM → provider
  • 需要 agent 学一种新的处理方式 → Skill
  • 需要 agent 调用现成的外部服务 → MCP

关于本系列的源码

本系列所有分析均基于开源项目 MyCodeAgent

源码里已经按照本系列文章的讲解顺序,在关键位置加入了配套注释------读文章时可以对照代码,也可以直接克隆下来自己跑、改、扩展,基于它开发你自己的 agent。

bash 复制代码
git clone https://github.com/chendongqi/MyCodeAgent
cd MyCodeAgent
cp .env.example .env   # 填入你的 LLM API key
uv sync
uv run python main.py

欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。

更多实用知识和有趣产品,欢迎访问我的个人主页

相关推荐
xian_wwq1 小时前
【学习笔记】深度认知系列-第16讲-提示词工程
人工智能·笔记·学习
乃嘿仔1 小时前
AI 热点日报 · 2026-09-04
人工智能·chatgpt
落羽的落羽1 小时前
【AI】快速理解AI应用的相关名词概念
linux·c++·人工智能·python·计算机网络·算法
不是株1 小时前
AI Agent 记忆系统全解:从 Markdown、SQLite、向量检索到 GraphRAG
sqlite·知识图谱·agent·memory·graphrag·harness
“AI国潮设计-小江”1 小时前
《Python实战 | SDXL大模型批量生成“英歌舞海浪”蛋糕IP,附核心Prompt控制代码与IP授权变现思路》
人工智能·python·prompt·aigc
虹科网络安全2 小时前
使用艾体宝 IOTA 10 CORE+ 监控企业网络中的 AI 流量
网络·人工智能
朝阳资本论2 小时前
群核科技:从空间设计龙头到物理AI“卖水人”的升维之战
人工智能
七夜zippoe2 小时前
为什么 2026 年每个 Java 团队都该懂 AI Agent
java·开发语言·人工智能
举个栗子。2 小时前
SwarmForge:AI 智能体协同编程框架,让多个 Agent 在隔离工作区并行协作
人工智能·开源·ai编程