@optave/codegraph 3.9.6 → 3.10.0

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 (70) hide show
  1. package/README.md +30 -16
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +1 -1
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +50 -8
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/domain/graph/builder/context.d.ts +10 -0
  9. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  10. package/dist/domain/graph/builder/context.js +10 -0
  11. package/dist/domain/graph/builder/context.js.map +1 -1
  12. package/dist/domain/graph/builder/helpers.d.ts +7 -2
  13. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  14. package/dist/domain/graph/builder/helpers.js +7 -2
  15. package/dist/domain/graph/builder/helpers.js.map +1 -1
  16. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  17. package/dist/domain/graph/builder/pipeline.js +197 -33
  18. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  19. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  20. package/dist/domain/graph/builder/stages/collect-files.js +8 -0
  21. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  22. package/dist/domain/graph/builder/stages/detect-changes.d.ts +24 -0
  23. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  24. package/dist/domain/graph/builder/stages/detect-changes.js +117 -3
  25. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  26. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  27. package/dist/domain/graph/builder/stages/finalize.js +9 -6
  28. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  29. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +30 -0
  30. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  31. package/dist/domain/graph/builder/stages/insert-nodes.js +36 -13
  32. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  33. package/dist/domain/parser.d.ts +14 -1
  34. package/dist/domain/parser.d.ts.map +1 -1
  35. package/dist/domain/parser.js +77 -10
  36. package/dist/domain/parser.js.map +1 -1
  37. package/dist/domain/search/models.js +1 -1
  38. package/dist/domain/wasm-worker-entry.js +12 -12
  39. package/dist/domain/wasm-worker-entry.js.map +1 -1
  40. package/dist/infrastructure/config.d.ts +1 -0
  41. package/dist/infrastructure/config.d.ts.map +1 -1
  42. package/dist/infrastructure/config.js +1 -0
  43. package/dist/infrastructure/config.js.map +1 -1
  44. package/dist/mcp/server.d.ts.map +1 -1
  45. package/dist/mcp/server.js +14 -8
  46. package/dist/mcp/server.js.map +1 -1
  47. package/dist/mcp/tool-registry.d.ts +1 -1
  48. package/dist/mcp/tool-registry.d.ts.map +1 -1
  49. package/dist/mcp/tool-registry.js +19 -5
  50. package/dist/mcp/tool-registry.js.map +1 -1
  51. package/dist/types.d.ts +1 -0
  52. package/dist/types.d.ts.map +1 -1
  53. package/grammars/tree-sitter-erlang.wasm +0 -0
  54. package/package.json +8 -7
  55. package/src/ast-analysis/engine.ts +3 -1
  56. package/src/ast-analysis/visitors/ast-store-visitor.ts +45 -9
  57. package/src/domain/graph/builder/context.ts +10 -0
  58. package/src/domain/graph/builder/helpers.ts +8 -3
  59. package/src/domain/graph/builder/pipeline.ts +211 -33
  60. package/src/domain/graph/builder/stages/collect-files.ts +9 -0
  61. package/src/domain/graph/builder/stages/detect-changes.ts +130 -4
  62. package/src/domain/graph/builder/stages/finalize.ts +9 -6
  63. package/src/domain/graph/builder/stages/insert-nodes.ts +38 -14
  64. package/src/domain/parser.ts +83 -9
  65. package/src/domain/search/models.ts +1 -1
  66. package/src/domain/wasm-worker-entry.ts +12 -12
  67. package/src/infrastructure/config.ts +1 -0
  68. package/src/mcp/server.ts +16 -9
  69. package/src/mcp/tool-registry.ts +23 -5
  70. package/src/types.ts +1 -0
@@ -12,10 +12,12 @@ import path from 'node:path';
12
12
  import { performance } from 'node:perf_hooks';
13
13
  import { bulkNodeIdsByFile } from '../../../../db/index.js';
14
14
  import { debug } from '../../../../infrastructure/logger.js';
15
+ import { normalizePath } from '../../../../shared/constants.js';
15
16
  import { toErrorMessage } from '../../../../shared/errors.js';
16
17
  import type {
17
18
  BetterSqlite3Database,
18
19
  ExtractorOutput,
20
+ FileToParse,
19
21
  MetadataUpdate,
20
22
  SqliteStatement,
21
23
  } from '../../../../types.js';
@@ -90,16 +92,30 @@ function marshalSymbolBatches(allSymbols: Map<string, ExtractorOutput>): InsertN
90
92
  return batches;
91
93
  }
92
94
 
93
- /** Build file hash entries from parsed symbols and precomputed/metadata sources. */
94
- function buildFileHashes(
95
- allSymbols: Map<string, ExtractorOutput>,
95
+ /**
96
+ * Build file hash entries for every collected file, including those that
97
+ * produced zero symbols (empty files, parsers that silently no-op'd, or
98
+ * optional-language extensions whose grammar wasn't installed). Iterating the
99
+ * symbol map instead would skip such files and leave them missing from
100
+ * `file_hashes`, which permanently breaks the JS-side fast-skip pre-flight on
101
+ * any subsequent no-op rebuild (#1068).
102
+ *
103
+ * Exported for unit testing.
104
+ */
105
+ export function buildFileHashes(
106
+ filesToParse: FileToParse[],
96
107
  precomputedData: Map<string, PrecomputedFileData>,
97
108
  metadataUpdates: MetadataUpdate[],
98
109
  rootDir: string,
99
110
  ): Array<{ file: string; hash: string; mtime: number; size: number }> {
100
111
  const fileHashes: Array<{ file: string; hash: string; mtime: number; size: number }> = [];
112
+ const seen = new Set<string>();
113
+
114
+ for (const item of filesToParse) {
115
+ const relPath = item.relPath ?? normalizePath(path.relative(rootDir, item.file));
116
+ if (seen.has(relPath)) continue;
117
+ seen.add(relPath);
101
118
 
102
- for (const [relPath] of allSymbols) {
103
119
  const precomputed = precomputedData.get(relPath);
104
120
  if (precomputed?._reverseDepOnly) {
105
121
  continue; // file unchanged, hash already correct
@@ -112,7 +128,7 @@ function buildFileHashes(
112
128
  size = precomputed.stat.size;
113
129
  } else {
114
130
  const rawStat = fileStat(path.join(rootDir, relPath));
115
- mtime = rawStat ? Math.floor(rawStat.mtimeMs) : 0;
131
+ mtime = rawStat ? rawStat.mtime : 0;
116
132
  size = rawStat ? rawStat.size : 0;
117
133
  }
118
134
  fileHashes.push({ file: relPath, hash: precomputed.hash, mtime, size });
@@ -127,7 +143,7 @@ function buildFileHashes(
127
143
  }
128
144
  if (code !== null) {
129
145
  const stat = fileStat(absPath);
130
- const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
146
+ const mtime = stat ? stat.mtime : 0;
131
147
  const size = stat ? stat.size : 0;
132
148
  fileHashes.push({ file: relPath, hash: fileHash(code), mtime, size });
133
149
  }
@@ -136,7 +152,7 @@ function buildFileHashes(
136
152
 
137
153
  // Also include metadata-only updates (self-heal mtime/size without re-parse)
138
154
  for (const item of metadataUpdates) {
139
- const mtime = item.stat ? Math.floor(item.stat.mtime) : 0;
155
+ const mtime = item.stat ? item.stat.mtime : 0;
140
156
  const size = item.stat ? item.stat.size : 0;
141
157
  fileHashes.push({ file: item.relPath, hash: item.hash, mtime, size });
142
158
  }
@@ -157,7 +173,7 @@ function tryNativeInsert(ctx: PipelineContext): boolean {
157
173
  for (const item of filesToParse) {
158
174
  if (item.relPath) precomputedData.set(item.relPath, item as PrecomputedFileData);
159
175
  }
160
- const fileHashes = buildFileHashes(allSymbols, precomputedData, metadataUpdates, rootDir);
176
+ const fileHashes = buildFileHashes(filesToParse, precomputedData, metadataUpdates, rootDir);
161
177
 
162
178
  // In native-first mode (single rusqlite connection), no WAL dance is needed.
163
179
  // In dual-connection mode, checkpoint JS side before native write, then
@@ -321,7 +337,7 @@ function insertChildrenAndEdges(
321
337
 
322
338
  function updateFileHashes(
323
339
  _db: BetterSqlite3Database,
324
- allSymbols: Map<string, ExtractorOutput>,
340
+ filesToParse: FileToParse[],
325
341
  precomputedData: Map<string, PrecomputedFileData>,
326
342
  metadataUpdates: MetadataUpdate[],
327
343
  rootDir: string,
@@ -329,7 +345,15 @@ function updateFileHashes(
329
345
  ): void {
330
346
  if (!upsertHash) return;
331
347
 
332
- for (const [relPath] of allSymbols) {
348
+ // Iterate every collected file (#1068): files that produced zero symbols
349
+ // (empty, parser no-op, or grammar-missing optional language) still need a
350
+ // hash row, otherwise the next no-op rebuild's fast-skip pre-flight rejects.
351
+ const seen = new Set<string>();
352
+ for (const item of filesToParse) {
353
+ const relPath = item.relPath ?? normalizePath(path.relative(rootDir, item.file));
354
+ if (seen.has(relPath)) continue;
355
+ seen.add(relPath);
356
+
333
357
  const precomputed = precomputedData.get(relPath);
334
358
  if (precomputed?._reverseDepOnly) {
335
359
  // no-op: file unchanged, hash already correct
@@ -341,7 +365,7 @@ function updateFileHashes(
341
365
  size = precomputed.stat.size;
342
366
  } else {
343
367
  const rawStat = fileStat(path.join(rootDir, relPath));
344
- mtime = rawStat ? Math.floor(rawStat.mtimeMs) : 0;
368
+ mtime = rawStat ? rawStat.mtime : 0;
345
369
  size = rawStat ? rawStat.size : 0;
346
370
  }
347
371
  upsertHash.run(relPath, precomputed.hash, mtime, size);
@@ -356,7 +380,7 @@ function updateFileHashes(
356
380
  }
357
381
  if (code !== null) {
358
382
  const stat = fileStat(absPath);
359
- const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
383
+ const mtime = stat ? stat.mtime : 0;
360
384
  const size = stat ? stat.size : 0;
361
385
  upsertHash.run(relPath, fileHash(code), mtime, size);
362
386
  }
@@ -365,7 +389,7 @@ function updateFileHashes(
365
389
 
366
390
  // Also update metadata-only entries (self-heal mtime/size without re-parse)
367
391
  for (const item of metadataUpdates) {
368
- const mtime = item.stat ? Math.floor(item.stat.mtime) : 0;
392
+ const mtime = item.stat ? item.stat.mtime : 0;
369
393
  const size = item.stat ? item.stat.size : 0;
370
394
  upsertHash.run(item.relPath, item.hash, mtime, size);
371
395
  }
@@ -415,7 +439,7 @@ export async function insertNodes(ctx: PipelineContext): Promise<void> {
415
439
  const insertAll = ctx.db.transaction(() => {
416
440
  insertDefinitionsAndExports(ctx.db, allSymbols);
417
441
  insertChildrenAndEdges(ctx.db, allSymbols);
418
- updateFileHashes(ctx.db, allSymbols, precomputedData, metadataUpdates, rootDir, upsertHash);
442
+ updateFileHashes(ctx.db, filesToParse, precomputedData, metadataUpdates, rootDir, upsertHash);
419
443
  });
420
444
 
421
445
  insertAll();
@@ -316,16 +316,23 @@ export function getParser(parsers: Map<string, Parser | null>, filePath: string)
316
316
  *
317
317
  * Name is preserved for caller compatibility; the function now ensures
318
318
  * *analysis data* rather than *trees*.
319
+ *
320
+ * `needsFn` (optional): when provided, only files for which it returns true are
321
+ * re-parsed. Without it the function falls back to "any WASM-parseable file
322
+ * without _tree", which was the source of #1036 — a single file missing one
323
+ * analysis triggered a full-build re-parse of every WASM-parseable file.
319
324
  */
320
325
  export async function ensureWasmTrees(
321
326
  fileSymbols: Map<string, any>,
322
327
  rootDir: string,
328
+ needsFn?: (relPath: string, symbols: any) => boolean,
323
329
  ): Promise<void> {
324
330
  // Collect files that still need analysis data and are parseable by WASM.
325
331
  const pending: Array<{ relPath: string; absPath: string; symbols: any }> = [];
326
332
  for (const [relPath, symbols] of fileSymbols) {
327
333
  if (symbols._tree) continue; // legacy path — leave existing trees alone
328
334
  if (!_extToLang.has(path.extname(relPath).toLowerCase())) continue;
335
+ if (needsFn && !needsFn(relPath, symbols)) continue;
329
336
  pending.push({ relPath, absPath: path.join(rootDir, relPath), symbols });
330
337
  }
331
338
  if (pending.length === 0) return;
@@ -1060,6 +1067,71 @@ async function parseFilesWasm(
1060
1067
  return result;
1061
1068
  }
1062
1069
 
1070
+ /**
1071
+ * Files at or below this count use the inline parse path (no worker spawn).
1072
+ *
1073
+ * Sized for typical engine-parity drops: a handful of fixture files in one
1074
+ * or two languages (the recurring HCL case is 4 files). Above this, the
1075
+ * worker-pool's IPC + crash-isolation cost (#965) is amortized over enough
1076
+ * parse work to be worth paying; below it, the ~1–2s cold-start dominates.
1077
+ */
1078
+ const INLINE_BACKFILL_THRESHOLD = 16;
1079
+
1080
+ /**
1081
+ * Inline WASM parse (no worker) for small file batches.
1082
+ *
1083
+ * Used by the engine-parity backfill path when the native engine drops a
1084
+ * handful of files (typically test fixtures). The worker pool's per-call
1085
+ * IPC + grammar-init overhead can cost 1–2s on slow CI runners — for a
1086
+ * 4-file backfill, that dwarfs the ~10ms of actual parse work.
1087
+ *
1088
+ * Returns symbols with `_tree` set so `runAnalyses` can run AST/CFG/dataflow
1089
+ * visitors via the unified walker (mirrors how WASM-engine results behaved
1090
+ * before the worker pool was introduced).
1091
+ */
1092
+ async function parseFilesWasmInline(
1093
+ filePaths: string[],
1094
+ rootDir: string,
1095
+ ): Promise<Map<string, ExtractorOutput>> {
1096
+ const result = new Map<string, ExtractorOutput>();
1097
+ if (filePaths.length === 0) return result;
1098
+ const parsers = await ensureParsersForFiles(filePaths);
1099
+ for (const filePath of filePaths) {
1100
+ if (!_extToLang.has(path.extname(filePath).toLowerCase())) continue;
1101
+ let code: string;
1102
+ try {
1103
+ code = fs.readFileSync(filePath, 'utf-8');
1104
+ } catch (err: unknown) {
1105
+ warn(`Skipping ${path.relative(rootDir, filePath)}: ${(err as Error).message}`);
1106
+ continue;
1107
+ }
1108
+ const extracted = wasmExtractSymbols(parsers, filePath, code);
1109
+ if (!extracted) continue;
1110
+ const relPath = path.relative(rootDir, filePath).split(path.sep).join('/');
1111
+ const symbols = extracted.symbols as ExtractorOutput & { _tree?: unknown; _langId?: string };
1112
+ symbols._tree = extracted.tree;
1113
+ symbols._langId = extracted.langId;
1114
+ result.set(relPath, symbols);
1115
+ }
1116
+ return result;
1117
+ }
1118
+
1119
+ /**
1120
+ * Backfill helper: small batches use the inline (main-thread) path; larger
1121
+ * batches keep the worker-pool isolation against tree-sitter WASM crashes
1122
+ * (#965). Threshold matches typical engine-parity drop sizes (a few fixture
1123
+ * files in one or two languages).
1124
+ */
1125
+ export async function parseFilesWasmForBackfill(
1126
+ filePaths: string[],
1127
+ rootDir: string,
1128
+ ): Promise<Map<string, ExtractorOutput>> {
1129
+ if (filePaths.length <= INLINE_BACKFILL_THRESHOLD) {
1130
+ return parseFilesWasmInline(filePaths, rootDir);
1131
+ }
1132
+ return parseFilesWasm(filePaths, rootDir);
1133
+ }
1134
+
1063
1135
  /**
1064
1136
  * Parse multiple files in bulk and return a Map<relPath, symbols>.
1065
1137
  */
@@ -1110,7 +1182,7 @@ export async function parseFilesAuto(
1110
1182
  );
1111
1183
  if (dropped.length > 0) {
1112
1184
  warn(`Native engine dropped ${dropped.length} file(s); falling back to WASM for parity`);
1113
- const wasmResults = await parseFilesWasm(dropped, rootDir);
1185
+ const wasmResults = await parseFilesWasmForBackfill(dropped, rootDir);
1114
1186
  for (const [relPath, symbols] of wasmResults) {
1115
1187
  result.set(relPath, symbols);
1116
1188
  }
@@ -1125,15 +1197,17 @@ export async function parseFilesAuto(
1125
1197
  export function getActiveEngine(opts: ParseEngineOpts = {}): {
1126
1198
  name: 'native' | 'wasm';
1127
1199
  version: string | null;
1200
+ binaryVersion: string | null;
1128
1201
  } {
1129
1202
  const { name, native } = resolveEngine(opts);
1130
- let version: string | null = native
1131
- ? typeof native.engineVersion === 'function'
1132
- ? native.engineVersion()
1133
- : null
1134
- : null;
1135
- // Prefer platform package.json version over binary-embedded version
1136
- // to handle stale binaries that weren't recompiled during a release
1203
+ const binaryVersion: string | null =
1204
+ native && typeof native.engineVersion === 'function' ? native.engineVersion() : null;
1205
+ // The display version prefers the platform package.json so the "Using native
1206
+ // engine (vX)" log matches the npm release the user installed. The Rust
1207
+ // orchestrator's check_version_mismatch compares against CARGO_PKG_VERSION
1208
+ // (the binary's own value), so build_meta writes must use `binaryVersion`,
1209
+ // not this display value see pipeline.ts and finalize.ts (#1066).
1210
+ let version: string | null = binaryVersion;
1137
1211
  if (native) {
1138
1212
  try {
1139
1213
  version = getNativePackageVersion() ?? version;
@@ -1141,7 +1215,7 @@ export function getActiveEngine(opts: ParseEngineOpts = {}): {
1141
1215
  debug(`getNativePackageVersion failed: ${(e as Error).message}`);
1142
1216
  }
1143
1217
  }
1144
- return { name, version };
1218
+ return { name, version, binaryVersion };
1145
1219
  }
1146
1220
 
1147
1221
  /**
@@ -42,7 +42,7 @@ export const MODELS: Record<string, ModelConfig> = {
42
42
  quantized: false,
43
43
  },
44
44
  'jina-code': {
45
- name: 'Xenova/jina-embeddings-v2-base-code',
45
+ name: 'jinaai/jina-embeddings-v2-base-code',
46
46
  dim: 768,
47
47
  contextWindow: 8192,
48
48
  desc: 'Code-aware (~137MB). Trained on code+text, best for code search.',
@@ -708,18 +708,18 @@ async function handleParse(msg: WorkerParseRequest): Promise<SerializedExtractor
708
708
  file?: string;
709
709
  parentNodeId?: number | null;
710
710
  }>;
711
- if (astRows.length > 0) {
712
- // Strip `file` and `parentNodeId` main thread re-resolves parent IDs
713
- // against its DB in features/ast.ts::collectFileAstRows, and `file` is
714
- // known from the map key.
715
- serializedAstNodes = astRows.map((n) => ({
716
- line: n.line,
717
- kind: n.kind,
718
- name: n.name ?? '',
719
- text: n.text ?? undefined,
720
- receiver: n.receiver ?? undefined,
721
- }));
722
- }
711
+ // Always set an array (even empty) — leaving astNodes undefined makes
712
+ // engine.ts::fileNeedsWasmTree treat the file as un-walked and trigger
713
+ // a full ensureWasmTrees re-parse of every WASM-parseable file (#1036).
714
+ // Strip `file` and `parentNodeId` — main thread re-resolves both in
715
+ // features/ast.ts::collectFileAstRows.
716
+ serializedAstNodes = astRows.map((n) => ({
717
+ line: n.line,
718
+ kind: n.kind,
719
+ name: n.name ?? '',
720
+ text: n.text ?? undefined,
721
+ receiver: n.receiver ?? undefined,
722
+ }));
723
723
  }
724
724
 
725
725
  if (complexityVisitor) storeComplexityResults(results, defs, entry.id);
@@ -147,6 +147,7 @@ export const DEFAULTS = {
147
147
  implementations: 50,
148
148
  interfaces: 50,
149
149
  },
150
+ disabledTools: [] as string[],
150
151
  },
151
152
  } satisfies CodegraphConfig;
152
153
 
package/src/mcp/server.ts CHANGED
@@ -98,17 +98,15 @@ async function resolveDbPath(
98
98
  return dbPath;
99
99
  }
100
100
 
101
- function validateMultiRepoAccess(multiRepo: boolean, name: string, args: { repo?: string }): void {
101
+ function validateMultiRepoAccess(multiRepo: boolean, args: { repo?: string }): void {
102
102
  if (!multiRepo && args.repo) {
103
103
  throw new ConfigError(
104
104
  'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to access other repositories.',
105
105
  );
106
106
  }
107
- if (!multiRepo && name === 'list_repos') {
108
- throw new ConfigError(
109
- 'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to list repositories.',
110
- );
111
- }
107
+ // Note: the `list_repos` tool is excluded from `enabledToolNames` when
108
+ // `multiRepo` is false (see `buildToolList`), so any call to it is rejected
109
+ // earlier in `createCallToolHandler` with an "Unknown tool" error.
112
110
  }
113
111
 
114
112
  /**
@@ -163,11 +161,17 @@ function createCallToolHandler(
163
161
  customDbPath: string | undefined,
164
162
  allowedRepos: string[] | undefined,
165
163
  getQueries: () => Promise<unknown>,
164
+ enabledToolNames: Set<string>,
166
165
  ) {
167
166
  return async (request: any) => {
168
167
  const { name, arguments: args } = request.params;
169
168
  try {
170
- validateMultiRepoAccess(multiRepo, name, args);
169
+ if (!enabledToolNames.has(name)) {
170
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
171
+ }
172
+
173
+ validateMultiRepoAccess(multiRepo, args);
174
+
171
175
  const dbPath = await resolveDbPath(customDbPath, args, allowedRepos);
172
176
 
173
177
  const toolEntry = TOOL_HANDLERS.get(name);
@@ -209,6 +213,9 @@ export async function startMCPServer(
209
213
  // Apply config-based MCP page-size overrides
210
214
  const config = options.config || loadConfig();
211
215
  initMcpDefaults(config.mcp?.defaults ? { ...config.mcp.defaults } : undefined);
216
+ const disabledTools = [...(config.mcp?.disabledTools ?? [])];
217
+ const enabledTools = buildToolList(multiRepo, disabledTools);
218
+ const enabledToolNames = new Set(enabledTools.map((tool) => tool.name));
212
219
 
213
220
  const { Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema } =
214
221
  await loadMCPSdk();
@@ -225,12 +232,12 @@ export async function startMCPServer(
225
232
  );
226
233
 
227
234
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
228
- tools: buildToolList(multiRepo),
235
+ tools: enabledTools,
229
236
  }));
230
237
 
231
238
  server.setRequestHandler(
232
239
  CallToolRequestSchema,
233
- createCallToolHandler(multiRepo, customDbPath, allowedRepos, getQueries),
240
+ createCallToolHandler(multiRepo, customDbPath, allowedRepos, getQueries, enabledToolNames),
234
241
  );
235
242
 
236
243
  const transport = new (StdioServerTransport as any)();
@@ -29,6 +29,17 @@ const PAGINATION_PROPS: Record<string, unknown> = {
29
29
  offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
30
30
  };
31
31
 
32
+ function normalizeToolName(name: string): string {
33
+ return name
34
+ .trim()
35
+ .toLowerCase()
36
+ .replace(/^codegraph\d+_/, '');
37
+ }
38
+
39
+ function buildDisabledToolSet(disabledTools?: string[]): Set<string> {
40
+ return new Set((disabledTools || []).map((name) => normalizeToolName(name)).filter(Boolean));
41
+ }
42
+
32
43
  const BASE_TOOLS: ToolSchema[] = [
33
44
  {
34
45
  name: 'query',
@@ -849,18 +860,25 @@ const LIST_REPOS_TOOL: ToolSchema = {
849
860
  /**
850
861
  * Build the tool list based on multi-repo mode.
851
862
  */
852
- export function buildToolList(multiRepo: boolean): ToolSchema[] {
853
- if (!multiRepo) return BASE_TOOLS;
854
- return [
855
- ...BASE_TOOLS.map((tool) => ({
863
+ export function buildToolList(multiRepo: boolean, disabledTools?: string[]): ToolSchema[] {
864
+ const disabled = buildDisabledToolSet(disabledTools);
865
+ const includeTool = (tool: ToolSchema): boolean => !disabled.has(normalizeToolName(tool.name));
866
+ const baseTools = BASE_TOOLS.filter(includeTool);
867
+
868
+ if (!multiRepo) return baseTools;
869
+
870
+ const tools: ToolSchema[] = [
871
+ ...baseTools.map((tool) => ({
856
872
  ...tool,
857
873
  inputSchema: {
858
874
  ...tool.inputSchema,
859
875
  properties: { ...tool.inputSchema.properties, ...REPO_PROP },
860
876
  },
861
877
  })),
862
- LIST_REPOS_TOOL,
863
878
  ];
879
+
880
+ if (includeTool(LIST_REPOS_TOOL)) tools.push(LIST_REPOS_TOOL);
881
+ return tools;
864
882
  }
865
883
 
866
884
  // Backward-compatible export: full multi-repo tool list
package/src/types.ts CHANGED
@@ -1201,6 +1201,7 @@ export interface CodegraphConfig {
1201
1201
 
1202
1202
  mcp: {
1203
1203
  defaults: McpDefaults;
1204
+ disabledTools?: string[];
1204
1205
  };
1205
1206
  }
1206
1207