Agent 开发进阶(十九):MCP 与插件系统,让 Agent 接入外部能力
本文是「从零构建 Coding Agent」系列的最后一篇,适合想让 Agent 接入外部工具和插件的开发者。
先问一个问题
当你的 Agent 系统越来越强大时,你是否遇到过这样的需求:
- 希望接入数据库工具,但不想把数据库逻辑写进主程序
- 希望使用浏览器工具,但不想维护浏览器驱动
- 希望集成第三方服务,但不想每次都修改主程序
如果你的答案是肯定的,那么你需要一个 MCP 与插件系统。
工具扩展的「硬编码困境」问题
到了这一阶段,你的 Agent 已经具备了完整的系统能力:
- 核心循环运行
- 工具使用与分发
- 会话内规划(TodoWrite)
- 子智能体机制(Subagent)
- 技能加载
- 上下文压缩
- 权限系统
- Hook 系统
- Memory 系统
- 系统提示词组装
- 错误恢复
- 任务系统
- 后台任务系统
- 定时调度系统
- Agent 团队系统
- 团队协议系统
- 自治智能体系统
- Worktree 任务隔离
但当需要接入外部工具时,硬编码的方式会遇到明显问题:
- 扩展困难:每次添加新工具都需要修改主程序
- 维护复杂:外部工具的逻辑混在主程序中
- 耦合严重:主程序和外部工具紧密耦合
- 安全性差:外部工具绕过了权限系统
虽然前面的章节已经构建了完整的系统,但工具仍然都是写在自己的 Python 代码里。
所以到了这个阶段,我们需要一个 MCP 与插件系统:
让外部程序也能把工具接进 agent,而不用每次都改主程序。
MCP 与插件系统的核心设计:统一协议与插件发现
用一个图来表示 MCP 与插件系统的工作流程:
启动时
->
PluginLoader 找到 manifest
->
得到 server 配置
->
MCP client 连接 server
->
list_tools 并标准化名字
->
和 native tools 一起合并进同一个工具池
运行时
->
LLM 产出 tool_use
->
统一权限闸门
->
native route 或 mcp route
->
结果标准化
->
tool_result 回到同一个主循环
关键点只有三个:
- 统一协议:让 agent 和外部工具程序对话
- 插件发现:自动发现和加载外部工具配置
- 统一路由:本地工具和外部工具走同一条执行路径
几个必须搞懂的概念
MCP(Model Context Protocol)
你可以先把 MCP 理解成:
一套让 agent 和外部工具程序对话的统一协议。
在教学版里,不必一开始就背很多协议细节。 你只要先抓住这条主线:
- 启动一个外部工具服务进程
- 问它「你有哪些工具」
- 当模型要用它的工具时,把请求转发给它
- 再把结果带回 agent 主循环
Plugin(插件)
如果 MCP 解决的是「外部工具怎么通信」, 那 plugin 解决的是「这些外部工具配置怎么被发现」。
最小 plugin 可以非常简单:
.claude-plugin/
plugin.json
里面写:
- 插件名
- 版本
- 它提供哪些 MCP server
- 每个 server 的启动命令是什么
工具名前缀
为了避免命名冲突,最常见的做法是:
mcp__{server}__{tool}
比如:
mcp__postgres__querymcp__browser__open_tab
这样一眼就知道:
- 这是 MCP 工具
- 它来自哪个 server
- 它原始工具名是什么
最小实现
1. MCP Client
import json
import subprocess
import threading
from pathlib import Path
class MCPClient:
"""MCP 客户端"""
def __init__(self, name, command, args=None, env=None):
self.name = name
self.command = command
self.args = args or []
self.env = env or {}
self.process = None
self.tools = {}
self.connected = False
def connect(self):
"""连接到 MCP server"""
print(f"连接到 MCP server: {self.name}")
# 启动外部进程
full_command = [self.command] + self.args
self.process = subprocess.Popen(
full_command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env={**os.environ, **self.env}
)
# 等待连接
# 简化版本直接标记为已连接
self.connected = True
# 列出工具
self._list_tools()
return self.connected
def _list_tools(self):
"""列出 server 提供的工具"""
# 简化版本:模拟工具列表
# 实际应该通过 MCP 协议请求
self.tools = {
f"mcp__{self.name}__query": {
"name": f"mcp__{self.name}__query",
"description": f"Query {self.name} database",
"parameters": {
"query": {"type": "string", "description": "SQL query"}
}
},
f"mcp__{self.name}__execute": {
"name": f"mcp__{self.name}__execute",
"description": f"Execute command on {self.name}",
"parameters": {
"command": {"type": "string", "description": "Command to execute"}
}
}
}
print(f"发现 {len(self.tools)} 个工具")
def call_tool(self, tool_name, arguments):
"""调用工具"""
# 移除前缀,获取原始工具名
original_name = tool_name.replace(f"mcp__{self.name}__", "")
print(f"调用 MCP 工具: {original_name}")
# 简化版本:模拟调用
# 实际应该通过 MCP 协议发送请求
result = {
"source": "mcp",
"server": self.name,
"tool": original_name,
"status": "ok",
"preview": f"Executed {original_name} on {self.name}",
"output": f"Result from {original_name}: {arguments}"
}
return result
def disconnect(self):
"""断开连接"""
if self.process:
self.process.terminate()
self.connected = False
print(f"已断开 MCP server: {self.name}")
2. Plugin Loader
import json
from pathlib import Path
class PluginLoader:
"""插件加载器"""
def __init__(self, plugins_dir=".claude-plugin"):
self.plugins_dir = Path(plugins_dir)
self.plugins_dir.mkdir(exist_ok=True)
self.plugins = {}
self._load_plugins()
def _load_plugins(self):
"""加载所有插件"""
for plugin_file in self.plugins_dir.glob("*/plugin.json"):
try:
plugin = json.loads(plugin_file.read_text(encoding="utf-8"))
self.plugins[plugin["name"]] = plugin
print(f"加载插件: {plugin['name']} v{plugin['version']}")
except Exception as e:
print(f"加载插件失败 {plugin_file}: {e}")
def get_mcp_servers(self):
"""获取所有 MCP server 配置"""
servers = {}
for plugin_name, plugin in self.plugins.items():
if "mcpServers" in plugin:
for server_name, server_config in plugin["mcpServers"].items():
full_name = f"{plugin_name}__{server_name}"
servers[full_name] = {
"name": full_name,
"command": server_config.get("command"),
"args": server_config.get("args", []),
"env": server_config.get("env", {})
}
return servers
def list(self):
"""列出所有插件"""
if not self.plugins:
return "暂无插件"
lines = ["# 插件\n"]
for name, plugin in self.plugins.items():
lines.append(f"- **{name}** v{plugin['version']}")
if "mcpServers" in plugin:
servers = list(plugin["mcpServers"].keys())
lines.append(f" MCP Servers: {', '.join(servers)}")
return "\n".join(lines)
3. 统一工具路由器
class UnifiedToolRouter:
"""统一工具路由器"""
def __init__(self):
self.native_tools = {}
self.mcp_clients = {}
self.tool_pool = {}
def register_native_tool(self, name, handler):
"""注册本地工具"""
self.native_tools[name] = handler
self.tool_pool[name] = {
"name": name,
"type": "native",
"handler": handler
}
def register_mcp_client(self, client):
"""注册 MCP 客户端"""
self.mcp_clients[client.name] = client
# 将 MCP 工具添加到工具池
for tool_name, tool_def in client.tools.items():
self.tool_pool[tool_name] = {
"name": tool_name,
"type": "mcp",
"client": client.name,
"definition": tool_def
}
def call(self, tool_name, arguments):
"""调用工具"""
tool = self.tool_pool.get(tool_name)
if not tool:
return {"success": False, "error": f"工具 {tool_name} 不存在"}
if tool["type"] == "native":
# 调用本地工具
handler = self.native_tools[tool_name]
return handler(**arguments)
elif tool["type"] == "mcp":
# 调用 MCP 工具
client = self.mcp_clients[tool["client"]]
return client.call_tool(tool_name, arguments)
def list_tools(self):
"""列出所有工具"""
tools = []
for tool_name, tool in self.tool_pool.items():
if tool["type"] == "native":
tools.append(f"- {tool_name} [native]")
else:
tools.append(f"- {tool_name} [mcp:{tool['client']}]")
return "\n".join(tools)
4. 集成到主系统
class MCPEnabledAgent:
"""支持 MCP 的 Agent"""
def __init__(self):
# 加载插件
self.plugin_loader = PluginLoader()
# 创建工具路由器
self.router = UnifiedToolRouter()
# 注册本地工具
self._register_native_tools()
# 连接 MCP servers
self._connect_mcp_servers()
def _register_native_tools(self):
"""注册本地工具"""
def native_tool_example(arg1, arg2):
return f"本地工具执行: {arg1}, {arg2}"
self.router.register_native_tool("native_tool", native_tool_example)
def _connect_mcp_servers(self):
"""连接 MCP servers"""
servers = self.plugin_loader.get_mcp_servers()
for server_name, server_config in servers.items():
print(f"连接 MCP server: {server_name}")
client = MCPClient(
name=server_name,
command=server_config["command"],
args=server_config["args"],
env=server_config["env"]
)
if client.connect():
self.router.register_mcp_client(client)
def run(self):
"""运行 Agent"""
print("Agent 已启动")
print("\n可用工具:")
print(self.router.list_tools())
# 主循环
while True:
# 这里应该调用模型并处理工具调用
# 简化版本只展示路由能力
print("\n输入工具名称和参数(或 'exit' 退出):")
user_input = input("> ")
if user_input.lower() == "exit":
break
# 简化版本:直接调用工具
# 实际应该从模型输出中解析工具调用
result = self.router.call(user_input, {})
print(f"结果: {result}")
5. 插件配置示例
创建 .claude-plugin/my-db-tools/plugin.json:
{
"name": "my-db-tools",
"version": "1.0.0",
"description": "数据库工具插件",
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "postgresql://localhost:5432/mydb"
}
},
"mysql": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-mysql"],
"env": {
"DATABASE_URL": "mysql://localhost:3306/mydb"
}
}
}
}
核心功能说明
1. 插件发现
自动发现和加载插件:
plugin_loader = PluginLoader()
plugins = plugin_loader.get_mcp_servers()
插件配置包含:
- 插件名和版本
- 提供的 MCP server 列表
- 每个 server 的启动命令和参数
2. MCP 连接
连接到外部 MCP server:
client = MCPClient(name="postgres", command="npx", args=["-y", "@modelcontextprotocol/server-postgres"])
client.connect()
连接过程:
- 启动外部进程
- 等待连接建立
- 列出 server 提供的工具
3. 统一工具路由
本地工具和 MCP 工具统一路由:
router = UnifiedToolRouter()
router.register_native_tool("native_tool", handler)
router.register_mcp_client(mcp_client)
# 统一调用接口
result = router.call(tool_name, arguments)
4. 工具名标准化
使用前缀避免命名冲突:
# 本地工具
"native_tool"
# MCP 工具
"mcp__postgres__query"
"mcp__browser__open_tab"
Plugin、MCP Server、MCP Tool 的边界
| 层级 | 它是什么 | 它负责什么 |
|---|---|---|
| plugin manifest | 一份配置声明 | 告诉系统要发现和启动哪些 server |
| MCP server | 一个外部进程 / 连接对象 | 对外暴露一组能力 |
| MCP tool | server 暴露的一项具体调用能力 | 真正被模型点名调用 |
换成一句最短的话说:
- plugin 负责「发现」
- server 负责「连接」
- tool 负责「调用」
新手最容易犯的 3 个错
1. 一上来讲太多协议细节
# ❌ 错误
# 一上来就讲复杂的协议细节
class MCPClient:
def __init__(self):
self.transport = None
self.auth = None
self.resources = []
self.prompts = []
# ... 太多细节
# ✅ 正确
# 先抓住核心主线
class MCPClient:
def __init__(self, name, command):
self.name = name
self.command = command
self.tools = {}
def connect(self):
# 启动进程
pass
def call_tool(self, tool_name, arguments):
# 调用工具
pass
2. 把 MCP 当成一套完全不同的工具系统
# ❌ 错误
# MCP 工具绕过权限系统
def call_mcp_tool(tool_name, arguments):
# 直接调用,不检查权限
return mcp_client.call(tool_name, arguments)
# ✅ 正确
# MCP 工具也要走同一条权限管道
def call_tool(tool_name, arguments):
# 先检查权限
if not check_permission(tool_name):
return {"error": "Permission denied"}
# 再路由
return router.call(tool_name, arguments)
3. 忽略命名与路由
# ❌ 错误
# 没有统一前缀,容易冲突
tools = {
"query": {...}, # 来自 postgres
"query": {...}, # 来自 mysql,冲突!
}
# ✅ 正确
# 使用前缀避免冲突
tools = {
"mcp__postgres__query": {...},
"mcp__mysql__query": {...},
}
为什么这很重要
因为一个真正强大的系统,需要灵活的扩展能力。
MCP 与插件系统让你能够:
- 灵活扩展:无需修改主程序即可添加新工具
- 解耦架构:外部工具与主程序解耦,降低维护成本
- 统一管理:本地工具和外部工具统一管理
- 安全可控:外部工具仍然走同一条权限管道
- 生态丰富:可以接入各种第三方工具和服务
推荐的实现步骤
- 第一步:实现 MCPClient 类,支持连接和工具调用
- 第二步:实现 PluginLoader 类,自动发现插件配置
- 第三步:实现 UnifiedToolRouter,统一路由本地和 MCP 工具
- 第四步:实现工具名标准化,使用前缀避免冲突
- 第五步:集成到主系统,支持插件自动加载
- 第六步:确保 MCP 工具也走同一条权限管道
- 第七步:创建示例插件,演示如何扩展系统
MCP 与插件系统的价值
如果说前 18 章都在教你把系统内部搭起来, 那 s19 在教你:
如何把系统向外打开。
从这里开始,工具不再只来自你手写的 Python 文件, 还可以来自别的进程、别的系统、别的服务。
这就是为什么它适合作为最后一章。
系列总结
至此,我们已经完成了「从零构建 Coding Agent」系列的全部 19 篇文章:
- Agent Loop 核心循环:理解智能体的基本工作原理
- 工具分发机制:实现工具的动态调用
- 会话内规划:管理任务的短期计划
- 子智能体机制:实现任务的委派和隔离
- 技能加载系统:按需加载知识,优化 Prompt
- 上下文压缩:解决上下文窗口超限问题
- 权限系统:控制工具执行权限
- Hook 系统:在固定时机插入扩展行为
- Memory 系统:保存跨会话的有价值信息
- 系统提示词构建:动态组装系统提示词
- 错误恢复:处理执行过程中的错误
- 任务系统:管理复杂任务的依赖关系
- 后台任务系统:让慢命令不阻塞主循环
- 定时调度系统:按时间自动触发任务
- Agent 团队系统:让多个智能体协同工作
- 团队协议系统:实现结构化的团队协作
- 自治智能体系统:让智能体主动认领任务
- Worktree 任务隔离:让多个任务互不干扰
- MCP 与插件系统:接入外部工具和插件
从核心循环到插件扩展,从单智能体到团队协作,从手动分配到自主执行,我们一步步构建了一个完整的 Coding Agent 系统。
一句话总结:MCP 的本质,不是协议名词堆砌,而是把外部工具安全、统一地接进你的 agent。
如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。