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

283 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# s19: MCP Tools — 外接工具,标准协议
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s17 → s18 → `s19` → [s20](../s20_comprehensive/)
> *"外接工具, 标准协议"* — 发现、组装、调用Agent 不需要知道工具是谁写的。
>
> **Harness 层**: 插件 — 外部能力通过标准协议接入。
---
## 问题
s01 到 s18Agent 的所有工具都是手写的——bash、read、write、task、worktree。每个工具的输入验证、执行逻辑、错误处理都是你一行行写的。
现在你有 3 个外部服务想接入:公司的 Jira API查 issue、建 ticket、自建的部署系统触发 deploy、看日志、团队的 Notion 知识库(搜文档、建页面)。你不想为每个服务重写一套工具代码。
你需要一个标准协议——外部服务只要实现它Agent 就能直接调用,不管服务用什么语言写的。
---
## 解决方案
![MCP Architecture](images/mcp-architecture.svg)
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发现 + 调用
```python
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连接 + 发现
```python
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名称规范化
```python
_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组装工具池
```python
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 去掉了缓存:
```python
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 |
---
## 试一下
```sh
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。机制很多循环一个。
<details>
<summary>深入 CC 源码</summary>
> 以下基于 CC 源码 `services/mcp/client.ts`、`auth.ts`、`config.ts`、`channelNotification.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`
```typescript
// 去重时优先保留内置工具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 继承:简化代码结构
</details>
<!-- translation-sync: zh@v2, en@v0, ja@v0 -->