跳转到内容

Architecture

此内容尚不支持你的语言。

Claude Code’s tool system is a unified abstraction layer that allows 40+ built-in tools, MCP tools, and custom tools to coexist under a single dispatch mechanism. This chapter covers the Tool interface, registration, and runtime filtering.

Every tool in Claude Code implements the Tool type defined in src/Tool.ts. It is one of the largest type definitions in the codebase — over 50 methods and properties — but can be understood in layers:

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>;
};

The three core methods:

  • call: Executes the tool. Receives validated input, the current context, and a progress callback.
  • description: Returns the tool description sent to the API. Can be dynamic based on input and context.
  • prompt: Returns instructions for the model about how to use this 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?
}

These methods control how the tool participates in concurrent execution, permission checks, and interrupt handling.

{
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 flow: validateInputcheckPermissionscanUseTool (user prompt if needed).

{
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;
}

These methods control how the tool appears in the TUI. Each stage of the tool lifecycle has a dedicated renderer.

Rather than requiring every tool to implement all 50+ methods, Claude Code provides buildTool() which fills in safe defaults:

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>;
}

Tools are registered in the app startup flow, assembled into a Tools array (which is 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]

Built-in tools are imported directly from their module files:

// 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 tools are dynamically created at runtime when MCP servers connect (see MCP Integration).

Every tool defines an inputSchema using Zod v4. The schema serves dual purposes:

  1. Runtime validation: Input from the API is validated before execution
  2. API schema generation: Converted to JSON Schema for the tool definition sent to the 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'),
});

Validation happens in 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,
}],
});
}

For MCP tools, the inputJSONSchema field provides the JSON Schema directly (from the MCP server’s tool listing), bypassing Zod conversion.

Tools are looked up by name (or alias) using 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));
}

Aliases support tool renaming without breaking existing conversations — the old name is kept as an alias for backward compatibility.

Not all tools are available in every context. Filtering happens at several levels:

Each tool can self-disable based on runtime conditions:

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

The tools array is filtered before being passed to the 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;
});

When ToolSearch is enabled, tools with shouldDefer: true are sent to the API with defer_loading: true. The model must use the ToolSearch tool to discover them before they can be called:

// 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
}

In plan mode, write tools are filtered out, leaving only read-only tools available.

Tool execution returns a 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>;
};
};

The contextModifier is powerful — it allows a tool to modify the execution context for subsequent tools. For example, the EnterPlanModeTool uses it to switch permission modes.

Each tool specifies maxResultSizeChars to control when results are persisted to disk:

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

When exceeded, the result is saved to a temp file and the model receives a preview with:

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

Every tool receives a ToolUseContext — a rich object containing the entire execution environment:

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
};

Key fields:

  • abortController: For cancellation propagation
  • readFileState: LRU cache of recently read files
  • getAppState/setAppState: Access to the global application state
  • messages: The current conversation history
  • contentReplacementState: Tracks which large results have been persisted