* Execute a tool by name. * * `sessionState` is the CALLER's per-session explore history (CG-17). The * daemon shares one ToolHandler across every connected session, so this state * cannot live on the handler — each session owns one and hands it in, which is * what keeps two sessions
(
toolName: string,
args: Record<string, unknown>,
sessionState?: ExploreSessionState,
)
| 2643 | * Omit it (the CLI does) and explore behaves exactly as before, untracked. |
| 2644 | */ |
| 2645 | async execute( |
| 2646 | toolName: string, |
| 2647 | args: Record<string, unknown>, |
| 2648 | sessionState?: ExploreSessionState, |
| 2649 | ): Promise<ToolResult> { |
| 2650 | if (this.closing) return this.textResult('This MCP session is closing; retry with a connected session.'); |
| 2651 | this.activeCalls++; |
| 2652 | try { |
| 2653 | // Block the first tool call on the engine's post-open reconcile so we |
| 2654 | // never serve rows for files deleted/edited while no MCP server was |
| 2655 | // running. The wait is time-boxed (#905): a huge-repo reconcile takes |
| 2656 | // minutes, and blocking the first call on all of it reads as a hang, so |
| 2657 | // we wait briefly then serve and let it finish in the background. The |
| 2658 | // gate stays installed until reconciliation settles, including on timeout. |
| 2659 | // Catch-up failures are logged by the engine; we proceed regardless so a |
| 2660 | // transient sync error never breaks tools. |
| 2661 | if (this.catchUpGate) { |
| 2662 | const gate = this.catchUpGate; |
| 2663 | await this.awaitCatchUpGate(gate); |
| 2664 | } |
| 2665 | // Honor the optional tool allowlist (CODEGRAPH_MCP_TOOLS): a trimmed |
| 2666 | // surface rejects ablated tools defensively even if a client cached them. |
| 2667 | if (!this.isToolAllowed(toolName)) { |
| 2668 | return this.errorResult(`Tool ${toolName} is disabled via CODEGRAPH_MCP_TOOLS`); |
| 2669 | } |
| 2670 | // Cross-cutting input validation. All tools accept an optional |
| 2671 | // `projectPath` and most accept either `query`, `task`, or |
| 2672 | // `symbol` — bound their lengths centrally so individual handlers |
| 2673 | // can stay focused on tool-specific logic. |
| 2674 | const pathCheck = this.validateOptionalPath(args.projectPath, 'projectPath'); |
| 2675 | if (typeof pathCheck === 'object' && pathCheck !== undefined) { |
| 2676 | return pathCheck; |
| 2677 | } |
| 2678 | // An explicit project gets the same first-call guarantee as the default |
| 2679 | // (#1835): its post-open catch-up sync finishes (time-boxed) before we |
| 2680 | // serve it. Resolved on the main thread so the watcher lives here even |
| 2681 | // when dispatch is off-loaded to a worker. |
| 2682 | if (typeof pathCheck === 'string') { |
| 2683 | await this.awaitProjectGate(pathCheck); |
| 2684 | } |
| 2685 | // The `path` and `pattern` properties used by codegraph_files are |
| 2686 | // also path-shaped — apply the same cap. |
| 2687 | if (args.path !== undefined) { |
| 2688 | const check = this.validateOptionalPath(args.path, 'path'); |
| 2689 | if (typeof check === 'object' && check !== undefined) return check; |
| 2690 | } |
| 2691 | if (args.pattern !== undefined) { |
| 2692 | const check = this.validateOptionalPath(args.pattern, 'pattern'); |
| 2693 | if (typeof check === 'object' && check !== undefined) return check; |
| 2694 | } |
| 2695 | |
| 2696 | const project = await this.getCodeGraph(args.projectPath as string | undefined); |
| 2697 | // Recover a watcher disabled by prolonged lock contention on the next call. |
| 2698 | // The stale banner remains until the watcher finishes its full scan; |
| 2699 | // frequent calls cannot bypass its cooldown (#1959). |
| 2700 | if (project.rearmWatcherAfterLockContention?.()) { |
| 2701 | process.stderr.write('[CodeGraph MCP] Re-armed file watcher; full catch-up pending.\n'); |
| 2702 | } |