跳转到内容

架构

Claude Code 的 tool 系统是一个统一的抽象层,使 40 余个 builtin tool、MCP tool 以及自定义 tool 能够共存于同一套分发机制之下。本章介绍 Tool interface、注册流程以及运行时过滤。

Claude Code 中每个 tool 都实现了定义在 src/Tool.ts 中的 Tool 类型。这是代码库中体量最大的类型定义之一——超过 50 个方法和属性——但可以分层理解:

src/Tool.ts
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = {
readonly name: string;
readonly inputSchema: Input; // Zod schema for validation
readonly inputJSONSchema?: ToolInputJSONSchema; // Optional JSON Schema override
call(
args: z.infer<Input>,
context: ToolUseContext,
canUseTool: CanUseToolFn,
parentMessage: AssistantMessage,
onProgress?: ToolCallProgress<P>,
): Promise<ToolResult<Output>>;
description(
input: z.infer<Input>,
options: {
isNonInteractiveSession: boolean;
toolPermissionContext: ToolPermissionContext;
tools: Tools;
},
): Promise<string>;
prompt(options: {
getToolPermissionContext: () => Promise<ToolPermissionContext>;
tools: Tools;
agents: AgentDefinition[];
}): Promise<string>;
};

三个核心方法:

  • call:执行 tool。接收经过验证的输入、当前 context 以及进度回调。
  • description:返回发送给 API 的 tool 描述。可根据输入和 context 动态生成。
  • prompt:返回指导模型如何使用该 tool 的说明。
{
isConcurrencySafe(input: z.infer<Input>): boolean; // Can run in parallel?
isEnabled(): boolean; // Available in current config?
isReadOnly(input: z.infer<Input>): boolean; // Side-effect free?
isDestructive?(input: z.infer<Input>): boolean; // Irreversible operation?
interruptBehavior?(): 'cancel' | 'block'; // How to handle user interrupts
requiresUserInteraction?(): boolean; // Needs human input?
}

这些方法控制 tool 如何参与并发执行、permission 检查以及中断处理。

{
checkPermissions(
input: z.infer<Input>,
context: ToolUseContext,
): Promise<PermissionResult>;
validateInput?(
input: z.infer<Input>,
context: ToolUseContext,
): Promise<ValidationResult>;
preparePermissionMatcher?(
input: z.infer<Input>,
): Promise<(pattern: string) => boolean>;
}

Permission 流程:validateInputcheckPermissionscanUseTool(如需则弹出用户提示)。

{
renderToolUseMessage(input: Partial<z.infer<Input>>, options: {...}): React.ReactNode;
renderToolResultMessage?(content: Output, progressMessages: [...], options: {...}): React.ReactNode;
renderToolUseProgressMessage?(progressMessages: [...], options: {...}): React.ReactNode;
renderToolUseRejectedMessage?(input: z.infer<Input>, options: {...}): React.ReactNode;
renderToolUseErrorMessage?(result: ToolResultBlockParam['content'], options: {...}): React.ReactNode;
renderGroupedToolUse?(toolUses: Array<{...}>, options: {...}): React.ReactNode | null;
renderToolUseTag?(input: Partial<z.infer<Input>>): React.ReactNode;
}

这些方法控制 tool 在 TUI 中的呈现方式。tool 生命周期的每个阶段都有专属渲染器。

为避免每个 tool 都需实现全部 50 余个方法,Claude Code 提供了 buildTool(),它会填充安全的默认值:

src/Tool.ts
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?: unknown) => false, // assume not safe
isReadOnly: (_input?: unknown) => false, // assume writes
isDestructive: (_input?: unknown) => false,
checkPermissions: (input) => 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 在应用启动流程中注册,组装为 Tools 数组(即 readonly Tool[]):

graph TD
    A[App Startup] --> B[Build built-in tools]
    A --> C[Connect MCP servers]
    C --> D[Fetch MCP tool lists]
    D --> E[Create MCPTool wrappers]
    B --> F[Merge: built-in + MCP tools]
    E --> F
    F --> G[tools: Tools]
    G --> H[ToolUseContext.options.tools]
    H --> I[queryLoop]

Builtin tool 直接从各自的模块文件导入:

// Conceptual assembly (spread across multiple files)
import { BashTool } from './tools/BashTool/BashTool.js';
import { FileReadTool } from './tools/FileReadTool/FileReadTool.js';
import { FileEditTool } from './tools/FileEditTool/FileEditTool.js';
// ... 40+ more
const builtinTools: Tools = [
BashTool, FileReadTool, FileEditTool, FileWriteTool,
GlobTool, GrepTool, AgentTool, TodoWriteTool,
WebSearchTool, WebFetchTool, NotebookEditTool,
// ...
];

MCP tool 在 MCP 服务器连接时于运行时动态创建(参见 MCP Integration)。

每个 tool 都使用 Zod v4 定义 inputSchema。该 schema 具有双重用途:

  1. 运行时验证:来自 API 的输入在执行前进行验证
  2. API schema 生成:转换为 JSON Schema,作为 tool 定义发送给 API
// Example: FileReadTool input schema
const inputSchema = z.object({
file_path: z.string().describe('The absolute path to the file to read'),
offset: z.number().optional().describe('Line number to start reading from'),
limit: z.number().optional().describe('Number of lines to read'),
});

验证发生在 runToolUse() 中:

src/services/tools/toolExecution.ts
const parsedInput = tool.inputSchema.safeParse(block.input);
if (!parsedInput.success) {
return createUserMessage({
content: [{
type: 'tool_result',
content: `<tool_use_error>Error: Invalid input - ${parsedInput.error}</tool_use_error>`,
is_error: true,
tool_use_id: block.id,
}],
});
}

对于 MCP tool,inputJSONSchema 字段直接提供 JSON Schema(来自 MCP 服务器的 tool 列表),绕过 Zod 转换。

Tool 通过名称(或别名)使用 findToolByName() 进行查找:

src/Tool.ts
export function toolMatchesName(
tool: { name: string; aliases?: string[] },
name: string,
): boolean {
return tool.name === name || (tool.aliases?.includes(name) ?? false);
}
export function findToolByName(tools: Tools, name: string): Tool | undefined {
return tools.find(t => toolMatchesName(t, name));
}

别名支持 tool 重命名而不破坏已有对话——旧名称作为别名保留,以保证向后兼容。

并非所有 tool 在每种 context 下都可用。过滤在多个层面进行:

每个 tool 可根据运行时条件自行禁用:

// Example: LSPTool is only enabled when LSP servers are connected
isEnabled(): boolean {
return getLspClients().length > 0;
}

tools 数组在传递给 API 前会被过滤:

// Tools are filtered based on the query context
const availableTools = tools.filter(tool => {
if (!tool.isEnabled()) return false;
// Additional context-specific filters
return true;
});

ToolSearch 启用时,带有 shouldDefer: true 的 tool 会以 defer_loading: true 发送给 API。模型必须先使用 ToolSearch tool 发现它们,才能调用:

// Tool definition
{
name: 'NotebookEdit',
shouldDefer: true, // deferred by default
searchHint: 'jupyter', // keyword for ToolSearch matching
}
// Tools with alwaysLoad override deferral
{
name: 'mcp__server__critical_tool',
alwaysLoad: true, // always sent in full
}

在计划模式下,写入类 tool 会被过滤掉,仅保留只读 tool 可用。

Tool 执行返回 ToolResult

export type ToolResult<T> = {
data: T; // The tool's output
newMessages?: Message[]; // Additional messages to inject
contextModifier?: (context: ToolUseContext) => ToolUseContext; // Modify context for next turn
mcpMeta?: { // MCP protocol metadata
_meta?: Record<string, unknown>;
structuredContent?: Record<string, unknown>;
};
};

contextModifier 功能强大——它允许 tool 修改后续 tool 的执行 context。例如,EnterPlanModeTool 利用它来切换 permission 模式。

每个 tool 通过 maxResultSizeChars 指定何时将结果持久化到磁盘:

{
name: 'Bash',
maxResultSizeChars: 30_000, // persist results > 30K chars
}
{
name: 'Read',
maxResultSizeChars: Infinity, // never persist (self-bounded)
}

超出限制时,结果保存到临时文件,模型收到包含以下内容的预览:

[Full output saved to /tmp/.../result.txt]
<preview>first 1000 chars...</preview>

每个 tool 都接收一个 ToolUseContext——包含整个执行环境的富对象:

export type ToolUseContext = {
options: {
commands: Command[];
debug: boolean;
mainLoopModel: string;
tools: Tools;
verbose: boolean;
thinkingConfig: ThinkingConfig;
mcpClients: MCPServerConnection[];
mcpResources: Record<string, ServerResource[]>;
isNonInteractiveSession: boolean;
agentDefinitions: AgentDefinitionsResult;
};
abortController: AbortController;
readFileState: FileStateCache;
getAppState(): AppState;
setAppState(f: (prev: AppState) => AppState): void;
messages: Message[];
// ... 30+ more fields
};

关键字段:

  • abortController:用于取消传播
  • readFileState:最近读取文件的 LRU cache
  • getAppState/setAppState:访问全局应用状态
  • messages:当前对话历史
  • contentReplacementState:追踪哪些大型结果已被持久化