Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

s19: MCP Plugin — 能力不够?插上外接工具

中文 · English · 日本語

s01 → ... → s17 → s18 → s19

"能力不够? 插上 MCP" — 多传输、通道路由、工具池合并。

Harness 层: 插件 — 外部能力通过标准协议接入。


问题

从 s01 到 s18,Agent 用的所有工具都是你手写的——bash、read、write、task、todo_write。你自己写代码实现每个工具的输入验证、执行逻辑、错误处理。

但现在你有 3 个外部服务想接入 Agent:公司的 Jira API(查 issue、创建 ticket)、自建的部署系统(触发 deploy、查看日志)、团队的 Notion 知识库(搜索文档、创建页面)。你不想为每个服务重写一套工具代码。

你需要一套标准协议——只要外部服务实现了这个协议,Agent 就能直接调用它的工具,不管服务是用什么语言写的。


解决方案

MCP Architecture

MCP(Model Context Protocol)定义了 Agent 如何发现和调用外部工具。核心概念:

概念 作用
MCPClient Agent 这边的客户端,连接 server、发现工具、调用工具
MCP Server 外部服务那边,实现 tools/list + tools/call
assemble_tool_pool 把内置工具和 MCP 工具合并成一个池子
mcp__server__tool 命名 避免不同 server 的工具名冲突

s18 的全部能力保留(worktree 隔离、自主认领、空闲轮询、协议系统)。新增一样:connect_mcp 工具——连接外部服务,发现工具,合并到工具池。


工作原理

MCPClient:发现 + 调用

class MCPClient:
    def __init__(self, name: str):
        self.name = name
        self.tools: list[dict] = []
        self._handlers: dict[str, callable] = {}

    def register(self, tool_defs, handlers):
        """Simulates tools/list discovery."""
        self.tools = tool_defs
        self._handlers = handlers

    def call_tool(self, tool_name: str, args: dict) -> str:
        """Simulates tools/call."""
        handler = self._handlers.get(tool_name)
        if not handler:
            return f"MCP error: unknown tool '{tool_name}'"
        return handler(**args)

教学版用 mock handler 模拟 stdio JSON-RPC。真实版会启动子进程,通过 stdin/stdout 发送 tools/listtools/call 请求。

connect_mcp:连接 + 发现

def connect_mcp(name: str) -> str:
    """Connect to an MCP server and discover its tools."""
    factory = MOCK_SERVERS.get(name)
    mcp_client = factory()
    mcp_clients[name] = mcp_client
    tool_names = [t["name"] for t in mcp_client.tools]
    return f"Connected to '{name}'. Discovered: {', '.join(tool_names)}"

连接后,server 提供的工具立即可用。

assemble_tool_pool:合并

def assemble_tool_pool() -> tuple[list[dict], dict]:
    tools = list(BUILTIN_TOOLS)
    handlers = dict(BUILTIN_HANDLERS)
    for server_name, mcp_client in mcp_clients.items():
        for tool_def in mcp_client.tools:
            prefixed = f"mcp__{server_name}__{tool_def['name']}"
            tools.append({
                "name": prefixed,
                "description": tool_def.get("description", ""),
                "input_schema": tool_def.get("inputSchema", {}),
            })
            handlers[prefixed] = (
                lambda **kw, c=mcp_client, t=tool_def["name"]: c.call_tool(t, kw))
    return tools, handlers

前缀 mcp__{server}__{tool} 避免不同 server 的工具名冲突。调用 connect_mcp 后,agent_loop 自动重新 assemble,新工具立即可用。


相对 s18 的变更

组件 之前 (s18) 之后 (s19)
工具来源 全部手写 builtin 手写 + MCP 外部工具动态发现
工具池 固定 BUILTIN_TOOLS assemble_tool_pool 动态合并 mcp__ 前缀工具
新类型 MCPClient 类(模拟 tools/list + tools/call)
命名空间 mcp__server__tool 避免冲突
Lead 工具 16 (s18) 17 (+connect_mcp)
扩展方式 写代码加工具 标准协议,任意语言实现 server

试一下

cd learn-claude-code
python s19_mcp_plugin/code.py

试试这些 prompt:

  1. Connect to the docs MCP server and search for something
  2. Connect to the deploy server and trigger a deployment
  3. Connect both servers — what tools are now available?

观察重点:连接 MCP server 后,工具名是否带 mcp__docs__mcp__deploy__ 前缀?两个 server 的工具是否同时可用?


你走到了这里

这是最后一章。回顾你走过的路:

s01-s04   工具管线     loop → dispatch → permission → hooks
s05-s08   单Agent能力   planning → subagent → skill → compact
s09-s11   知识与韧性    memory → prompt → error recovery
s12-s14   持久化工作    task graph → background → cron
s15-s19   多Agent平台   teams → protocols → autonomy → worktree → MCP

你已经从零构建了一个完整 Agent 的 harness。19 个章节,每个只加一个机制。每个机制都挂在同一个 while True 循环上——循环本身,从未改变。

深入 CC 源码

以下基于 CC 源码 services/mcp/client.ts(3348 行)、auth.ts(2466 行)、config.ts(1579 行)、channelNotification.ts(317 行)的完整分析。

一、6 种 Transport 类型

教学版只展示了 stdio。CC 支持 6 种传输(types.ts:23-25):

Transport 通信方式
stdio 子进程 stdin/stdout(跨平台默认)
sse HTTP Server-Sent Events
http Streamable HTTP(POST/SSE 双向)
ws WebSocket
sse-ide IDE 内嵌 SSE 传输
sdk 进程内 SDK 传输

连接时本地(stdio)和远程(http/sse/ws)服务器分批并发:本地批量 3 个,远程批量 20 个。

二、工具池合并的精确算法

assembleToolPool()tools.ts:345-367):

// 去重时优先保留内置工具(name 相同时内置在前)
return uniqBy(
  [...builtInTools.sort(byName), ...filteredMcpTools.sort(byName)],
  'name',
)

关键细节:内置工具和 MCP 工具分开排序,不是合起来排。原因是 CC 的 claude_code_system_cache_policy 在最后一个内置工具之后的某个位置放全局缓存断点——混排会破坏这个设计。

三、命名规则:mcp__server__tool

buildMcpToolName()mcpStringUtils.ts:50-52):

mcp__<normalizedServerName>__<normalizedToolName>

所有非 [a-zA-Z0-9_-] 字符替换为 _。例如 slack 服务器的 post_messagemcp__slack__post_message

四、Channel 通知:服务器反向推消息

教学版只讲了 Agent → MCP Server 的单向调用。CC 还支持反向通知channelNotification.ts):

  1. Server 声明 capabilities.experimental['claude/channel']
  2. Server 通过 MCP 通知 notifications/claude/channel 给 Agent 发消息
  3. 消息包装在 <channel source="serverName">...</channel> XML 标签中
  4. Agent 被 SleepTool 唤醒(1 秒内)

Server 还可以请求权限:notifications/claude/channel/permission_request → Agent 回复 notifications/claude/channel/permission。用户通过 5 字母短 ID 确认/拒绝。

五、OAuth 认证流程

CC 的 MCP 认证(auth.ts,2466 行)支持完整的 OAuth 2.0 + PKCE 流程:

  • 通过公钥客户端 + PKCE 发现 OAuth 元数据(RFC 8414 / RFC 9728)
  • 本地回调服务器接收授权码
  • 令牌通过 getSecureStorage() 持久化(macOS Keychain / Linux 加密文件 / Windows 凭据管理器)
  • 过期前 5 分钟自动刷新
  • 支持跨应用访问(XAA):浏览器获取 id_token → RFC 8693 + RFC 7523 交换 → 无需反复弹浏览器

六、配置来源与优先级

MCP 服务器配置来自 6 个来源,优先级从低到高(config.ts:1071-1251):

插件 > claude.ai 连接器 > 用户 settings.json > 项目 .mcp.json > 本地 settings.local.json

同名服务器按内容签名去重。企业 managed-mcp.json 存在时完全排除其他配置。

七、连接生命周期的错误处理

CC 对 MCP 连接有精细的错误分类和重试(client.ts:1266-1402):

  • 终局性错误(ECONNRESET、ETIMEDOUT、EPIPE 等):连续 3 次 → 关闭 + 重连
  • 工具调用 401:令牌过期 → 抛出 McpAuthError → 触发重认证
  • 工具调用超时Promise.race 超时(可配置,默认 ~28 小时)
  • Stdio 断连:按 SIGINT → SIGTERM → SIGKILL 顺序杀进程

教学版的简化是刻意的

  • 6 种 transport → 1 种(stdio):概念量可控
  • Channel 反向通知 → 省略:教学版 Agent 是主动方
  • OAuth 流程 → 省略:教学版假设 server 不需要认证
  • 6 层配置优先级 → 省略:教学版直接传 server_command
  • 复杂的错误分类 → 省略:教学版用 try/except 兜底