@esankhan3/code-search-mcp 0.4.2 → 0.4.3

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.
package/dist/server.js CHANGED
@@ -224,7 +224,7 @@ async function autoIndex(ctx) {
224
224
  try {
225
225
  const kbPath = getKnowledgeBasePath(ctx.projectName);
226
226
  const hasLanceDB = existsSync(join(kbPath, "lancedb"));
227
- const hasGraph = existsSync(join(kbPath, "system_graph_v2.json"));
227
+ const hasGraph = existsSync(join(kbPath, "system_graph.sqlite")) || existsSync(join(kbPath, "system_graph_v2.json"));
228
228
  if (hasLanceDB && hasGraph) {
229
229
  ctx.indexReady = true;
230
230
  console.error(`[code-search-mcp] Index loaded for "${ctx.projectName}"`);
@@ -5,7 +5,8 @@ import {
5
5
  loadAllProfiles,
6
6
  loadProfile,
7
7
  getAllChanges,
8
- getChangedFilesList
8
+ getChangedFilesList,
9
+ openSystemGraphStore
9
10
  } from "@esankhan3/anvil-knowledge-core";
10
11
  const GRAPH_TOOL_NAMES = [
11
12
  "get_repo_graph",
@@ -19,43 +20,19 @@ const GRAPH_TOOL_NAMES = [
19
20
  "search_graph",
20
21
  "detect_changes"
21
22
  ];
22
- function loadSystemGraph(kbPath) {
23
- const p = join(kbPath, "system_graph_v2.json");
24
- if (!existsSync(p)) return null;
25
- try {
26
- const g = JSON.parse(readFileSync(p, "utf-8"));
27
- return { nodes: g.nodes ?? [], edges: g.edges ?? [] };
28
- } catch {
29
- return null;
30
- }
31
- }
32
- function nodeLabel(node) {
33
- return node.attributes?.label ?? node.key.split("::").slice(2).join("::") ?? node.key;
34
- }
35
- const edgeType = (e) => e.attributes?.type ?? e.attributes?.relation;
36
- function resolveEntityNodes(nodes, name, repo, fuzzy = false) {
37
- const inRepo = (key) => !repo || key.startsWith(repo + "::");
38
- if (fuzzy) {
39
- return nodes.filter((n) => inRepo(n.key) && ((n.attributes?.label ?? "").includes(name) || n.key.includes(name))).map((n) => n.key);
40
- }
41
- return nodes.filter((n) => inRepo(n.key) && (n.attributes?.label === name || n.key.endsWith("::" + name))).map((n) => n.key);
42
- }
43
- function buildAdjacency(edges, direction) {
44
- const adj = /* @__PURE__ */ new Map();
45
- const add = (a, b) => {
46
- if (!adj.has(a)) adj.set(a, /* @__PURE__ */ new Set());
47
- adj.get(a).add(b);
23
+ const repoOf = (key) => key.split("::")[0] ?? "";
24
+ function makeNeighborGetter(store, direction) {
25
+ const cache = /* @__PURE__ */ new Map();
26
+ return (key) => {
27
+ let v = cache.get(key);
28
+ if (v === void 0) {
29
+ v = store.neighborsOf(key, direction);
30
+ cache.set(key, v);
31
+ }
32
+ return v;
48
33
  };
49
- for (const e of edges) {
50
- if (!e.source || !e.target) continue;
51
- if (edgeType(e) === "contains") continue;
52
- if ((e.attributes?.confidence ?? 0.8) < 0.7) continue;
53
- if (direction === "callees" || direction === "both") add(e.source, e.target);
54
- if (direction === "callers" || direction === "both") add(e.target, e.source);
55
- }
56
- return adj;
57
34
  }
58
- function shortestPath(adj, fromKeys, toKeys, maxDepth) {
35
+ function shortestPath(getNeighbors, fromKeys, toKeys, maxDepth) {
59
36
  const seed = fromKeys.find((k) => toKeys.has(k));
60
37
  if (seed) return [seed];
61
38
  const visited = new Set(fromKeys);
@@ -63,7 +40,7 @@ function shortestPath(adj, fromKeys, toKeys, maxDepth) {
63
40
  for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
64
41
  const next = [];
65
42
  for (const path of frontier) {
66
- for (const nb of adj.get(path[path.length - 1]) ?? []) {
43
+ for (const nb of getNeighbors(path[path.length - 1])) {
67
44
  if (visited.has(nb)) continue;
68
45
  if (toKeys.has(nb)) return [...path, nb];
69
46
  visited.add(nb);
@@ -74,14 +51,14 @@ function shortestPath(adj, fromKeys, toKeys, maxDepth) {
74
51
  }
75
52
  return null;
76
53
  }
77
- function reachable(adj, fromKeys, maxDepth, cap = 50) {
54
+ function reachable(getNeighbors, fromKeys, maxDepth, cap = 50) {
78
55
  const visited = new Set(fromKeys);
79
56
  const out = [];
80
57
  let frontier = [...fromKeys];
81
58
  for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
82
59
  const next = [];
83
60
  for (const k of frontier) {
84
- for (const nb of adj.get(k) ?? []) {
61
+ for (const nb of getNeighbors(k)) {
85
62
  if (visited.has(nb)) continue;
86
63
  visited.add(nb);
87
64
  out.push({ key: nb, depth });
@@ -219,14 +196,14 @@ function registerGraphTools() {
219
196
  }
220
197
  async function handleGraphTool(name, args, ctx) {
221
198
  if (!GRAPH_TOOL_NAMES.includes(name)) return null;
199
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
200
+ const NO_GRAPH = "No system graph found. Build KB first.";
222
201
  try {
223
202
  const kbPath = getKnowledgeBasePath(ctx.projectName);
224
203
  if (name === "get_repo_graph") {
225
204
  const repo = args.repo;
226
205
  const graphPath = join(kbPath, repo, "graph.json");
227
- if (!existsSync(graphPath)) {
228
- return { content: [{ type: "text", text: `No graph found for repo "${repo}"` }] };
229
- }
206
+ if (!existsSync(graphPath)) return text(`No graph found for repo "${repo}"`);
230
207
  const graph = JSON.parse(readFileSync(graphPath, "utf-8"));
231
208
  const summary = `# ${repo} AST Graph
232
209
 
@@ -237,140 +214,132 @@ async function handleGraphTool(name, args, ctx) {
237
214
  ${(graph.nodes ?? []).slice(0, 50).map((n) => `- \`${n.id}\` (${n.type})`).join("\n")}
238
215
 
239
216
  ${graph.nodes?.length > 50 ? `... and ${graph.nodes.length - 50} more` : ""}`;
240
- return { content: [{ type: "text", text: summary }] };
217
+ return text(summary);
241
218
  }
242
219
  if (name === "get_cross_repo_edges") {
243
- const sys = loadSystemGraph(kbPath);
244
- if (!sys) return { content: [{ type: "text", text: "No system graph found. Build KB first." }] };
245
- const repo = args.repo;
246
- const relevant = repo ? sys.edges.filter((e) => e.source.startsWith(repo + "::") || e.target.startsWith(repo + "::")) : sys.edges;
247
- const crossRepo = relevant.filter((e) => {
248
- const src = e.source.split("::")[0];
249
- const tgt = e.target.split("::")[0];
250
- return src && tgt && src !== tgt;
251
- });
252
- if (crossRepo.length === 0) {
253
- return { content: [{ type: "text", text: repo ? `No cross-repo edges found for "${repo}"` : "No cross-repo edges found" }] };
254
- }
255
- const text = crossRepo.slice(0, 50).map(
256
- (e) => `- ${e.source.split("::")[0]} \u2192 ${e.target.split("::")[0]} (${edgeType(e) ?? "edge"})`
257
- ).join("\n");
258
- return { content: [{ type: "text", text: `# Cross-Repo Edges${repo ? ` for ${repo}` : ""}
220
+ const store = await openSystemGraphStore(kbPath);
221
+ if (!store) return text(NO_GRAPH);
222
+ try {
223
+ const repo = args.repo;
224
+ const { edges, total } = store.crossRepoEdges(repo, 50);
225
+ if (total === 0) return text(repo ? `No cross-repo edges found for "${repo}"` : "No cross-repo edges found");
226
+ const body = edges.map((e) => `- ${repoOf(e.source)} \u2192 ${repoOf(e.target)} (${e.type ?? "edge"})`).join("\n");
227
+ return text(`# Cross-Repo Edges${repo ? ` for ${repo}` : ""}
259
228
 
260
- ${crossRepo.length} edges found:
229
+ ${total} edges found:
261
230
 
262
- ${text}${crossRepo.length > 50 ? `
231
+ ${body}${total > edges.length ? `
263
232
 
264
- ... and ${crossRepo.length - 50} more` : ""}` }] };
233
+ ... and ${total - edges.length} more` : ""}`);
234
+ } finally {
235
+ store.close();
236
+ }
265
237
  }
266
238
  if (name === "find_callers" || name === "find_dependencies") {
267
- const funcName = args.function;
268
- const repoFilter = args.repo;
269
- const fuzzy = args.fuzzy === true;
270
- const sys = loadSystemGraph(kbPath);
271
- if (!sys) return { content: [{ type: "text", text: "No system graph found." }] };
272
- const matchKeys = new Set(resolveEntityNodes(sys.nodes, funcName, repoFilter, fuzzy));
273
- if (matchKeys.size === 0) {
274
- return { content: [{ type: "text", text: `No entity found matching "${funcName}"${repoFilter ? ` in ${repoFilter}` : ""}. Try fuzzy:true for substring matching.` }] };
275
- }
276
- const results = name === "find_callers" ? sys.edges.filter((e) => matchKeys.has(e.target) && edgeType(e) !== "contains").map((e) => e.source) : sys.edges.filter((e) => matchKeys.has(e.source) && edgeType(e) !== "contains").map((e) => e.target);
277
- const unique = [...new Set(results)].slice(0, 30);
278
- const direction = name === "find_callers" ? "Callers of" : "Dependencies of";
279
- if (unique.length === 0) {
280
- return { content: [{ type: "text", text: `# ${direction} "${funcName}"
239
+ const store = await openSystemGraphStore(kbPath);
240
+ if (!store) return text("No system graph found.");
241
+ try {
242
+ const funcName = args.function;
243
+ const repoFilter = args.repo;
244
+ const fuzzy = args.fuzzy === true;
245
+ const matched = store.resolveNodes(funcName, repoFilter, fuzzy);
246
+ if (matched.length === 0) {
247
+ return text(`No entity found matching "${funcName}"${repoFilter ? ` in ${repoFilter}` : ""}. Try fuzzy:true for substring matching.`);
248
+ }
249
+ const keys = matched.map((m) => m.key);
250
+ const unique = name === "find_callers" ? store.callers(keys, 30) : store.dependencies(keys, 30);
251
+ const direction = name === "find_callers" ? "Callers of" : "Dependencies of";
252
+ if (unique.length === 0) {
253
+ return text(`# ${direction} "${funcName}"
281
254
 
282
- Matched ${matchKeys.size} entit${matchKeys.size === 1 ? "y" : "ies"}, but no ${name === "find_callers" ? "callers" : "dependencies"} found.` }] };
283
- }
284
- return { content: [{ type: "text", text: `# ${direction} "${funcName}"
255
+ Matched ${matched.length} entit${matched.length === 1 ? "y" : "ies"}, but no ${name === "find_callers" ? "callers" : "dependencies"} found.`);
256
+ }
257
+ return text(`# ${direction} "${funcName}"
285
258
 
286
259
  ${unique.length} found:
287
- ${unique.map((r) => `- \`${r}\``).join("\n")}` }] };
260
+ ${unique.map((r) => `- \`${r}\``).join("\n")}`);
261
+ } finally {
262
+ store.close();
263
+ }
288
264
  }
289
265
  if (name === "impact_analysis") {
290
- const file = args.file;
291
- const repo = args.repo;
292
- const entity = args.entity;
293
- const sys = loadSystemGraph(kbPath);
294
- if (!sys) return { content: [{ type: "text", text: "No system graph found." }] };
295
- const fileNodes = sys.nodes.filter((n) => {
296
- const matchesFile = n.key.includes(`${repo}::${file}::`);
297
- const matchesEntity = !entity || n.key.endsWith(`::${entity}`);
298
- return matchesFile && matchesEntity;
299
- });
300
- const nodeKeys = new Set(fileNodes.map((n) => n.key));
301
- const dependents = sys.edges.filter((e) => nodeKeys.has(e.target) && !nodeKeys.has(e.source) && edgeType(e) !== "contains");
302
- const dependentRepos = new Set(dependents.map((e) => e.source.split("::")[0]));
303
- const text = [
304
- `# Impact Analysis: ${repo}/${file}${entity ? `::${entity}` : ""}`,
305
- "",
306
- `## Entities in scope: ${fileNodes.length}`,
307
- ...fileNodes.slice(0, 20).map((n) => `- \`${n.key}\``),
308
- "",
309
- `## Dependents: ${dependents.length} edges from ${dependentRepos.size} repos`,
310
- ...dependents.slice(0, 30).map((e) => `- \`${e.source}\` \u2192 \`${e.target}\` (${edgeType(e) ?? "edge"})`),
311
- dependents.length > 30 ? `
312
- ... and ${dependents.length - 30} more` : "",
313
- "",
314
- `## Affected repos: ${[...dependentRepos].join(", ") || "none"}`
315
- ].join("\n");
316
- return { content: [{ type: "text", text }] };
266
+ const store = await openSystemGraphStore(kbPath);
267
+ if (!store) return text("No system graph found.");
268
+ try {
269
+ const file = args.file;
270
+ const repo = args.repo;
271
+ const entity = args.entity;
272
+ const fileKeys = store.nodesInFiles(repo, [file], entity);
273
+ const { edges: dependents, total, repos } = store.dependents(fileKeys, 30);
274
+ const body = [
275
+ `# Impact Analysis: ${repo}/${file}${entity ? `::${entity}` : ""}`,
276
+ "",
277
+ `## Entities in scope: ${fileKeys.length}`,
278
+ ...fileKeys.slice(0, 20).map((k) => `- \`${k}\``),
279
+ "",
280
+ `## Dependents: ${total} edges from ${repos.length} repos`,
281
+ ...dependents.map((e) => `- \`${e.source}\` \u2192 \`${e.target}\` (${e.type ?? "edge"})`),
282
+ total > dependents.length ? `
283
+ ... and ${total - dependents.length} more` : "",
284
+ "",
285
+ `## Affected repos: ${repos.join(", ") || "none"}`
286
+ ].join("\n");
287
+ return text(body);
288
+ } finally {
289
+ store.close();
290
+ }
317
291
  }
318
292
  if (name === "trace_path") {
319
- const sys = loadSystemGraph(kbPath);
320
- if (!sys) return { content: [{ type: "text", text: "No system graph found." }] };
321
- const from = args.from;
322
- const to = args.to;
323
- const repo = args.repo;
324
- const fuzzy = args.fuzzy === true;
325
- const direction = args.direction ?? "callees";
326
- const maxDepth = Math.max(1, Math.min(10, args.maxDepth || 4));
327
- const fromKeys = resolveEntityNodes(sys.nodes, from, repo, fuzzy);
328
- if (fromKeys.length === 0) {
329
- return { content: [{ type: "text", text: `No entity found matching "${from}". Try fuzzy:true.` }] };
330
- }
331
- const labelOf = new Map(sys.nodes.map((n) => [n.key, nodeLabel(n)]));
332
- const adj = buildAdjacency(sys.edges, direction);
333
- if (to) {
334
- const toKeys = new Set(resolveEntityNodes(sys.nodes, to, repo, fuzzy));
335
- if (toKeys.size === 0) {
336
- return { content: [{ type: "text", text: `No entity found matching target "${to}". Try fuzzy:true.` }] };
337
- }
338
- const path = shortestPath(adj, fromKeys, toKeys, maxDepth);
339
- if (!path) {
340
- return { content: [{ type: "text", text: `No path from "${from}" to "${to}" within ${maxDepth} hops (direction: ${direction}).` }] };
341
- }
342
- const arrow = direction === "callers" ? " \u2190 " : " \u2192 ";
343
- const rendered = path.map((k) => labelOf.get(k) ?? k).join(arrow);
344
- return { content: [{ type: "text", text: `# Path (${path.length - 1} hops)
293
+ const store = await openSystemGraphStore(kbPath);
294
+ if (!store) return text("No system graph found.");
295
+ try {
296
+ const from = args.from;
297
+ const to = args.to;
298
+ const repo = args.repo;
299
+ const fuzzy = args.fuzzy === true;
300
+ const direction = args.direction ?? "callees";
301
+ const maxDepth = Math.max(1, Math.min(10, args.maxDepth || 4));
302
+ const fromKeys = store.resolveNodes(from, repo, fuzzy).map((m) => m.key);
303
+ if (fromKeys.length === 0) return text(`No entity found matching "${from}". Try fuzzy:true.`);
304
+ const getNeighbors = makeNeighborGetter(store, direction);
305
+ if (to) {
306
+ const toKeys = new Set(store.resolveNodes(to, repo, fuzzy).map((m) => m.key));
307
+ if (toKeys.size === 0) return text(`No entity found matching target "${to}". Try fuzzy:true.`);
308
+ const path = shortestPath(getNeighbors, fromKeys, toKeys, maxDepth);
309
+ if (!path) return text(`No path from "${from}" to "${to}" within ${maxDepth} hops (direction: ${direction}).`);
310
+ const labelOf2 = store.labelsOf(path);
311
+ const arrow = direction === "callers" ? " \u2190 " : " \u2192 ";
312
+ const rendered = path.map((k) => labelOf2.get(k) ?? k).join(arrow);
313
+ return text(`# Path (${path.length - 1} hops)
345
314
 
346
315
  ${rendered}
347
316
 
348
- ${path.map((k) => `- \`${k}\``).join("\n")}` }] };
349
- }
350
- const nodes = reachable(adj, fromKeys, maxDepth);
351
- if (nodes.length === 0) {
352
- return { content: [{ type: "text", text: `No ${direction} reachable from "${from}" within ${maxDepth} hops.` }] };
353
- }
354
- const byDepth = /* @__PURE__ */ new Map();
355
- for (const { key, depth } of nodes) {
356
- if (!byDepth.has(depth)) byDepth.set(depth, []);
357
- byDepth.get(depth).push(`\`${labelOf.get(key) ?? key}\` (${key})`);
358
- }
359
- const sections = [...byDepth.entries()].sort((a, b) => a[0] - b[0]).map(([d, ks]) => `## Depth ${d}
317
+ ${path.map((k) => `- \`${k}\``).join("\n")}`);
318
+ }
319
+ const nodes = reachable(getNeighbors, fromKeys, maxDepth);
320
+ if (nodes.length === 0) return text(`No ${direction} reachable from "${from}" within ${maxDepth} hops.`);
321
+ const labelOf = store.labelsOf(nodes.map((n) => n.key));
322
+ const byDepth = /* @__PURE__ */ new Map();
323
+ for (const { key, depth } of nodes) {
324
+ if (!byDepth.has(depth)) byDepth.set(depth, []);
325
+ byDepth.get(depth).push(`\`${labelOf.get(key) ?? key}\` (${key})`);
326
+ }
327
+ const sections = [...byDepth.entries()].sort((a, b) => a[0] - b[0]).map(([d, ks]) => `## Depth ${d}
360
328
  ${ks.map((k) => `- ${k}`).join("\n")}`).join("\n\n");
361
- return { content: [{ type: "text", text: `# Reachable from "${from}" (${direction}, \u2264${maxDepth} hops)
329
+ return text(`# Reachable from "${from}" (${direction}, \u2264${maxDepth} hops)
362
330
 
363
331
  ${nodes.length} nodes:
364
332
 
365
- ${sections}` }] };
333
+ ${sections}`);
334
+ } finally {
335
+ store.close();
336
+ }
366
337
  }
367
338
  if (name === "find_dead_code") {
368
339
  const repo = args.repo;
369
340
  const limit = args.limit || 50;
370
341
  const graphPath = join(kbPath, repo, "graph.json");
371
- if (!existsSync(graphPath)) {
372
- return { content: [{ type: "text", text: `No graph found for repo "${repo}"` }] };
373
- }
342
+ if (!existsSync(graphPath)) return text(`No graph found for repo "${repo}"`);
374
343
  const graph = JSON.parse(readFileSync(graphPath, "utf-8"));
375
344
  const ENTITY_TYPES = /* @__PURE__ */ new Set(["function", "method", "class", "struct", "enum", "trait"]);
376
345
  const inDegree = /* @__PURE__ */ new Map();
@@ -379,13 +348,11 @@ ${sections}` }] };
379
348
  inDegree.set(l.target, (inDegree.get(l.target) ?? 0) + 1);
380
349
  }
381
350
  const dead = (graph.nodes ?? []).filter((n) => ENTITY_TYPES.has(n.type) && (inDegree.get(n.id) ?? 0) === 0);
382
- if (dead.length === 0) {
383
- return { content: [{ type: "text", text: `# Dead code in ${repo}
351
+ if (dead.length === 0) return text(`# Dead code in ${repo}
384
352
 
385
- No zero-caller entities found.` }] };
386
- }
353
+ No zero-caller entities found.`);
387
354
  const rows = dead.slice(0, limit).map((n) => `- \`${n.label ?? n.id}\` (${n.type})${n.file ? ` \u2014 ${n.file}` : ""}`).join("\n");
388
- return { content: [{ type: "text", text: `# Dead code in ${repo} (heuristic)
355
+ return text(`# Dead code in ${repo} (heuristic)
389
356
 
390
357
  ${dead.length} zero-caller entit${dead.length === 1 ? "y" : "ies"}:
391
358
 
@@ -393,17 +360,15 @@ ${rows}${dead.length > limit ? `
393
360
 
394
361
  ... and ${dead.length - limit} more` : ""}
395
362
 
396
- _Note: exported APIs, reflection, and dynamic dispatch can cause false positives._` }] };
363
+ _Note: exported APIs, reflection, and dynamic dispatch can cause false positives._`);
397
364
  }
398
365
  if (name === "get_architecture") {
399
366
  const repo = args.repo;
400
367
  if (repo) {
401
368
  const profile = loadProfile(ctx.projectName, repo);
402
- if (!profile) {
403
- return { content: [{ type: "text", text: `No profile for "${repo}". Run profiling (requires LLM) to generate one.` }] };
404
- }
369
+ if (!profile) return text(`No profile for "${repo}". Run profiling (requires LLM) to generate one.`);
405
370
  const ep = (xs) => xs?.length ? xs.map((e) => ` - ${e.type}: ${e.identifier} \u2014 ${e.description}`).join("\n") : " - (none)";
406
- const text = [
371
+ const body = [
407
372
  `# ${profile.name} \u2014 ${profile.role} (${profile.domain})`,
408
373
  "",
409
374
  profile.description,
@@ -417,7 +382,7 @@ _Note: exported APIs, reflection, and dynamic dispatch can cause false positives
417
382
  "## Consumes",
418
383
  ep(profile.consumes)
419
384
  ].join("\n");
420
- return { content: [{ type: "text", text }] };
385
+ return text(body);
421
386
  }
422
387
  const pgPath = join(kbPath, "PROJECT_GRAPH.json");
423
388
  const pg = existsSync(pgPath) ? JSON.parse(readFileSync(pgPath, "utf-8")) : null;
@@ -425,15 +390,13 @@ _Note: exported APIs, reflection, and dynamic dispatch can cause false positives
425
390
  const lines = ["# Project Architecture", "", pg.architectureSummary ?? ""];
426
391
  if (pg.repoRoles && Object.keys(pg.repoRoles).length) {
427
392
  lines.push("", "## Repo Roles");
428
- for (const [name2, r] of Object.entries(pg.repoRoles)) {
429
- lines.push(`- **${name2}** \u2014 ${r.role} (${r.criticality}): ${(r.responsibilities ?? []).join("; ")}`);
393
+ for (const [n, r] of Object.entries(pg.repoRoles)) {
394
+ lines.push(`- **${n}** \u2014 ${r.role} (${r.criticality}): ${(r.responsibilities ?? []).join("; ")}`);
430
395
  }
431
396
  }
432
397
  if (pg.relationships?.length) {
433
398
  lines.push("", "## Relationships");
434
- for (const rel of pg.relationships) {
435
- lines.push(`- ${rel.from} \u2192 ${rel.to} (${rel.type}): ${rel.description}`);
436
- }
399
+ for (const rel of pg.relationships) lines.push(`- ${rel.from} \u2192 ${rel.to} (${rel.type}): ${rel.description}`);
437
400
  }
438
401
  if (pg.keyFlows?.length) {
439
402
  lines.push("", "## Key Flows");
@@ -442,77 +405,51 @@ _Note: exported APIs, reflection, and dynamic dispatch can cause false positives
442
405
  for (const s of f.steps ?? []) lines.push(` - ${s.repo}/${s.component}: ${s.action} [${s.protocol}]`);
443
406
  }
444
407
  }
445
- return { content: [{ type: "text", text: lines.join("\n") }] };
408
+ return text(lines.join("\n"));
446
409
  }
447
410
  const summaryPath = join(kbPath, "PROJECT_SUMMARY.md");
448
- if (existsSync(summaryPath)) {
449
- return { content: [{ type: "text", text: readFileSync(summaryPath, "utf-8") }] };
450
- }
411
+ if (existsSync(summaryPath)) return text(readFileSync(summaryPath, "utf-8"));
451
412
  const profiles = loadAllProfiles(ctx.projectName);
452
413
  if (profiles.length) {
453
- const text = `# Project Repos
414
+ return text(`# Project Repos
454
415
 
455
416
  ${profiles.map((p) => `- **${p.name}** \u2014 ${p.role} (${p.domain}): ${p.description}`).join("\n")}
456
417
 
457
- _Run project-graph generation (requires LLM) for a full architecture view._`;
458
- return { content: [{ type: "text", text }] };
418
+ _Run project-graph generation (requires LLM) for a full architecture view._`);
459
419
  }
460
- return { content: [{ type: "text", text: "No architecture data yet. Index the project, then run profiling / project-graph generation (requires an LLM) for an architecture overview." }] };
420
+ return text("No architecture data yet. Index the project, then run profiling / project-graph generation (requires an LLM) for an architecture overview.");
461
421
  }
462
422
  if (name === "search_graph") {
463
- const sys = loadSystemGraph(kbPath);
464
- if (!sys) return { content: [{ type: "text", text: "No system graph found. Build KB first." }] };
465
- const repo = args.repo;
466
- const type = args.type;
467
- const file = args.file;
468
- const minDegree = args.minDegree ?? 0;
469
- const limit = args.limit || 50;
470
- const nameArg = args.name;
471
- let nameTest = () => true;
472
- if (nameArg) {
473
- try {
474
- const re = new RegExp(nameArg, "i");
475
- nameTest = (s) => re.test(s);
476
- } catch {
477
- const lc = nameArg.toLowerCase();
478
- nameTest = (s) => s.toLowerCase().includes(lc);
479
- }
480
- }
481
- const degree = /* @__PURE__ */ new Map();
482
- for (const e of sys.edges) {
483
- if (edgeType(e) === "contains") continue;
484
- degree.set(e.source, (degree.get(e.source) ?? 0) + 1);
485
- degree.set(e.target, (degree.get(e.target) ?? 0) + 1);
486
- }
487
- const matches = sys.nodes.filter((n) => {
488
- if (repo && !n.key.startsWith(repo + "::")) return false;
489
- if (type && n.attributes?.type !== type) return false;
490
- const f = n.attributes?.file ?? n.key.split("::")[1] ?? "";
491
- if (file && !f.includes(file)) return false;
492
- if (nameArg && !(nameTest(n.attributes?.label ?? "") || nameTest(n.key))) return false;
493
- return (degree.get(n.key) ?? 0) >= minDegree;
494
- }).map((n) => ({ key: n.key, label: nodeLabel(n), type: n.attributes?.type ?? "?", file: n.attributes?.file ?? n.key.split("::")[1] ?? "", deg: degree.get(n.key) ?? 0 })).sort((a, b) => b.deg - a.deg);
495
- if (matches.length === 0) {
496
- return { content: [{ type: "text", text: "No entities match the given filters." }] };
497
- }
498
- const rows = matches.slice(0, limit).map((m) => `- \`${m.label}\` (${m.type}, ${m.file}) \u2014 degree ${m.deg} \`${m.key}\``).join("\n");
499
- return { content: [{ type: "text", text: `# search_graph \u2014 ${matches.length} match${matches.length === 1 ? "" : "es"}
423
+ const store = await openSystemGraphStore(kbPath);
424
+ if (!store) return text(NO_GRAPH);
425
+ try {
426
+ const { rows, total } = store.searchNodes({
427
+ name: args.name,
428
+ type: args.type,
429
+ file: args.file,
430
+ repo: args.repo,
431
+ minDegree: args.minDegree ?? 0
432
+ }, args.limit || 50);
433
+ if (total === 0) return text("No entities match the given filters.");
434
+ const body = rows.map((m) => `- \`${m.label}\` (${m.type}, ${m.file}) \u2014 degree ${m.degree} \`${m.key}\``).join("\n");
435
+ return text(`# search_graph \u2014 ${total} match${total === 1 ? "" : "es"}
500
436
 
501
- ${rows}${matches.length > limit ? `
437
+ ${body}${total > rows.length ? `
502
438
 
503
- ... and ${matches.length - limit} more` : ""}` }] };
439
+ ... and ${total - rows.length} more` : ""}`);
440
+ } finally {
441
+ store.close();
442
+ }
504
443
  }
505
444
  if (name === "detect_changes") {
506
445
  const repo = args.repo;
507
446
  const limit = args.limit || 50;
508
447
  if (!ctx.directoryPath) {
509
- return { content: [{ type: "text", text: "detect_changes needs a local repo path \u2014 available in local/serve mode only (not remote-proxy mode)." }] };
448
+ return text("detect_changes needs a local repo path \u2014 available in local/serve mode only (not remote-proxy mode).");
510
449
  }
511
450
  const candidate = join(ctx.directoryPath, repo);
512
451
  const repoPath = existsSync(join(candidate, ".git")) ? candidate : ctx.directoryPath;
513
- if (!existsSync(repoPath)) {
514
- return { content: [{ type: "text", text: `Repo path not found: ${repoPath}` }] };
515
- }
452
+ if (!existsSync(repoPath)) return text(`Repo path not found: ${repoPath}`);
516
453
  let baseSha = args.baseSha;
517
454
  if (!baseSha) {
518
455
  const metaPath = join(kbPath, repo, "index_meta.json");
@@ -523,43 +460,42 @@ ${rows}${matches.length > limit ? `
523
460
  }
524
461
  }
525
462
  }
526
- if (!baseSha) {
527
- return { content: [{ type: "text", text: "No base commit available. Pass baseSha, or index the repo first so a last-indexed SHA exists." }] };
528
- }
463
+ if (!baseSha) return text("No base commit available. Pass baseSha, or index the repo first so a last-indexed SHA exists.");
529
464
  const diff = getAllChanges(repoPath, baseSha);
530
- if (diff.fallbackToFull) {
531
- return { content: [{ type: "text", text: `git diff against ${baseSha.slice(0, 7)} failed or is too large to map incrementally. Check the base SHA.` }] };
532
- }
465
+ if (diff.fallbackToFull) return text(`git diff against ${baseSha.slice(0, 7)} failed or is too large to map incrementally. Check the base SHA.`);
533
466
  const changedFiles = getChangedFilesList(diff);
534
- if (changedFiles.length === 0 && diff.deleted.length === 0) {
535
- return { content: [{ type: "text", text: `No source changes since ${baseSha.slice(0, 7)}.` }] };
536
- }
537
- const sys = loadSystemGraph(kbPath);
538
- const changedKeys = /* @__PURE__ */ new Set();
539
- if (sys) {
540
- for (const f of changedFiles) {
541
- for (const n of sys.nodes) {
542
- if (n.key.startsWith(`${repo}::${f}::`) || n.key === `${repo}::${f}`) changedKeys.add(n.key);
543
- }
467
+ if (changedFiles.length === 0 && diff.deleted.length === 0) return text(`No source changes since ${baseSha.slice(0, 7)}.`);
468
+ const store = await openSystemGraphStore(kbPath);
469
+ let changedKeys = [];
470
+ let dependents = [];
471
+ let depTotal = 0;
472
+ let depRepos = [];
473
+ if (store) {
474
+ try {
475
+ changedKeys = store.nodesInFiles(repo, changedFiles);
476
+ const d = store.dependents(changedKeys, limit);
477
+ dependents = d.edges;
478
+ depTotal = d.total;
479
+ depRepos = d.repos;
480
+ } finally {
481
+ store.close();
544
482
  }
545
483
  }
546
- const dependents = sys ? sys.edges.filter((e) => changedKeys.has(e.target) && !changedKeys.has(e.source) && edgeType(e) !== "contains") : [];
547
- const dependentRepos = new Set(dependents.map((e) => e.source.split("::")[0]));
548
484
  const lines = [
549
485
  `# Changes since ${baseSha.slice(0, 7)} \u2014 ${repo}`,
550
486
  "",
551
487
  `## Changed files: ${changedFiles.length} (+${diff.added.length} / ~${diff.modified.length}), deleted ${diff.deleted.length}`,
552
488
  ...changedFiles.slice(0, 30).map((f) => `- ${f}`),
553
489
  "",
554
- `## Affected entities: ${changedKeys.size}`,
555
- ...[...changedKeys].slice(0, 30).map((k) => `- \`${k}\``),
490
+ `## Affected entities: ${changedKeys.length}`,
491
+ ...changedKeys.slice(0, 30).map((k) => `- \`${k}\``),
556
492
  "",
557
- `## Dependents: ${dependents.length} edges from ${dependentRepos.size} repo(s)`,
558
- ...dependents.slice(0, limit).map((e) => `- \`${e.source}\` \u2192 \`${e.target}\` (${edgeType(e) ?? "edge"})`),
493
+ `## Dependents: ${depTotal} edges from ${depRepos.length} repo(s)`,
494
+ ...dependents.map((e) => `- \`${e.source}\` \u2192 \`${e.target}\` (${e.type ?? "edge"})`),
559
495
  "",
560
- `## Affected repos: ${[...dependentRepos].join(", ") || "none"}`
496
+ `## Affected repos: ${depRepos.join(", ") || "none"}`
561
497
  ];
562
- return { content: [{ type: "text", text: lines.join("\n") }] };
498
+ return text(lines.join("\n"));
563
499
  }
564
500
  return null;
565
501
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esankhan3/code-search-mcp",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "MCP server for multi-repo code search — GitHub org or local path, zero config",
5
5
  "type": "module",
6
6
  "license": "MIT",