工具系统总览

📑 目录

在前面的文章中,我们已经深入了解了 Claude Code 的查询引擎(Query Engine)、状态管理(AppState)和权限系统(Permission System)。现在,让我们把视线投向整个系统中最具"可扩展性"的模块——工具系统(Tool System)。

Claude Code 的底层是一个强大的 Agent 运行时,而它之所以能与外部世界交互——读取文件、执行命令、搜索网络、调用 MCP 服务——全靠一套精心设计的工具协议。截至当前版本,这套系统已经承载了超过 40 个内置工具,并且支持通过 MCP(Model Context Protocol) 动态注入外部工具。

本文将从 Tool.ts 的抽象基类协议出发,遍历 tools.ts 的注册表组装逻辑,拆解 Schema 系统的设计哲学,最终呈现一幅完整的工具系统全景图。

一、Tool.ts:一切工具的元协议

src/Tool.ts 是整个工具系统的"宪法"。它并不直接实现任何具体功能,而是定义了所有工具必须遵守的契约——从输入 Schema 到执行语义,从权限声明到结果渲染。

1.1 Tool 类型的核心结构

Tool.ts 中,Tool 被定义为一个 TypeScript 泛型接口(第 340-540 行附近)。它不是一个传统的面向对象抽象类,而是一个结构类型(Structural Typing)的接口,这意味着任何对象只要满足其形状,就可以被视为一个 Tool。

// src/Tool.ts (第 340 行起,精简版)
export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
  P extends ToolProgressData = ToolProgressData,
> = {
  name: string
  aliases?: string[]
  searchHint?: string
  readonly inputSchema: Input
  readonly inputJSONSchema?: ToolInputJSONSchema
  outputSchema?: z.ZodType<unknown>
  call(args: z.infer<Input>, context: ToolUseContext, ...): Promise<ToolResult<Output>>
  description(input, options): Promise<string>
  prompt(options): Promise<string>
  checkPermissions(input, context): Promise<PermissionResult>
  isConcurrencySafe(input): boolean
  isReadOnly(input): boolean
  isDestructive?(input): boolean
  isEnabled(): boolean
  userFacingName(input): string
  mapToolResultToToolResultBlockParam(content, toolUseID): ToolResultBlockParam
  renderToolResultMessage?(content, ...): React.ReactNode
  // ... 约 30+ 个字段与方法
}

这个设计有几个关键决策值得注意:

第一,泛型三元组 <Input, Output, P>Input 是 Zod Schema 的类型,Output 是工具返回值类型,P 是进度(Progress)数据类型。这种设计让工具的类型安全贯穿定义 → 调用 → 结果渲染的全链路。

第二,call 是唯一的执行入口。不像某些框架把"执行"分散到多个生命周期钩子中,Claude Code 坚持单入口原则——所有副作用都发生在 call() 方法内部。这使得工具的语义非常清晰,也便于权限系统在调用前做统一拦截。

第三,渲染与执行分离call() 返回的是纯数据(ToolResult<Output>),而 renderToolResultMessage() 负责把数据渲染成 React 节点。这种数据层与表现层分离的架构,让同一个工具可以在 CLI(纯文本)、Web UI(React)和 SDK(JSON)三种场景下复用同一套执行逻辑。

1.2 buildTool:从定义到实例的桥梁

并非所有工具都需要实现全部 30+ 个字段。Claude Code 提供了一个名为 buildTool 的工厂函数(第 783 行),它接受一个部分定义ToolDef),自动填充安全默认值:

// src/Tool.ts (第 721-793 行,精简版)
export type ToolDef<Input, Output, P> =
  Omit<Tool<Input, Output, P>, DefaultableToolKeys> &
  Partial<Pick<Tool<Input, Output, P>, DefaultableToolKeys>>

const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: (_input?: unknown) => false,  // 默认不安全
  isReadOnly: (_input?: unknown) => false,          // 默认可写
  isDestructive: (_input?: unknown) => false,       // 默认非破坏性
  checkPermissions: (input, _ctx) =>
    Promise.resolve({ behavior: 'allow', updatedInput: input }),
  toAutoClassifierInput: (_input?: unknown) => '',   // 默认跳过分类器
  userFacingName: (_input?: unknown) => '',
}

export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
  return {
    ...TOOL_DEFAULTS,
    userFacingName: () => def.name,
    ...def,
  } as BuiltTool<D>
}

注意 TOOL_DEFAULTS 中的安全默认策略(Fail-Closed):

  • isConcurrencySafe 默认为 false——新工具不会意外地被并发调用。
  • isReadOnly 默认为 false——新工具默认被视为会修改系统状态,从而触发权限检查。
  • checkPermissions 默认放行,但将权限决策推迟到上层的通用权限系统。

这种设计极大地降低了新增工具的边际成本。开发者只需关注 call()inputSchemaprompt() 这三个核心要素,其余字段由框架补齐。

1.3 权限声明的三层模型

每个工具通过三个布尔方法向系统声明自己的"安全属性":

isReadOnly(input): boolean      // 是否只读?决定是否需要权限弹窗
isDestructive?(input): boolean  // 是否破坏性?用于风险提示(如覆盖文件)
isConcurrencySafe(input): boolean // 是否线程安全?决定能否与其他工具并发

BashTool 为例(src/tools/BashTool/BashTool.tsx,第 440-460 行):

isConcurrencySafe(input) {
  return this.isReadOnly?.(input) ?? false
},
isReadOnly(input) {
  const compoundCommandHasCd = commandHasAnyCd(input.command)
  const result = checkReadOnlyConstraints(input, compoundCommandHasCd)
  return result.behavior === 'allow'
},

BashToolisReadOnly 并不是硬编码的,而是动态分析命令字符串——如果检测到 git pushrm 等写操作,则返回 false,触发权限检查;如果是 lscatgrep 等读操作,则返回 true,允许静默执行。

二、tools.ts:工具池的组装与过滤

如果说 Tool.ts 是"宪法",那么 src/tools.ts 就是"内阁名单"——它负责把所有工具聚合起来,根据运行时环境过滤,最终形成一个可用的工具池(Tool Pool)。

2.1 getAllBaseTools:40+ 工具的大集合

getAllBaseTools()src/tools.ts,第 155-230 行)是整个系统的工具来源单一真相源(Single Source of Truth)。它返回一个数组,包含所有可能可用的内置工具:

export function getAllBaseTools(): Tools {
  return [
    AgentTool,
    TaskOutputTool,
    BashTool,
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    ExitPlanModeV2Tool,
    FileReadTool,
    FileEditTool,
    FileWriteTool,
    NotebookEditTool,
    WebFetchTool,
    TodoWriteTool,
    WebSearchTool,
    TaskStopTool,
    AskUserQuestionTool,
    SkillTool,
    EnterPlanModeTool,
    ...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
    ...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),
    ...(isTodoV2Enabled()
      ? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool]
      : []),
    ...(isEnvTruthy(process.env.ENABLE_LSP_TOOL) ? [LSPTool] : []),
    ...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
    getSendMessageTool(),
    ...(isAgentSwarmsEnabled()
      ? [getTeamCreateTool(), getTeamDeleteTool()]
      : []),
    ...(feature('AGENT_TRIGGERS')
      ? [CronCreateTool, CronDeleteTool, CronListTool]
      : []),
    ...(feature('MONITOR_TOOL') ? [MonitorTool] : []),
    ...(feature('WEB_BROWSER_TOOL') ? [WebBrowserTool] : []),
    ...(feature('HISTORY_SNIP') ? [SnipTool] : []),
    ...(feature('WORKFLOW_SCRIPTS') ? [WorkflowTool] : []),
    // ... 更多条件编译的工具
    ListMcpResourcesTool,
    ReadMcpResourceTool,
    ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
  ]
}

这段代码揭示了几个重要的工程实践:

环境门控(Feature Flag)。大量工具被包裹在 feature('XXX')process.env.XXX 的条件中。这使得 Anthropic 可以在同一份代码库中同时维护内部版(ant)和公开版,以及逐步灰度新功能。

延迟加载(Lazy Require)。部分工具使用 require() 而非 import 来打破循环依赖。例如 getSendMessageTool()getTeamCreateTool() 等函数在被调用时才加载对应模块(第 70-85 行)。

动态省略。如果运行环境已经嵌入了 bfs/ugrep 等快速搜索工具(通过 hasEmbeddedSearchTools() 判断),则不再注册独立的 GlobToolGrepTool,避免功能冗余。

2.2 权限过滤:从全量到可用

getAllBaseTools() 只是第一步。实际发送到 LLM 的工具池需要经过多层过滤,getTools() 函数(第 247-290 行)负责这项工作:

export const getTools = (permissionContext: ToolPermissionContext): Tools => {
  // Simple mode: 仅 Bash, Read, Edit
  if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
    return filterToolsByDenyRules([BashTool, FileReadTool, FileEditTool], permissionContext)
  }

  const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
  let allowedTools = filterToolsByDenyRules(tools, permissionContext)

  // REPL mode: 隐藏原始工具,只暴露 REPL wrapper
  if (isReplModeEnabled()) {
    allowedTools = allowedTools.filter(tool => !REPL_ONLY_TOOLS.has(tool.name))
  }

  const isEnabled = allowedTools.map(_ => _.isEnabled())
  return allowedTools.filter((_, i) => isEnabled[i])
}

过滤链路如下:

  1. Deny Rules 过滤filterToolsByDenyRules() 根据用户配置的拒绝规则(如 "禁止所有 MCP 工具" 或 "禁止 Bash")剔除工具。这一步甚至发生在模型看到工具列表之前——被拒绝的工具对 LLM 完全不可见。
  2. REPL 模式过滤:如果启用了 REPL 模式,原始工具(Bash/Read/Edit 等)被隐藏,仅暴露 REPLTool。REPL 内部通过 VM 上下文仍可访问这些工具,但 LLM 只能通过 REPL 间接调用。
  3. isEnabled() 过滤:每个工具可以基于自身状态决定是否可用(如 LSP 工具仅在检测到语言服务器时启用)。

2.3 assembleToolPool:内置工具 + MCP 工具的合并

当用户配置了 MCP 服务器时,工具池还需要合并外部动态工具assembleToolPool()(第 295-320 行)是这一过程的单一真相源:

export function assembleToolPool(
  permissionContext: ToolPermissionContext,
  mcpTools: Tools,
): Tools {
  const builtInTools = getTools(permissionContext)
  const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)

  // 按名称排序,保证 Prompt Cache 的稳定性
  const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  return uniqBy(
    [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
    'name',
  )
}

这里有一个精妙的细节:排序是为了 Prompt Cache 稳定。Anthropic API 支持系统提示(System Prompt)的缓存复用(cache_control)。如果工具列表的顺序在不同请求间发生变化,缓存键就会失效。因此 assembleToolPool 对内置工具和 MCP 工具分别排序后再合并,确保只要工具集合不变,顺序就恒定。

uniqByname 去重时,内置工具优先于 MCP 工具——因为内置工具在数组前面。这防止了恶意 MCP 服务器通过同名工具劫持内置功能。

2.4 子代理工具过滤

Claude Code 支持多 Agent 协作(Subagent),但并非所有工具都适合子代理使用。src/constants/tools.ts 定义了几套工具白名单:

// src/constants/tools.ts (第 40-100 行)
export const ALL_AGENT_DISALLOWED_TOOLS = new Set([
  TASK_OUTPUT_TOOL_NAME,      // 防止递归输出
  EXIT_PLAN_MODE_V2_TOOL_NAME,// 计划模式是主线程抽象
  ENTER_PLAN_MODE_TOOL_NAME,
  ASK_USER_QUESTION_TOOL_NAME,// 子代理不能直接向用户提问
  TASK_STOP_TOOL_NAME,        // 需要主线程任务状态
])

export const ASYNC_AGENT_ALLOWED_TOOLS = new Set([
  FILE_READ_TOOL_NAME, WEB_SEARCH_TOOL_NAME, TODO_WRITE_TOOL_NAME,
  GREP_TOOL_NAME, WEB_FETCH_TOOL_NAME, GLOB_TOOL_NAME,
  ...SHELL_TOOL_NAMES, FILE_EDIT_TOOL_NAME, FILE_WRITE_TOOL_NAME,
  NOTEBOOK_EDIT_TOOL_NAME, SKILL_TOOL_NAME,
])

export const COORDINATOR_MODE_ALLOWED_TOOLS = new Set([
  AGENT_TOOL_NAME, TASK_STOP_TOOL_NAME,
  SEND_MESSAGE_TOOL_NAME, SYNTHETIC_OUTPUT_TOOL_NAME,
])

这种分层过滤确保了:

  • 异步 Agent 只能使用文件/网络/Shell 等"安全"工具,不能操作计划模式或输出管道。
  • Coordinator(协调器)模式 下,主协调器仅保留 Agent 管理和消息发送工具,实际工作由 Worker 代理完成。
  • In-Process Teammate 可以额外使用任务管理工具(TaskCreate/Get/Update/List),实现团队级协作。

三、Schema 系统:Zod 与模型决策的桥梁

工具能做什么,是由 Schema 告诉模型的。Claude Code 的 Schema 系统基于 Zod v4,并在此基础上构建了一套与 Anthropic API 无缝对接的转换层。

3.1 Zod Schema 的定义范式

每个工具都通过 inputSchema 定义其输入参数。以 FileReadTool 为例(src/tools/FileReadTool/FileReadTool.ts,第 227-245 行):

const inputSchema = lazySchema(() =>
  z.object({
    file_path: z.string().describe('The path to the file to read'),
    offset: z.number().optional().describe('Line number to start reading from'),
    limit: z.number().optional().describe('Max number of lines to read'),
    pages: z.string().optional().describe('Page range for PDF files'),
  }),
)

注意 .describe() 的使用——这些描述字符串直接流入模型看到的工具定义中,是模型决定是否调用该工具、以及如何填写参数的关键依据。

Claude Code 使用了 lazySchema() 工具函数来实现延迟初始化。某些 Schema 的定义依赖于运行时配置(如 isBackgroundTasksDisabled),延迟加载可以避免在模块初始化时就执行这些判断。

3.2 outputSchema 与类型安全

除了输入,FileReadTool 还定义了输出 Schema(第 300-340 行):

const outputSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('text'), file: z.object({ ... }) }),
  z.object({ type: z.literal('image'), file: z.object({ ... }) }),
  z.object({ type: z.literal('pdf'), file: z.object({ ... }) }),
  z.object({ type: z.literal('notebook'), file: z.object({ ... }) }),
])

这里使用了 z.discriminatedUnion 来实现标签联合类型(Tagged Union)。FileReadTool 的返回值根据文件类型不同而有完全不同的形状,但通过 type 字段的判别,TypeScript 可以在 switch (data.type) 中自动收窄类型。

3.3 JSON Schema 转换与 MCP 兼容

对于内置工具,Zod Schema 会被自动转换为 JSON Schema 后发送给 Anthropic API。但 MCP 工具有所不同——它们可能直接提供 inputJSONSchemaTool.ts 第 52 行定义),绕过 Zod 转换:

export type ToolInputJSONSchema = {
  [x: string]: unknown
  type: 'object'
  properties?: { [x: string]: unknown }
}

这体现了 Claude Code 对 MCP 协议 的兼容:MCP 服务器使用 JSON Schema 定义工具,Claude Code 可以直接透传,无需再经过 Zod 这一层。

3.4 描述文本(Description)的工程意义

description() 方法返回的文本是模型可见的"工具说明书"。在 FileReadTool 中(第 350-365 行):

async prompt() {
  const limits = getDefaultFileReadingLimits()
  const maxSizeInstruction = limits.includeMaxSizeInPrompt
    ? `. Files larger than ${formatFileSize(limits.maxSizeBytes)} will return an error`
    : ''
  return renderPromptTemplate(
    pickLineFormatInstruction(),
    maxSizeInstruction,
    OFFSET_INSTRUCTION_DEFAULT,
  )
}

prompt() 返回的内容会被注入到系统提示中,作为该工具的"详细说明书"。它与 description()(单行摘要)形成层级关系:

  • description():一行摘要,出现在工具列表中,帮助模型快速判断是否需要该工具。
  • prompt():完整说明,包括参数含义、边界条件、使用建议,帮助模型正确填写参数。

这种分层设计的工程意义在于节省 Token:并非所有工具的完整说明都需要塞进每次请求的上下文。Claude Code 通过 ToolSearch 机制(后文详述)将不常用工具的 prompt() 延迟加载,只在模型主动搜索时才提供。

四、工具全景:40+ 工具的分类地图

graph TD
    A[Claude Code Tool System
40+ Tools] --> B[文件操作类] A --> C[Shell 执行类] A --> D[Agent 协作类] A --> E[网络与搜索类] A --> F[MCP 与 LSP 类] A --> G[交互与管理类] A --> H[计划与工作区类] A --> I[其他扩展类] B --> B1[FileReadTool
读取文本/图片/PDF/Notebook] B --> B2[FileEditTool
字符串替换编辑] B --> B3[FileWriteTool
创建/覆盖文件] B --> B4[GlobTool
文件 glob 匹配] B --> B5[GrepTool
内容正则搜索] B --> B6[NotebookEditTool
Jupyter Notebook 编辑] C --> C1[BashTool
Unix Shell 执行] C --> C2[PowerShellTool
Windows PowerShell] C --> C3[REPLTool
交互式解释器] D --> D1[AgentTool
子代理创建与管理] D --> D2[SendMessageTool
Agent 间消息] D --> D3[TaskOutputTool
任务结果输出] D --> D4[TaskStopTool
停止任务] D --> D5[TaskCreate/Get/Update/List
任务生命周期] D --> D6[TeamCreate/DeleteTool
团队管理] E --> E1[WebSearchTool
网络搜索] E --> E2[WebFetchTool
URL 内容获取] E --> E3[WebBrowserTool
浏览器自动化] F --> F1[ListMcpResourcesTool
列举 MCP 资源] F --> F2[ReadMcpResourceTool
读取 MCP 资源] F --> F3[MCPTool 动态注入
MCP 服务器工具] F --> F4[LSPTool
语言服务器协议] G --> G1[AskUserQuestionTool
向用户提问] G --> G2[TodoWriteTool
待办列表管理] G --> G3[SkillTool
技能脚本调用] G --> G4[BriefTool
摘要生成] G --> G5[ConfigTool
配置管理] H --> H1[EnterPlanModeTool
进入计划模式] H --> H2[ExitPlanModeV2Tool
退出计划模式] H --> H3[EnterWorktreeTool
进入工作树] H --> H4[ExitWorktreeTool
退出工作树] I --> I1[ToolSearchTool
工具搜索/延迟加载] I --> I2[WorkflowTool
工作流脚本] I --> I3[MonitorTool
进程监控] I --> I4[CronCreate/Delete/List
定时任务] I --> I5[SnipTool
历史消息裁剪] I --> I6[TerminalCaptureTool
终端捕获]

4.1 文件操作类(6 个)

这是最高频的工具集合,几乎每次对话都会用到:

  • FileReadTool:支持文本、图片、PDF(含分页读取)、Jupyter Notebook。内置缓存去重(readFileState),同文件未修改时返回 file_unchanged 避免重复传输。
  • FileEditTool:基于字符串匹配(old_stringnew_string)的精确编辑,支持多行块替换。
  • FileWriteTool:创建新文件或完全覆盖已有文件。
  • GlobTool / GrepTool:文件搜索工具。在 Ant 内部构建中,如果嵌入了 bfs/ugrep,这两个工具会被省略,由 Shell 别名替代。
  • NotebookEditTool:专门处理 .ipynb 文件的单元格级编辑。

4.2 Shell 执行类(3 个)

  • BashTool:最复杂的工具之一(1143 行)。支持前台/后台执行、超时控制、沙箱检测(shouldUseSandbox)、命令语义分析(区分 read/search/list/write)、进度渲染。
  • PowerShellTool:Windows 环境的 Shell 执行,条件编译(仅在 Windows 或显式启用时出现)。
  • REPLTool:交互式解释器包装器,将 Bash/Read/Edit 等工具封装在 VM 上下文中,实现类似 Jupyter 的 REPL 体验。

4.3 Agent 协作类(6+ 个)

  • AgentTool:创建子代理(Subagent),是整个多 Agent 架构的核心。
  • SendMessageTool:Agent 间通信,支持 Coordinator-Worker 模式的指令传递。
  • TaskOutputTool / TaskStopTool:任务结果输出和强制停止。
  • TaskCreate/Get/Update/List:Todo V2 任务管理系统的 CRUD。
  • TeamCreateTool / TeamDeleteTool:Agent Swarms 的团队生命周期管理。

4.4 网络类(3 个)

  • WebSearchTool:搜索引擎集成,支持查询和结果摘要。
  • WebFetchTool:直接抓取 URL 内容,支持静态页面。
  • WebBrowserTool:条件编译的浏览器自动化工具,可执行 JavaScript、截图等。

4.5 MCP 类(3+ 个)

  • ListMcpResourcesTool / ReadMcpResourceTool:枚举和读取 MCP 服务器的 Resource。
  • MCPTool(动态):由 MCP 服务器在连接时动态注入,通过 mcpInfo 字段记录 {serverName, toolName}
  • McpAuth 相关:处理 MCP 服务器的 OAuth/Token 认证流程。

4.6 LSP 类(1 个)

  • LSPTool:语言服务器协议客户端,条件编译(ENABLE_LSP_TOOL)。支持跳转到定义、查找引用、诊断获取等 IDE 级功能。

4.7 交互与管理类(5 个)

  • AskUserQuestionTool:当模型需要澄清时,主动向用户提问并等待回答。
  • TodoWriteTool:管理任务列表,在 UI 中渲染为独立的 Todo 面板。
  • SkillTool:调用项目本地的 Skill 脚本(.claude/skills/ 目录下的自定义工具)。
  • BriefTool:生成对话摘要,用于长会话的上下文压缩。
  • ConfigTool:读取/修改 Claude Code 的配置项(Ant 内部专用)。

4.8 计划与工作区类(4 个)

  • EnterPlanModeTool / ExitPlanModeV2Tool:计划模式的进入和退出。计划模式下模型先输出完整的执行计划,经用户确认后再逐步执行。
  • EnterWorktreeTool / ExitWorktreeTool:Git Worktree 的切换支持,条件编译。

4.9 其他扩展类(6+ 个)

  • ToolSearchTool:支持工具的延迟加载(Deferred Loading)。当工具数量超过阈值时,不常用工具的完整 Schema 不会直接发给模型,而是需要通过 ToolSearch 按需检索。
  • WorkflowTool:执行预定义的工作流脚本(条件编译 WORKFLOW_SCRIPTS)。
  • MonitorTool:监控长时间运行的进程或日志流(条件编译 MONITOR_TOOL)。
  • CronCreate/Delete/List:定时任务调度(条件编译 AGENT_TRIGGERS)。
  • SnipTool:裁剪历史消息,控制上下文长度(条件编译 HISTORY_SNIP)。
  • TerminalCaptureTool:捕获终端面板输出(条件编译 TERMINAL_PANEL)。

五、工具系统的扩展机制

5.1 如何添加一个新工具

基于 buildTool 工厂,新增一个工具的标准步骤如下:

第一步,创建目录和文件。每个工具有独立的目录,如 src/tools/MyTool/MyTool.ts

import { z } from 'zod/v4'
import { buildTool, type ToolDef } from '../../Tool.js'

const inputSchema = z.object({
  param: z.string().describe('Parameter description'),
})

const outputSchema = z.object({
  result: z.string(),
})

export const MyTool = buildTool({
  name: 'my_tool',
  searchHint: 'do something useful',
  maxResultSizeChars: 10_000,
  inputSchema,
  outputSchema,
  async description() {
    return 'Does something useful'
  },
  async prompt() {
    return 'Detailed prompt for the model...'
  },
  isReadOnly() {
    return true  // 或 false
  },
  isConcurrencySafe() {
    return true
  },
  async call(args, context, canUseTool, parentMessage, onProgress) {
    // 执行逻辑
    return { data: { result: 'done' } }
  },
  mapToolResultToToolResultBlockParam(data, toolUseID) {
    return {
      tool_use_id: toolUseID,
      type: 'tool_result',
      content: data.result,
    }
  },
} satisfies ToolDef<typeof inputSchema, z.infer<typeof outputSchema>>)

第二步,注册到工具池。在 src/tools.tsgetAllBaseTools() 数组中添加新工具。如果需要条件编译,使用 feature() 或环境变量包裹。

第三步,更新权限常量(如果需要)。如果该工具不应该被子代理使用,在 src/constants/tools.tsALL_AGENT_DISALLOWED_TOOLSASYNC_AGENT_ALLOWED_TOOLS 中添加工具名。

5.2 工具测试框架

Claude Code 没有独立的"工具测试框架",但每个工具的测试遵循以下模式(从源码中的 __tests__ 目录推断):

  1. Schema 验证测试:确保 Zod Schema 能正确解析合法输入、拒绝非法输入。
  2. 权限测试:模拟不同的 ToolPermissionContext,验证 checkPermissions() 的返回值。
  3. Mock 调用测试:使用 Jest/Vitest 的 spyOn 来模拟 ToolUseContext 和文件系统操作,测试 call() 的逻辑。
  4. Render 保真测试transcriptSearch.renderFidelity.test.tsx 会检查 renderToolResultMessage 渲染的文本与 extractSearchText() 返回的文本是否一致,防止搜索索引与实际显示内容出现偏差。

5.3 工具 Schema 缓存与延迟加载

当工具数量膨胀时(尤其是接入多个 MCP 服务器后),系统提示中的工具定义会消耗大量 Token。Claude Code 通过两种机制缓解这个问题:

系统提示缓存(Prompt Cache)。Anthropic API 支持对系统提示进行缓存复用。assembleToolPool() 中的排序策略正是为了保证缓存键的稳定性——只要工具列表和顺序不变,缓存就命中。

延迟加载(Deferred Loading)。当工具数量超过阈值时,isToolSearchEnabledOptimistic() 返回 trueToolSearchTool 被注册到工具池中。此时,标记了 shouldDefer: true 的工具不会发送完整的 Schema 给模型,而是只发送名称和摘要。模型如果需要调用该工具,必须先通过 ToolSearchTool 查询获取完整 Schema。这在 MCP 场景下尤为重要——防止 50+ MCP 工具一次性挤爆上下文。

sequenceDiagram
    participant LLM as Claude API
    participant CC as Claude Code
    participant TP as Tool Pool
    participant MCP as MCP Server

    Note over CC,MCP: 初始化阶段
    MCP->>CC: 注册工具列表 (name + description)
    CC->>TP: assembleToolPool(builtIn + mcp)
    TP->>LLM: 发送高频工具完整 Schema
+ 低频工具延迟标记 Note over LLM,CC: 运行阶段 LLM->>CC: ToolSearch(keyword="database") CC->>TP: 检索匹配工具 TP->>LLM: 返回完整 Schema LLM->>CC: 调用目标工具

六、总结

Claude Code 的工具系统是协议驱动、分层过滤、类型安全、动态扩展的典范设计。其核心架构可以概括为:

  • 一个基类协议Tool.ts):通过 TypeScript 结构类型定义工具契约,buildTool 工厂提供安全默认值,降低扩展成本。
  • 一个注册中心tools.ts):getAllBaseTools() 聚合 40+ 工具,getTools() 多层过滤,assembleToolPool() 合并 MCP 动态工具。
  • 一套 Schema 系统(Zod):输入输出全链路类型安全,.describe() 直接影响模型决策,延迟加载和缓存机制优化 Token 开销。
  • 一层权限网关:工具通过 isReadOnly/isDestructive/checkPermissions 声明安全属性,Permission Context 在调用前统一拦截。

这种架构让 Claude Code 能够在同一份代码库中同时支持:极简的 --simple 模式(3 个工具)、标准的交互模式(30+ 工具)、复杂的 Coordinator-Worker 多 Agent 模式、以及外部 MCP 生态的无限扩展。对于任何正在构建 AI Agent 平台的开发者来说,这套设计都是一份值得深入研究的参考实现。