feat: refresh course through workflow and goal loops

This commit is contained in:
Haoran
2026-07-30 19:14:04 +08:00
parent 2dd1852d9e
commit cb8fae1bdd
125 changed files with 10882 additions and 7661 deletions

View File

@@ -1,47 +1,47 @@
# s19: MCP Tools — 外接工具,标准协议
# s19: MCP Tools — External Tools, Standard Protocol
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → s17 → s18 → `s19` → [s20](../s20_comprehensive/)
s01 → ... → s17 → s18 → `s19` → [s20](../s20_comprehensive/) → s21 → s22
> *"外接工具, 标准协议"* — 发现、组装、调用Agent 不需要知道工具是谁写的。
> *"External tools, standard protocol"* — Discover, assemble, invoke. Agent doesn't need to know who wrote them.
>
> **Harness 层**: 插件 — 外部能力通过标准协议接入。
> **Harness layer**: Plugins — External capabilities via a standard protocol.
---
## 问题
## The Problem
s01 到 s18Agent 的所有工具都是手写的——bashreadwritetaskworktree。每个工具的输入验证、执行逻辑、错误处理,都是你一行行写的。
From s01 through s18, every tool the agent uses was hand-written — bash, read, write, task, worktree. Input validation, execution logic, error handling — all written line by line.
现在你有 3 个外部服务想接入:公司的 Jira API查 issue、建 ticket、自建的部署系统触发 deploy、看日志、团队的 Notion 知识库(搜文档、建页面)。你不想为每个服务重写一套工具代码。
Now you have 3 external services to integrate: the company's Jira API (query issues, create tickets), an in-house deployment system (trigger deploys, view logs), and the team's Notion knowledge base (search docs, create pages). You don't want to rewrite tool code for every service.
你需要一个标准协议——外部服务只要实现它Agent 就能直接调用,不管服务用什么语言写的。
You need a standard protocol — as long as an external service implements it, the agent can call its tools directly, regardless of what language the service is written in.
---
## 解决方案
## The Solution
![MCP Architecture](images/mcp-architecture.svg)
![MCP Architecture](images/mcp-architecture.en.svg)
MCPModel Context Protocol)定义了 Agent 如何发现和调用外部工具。核心概念:
MCP (Model Context Protocol) defines how agents discover and invoke external tools. Core concepts:
| 概念 | 作用 |
| Concept | Purpose |
|------|------|
| MCPClient | Agent 端的客户端,连接 server、发现工具、调用工具 |
| MCP Server | 外部服务,实现 `tools/list` + `tools/call` |
| assemble_tool_pool | 把内置工具和 MCP 工具组装成一个工具池 |
| mcp\_\_server\_\_tool 命名 | 避免不同 server 的工具名冲突 |
| MCPClient | The agent-side client — connects to servers, discovers tools, invokes tools |
| MCP Server | The external service — implements `tools/list` + `tools/call` |
| assemble_tool_pool | Assembles built-in tools and MCP tools into one tool pool |
| mcp\_\_server\_\_tool naming | Prevents tool name collisions across different servers |
沿用 s18 的教学版 worktree 隔离、自主认领、空闲轮询、协议系统。本章新增:`connect_mcp` 工具——连接外部服务,发现工具,加入工具池。
Carries forward s18's teaching-version worktree isolation, autonomous claiming, idle polling, and protocol system. This chapter adds: the `connect_mcp` tool — connect to external services, discover tools, add them to the tool pool.
教学版用 mock handler 模拟外部 server。真实版会启动子进程通过 stdin/stdout 发送 JSON-RPC 请求。mock 的好处是不依赖外部服务就能跑完整流程;代价是你看不到真正的网络通信和进程管理。
The tutorial uses mock handlers to simulate external servers. The real version would spawn subprocesses and communicate via stdin/stdout JSON-RPC. Mocks let you run the full flow without external dependencies; the tradeoff is you don't see real network communication or process management.
---
## 工作原理
## How It Works
### MCPClient:发现 + 调用
### MCPClient: Discovery + Invocation
```python
class MCPClient:
@@ -63,9 +63,9 @@ class MCPClient:
return handler(**args)
```
教学版用 Python 函数模拟 server 的工具实现。真实版通过 stdio JSON-RPC 与子进程通信。
The tutorial uses Python functions to simulate server tool implementations. The real version communicates with subprocesses via stdio JSON-RPC.
### connect_mcp:连接 + 发现
### connect_mcp: Connect + Discover
```python
def connect_mcp(name: str) -> str:
@@ -79,9 +79,9 @@ def connect_mcp(name: str) -> str:
return f"Connected to '{name}'. Discovered: ..."
```
连接后server 提供的工具立即可用。
After connecting, the server's tools are immediately available.
### normalize_mcp_name:名称规范化
### normalize_mcp_name: Name Normalization
```python
_DISALLOWED_CHARS = re.compile(r'[^a-zA-Z0-9_-]')
@@ -90,9 +90,9 @@ def normalize_mcp_name(name: str) -> str:
return _DISALLOWED_CHARS.sub('_', name)
```
所有非 `[a-zA-Z0-9_-]` 的字符替换为 `_`。防止 server 名或工具名中包含特殊字符导致命名冲突或注入问题。
All non-`[a-zA-Z0-9_-]` characters are replaced with `_`. Prevents special characters in server or tool names from causing naming conflicts or injection issues.
### assemble_tool_pool:组装工具池
### assemble_tool_pool: Assemble Tool Pool
```python
def assemble_tool_pool() -> tuple[list[dict], dict]:
@@ -110,173 +110,173 @@ def assemble_tool_pool() -> tuple[list[dict], dict]:
return tools, handlers
```
前缀 `mcp__{server}__{tool}` 避免不同 server 的工具名冲突。名称经过 `normalize_mcp_name` 规范化。
The prefix `mcp__{server}__{tool}` prevents tool name collisions across different servers. Names are normalized through `normalize_mcp_name`.
MCP 工具的 description `(readOnly)` `(destructive)` 标注——教学版用文本标注,真实 CC 用 tool annotations 结构体让权限系统判断。
MCP tool descriptions include `(readOnly)` or `(destructive)` annotations — the tutorial uses text annotations, while real CC uses structured tool annotations for the permission system.
### 无缓存工具池变了prompt 也变
### No Cache: Tool Pool Changes, Prompt Changes Too
s10-s18 agent_loop prompt cache 避免重复序列化。s19 去掉了缓存:
s10-s18's agent_loop used prompt caching to avoid re-serialization. s19 removes the cache:
```python
def agent_loop(messages, context):
tools, handlers = assemble_tool_pool() # 每次重新构建
system = assemble_system_prompt(context) # 每次重新生成
tools, handlers = assemble_tool_pool() # Rebuild every time
system = assemble_system_prompt(context) # Regenerate every time
...
if any(b.name == "connect_mcp" ...):
tools, handlers = assemble_tool_pool() # 连接后重建
tools, handlers = assemble_tool_pool() # Rebuild after connection
system = assemble_system_prompt(context)
```
原因:`connect_mcp` 之后工具池变化了——新增了 `mcp__docs__search` 等工具。缓存中的工具列表是旧的,继续用会导致模型调用不到新工具。教学版直接去掉缓存,代价是多花一点序列化时间。
Reason: after `connect_mcp`, the tool pool changes — new tools like `mcp__docs__search` are added. The cached tool list is stale; continuing to use it means the model can't call the new tools. The tutorial simply removes caching, at the cost of slightly more serialization time.
### MCP 工具只有 Lead 可用
### MCP Tools: Lead Only
教学版中,`connect_mcp` Lead 工具,`assemble_tool_pool` 也只服务于 Lead agent_loopTeammate 仍使用固定的 8 个子集工具(bashread_filewrite_filesend_messagesubmit_planlist_tasksclaim_taskcomplete_task)。
In the tutorial, `connect_mcp` is a Lead tool, and `assemble_tool_pool` only serves the Lead's agent_loop. Teammates still use a fixed 8-tool subset (bash, read_file, write_file, send_message, submit_plan, list_tasks, claim_task, complete_task).
这是教学简化。真实 CC 中MCP 工具对主 agent 和子 agent 都可用——子 agent 继承父级的 MCP 配置。
This is a teaching simplification. In real CC, MCP tools are available to both the main agent and sub-agents — sub-agents inherit the parent's MCP configuration.
---
## 相对 s18 的变更
## Changes from s18
| 组件 | 之前 (s18) | 之后 (s19) |
| Component | Before (s18) | After (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 |
| Tool source | All hand-written built-in | Hand-written + MCP external tools with dynamic discovery |
| Tool pool | Fixed BUILTIN_TOOLS | assemble_tool_pool dynamically assembles mcp\_\_ prefixed tools |
| Name safety | None | normalize_mcp_name normalization |
| New type | — | MCPClient class (simulates tools/list + tools/call) |
| Namespace | — | mcp\_\_server\_\_tool prevents collisions |
| Tool descriptions | No annotations | (readOnly)/(destructive) annotations |
| Prompt cache | Yes (since s10) | Removed — tool pool is dynamic, cache goes stale |
| Lead tools | 17 (s18) | 18 (+connect_mcp) |
| Teammate tools | 8 (s18) | 8 (unchanged, MCP tools are Lead-only) |
| Extension method | Write code to add tools | Standard protocol, implement servers in any language |
---
## 试一下
## Try It Out
```sh
cd learn-claude-code
python s19_mcp_plugin/code.py
```
试试这些 prompt
Try these prompts:
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) 标注?
What to observe: After connecting to an MCP server, do tool names have `mcp__docs__` or `mcp__deploy__` prefixes? Are both servers' tools available simultaneously? Do MCP tool descriptions include (readOnly)/(destructive) annotations?
---
## 接下来
## What's Next
现在 Agent 可以通过标准协议接入外部工具了。但前面 19 章每章都只加一个机制,真实 Agent 不会这样拆开运行。
The Agent can now connect external tools through a standard protocol. But the first 19 chapters each add one mechanism in isolation; a real Agent does not run as 19 separate demos.
工具、权限、hooks、todo、任务图、记忆、压缩、后台、cron、团队、worktree、MCP 这些机制应该挂在同一个循环上,而不是散在 19 个 demo 里。
Tools, permissions, hooks, todo, task graph, memory, compact, background work, cron, teams, worktrees, and MCP should all attach to the same loop, not live in separate examples.
s20 Comprehensive Agent → 把前 19 章的机制合回一个完整 harness。机制很多循环一个。
s20 Comprehensive Agent → Combine the first 19 chapters into one complete harness. Many mechanisms, one loop.
<details>
<summary>深入 CC 源码</summary>
<summary>Deep Dive into CC Source</summary>
> 以下基于 CC 源码 `services/mcp/client.ts``auth.ts``config.ts``channelNotification.ts` 的分析。
> The following is based on analysis of CC source: `services/mcp/client.ts`, `auth.ts`, `config.ts`, `channelNotification.ts`.
### 一、6 种 Transport 类型
### 1. Six Transport Types
教学版只展示了 stdio mockCC 支持 6 种传输(`types.ts:23-25`
The tutorial only shows a stdio mock. CC supports 6 transport types (`types.ts:23-25`):
| Transport | 通信方式 |
| Transport | Communication method |
|-----------|---------|
| `stdio` | 子进程 stdin/stdout(跨平台默认) |
| `stdio` | Subprocess stdin/stdout (cross-platform default) |
| `sse` | HTTP Server-Sent Events |
| `http` | Streamable HTTPPOST/SSE 双向) |
| `http` | Streamable HTTP (POST/SSE bidirectional) |
| `ws` | WebSocket |
| `sse-ide` | IDE 内嵌 SSE 传输 |
| `sdk` | 进程内 SDK 传输 |
| `sse-ide` | IDE-embedded SSE transport |
| `sdk` | In-process SDK transport |
连接时本地stdio和远程http/sse/ws服务器分批并发本地批量 3 个,远程批量 20 个。
On connection, local (stdio) and remote (http/sse/ws) servers are batched concurrently: local batch of 3, remote batch of 20.
### 二、工具池组装算法
### 2. Tool Pool Merging Algorithm
`assembleToolPool()``tools.ts:345-364`
`assembleToolPool()` (`tools.ts:345-364`):
```typescript
// 去重时优先保留内置工具name 相同时内置在前)
// Dedup with priority: built-in tools win on name collision (sorted first)
return uniqBy(
[...builtInTools.sort(byName), ...filteredMcpTools.sort(byName)],
'name',
)
```
内置工具和 MCP 工具分开排序,不是合起来排。原因是 CC `claude_code_system_cache_policy` 在最后一个内置工具之后的某个位置放全局缓存断点——混排会破坏这个设计。
Built-in and MCP tools are sorted separately, not together. The reason is CC's `claude_code_system_cache_policy` places a global cache breakpoint after the last built-in tool at a specific position — mixing the sort would break this design.
### 三、命名规则:`mcp__server__tool`
### 3. Naming Convention: `mcp__server__tool`
`buildMcpToolName()``mcpStringUtils.ts:50-52`
`buildMcpToolName()` (`mcpStringUtils.ts:50-52`):
```
mcp__<normalizedServerName>__<normalizedToolName>
```
所有非 `[a-zA-Z0-9_-]` 字符替换为 `_``normalization.ts:17-23`)。教学版的 `normalize_mcp_name` 用同样的规则。
All non-`[a-zA-Z0-9_-]` characters are replaced with `_` (`normalization.ts:17-23`). The tutorial's `normalize_mcp_name` uses the same rule.
### 四、权限检查
### 4. Permission Checks
CC 对 MCP 工具有独立的权限系统。`checkPermissions()` 对 MCP 工具的检查逻辑不同于内置工具——MCP 工具可以声明自己的权限需求(readOnlydestructiveCC 根据声明决定是否需要用户确认。教学版只在 description 中用文本标注 `(readOnly)` / `(destructive)`,不做权限拦截。
CC has a separate permission system for MCP tools. `checkPermissions()` applies different logic for MCP tools than for built-in tools — MCP tools can declare their own permission requirements (readOnly, destructive, etc.), and CC decides whether user confirmation is needed based on the declaration. The tutorial only uses text annotations `(readOnly)` / `(destructive)` in descriptions, without permission enforcement.
### 五、配置来源与优先级
### 5. Configuration Sources and Priority
MCP 服务器配置来自多个来源。CC 的配置优先级从低到高:
MCP server configuration comes from multiple sources. CC's priority from lowest to highest:
```
claude.ai 连接器 < plugin < user settings.json < approved project .mcp.json < local settings.local.json
claude.ai connectors < plugin < user settings.json < approved project .mcp.json < local settings.local.json
```
`claude.ai` 连接器单独拉取、按内容签名去重,以最低优先级合并(`config.ts:1267-1289`)。企业 `managed-mcp.json` 存在时完全排除其他配置。
`claude.ai` connectors are fetched separately, deduplicated by content signature, and merged at the lowest precedence (`config.ts:1267-1289`). When enterprise `managed-mcp.json` exists, all other configurations are excluded.
教学版直接传 server name 给 `MOCK_SERVERS` 字典,不做配置合并。
The tutorial passes server names directly to the `MOCK_SERVERS` dict, without config merging.
### 六、Channel 通知:服务器反向推消息
### 6. Channel Notifications: Servers Push Messages Back
教学版只讲了 Agent → MCP Server 的单向调用。CC 还支持反向通知(`channelNotification.ts`
The tutorial only covers agent → MCP Server unidirectional calls. CC also supports reverse notifications (`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 秒内)
1. Server declares `capabilities.experimental['claude/channel']`
2. Server sends messages to agent via MCP notification `notifications/claude/channel`
3. Messages are wrapped in `<channel source="serverName">...</channel>` XML tags
4. Agent is woken up by SleepTool (within 1 second)
Server 还可以请求权限:`notifications/claude/channel/permission_request` → Agent 回复 `notifications/claude/channel/permission`。用户通过 5 字母短 ID 确认/拒绝。
Servers can also request permissions: `notifications/claude/channel/permission_request` → Agent replies `notifications/claude/channel/permission`. Users confirm/deny via a 5-letter short ID.
### 七、OAuth 认证流程
### 7. OAuth Authentication Flow
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's MCP authentication (`auth.ts`) supports a full OAuth 2.0 + PKCE flow:
- OAuth metadata discovery via public client + PKCE (RFC 8414 / RFC 9728)
- Local callback server receives authorization code
- Tokens persisted via `getSecureStorage()` (macOS Keychain / Linux encrypted file / Windows Credential Manager)
- Auto-refresh 5 minutes before expiry
- Cross-application access (XAA): browser gets id_token → RFC 8693 + RFC 7523 exchange → no repeated browser popups
### 八、连接生命周期的错误处理
### 8. Connection Lifecycle Error Handling
CC 对 MCP 连接有精细的错误分类和重试(`client.ts:1266-1402`
- 终局性错误(ECONNRESETETIMEDOUTEPIPE 等):连续 3 次 → 关闭 + 重连
- 工具调用 401令牌过期 → 抛出 `McpAuthError` → 触发重认证
- 工具调用超时:`Promise.race` 超时(可配置,默认约 28 小时)
- Stdio 断连:按 SIGINT → SIGTERM → SIGKILL 顺序杀进程
CC has fine-grained error classification and retry for MCP connections (`client.ts:1266-1402`):
- Terminal errors (ECONNRESET, ETIMEDOUT, EPIPE, etc.): 3 consecutive failures → close + reconnect
- Tool call 401: Token expired → throw `McpAuthError` → trigger re-authentication
- Tool call timeout: `Promise.race` timeout (configurable, default ~28 hours)
- Stdio disconnect: Kill process in SIGINT → SIGTERM → SIGKILL order
### 教学版的简化
### The Tutorial's Simplifications
- 6 transport → 1 种(mock stdio):概念量可控
- Channel 反向通知 → 省略:教学版 Agent 是主动方
- OAuth 流程 → 省略:教学版假设 server 不需要认证
- 多层配置优先级 → 省略:教学版直接传 server name
- 复杂的错误分类 → 省略:教学版用 try/except 兜底
- MCP 工具只给 Lead → 省略子 agent 继承:简化代码结构
- 6 transport types → 1 (mock stdio): Manageable concept count
- Channel reverse notifications → omitted: Tutorial agent is always the initiator
- OAuth flow → omitted: Tutorial assumes servers need no auth
- Multi-layer config priority → omitted: Tutorial passes server name directly
- Complex error classification → omitted: Tutorial uses try/except as fallback
- MCP tools Lead-only → omitted sub-agent inheritance: Simplifies code structure
</details>
<!-- translation-sync: zh@v2, en@v0, ja@v0 -->
<!-- translation-sync: zh@v2, en@v2, ja@v0 -->