From 8b63e54e945679f5fb805ae942de3fb4a2134877 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Wed, 1 Apr 2026 16:43:45 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=87=E6=A1=A3=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/agent/sub-agents.mdx | 214 +++++++++++++++++----- docs/extensibility/hooks.mdx | 266 ++++++++++++++++++++++------ docs/extensibility/mcp-protocol.mdx | 203 ++++++++++++++++----- 3 files changed, 542 insertions(+), 141 deletions(-) diff --git a/docs/agent/sub-agents.mdx b/docs/agent/sub-agents.mdx index 016b2f5..a0afc6c 100644 --- a/docs/agent/sub-agents.mdx +++ b/docs/agent/sub-agents.mdx @@ -1,70 +1,194 @@ --- -title: "子 Agent 机制 - AI 分身术与任务委派" -description: "深入解析 Claude Code 子 Agent 机制:主 Agent 如何通过 AgentTool 委派子任务,子 Agent 的生命周期管理、工具继承和结果回传。" -keywords: ["子 Agent", "Agent 分身", "任务委派", "AgentTool", "多 Agent"] +title: "子 Agent 机制 - AgentTool 的执行链路与隔离架构" +description: "从源码角度解析 Claude Code 子 Agent:AgentTool.call() 的完整执行链路、Fork 子进程的 Prompt Cache 共享、Worktree 隔离、工具池独立组装、以及结果回传的数据格式。" +keywords: ["子 Agent", "AgentTool", "任务委派", "forkSubagent", "子进程隔离"] --- -{/* 本章目标:解释子 Agent 机制的设计和应用场景 */} +{/* 本章目标:从源码角度揭示子 Agent 的完整执行链路、工具隔离、通信协议和生命周期管理 */} -## 为什么需要子 Agent +## 执行链路总览 -有些任务太大,一个 AI 实例忙不过来: +一条 `Agent(prompt="修复 bug")` 调用的完整路径: -- "在 5 个不同的文件中分别找到并修复同类 bug" -- "一边重构后端 API,一边更新前端调用" -- "研究这个库的用法,同时修改我们的代码" +``` +AI 生成 tool_use: { prompt: "修复 bug", subagent_type: "Explore" } + ↓ +AgentTool.call() ← 入口(AgentTool.tsx:239) + ├── 解析 effectiveType(fork vs 命名 agent) + ├── filterDeniedAgents() ← 权限过滤 + ├── 检查 requiredMcpServers ← MCP 依赖验证(最长等 30s) + ├── assembleToolPool(workerPermissionContext) ← 独立组装工具池 + ├── createAgentWorktree() ← 可选 worktree 隔离 + ↓ +runAgent() ← 核心执行(runAgent.ts:248) + ├── getAgentSystemPrompt() ← 构建 agent 专属 system prompt + ├── initializeAgentMcpServers() ← agent 级 MCP 服务器 + ├── executeSubagentStartHooks() ← Hook 注入 + ├── query() ← 进入标准 agentic loop + │ ├── 消息流逐条 yield + │ └── recordSidechainTranscript() ← JSONL 持久化 + ↓ +finalizeAgentTool() ← 结果汇总 + ├── 提取文本内容 + usage 统计 + └── mapToolResultToToolResultBlockParam() ← 格式化为 tool_result +``` -## 分身术的运作方式 +## 两种子 Agent 路径:命名 Agent vs Fork -Claude Code 中的 Agent 工具让 AI 能够**启动另一个 AI 实例**来处理子任务: +`AgentTool.call()` 根据是否提供 `subagent_type` 走两条完全不同的路径(`AgentTool.tsx:322-356`): - - - 主 Agent 判断任务可以被拆解为独立的子任务 - - - 通过 Agent 工具创建一个或多个子 Agent,每个子 Agent 收到一个清晰的子任务描述 - - - 多个子 Agent 可以同时工作,互不干扰 - - - 子 Agent 完成后,结果返回给主 Agent,主 Agent 汇总并呈现给用户 - - +| 维度 | 命名 Agent(`subagent_type` 指定) | Fork 子进程(`subagent_type` 省略) | +|------|-------------------------------------|--------------------------------------| +| **触发条件** | `subagent_type` 有值 | `isForkSubagentEnabled()` && 未指定类型 | +| **System Prompt** | Agent 自身的 `getSystemPrompt()` | 继承父 Agent 的完整 System Prompt | +| **工具池** | `assembleToolPool()` 独立组装 | 父 Agent 的原始工具池(`useExactTools: true`) | +| **上下文** | 仅任务描述 | 父 Agent 的完整对话历史(`forkContextMessages`) | +| **模型** | 可独立指定 | 继承父模型(`model: 'inherit'`) | +| **权限模式** | Agent 定义的 `permissionMode` | `'bubble'`(上浮到父终端) | +| **目的** | 专业任务委派 | Prompt Cache 命中率优化 | -## 子 Agent 的边界 +Fork 路径的设计核心是 **Prompt Cache 共享**:所有 fork 子进程共享父 Agent 的完整 `assistant` 消息(所有 `tool_use` 块),用相同的占位符 `tool_result` 填充,只有最后一个 `text` 块包含各自的指令。这使得 API 请求前缀字节完全一致,最大化缓存命中。 -子 Agent 不是和主 Agent 完全一样的——它有明确的能力边界: +```typescript +// forkSubagent.ts:142 — 所有 fork 子进程的占位结果 +const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background' -| 特性 | 主 Agent | 子 Agent | -|------|---------|---------| -| 可用工具 | 全部工具 | 受限子集(不能再启动子 Agent 等) | -| 上下文 | 完整的会话历史 | 只有主 Agent 给的任务描述 | -| 权限 | 用户设定 | 继承主 Agent 的权限,或更严格 | -| 状态 | 可修改全局状态 | 隔离的状态空间 | +// buildForkedMessages() 构建: +// [assistant(全量 tool_use), user(placeholder_results..., 子进程指令)] +``` -## 通信方式 +### Fork 递归防护 -主 Agent 和子 Agent 之间通过**消息邮箱**通信: +Fork 子进程保留 Agent 工具(为了 cache-identical tool defs),但通过两道防线防止递归 fork(`AgentTool.tsx:332`): -- 主 Agent 通过 `Agent` 工具启动子 Agent -- 子 Agent 通过 `SendMessage` 工具向主 Agent 报告进度 -- 这种松耦合的通信方式让 Agent 可以异步协作 +1. **`querySource` 检查**(压缩安全):`context.options.querySource === 'agent:builtin:fork'` +2. **消息扫描**(降级兜底):检测 `` 标签 + +## 工具池的独立组装 + +子 Agent 不继承父 Agent 的工具限制——它的工具池完全独立组装(`AgentTool.tsx:573-577`): + +```typescript +const workerPermissionContext = { + ...appState.toolPermissionContext, + mode: selectedAgent.permissionMode ?? 'acceptEdits' +} +const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools) +``` + +关键设计决策: +- **权限模式独立**:子 Agent 使用 `selectedAgent.permissionMode`(默认 `acceptEdits`),不受父 Agent 当前模式的限制 +- **MCP 工具继承**:`appState.mcp.tools` 包含所有已连接的 MCP 工具,子 Agent 自动获得 +- **Agent 级 MCP 服务器**:`runAgent()` 中的 `initializeAgentMcpServers()` 可以为特定 Agent 额外连接专属 MCP 服务器 + +### 工具过滤的 resolveAgentTools + +`runAgent.ts:500-502` 在工具组装后进一步过滤: + +```typescript +const resolvedTools = useExactTools + ? availableTools // Fork: 直接使用父工具 + : resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools +``` + +`resolveAgentTools()` 会根据 Agent 定义中的 `tools` 字段过滤可用工具,将 `['*']` 映射为全量工具。 + +## Worktree 隔离机制 + +`isolation: "worktree"` 参数让子 Agent 在独立的 git worktree 中工作(`AgentTool.tsx:590-593`): + +```typescript +const slug = `agent-${earlyAgentId.slice(0, 8)}` +worktreeInfo = await createAgentWorktree(slug) +``` + +Worktree 生命周期: +1. **创建**:在 `.git/worktrees/` 下创建独立工作副本 +2. **CWD 覆盖**:`runWithCwdOverride(worktreePath, fn)` 让所有文件操作在 worktree 中执行 +3. **路径翻译**:Fork + worktree 时注入路径翻译通知(`buildWorktreeNotice`) +4. **清理**(`cleanupWorktreeIfNeeded`): + - Hook-based worktree → 始终保留 + - 有变更 → 保留,返回 `worktreePath` + - 无变更 → 自动删除 + +## 生命周期管理:同步 vs 异步 + +### 异步 Agent(后台运行) + +当 `run_in_background=true` 或 `selectedAgent.background=true` 时,Agent 立即返回 `async_launched` 状态(`AgentTool.tsx:686-764`): + +``` +registerAsyncAgent(agentId, ...) ← 注册到 AppState.tasks + ↓ (void — 火后不管) +runAsyncAgentLifecycle() ← 后台执行 + ├── runAgent().onCacheSafeParams ← 进度摘要初始化 + ├── 消息流迭代 + ├── completeAsyncAgent() ← 标记完成 + ├── classifyHandoffIfNeeded() ← 安全检查 + └── enqueueAgentNotification() ← 通知主 Agent +``` + +异步 Agent 获得独立的 `AbortController`,不与父 Agent 共享——用户按 ESC 取消主线程不会杀掉后台 Agent。 + +### 同步 Agent(前台运行) + +同步 Agent 的关键特性是 **可后台化**(`AgentTool.tsx:818-833`): + +```typescript +const registration = registerAgentForeground({ + autoBackgroundMs: getAutoBackgroundMs() || undefined // 默认 120s +}) +backgroundPromise = registration.backgroundSignal.then(...) +``` + +在 agentic loop 的每次迭代中,系统用 `Promise.race` 竞争下一条消息和后台化信号: + +```typescript +const raceResult = await Promise.race([ + nextMessagePromise.then(r => ({ type: 'message', result: r })), + backgroundPromise // 超过 autoBackgroundMs 触发 +]) +``` + +后台化后,前台迭代器被终止(`agentIterator.return()`),新的 `runAgent()` 以 `isAsync: true` 重新启动,当前台的输出文件继续写入。 + +## 结果回传格式 + +`mapToolResultToToolResultBlockParam()` 根据状态返回不同格式(`AgentTool.tsx:1298-1375`): + +| 状态 | 返回内容 | +|------|---------| +| `completed` | 内容 + `` 块(token/tool_calls/duration) | +| `async_launched` | agentId + outputFile 路径 + 操作指引 | +| `teammate_spawned` | agent_id + name + team_name | +| `remote_launched` | taskId + sessionUrl + outputFile | + +对于一次性内置 Agent(Explore、Plan),`` 块被省略——每周节省约 1-2 Gtok 的上下文窗口。 + +## MCP 依赖的等待机制 + +如果 Agent 声明了 `requiredMcpServers`,`call()` 会等待这些服务器连接完成(`AgentTool.tsx:371-410`): + +```typescript +const MAX_WAIT_MS = 30_000 // 最长等 30 秒 +const POLL_INTERVAL_MS = 500 // 每 500ms 轮询 +``` + +早期退出条件:任何必需服务器进入 `failed` 状态时立即停止等待。工具可用性通过 `mcp__` 前缀工具名解析(`mcp__serverName__toolName`)判断。 ## 适用场景 - 多个子 Agent 同时搜索不同方向的信息 + 多个 fork 子进程并行搜索不同方向,共享 Prompt Cache 前缀,只有指令不同 - - 把大规模修改拆分到多个子 Agent 并行执行 - - - 一个子 Agent 在后台运行测试,主 Agent 继续写代码 + + 使用命名 Agent(Explore/Plan/verification)执行专业任务,受限工具集 + 独立权限 - 在 worktree 中启动子 Agent 尝试一个方案,不影响主分支 + `isolation: "worktree"` 在独立工作副本中尝试方案,不影响主分支 + + + `run_in_background: true` 启动长时间构建/测试任务,主 Agent 继续工作 diff --git a/docs/extensibility/hooks.mdx b/docs/extensibility/hooks.mdx index cc82b3b..7fa62e7 100644 --- a/docs/extensibility/hooks.mdx +++ b/docs/extensibility/hooks.mdx @@ -1,73 +1,239 @@ --- -title: "Hooks 生命周期钩子 - 自定义行为注入" -description: "解析 Claude Code Hooks 系统:在 AI 工具调用的关键节点(PreToolUse、PostToolUse、Notification 等)插入自定义 shell 命令,实现行为定制。" -keywords: ["Hooks", "生命周期钩子", "自定义 Hook", "行为注入", "PreToolUse"] +title: "Hooks 生命周期钩子 - 执行引擎与拦截协议" +description: "从源码角度解析 Claude Code Hooks 系统:22 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。" +keywords: ["Hooks", "生命周期钩子", "拦截器", "PreToolUse", "Hook 协议"] --- -{/* 本章目标:解释 Hooks 系统的设计和应用场景 */} +{/* 本章目标:从源码角度揭示 Hook 的执行引擎、匹配机制、返回值协议和生命周期管理 */} -## 什么是 Hooks +## 22 种 Hook 事件 -Hooks 是用户定义的 shell 命令,在 Claude Code 生命周期的特定时刻自动执行。 +Claude Code 定义了 22 种 Hook 事件(`coreTypes.ts:25-53`),覆盖完整的 Agent 生命周期: -类比:React 的 `useEffect` 让你在组件渲染后执行自定义逻辑。Claude Code 的 Hooks 让你在 AI 的关键行为前后执行自定义脚本。 +| 阶段 | 事件 | 触发时机 | 匹配字段 | +|------|------|---------|---------| +| **会话** | `SessionStart` | 会话启动 | `source` | +| | `SessionEnd` | 会话结束 | `reason` | +| | `Setup` | 初始化完成 | `trigger` | +| **用户交互** | `UserPromptSubmit` | 用户提交消息 | — | +| | `Stop` | Agent 停止响应 | — | +| | `StopFailure` | Agent 停止失败 | `error` | +| **工具执行** | `PreToolUse` | 工具调用前 | `tool_name` | +| | `PostToolUse` | 工具调用后(成功) | `tool_name` | +| | `PostToolUseFailure` | 工具调用后(失败) | `tool_name` | +| **权限** | `PermissionRequest` | 权限请求 | `tool_name` | +| | `PermissionDenied` | 权限被拒 | `tool_name` | +| **子 Agent** | `SubagentStart` | 子 Agent 启动 | `agent_type` | +| | `SubagentStop` | 子 Agent 停止 | `agent_type` | +| **压缩** | `PreCompact` | 上下文压缩前 | `trigger` | +| | `PostCompact` | 上下文压缩后 | `trigger` | +| **协作** | `TeammateIdle` | Teammate 空闲 | — | +| | `TaskCreated` | 任务创建 | — | +| | `TaskCompleted` | 任务完成 | — | +| **MCP** | `Elicitation` | MCP 服务器请求用户输入 | `mcp_server_name` | +| | `ElicitationResult` | Elicitation 结果返回 | `mcp_server_name` | +| **环境** | `ConfigChange` | 配置变更 | `source` | +| | `CwdChanged` | 工作目录变更 | — | +| | `FileChanged` | 文件变更 | `file_path` | +| | `InstructionsLoaded` | 指令加载 | `load_reason` | +| | `WorktreeCreate` / `WorktreeRemove` | Worktree 操作 | — | -## 可用的 Hook 事件 +## 6 种 Hook 类型 -| 事件 | 触发时机 | 典型用途 | +Hooks 配置支持 6 种执行方式(`src/types/hooks.ts`): + +| 类型 | 执行方式 | 适用场景 | |------|---------|---------| -| **PreToolUse** | 工具调用前 | 拦截危险操作、自定义审批逻辑 | -| **PostToolUse** | 工具调用后 | 记录日志、触发通知、自动格式化 | -| **PreCompact** | 上下文压缩前 | 标记不可丢失的信息 | -| **PostCompact** | 上下文压缩后 | 验证关键信息是否保留 | -| **Notification** | AI 发出通知时 | 自定义通知渠道(Slack、邮件等) | -| **StopFailure** | AI 循环异常停止时 | 自定义错误处理 | +| `command` | Shell 命令(bash/PowerShell) | 通用脚本、CI 检查 | +| `prompt` | 注入到 AI 上下文 | 代码规范提醒 | +| `agent` | 启动子 Agent 执行 | 复杂分析任务 | +| `http` | HTTP 请求 | 远程服务、Webhook | +| `callback` | 内部 JS 函数 | 系统内置 Hook | +| `function` | 运行时注册的函数 Hook | Agent/Skill 内部使用 | -## Hook 的能力 +## 执行引擎:execCommandHook -Hook 脚本不仅能"观察",还能"干预": +`execCommandHook()`(`src/utils/hooks.ts:829-1417`)是命令型 Hook 的执行核心: - - - 返回特定信号可以阻止工具调用执行 - - - 返回结构化的 JSON 输出,影响 Claude Code 的后续行为 - - - 向 AI 的对话中注入额外信息 - - - 调用 CI/CD、发送通知、更新 Issue tracker - - +``` +execCommandHook(hook, hookEvent, hookName, jsonInput, signal) + ├── Shell 选择: hook.shell ?? DEFAULT_HOOK_SHELL + │ ├── bash: spawn(cmd, [], { shell: gitBashPath | true }) + │ └── powershell: spawn(pwsh, ['-NoProfile', '-NonInteractive', '-Command', cmd]) + ├── 变量替换 + │ ├── ${CLAUDE_PLUGIN_ROOT} → pluginRoot 路径 + │ ├── ${CLAUDE_PLUGIN_DATA} → plugin 数据目录 + │ └── ${user_config.X} → 用户配置值 + ├── 环境变量注入 + │ ├── CLAUDE_PROJECT_DIR + │ ├── CLAUDE_ENV_FILE(SessionStart/Setup/CwdChanged/FileChanged) + │ └── CLAUDE_PLUGIN_OPTION_*(plugin options) + ├── stdin 写入: jsonInput + '\n' + ├── 超时: hook.timeout * 1000 ?? 600000ms(10分钟) + └── 异步检测: 检查 stdout 首行是否为 {"async":true} +``` -## 配置方式 +### 异步 Hook 的检测协议 -Hooks 在 `settings.json` 中配置: +Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务(`hooks.ts:1199-1246`): + +```typescript +const firstLine = firstLineOf(stdout).trim() +if (isAsyncHookJSONOutput(parsed)) { + executeInBackground({ + processId: `async_hook_${child.pid}`, + asyncResponse: parsed, + ... + }) +} +``` + +后台 Hook 通过 `registerPendingAsyncHook()` 注册到 `AsyncHookRegistry`,完成后通过 `enqueuePendingNotification()` 通知主线程。 + +### asyncRewake:Hook 唤醒模型 + +`asyncRewake` 模式的 Hook 绕过 `AsyncHookRegistry`。当 Hook 退出码为 2 时,通过 `enqueuePendingNotification()` 以 `task-notification` 模式注入消息,唤醒空闲的模型(通过 `useQueueProcessor`)或在忙碌时注入 `queued_command` 附件。 + +## Hook 输出的 JSON Schema + +同步 Hook 的输出遵循严格的 Zod schema(`src/types/hooks.ts:49-567`): ```json { - "hooks": { - "PostToolUse": [ - { - "matcher": { "tool_name": "Write" }, - "hooks": [ - { - "type": "command", - "command": "npx prettier --write $CLAUDE_FILE_PATH" - } - ] - } - ] + "continue": false, // 是否继续执行 + "suppressOutput": true, // 隐藏 stdout + "stopReason": "安全检查失败", // continue=false 时的原因 + "decision": "approve" | "block", // 全局决策 + "reason": "原因说明", // 决策原因 + "systemMessage": "警告内容", // 注入到上下文的系统消息 + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow" | "deny" | "ask", + "permissionDecisionReason": "匹配了安全规则", + "updatedInput": { ... }, // 修改后的工具输入 + "additionalContext": "额外上下文" // 注入到对话 } } ``` -这个例子:每当 AI 写入一个文件后,自动用 Prettier 格式化。 +### 各事件的 hookSpecificOutput -## 安全控制 +| 事件 | 专有字段 | 作用 | +|------|---------|------| +| `PreToolUse` | `permissionDecision`, `updatedInput`, `additionalContext` | 拦截/修改工具输入 | +| `UserPromptSubmit` | `additionalContext` | 注入额外上下文 | +| `PostToolUse` | `additionalContext`, `updatedMCPToolOutput` | 修改 MCP 工具输出 | +| `SessionStart` | `initialUserMessage`, `watchPaths` | 设置初始消息和文件监控 | +| `PermissionDenied` | `retry` | 指示是否重试 | +| `Elicitation` | `action`, `content` | 控制用户输入对话框 | -- 托管设置(企业管理员)的 Hooks 优先级最高,用户不能覆盖 -- Hook 执行有超时限制 -- Hook 的输出会被解析和验证,防止注入攻击 +## Hook 匹配机制:getMatchingHooks + +`getMatchingHooks()`(`hooks.ts:1685-1956`)负责从所有来源中查找匹配的 Hook: + +### 多来源合并 + +``` +getHooksConfig() + ├── getHooksConfigFromSnapshot() ← settings.json 中的 Hook(user/project/local) + ├── getRegisteredHooks() ← SDK 注册的 callback Hook + ├── getSessionHooks() ← Agent/Skill 前置注册的 session Hook + └── getSessionFunctionHooks() ← 运行时 function Hook +``` + +### 匹配规则 + +`matcher` 字段支持三种模式(`matchesPattern()`, `hooks.ts:1428-1463`): + +``` +"Write" → 精确匹配 +"Write|Edit" → 管道分隔的多值匹配 +"^Bash(git.*)" → 正则匹配 +"*" 或 "" → 通配(匹配所有) +``` + +### if 条件过滤 + +Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()`(`hooks.ts:1472-1503`)预编译匹配器: + +```json +{ + "hooks": [{ + "command": "check-git-branch.sh", + "if": "Bash(git push*)" + }] +} +``` + +`if` 条件使用 `permissionRuleValueFromString` 解析,支持与权限规则相同的语法(工具名 + 参数模式)。Bash 工具还会使用 tree-sitter 进行 AST 级别的命令解析。 + +### Hook 去重 + +同一个 Hook 命令在不同配置层级(user/project/local)可能重复。系统按 `pluginRoot\0command` 做 Map 去重,保留**最后合并的层级**。 + +## 工作区信任检查 + +**所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()`, `hooks.ts:286-296`)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。 + +```typescript +// 交互模式下,所有 Hook 要求信任 +const hasTrust = checkHasTrustDialogAccepted() +return !hasTrust +``` + +SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。 + +## 四种 Hook 能力的源码映射 + +### 1. 拦截操作(PreToolUse) + +```json +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny" + } +} +``` + +`processHookJSONOutput()` 将 `permissionDecision` 映射为 `result.permissionBehavior = 'deny'`,并设置 `blockingError`,阻止工具执行。 + +### 2. 修改行为(updatedInput / updatedMCPToolOutput) + +```json +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "updatedInput": { "command": "npm test -- --bail" } + } +} +``` + +`updatedInput` 替换原始工具输入;`updatedMCPToolOutput`(PostToolUse 事件)替换 MCP 工具的返回值——可用于过滤敏感数据。 + +### 3. 注入上下文(additionalContext / systemMessage) + +- `additionalContext` → 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息 +- `systemMessage` → 注入为系统警告,直接显示给用户 + +### 4. 控制流程(continue / stopReason) + +```json +{ "continue": false, "stopReason": "构建失败,停止执行" } +``` + +`continue: false` 设置 `preventContinuation = true`,阻止 Agent 继续执行后续操作。 + +## Session Hook 的生命周期 + +Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(`runAgent.ts:567-575`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()` 清理。 + +```typescript +// runAgent.ts:567 — 注册 agent 的前置 Hook +registerFrontmatterHooks(rootSetAppState, agentId, agentDefinition.hooks, ...) + +// runAgent.ts:820 — finally 块清理 +clearSessionHooks(rootSetAppState, agentId) +``` + +这确保 Agent A 的 Hook 不会泄漏到 Agent B 的执行中。 diff --git a/docs/extensibility/mcp-protocol.mdx b/docs/extensibility/mcp-protocol.mdx index 52a24db..126fcd4 100644 --- a/docs/extensibility/mcp-protocol.mdx +++ b/docs/extensibility/mcp-protocol.mdx @@ -1,68 +1,175 @@ --- -title: "MCP 协议 - 开放的工具生态扩展" -description: "深入解析 Claude Code 的 MCP(Model Context Protocol)集成:通过标准协议对接数据库、API 和自定义服务,突破内置工具的能力边界。" -keywords: ["MCP", "Model Context Protocol", "工具扩展", "外部集成", "API 对接"] +title: "MCP 协议 - 连接管理、工具发现与执行链路" +description: "从源码角度解析 Claude Code 的 MCP 集成:7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。" +keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现"] --- -{/* 本章目标:解释 MCP 协议如何扩展 AI 的能力边界 */} +{/* 本章目标:从源码角度揭示 MCP 客户端的连接管理、工具发现协议和执行链路 */} -## 内置工具的局限 +## 架构总览:从配置到可用工具 -Claude Code 内置了 50+ 工具,覆盖了通用的软件开发需求。但每个团队都有特殊需求: +``` +settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } } + ↓ +getAllMcpConfigs() ← 合并 user/project/local 三级配置 + ↓ +useManageMCPConnections() ← React Hook 管理连接生命周期 + ↓ +connectToServer(name, config) ← memoize 缓存(lodash memoize) + ├── 创建 Transport(stdio/sse/http/...) + ├── new Client() ← @modelcontextprotocol/sdk + ├── client.connect(transport) ← 超时控制(MCP_TIMEOUT, 默认 30s) + └── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending } + ↓ +fetchToolsForClient(client) ← LRU(20) 缓存 + ├── client.request({ method: 'tools/list' }) + └── 每个工具包装为 MCPTool ← 统一 Tool 接口 + ↓ +assembleToolPool() ← 合并内置工具 + MCP 工具 + ↓ +工具名格式: mcp____ ← buildMcpToolName() +``` -- 连接内部数据库查询数据 -- 调用公司内部 API -- 操作特定的 DevOps 工具 -- 访问私有的知识库 +## 7 种传输层实现 -不可能把所有人的需求都内置进去。 +`connectToServer()`(`client.ts:596-1643`)根据 `config.type` 分发到不同的 Transport 实现: -## MCP:一个标准的"插头" +| 传输类型 | Transport 类 | 适用场景 | 认证方式 | +|----------|-------------|---------|---------| +| `stdio`(默认) | `StdioClientTransport` | 本地子进程 | 无 | +| `sse` | `SSEClientTransport` | 远程 SSE 服务 | `ClaudeAuthProvider` + OAuth | +| `http` | `StreamableHTTPClientTransport` | HTTP 流 | `ClaudeAuthProvider` + OAuth | +| `sse-ide` | `SSEClientTransport` | IDE 集成 | lockfile token | +| `ws-ide` | `WebSocketTransport` | IDE WebSocket | `X-Claude-Code-Ide-Authorization` | +| `ws` | `WebSocketTransport` | WebSocket 服务 | session ingress token | +| `claudeai-proxy` | `StreamableHTTPClientTransport` | claude.ai 代理 | OAuth bearer + 401 重试 | -**Model Context Protocol**(模型上下文协议)是 Anthropic 提出的开放标准,定义了 AI 与外部工具之间的通信方式。 +### stdio 传输的进程管理 - - MCP 连接架构图 - +stdio 类型的 MCP 服务器作为子进程运行,cleanup 时采用 **信号升级策略**(`client.ts:1431-1564`): -类比:USB 是电脑连接外设的标准接口。MCP 是 AI 连接外部能力的标准接口。 +``` +SIGINT (100ms) → SIGTERM (400ms) → SIGKILL +``` -## 工作原理 +总清理时间上限 600ms,防止 MCP 服务器关闭阻塞 CLI 退出。 - - - 开发者编写一个 MCP Server,暴露自定义工具(比如"查询数据库"、"发送 Slack 消息") - - - 在配置文件中声明要连接的 MCP Server - - - 连接后,MCP Server 提供的工具自动出现在 AI 的可用工具列表中 - - - AI 像使用内置工具一样使用 MCP 工具——无需知道底层实现 - - +### 远程传输的认证状态机 -## 三种连接方式 +SSE/HTTP 类型使用 `ClaudeAuthProvider` 实现 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,并写入 15 分钟 TTL 的缓存文件(`mcp-needs-auth-cache.json`),避免重复弹出认证提示。 -| 方式 | 适用场景 | -|------|---------| -| **stdio** | MCP Server 作为子进程运行,通过标准输入/输出通信。最简单 | -| **SSE** | 通过 HTTP Server-Sent Events 通信。适合远程服务 | -| **StreamableHTTP** | 基于 HTTP 流的双向通信。适合复杂的交互场景 | +``` +连接尝试 → 401 Unauthorized + ↓ +handleRemoteAuthFailure() + ├── logEvent('tengu_mcp_server_needs_auth') + ├── setMcpAuthCacheEntry(name) ← 写入 15min TTL 缓存 + └── return { type: 'needs-auth' } ← UI 显示认证提示 +``` -## 权限一视同仁 +## 连接缓存与重连机制 -MCP 提供的工具和内置工具一样受权限系统管控: +`connectToServer` 使用 lodash `memoize` 缓存连接对象,缓存 key 为 `${name}-${JSON.stringify(config)}`。 -- 需要用户确认才能调用 -- 可以设置 allow/deny 规则 -- 支持沙箱限制 +### 缓存失效触发 -这确保了第三方工具不会绕过安全边界。 +当连接关闭时(`client.onclose`),清除所有相关缓存(`client.ts:1376-1404`): -## 实际例子 +```typescript +client.onclose = () => { + const key = getServerCacheKey(name, serverRef) + fetchToolsForClient.cache.delete(name) // 工具缓存 + fetchResourcesForClient.cache.delete(name) // 资源缓存 + fetchCommandsForClient.cache.delete(name) // 命令缓存 + connectToServer.cache.delete(key) // 连接缓存 +} +``` + +### 连接降级检测 + +远程传输有 **连续错误计数器**(`client.ts:1229`): + +```typescript +let consecutiveConnectionErrors = 0 +const MAX_ERRORS_BEFORE_RECONNECT = 3 +``` + +遇到终端错误(ECONNRESET、ETIMEDOUT、EPIPE 等)连续 3 次后,主动关闭 transport 触发重连。对于 HTTP 传输,还检测 session 过期(404 + JSON-RPC code -32001)。 + +### 请求级超时保护 + +每个 HTTP 请求使用独立的 `setTimeout` 超时(`wrapFetchWithTimeout`,`client.ts:493`),而非共享 `AbortSignal.timeout()`。原因是 Bun 对 AbortSignal.timeout 的 GC 是惰性的——每个请求约 2.4KB 原生内存,即使请求毫秒级完成也要等 60s 才回收。 + +```typescript +const controller = new AbortController() +const timer = setTimeout(c => c.abort(...), MCP_REQUEST_TIMEOUT_MS, controller) +timer.unref?.() // 不阻止进程退出 +``` + +## 工具发现:从 MCP 到 Tool 接口 + +`fetchToolsForClient()`(`client.ts:1745-2000`)使用 `memoizeWithLRU` 缓存(上限 20),将 MCP 工具转换为 Claude Code 的统一 Tool 接口: + +```typescript +const fullyQualifiedName = buildMcpToolName(client.name, tool.name) +// 结果: "mcp__my-db__query" +``` + +### 工具描述截断 + +MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`)。OpenAPI 生成的 MCP 服务器曾观察到 15-60KB 的描述文档。 + +### 工具能力标注 + +每个 MCP 工具根据 `tool.annotations` 自动标注: + +| 注解 | 映射到 | 含义 | +|------|--------|------| +| `readOnlyHint` | `isReadOnly()` + `isConcurrencySafe()` | 只读,可并行 | +| `destructiveHint` | `isDestructive()` | 破坏性操作 | +| `openWorldHint` | `isOpenWorld()` | 开放世界(不可枚举) | +| `title` | `userFacingName()` | 显示名称 | + +### MCP 工具的权限检查 + +MCP 工具默认返回 `{ behavior: 'passthrough' }`(`client.ts:1816-1834`),意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。 + +## MCP 工具的执行链路 + +``` +AI 生成 tool_use: { name: "mcp__my-db__query", input: { sql: "..." } } + ↓ +MCPTool.call() ← client.ts:1835 + ├── ensureConnectedClient() ← 确保连接有效(重连) + ├── callMCPToolWithUrlElicitationRetry() ← 带 Elicitation 重试 + │ ├── client.request({ method: 'tools/call' }) + │ ├── 处理图片结果(resize + persist) + │ └── 内容截断(mcpContentNeedsTruncation) + ├── McpSessionExpiredError → 重试一次 + └── 返回 { data: content, mcpMeta } +``` + +### Session 过期自动重试 + +HTTP 传输的 MCP session 可能过期。检测到 `McpSessionExpiredError` 后自动重试一次(`client.ts:1862`),因为 `ensureConnectedClient()` 已经清除了缓存并建立了新连接。 + +### 内容截断与持久化 + +大型 MCP 工具输出通过 `truncateMcpContentIfNeeded` 截断,二进制内容(图片)通过 `persistBinaryContent` 写入文件并返回文件路径。图片自动 resize(`maybeResizeAndDownsampleImageBuffer`)。 + +## MCP 连接的并发控制 + +```typescript +// 本地服务器并发连接数 +getMcpServerConnectionBatchSize() // 默认 3 + +// 远程服务器并发连接数 +getRemoteMcpServerConnectionBatchSize() // 默认 20 +``` + +本地 MCP 服务器(stdio)是重量级的子进程,默认限制 3 个并发连接。远程服务器是轻量级 HTTP 请求,允许 20 个并发。 + +## 实际配置示例 ```json // settings.json 中的 MCP 配置 @@ -72,9 +179,13 @@ MCP 提供的工具和内置工具一样受权限系统管控: "command": "npx", "args": ["@my-org/db-mcp-server"], "env": { "DB_URL": "postgres://..." } + }, + "remote-api": { + "type": "http", + "url": "https://api.example.com/mcp" } } } ``` -配置后,AI 就多了"查询数据库"这个能力——用自然语言描述需求,AI 自动生成查询并执行。 +配置后,AI 的工具列表中会出现 `mcp__my-database__query` 和 `mcp__remote-api__*` 工具——与内置工具使用相同的权限检查链路和 UI 渲染。