vault backup: 2026-06-09 23:15:17

This commit is contained in:
2026-06-09 23:15:17 +08:00
parent 31fd89aafe
commit e92c0327d9
111 changed files with 7276 additions and 8846 deletions
@@ -1,12 +1,17 @@
---
title: "自定义 Agent - 从 Markdown 到运行时的完整链路"
description: "揭秘 Claude Code 自定义 Agent 完整链路:Agent 定义的 Markdown 数据模型、三种加载来源、工具过滤策略和与 AgentTool 的联动机制。"
keywords: ["自定义 Agent", "Agent 定义", "Markdown Agent", "Agent 配置", "角色定制"]
tags: [自定义Agent, Agent定义, Markdown, Agent配置, 角色定制]
create time: 2026-06-09 22:30
---
{/* 本章目标:揭示 Agent 定义的完整数据模型、加载发现机制、工具过滤和与 AgentTool 的联动 */}
# 自定义 Agent - 从 Markdown 到运行时的完整链路
## Agent 定义的三种来源
## 概述
揭秘 Claude Code 自定义 Agent 完整链路:Agent 定义的 Markdown 数据模型、三种加载来源、工具过滤策略和与 AgentTool 的联动机制。
## 正文
### Agent 定义的三种来源
Claude Code 的 Agent 不仅仅来自用户自定义——系统有三类来源,按优先级合并:
@@ -18,7 +23,7 @@ Claude Code 的 Agent 不仅仅来自用户自定义——系统有三类来源
合并逻辑在 `getActiveAgentsFromList()` 中:按 `agentType` 去重,后者覆盖前者。这意味着你可以在 `.claude/agents/` 中放一个 `Explore.md` 来完全替换内置的 Explore Agent。
## Markdown Agent 文件的完整格式
### Markdown Agent 文件的完整格式
```markdown
---
@@ -69,7 +74,7 @@ color: "blue" # 终端中的 Agent 颜色标识
(正文内容 = system prompt)
```
### 字段解析细节
#### 字段解析细节
- **`tools`**:通过 `parseAgentToolsFromFrontmatter()` 解析,支持逗号分隔字符串或数组
- **`model: "inherit"`**:使用主线程的模型(区分大小写,只有小写 "inherit" 有效)
@@ -77,46 +82,44 @@ color: "blue" # 终端中的 Agent 颜色标识
- **`isolation: "remote"`**:仅在 Anthropic 内部可用(`USER_TYPE === 'ant'`),外部构建只支持 `worktree`
- **`background`**:`true` 使 Agent 始终在后台运行,主线程不等待结果
## 加载与发现机制
### 加载与发现机制
`getAgentDefinitionsWithOverrides()`(被 `memoize` 缓存)执行完整的发现流程:
```
1. 加载 Markdown 文件
├── loadMarkdownFilesForSubdir('agents', cwd)
│ ├── ~/.claude/agents/*.md (用户级,source = 'userSettings')
│ ├── .claude/agents/*.md (项目级,source = 'projectSettings')
│ └── managed/policy sources (策略级,source = 'policySettings')
│
└── 每个 .md 文件:
├── 解析 YAML frontmatter
├── 正文作为 system prompt
├── 校验必需字段(name, description)
├── 静默跳过无 frontmatter 的 .md 文件(可能是参考文档)
└── 解析失败 → 记录到 failedFiles,不阻塞其他 Agent
2. 并行加载 Plugin Agents
└── loadPluginAgents() → memoized
3. 初始化 Memory Snapshots(如果 AGENT_MEMORY_SNAPSHOT 启用)
└── initializeAgentMemorySnapshots()
4. 合并 Built-in + Plugin + Custom
└── getActiveAgentsFromList() → 按 agentType 去重,后者覆盖前者
5. 分配颜色
└── setAgentColor(agentType, color) → 终端 UI 中区分不同 Agent
```mermaid
flowchart TD
A["1. 加载 Markdown 文件"] --> B["loadMarkdownFilesForSubdir('agents', cwd)"]
B --> C["~/.claude/agents/*.md 用户级 source='userSettings'"]
B --> D[".claude/agents/*.md 项目级 source='projectSettings'"]
B --> E["managed/policy sources 策略级 source='policySettings'"]
A --> F["每个 .md 文件"]
F --> G["解析 YAML frontmatter"]
F --> H["正文作为 system prompt"]
F --> I["校验必需字段 name/description"]
F --> J["静默跳过无 frontmatter 的 .md 文件"]
F --> K["解析失败 -> 记录到 failedFiles,不阻塞其他 Agent"]
A --> L["2. 并行加载 Plugin Agents"]
L --> M["loadPluginAgents() memoized"]
A --> N["3. 初始化 Memory Snapshots"]
N --> O["initializeAgentMemorySnapshots()"]
A --> P["4. 合并 Built-in + Plugin + Custom"]
P --> Q["getActiveAgentsFromList() 按 agentType 去重,后者覆盖前者"]
Q --> R["5. 分配颜色"]
R --> S["setAgentColor(agentType, color)"]
```
## 工具过滤的实现
### 工具过滤的实现
当 Agent 被派生时,`AgentTool` 根据定义中的 `tools` / `disallowedTools` 过滤可用工具列表:
```
全部工具
↓ disallowedTools 移除
↓ tools 白名单过滤(如果指定)
可用工具
```mermaid
flowchart TD
A["全部工具"] --> B["disallowedTools 移除"]
B --> C{"tools 指定?"}
C -->|"是"| D["tools 白名单过滤"]
C -->|"否"| E["全部保留"]
D --> F["可用工具"]
E --> F
```
- **`tools` 未指定**:Agent 可以使用所有工具(默认全能)
@@ -137,7 +140,7 @@ disallowedTools: [
]
```
## System Prompt 的注入方式
### System Prompt 的注入方式
Agent 的 system prompt 通过 `getSystemPrompt()` 闭包延迟生成:
@@ -158,36 +161,27 @@ getSystemPrompt: () => {
对于 Built-in Agent,`getSystemPrompt` 接受 `toolUseContext` 参数,可以根据运行时状态(如是否使用嵌入式搜索工具)动态调整 prompt 内容。
## 与 AgentTool 的联动
### 与 AgentTool 的联动
当主 Agent 需要派生子 Agent 时:
```
AgentTool.call({ subagent_type: "reviewer", ... })
↓
1. 从 agentDefinitions.activeAgents 查找 agentType === "reviewer"
↓
2. 检查 requiredMcpServers(如果 Agent 要求特定 MCP 服务器)
↓
3. 过滤工具列表(tools / disallowedTools)
↓
4. 解析模型:
- "inherit" → 使用主线程模型
- 具体模型名 → 直接使用
- 未指定 → 主线程模型
↓
5. 解析权限模式(permissionMode)
↓
6. 构建隔离环境(如果 isolation === "worktree")
↓
7. 注入 system prompt(getSystemPrompt())
↓
8. 注入 initialPrompt(如果定义了)
↓
9. 启动子 Agent 循环(forkSubagent / runAgent)
```mermaid
flowchart TD
A["AgentTool.call({ subagent_type: 'reviewer', ... })"] --> B["从 agentDefinitions.activeAgents 查找 agentType === 'reviewer'"]
B --> C["检查 requiredMcpServers"]
C --> D["过滤工具列表 tools/disallowedTools"]
D --> E{"解析模型"}
E -->|"'inherit'"| F["使用主线程模型"]
E -->|"具体模型名"| G["直接使用"]
E -->|"未指定"| F
E --> H["解析权限模式 permissionMode"]
H --> I["构建隔离环境 isolation === 'worktree'"]
I --> J["注入 system prompt getSystemPrompt()"]
J --> K["注入 initialPrompt 如果定义了"]
K --> L["启动子 Agent 循环 forkSubagent/runAgent"]
```
## 内置 Agent 参考
### 内置 Agent 参考
| Agent | agentType | 角色 | 工具限制 | 模型 |
|-------|-----------|------|---------|------|
@@ -198,9 +192,10 @@ AgentTool.call({ subagent_type: "reviewer", ... })
| **Code Guide** | `claude-code-guide` | Claude Code 使用指南 | 只读 | — |
| **Statusline Setup** | `statusline-setup` | 终端状态栏配置 | 有限 | — |
SDK 入口(`sdk-ts`/`sdk-py`/`sdk-cli`)不加载 Code Guide Agent。环境变量 `CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS` 可以完全禁用内置 Agent,给 SDK 用户提供空白画布。
> [!tip]
> SDK 入口(`sdk-ts`/`sdk-py`/`sdk-cli`)不加载 Code Guide Agent。环境变量 `CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS` 可以完全禁用内置 Agent,给 SDK 用户提供空白画布。
## Agent Memory:持久化的 Agent 状态
### Agent Memory:持久化的 Agent 状态
当 `memory` 字段启用时,Agent 获得跨会话的持久记忆:
@@ -209,3 +204,9 @@ SDK 入口(`sdk-ts`/`sdk-py`/`sdk-cli`)不加载 Code Guide Agent。环境
- **`user`**:所有项目共享
Memory 通过 `loadAgentMemoryPrompt()` 注入到 system prompt 末尾,包含读写记忆的指令。Agent Memory Snapshot 机制在项目间同步 `user` 级记忆。
## 关联笔记
- [[claude-code-best/docs/features/extensibility/hooks]]
- [[claude-code-best/docs/features/extensibility/skills]]
- [[claude-code-best/docs/features/fork-subagent]]
@@ -1,12 +1,17 @@
---
title: "Hooks 生命周期钩子 - 执行引擎与拦截协议"
description: "从源码角度解析 Claude Code Hooks 系统:27 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。"
keywords: ["Hooks", "生命周期钩子", "拦截器", "PreToolUse", "Hook 协议"]
tags: [Hooks, 生命周期钩子, 拦截器, PreToolUse, Hook协议]
create time: 2026-06-09 22:30
---
{/* 本章目标:从源码角度揭示 Hook 的执行引擎、匹配机制、返回值协议和生命周期管理 */}
# Hooks 生命周期钩子 - 执行引擎与拦截协议
## 27 种 Hook 事件
## 概述
从源码角度解析 Claude Code Hooks 系统:27 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。
## 正文
### 27 种 Hook 事件
Claude Code 定义了 27 种 Hook 事件(`HOOK_EVENTS` 数组,`src/entrypoints/sdk/coreTypes.ts`),覆盖完整的 Agent 生命周期:
@@ -39,11 +44,11 @@ Claude Code 定义了 27 种 Hook 事件(`HOOK_EVENTS` 数组,`src/entrypoin
| | `InstructionsLoaded` | 指令加载 | `load_reason` |
| | `WorktreeCreate` / `WorktreeRemove` | Worktree 操作 | — |
## 6 种 Hook 类型
### 6 种 Hook 类型
Hooks 配置支持 6 种执行方式,类型定义分布在 3 个文件中:
- **可持久化类型**(`command`、`prompt`、`agent`、`http`)— Zod schema 定义在 `src/schemas/hooks.ts`,通过 `z.discriminatedUnion('type', [...])` 声明
- **可持久化类型**(`command`、`prompt`、`agent`、`http`)— Zod schema 定义在 `src/schemas/hooks.ts`
- **callback 类型** — TypeScript 接口定义在 `src/types/hooks.ts`,用于 SDK 注册的内部 JS 函数
- **function 类型** — 定义在 `src/utils/hooks/sessionHooks.ts`,用于运行时动态注册的函数 Hook
@@ -56,31 +61,31 @@ Hooks 配置支持 6 种执行方式,类型定义分布在 3 个文件中:
| `callback` | 内部 JS 函数 | 系统内置 Hook |
| `function` | 运行时注册的函数 Hook | Agent/Skill 内部使用 |
## 执行引擎:execCommandHook
### 执行引擎:execCommandHook
`execCommandHook()`(`src/utils/hooks.ts`,`execCommandHook` 函数)是命令型 Hook 的执行核心:
`execCommandHook()`(`src/utils/hooks.ts`)是命令型 Hook 的执行核心:
```
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}
```mermaid
flowchart TD
A["execCommandHook(hook, hookEvent, hookName, jsonInput, signal)"] --> B{"Shell 选择"}
B -->|"bash"| C["spawn(cmd, [], { shell: gitBashPath | true })"]
B -->|"powershell"| D["spawn(pwsh, ['-NoProfile', '-NonInteractive', '-Command', cmd])"]
A --> E["变量替换"]
E --> F["${CLAUDE_PLUGIN_ROOT} -> pluginRoot 路径"]
E --> G["${CLAUDE_PLUGIN_DATA} -> plugin 数据目录"]
E --> H["${user_config.X} -> 用户配置值"]
A --> I["环境变量注入"]
I --> J["CLAUDE_PROJECT_DIR"]
I --> K["CLAUDE_ENV_FILE SessionStart/Setup/CwdChanged/FileChanged"]
I --> L["CLAUDE_PLUGIN_OPTION_* plugin options"]
A --> M["stdin 写入: jsonInput + '\\n'"]
A --> N["超时: hook.timeout * 1000 ?? 600000ms 10分钟"]
A --> O["异步检测: 检查 stdout 首行是否为 {async:true}"]
```
### 异步 Hook 的检测协议
#### 异步 Hook 的检测协议
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务(`isAsyncHookJSONOutput` 检测 + `executeInBackground` 调用):
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务:
```typescript
const firstLine = firstLineOf(stdout).trim()
@@ -95,13 +100,13 @@ if (isAsyncHookJSONOutput(parsed)) {
后台 Hook 通过 `registerPendingAsyncHook()` 注册到 `AsyncHookRegistry`,完成后通过 `enqueuePendingNotification()` 通知主线程。
### asyncRewake:Hook 唤醒模型
#### asyncRewake:Hook 唤醒模型
`asyncRewake` 模式的 Hook 绕过 `AsyncHookRegistry`。当 Hook 退出码为 2 时,通过 `enqueuePendingNotification()` 以 `task-notification` 模式注入消息,唤醒空闲的模型(通过 `useQueueProcessor`)或在忙碌时注入 `queued_command` 附件。
`asyncRewake` 模式的 Hook 绕过 `AsyncHookRegistry`。当 Hook 退出码为 2 时,通过 `enqueuePendingNotification()` 以 `task-notification` 模式注入消息,唤醒空闲的模型或在忙碌时注入 `queued_command` 附件。
## Hook 输出的 JSON Schema
### Hook 输出的 JSON Schema
同步 Hook 的输出遵循严格的 Zod schema(`syncHookResponseSchema`,定义在 `src/types/hooks.ts`,`hookJSONOutputSchema` 定义在 `src/schemas/hooks.ts`):
同步 Hook 的输出遵循严格的 Zod schema:
```json
{
@@ -121,7 +126,7 @@ if (isAsyncHookJSONOutput(parsed)) {
}
```
### 各事件的 hookSpecificOutput
#### 各事件的 hookSpecificOutput
| 事件 | 专有字段 | 作用 |
|------|---------|------|
@@ -141,34 +146,34 @@ if (isAsyncHookJSONOutput(parsed)) {
| `FileChanged` | `watchPaths` | 文件变更后更新监控路径 |
| `WorktreeCreate` | `worktreePath` | Worktree 创建通知 |
## Hook 匹配机制:getMatchingHooks
### Hook 匹配机制:getMatchingHooks
`getMatchingHooks()`(`src/utils/hooks.ts`,`getMatchingHooks` 函数)负责从所有来源中查找匹配的 Hook:
`getMatchingHooks()`(`src/utils/hooks.ts`)负责从所有来源中查找匹配的 Hook:
### 多来源合并
#### 多来源合并
```
getHooksConfig()
├── getHooksConfigFromSnapshot() ← settings.json 中的 Hook(user/project/local)
├── getRegisteredHooks() ← SDK 注册的 callback Hook
├── getSessionHooks() ← Agent/Skill 前置注册的 session Hook
└── getSessionFunctionHooks() ← 运行时 function Hook
```mermaid
flowchart TD
A["getHooksConfig()"] --> B["getHooksConfigFromSnapshot() settings.json 中的 Hook user/project/local"]
A --> C["getRegisteredHooks() SDK 注册的 callback Hook"]
A --> D["getSessionHooks() Agent/Skill 前置注册的 session Hook"]
A --> E["getSessionFunctionHooks() 运行时 function Hook"]
```
### 匹配规则
#### 匹配规则
`matcher` 字段支持三种模式(`matchesPattern()` 函数,`src/utils/hooks.ts`):
`matcher` 字段支持三种模式:
```
"Write" → 精确匹配
"Write|Edit" → 管道分隔的多值匹配
"^Bash(git.*)" → 正则匹配
"*" 或 "" → 通配(匹配所有)
"Write" -> 精确匹配
"Write|Edit" -> 管道分隔的多值匹配
"^Bash(git.*)" -> 正则匹配
"*" 或 "" -> 通配(匹配所有)
```
### if 条件过滤
#### if 条件过滤
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()`(`src/utils/hooks.ts`,`prepareIfConditionMatcher` 函数)预编译匹配器:
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()` 预编译匹配器:
```json
{
@@ -181,13 +186,13 @@ Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditio
`if` 条件使用 `permissionRuleValueFromString` 解析,支持与权限规则相同的语法(工具名 + 参数模式)。Bash 工具还会使用 tree-sitter 进行 AST 级别的命令解析。
### Hook 去重
#### Hook 去重
同一个 Hook 命令在不同配置层级(user/project/local)可能重复。系统按四部分复合键做 Map 去重:`${pluginRoot}\0${shell}\0${command}\0${ifCondition}`(由 `hookDedupKey()` 函数构建),保留**最后合并的层级**。
同一个 Hook 命令在不同配置层级(user/project/local)可能重复。系统按四部分复合键做 Map 去重:`${pluginRoot}\0${shell}\0${command}\0${ifCondition}`,保留**最后合并的层级**。
## 工作区信任检查
### 工作区信任检查
**所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()` 函数,`src/utils/hooks.ts`)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
**所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()` 函数)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
```typescript
// 交互模式下,所有 Hook 要求信任
@@ -195,11 +200,12 @@ const hasTrust = checkHasTrustDialogAccepted()
return !hasTrust
```
SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。
> [!tip]
> SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。
## 四种 Hook 能力的源码映射
### 四种 Hook 能力的源码映射
### 1. 拦截操作(PreToolUse)
#### 1. 拦截操作(PreToolUse)
```json
{
@@ -212,7 +218,7 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
`processHookJSONOutput()` 将 `permissionDecision` 映射为 `result.permissionBehavior = 'deny'`,并设置 `blockingError`,阻止工具执行。
### 2. 修改行为(updatedInput / updatedMCPToolOutput)
#### 2. 修改行为(updatedInput / updatedMCPToolOutput)
```json
{
@@ -225,12 +231,12 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
`updatedInput` 替换原始工具输入;`updatedMCPToolOutput`(PostToolUse 事件)替换 MCP 工具的返回值——可用于过滤敏感数据。
### 3. 注入上下文(additionalContext / systemMessage)
#### 3. 注入上下文(additionalContext / systemMessage)
- `additionalContext` → 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息
- `systemMessage` → 注入为系统警告,直接显示给用户
- `additionalContext` -> 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息
- `systemMessage` -> 注入为系统警告,直接显示给用户
### 4. 控制流程(continue / stopReason)
#### 4. 控制流程(continue / stopReason)
```json
{ "continue": false, "stopReason": "构建失败,停止执行" }
@@ -238,9 +244,9 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
`continue: false` 设置 `preventContinuation = true`,阻止 Agent 继续执行后续操作。
## Session Hook 的生命周期
### Session Hook 的生命周期
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`packages/builtin-tools/src/tools/AgentTool/runAgent.ts`;定义位置:`src/utils/hooks/registerFrontmatterHooks.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(定义位置:`src/utils/hooks/sessionHooks.ts`)清理。
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册,绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()` 清理。
```typescript
// runAgent.ts — 注册 agent 的前置 Hook
@@ -251,3 +257,8 @@ clearSessionHooks(rootSetAppState, agentId)
```
这确保 Agent A 的 Hook 不会泄漏到 Agent B 的执行中。
## 关联笔记
- [[claude-code-best/docs/features/extensibility/custom-agents]]
- [[claude-code-best/docs/features/extensibility/skills]]
@@ -1,14 +1,25 @@
---
title: "MCP 配置 - 多来源合并、作用域与策略管控"
description: "详细说明 Claude Code MCP 配置的来源层次、合并优先级、传输类型、企业策略管控、插件集成和保留名称机制。"
keywords: ["MCP", "配置", "settings.json", ".mcp.json", "企业策略", "插件"]
tags:
- MCP
- 配置
- 企业策略
- 插件
create time: 2026-06-09 22:30
---
## 配置来源与作用域
# MCP 配置 - 多来源合并、作用域与策略管控
## 概述
详细说明 Claude Code MCP 配置的来源层次、合并优先级、传输类型、企业策略管控、插件集成和保留名称机制。
## 正文
### 配置来源与作用域
Claude Code 的 MCP 配置来自多个来源,每个来源对应一个 `scope`(作用域)。配置按优先级合并,高优先级来源的同名配置覆盖低优先级。
### 来源列表
#### 来源列表
| 来源 | Scope | 文件/接口 | 说明 |
|------|-------|----------|------|
@@ -21,25 +32,20 @@ Claude Code 的 MCP 配置来自多个来源,每个来源对应一个 `scope`
| 内置动态 | `dynamic` | 代码中注册 | Computer Use / Chrome 等内置服务器 |
| IDE SDK | `sdk` | IDE 传入 | VS Code / JetBrains 嵌入模式 |
### 合并优先级(从低到高)
#### 合并优先级(从低到高)
```
claude.ai 连接器 ← 最低优先级
↓ 去重
插件服务器
↓ 去重
用户全局配置
↓
项目配置(.mcp.json) ← 需要用户审批
↓
本地项目配置
↓
动态配置(内置 MCP) ← 最高优先级
```mermaid
flowchart TD
A["claude.ai 连接器 最低优先级"] -->|"去重"| B["插件服务器"]
B -->|"去重"| C["用户全局配置"]
C --> D["项目配置 .mcp.json 需要用户审批"]
D --> E["本地项目配置"]
E --> F["动态配置 内置 MCP 最高优先级"]
```
`Object.assign({}, dedupedPluginServers, userServers, approvedProjectServers, localServers)` 实现合并——后出现的同名键覆盖前者。
## 企业管控模式
### 企业管控模式
当 `managed-mcp.json` 文件存在时,进入 **排他模式**:
@@ -57,9 +63,9 @@ if (doesEnterpriseMcpConfigExist()) {
- 仍然应用策略过滤(allowlist/denylist)
- 无法通过 CLI 添加新服务器(`addMcpConfig` 会拒绝)
## 传输类型与配置 Schema
### 传输类型与配置 Schema
### stdio(默认)
#### stdio(默认)
启动子进程,通过 stdin/stdout JSON-RPC 通信。
@@ -75,9 +81,10 @@ if (doesEnterpriseMcpConfigExist()) {
`type` 字段可省略(默认为 `stdio`)。环境变量通过 `env` 传递给子进程,会与当前进程环境合并。
**Windows 注意**:使用 `npx` 需要包装为 `cmd /c npx`,否则会报错。
> [!warning]
> **Windows 注意**:使用 `npx` 需要包装为 `cmd /c npx`,否则会报错。
### SSE(Server-Sent Events)
#### SSE(Server-Sent Events)
通过 HTTP SSE 连接远程 MCP 服务器。
@@ -97,7 +104,7 @@ if (doesEnterpriseMcpConfigExist()) {
支持 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,15 分钟 TTL 缓存避免重复提示。
### HTTP(Streamable HTTP)
#### HTTP(Streamable HTTP)
HTTP 流式传输。
@@ -113,7 +120,7 @@ HTTP 流式传输。
支持与 SSE 相同的 OAuth 配置。
### WebSocket
#### WebSocket
```json
{
@@ -124,24 +131,24 @@ HTTP 流式传输。
}
```
### IDE 专用类型(内部)
#### IDE 专用类型(内部)
`sse-ide` 和 `ws-ide` 是 IDE 扩展专用类型,不由用户直接配置。
- `sse-ide`:使用 lockfile token 认证
- `ws-ide`:使用 `X-Claude-Code-Ide-Authorization` header
### SDK 类型(内部)
#### SDK 类型(内部)
`type: "sdk"` 由 IDE 嵌入模式传入,不经过保留名称检查和企业管控排他限制。
### claude.ai 代理类型(内部)
#### claude.ai 代理类型(内部)
`type: "claudeai-proxy"` 由 claude.ai 网页端配置的连接器使用,通过 OAuth bearer token 认证并支持 401 重试。
## 配置操作
### 配置操作
### 添加 MCP 服务器
#### 添加 MCP 服务器
通过 CLI 命令 `claude mcp add` 或 API 调用 `addMcpConfig()`:
@@ -164,19 +171,19 @@ claude mcp add my-remote -s user -t http -u https://mcp.example.com/mcp
4. **Schema 验证**:Zod 校验配置格式
5. **策略检查**:denylist 拒绝、allowlist 验证
### 移除 MCP 服务器
#### 移除 MCP 服务器
```bash
claude mcp remove my-server -s user
```
### 列出 MCP 服务器
#### 列出 MCP 服务器
```bash
claude mcp list
```
## 项目配置审批
### 项目配置审批
`.mcp.json` 中的项目配置需要用户显式审批才能生效:
@@ -192,7 +199,7 @@ for (const [name, config] of Object.entries(projectServers)) {
首次打开项目时,Claude Code 会提示用户审批 `.mcp.json` 中的每个服务器。审批状态持久化在本地配置中。
## 插件 MCP 集成
### 插件 MCP 集成
插件通过 manifest 中的 `.mcp.json` 或 `.mcpb` 文件声明 MCP 服务器:
@@ -204,11 +211,11 @@ const pluginServerResults = await Promise.all(
)
```
### 插件命名空间
#### 插件命名空间
插件 MCP 服务器名格式为 `plugin:<pluginName>:<serverName>`,不会与手动配置的名称冲突。
### 去重机制
#### 去重机制
插件服务器通过内容签名去重(`dedupPluginMcpServers`):
@@ -221,15 +228,15 @@ const pluginServerResults = await Promise.all(
2. 先加载的插件优先于后加载的
3. 被抑制的插件服务器在 `/plugin` UI 中显示提示
### claude.ai 连接器去重
#### claude.ai 连接器去重
claude.ai 连接器使用相同的内容签名机制去重(`dedupClaudeAiMcpServers`):
claude.ai 连接器使用相同的内容签名机制去重:
- 仅启用的手动配置参与去重(禁用的手动配置不应抑制连接器)
- 连接器名格式为 `claude.ai <DisplayName>`
## 策略管控
### 策略管控
### Allowlist / Denylist
#### Allowlist / Denylist
企业策略通过 allowlist 和 denylist 控制可用的 MCP 服务器:
@@ -248,11 +255,11 @@ for (const [name, serverConfig] of Object.entries(configs)) {
- stdio 类型的 command + args 匹配
- URL 类型的 URL 模式匹配(支持通配符)
### 插件专用模式
#### 插件专用模式
`isRestrictedToPluginOnly('mcp')` 启用时,只允许插件提供的 MCP 服务器——用户/项目级配置被忽略。
## 环境变量展开
### 环境变量展开
MCP 配置中的环境变量支持 `$VAR` 和 `${VAR}` 语法展开:
@@ -271,11 +278,11 @@ MCP 配置中的环境变量支持 `$VAR` 和 `${VAR}` 语法展开:
展开时缺失的变量会生成警告信息,但不阻止配置加载。
## 内置 MCP 动态注册
### 内置 MCP 动态注册
内置 MCP 服务器在 `main.tsx` 启动流程中动态注入配置:
### Computer Use MCP
#### Computer Use MCP
```typescript
// src/utils/computerUse/setup.ts
@@ -303,7 +310,7 @@ export function setupComputerUseMCP(): {
- 非非交互式会话
- GrowthBook gate `getChicagoEnabled()` 返回 true
### Claude in Chrome MCP
#### Claude in Chrome MCP
```typescript
// 类似 Computer Use,在 main.tsx 中注册
@@ -315,11 +322,11 @@ dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig }
- `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置
- Chrome 扩展已安装
### VSCode SDK MCP
#### VSCode SDK MCP
IDE 嵌入模式通过初始化消息传入 `type:'sdk'` 的配置,由 `setupVscodeSdkMcp()` 设置双向通知。
## 保留名称
### 保留名称
以下 MCP 服务器名称被保留,用户无法手动配置同名服务器:
@@ -333,7 +340,7 @@ IDE 嵌入模式通过初始化消息传入 `type:'sdk'` 的配置,由 `setupV
1. `addMcpConfig()`(`config.ts:636-648`)— 运行时拒绝
2. `main.tsx` 启动检查(`main.tsx:2351-2368`)— 启动时退出
## 关键源文件索引
### 关键源文件索引
| 文件 | 职责 |
|------|------|
@@ -344,3 +351,7 @@ IDE 嵌入模式通过初始化消息传入 `type:'sdk'` 的配置,由 `setupV
| `src/utils/computerUse/setup.ts` | Computer Use 动态注册 |
| `src/utils/claudeInChrome/common.ts` | Chrome MCP 保留名与工具名 |
| `src/services/mcp/vscodeSdkMcp.ts` | VSCode SDK 双向通知 |
## 关联笔记
- [[claude-code-best/docs/features/extensibility/mcp-protocol]]
@@ -1,57 +1,58 @@
---
title: "MCP 协议 - 连接管理、工具发现与执行链路"
description: "从源码角度解析 Claude Code 的 MCP 集成:内置 MCP 与外部 MCP 的区别、7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现", "内置 MCP", "外部 MCP"]
tags:
- MCP
- 工具扩展
- MCP客户端
- 工具发现
- 内置MCP
- 外部MCP
create time: 2026-06-09 22:30
---
{/* 本章目标:从源码角度揭示 MCP 客户端的两种运行模式(内置/外部)、连接管理、工具发现协议和执行链路 */}
# MCP 协议 - 连接管理、工具发现与执行链路
## 架构总览:从配置到可用工具
## 概述
```
配置层(多来源合并)
├── settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } } ← 外部
├── .mcp.json: 项目级 MCP 配置 ← 外部
├── 插件 manifest (.mcp.json / .mcpb) ← 外部(插件)
├── claude.ai connectors ← 外部(远程)
├── enterprise managed-mcp.json ← 外部(企业管控)
├── setupComputerUseMCP() / setupClaudeInChrome() ← 内置(动态注册)
└── SDK 传入 (type:'sdk') ← 内置(IDE 嵌入)
↓
getAllMcpConfigs() ← enterprise 独占 或 合并 user/project/local + plugin + claude.ai
↓
useManageMCPConnections() ← React Hook 管理连接生命周期
↓
connectToServer(name, config) ← memoize 缓存(lodash memoize)
├── 判断:内置 MCP → InProcessTransport(同进程)
├── 判断:外部 stdio → StdioClientTransport(子进程)
├── 判断:远程 SSE/HTTP/WS → 网络传输
└── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending | disabled }
↓
fetchToolsForClient(client) ← LRU(20) 缓存
├── client.request({ method: 'tools/list' })
└── 每个工具包装为 MCPTool ← 统一 Tool 接口
↓
assembleToolPool() ← 合并内置工具 + MCP 工具
↓
工具名格式: mcp__<serverName>__<toolName> ← buildMcpToolName()
从源码角度解析 Claude Code 的 MCP 集成:内置 MCP 与外部 MCP 的区别、7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。
## 正文
### 架构总览:从配置到可用工具
```mermaid
flowchart TD
A["配置层 多来源合并"] --> B["getAllMcpConfigs()"]
B --> C["useManageMCPConnections() React Hook 管理连接生命周期"]
C --> D["connectToServer(name, config) memoize 缓存"]
D --> E{"判断类型"}
E -->|"内置 MCP"| F["InProcessTransport 同进程"]
E -->|"外部 stdio"| G["StdioClientTransport 子进程"]
E -->|"远程 SSE/HTTP/WS"| H["网络传输"]
F --> I["MCPServerConnection connected/failed/needs-auth/pending/disabled"]
G --> I
H --> I
I --> J["fetchToolsForClient(client) LRU20 缓存"]
J --> K["client.request({ method: 'tools/list' })"]
K --> L["每个工具包装为 MCPTool 统一 Tool 接口"]
L --> M["assembleToolPool() 合并内置工具 + MCP 工具"]
M --> N["工具名格式: mcp__serverName__toolName"]
```
## 两种 MCP 模式:内置 vs 外部
### 两种 MCP 模式:内置 vs 外部
Claude Code 的 MCP 实现区分 **内置 MCP 服务器** 和 **外部 MCP 服务器**。两者使用相同的客户端协议和工具发现机制,但在连接方式、生命周期管理和配置来源上完全不同。
### 内置 MCP 服务器
#### 内置 MCP 服务器
内置 MCP 服务器由 Claude Code 自身提供,无需用户手动配置。它们在启动时自动注册为 `dynamic` scope 的配置,并在同进程内运行。
| 服务器 | 名称 | 包路径 | Feature Flag | 启用方式 |
|--------|------|--------|-------------|---------|
| Computer Use | `computer-use` | `@ant/computer-use-mcp` | `CHICAGO_MCP` | GrowthBook gate + macOS + interactive |
| Claude in Chrome | `claude-in-chrome` | `@ant/claude-for-chrome-mcp` | — | `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置 |
| Claude in Chrome | `claude-in-chrome` | `@ant/claude-for-chrome-mcp` | — | `--chrome` 参数或配置 |
| VSCode SDK | `claude-vscode` | — | — | IDE 嵌入模式 (type:`sdk`) |
#### InProcessTransport:零开销同进程通信
##### InProcessTransport:零开销同进程通信
内置服务器通过 `InProcessTransport`(`src/services/mcp/InProcessTransport.ts`)运行,**不启动子进程**:
@@ -72,7 +73,7 @@ transport = clientTransport
- `close()` 双向关闭,任一端关闭都会触发两端的 `onclose` 回调
- 无网络开销、无 IPC 序列化、无进程启动时间
#### 动态注册流程
##### 动态注册流程
内置服务器在 `main.tsx` 的启动流程中注册,注入 `dynamicMcpConfig`:
@@ -89,20 +90,7 @@ if (feature("CHICAGO_MCP") && getPlatform() !== "unknown" && !getIsNonInteractiv
}
```
`setupComputerUseMCP()` 返回的配置(`src/utils/computerUse/setup.ts`):
```typescript
{
"computer-use": {
type: "stdio", // 类型标记为 stdio(但 client.ts 会拦截为 InProcessTransport)
command: process.execPath,
args: ["--computer-use-mcp"],
scope: "dynamic", // 动态作用域,不持久化
}
}
```
#### 连接时拦截
##### 连接时拦截
`connectToServer()` 在 `client.ts:906-944` 中根据服务器名拦截内置服务器:
@@ -130,9 +118,9 @@ if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
}
```
#### 保留名称保护
##### 保留名称保护
内置服务器的名称被保留,用户无法手动添加同名配置(`config.ts:636-648`):
内置服务器的名称被保留,用户无法手动添加同名配置:
```typescript
// 添加 MCP 配置时检查保留名
@@ -146,7 +134,7 @@ if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
启动时也有全局检查(`main.tsx:2351-2368`):如果用户配置中包含保留名(非 `type:'sdk'`),直接 `process.exit(1)`。
#### VSCode SDK MCP
##### VSCode SDK MCP
VSCode SDK MCP 是特殊的内置模式。IDE(如 VS Code、JetBrains)通过嵌入方式启动 Claude Code,并传入 `type:'sdk'` 的 MCP 配置。这类配置:
- 不经过保留名称检查(IDE 可以使用任意名称)
@@ -167,11 +155,11 @@ export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
}
```
### 外部 MCP 服务器
#### 外部 MCP 服务器
外部 MCP 服务器由用户在配置文件中声明,通过子进程或网络连接运行。
#### 配置来源
##### 配置来源
| 来源 | Scope | 文件位置 | 优先级 |
|------|-------|---------|--------|
@@ -182,10 +170,9 @@ export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
| claude.ai | `claudeai` | 通过 API 获取 | 低 |
| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | 排他(存在时覆盖全部) |
#### 配置示例
##### 配置示例
```json
// settings.json / .mcp.json 中的 MCP 配置
{
"mcpServers": {
// stdio 类型 — 启动子进程
@@ -216,16 +203,16 @@ export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
}
```
#### 配置合并与去重
##### 配置合并与去重
`getAllMcpConfigs()`(`config.ts`)按优先级合并多个来源的配置:
1. 企业管控配置存在时,**独占返回**(忽略所有其他来源)
2. 否则合并:user → project → local → plugin → claude.ai
3. 插件与手动配置去重:通过 `getMcpServerSignature()` 生成内容签名(基于 command/args/url),插件配置被同名手动配置抑制
2. 否则合并:user -> project -> local -> plugin -> claude.ai
3. 插件与手动配置去重:通过 `getMcpServerSignature()` 生成内容签名,插件配置被同名手动配置抑制
4. `addScopeToServers()` 为每个配置项标注来源 scope
## 7 种传输层实现
### 7 种传输层实现
`connectToServer()`(`client.ts:596-1643`)根据 `config.type` 分发到不同的 Transport 实现:
@@ -240,36 +227,36 @@ export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
| `claudeai-proxy` | `StreamableHTTPClientTransport` | claude.ai 代理 | OAuth bearer + 401 重试 |
| InProcess(内置) | `InProcessTransport` | Computer Use / Chrome | 无(同进程) |
### stdio 传输的进程管理
#### stdio 传输的进程管理
stdio 类型的 MCP 服务器作为子进程运行,cleanup 时采用 **信号升级策略**(`client.ts:1431-1564`):
stdio 类型的 MCP 服务器作为子进程运行,cleanup 时采用 **信号升级策略**:
```
SIGINT (100ms) → SIGTERM (400ms) → SIGKILL
SIGINT (100ms) -> SIGTERM (400ms) -> SIGKILL
```
总清理时间上限 600ms,防止 MCP 服务器关闭阻塞 CLI 退出。
### 远程传输的认证状态机
#### 远程传输的认证状态机
SSE/HTTP 类型使用 `ClaudeAuthProvider` 实现 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,并写入 15 分钟 TTL 的缓存文件(`mcp-needs-auth-cache.json`),避免重复弹出认证提示。
SSE/HTTP 类型使用 `ClaudeAuthProvider` 实现 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,并写入 15 分钟 TTL 的缓存文件,避免重复弹出认证提示。
```
连接尝试 → 401 Unauthorized
↓
handleRemoteAuthFailure()
├── logEvent('tengu_mcp_server_needs_auth')
├── setMcpAuthCacheEntry(name) ← 写入 15min TTL 缓存
└── return { type: 'needs-auth' } ← UI 显示认证提示
```mermaid
flowchart TD
A["连接尝试"] --> B["401 Unauthorized"]
B --> C["handleRemoteAuthFailure()"]
C --> D["logEvent('tengu_mcp_server_needs_auth')"]
C --> E["setMcpAuthCacheEntry(name) 写入 15min TTL 缓存"]
C --> F["return { type: 'needs-auth' } UI 显示认证提示"]
```
## 连接缓存与重连机制
### 连接缓存与重连机制
`connectToServer` 使用 lodash `memoize` 缓存连接对象,缓存 key 为 `${name}-${JSON.stringify(config)}`。
### 缓存失效触发
#### 缓存失效触发
当连接关闭时(`client.onclose`),清除所有相关缓存(`client.ts:1376-1404`):
当连接关闭时(`client.onclose`),清除所有相关缓存:
```typescript
client.onclose = () => {
@@ -281,9 +268,9 @@ client.onclose = () => {
}
```
### 连接降级检测
#### 连接降级检测
远程传输有 **连续错误计数器**(`client.ts:1229`):
远程传输有 **连续错误计数器**:
```typescript
let consecutiveConnectionErrors = 0
@@ -292,9 +279,9 @@ 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 才回收。
每个 HTTP 请求使用独立的 `setTimeout` 超时,而非共享 `AbortSignal.timeout()`。原因是 Bun 对 AbortSignal.timeout 的 GC 是惰性的——每个请求约 2.4KB 原生内存,即使请求毫秒级完成也要等 60s 才回收。
```typescript
const controller = new AbortController()
@@ -302,7 +289,7 @@ const timer = setTimeout(c => c.abort(...), MCP_REQUEST_TIMEOUT_MS, controller)
timer.unref?.() // 不阻止进程退出
```
## 工具发现:从 MCP 到 Tool 接口
### 工具发现:从 MCP 到 Tool 接口
`fetchToolsForClient()`(`client.ts:1744-2000`)使用 `memoizeWithLRU` 缓存(上限 100),将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
@@ -311,19 +298,19 @@ const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
// 结果: "mcp__my-database__query"
```
### 内置 MCP 的工具发现
#### 内置 MCP 的工具发现
内置 MCP 服务器虽然使用 InProcessTransport,但工具发现流程与外部服务器完全一致:
- **Computer Use**:`createComputerUseMcpServerForCli()` 在 `src/utils/computerUse/mcpServer.ts` 中构建 MCP Server 对象,注册 `ListToolsRequestSchema` handler。工具描述包含平台特定的已安装应用列表(1s 超时枚举)。
- **Claude in Chrome**:`createClaudeForChromeMcpServer()` 在 `@ant/claude-for-chrome-mcp` 包中构建 Server,提供 17+ 个浏览器控制工具。
- **Computer Use**:`createComputerUseMcpServerForCli()` 构建 MCP Server 对象,注册 `ListToolsRequestSchema` handler。工具描述包含平台特定的已安装应用列表(1s 超时枚举)。
- **Claude in Chrome**:`createClaudeForChromeMcpServer()` 构建 Server,提供 17+ 个浏览器控制工具。
- **VSCode SDK**:由 IDE 端提供工具列表,通过 SDK transport 传递。
### 工具描述截断
#### 工具描述截断
MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`)。OpenAPI 生成的 MCP 服务器曾观察到 15-60KB 的描述文档。
### 工具能力标注
#### 工具能力标注
每个 MCP 工具根据 `tool.annotations` 自动标注:
@@ -334,36 +321,36 @@ MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`)。OpenAPI
| `openWorldHint` | `isOpenWorld()` | 开放世界(不可枚举) |
| `title` | `userFacingName()` | 显示名称 |
### MCP 工具的权限检查
#### MCP 工具的权限检查
MCP 工具默认返回 `{ behavior: 'passthrough' }`(`client.ts:1816-1834`),意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。
MCP 工具默认返回 `{ behavior: 'passthrough' }`,意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。
内置 MCP 服务器的工具通过 `allowedTools` 列表自动授权——在 `main.tsx` 启动时加入,绕过普通权限提示。例如 Computer Use 工具的 `request_access` 自行处理会话级审批。
## 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 }
```mermaid
flowchart TD
A["AI 生成 tool_use: { name: 'mcp__my-db__query', input: { sql: '...' } }"] --> B["MCPTool.call()"]
B --> C["ensureConnectedClient() 确保连接有效 重连"]
C --> D["callMCPToolWithUrlElicitationRetry() 带 Elicitation 重试"]
D --> E["client.request({ method: 'tools/call' })"]
D --> F["处理图片结果 resize + persist"]
D --> G["内容截断 mcpContentNeedsTruncation"]
E --> H{"McpSessionExpiredError?"}
H -->|"是"| I["重试一次"]
H -->|"否"| J["返回 { data: content, mcpMeta }"]
```
### Session 过期自动重试
#### Session 过期自动重试
HTTP 传输的 MCP session 可能过期。检测到 `McpSessionExpiredError` 后自动重试一次(`client.ts:1862`),因为 `ensureConnectedClient()` 已经清除了缓存并建立了新连接。
HTTP 传输的 MCP session 可能过期。检测到 `McpSessionExpiredError` 后自动重试一次,因为 `ensureConnectedClient()` 已经清除了缓存并建立了新连接。
### 内容截断与持久化
#### 内容截断与持久化
大型 MCP 工具输出通过 `truncateMcpContentIfNeeded` 截断,二进制内容(图片)通过 `persistBinaryContent` 写入文件并返回文件路径。图片自动 resize(`maybeResizeAndDownsampleImageBuffer`)。
大型 MCP 工具输出通过 `truncateMcpContentIfNeeded` 截断,二进制内容(图片)通过 `persistBinaryContent` 写入文件并返回文件路径。图片自动 resize。
## MCP 连接的并发控制
### MCP 连接的并发控制
```typescript
// 本地服务器并发连接数
@@ -375,12 +362,12 @@ getRemoteMcpServerConnectionBatchSize() // 默认 20
本地 MCP 服务器(stdio)是重量级的子进程,默认限制 3 个并发连接。远程服务器是轻量级 HTTP 请求,允许 20 个并发。
## 内置 vs 外部 MCP 对比总结
### 内置 vs 外部 MCP 对比总结
| 维度 | 内置 MCP | 外部 MCP |
|------|---------|---------|
| **Transport** | `InProcessTransport`(同进程) | stdio / SSE / HTTP / WebSocket |
| **配置来源** | `setupComputerUseMCP()` / `setupClaudeInChrome()` 等动态注册 | settings.json / .mcp.json / 插件 / claude.ai |
| **配置来源** | `setupComputerUseMCP()` 等动态注册 | settings.json / .mcp.json / 插件 / claude.ai |
| **Scope** | `dynamic` | `user` / `project` / `local` / `enterprise` / `claudeai` |
| **进程模型** | 同进程,零开销 | 子进程(stdio)或网络连接 |
| **名称保护** | 保留名,用户不可添加同名 | 自由命名(字母数字 + `-_`) |
@@ -388,9 +375,9 @@ getRemoteMcpServerConnectionBatchSize() // 默认 20
| **权限** | `allowedTools` 自动授权 | `passthrough` 进入权限确认 |
| **Feature Flag** | `CHICAGO_MCP`(Computer Use)等 | 无(始终可用) |
| **工具发现** | 与外部相同(MCP 协议) | 标准 MCP `tools/list` |
| **清理** | `inProcessServer.close()` | 信号升级策略 SIGINT→SIGTERM→SIGKILL |
| **清理** | `inProcessServer.close()` | 信号升级策略 SIGINT->SIGTERM->SIGKILL |
## 关键源文件索引
### 关键源文件索引
| 文件 | 职责 |
|------|------|
@@ -405,3 +392,7 @@ getRemoteMcpServerConnectionBatchSize() // 默认 20
| `src/utils/claudeInChrome/mcpServer.ts` | Chrome MCP Server 构建 + Bridge 配置 |
| `src/tools/MCPTool/MCPTool.ts` | MCP 工具包装:统一 Tool 接口 |
| `src/entrypoints/mcp.ts` | MCP server 入口(Claude Code 作为 MCP server) |
## 关联笔记
- [[claude-code-best/docs/features/extensibility/mcp-configuration]]
@@ -1,38 +1,44 @@
---
title: "Skills 技能系统 - Prompt 即能力的架构哲学"
description: "深入剖析 Claude Code Skills 系统的完整实现:从磁盘加载、Frontmatter 解析、预算感知描述截断、双模式执行(inline/fork)、权限白名单、条件激活、动态发现到远程技能加载,揭示一条完整的 Skill 生命周期链路。"
keywords: ["Skills", "SkillTool", "技能加载", "Frontmatter", "whenToUse", "allowedTools", "fork执行", "动态发现"]
tags: [Skills, SkillTool, 技能加载, Frontmatter, whenToUse, allowedTools, fork执行, 动态发现]
create time: 2026-06-09 22:30
---
{/* 本章目标:揭示 Skill 系统从文件到执行的全链路实现 */}
# Skills 技能系统 - Prompt 即能力的架构哲学
## Tool vs Skill:本质差异
## 概述
深入剖析 Claude Code Skills 系统的完整实现:从磁盘加载、Frontmatter 解析、预算感知描述截断、双模式执行(inline/fork)、权限白名单、条件激活、动态发现到远程技能加载,揭示一条完整的 Skill 生命周期链路。
## 正文
### Tool vs Skill:本质差异
| | Tool | Skill |
|---|---|---|
| 粒度 | 单个原子操作(读文件、执行命令) | 一套完整的工作流(代码审查、创建 PR) |
| 触发方式 | AI 自主选择 | 用户 `/skill-name` 或 AI 通过 `SkillTool` 自动匹配 |
| 本质 | TypeScript 执行逻辑 | **Prompt + 权限配置**的声明式封装 |
| 注册位置 | `src/tools.ts` → `getTools()` | `src/commands.ts` → `getCommands()` |
| 执行器 | 各 Tool 的 `call()` 方法 | `SkillTool.call()` → 两条分支(inline / fork) |
| 注册位置 | `src/tools.ts` -> `getTools()` | `src/commands.ts` -> `getCommands()` |
| 执行器 | 各 Tool 的 `call()` 方法 | `SkillTool.call()` -> 两条分支(inline / fork) |
Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Prompt 质量**。一个代码审查 Skill 不需要审查引擎,只需告诉 AI "审查什么、按什么顺序、输出什么格式"——Skill 把这种"经验"封装为可复用的 Markdown。
> [!tip]
> Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Prompt 质量**。一个代码审查 Skill 不需要审查引擎,只需告诉 AI "审查什么、按什么顺序、输出什么格式"——Skill 把这种"经验"封装为可复用的 Markdown。
## Skill 的五个来源与加载链路
### Skill 的五个来源与加载链路
### 1. 内置命令(Built-in Commands)
#### 1. 内置命令(Built-in Commands)
硬编码在 `src/commands.ts:299` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口(`src/types/command.ts`)。
硬编码在 `src/commands.ts:299` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口。
### 2. Bundled Skills(编译时打包)
#### 2. Bundled Skills(编译时打包)
通过 `registerBundledSkill()`(`src/skills/bundledSkills.ts:53`)在模块初始化时注册。关键特性:
- **延迟文件提取**:如果 Skill 声明了 `files`(参考文件),首次调用时才解压到临时目录(`getBundledSkillExtractDir()`),使用 `O_NOFOLLOW | O_EXCL` 防止符号链接攻击(`safeWriteFile`,第 186 行)
- **延迟文件提取**:如果 Skill 声明了 `files`(参考文件),首次调用时才解压到临时目录,使用 `O_NOFOLLOW | O_EXCL` 防止符号链接攻击
- **闭包级 memoize**:并发调用共享同一个 extraction promise,避免竞态写入
- 来源标记为 `source: 'bundled'`,在 Prompt 预算中享有**不可截断**的特权
### 3. 磁盘 Skills(`.claude/skills/`)
#### 3. 磁盘 Skills(`.claude/skills/`)
由 `loadSkillsFromSkillsDir()`(`src/skills/loadSkillsDir.ts:407`)加载,这是最重要的加载路径:
@@ -45,27 +51,28 @@ Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Promp
**加载协议**:只识别 `skill-name/SKILL.md` 目录格式,不再支持单文件 `.md`。加载流程:
1. `readdir` 扫描目录 → 仅保留 `isDirectory()` 或 `isSymbolicLink()` 的条目
1. `readdir` 扫描目录 -> 仅保留 `isDirectory()` 或 `isSymbolicLink()` 的条目
2. 在每个子目录中查找 `SKILL.md`,未找到则跳过
3. `parseFrontmatter()` 解析 YAML 头部,提取 `whenToUse`、`allowedTools`、`context` 等字段
4. `parseSkillFrontmatterFields()`(第 185 行)统一解析 16 个 frontmatter 字段
5. `createSkillCommand()`(第 270 行)构造 `Command` 对象
**去重机制**:使用 `realpath()` 解析符号链接获得规范路径(`getFileIdentity`,第 118 行),避免通过符号链接或重叠父目录导致的重复加载。
**去重机制**:使用 `realpath()` 解析符号链接获得规范路径,避免通过符号链接或重叠父目录导致的重复加载。
### 4. MCP Skills(动态发现)
#### 4. MCP Skills(动态发现)
通过 `registerMCPSkillBuilders()` 注册构建器,MCP Server 的 prompt 被 `mcpSkillBuilders.ts` 转换为 `Command` 对象。标记为 `loadedFrom: 'mcp'`。
**安全边界**:MCP Skills 的 Prompt 内容**禁止执行内联 shell 命令**(`loadSkillsDir.ts:374` 的 `loadedFrom !== 'mcp'` 守卫),因为远程内容不可信。
> [!warning]
> **安全边界**:MCP Skills 的 Prompt 内容**禁止执行内联 shell 命令**(`loadSkillsDir.ts:374` 的 `loadedFrom !== 'mcp'` 守卫),因为远程内容不可信。
### 5. Legacy Commands(`/commands/` 目录)
#### 5. Legacy Commands(`/commands/` 目录)
向后兼容的旧格式,由 `loadSkillsFromCommandsDir()`(第 566 行)加载。同时支持 `SKILL.md` 目录格式和单 `.md` 文件格式。
## Frontmatter 字段全景
### Frontmatter 字段全景
一个 `SKILL.md` 的完整 frontmatter(`parseSkillFrontmatterFields`,第 185 行):
一个 `SKILL.md` 的完整 frontmatter:
```yaml
---
@@ -96,91 +103,87 @@ shell: ["bash"] # Shell 执行环境
解析后有 16 个字段被提取,其中 `allowedTools`、`model`、`effort` 在执行时动态修改 `toolPermissionContext`。
## 两条执行路径:Inline vs Fork
### 两条执行路径:Inline vs Fork
SkillTool(`packages/builtin-tools/src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
### Inline 模式(默认)
#### Inline 模式(默认)
Skill 的 Prompt 内容被注入为 **UserMessage**,在主对话流中继续执行:
1. `processPromptSlashCommand()` 处理参数替换(`$ARGUMENTS`)和 shell 命令展开(`` !`...` ``)
1. `processPromptSlashCommand()` 处理参数替换(`$ARGUMENTS`)和 shell 命令展开
2. `${CLAUDE_SKILL_DIR}` 被替换为 Skill 所在目录的绝对路径
3. `${CLAUDE_SESSION_ID}` 被替换为当前会话 ID
4. 返回 `newMessages`(注入到对话流)+ `contextModifier`(修改权限上下文)
`contextModifier`(第 776 行)做了三件事:
`contextModifier` 做了三件事:
- **工具白名单注入**:将 `allowedTools` 合并到 `alwaysAllowRules.command`
- **模型切换**:`resolveSkillModelOverride()` 处理模型覆盖,保留 `[1m]` 后缀以避免 200K 窗口截断
- **努力级别覆盖**:修改 `effortValue`
### Fork 模式(`context: fork`)
#### Fork 模式(`context: fork`)
Skill 在**独立子 Agent** 中执行(`executeForkedSkill`,第 122 行):
Skill 在**独立子 Agent** 中执行(`executeForkedSkill`):
1. `prepareForkedCommandContext()` 构建隔离的 Agent 定义和 Prompt
2. `runAgent()` 启动子 Agent 循环,拥有独立的 token 预算
3. 通过 `onProgress` 回调报告工具使用进度
4. 结果通过 `extractResultText()` 提取,子 Agent 的全部消息在提取后被释放(`agentMessages.length = 0`)
4. 结果通过 `extractResultText()` 提取,子 Agent 的全部消息在提取后被释放
5. 最终通过 `clearInvokedSkillsForAgent()` 清理状态
Fork 模式适用于需要强隔离的场景(如长时间运行的审查任务),避免污染主对话的上下文。
## 权限模型:Safe Properties 白名单
### 权限模型:Safe Properties 白名单
`checkPermissions()`(第 433 行)实现了一个五层权限检查:
`checkPermissions()` 实现了一个五层权限检查:
```
1. Deny 规则匹配(支持精确匹配和 prefix:* 通配符)
↓ 未命中
2. 远程 canonical Skill 自动放行(EXPERIMENTAL_SKILL_SEARCH + USER_TYPE === 'ant')
↓ 未命中
3. Allow 规则匹配
↓ 未命中
4. Safe Properties 白名单检查(skillHasOnlySafeProperties,第 911 行)
↓ 有非安全属性
5. Ask 用户确认(附带精确匹配和前缀匹配两条建议规则)
```mermaid
flowchart TD
A["1. Deny 规则匹配 精确匹配和 prefix:* 通配符"] -->|"未命中"| B["2. 远程 canonical Skill 自动放行 EXPERIMENTAL_SKILL_SEARCH + USER_TYPE === 'ant'"]
B -->|"未命中"| C["3. Allow 规则匹配"]
C -->|"未命中"| D["4. Safe Properties 白名单检查 skillHasOnlySafeProperties"]
D -->|"有非安全属性"| E["5. Ask 用户确认 附带精确匹配和前缀匹配两条建议规则"]
```
**Safe Properties**(`SAFE_SKILL_PROPERTIES`,第 876 行)是一个包含 30 个属性名的白名单(覆盖 `PromptCommand` 和 `CommandBase` 两个类型的所有安全属性)。任何不在白名单中的**有意义的属性值**(排除 `undefined`、`null`、空数组、空对象)都会触发权限请求。这是**正向安全**设计——未来新增的属性默认需要权限。
**Safe Properties** 是一个包含 30 个属性名的白名单(覆盖 `PromptCommand` 和 `CommandBase` 两个类型的所有安全属性)。任何不在白名单中的**有意义的属性值**都会触发权限请求。这是**正向安全**设计——未来新增的属性默认需要权限。
## Prompt 预算:1% 上下文窗口的截断策略
### Prompt 预算:1% 上下文窗口的截断策略
Skill 列表注入 System Prompt 时有严格的字符预算(`prompt.ts`):
Skill 列表注入 System Prompt 时有严格的字符预算:
- **预算计算**:`contextWindowTokens × 4 chars/token × 1%`(约 8000 字符)
- **单条上限**:`MAX_LISTING_DESC_CHARS = 250` 字符(超出截断为 `…`)
- **预算计算**:`contextWindowTokens x 4 chars/token x 1%`(约 8000 字符)
- **单条上限**:`MAX_LISTING_DESC_CHARS = 250` 字符(超出截断为 `...`)
- **Bundled Skills 不可截断**:它们始终保留完整描述,预算不足时只截断非 bundled 的
- **降级策略**:
1. 尝试完整描述 → 超预算?
2. Bundled 保留完整,非 bundled 均分剩余预算 → 每条描述低于 20 字符?
1. 尝试完整描述 -> 超预算?
2. Bundled 保留完整,非 bundled 均分剩余预算 -> 每条描述低于 20 字符?
3. 非 bundled 仅保留名称
`formatCommandsWithinBudget()`(`prompt.ts:70`)实现了这个三级降级。
`formatCommandsWithinBudget()` 实现了这个三级降级。
## 动态发现与条件激活
### 动态发现与条件激活
### 基于文件路径的动态发现
#### 基于文件路径的动态发现
`discoverSkillDirsForPaths()`(`loadSkillsDir.ts:861`)在文件操作时触发:
`discoverSkillDirsForPaths()` 在文件操作时触发:
1. 从被操作的文件路径开始,**向上遍历**至 CWD(不包含 CWD 本身)
2. 在每层查找 `.claude/skills/` 目录
3. 使用 `realpath` 去重,`git check-ignore` 过滤 gitignored 目录
4. 按路径深度排序(**深层优先**),更接近文件的 Skill 优先级更高
### 条件激活(paths frontmatter)
#### 条件激活(paths frontmatter)
带有 `paths` 模式的 Skill 在加载时不会立即可用,而是存入 `conditionalSkills` Map。当被操作的文件路径匹配某个 Skill 的 paths 模式时(使用 `ignore` 库做 gitignore 风格匹配),该 Skill 才被**激活**——从 `conditionalSkills` 移入 `dynamicSkills`。
带有 `paths` 模式的 Skill 在加载时不会立即可用,而是存入 `conditionalSkills` Map。当被操作的文件路径匹配某个 Skill 的 paths 模式时,该 Skill 才被**激活**——从 `conditionalSkills` 移入 `dynamicSkills`。
这意味着一个只在 `*.test.ts` 上激活的测试 Skill,平时完全不可见,只有当 AI 读取或编辑测试文件时才会出现。
## 使用频率排名
### 使用频率排名
`recordSkillUsage()`(`skillUsageTracking.ts`)使用指数衰减算法计算 Skill 排名分数:
`recordSkillUsage()` 使用指数衰减算法计算 Skill 排名分数:
```
score = usageCount × max(0.5^(daysSinceUse / 7), 0.1)
score = usageCount x max(0.5^(daysSinceUse / 7), 0.1)
```
- **7 天半衰期**:一周前的使用权重减半
@@ -189,33 +192,42 @@ score = usageCount × max(0.5^(daysSinceUse / 7), 0.1)
排名数据持久化在全局配置的 `skillUsage` 字段中。
## 远程技能加载(Experimental)
### 远程技能加载(Experimental)
通过 `EXPERIMENTAL_SKILL_SEARCH` feature flag 控制,支持从远程(AKI/GCS/S3)加载 `_canonical_<slug>` 格式的 Skill:
1. `validateInput()` 中 `stripCanonicalPrefix()` 拦截 canonical 名称
2. `executeRemoteSkill()`(第 970 行)从远程 URL 加载 SKILL.md
2. `executeRemoteSkill()` 从远程 URL 加载 SKILL.md
3. 支持 `gs://`、`https://`、`s3://` 等 URL 协议
4. 内容经过 frontmatter 剥离、`${CLAUDE_SKILL_DIR}` 替换后直接注入
5. 通过 `addInvokedSkill()` 注册到 compaction 保留状态,确保压缩后仍可恢复
6. 远程 Skill 不经过 `processPromptSlashCommand`——无 `!command` 替换、无 `$ARGUMENTS` 展开
## 完整生命周期总结
### 完整生命周期总结
```mermaid
flowchart TD
A["磁盘 SKILL.md"] --> B["parseFrontmatter()"]
B --> C["parseSkillFrontmatterFields() 16 个字段"]
C --> D["createSkillCommand() Command 对象"]
D --> E["去重 realpath + seenFileIds"]
E --> F{"条件 Skill?"}
F -->|"是"| G["conditionalSkills Map 等待路径匹配激活"]
F -->|"否"| H["getSkillDirCommands() memoize 缓存"]
G --> H
H --> I["getAllCommands() 合并 local + MCP"]
I --> J["formatCommandsWithinBudget() 截断后的 Skill 列表注入 System Prompt"]
J --> K["AI 选择匹配的 Skill"]
K --> L["SkillTool.validateInput() 名称校验 + 存在性检查"]
L --> M["SkillTool.checkPermissions() 五层权限检查"]
M --> N["SkillTool.call() inline 或 fork 执行"]
N --> O["contextModifier() 注入 allowedTools + model + effort"]
O --> P["recordSkillUsage() 更新使用频率排名"]
```
磁盘 SKILL.md
↓ parseFrontmatter()
↓ parseSkillFrontmatterFields() → 16 个字段
↓ createSkillCommand() → Command 对象
↓ 去重(realpath + seenFileIds)
↓ 条件 Skill → conditionalSkills Map(等待路径匹配激活)
↓ getSkillDirCommands() memoize 缓存
↓ getAllCommands() 合并 local + MCP
↓ formatCommandsWithinBudget() → 截断后的 Skill 列表注入 System Prompt
↓ AI 选择匹配的 Skill
↓ SkillTool.validateInput() → 名称校验 + 存在性检查
↓ SkillTool.checkPermissions() → 五层权限检查
↓ SkillTool.call() → inline 或 fork 执行
↓ contextModifier() → 注入 allowedTools + model + effort
↓ recordSkillUsage() → 更新使用频率排名
```
## 关联笔记
- [[claude-code-best/docs/features/extensibility/custom-agents]]
- [[claude-code-best/docs/features/extensibility/hooks]]
- [[claude-code-best/docs/features/mcp-skills]]
- [[claude-code-best/docs/features/experimental-skill-search]]