Agent 开发进阶(十九):MCP 与插件系统,让 Agent 接入外部能力

3 阅读10分钟

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 回到同一个主循环

关键点只有三个:

  1. 统一协议:让 agent 和外部工具程序对话
  2. 插件发现:自动发现和加载外部工具配置
  3. 统一路由:本地工具和外部工具走同一条执行路径

几个必须搞懂的概念

MCP(Model Context Protocol)

你可以先把 MCP 理解成:

一套让 agent 和外部工具程序对话的统一协议。

在教学版里,不必一开始就背很多协议细节。 你只要先抓住这条主线:

  1. 启动一个外部工具服务进程
  2. 问它「你有哪些工具」
  3. 当模型要用它的工具时,把请求转发给它
  4. 再把结果带回 agent 主循环

Plugin(插件)

如果 MCP 解决的是「外部工具怎么通信」, 那 plugin 解决的是「这些外部工具配置怎么被发现」。

最小 plugin 可以非常简单:

.claude-plugin/
  plugin.json

里面写:

  • 插件名
  • 版本
  • 它提供哪些 MCP server
  • 每个 server 的启动命令是什么

工具名前缀

为了避免命名冲突,最常见的做法是:

mcp__{server}__{tool}

比如:

  • mcp__postgres__query
  • mcp__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 toolserver 暴露的一项具体调用能力真正被模型点名调用

换成一句最短的话说

  • 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 与插件系统让你能够:

  1. 灵活扩展:无需修改主程序即可添加新工具
  2. 解耦架构:外部工具与主程序解耦,降低维护成本
  3. 统一管理:本地工具和外部工具统一管理
  4. 安全可控:外部工具仍然走同一条权限管道
  5. 生态丰富:可以接入各种第三方工具和服务

推荐的实现步骤

  1. 第一步:实现 MCPClient 类,支持连接和工具调用
  2. 第二步:实现 PluginLoader 类,自动发现插件配置
  3. 第三步:实现 UnifiedToolRouter,统一路由本地和 MCP 工具
  4. 第四步:实现工具名标准化,使用前缀避免冲突
  5. 第五步:集成到主系统,支持插件自动加载
  6. 第六步:确保 MCP 工具也走同一条权限管道
  7. 第七步:创建示例插件,演示如何扩展系统

MCP 与插件系统的价值

如果说前 18 章都在教你把系统内部搭起来, 那 s19 在教你:

如何把系统向外打开。

从这里开始,工具不再只来自你手写的 Python 文件, 还可以来自别的进程、别的系统、别的服务。

这就是为什么它适合作为最后一章。

系列总结

至此,我们已经完成了「从零构建 Coding Agent」系列的全部 19 篇文章:

  1. Agent Loop 核心循环:理解智能体的基本工作原理
  2. 工具分发机制:实现工具的动态调用
  3. 会话内规划:管理任务的短期计划
  4. 子智能体机制:实现任务的委派和隔离
  5. 技能加载系统:按需加载知识,优化 Prompt
  6. 上下文压缩:解决上下文窗口超限问题
  7. 权限系统:控制工具执行权限
  8. Hook 系统:在固定时机插入扩展行为
  9. Memory 系统:保存跨会话的有价值信息
  10. 系统提示词构建:动态组装系统提示词
  11. 错误恢复:处理执行过程中的错误
  12. 任务系统:管理复杂任务的依赖关系
  13. 后台任务系统:让慢命令不阻塞主循环
  14. 定时调度系统:按时间自动触发任务
  15. Agent 团队系统:让多个智能体协同工作
  16. 团队协议系统:实现结构化的团队协作
  17. 自治智能体系统:让智能体主动认领任务
  18. Worktree 任务隔离:让多个任务互不干扰
  19. MCP 与插件系统:接入外部工具和插件

从核心循环到插件扩展,从单智能体到团队协作,从手动分配到自主执行,我们一步步构建了一个完整的 Coding Agent 系统。


一句话总结:MCP 的本质,不是协议名词堆砌,而是把外部工具安全、统一地接进你的 agent。


如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。