Skip to content

ToolExecutorCoordinator: Tool Routing Table

源码版本v4.0.10

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 ToolExecutor was a switch statement spanning hundreds of lines; every new tool forced an edit to the main file. The Coordinator pulls that switch out into toolHandlersMap, 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 | undefined factories, not instances. A handler instance is only created at registerByName time, so the ToolValidator can be injected at construction (registerByName:118).
  • Returning undefined is a legal value: the factory for TODO (focus_chain) returns undefined (todo undefined:97). This signals "this tool name currently has no handler"; the upstream has check treats it as unregistered.
  • SharedToolHandler reuses an implementation: when multiple tool names share one handler implementation (e.g. write_to_file, replace_in_file, and new_rule all use WriteToFileToolHandler), the wrapper class wraps the base handler and only swaps the name field (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 getHandler entry point, every name carrying CLINE_MCP_TOOL_IDENTIFIER is collapsed to MCP_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: handlers and dynamicSubagentHandlers.
  • toolHandlersMap:79 — the static registry, ClineDefaultTool enum → factory function.
  • register:114 — stores a handler instance in the handlers map keyed by name.
  • 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 calls getHandler and tests for null.
  • getHandler:135 — lookup logic: MCP normalization → static table → dynamic subagent.
  • dynamic subagent branch:146 — lazy creation of SharedToolHandler(UseSubagentsToolHandler) for dynamic subagent tools.
  • execute:162 — after getting the handler, calls handler.execute(config, block); no try/catch of its own.
  • registerToolHandlers:201 — runs through all of toolUseNames during ToolExecutor construction to register every tool.
  • toolUseNames:40 — the array of all tool names generated from the ClineDefaultTool enum; 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:

typescript
// 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:

typescript
// 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: execute calls getHandler and, if it returns nothing, throws No handler registered for tool: ${block.name} (throw on missing:165). In practice the upstream ToolExecutor.execute gates this with a has check, 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) returns undefined (todo undefined:97). When registerByName sees 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 the CLINE_MCP_TOOL_IDENTIFIER substring is normalized — this relies on the server name and tool name themselves never containing that identifier.
  • Dynamic subagent handlers are never cleaned up: the dynamicSubagentHandlers map 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: registerByName invokes the factory each time, building a fresh handler instance (factory call:119). But because toolUseNames has no duplicates, each name is registered exactly once, so there is exactly one instance per name.
  • SharedToolHandler does not share state: SharedToolHandler stores the baseHandler as a private field and forwards every call to the same base instance (shared execute:62). So write_to_file, replace_in_file, and new_rule are 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.

See official docs: Cline 文档 · README