ToolExecutorCoordinator: Tool Routing Table
Responsibilities
ToolExecutorCoordinator is Cline's "tool name → handler" routing table. The tool_use block emitted by the LLM carries a name field (such as read_file, write_to_file, use_mcp_tool); the Coordinator looks up the matching handler instance by that name and hands execution over. It does no business logic itself — only registration, lookup, and forwarding.
It sits between ToolExecutor and the concrete handlers. ToolExecutor is the only tool entry point the Task calls (executeTool:212); after performing the generic work — rejection checks, plan-mode restrictions, partial/complete branching, PostToolUse hooks — it delegates the actual execution of a given tool to the Coordinator (coordinator.execute:575). Internally the Coordinator holds a Map<string, IToolHandler> as the registry; if no handler matches, it throws No handler registered for tool.
It also handles one special case: MCP tool-name normalization and the lazy instantiation of dynamic subagent tools. MCP tool names look like mcp__server__tool; any name carrying the CLINE_MCP_TOOL_IDENTIFIER prefix is collapsed to UseMcpToolHandler (mcp normalize:137). Dynamic subagent tool names that have been registered with AgentConfigLoader are wrapped via SharedToolHandler around UseSubagentsToolHandler and injected into the dynamic table (dynamic subagent:146).
Design motivation
- A handler table replaces a giant switch: the older
ToolExecutorwas a switch statement spanning hundreds of lines; every new tool forced an edit to the main file. The Coordinator pulls that switch out intotoolHandlersMap, so adding a tool means writing a handler class and adding one entry to the map (toolHandlersMap:79). - Factory functions, not direct instances: the map stores
(v: ToolValidator) => IToolHandler | undefinedfactories, not instances. A handler instance is only created atregisterByNametime, so the ToolValidator can be injected at construction (registerByName:118). - Returning undefined is a legal value: the factory for
TODO(focus_chain) returnsundefined(todo undefined:97). This signals "this tool name currently has no handler"; the upstreamhascheck treats it as unregistered. - SharedToolHandler reuses an implementation: when multiple tool names share one handler implementation (e.g.
write_to_file,replace_in_file, andnew_ruleall useWriteToFileToolHandler), the wrapper class wraps the base handler and only swaps thenamefield (SharedToolHandler:52). Three identical classes are avoided. - MCP name normalization up front: MCP tool names are dynamic but the behavior is always the same — call an MCP server. At the
getHandlerentry point, every name carryingCLINE_MCP_TOOL_IDENTIFIERis collapsed toMCP_USE, so a single handler covers all MCP tools (mcp normalize:137).
Key files
IToolHandler:34— the handler interface:name,execute,getDescription.IPartialBlockHandler:40— the streaming partial-block interface; handlers implement it optionally.IFullyManagedTool:44— a marker type for handlers that implement both the complete and partial interfaces.SharedToolHandler:52— the wrapper class that lets one handler implementation back multiple tool names.ToolExecutorCoordinator:75— the class definition, holding two maps:handlersanddynamicSubagentHandlers.toolHandlersMap:79— the static registry,ClineDefaultToolenum → factory function.register:114— stores a handler instance in thehandlersmap keyed byname.registerByName:118— given an enum value, pulls the factory from the map, builds the instance, and registers it.has:128— checks whether a tool name is registered; internally just callsgetHandlerand tests for null.getHandler:135— lookup logic: MCP normalization → static table → dynamic subagent.dynamic subagent branch:146— lazy creation ofSharedToolHandler(UseSubagentsToolHandler)for dynamic subagent tools.execute:162— after getting the handler, callshandler.execute(config, block); no try/catch of its own.registerToolHandlers:201— runs through all oftoolUseNamesduring ToolExecutor construction to register every tool.toolUseNames:40— the array of all tool names generated from theClineDefaultToolenum; drives the registration loop.
Data flow
Registration happens once during ToolExecutor construction; execution runs a lookup + execute for each tool_use block. The registration loop is short:
// apps/vscode/src/core/task/ToolExecutor.ts
private registerToolHandlers(): void {
const validator = new ToolValidator(this.clineIgnoreController)
// Register all tools via toolUseNames
for (const tool of toolUseNames) {
this.coordinator.registerByName(tool, validator)
}
}toolUseNames is auto-generated from the ClineDefaultTool enum (toolUseNames:40). Adding an enum value makes it register here automatically, provided the corresponding factory exists in toolHandlersMap. The Validator is constructed with the ClineIgnoreController, since every file/path handler needs it for access checks (ToolValidator:10).
The lookup logic at execution time:
// apps/vscode/src/core/task/tools/ToolExecutorCoordinator.ts
getHandler(toolName: string): IToolHandler | undefined {
// HACK: Normalize MCP tool names to the standard handler
if (toolName.includes(CLINE_MCP_TOOL_IDENTIFIER)) {
toolName = ClineDefaultTool.MCP_USE
}
const staticHandler = this.handlers.get(toolName)
if (staticHandler) {
return staticHandler
}
if (AgentConfigLoader.getInstance().isDynamicSubagentTool(toolName)) {
const existingHandler = this.dynamicSubagentHandlers.get(toolName)
if (existingHandler) {
return existingHandler
}
const handler = new SharedToolHandler(toolName as ClineDefaultTool, new UseSubagentsToolHandler())
this.dynamicSubagentHandlers.set(toolName, handler)
return handler
}
return undefined
}Three-tier lookup: after MCP name normalization, query the static table; on a miss, check whether it is a dynamic subagent tool, and if so wrap a UseSubagentsToolHandler in SharedToolHandler, register it in the dynamic table, and return it; if neither matches, return undefined, and the upstream ToolExecutor.execute treats this as "unregistered" and goes down the legacy branch (has check:316). The dynamic subagent table uses a separate map because its names are runtime strings that cannot belong to the ClineDefaultTool enum.
Boundaries and failure modes
- Throw on unregistered:
executecallsgetHandlerand, if it returns nothing, throwsNo handler registered for tool: ${block.name}(throw on missing:165). In practice the upstreamToolExecutor.executegates this with ahascheck, so by the time this is reached a match is guaranteed; the throw is a safety net. - TODO has no handler: the factory for
ClineDefaultTool.TODO(focus_chain) returnsundefined(todo undefined:97). WhenregisterByNamesees undefined it skips registration — equivalent to "the Coordinator does not own this tool name"; the focus-chain module handles it separately. - MCP name normalization is a hack: the comment says so outright (
hack comment:136). Any MCP tool name containing theCLINE_MCP_TOOL_IDENTIFIERsubstring is normalized — this relies on the server name and tool name themselves never containing that identifier. - Dynamic subagent handlers are never cleaned up: the
dynamicSubagentHandlersmap only grows, never shrinks, and its lifetime matches the Coordinator's (dynamic cache set:152). The Coordinator's lifetime matches the ToolExecutor's, which matches the Task's, so every subagent tool name encountered during a Task's lifetime stays cached. - Each factory call builds a new instance:
registerByNameinvokes the factory each time, building a fresh handler instance (factory call:119). But becausetoolUseNameshas no duplicates, each name is registered exactly once, so there is exactly one instance per name. - SharedToolHandler does not share state:
SharedToolHandlerstores thebaseHandleras a private field and forwards every call to the same base instance (shared execute:62). Sowrite_to_file,replace_in_file, andnew_ruleare in fact the same handler instance, and their internal state is shared.
Summary
The Coordinator decouples tool names from implementations, so adding a tool only requires one map entry and one handler class. Its lookup has three tiers: MCP normalization, the static table, and dynamic subagent. To see what a handler looks like and how handlers are registered, see /tools/handlers-overview; for the permission and parameter checks that run before execution, see /tools/validator.