本章总览
Tool.ts(约 790 行)定义 Claude Code 全部工具共享的 TypeScript 契约:Tool 泛型接口、ToolUseContext 运行时上下文、buildTool 默认值工厂,以及 toolMatchesName / findToolByName 查找辅助。tools.ts 是内置工具的「注册表」:getAllBaseTools 列出所有可能工具,getTools 按权限与环境过滤,assembleToolPool 与 MCP 合并。本章要求你能从 API 看到的 tool name 反查到具体 Tool 导出,并理解 call 签名中各参数的职责。
学完本章你应该能
- 列举 Tool 接口的核心方法及其在生命周期中的调用顺序
- 解释 buildTool 的 fail-closed 默认值策略
- 说明 getTools 与 assembleToolPool 的差异与使用场景
- 理解 ToolUseContext 中 abortController、setToolJSX、messages 的用途
- 能在 feature gate / env 条件下预测某工具是否出现在 getAllBaseTools
核心概念(先读懂这些)
Tool 不是 MCP Tool 的简单包装
内置 Tool 与 MCP Tool 最终都满足同一 Tool 接口,但构造路径不同:内置通过 buildTool({ name, call, ... }) 导出常量;MCP 在 client 连接后动态合成。接口上的 isMcp、mcpInfo、inputJSONSchema 等字段主要服务 MCP 路径。读 call() 时要分清 context.options.tools 是当前 turn 的快照还是 refreshTools 刷新后的池。
ToolUseContext 是会话级依赖注入
call() 第二个参数 ToolUseContext 携带 abortController、getAppState/setAppState、readFileState、messages 等。子 Agent 通过 createSubagentContext 克隆并收窄权限;setAppState 对 async agent 可能是 no-op,setAppStateForTasks 始终到达根 store。render* 方法不接收 context,但 call 内可通过 setToolJSX 驱动 REPL overlay。
buildTool 集中默认值
60+ 工具通过 buildTool 定义,避免每个文件重复 isEnabled/isConcurrencySafe/checkPermissions 等 stub。默认 isConcurrencySafe=false(保守)、checkPermissions=allow(交给 permissions.ts)。安全相关工具必须 override toAutoClassifierInput,否则分类器跳过该工具。
建议学习步骤
- 阅读 Tool 接口定义(call、checkPermissions、validateInput)
- 阅读 buildTool 与 TOOL_DEFAULTS
- 阅读 getAllBaseTools 的条件展开
- 阅读 getTools 过滤链
- 阅读 assembleToolPool 排序与去重
- 在源码树打开 Tool.ts 对照行号
常见误区
注意
不要把 ToolProgress 与 HookProgress 混为一谈(filterToolProgressMessages 负责分离)
注意
getMergedTools 不去重,assembleToolPool 才去重;token 计数场景注意选对函数
注意
REPL 模式下 REPL_ONLY_TOOLS 隐藏原始 Bash/Read,但 REPL VM 内仍可调用
在架构中的位置
工具生命周期:
tools.ts getTools / assembleToolPool
→ query 把 Tools 放入 toolUseContext.options.tools
→ API 请求携带 tool schemas(description + inputSchema)
→ 模型返回 tool_use blocks
→ StreamingToolExecutor.addTool → runToolUse → tool.call()
→ mapToolResultToToolResultBlockParam → user tool_result message
Tool.ts 位于依赖图中心但被刻意保持「无具体工具 import」:类型与工厂 only。tools.ts 是唯一知道「有哪些内置工具」的模块,且与 GrowthBook dynamic config 注释要求保持同步。
Tool 泛型接口:call 与权限钩子
Tool<Input, Output, P> 的核心方法:
| 方法 | 阶段 | 说明 |
|---|---|---|
| validateInput? | call 前 | 返回 ValidationResult,失败则模型看到 errorCode |
| checkPermissions | canUseTool 内 | 工具特有权限(Bash 命令模式等) |
| call | 执行 | 返回 ToolResult { data, newMessages?, contextModifier? } |
| mapToolResultToToolResultBlockParam | 结果序列化 | 转为 API tool_result block |
| renderTool* | UI | Ink 组件,与模型 payload 可不同 |
call 签名:
call(args, context, canUseTool, parentMessage, onProgress?)
canUseTool 由 useCanUseTool / permissions 提供;多数工具在 call 内不再调用,但 FileEdit 等可能在执行前二次确认。onProgress 推送 ProgressMessage,StreamingToolExecutor 立即 yield。
并发语义: isConcurrencySafe(input) 决定 StreamingToolExecutor 能否与其他 safe 工具并行。默认 false。
源码引用: src/Tool.ts · 第 362–420 行(共 793 行)
362| export type Tool<
363| Input extends AnyObject = AnyObject,
364| Output = unknown,
365| P extends ToolProgressData = ToolProgressData,
366| > = {
367| /**
368| * Optional aliases for backwards compatibility when a tool is renamed.
369| * The tool can be looked up by any of these names in addition to its primary name.
370| */
371| aliases?: string[]
372| /**
373| * One-line capability phrase used by ToolSearch for keyword matching.
374| * Helps the model find this tool via keyword search when it's deferred.
375| * 3–10 words, no trailing period.
376| * Prefer terms not already in the tool name (e.g. 'jupyter' for NotebookEdit).
377| */
378| searchHint?: string
379| call(
380| args: z.infer<Input>,
381| context: ToolUseContext,
382| canUseTool: CanUseToolFn,
383| parentMessage: AssistantMessage,
384| onProgress?: ToolCallProgress<P>,
385| ): Promise<ToolResult<Output>>
386| description(
387| input: z.infer<Input>,
388| options: {
389| isNonInteractiveSession: boolean
390| toolPermissionContext: ToolPermissionContext
391| tools: Tools
392| },
393| ): Promise<string>
394| readonly inputSchema: Input
395| // Type for MCP tools that can specify their input schema directly in JSON Schema format
396| // rather than converting from Zod schema
397| readonly inputJSONSchema?: ToolInputJSONSchema
398| // Optional because TungstenTool doesn't define this. TODO: Make it required.
399| // When we do that, we can also go through and make this a bit more type-safe.
400| outputSchema?: z.ZodType<unknown>
401| inputsEquivalent?(a: z.infer<Input>, b: z.infer<Input>): boolean
402| isConcurrencySafe(input: z.infer<Input>): boolean
403| isEnabled(): boolean
404| isReadOnly(input: z.infer<Input>): boolean
405| /** Defaults to false. Only set when the tool performs irreversible operations (delete, overwrite, send). */
406| isDestructive?(input: z.infer<Input>): boolean
407| /**
408| * What should happen when the user submits a new message while this tool
409| * is running.
410| *
411| * - `'cancel'` — stop the tool and discard its result
412| * - `'block'` — keep running; the new message waits
413| *
414| * Defaults to `'block'` when not implemented.
415| */
416| interruptBehavior?(): 'cancel' | 'block'
417| /**
418| * Returns information about whether this tool use is a search or read operation
419| * that should be collapsed into a condensed display in the UI. Examples include
420| * file searching (Grep, Glob), file reading (Read), and bash commands like find,
源码引用: src/Tool.ts · 第 489–504 行(共 793 行)
489| validateInput?(
490| input: z.infer<Input>,
491| context: ToolUseContext,
492| ): Promise<ValidationResult>
493|
494| /**
495| * Determines if the user is asked for permission. Only called after validateInput() passes.
496| * General permission logic is in permissions.ts. This method contains tool-specific logic.
497| * @param input
498| * @param context
499| */
500| checkPermissions(
501| input: z.infer<Input>,
502| context: ToolUseContext,
503| ): Promise<PermissionResult>
504|
ToolUseContext 关键字段
ToolUseContext(约 158 行起)是会话级依赖容器:
options: commands、mainLoopModel、tools、agentDefinitions、mcpClients、refreshTools 等。tools 字段是当前 turn 可用工具快照。
状态回调:
- setInProgressToolUseIDs — REPL 显示 spinner / 禁止重复提交
- setHasInterruptibleToolInProgress — ESC 中断语义(interruptBehavior=cancel 的工具)
- setToolJSX — 工具驱动的全屏 overlay(如 AskUserQuestion)
- appendSystemMessage — UI-only system 行,API 边界 strip
子 Agent 专用:
- agentId / agentType — hooks 区分主线程与子 agent
- setAppStateForTasks — 后台任务注册不受 async no-op 影响
- contentReplacementState — tool result 预算替换
- renderedSystemPrompt — fork 子 agent 共享父 prompt cache
读 call 实现时,先确认 toolUseContext.agentId 是否存在,许多工具(Bash cwd、权限)行为因此不同。
源码引用: src/Tool.ts · 第 158–210 行(共 793 行)
158| export type ToolUseContext = {
159| options: {
160| commands: Command[]
161| debug: boolean
162| mainLoopModel: string
163| tools: Tools
164| verbose: boolean
165| thinkingConfig: ThinkingConfig
166| mcpClients: MCPServerConnection[]
167| mcpResources: Record<string, ServerResource[]>
168| isNonInteractiveSession: boolean
169| agentDefinitions: AgentDefinitionsResult
170| maxBudgetUsd?: number
171| /** Custom system prompt that replaces the default system prompt */
172| customSystemPrompt?: string
173| /** Additional system prompt appended after the main system prompt */
174| appendSystemPrompt?: string
175| /** Override querySource for analytics tracking */
176| querySource?: QuerySource
177| /** Optional callback to get the latest tools (e.g., after MCP servers connect mid-query) */
178| refreshTools?: () => Tools
179| }
180| abortController: AbortController
181| readFileState: FileStateCache
182| getAppState(): AppState
183| setAppState(f: (prev: AppState) => AppState): void
184| /**
185| * Always-shared setAppState for session-scoped infrastructure (background
186| * tasks, session hooks). Unlike setAppState, which is no-op for async agents
187| * (see createSubagentContext), this always reaches the root store so agents
188| * at any nesting depth can register/clean up infrastructure that outlives
189| * a single turn. Only set by createSubagentContext; main-thread contexts
190| * fall back to setAppState.
191| */
192| setAppStateForTasks?: (f: (prev: AppState) => AppState) => void
193| /**
194| * Optional handler for URL elicitations triggered by tool call errors (-32042).
195| * In print/SDK mode, this delegates to structuredIO.handleElicitation.
196| * In REPL mode, this is undefined and the queue-based UI path is used.
197| */
198| handleElicitation?: (
199| serverName: string,
200| params: ElicitRequestURLParams,
201| signal: AbortSignal,
202| ) => Promise<ElicitResult>
203| setToolJSX?: SetToolJSXFn
204| addNotification?: (notif: Notification) => void
205| /** Append a UI-only system message to the REPL message list. Stripped at the
206| * normalizeMessagesForAPI boundary — the Exclude<> makes that type-enforced. */
207| appendSystemMessage?: (
208| msg: Exclude<SystemMessage, SystemLocalCommandMessage>,
209| ) => void
210| /** Send an OS-level notification (iTerm2, Kitty, Ghostty, bell, etc.) */
源码引用: src/Tool.ts · 第 245–300 行(共 793 行)
245| agentId?: AgentId // Only set for subagents; use getSessionId() for session ID. Hooks use this to distinguish subagent calls.
246| agentType?: string // Subagent type name. For the main thread's --agent type, hooks fall back to getMainThreadAgentType().
247| /** When true, canUseTool must always be called even when hooks auto-approve.
248| * Used by speculation for overlay file path rewriting. */
249| requireCanUseTool?: boolean
250| messages: Message[]
251| fileReadingLimits?: {
252| maxTokens?: number
253| maxSizeBytes?: number
254| }
255| globLimits?: {
256| maxResults?: number
257| }
258| toolDecisions?: Map<
259| string,
260| {
261| source: string
262| decision: 'accept' | 'reject'
263| timestamp: number
264| }
265| >
266| queryTracking?: QueryChainTracking
267| /** Callback factory for requesting interactive prompts from the user.
268| * Returns a prompt callback bound to the given source name.
269| * Only available in interactive (REPL) contexts. */
270| requestPrompt?: (
271| sourceName: string,
272| toolInputSummary?: string | null,
273| ) => (request: PromptRequest) => Promise<PromptResponse>
274| toolUseId?: string
275| criticalSystemReminder_EXPERIMENTAL?: string
276| /** When true, preserve toolUseResult on messages even for subagents.
277| * Used by in-process teammates whose transcripts are viewable by the user. */
278| preserveToolUseResults?: boolean
279| /** Local denial tracking state for async subagents whose setAppState is a
280| * no-op. Without this, the denial counter never accumulates and the
281| * fallback-to-prompting threshold is never reached. Mutable — the
282| * permissions code updates it in place. */
283| localDenialTracking?: DenialTrackingState
284| /**
285| * Per-conversation-thread content replacement state for the tool result
286| * budget. When present, query.ts applies the aggregate tool result budget.
287| * Main thread: REPL provisions once (never resets — stale UUID keys
288| * are inert). Subagents: createSubagentContext clones the parent's state
289| * by default (cache-sharing forks need identical decisions), or
290| * resumeAgentBackground threads one reconstructed from sidechain records.
291| */
292| contentReplacementState?: ContentReplacementState
293| /**
294| * Parent's rendered system prompt bytes, frozen at turn start.
295| * Used by fork subagents to share the parent's prompt cache — re-calling
296| * getSystemPrompt() at fork-spawn time can diverge (GrowthBook cold→warm)
297| * and bust the cache. See forkSubagent.ts.
298| */
299| renderedSystemPrompt?: SystemPrompt
300| }
buildTool 与 TOOL_DEFAULTS
buildTool(def) 展开 { ...TOOL_DEFAULTS, userFacingName: () => def.name, ...def }。
默认值(fail-closed):
- isEnabled → true
- isConcurrencySafe → false
- isReadOnly → false
- isDestructive → false
- checkPermissions → allow(defer 到 permissions.ts)
- toAutoClassifierInput → ''(跳过 classifier)
ToolDef 类型允许省略 DefaultableToolKeys;BuiltTool<D> 类型级保证最终 Tool 完整。
工程意义: 新增工具时只需实现 call、inputSchema、prompt、render* 等差异化方法;不必复制 7 个 stub。若忘记 override isConcurrencySafe,工具会自动串行执行——对写操作通常是正确默认。
源码引用: src/Tool.ts · 第 743–792 行(共 793 行)
743| /**
744| * Build a complete `Tool` from a partial definition, filling in safe defaults
745| * for the commonly-stubbed methods. All tool exports should go through this so
746| * that defaults live in one place and callers never need `?.() ?? default`.
747| *
748| * Defaults (fail-closed where it matters):
749| * - `isEnabled` → `true`
750| * - `isConcurrencySafe` → `false` (assume not safe)
751| * - `isReadOnly` → `false` (assume writes)
752| * - `isDestructive` → `false`
753| * - `checkPermissions` → `{ behavior: 'allow', updatedInput }` (defer to general permission system)
754| * - `toAutoClassifierInput` → `''` (skip classifier — security-relevant tools must override)
755| * - `userFacingName` → `name`
756| */
757| const TOOL_DEFAULTS = {
758| isEnabled: () => true,
759| isConcurrencySafe: (_input?: unknown) => false,
760| isReadOnly: (_input?: unknown) => false,
761| isDestructive: (_input?: unknown) => false,
762| checkPermissions: (
763| input: { [key: string]: unknown },
764| _ctx?: ToolUseContext,
765| ): Promise<PermissionResult> =>
766| Promise.resolve({ behavior: 'allow', updatedInput: input }),
767| toAutoClassifierInput: (_input?: unknown) => '',
768| userFacingName: (_input?: unknown) => '',
769| }
770|
771| // The defaults type is the ACTUAL shape of TOOL_DEFAULTS (optional params so
772| // both 0-arg and full-arg call sites type-check — stubs varied in arity and
773| // tests relied on that), not the interface's strict signatures.
774| type ToolDefaults = typeof TOOL_DEFAULTS
775|
776| // D infers the concrete object-literal type from the call site. The
777| // constraint provides contextual typing for method parameters; `any` in
778| // constraint position is structural and never leaks into the return type.
779| // BuiltTool<D> mirrors runtime `{...TOOL_DEFAULTS, ...def}` at the type level.
780| // eslint-disable-next-line @typescript-eslint/no-explicit-any
781| type AnyToolDef = ToolDef<any, any, any>
782|
783| export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
784| // The runtime spread is straightforward; the `as` bridges the gap between
785| // the structural-any constraint and the precise BuiltTool<D> return. The
786| // type semantics are proven by the 0-error typecheck across all 60+ tools.
787| return {
788| ...TOOL_DEFAULTS,
789| userFacingName: () => def.name,
790| ...def,
791| } as BuiltTool<D>
792| }
toolMatchesName 与 findToolByName
工具可声明 aliases(如 AgentTool 的 Task 旧名)。toolMatchesName 同时匹配 name 与 aliases;findToolByName 在 Tools 数组上查找。
StreamingToolExecutor.addTool 与 runToolUse 均通过 findToolByName 解析 block.name。权限规则、hooks、analytics 也应使用同一 helper,避免 Task vs Agent 分裂。
searchHint 字段供 ToolSearch 关键词匹配:3–10 词,不含 tool name 已有词汇。shouldDefer / alwaysLoad 控制 ToolSearch 实验下的 schema 可见性。
源码引用: src/Tool.ts · 第 345–360 行(共 793 行)
345| /**
346| * Checks if a tool matches the given name (primary name or alias).
347| */
348| export function toolMatchesName(
349| tool: { name: string; aliases?: string[] },
350| name: string,
351| ): boolean {
352| return tool.name === name || (tool.aliases?.includes(name) ?? false)
353| }
354|
355| /**
356| * Finds a tool by name or alias from a list of tools.
357| */
358| export function findToolByName(tools: Tools, name: string): Tool | undefined {
359| return tools.find(t => toolMatchesName(t, name))
360| }
源码引用: src/Tool.ts · 第 436–449 行(共 793 行)
436| isMcp?: boolean
437| isLsp?: boolean
438| /**
439| * When true, this tool is deferred (sent with defer_loading: true) and requires
440| * ToolSearch to be used before it can be called.
441| */
442| readonly shouldDefer?: boolean
443| /**
444| * When true, this tool is never deferred — its full schema appears in the
445| * initial prompt even when ToolSearch is enabled. For MCP tools, set via
446| * `_meta['anthropic/alwaysLoad']`. Use for tools the model must see on
447| * turn 1 without a ToolSearch round-trip.
448| */
449| readonly alwaysLoad?: boolean
getAllBaseTools:条件注册
getAllBaseTools(约 193 行)返回当前构建环境下可能存在的全部内置工具数组。注释强调必须与 Statsig claude_code_global_system_caching 保持同步。
典型条件分支:
- hasEmbeddedSearchTools() — ant 内置 bfs/ugrep 时省略 Glob/Grep
- feature('KAIROS') / feature('AGENT_TRIGGERS') — 定时与远程触发工具
- isTodoV2Enabled() — TaskCreate/Get/Update/List
- isReplModeEnabled() + REPLTool — ant 内置 REPL
- isToolSearchEnabledOptimistic() — ToolSearchTool
- process.env.USER_TYPE === 'ant' — ConfigTool、TungstenTool 等
lazy require(TeamCreateTool 等)打破 tools.ts ↔ 具体工具的循环依赖。读此函数是理解「为什么某环境缺少某工具」的最佳入口。
源码引用: src/tools.ts · 第 193–251 行(共 390 行)
193| export function getAllBaseTools(): Tools {
194| return [
195| AgentTool,
196| TaskOutputTool,
197| BashTool,
198| // Ant-native builds have bfs/ugrep embedded in the bun binary (same ARGV0
199| // trick as ripgrep). When available, find/grep in Claude's shell are aliased
200| // to these fast tools, so the dedicated Glob/Grep tools are unnecessary.
201| ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
202| ExitPlanModeV2Tool,
203| FileReadTool,
204| FileEditTool,
205| FileWriteTool,
206| NotebookEditTool,
207| WebFetchTool,
208| TodoWriteTool,
209| WebSearchTool,
210| TaskStopTool,
211| AskUserQuestionTool,
212| SkillTool,
213| EnterPlanModeTool,
214| ...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
215| ...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),
216| ...(SuggestBackgroundPRTool ? [SuggestBackgroundPRTool] : []),
217| ...(WebBrowserTool ? [WebBrowserTool] : []),
218| ...(isTodoV2Enabled()
219| ? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool]
220| : []),
221| ...(OverflowTestTool ? [OverflowTestTool] : []),
222| ...(CtxInspectTool ? [CtxInspectTool] : []),
223| ...(TerminalCaptureTool ? [TerminalCaptureTool] : []),
224| ...(isEnvTruthy(process.env.ENABLE_LSP_TOOL) ? [LSPTool] : []),
225| ...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
226| getSendMessageTool(),
227| ...(ListPeersTool ? [ListPeersTool] : []),
228| ...(isAgentSwarmsEnabled()
229| ? [getTeamCreateTool(), getTeamDeleteTool()]
230| : []),
231| ...(VerifyPlanExecutionTool ? [VerifyPlanExecutionTool] : []),
232| ...(process.env.USER_TYPE === 'ant' && REPLTool ? [REPLTool] : []),
233| ...(WorkflowTool ? [WorkflowTool] : []),
234| ...(SleepTool ? [SleepTool] : []),
235| ...cronTools,
236| ...(RemoteTriggerTool ? [RemoteTriggerTool] : []),
237| ...(MonitorTool ? [MonitorTool] : []),
238| BriefTool,
239| ...(SendUserFileTool ? [SendUserFileTool] : []),
240| ...(PushNotificationTool ? [PushNotificationTool] : []),
241| ...(SubscribePRTool ? [SubscribePRTool] : []),
242| ...(getPowerShellTool() ? [getPowerShellTool()] : []),
243| ...(SnipTool ? [SnipTool] : []),
244| ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
245| ListMcpResourcesTool,
246| ReadMcpResourceTool,
247| // Include ToolSearchTool when tool search might be enabled (optimistic check)
248| // The actual decision to defer tools happens at request time in claude.ts
249| ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
250| ]
251| }
getTools:运行时过滤
getTools(permissionContext) 在 getAllBaseTools 基础上应用运行时过滤:
- CLAUDE_CODE_SIMPLE — 仅 Bash、Read、Edit(coordinator 模式额外 Agent/TaskStop)
- specialTools 排除 — ListMcpResources、ReadMcpResource、SyntheticOutput
- filterToolsByDenyRules — 整工具 deny(含 MCP server 前缀规则)
- REPL 模式 — REPL 启用时隐藏 REPL_ONLY_TOOLS 原始 primitive
- isEnabled() — 各工具自定义开关
与 getAllBaseTools 的区别:后者用于 preset 列表与 system cache 键;前者用于实际 API 请求与 Executor。getToolsForDefaultPreset 进一步 map isEnabled 得到默认 preset 工具名列表。
源码引用: src/tools.ts · 第 271–327 行(共 390 行)
271| export const getTools = (permissionContext: ToolPermissionContext): Tools => {
272| // Simple mode: only Bash, Read, and Edit tools
273| if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
274| // --bare + REPL mode: REPL wraps Bash/Read/Edit/etc inside the VM, so
275| // return REPL instead of the raw primitives. Matches the non-bare path
276| // below which also hides REPL_ONLY_TOOLS when REPL is enabled.
277| if (isReplModeEnabled() && REPLTool) {
278| const replSimple: Tool[] = [REPLTool]
279| if (
280| feature('COORDINATOR_MODE') &&
281| coordinatorModeModule?.isCoordinatorMode()
282| ) {
283| replSimple.push(TaskStopTool, getSendMessageTool())
284| }
285| return filterToolsByDenyRules(replSimple, permissionContext)
286| }
287| const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
288| // When coordinator mode is also active, include AgentTool and TaskStopTool
289| // so the coordinator gets Task+TaskStop (via useMergedTools filtering) and
290| // workers get Bash/Read/Edit (via filterToolsForAgent filtering).
291| if (
292| feature('COORDINATOR_MODE') &&
293| coordinatorModeModule?.isCoordinatorMode()
294| ) {
295| simpleTools.push(AgentTool, TaskStopTool, getSendMessageTool())
296| }
297| return filterToolsByDenyRules(simpleTools, permissionContext)
298| }
299|
300| // Get all base tools and filter out special tools that get added conditionally
301| const specialTools = new Set([
302| ListMcpResourcesTool.name,
303| ReadMcpResourceTool.name,
304| SYNTHETIC_OUTPUT_TOOL_NAME,
305| ])
306|
307| const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
308|
309| // Filter out tools that are denied by the deny rules
310| let allowedTools = filterToolsByDenyRules(tools, permissionContext)
311|
312| // When REPL mode is enabled, hide primitive tools from direct use.
313| // They're still accessible inside REPL via the VM context.
314| if (isReplModeEnabled()) {
315| const replEnabled = allowedTools.some(tool =>
316| toolMatchesName(tool, REPL_TOOL_NAME),
317| )
318| if (replEnabled) {
319| allowedTools = allowedTools.filter(
320| tool => !REPL_ONLY_TOOLS.has(tool.name),
321| )
322| }
323| }
324|
325| const isEnabled = allowedTools.map(_ => _.isEnabled())
326| return allowedTools.filter((_, i) => isEnabled[i])
327| }
源码引用: src/tools.ts · 第 262–269 行(共 390 行)
262| export function filterToolsByDenyRules<
263| T extends {
264| name: string
265| mcpInfo?: { serverName: string; toolName: string }
266| },
267| >(tools: readonly T[], permissionContext: ToolPermissionContext): T[] {
268| return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
269| }
assembleToolPool 与 getMergedTools
assembleToolPool 是 REPL(useMergedTools)与 runAgent(coordinator worker)的统一工具池组装:
- builtInTools = getTools(permissionContext)
- allowedMcpTools = filterToolsByDenyRules(mcpTools, ...)
- 分区排序:built-in 按 name 排序后 concat MCP 排序结果
- uniqBy name,built-in 优先
为何分区排序: server 的 claude_code_system_cache_policy 在最后一个 matched built-in 后设 cache breakpoint;flat sort 会把 MCP 插入 built-in 中间,破坏 cache 键。
getMergedTools 简单 concat 不去重,用于 token 计数等需要「看见全部 MCP」的场景。
源码引用: src/tools.ts · 第 345–389 行(共 390 行)
345| export function assembleToolPool(
346| permissionContext: ToolPermissionContext,
347| mcpTools: Tools,
348| ): Tools {
349| const builtInTools = getTools(permissionContext)
350|
351| // Filter out MCP tools that are in the deny list
352| const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
353|
354| // Sort each partition for prompt-cache stability, keeping built-ins as a
355| // contiguous prefix. The server's claude_code_system_cache_policy places a
356| // global cache breakpoint after the last prefix-matched built-in tool; a flat
357| // sort would interleave MCP tools into built-ins and invalidate all downstream
358| // cache keys whenever an MCP tool sorts between existing built-ins. uniqBy
359| // preserves insertion order, so built-ins win on name conflict.
360| // Avoid Array.toSorted (Node 20+) — we support Node 18. builtInTools is
361| // readonly so copy-then-sort; allowedMcpTools is a fresh .filter() result.
362| const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
363| return uniqBy(
364| [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
365| 'name',
366| )
367| }
368|
369| /**
370| * Get all tools including both built-in tools and MCP tools.
371| *
372| * This is the preferred function when you need the complete tools list for:
373| * - Tool search threshold calculations (isToolSearchEnabled)
374| * - Token counting that includes MCP tools
375| * - Any context where MCP tools should be considered
376| *
377| * Use getTools() only when you specifically need just built-in tools.
378| *
379| * @param permissionContext - Permission context for filtering built-in tools
380| * @param mcpTools - MCP tools from appState.mcp.tools
381| * @returns Combined array of built-in and MCP tools
382| */
383| export function getMergedTools(
384| permissionContext: ToolPermissionContext,
385| mcpTools: Tools,
386| ): Tools {
387| const builtInTools = getTools(permissionContext)
388| return [...builtInTools, ...mcpTools]
389| }
源码目录与关联文件
强关联:services/tools/toolExecution.ts(runToolUse 编排)、hooks/useCanUseTool.tsx(CanUseToolFn)、constants/tools.ts(Agent 禁用工具列表)。点击 Tool.ts 跳回本章源码块。
动手练习
- 在 getAllBaseTools 中搜索 feature(,列出当前构建会启用的实验工具
- 设置 CLAUDE_CODE_SIMPLE=1,对比 getTools 返回值长度
- 阅读 buildTool 源码,写出新增 Tool 时必须实现的最低方法集
- 追踪 assembleToolPool 的调用方(grep),确认 REPL 与 Agent 使用同一入口
- 对照 Tool 接口上的 backfillObservableInput 注释,理解 prompt cache 与 observable input 的关系
本章小结与延伸
Tool.ts + tools.ts = 插件契约 + 注册表。具体实现见 bash-tool、agent-tool;执行调度见 streaming-executor。 继续学习: