* Execute a tool by name
(toolName: string, args: Record<string, unknown>)
| 1424 | * Execute a tool by name |
| 1425 | */ |
| 1426 | async execute(toolName: string, args: Record<string, unknown>): Promise<ToolResult> { |
| 1427 | try { |
| 1428 | // Block the first tool call on the engine's post-open reconcile so we |
| 1429 | // never serve rows for files deleted/edited while no MCP server was |
| 1430 | // running. The wait is time-boxed (#905): a huge-repo reconcile takes |
| 1431 | // minutes, and blocking the first call on all of it reads as a hang, so |
| 1432 | // we wait briefly then serve and let it finish in the background. The |
| 1433 | // gate is cleared after first await — subsequent calls pay nothing. |
| 1434 | // Catch-up failures are logged by the engine; we proceed regardless so a |
| 1435 | // transient sync error never breaks tools. |
| 1436 | if (this.catchUpGate) { |
| 1437 | const gate = this.catchUpGate; |
| 1438 | this.catchUpGate = null; |
| 1439 | await this.awaitCatchUpGate(gate); |
| 1440 | } |
| 1441 | // Honor the optional tool allowlist (CODEGRAPH_MCP_TOOLS): a trimmed |
| 1442 | // surface rejects ablated tools defensively even if a client cached them. |
| 1443 | if (!this.isToolAllowed(toolName)) { |
| 1444 | return this.errorResult(`Tool ${toolName} is disabled via CODEGRAPH_MCP_TOOLS`); |
| 1445 | } |
| 1446 | // Cross-cutting input validation. All tools accept an optional |
| 1447 | // `projectPath` and most accept either `query`, `task`, or |
| 1448 | // `symbol` — bound their lengths centrally so individual handlers |
| 1449 | // can stay focused on tool-specific logic. |
| 1450 | const pathCheck = this.validateOptionalPath(args.projectPath, 'projectPath'); |
| 1451 | if (typeof pathCheck === 'object' && pathCheck !== undefined) { |
| 1452 | return pathCheck; |
| 1453 | } |
| 1454 | // The `path` and `pattern` properties used by codegraph_files are |
| 1455 | // also path-shaped — apply the same cap. |
| 1456 | if (args.path !== undefined) { |
| 1457 | const check = this.validateOptionalPath(args.path, 'path'); |
| 1458 | if (typeof check === 'object' && check !== undefined) return check; |
| 1459 | } |
| 1460 | if (args.pattern !== undefined) { |
| 1461 | const check = this.validateOptionalPath(args.pattern, 'pattern'); |
| 1462 | if (typeof check === 'object' && check !== undefined) return check; |
| 1463 | } |
| 1464 | |
| 1465 | // codegraph_status reports watcher state (pending files, degraded mode, |
| 1466 | // worktree warning) and embeds its own sections — it must run on the MAIN |
| 1467 | // thread against the watched default instance, so it is NEVER off-loaded to |
| 1468 | // a worker (whose read connection has no watcher). It also skips the |
| 1469 | // auto-banner wrapper to avoid duplicating its own pending-files section. |
| 1470 | if (toolName === 'codegraph_status') { |
| 1471 | return await this.handleStatus(args); |
| 1472 | } |
| 1473 | |
| 1474 | // Read tools: off-load the CPU-heavy dispatch to the worker pool when one |
| 1475 | // is attached, healthy, AND has finished its first cold start (daemon |
| 1476 | // mode), so the daemon's single event loop stays free for the MCP |
| 1477 | // transport under concurrent load — otherwise N concurrent explores |
| 1478 | // serialize AND starve the transport until the whole batch drains |
| 1479 | // (clients then time out). Before the first worker is warm, calls run |
| 1480 | // in-process: a call queued behind a cold start sat invisible until the |
| 1481 | // 45s busy backstop — the daemon's first tool call stalling for however |
| 1482 | // long a worker spawn takes on a loaded machine (the #662 flake). With |
| 1483 | // no pool (direct mode) or a degraded one, dispatch runs in-process |