一个问题引入
前两篇讲的扩展方式都是"自己做":自己写工具类、自己写 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:search 和 context7: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 做了三件事:
- 参数接口对齐 :把 MCP 工具的 JSON Schema 翻译成
ToolParameter列表,让get_openai_tools()能生成正确的 Function Calling schema - 执行转发 :调用
call_tool_sync()把请求发给远端 - 结果归一化 :把 MCP 格式的响应转成
ToolResult,让管道后续步骤不需要关心这个工具是本地还是远端
注册完成后,tavily_search 和 Read、Bash 在框架看来没有任何区别------它们都在 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_search 和 context7_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 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页