在前十八章,我们的 Agent 已经拥有完整的内部能力体系:循环、工具、计划、子代理、技能、压缩、权限、Hook、记忆、提示词流水线、错误恢复、任务系统、后台任务、定时调度、多 Agent 团队、团队协议、自主代理、Worktree 隔离,所有工具都写在本地代码里。
但真实系统走到一定阶段,会遇到一个自然的需求:能不能让外部程序也把工具接进来,而不用每次都改主程序?
这一章 S19,我们给 Agent 加上MCP 与插件系统:让外部工具通过统一协议安全接入,和本地工具处在同一控制面,实现 "外部能力总线",让工具不再局限于本地硬编码。
本章核心信息
- 核心闭环:插件配置发现 → MCP Server 启动 → 工具标准化注册 → 统一路由分发 → 权限检查 → 结果回流主循环
- 工具数量:4 个
- 核心思想:外部能力系统不该是外挂;它们应和原生工具一起处在同一控制面上
先看懂本章所有名词
-
MCP(Model Context Protocol)
一套让 Agent 和外部工具程序对话的统一协议,定义了如何发现工具、调用工具、接收结果的标准方式。
-
MCP Server
提供外部工具能力的独立进程,通过 MCP 协议暴露工具列表和调用接口,Agent 通过连接 Server 使用这些工具。
-
插件(Plugin)
负责外部工具配置的声明文件,告诉主程序如何发现、启动和连接 MCP Server,包含插件名、版本、Server 配置等信息。
-
工具前缀规则
区分本地工具和外部 MCP 工具的命名约定,格式为
mcp__{server}__{tool},避免命名冲突,一眼就能识别工具来源。 -
统一路由器
负责分发工具调用请求的核心组件,判断是本地工具还是 MCP 工具,分别交给对应的处理器处理。
-
外部能力总线
所有外部工具通过 MCP 接入后,和本地工具共享同一套权限检查、结果处理、主循环回流逻辑,形成统一的能力接入体系。
这一章到底要解决什么问题?
到 S18,Agent 的所有工具都写在本地代码里,存在明显的局限性:
- 新增工具必须修改主程序,扩展性差
- 外部程序无法直接提供能力,只能通过硬编码集成
- 工具来源单一,无法复用成熟的第三方服务能力
- 无法统一管理本地和外部工具的权限、调用和结果
本章要解决的核心问题:把工具来源从 "本地硬编码" 升级为 "外部可插拔",让外部工具通过统一协议安全接入,和本地工具共享同一控制面,实现系统的可扩展能力。
最小心智模型:外部工具接入的三层结构
LLM
|
| 发起工具调用请求
v
Agent 工具路由器
|
+-- 本地工具 → 交给本地 Python 处理器执行
|
+-- MCP 工具 → 交给 MCP Client 转发给外部 Server
|
v
MCP Server 执行工具并返回结果
一句话:MCP 不是独立的外挂系统,而是外部工具接入 Agent 的统一协议,最终和本地工具汇入同一套执行逻辑。
关键数据结构(本章灵魂)
1. 插件配置文件(plugin.json)
{
"name": "my-db-tools",
"version": "1.0.0",
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {}
}
}
}
它本质上是一份配置声明,告诉主程序如何发现和启动 MCP Server。
2. MCP Server 配置
python
server_config = {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {}
}
定义了启动外部 Server 的命令、参数和环境变量。
3. 标准化后的工具定义
python
{
"name": "mcp__postgres__query",
"description": "Run a SQL query on PostgreSQL",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"}
}
}
}
把 MCP Server 暴露的工具转成 Agent 能看懂的统一格式,加上mcp__前缀区分来源。
4. MCP Client 注册表
python
clients = {
"postgres": mcp_client_instance
}
记录已连接的 MCP Client 实例,方便路由器快速查找和转发请求。
最小实现代码(极简可运行)
第一步:实现一个基础的 MCPClient
python
class MCPClient:
def __init__(self, server_config):
self.config = server_config
self.process = None
self.tools = []
def connect(self):
# 启动外部Server进程
self.process = subprocess.Popen(
[self.config["command"]] + self.config["args"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# 建立连接并获取工具列表
self.tools = self.list_tools()
def list_tools(self):
# 向Server请求工具列表
# 教学版简化,实际按MCP协议通信
return [{"name": "query", "description": "Run SQL query"}]
def call_tool(self, tool_name, arguments):
# 转发工具调用请求给Server
# 教学版简化,实际按MCP协议通信
return {"status": "ok", "preview": "Query executed successfully"}
核心能力:连接 Server、获取工具列表、转发调用请求。
第二步:把外部工具标准化为 Agent 工具格式
python
def normalize_mcp_tools(server_name, raw_tools):
normalized = []
for tool in raw_tools:
# 加上mcp__前缀,格式:mcp__{server}__{tool}
prefixed_name = f"mcp__{server_name}__{tool['name']}"
normalized.append({
"name": prefixed_name,
"description": tool["description"],
"input_schema": tool.get("input_schema", {})
})
return normalized
关键:通过前缀规则区分本地和外部工具,避免命名冲突。
第三步:实现统一工具路由器
python
class ToolRouter:
def __init__(self, native_tools, mcp_clients):
self.native_tools = native_tools
self.mcp_clients = mcp_clients # key: server_name, value: MCPClient
def route(self, tool_name, arguments):
# 判断是否为MCP工具
if tool_name.startswith("mcp__"):
# 解析server名和工具名:mcp__postgres__query
parts = tool_name.split("__", 2)
server_name = parts[1]
tool_name = parts[2]
# 转发给对应的MCP Client
client = self.mcp_clients[server_name]
return client.call_tool(tool_name, arguments)
else:
# 本地工具,交给本地处理器
return self.native_tools[tool_name](arguments)
路由器只做一件事:根据工具名分发请求,不区分工具来源。
第四步:MCP 工具必须经过同一条权限管道
python
def check_permission(tool_name, arguments, mode):
# 权限检查逻辑和本地工具完全一致
# 即使是MCP工具,也不能绕开权限闸门
if tool_name.startswith("mcp__"):
# 提取server和工具信息,进行权限检查
if is_dangerous_mcp_tool(tool_name):
return "deny", "Dangerous MCP tool call blocked"
# 本地工具权限检查
return native_permission_check(tool_name, arguments, mode)
关键:外部工具也必须经过统一的权限检查,不能成为安全后门。
第五步:结果标准化回流主循环
python
def normalize_tool_result(tool_name, raw_result):
if tool_name.startswith("mcp__"):
parts = tool_name.split("__", 2)
server_name = parts[1]
return {
"source": "mcp",
"server": server_name,
"tool": parts[2],
"status": raw_result.get("status", "ok"),
"preview": raw_result.get("preview", "")
}
else:
# 本地工具结果格式
return {
"source": "native",
"tool": tool_name,
"status": raw_result.get("status", "ok"),
"preview": raw_result.get("preview", "")
}
无论本地还是外部工具,结果都要转成主循环能统一处理的格式。
插件、MCP Server、MCP Tool 三层关系
初学者很容易把这三个概念混在一起,这里用表格帮你理清边界:
| 层级 | 它是什么 | 它负责什么 |
|---|---|---|
| Plugin Manifest | 一份配置声明文件 | 告诉系统要发现和启动哪些 MCP Server |
| MCP Server | 一个外部进程 / 连接对象 | 对外暴露一组工具能力 |
| MCP Tool | Server 暴露的一项具体调用能力 | 真正被模型点名调用的功能 |
一句话总结:Plugin 负责 "发现",Server 负责 "连接",Tool 负责 "调用"。
如何接到前面章节的系统里
MCP 不是孤立的外挂,而是接入 Agent 已有体系的扩展层,完整链路如下:
启动时
PluginLoader 读取插件配置
->
解析并获取MCP Server配置
->
启动Server并建立连接
->
获取Server的工具列表
->
标准化工具名并合并进主程序工具池
运行时
LLM 生成工具调用请求
->
统一权限闸门检查(和本地工具完全一致)
->
工具路由器分发请求(本地/MCP)
->
执行工具并返回结果
->
结果标准化后回流主循环
关键:进入方式不同,但进入后必须回到同一条控制面和执行面。
初学者最容易被带偏的 3 个坑
-
一上来就讲太多协议细节不要一开始就陷入 transports、auth、resources 等复杂概念,主线只有一句话:外部工具也能像本地工具一样接进 Agent。
-
把 MCP 当成一套完全不同的工具系统它最终仍然汇入你原来的工具体系:一样要注册、一样要过权限、一样要返回 tool_result,不是独立的外挂。
-
忽略命名与路由规则没有统一前缀和路由,本地和外部工具会混在一起,系统很快就会混乱,无法区分工具来源。
S18 → S19 升级了什么?
| 模块 | S18 | S19 |
|---|---|---|
| 工具来源 | 本地硬编码工具 | 本地工具 + 外部可插拔 MCP 工具 |
| 扩展性 | 新增工具需修改主程序 | 外部工具通过插件配置接入,无需修改主程序 |
| 能力边界 | 仅依赖开发者手写的工具 | 可复用第三方成熟服务能力(如数据库、浏览器等) |
| 控制面 | 统一权限、路由、结果处理 | 本地和外部工具共享同一控制面,无安全后门 |
| 架构地位 | 多 Agent 并行隔离执行层 | 系统外部能力扩展层 |
本章教学边界
本章先停在 "tools-first" 的主线,不展开协议细节:
- 不深入讲 transports、认证、连接恢复等复杂协议内容
- 不展开 resources、prompts 等 MCP 扩展能力
- 只讲清外部工具如何被发现、命名、路由、过权限、回流主循环
这些扩展内容可以放到后续平台桥接文档中展开,正文先聚焦主线,避免读者失焦。
一句话总结本章
MCP 的本质不是协议名词堆砌,而是把外部工具安全、统一地接进你的 Agent。它让工具不再局限于本地硬编码,通过插件和 MCP 协议,让外部能力成为系统的可扩展总线,和本地工具共享同一控制面,实现真正的可扩展 Agent 平台。