* Execute a tool by name
(toolName: string, args: Record<string, unknown>)
| 1322 | * Execute a tool by name |
| 1323 | */ |
| 1324 | async execute(toolName: string, args: Record<string, unknown>): Promise<ToolResult> { |
| 1325 | try { |
| 1326 | // Block the first tool call on the engine's post-open reconcile so we |
| 1327 | // never serve rows for files deleted/edited while no MCP server was |
| 1328 | // running. The wait is time-boxed (#905): a huge-repo reconcile takes |
| 1329 | // minutes, and blocking the first call on all of it reads as a hang, so |
| 1330 | // we wait briefly then serve and let it finish in the background. The |
| 1331 | // gate is cleared after first await — subsequent calls pay nothing. |
| 1332 | // Catch-up failures are logged by the engine; we proceed regardless so a |
| 1333 | // transient sync error never breaks tools. |
| 1334 | if (this.catchUpGate) { |
| 1335 | const gate = this.catchUpGate; |
| 1336 | this.catchUpGate = null; |
| 1337 | await this.awaitCatchUpGate(gate); |
| 1338 | } |
| 1339 | // Honor the optional tool allowlist (CODEGRAPH_MCP_TOOLS): a trimmed |
| 1340 | // surface rejects ablated tools defensively even if a client cached them. |
| 1341 | if (!this.isToolAllowed(toolName)) { |
| 1342 | return this.errorResult(`Tool ${toolName} is disabled via CODEGRAPH_MCP_TOOLS`); |
| 1343 | } |
| 1344 | // Cross-cutting input validation. All tools accept an optional |
| 1345 | // `projectPath` and most accept either `query`, `task`, or |
| 1346 | // `symbol` — bound their lengths centrally so individual handlers |
| 1347 | // can stay focused on tool-specific logic. |
| 1348 | const pathCheck = this.validateOptionalPath(args.projectPath, 'projectPath'); |
| 1349 | if (typeof pathCheck === 'object' && pathCheck !== undefined) { |
| 1350 | return pathCheck; |
| 1351 | } |
| 1352 | // The `path` and `pattern` properties used by codegraph_files are |
| 1353 | // also path-shaped — apply the same cap. |
| 1354 | if (args.path !== undefined) { |
| 1355 | const check = this.validateOptionalPath(args.path, 'path'); |
| 1356 | if (typeof check === 'object' && check !== undefined) return check; |
| 1357 | } |
| 1358 | if (args.pattern !== undefined) { |
| 1359 | const check = this.validateOptionalPath(args.pattern, 'pattern'); |
| 1360 | if (typeof check === 'object' && check !== undefined) return check; |
| 1361 | } |
| 1362 | |
| 1363 | // codegraph_status reports watcher state (pending files, degraded mode, |
| 1364 | // worktree warning) and embeds its own sections — it must run on the MAIN |
| 1365 | // thread against the watched default instance, so it is NEVER off-loaded to |
| 1366 | // a worker (whose read connection has no watcher). It also skips the |
| 1367 | // auto-banner wrapper to avoid duplicating its own pending-files section. |
| 1368 | if (toolName === 'codegraph_status') { |
| 1369 | return await this.handleStatus(args); |
| 1370 | } |
| 1371 | |
| 1372 | // Read tools: off-load the CPU-heavy dispatch to the worker pool when one |
| 1373 | // is attached and healthy (daemon mode), so the daemon's single event loop |
| 1374 | // stays free for the MCP transport under concurrent load — otherwise N |
| 1375 | // concurrent explores serialize AND starve the transport until the whole |
| 1376 | // batch drains (clients then time out). With no pool (direct mode) or a |
| 1377 | // degraded one, dispatch runs in-process exactly as before. Either way the |
| 1378 | // result flows through the cross-cutting notices — worktree-index mismatch |
| 1379 | // (#155) and per-file staleness (#403) — which need the watched MAIN |
| 1380 | // instance and so are always applied here, never in the worker. |
| 1381 | const result = (this.queryPool && this.queryPool.healthy) |