* 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,
)
| 1952 | * Omit it (the CLI does) and explore behaves exactly as before, untracked. |
| 1953 | */ |
| 1954 | async execute( |
| 1955 | toolName: string, |
| 1956 | args: Record<string, unknown>, |
| 1957 | sessionState?: ExploreSessionState, |
| 1958 | ): Promise<ToolResult> { |
| 1959 | try { |
| 1960 | // Block the first tool call on the engine's post-open reconcile so we |
| 1961 | // never serve rows for files deleted/edited while no MCP server was |
| 1962 | // running. The wait is time-boxed (#905): a huge-repo reconcile takes |
| 1963 | // minutes, and blocking the first call on all of it reads as a hang, so |
| 1964 | // we wait briefly then serve and let it finish in the background. The |
| 1965 | // gate is cleared after first await — subsequent calls pay nothing. |
| 1966 | // Catch-up failures are logged by the engine; we proceed regardless so a |
| 1967 | // transient sync error never breaks tools. |
| 1968 | if (this.catchUpGate) { |
| 1969 | const gate = this.catchUpGate; |
| 1970 | this.catchUpGate = null; |
| 1971 | await this.awaitCatchUpGate(gate); |
| 1972 | } |
| 1973 | // Honor the optional tool allowlist (CODEGRAPH_MCP_TOOLS): a trimmed |
| 1974 | // surface rejects ablated tools defensively even if a client cached them. |
| 1975 | if (!this.isToolAllowed(toolName)) { |
| 1976 | return this.errorResult(`Tool ${toolName} is disabled via CODEGRAPH_MCP_TOOLS`); |
| 1977 | } |
| 1978 | // Cross-cutting input validation. All tools accept an optional |
| 1979 | // `projectPath` and most accept either `query`, `task`, or |
| 1980 | // `symbol` — bound their lengths centrally so individual handlers |
| 1981 | // can stay focused on tool-specific logic. |
| 1982 | const pathCheck = this.validateOptionalPath(args.projectPath, 'projectPath'); |
| 1983 | if (typeof pathCheck === 'object' && pathCheck !== undefined) { |
| 1984 | return pathCheck; |
| 1985 | } |
| 1986 | // The `path` and `pattern` properties used by codegraph_files are |
| 1987 | // also path-shaped — apply the same cap. |
| 1988 | if (args.path !== undefined) { |
| 1989 | const check = this.validateOptionalPath(args.path, 'path'); |
| 1990 | if (typeof check === 'object' && check !== undefined) return check; |
| 1991 | } |
| 1992 | if (args.pattern !== undefined) { |
| 1993 | const check = this.validateOptionalPath(args.pattern, 'pattern'); |
| 1994 | if (typeof check === 'object' && check !== undefined) return check; |
| 1995 | } |
| 1996 | |
| 1997 | // codegraph_status reports watcher state (pending files, degraded mode, |
| 1998 | // worktree warning) and embeds its own sections — it must run on the MAIN |
| 1999 | // thread against the watched default instance, so it is NEVER off-loaded to |
| 2000 | // a worker (whose read connection has no watcher). It also skips the |
| 2001 | // auto-banner wrapper to avoid duplicating its own pending-files section. |
| 2002 | if (toolName === 'codegraph_status') { |
| 2003 | return await this.handleStatus(args); |
| 2004 | } |
| 2005 | |
| 2006 | // Read tools: off-load the CPU-heavy dispatch to the worker pool when one |
| 2007 | // is attached, healthy, AND has finished its first cold start (daemon |
| 2008 | // mode), so the daemon's single event loop stays free for the MCP |
| 2009 | // transport under concurrent load — otherwise N concurrent explores |
| 2010 | // serialize AND starve the transport until the whole batch drains |
| 2011 | // (clients then time out). Before the first worker is warm, calls run |