Files
gui-yue 1baf1aca5a Follow up PR #265: refine chapters, diagrams, and add S20 (#283)
* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience

Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building
incrementally on the previous. Key fixes across chapters:

- s01-s04: agent loop, tool dispatch, permission pipeline, hooks
- s05-s08: todo write, subagent, skill loading, context compact
- s09-s11: memory system, system prompt assembly, error recovery
- s12-s14: task graph, background tasks, cron scheduler

All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS,
json.dumps cache, real-state context, can_start dep protection, etc.).

* feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools

Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform
chapters. Each chapter inherits all previous fixes and adds one mechanism:

- s15: agent teams (TeamCreate, teammate threads, shared task list)
- s16: team protocols (plan approval, shutdown handshake, consume_inbox)
- s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox)
- s18: worktree isolation (git worktree, bind_task, cwd switching, safety)
- s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache)

All appendix source code references verified against CC source. Config priority
corrected: claude.ai < plugin < user < project < local.

* fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash

- s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02)
- s06-s08: todo_write validates content/status required fields (inherited from s05)
- s09: extract_memories uses pre-compression snapshot instead of compacted messages
- s16: submit_plan docstring clarifies protocol-only (not code-level gate)
- s17-s19: match_response restores type mismatch validation (from s16)
- s17-s19: claim_task deps list handles missing dep files without crashing

* fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation

- s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task,
  non-interactive/SDK defaults to TodoWrite. Fix env var name to
  CLAUDE_CODE_ENABLE_TASKS (not TODO_V2).
- s14/s15: add _validate_cron_field with per-field range checks (minute 0-59,
  hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi.
  Replace old try/except validation that only caught exceptions.
- s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree,
  not just create_worktree.

* fix: align s16-s19 teaching tool consistency

* fix pr265 chapter diagrams

* Add comprehensive s20 harness chapter

* Fix chapter smoke test regressions

* Clarify README tutorial track transition

---------

Co-authored-by: Haoran <bill-billion@outlook.com>
2026-05-20 21:45:38 +08:00

11 KiB
Raw Permalink Blame History

s19: MCP Tools — 外接工具,标准协议

中文 · English · 日本語

s01 → ... → s17 → s18 → s19s20

"外接工具, 标准协议" — 发现、组装、调用Agent 不需要知道工具是谁写的。

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


问题

s01 到 s18Agent 的所有工具都是手写的——bash、read、write、task、worktree。每个工具的输入验证、执行逻辑、错误处理都是你一行行写的。

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

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


解决方案

MCP Architecture

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

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

沿用 s18 的教学版 worktree 隔离、自主认领、空闲轮询、协议系统。本章新增:connect_mcp 工具——连接外部服务,发现工具,加入工具池。

教学版用 mock handler 模拟外部 server。真实版会启动子进程通过 stdin/stdout 发送 JSON-RPC 请求。mock 的好处是不依赖外部服务就能跑完整流程;代价是你看不到真正的网络通信和进程管理。


工作原理

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)

教学版用 Python 函数模拟 server 的工具实现。真实版通过 stdio JSON-RPC 与子进程通信。

connect_mcp连接 + 发现

def connect_mcp(name: str) -> str:
    if name in mcp_clients:
        return f"MCP server '{name}' already connected"
    factory = MOCK_SERVERS.get(name)
    if not factory:
        return f"Unknown server '{name}'. Available: ..."
    mcp_client = factory()
    mcp_clients[name] = mcp_client
    return f"Connected to '{name}'. Discovered: ..."

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

normalize_mcp_name名称规范化

_DISALLOWED_CHARS = re.compile(r'[^a-zA-Z0-9_-]')

def normalize_mcp_name(name: str) -> str:
    return _DISALLOWED_CHARS.sub('_', name)

所有非 [a-zA-Z0-9_-] 的字符替换为 _。防止 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():
        safe_server = normalize_mcp_name(server_name)
        for tool_def in mcp_client.tools:
            safe_tool = normalize_mcp_name(tool_def["name"])
            prefixed = f"mcp__{safe_server}__{safe_tool}"
            tools.append(...)
            handlers[prefixed] = (
                lambda *, c=mcp_client, t=tool_def["name"], **kw:
                    c.call_tool(t, kw))
    return tools, handlers

前缀 mcp__{server}__{tool} 避免不同 server 的工具名冲突。名称经过 normalize_mcp_name 规范化。

MCP 工具的 description 带 (readOnly)(destructive) 标注——教学版用文本标注,真实 CC 用 tool annotations 结构体让权限系统判断。

无缓存工具池变了prompt 也变

s10-s18 的 agent_loop 用 prompt cache 避免重复序列化。s19 去掉了缓存:

def agent_loop(messages, context):
    tools, handlers = assemble_tool_pool()     # 每次重新构建
    system = assemble_system_prompt(context)    # 每次重新生成
    ...
    if any(b.name == "connect_mcp" ...):
        tools, handlers = assemble_tool_pool()  # 连接后重建
        system = assemble_system_prompt(context)

原因:connect_mcp 之后工具池变化了——新增了 mcp__docs__search 等工具。缓存中的工具列表是旧的,继续用会导致模型调用不到新工具。教学版直接去掉缓存,代价是多花一点序列化时间。

MCP 工具只有 Lead 可用

教学版中,connect_mcp 是 Lead 工具,assemble_tool_pool 也只服务于 Lead 的 agent_loop。Teammate 仍使用固定的 8 个子集工具bash、read_file、write_file、send_message、submit_plan、list_tasks、claim_task、complete_task

这是教学简化。真实 CC 中MCP 工具对主 agent 和子 agent 都可用——子 agent 继承父级的 MCP 配置。


相对 s18 的变更

组件 之前 (s18) 之后 (s19)
工具来源 全部手写 builtin 手写 + MCP 外部工具动态发现
工具池 固定 BUILTIN_TOOLS assemble_tool_pool 动态组装 mcp__ 前缀工具
名称安全 normalize_mcp_name 规范化
新类型 MCPClient 类(模拟 tools/list + tools/call
命名空间 mcp__server__tool 避免冲突
工具描述 无标注 (readOnly)/(destructive) 标注
prompt 缓存 s10 起) 去掉——工具池动态变化后缓存失效
Lead 工具 17 (s18) 18 (+connect_mcp)
Teammate 工具 8 (s18) 8不变MCP 工具仅 Lead 可用)
扩展方式 写代码加工具 标准协议,任意语言实现 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 的工具是否同时可用MCP 工具的 description 是否带 (readOnly)/(destructive) 标注?


接下来

现在 Agent 可以通过标准协议接入外部工具了。但前面 19 章每章都只加一个机制,真实 Agent 不会这样拆开运行。

工具、权限、hooks、todo、任务图、记忆、压缩、后台、cron、团队、worktree、MCP 这些机制应该挂在同一个循环上,而不是散在 19 个 demo 里。

s20 Comprehensive Agent → 把前 19 章的机制合回一个完整 harness。机制很多循环一个。

深入 CC 源码

以下基于 CC 源码 services/mcp/client.tsauth.tsconfig.tschannelNotification.ts 的分析。

一、6 种 Transport 类型

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

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

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

二、工具池组装算法

assembleToolPool()tools.ts:345-364

// 去重时优先保留内置工具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_-] 字符替换为 _normalization.ts:17-23)。教学版的 normalize_mcp_name 用同样的规则。

四、权限检查

CC 对 MCP 工具有独立的权限系统。checkPermissions() 对 MCP 工具的检查逻辑不同于内置工具——MCP 工具可以声明自己的权限需求readOnly、destructive 等CC 根据声明决定是否需要用户确认。教学版只在 description 中用文本标注 (readOnly) / (destructive),不做权限拦截。

五、配置来源与优先级

MCP 服务器配置来自多个来源。CC 的配置优先级从低到高:

claude.ai 连接器 < plugin < user settings.json < approved project .mcp.json < local settings.local.json

claude.ai 连接器单独拉取、按内容签名去重,以最低优先级合并(config.ts:1267-1289)。企业 managed-mcp.json 存在时完全排除其他配置。

教学版直接传 server name 给 MOCK_SERVERS 字典,不做配置合并。

六、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)支持完整的 OAuth 2.0 + PKCE 流程:

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

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

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

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

教学版的简化

  • 6 种 transport → 1 种mock stdio概念量可控
  • Channel 反向通知 → 省略:教学版 Agent 是主动方
  • OAuth 流程 → 省略:教学版假设 server 不需要认证
  • 多层配置优先级 → 省略:教学版直接传 server name
  • 复杂的错误分类 → 省略:教学版用 try/except 兜底
  • MCP 工具只给 Lead → 省略子 agent 继承:简化代码结构