* Execute a tool by name
(toolName: string, args: Record<string, unknown>)
| 1352 | * Execute a tool by name |
| 1353 | */ |
| 1354 | async execute(toolName: string, args: Record<string, unknown>): Promise<ToolResult> { |
| 1355 | try { |
| 1356 | // Block the first tool call on the engine's post-open reconcile so we |
| 1357 | // never serve rows for files deleted/edited while no MCP server was |
| 1358 | // running. The wait is time-boxed (#905): a huge-repo reconcile takes |
| 1359 | // minutes, and blocking the first call on all of it reads as a hang, so |
| 1360 | // we wait briefly then serve and let it finish in the background. The |
| 1361 | // gate is cleared after first await — subsequent calls pay nothing. |
| 1362 | // Catch-up failures are logged by the engine; we proceed regardless so a |
| 1363 | // transient sync error never breaks tools. |
| 1364 | if (this.catchUpGate) { |
| 1365 | const gate = this.catchUpGate; |
| 1366 | this.catchUpGate = null; |
| 1367 | await this.awaitCatchUpGate(gate); |
| 1368 | } |
| 1369 | // Honor the optional tool allowlist (CODEGRAPH_MCP_TOOLS): a trimmed |
| 1370 | // surface rejects ablated tools defensively even if a client cached them. |
| 1371 | if (!this.isToolAllowed(toolName)) { |
| 1372 | return this.errorResult(`Tool ${toolName} is disabled via CODEGRAPH_MCP_TOOLS`); |
| 1373 | } |
| 1374 | // Cross-cutting input validation. All tools accept an optional |
| 1375 | // `projectPath` and most accept either `query`, `task`, or |
| 1376 | // `symbol` — bound their lengths centrally so individual handlers |
| 1377 | // can stay focused on tool-specific logic. |
| 1378 | const pathCheck = this.validateOptionalPath(args.projectPath, 'projectPath'); |
| 1379 | if (typeof pathCheck === 'object' && pathCheck !== undefined) { |
| 1380 | return pathCheck; |
| 1381 | } |
| 1382 | // The `path` and `pattern` properties used by codegraph_files are |
| 1383 | // also path-shaped — apply the same cap. |
| 1384 | if (args.path !== undefined) { |
| 1385 | const check = this.validateOptionalPath(args.path, 'path'); |
| 1386 | if (typeof check === 'object' && check !== undefined) return check; |
| 1387 | } |
| 1388 | if (args.pattern !== undefined) { |
| 1389 | const check = this.validateOptionalPath(args.pattern, 'pattern'); |
| 1390 | if (typeof check === 'object' && check !== undefined) return check; |
| 1391 | } |
| 1392 | |
| 1393 | // codegraph_status reports watcher state (pending files, degraded mode, |
| 1394 | // worktree warning) and embeds its own sections — it must run on the MAIN |
| 1395 | // thread against the watched default instance, so it is NEVER off-loaded to |
| 1396 | // a worker (whose read connection has no watcher). It also skips the |
| 1397 | // auto-banner wrapper to avoid duplicating its own pending-files section. |
| 1398 | if (toolName === 'codegraph_status') { |
| 1399 | return await this.handleStatus(args); |
| 1400 | } |
| 1401 | |
| 1402 | // Read tools: off-load the CPU-heavy dispatch to the worker pool when one |
| 1403 | // is attached, healthy, AND has finished its first cold start (daemon |
| 1404 | // mode), so the daemon's single event loop stays free for the MCP |
| 1405 | // transport under concurrent load — otherwise N concurrent explores |
| 1406 | // serialize AND starve the transport until the whole batch drains |
| 1407 | // (clients then time out). Before the first worker is warm, calls run |
| 1408 | // in-process: a call queued behind a cold start sat invisible until the |
| 1409 | // 45s busy backstop — the daemon's first tool call stalling for however |
| 1410 | // long a worker spawn takes on a loaded machine (the #662 flake). With |
| 1411 | // no pool (direct mode) or a degraded one, dispatch runs in-process |