codesearch-mcp 0.2.1-alpha → 0.2.3-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +15 -8
  3. package/dist/app.js +45 -7
  4. package/dist/src/commands/EvalCommand.d.ts +10 -0
  5. package/dist/src/commands/EvalCommand.js +231 -0
  6. package/dist/src/commands/IndexCommand.js +79 -53
  7. package/dist/src/db/index.d.ts +1 -1
  8. package/dist/src/db/index.js +1 -2
  9. package/dist/src/db/schema.d.ts +5 -29
  10. package/dist/src/db/schema.js +2 -8
  11. package/dist/src/dependency-traversal.d.ts +16 -0
  12. package/dist/src/dependency-traversal.js +45 -0
  13. package/dist/src/enriched-prompt.d.ts +3 -2
  14. package/dist/src/enriched-prompt.js +3 -2
  15. package/dist/src/eval-results.d.ts +12 -0
  16. package/dist/src/eval-results.js +114 -0
  17. package/dist/src/file-utils.d.ts +15 -0
  18. package/dist/src/file-utils.js +24 -1
  19. package/dist/src/help.js +4 -1
  20. package/dist/src/interfaces/IAstParser.d.ts +13 -1
  21. package/dist/src/interfaces/IDatabase.d.ts +0 -14
  22. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +94 -0
  23. package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
  24. package/dist/src/interfaces/ISearcher.d.ts +11 -16
  25. package/dist/src/interfaces/index.d.ts +1 -0
  26. package/dist/src/project-scope.d.ts +19 -0
  27. package/dist/src/project-scope.js +49 -0
  28. package/dist/src/project-selector.d.ts +6 -0
  29. package/dist/src/project-selector.js +18 -0
  30. package/dist/src/retrieval-eval.d.ts +16 -0
  31. package/dist/src/retrieval-eval.js +214 -0
  32. package/dist/src/services/AstParserService.d.ts +66 -1
  33. package/dist/src/services/AstParserService.js +432 -28
  34. package/dist/src/services/DatabaseService.d.ts +0 -12
  35. package/dist/src/services/DatabaseService.js +2 -52
  36. package/dist/src/services/IndexerService.d.ts +8 -0
  37. package/dist/src/services/IndexerService.js +24 -11
  38. package/dist/src/services/McpServer.d.ts +7 -0
  39. package/dist/src/services/McpServer.js +42 -38
  40. package/dist/src/services/OpenAICompatibleEmbedder.js +2 -1
  41. package/dist/src/services/SearcherService.d.ts +189 -25
  42. package/dist/src/services/SearcherService.js +505 -203
  43. package/dist/src/symbol-rows.d.ts +14 -9
  44. package/dist/src/symbol-rows.js +17 -11
  45. package/dist/src/tools/FindSymbolTool.js +14 -5
  46. package/dist/src/tools/GetCallersTool.d.ts +3 -1
  47. package/dist/src/tools/GetCallersTool.js +16 -6
  48. package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
  49. package/dist/src/tools/GetDependenciesTool.js +15 -8
  50. package/dist/src/tools/GetDependentsTool.d.ts +3 -1
  51. package/dist/src/tools/GetDependentsTool.js +31 -16
  52. package/dist/src/tools/ListFilesTool.d.ts +3 -1
  53. package/dist/src/tools/ListFilesTool.js +14 -12
  54. package/dist/src/tools/ListProjectsTool.js +5 -1
  55. package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
  56. package/dist/src/tools/ReadFileRangeTool.js +12 -11
  57. package/dist/src/tools/ReadFileTool.d.ts +3 -1
  58. package/dist/src/tools/ReadFileTool.js +12 -10
  59. package/dist/src/tools/SearchCodebaseTool.js +3 -1
  60. package/dist/src/tools/format-helpers.d.ts +16 -1
  61. package/dist/src/tools/format-helpers.js +64 -0
  62. package/package.json +9 -8
@@ -33,14 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DatabaseService = exports.CURRENT_SYMBOL_INDEX_VERSION = void 0;
36
+ exports.DatabaseService = void 0;
37
37
  const node_postgres_1 = require("drizzle-orm/node-postgres");
38
38
  const pg_1 = require("pg");
39
39
  const drizzle_orm_1 = require("drizzle-orm");
40
40
  const schema = __importStar(require("../db/schema"));
41
41
  const db_1 = require("../db");
42
42
  const MIN_PG_VERSION_NUM = 150000;
43
- exports.CURRENT_SYMBOL_INDEX_VERSION = 2;
44
43
  class DatabaseService {
45
44
  config;
46
45
  pool;
@@ -140,6 +139,7 @@ class DatabaseService {
140
139
  await this.query(`CREATE INDEX IF NOT EXISTS idx_symbol_file ON code_symbol (project, "filePath")`);
141
140
  await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS container TEXT`);
142
141
  await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "isStatic" BOOLEAN NOT NULL DEFAULT false`);
142
+ await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "target" TEXT`);
143
143
  // DDL is transactional in PostgreSQL — either both statements apply or
144
144
  // neither does, so a failure here never leaves code_symbol without a
145
145
  // unique index (the old idx_symbol_unique is dropped only once the new
@@ -149,12 +149,6 @@ class DatabaseService {
149
149
  await tx.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_symbol_unique_v2 ' +
150
150
  'ON code_symbol (project, "filePath", container, name, kind, line) NULLS NOT DISTINCT');
151
151
  });
152
- await this.query(`
153
- CREATE TABLE IF NOT EXISTS symbol_index_version (
154
- project TEXT PRIMARY KEY,
155
- version INT NOT NULL
156
- )
157
- `);
158
152
  // code_call table
159
153
  await this.query(`
160
154
  CREATE TABLE IF NOT EXISTS code_call (
@@ -268,50 +262,6 @@ class DatabaseService {
268
262
  await tx.delete(db_1.codeDependency).where((0, drizzle_orm_1.eq)(db_1.codeDependency.project, name));
269
263
  });
270
264
  }
271
- async listProjectsWithIndexRows() {
272
- const rows = await this.query(`SELECT DISTINCT name FROM (
273
- SELECT "serviceName" AS name FROM code_chunk
274
- UNION
275
- SELECT project AS name FROM code_symbol
276
- ) t`);
277
- return rows.map(r => r.name);
278
- }
279
- /**
280
- * Among `projectNames` (or every project with rows when null), those with at
281
- * least one row in `code_chunk`/`code_symbol` and a `symbol_index_version`
282
- * below `CURRENT_SYMBOL_INDEX_VERSION` (a missing row counts as version 1).
283
- * A missing `symbol_index_version` table (un-migrated DB, `rag-mcp`/`rag-web`
284
- * bootstrapped with `initDb`) counts as "no marker rows" — every project with
285
- * rows is then outdated. Any other database error propagates.
286
- */
287
- async listOutdatedProjects(projectNames) {
288
- const rowsUnion = `
289
- SELECT "serviceName" AS name FROM code_chunk
290
- UNION
291
- SELECT project AS name FROM code_symbol
292
- `;
293
- const filterSql = projectNames ? 'AND t.name = ANY($1)' : '';
294
- const params = projectNames ? [projectNames] : [];
295
- try {
296
- const rows = await this.query(`SELECT DISTINCT t.name FROM (${rowsUnion}) t
297
- WHERE NOT EXISTS (
298
- SELECT 1 FROM symbol_index_version v
299
- WHERE v.project = t.name AND v.version >= ${exports.CURRENT_SYMBOL_INDEX_VERSION}
300
- )
301
- ${filterSql}`, params);
302
- return rows.map(r => r.name);
303
- }
304
- catch (err) {
305
- if (!/relation "symbol_index_version" does not exist/i.test(err.message))
306
- throw err;
307
- const rows = await this.query(`SELECT DISTINCT t.name FROM (${rowsUnion}) t ${projectNames ? 'WHERE t.name = ANY($1)' : ''}`, params);
308
- return rows.map(r => r.name);
309
- }
310
- }
311
- async upsertSymbolIndexVersion(project) {
312
- await this.query(`INSERT INTO symbol_index_version (project, version) VALUES ($1, $2)
313
- ON CONFLICT (project) DO UPDATE SET version = EXCLUDED.version`, [project, exports.CURRENT_SYMBOL_INDEX_VERSION]);
314
- }
315
265
  /** Guard against SQL injection — the dimension is interpolated into DDL. */
316
266
  assertDim(dimensions) {
317
267
  if (!Number.isInteger(dimensions) || dimensions <= 0 || dimensions > 2000) {
@@ -3,6 +3,7 @@ import type { IEmbedder } from '../interfaces/IEmbedder';
3
3
  import type { IAstParser } from '../interfaces/IAstParser';
4
4
  import type { IChunker } from '../interfaces/IChunker';
5
5
  import type { IDatabase } from '../interfaces/IDatabase';
6
+ import type { DiscoveredProject } from '../interfaces/IProjectDiscovery';
6
7
  import type { ILogger } from '../interfaces/ILogger';
7
8
  import type { IndexMeta } from '../interfaces/IDatabase';
8
9
  export interface IndexStats {
@@ -39,6 +40,13 @@ export declare class IndexerService {
39
40
  scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal, preCollectedFiles?: string[], opts?: {
40
41
  rootTsConfigPath?: string;
41
42
  }): Promise<IndexStats>;
43
+ /**
44
+ * Scan several leaf projects in one call, each with its own `rootTsConfigPath`
45
+ * (a unit's children may belong to different monorepos). No pruning, no
46
+ * console output — callers (CLI or a future hook) own reporting. A rejecting
47
+ * `scanAndIndex` call stops the loop before the next target and propagates.
48
+ */
49
+ scanProjects(targets: DiscoveredProject[], force: boolean, signal: AbortSignal, filesPerProject: string[][]): Promise<IndexStats[]>;
42
50
  pruneProjects(keep: string[]): Promise<{
43
51
  removed: string[];
44
52
  failed: string[];
@@ -254,12 +254,6 @@ class IndexerService {
254
254
  }
255
255
  }
256
256
  this.ast.clearCache(dir);
257
- // The marker is written exactly when the scan reaches here without having
258
- // been interrupted — never on the early-return abort path above, and never
259
- // when anything in this method throws (the error propagates instead).
260
- if (!signal?.aborted) {
261
- await this.db.upsertSymbolIndexVersion(serviceName);
262
- }
263
257
  stats.elapsedSec = Math.round((Date.now() - startTime) / 100) / 10;
264
258
  this.logger.debug('IndexerService: project indexed', {
265
259
  project: serviceName,
@@ -271,6 +265,22 @@ class IndexerService {
271
265
  });
272
266
  return stats;
273
267
  }
268
+ /**
269
+ * Scan several leaf projects in one call, each with its own `rootTsConfigPath`
270
+ * (a unit's children may belong to different monorepos). No pruning, no
271
+ * console output — callers (CLI or a future hook) own reporting. A rejecting
272
+ * `scanAndIndex` call stops the loop before the next target and propagates.
273
+ */
274
+ async scanProjects(targets, force, signal, filesPerProject) {
275
+ const allStats = [];
276
+ for (let i = 0; i < targets.length; i++) {
277
+ if (signal.aborted)
278
+ break;
279
+ const stats = await this.scanAndIndex(targets[i].path, targets[i].name, force, signal, filesPerProject[i], { rootTsConfigPath: targets[i].rootTsConfigPath ?? undefined });
280
+ allStats.push(stats);
281
+ }
282
+ return allStats;
283
+ }
274
284
  async pruneProjects(keep) {
275
285
  if (keep.length === 0)
276
286
  return { removed: [], failed: [] };
@@ -315,7 +325,7 @@ class IndexerService {
315
325
  return { status: 'skipped', chunks: 0, pendingMtimeRefresh: relFilePath };
316
326
  }
317
327
  const tAst = Date.now();
318
- const { edges: rawEdges, symbols: rawSymbols, members: rawMembers = [], calls: rawCalls, ownerRanges: rawOwnerRanges = [], } = analysis ?? { edges: [], symbols: [], calls: [] };
328
+ const { edges: rawEdges, symbols: rawSymbols, members: rawMembers = [], calls: rawCalls, ownerRanges: rawOwnerRanges = [], graphqlNames: rawGraphqlNames = [], } = analysis ?? { edges: [], symbols: [], calls: [] };
319
329
  // astMs is near-zero — actual AST time is spent upfront in analyzeProject()
320
330
  // Normalize edges to workspace-relative paths
321
331
  const edges = rawEdges
@@ -338,9 +348,12 @@ class IndexerService {
338
348
  line: c.line,
339
349
  col: c.col,
340
350
  }));
341
- // symbols ∪ members → code_symbol rows: overload sets collapse to their
342
- // representative row, then a final dedup by (container, name, kind, line).
343
- const symbolRows = (0, symbol_rows_1.buildSymbolRows)(rawSymbols, rawMembers).map(s => ({ ...s, filePath: relFilePath }));
351
+ // symbols ∪ members ∪ graphqlNames → code_symbol rows: overload sets
352
+ // collapse to their representative row (graphqlNames bypass the
353
+ // collapse), then a final dedup by (container, name, kind, line).
354
+ const symbolRows = (0, symbol_rows_1.buildSymbolRows)(rawSymbols, rawMembers, rawGraphqlNames).map(s => ({ ...s, filePath: relFilePath }));
355
+ // Side-effect imports bind no name, so they stay out of the Dependencies: line.
356
+ const promptEdges = edges.filter(e => e.depType !== 'import-side-effect');
344
357
  const astMs = Date.now() - tAst;
345
358
  const chunks = await this.chunker.chunk(code, {
346
359
  strategy: this.config.indexer.chunkStrategy,
@@ -357,7 +370,7 @@ class IndexerService {
357
370
  const embedBatchSize = this.config.embedding.batchSize;
358
371
  for (let i = 0; i < chunks.length; i += embedBatchSize) {
359
372
  const batch = chunks.slice(i, i + embedBatchSize);
360
- const prompts = batch.map(piece => (0, enriched_prompt_1.getEnrichedPrompt)(piece, relPath, serviceName, edges, rawSymbols));
373
+ const prompts = batch.map(piece => (0, enriched_prompt_1.getEnrichedPrompt)(piece, relPath, serviceName, promptEdges, rawSymbols));
361
374
  const vectors = await this.embedder.embedBatch(prompts, 'document');
362
375
  for (let j = 0; j < batch.length; j++) {
363
376
  const piece = batch[j];
@@ -1,6 +1,13 @@
1
1
  import type { Config } from '../config';
2
2
  import type { ISearcher } from '../interfaces/ISearcher';
3
3
  import type { ILogger } from '../interfaces/ILogger';
4
+ import type { IMcpToolHandler, ToolResponse } from '../interfaces/IMcpToolHandler';
5
+ /**
6
+ * The dispatch logic behind `CallToolRequestSchema` — pulled out so it is testable
7
+ * without a live stdio transport. A rejecting handler becomes `Tool error: <message>`.
8
+ */
9
+ export declare function dispatchTool(tools: Map<string, IMcpToolHandler>, name: string, args: Record<string, unknown>, logger: ILogger): Promise<ToolResponse>;
10
+ export declare function buildInstructions(t: number): string;
4
11
  /**
5
12
  * McpServer — thin orchestration layer that delegates tool handling
6
13
  * to individual IMcpToolHandler implementations (Open/Closed Principle).
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.McpServer = void 0;
37
+ exports.dispatchTool = dispatchTool;
38
+ exports.buildInstructions = buildInstructions;
37
39
  const fs = __importStar(require("fs"));
38
40
  const path = __importStar(require("path"));
39
41
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
@@ -48,6 +50,24 @@ function readPackageVersion() {
48
50
  return '0.0.0';
49
51
  }
50
52
  }
53
+ /**
54
+ * The dispatch logic behind `CallToolRequestSchema` — pulled out so it is testable
55
+ * without a live stdio transport. A rejecting handler becomes `Tool error: <message>`.
56
+ */
57
+ async function dispatchTool(tools, name, args, logger) {
58
+ const handler = tools.get(name);
59
+ if (!handler) {
60
+ return { isError: true, content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
61
+ }
62
+ try {
63
+ return await handler.handle(args);
64
+ }
65
+ catch (err) {
66
+ const reason = err instanceof Error ? err.message : String(err);
67
+ logger.error('McpServer: tool handler error', { tool: name, reason });
68
+ return { isError: true, content: [{ type: 'text', text: `Tool error: ${reason}` }] };
69
+ }
70
+ }
51
71
  function buildInstructions(t) {
52
72
  return `
53
73
  You have access to a local code intelligence server. Use these tools to search codebases by meaning, navigate import graphs, look up symbol definitions, and read source files.
@@ -58,7 +78,7 @@ Pick one workflow based on what you need:
58
78
 
59
79
  | Type | Signals | Workflow |
60
80
  |---|---|---|
61
- | **Lookup** | "where is X defined", "find Y" | find_symbol (if name known) → search_codebase (if not found) |
81
+ | **Lookup** | "where is X defined", "find Y" | find_symbol first when the exact name is known — exact and faster. Call search_codebase too, in parallel, when you also want surrounding context or the name might not match verbatim; fall back to it alone if find_symbol comes back empty. |
62
82
  | **Caller trace** | "who calls X", "find usages of Y", "what invokes Z" | find_symbol → get_callers |
63
83
  | **Conceptual** | "how does X work", "what is Y" | search_codebase → read_file_range |
64
84
  | **Flow audit** | "how does X connect to Y", "audit this flow", "trace the path" | search_codebase → **get_dependencies + get_dependents** → Grep |
@@ -68,26 +88,28 @@ Pick one workflow based on what you need:
68
88
 
69
89
  ## Tools
70
90
 
71
- - **list_projects** — call this first to see what projects are indexed and their exact names.
72
- - **search_codebase** — semantic search across indexed source files. Returns file path, line number, and a code chunk.
73
- - **read_file_range** — read a specific line range from any indexed file. Expects a **relative path** as returned by search_codebase.
91
+ Every file or directory argument (\`file_path\`, \`callee_file\`, \`path\`) is workspace-relative, exactly as printed by search_codebase (e.g. \`my-app/src/index.ts\`; \`.\` is the workspace root) — copy paths between tools unchanged.
92
+
93
+ - **list_projects** — call this first to see what projects are indexed, their exact names and their directories (\`path:\`).
94
+ - **search_codebase** — semantic search across indexed source files. Returns file path, line number, and a code chunk. **Pass \`project\` whenever the target project is known or guessable from the question** — this workspace indexes many projects at once, and an unscoped query competes against all of them, which \`rerank\` cannot fix (reranking only reorders whatever the vector search already retrieved). Pass \`rerank: false\` for "what does X do" / "where is X" lookups described without an exact name — measurement shows reranking hurts these (-0.23 to -0.31 MRR@10). If you already know the exact symbol name, use \`find_symbol\` instead, not \`search_codebase\` — it is faster and exact, and the rerank setting does not matter there. Leave reranking at its default (omit \`rerank\`, or pass \`true\`) for conceptual/architectural queries, where it helps (+0.08 MRR@10).
95
+ - **read_file_range** — read a specific line range from any file under the workspace, indexed or not.
74
96
  - **get_dependencies** — show what files a given file imports (direct or transitive). Use to understand what a file depends on.
75
97
  - **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers.
76
98
  - **find_symbol** — find where a symbol is defined by exact name: top-level declarations, class/object-literal members (methods, get/set accessors, constructors, function properties), interface and type-literal method signatures, and namespace-level declarations. Accepts a bare name (\`assertUsersInOrg\`), a qualified name (\`RepairsService.assertUsersInOrg\`, \`NS.Parser.parse\`) to disambiguate same-named members, a quoted name for string-literal members (\`Router.'kebab-name'\`), or a computed name (\`Stack.[Symbol.iterator]\`). Faster than semantic search when you know the name.
77
99
  - **get_callers** — find all call sites of a function or method by exact name. Accepts a bare or qualified (\`Class.method\`) symbol, using the same suffix matching as \`find_symbol\`. Uses the call graph index — faster and more precise than grep. A call through an interface-typed receiver is recorded under the interface member (e.g. \`IEmbedder.embedBatch\`), not the implementing class. Use after find_symbol to trace where a symbol is actually called.
78
- - **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
79
- - **read_file** — read a complete file by **absolute path** with line numbers. Truncates at max_lines with a hint to continue.
100
+ - **list_files** — browse the directory tree of a directory. Use to understand project structure before reading files.
101
+ - **read_file** — read a complete file with line numbers. Truncates at max_lines with a hint to continue.
80
102
 
81
103
  ## Recommended workflow
82
104
 
83
105
  ### Lookup / Conceptual
84
106
  1. Use \`list_projects\` to discover available projects before filtering by \`project\`.
85
- 2. Use \`search_codebase\` to find relevant code by meaning. If your query uses an ambiguous term (e.g. "subscription" could mean pub/sub or billing), pass a lower \`threshold\` (e.g. \`0.40–0.45\`) to filter out weak matches.
107
+ 2. Use \`search_codebase\` to find relevant code by meaning. **Always pass \`project\` when the question names or implies one** — an unscoped search ranks candidates across every indexed project together, so an unrelated project's weak match can outrank the right project's real one. If your query uses an ambiguous term (e.g. "subscription" could mean pub/sub or billing), pass a lower \`threshold\` (e.g. \`0.40–0.45\`) to filter out weak matches.
86
108
  3. If a result looks **truncated or incomplete**, call \`read_file_range\` with the returned \`file_path\` and \`start_line\`.
87
109
  4. For exploratory searches where results span chunk boundaries, pass \`include_neighbors: true\`.
88
110
  5. To immediately see a file's imports and callers without extra tool calls, pass \`include_graph_context: true\` to \`search_codebase\`. Use this when doing a quick structural survey; switch to \`get_dependencies\`/\`get_dependents\` for full transitive graphs.
89
- 6. Use \`find_symbol\` when you know the exact name — faster than semantic search. Use a qualified name (\`Class.method\`) to disambiguate same-named methods of different classes; a bare name searches every container. Hits marked \`(grep — not in index; may be stale or imprecise)\` come from a fallback text search, not the index — treat them as less reliable. After locating a symbol, use \`get_callers\` to find all call sites — faster and more precise than grep.
90
- 7. Use \`list_files\` to explore project structure before diving into files. Use \`read_file\` to read a complete file by absolute path.
111
+ 6. Use \`find_symbol\` when you know the exact name — faster and exact, unlike semantic search. Use a qualified name (\`Class.method\`) to disambiguate same-named methods of different classes; a bare name searches every container. When you also want surrounding context (callers' intent, nearby helpers, how it's described elsewhere), call \`search_codebase\` in parallel rather than after — the two tools answer different questions and neither blocks the other. Hits marked \`(grep — not in index; may be stale or imprecise)\` come from a fallback text search, not the index — treat them as less reliable. After locating a symbol, use \`get_callers\` to find all call sites — faster and more precise than grep. For a NestJS GraphQL operation, resolve field or type, pass the matching \`graphql_query\`/\`graphql_mutation\`/\`graphql_subscription\`/\`graphql_field\`/\`graphql_type\` kind to look it up by the name the schema exposes, not the TypeScript declaration name.
112
+ 7. Use \`list_files\` to explore project structure before diving into files. Use \`read_file\` to read a complete file.
91
113
 
92
114
  ### Flow audit (mandatory steps — do not skip 3 and 4)
93
115
  1. Use \`list_projects\` to confirm project names.
@@ -97,8 +119,6 @@ Pick one workflow based on what you need:
97
119
  5. For each peripheral file found in steps 3–4: use \`read_file_range\` or \`find_symbol\` to inspect it.
98
120
  6. Only after steps 3–4 draw architectural conclusions — \`search_codebase\` alone misses peripheral files because they are semantically thin (configs, types, loaders contain no domain vocabulary).
99
121
 
100
- > **Note:** If \`get_dependencies\` / \`get_dependents\` return empty, the file likely uses path aliases (e.g. \`@core/\`, \`@/\`, \`@db/\`). Fall back to \`Grep\` on the symbol name across the project.
101
-
102
122
  ### Impact analysis
103
123
  1. \`get_dependents(target_file)\` — direct callers.
104
124
  2. \`get_dependents(target_file, transitive: true)\` — full reverse graph.
@@ -115,7 +135,7 @@ Pick one workflow based on what you need:
115
135
 
116
136
  ## Dependency graph
117
137
 
118
- \`get_dependencies\` and \`get_dependents\` require \`file_path\` (as returned by \`search_codebase\`) and \`project\` (from \`list_projects\`). Pass \`transitive: true\` for the full dependency chain. depType values: \`import\` | \`import-default\` | \`import-namespace\` | \`reexport\` | \`reexport-namespace\`. \`fromSymbol\` — the top-level class or function in the importing file that uses this import (null if ambiguous or not tracked).
138
+ \`get_dependencies\` and \`get_dependents\` take \`file_path\` alone and list edges in every project. A path that names no file is an error, never an empty result. Pass \`transitive: true\` for the full dependency chain. depType values: \`import\` | \`import-default\` | \`import-namespace\` | \`import-side-effect\` | \`reexport\` | \`reexport-namespace\`. \`fromSymbol\` — the top-level class or function in the importing file that uses this import (null if ambiguous or not tracked).
119
139
 
120
140
  If \`get_dependents\` returns 0 results, add \`symbol\` (the exported name you care about) — this triggers a grep fallback that finds runtime usages in DI containers, string references, and dynamic lookups that the import graph misses.
121
141
 
@@ -161,27 +181,11 @@ class McpServer {
161
181
  }],
162
182
  };
163
183
  }
164
- const handler = this.tools.get(req.params.name);
165
- if (!handler) {
166
- return {
167
- isError: true,
168
- content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
169
- };
170
- }
171
- try {
172
- const t0 = Date.now();
173
- const result = await handler.handle((req.params.arguments ?? {}));
184
+ const t0 = Date.now();
185
+ const result = await dispatchTool(this.tools, req.params.name, (req.params.arguments ?? {}), this.logger);
186
+ if (!result.isError)
174
187
  this.logger.debug('McpServer: tool handled', { tool: req.params.name, ms: Date.now() - t0 });
175
- return result;
176
- }
177
- catch (err) {
178
- const reason = err instanceof Error ? err.message : String(err);
179
- this.logger.error('McpServer: tool handler error', { tool: req.params.name, reason });
180
- return {
181
- isError: true,
182
- content: [{ type: 'text', text: `Tool error: ${reason}` }],
183
- };
184
- }
188
+ return result;
185
189
  });
186
190
  await server.connect(new stdio_js_1.StdioServerTransport());
187
191
  this.logger.info('McpServer: started', { version: this.version });
@@ -194,13 +198,13 @@ class McpServer {
194
198
  const handlers = [
195
199
  new tools_1.ListProjectsTool(this.searcher),
196
200
  new tools_1.SearchCodebaseTool(this.searcher, this.config.search.defaultLimit, this.config.search.maxLimit, this.config.search.similarityThreshold, this.logger),
197
- new tools_1.ReadFileRangeTool(this.searcher),
198
- new tools_1.GetDependenciesTool(this.searcher),
199
- new tools_1.GetDependentsTool(this.searcher),
201
+ new tools_1.ReadFileRangeTool(this.searcher, this.logger),
202
+ new tools_1.GetDependenciesTool(this.searcher, this.logger),
203
+ new tools_1.GetDependentsTool(this.searcher, this.logger),
200
204
  new tools_1.FindSymbolTool(this.searcher),
201
- new tools_1.GetCallersTool(this.searcher),
202
- new tools_1.ListFilesTool(this.searcher),
203
- new tools_1.ReadFileTool(this.searcher),
205
+ new tools_1.GetCallersTool(this.searcher, this.logger),
206
+ new tools_1.ListFilesTool(this.searcher, this.logger),
207
+ new tools_1.ReadFileTool(this.searcher, this.logger),
204
208
  ];
205
209
  const map = new Map();
206
210
  for (const h of handlers) {
@@ -51,7 +51,8 @@ class OpenAICompatibleEmbedder {
51
51
  const res = await this.doFetch(this.url, {
52
52
  method: 'POST',
53
53
  headers,
54
- signal: AbortSignal.timeout(120_000),
54
+ // CPU inference of one batch of token-dense text (e.g. pseudo-localized strings) takes well over 2 min.
55
+ signal: AbortSignal.timeout(240_000),
55
56
  body: JSON.stringify(body),
56
57
  });
57
58
  if (!res.ok) {