@nithin554/code-crawler 0.1.55 → 0.1.58

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 (49) hide show
  1. package/README.md +24 -2
  2. package/dist/config.js +1 -1
  3. package/dist/config.js.map +1 -1
  4. package/dist/crawler.d.ts +1 -1
  5. package/dist/crawler.js +42 -15
  6. package/dist/crawler.js.map +1 -1
  7. package/dist/graph/budget.d.ts +46 -0
  8. package/dist/graph/budget.js +72 -0
  9. package/dist/graph/budget.js.map +1 -0
  10. package/dist/graph/insights.d.ts +217 -0
  11. package/dist/graph/insights.js +616 -0
  12. package/dist/graph/insights.js.map +1 -0
  13. package/dist/graph/queries.d.ts +15 -1
  14. package/dist/graph/queries.js +25 -28
  15. package/dist/graph/queries.js.map +1 -1
  16. package/dist/graph/store.d.ts +1 -1
  17. package/dist/graph/store.js +4 -10
  18. package/dist/graph/store.js.map +1 -1
  19. package/dist/index.js +2 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/parser/cache.js.map +1 -1
  22. package/dist/parser/controlFlow.js +1 -3
  23. package/dist/parser/controlFlow.js.map +1 -1
  24. package/dist/parser/languages/java.js +22 -7
  25. package/dist/parser/languages/java.js.map +1 -1
  26. package/dist/parser/languages/python.js +50 -18
  27. package/dist/parser/languages/python.js.map +1 -1
  28. package/dist/parser/languages/typescript.js +58 -19
  29. package/dist/parser/languages/typescript.js.map +1 -1
  30. package/dist/parser/moduleId.js +2 -1
  31. package/dist/parser/moduleId.js.map +1 -1
  32. package/dist/parser/treeSitter.d.ts +1 -1
  33. package/dist/parser/treeSitter.js +2 -2
  34. package/dist/parser/treeSitter.js.map +1 -1
  35. package/dist/resolver/common.js +89 -23
  36. package/dist/resolver/common.js.map +1 -1
  37. package/dist/resolver/dynamic.d.ts +1 -1
  38. package/dist/resolver/dynamic.js +4 -2
  39. package/dist/resolver/dynamic.js.map +1 -1
  40. package/dist/resolver/genericResolver.js +61 -12
  41. package/dist/resolver/genericResolver.js.map +1 -1
  42. package/dist/resolver/javaResolver.js +7 -4
  43. package/dist/resolver/javaResolver.js.map +1 -1
  44. package/dist/resolver/types.js +1 -1
  45. package/dist/resolver/types.js.map +1 -1
  46. package/dist/server.d.ts +1 -1
  47. package/dist/server.js +491 -173
  48. package/dist/server.js.map +1 -1
  49. package/package.json +9 -3
package/dist/server.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { NODE_KINDS, EDGE_KINDS } from "./graph/schema.js";
4
- import { crawlProject } from "./crawler.js";
5
- import { getCallers, getCallees, tracePath, traceExecution, getSubgraph, searchSymbols, suggestSymbols, findImplementations, getAspects, getDecorators, findAnnotated, hotspots, deadCode, findEntryPoints, findInjections, findReflections, findRoutes, externalDependencies, } from "./graph/queries.js";
6
3
  import { loadConfig } from "./config.js";
4
+ import { crawlProject } from "./crawler.js";
5
+ import { applyBudget, pageMeta } from "./graph/budget.js";
6
+ import { explainSymbol, exportGraph, findCycles, findUnresolved, impactAnalysis, unresolvedForNode, } from "./graph/insights.js";
7
+ import { deadCode, externalDependencies, findAnnotated, findEntryPoints, findImplementations, findInjections, findReflections, findRoutes, getAspects, getCallees, getCallers, getDecorators, getSubgraph, hotspots, searchSymbols, suggestSymbols, traceExecution, tracePath, } from "./graph/queries.js";
8
+ import { EDGE_KINDS, NODE_KINDS } from "./graph/schema.js";
7
9
  /**
8
10
  * MCP server surface for code-crawler.
9
11
  *
@@ -29,6 +31,18 @@ export function createServer(store, config) {
29
31
  kind: s.kind,
30
32
  filePath: s.filePath,
31
33
  });
34
+ /** Project a framework-wiring edge (route, injection, reflection). */
35
+ const wiring = (e) => ({
36
+ source: ref(e.source),
37
+ target: ref(e.target),
38
+ ...(e.httpMethod ? { httpMethod: e.httpMethod } : {}),
39
+ ...(e.path ? { path: e.path } : {}),
40
+ ...(e.framework ? { framework: e.framework } : {}),
41
+ ...(e.mechanism ? { mechanism: e.mechanism } : {}),
42
+ ...(e.injectedName ? { injectedName: e.injectedName } : {}),
43
+ ...(e.external ? { external: true } : {}),
44
+ ...(e.line !== undefined ? { line: line(e.line) } : {}),
45
+ });
32
46
  const notFound = (symbol) => {
33
47
  const suggestions = suggestSymbols(store, symbol);
34
48
  return text({
@@ -46,6 +60,40 @@ export function createServer(store, config) {
46
60
  hint: "Check the tool arguments and the index state (index_status); if the index is from an older version, run clear_index and crawl again.",
47
61
  });
48
62
  };
63
+ /**
64
+ * Opt-in pagination parameters. When neither is supplied the response shape
65
+ * is byte-identical to the pre-pagination shape.
66
+ */
67
+ const budgetParams = {
68
+ max_tokens: z
69
+ .number()
70
+ .int()
71
+ .min(100)
72
+ .max(200000)
73
+ .optional()
74
+ .describe("Soft cap on the estimated token cost of the returned list (about 4 characters per token). When the list does not fit, the response gains a `page` block carrying `nextCursor`."),
75
+ cursor: z
76
+ .number()
77
+ .int()
78
+ .min(0)
79
+ .optional()
80
+ .describe("Offset into the list; pass page.nextCursor from a previous response to continue."),
81
+ };
82
+ /**
83
+ * Attach a `page` block and cut the list when the caller asked for a budget.
84
+ * Counting keys keep reporting the full window size so clients can decide
85
+ * whether to ask for the next page. Each tool keeps its own counting key name,
86
+ * so a payload only gains `count` when it already had one.
87
+ */
88
+ const paged = (payload, key, items, args) => {
89
+ if (args.max_tokens === undefined && args.cursor === undefined)
90
+ return payload;
91
+ const page = applyBudget(items, { maxTokens: args.max_tokens, cursor: args.cursor });
92
+ const next = { ...payload, [key]: page.items, page: pageMeta(page) };
93
+ if ("count" in payload)
94
+ next.count = items.length;
95
+ return next;
96
+ };
49
97
  /**
50
98
  * Every handler shares one error boundary so MCP clients always receive the
51
99
  * same `{ ok, error, hint }` shape instead of a mix of throws and payloads.
@@ -84,7 +132,8 @@ export function createServer(store, config) {
84
132
  onProgress: progressToken === undefined
85
133
  ? undefined
86
134
  : (progress) => {
87
- if (progress.processed !== progress.total && progress.processed % step(progress.total) !== 0) {
135
+ if (progress.processed !== progress.total &&
136
+ progress.processed % step(progress.total) !== 0) {
88
137
  return;
89
138
  }
90
139
  void extra
@@ -117,15 +166,16 @@ export function createServer(store, config) {
117
166
  .optional()
118
167
  .describe("Restrict results to a node kind (class, method, field, ...)."),
119
168
  limit: z.number().int().min(1).max(200).optional().describe("Max results (default 50)."),
120
- }, async ({ query, kind, limit }) => {
169
+ ...budgetParams,
170
+ }, async ({ query, kind, limit, max_tokens, cursor }) => {
121
171
  const results = searchSymbols(store, query, { kind, limit });
122
- return text({
123
- query,
124
- count: results.length,
125
- results: results.map(ref),
126
- });
172
+ const refs = results.map(ref);
173
+ return text(paged({ query, count: refs.length, results: refs }, "results", refs, {
174
+ max_tokens,
175
+ cursor,
176
+ }));
127
177
  });
128
- tool("get_node", "Get metadata and source location for a single symbol by id or qualified name.", {
178
+ tool("get_node", "Get metadata and source location for a single symbol by id or qualified name. Includes an `unresolved` block when the symbol has call sites the resolver could not bind, or when unresolved calls elsewhere point at it.", {
129
179
  qualified_name: z
130
180
  .string()
131
181
  .describe("Symbol id or qualified name, e.g. 'com.acme.OrderService#place'."),
@@ -133,6 +183,7 @@ export function createServer(store, config) {
133
183
  const node = store.getNode(qualified_name) ?? store.getNodeByQualifiedName(qualified_name);
134
184
  if (!node)
135
185
  return notFound(qualified_name);
186
+ const unresolved = unresolvedForNode(store, node.id);
136
187
  return text({
137
188
  id: node.id,
138
189
  kind: node.kind,
@@ -143,6 +194,23 @@ export function createServer(store, config) {
143
194
  startLine: line(node.properties.startLine),
144
195
  endLine: line(node.properties.endLine),
145
196
  },
197
+ ...(unresolved
198
+ ? {
199
+ unresolved: {
200
+ callerCount: unresolved.outgoing.length,
201
+ referencedByCount: unresolved.incoming.length,
202
+ calls: unresolved.outgoing.map((c) => ({
203
+ target: c.targetLabel,
204
+ reason: c.reason,
205
+ line: line(c.line),
206
+ })),
207
+ referencedBy: unresolved.incoming.map((i) => ({
208
+ caller: ref(i.caller),
209
+ line: line(i.line),
210
+ })),
211
+ },
212
+ }
213
+ : {}),
146
214
  });
147
215
  });
148
216
  tool("get_callers", "Find every symbol that calls the given symbol, directly or transitively (the call graph above it). Optionally follow virtual-dispatch and aspect-advice edges for dynamic callers.", {
@@ -158,7 +226,8 @@ export function createServer(store, config) {
158
226
  .describe("Also follow ADVISES edges (aspect advice methods)."),
159
227
  max_depth: z.number().int().min(1).max(50).optional().describe("Traversal depth cap."),
160
228
  limit: z.number().int().min(1).max(500).optional().describe("Max callers returned."),
161
- }, async ({ symbol, transitive, include_dynamic, include_advice, max_depth, limit }) => {
229
+ ...budgetParams,
230
+ }, async ({ symbol, transitive, include_dynamic, include_advice, max_depth, limit, max_tokens, cursor, }) => {
162
231
  const result = getCallers(store, symbol, {
163
232
  transitive,
164
233
  includeDynamic: include_dynamic,
@@ -168,15 +237,16 @@ export function createServer(store, config) {
168
237
  });
169
238
  if (!result)
170
239
  return notFound(symbol);
171
- return text({
172
- target: ref(result.target),
173
- callers: result.callers.map((c) => ({
174
- caller: ref(c.caller),
175
- depth: c.depth,
176
- callSiteLine: line(c.callSiteLine),
177
- edgeKind: c.edgeKind,
178
- })),
179
- });
240
+ const callers = result.callers.map((c) => ({
241
+ caller: ref(c.caller),
242
+ depth: c.depth,
243
+ callSiteLine: line(c.callSiteLine),
244
+ edgeKind: c.edgeKind,
245
+ }));
246
+ return text(paged({ target: ref(result.target), count: callers.length, callers }, "callers", callers, {
247
+ max_tokens,
248
+ cursor,
249
+ }));
180
250
  });
181
251
  tool("get_callees", "Find every symbol the given symbol calls, directly or transitively (the call graph below it). Set include_dynamic to also follow virtual/interface dispatch targets.", {
182
252
  symbol: z.string().describe("Method/constructor id or qualified name."),
@@ -191,7 +261,8 @@ export function createServer(store, config) {
191
261
  .describe("Also follow ADVISES edges (aspect advice methods)."),
192
262
  max_depth: z.number().int().min(1).max(50).optional().describe("Traversal depth cap."),
193
263
  limit: z.number().int().min(1).max(500).optional().describe("Max callees returned."),
194
- }, async ({ symbol, transitive, include_dynamic, include_advice, max_depth, limit }) => {
264
+ ...budgetParams,
265
+ }, async ({ symbol, transitive, include_dynamic, include_advice, max_depth, limit, max_tokens, cursor, }) => {
195
266
  const result = getCallees(store, symbol, {
196
267
  transitive,
197
268
  includeDynamic: include_dynamic,
@@ -201,15 +272,16 @@ export function createServer(store, config) {
201
272
  });
202
273
  if (!result)
203
274
  return notFound(symbol);
204
- return text({
205
- source: ref(result.source),
206
- callees: result.callees.map((c) => ({
207
- callee: ref(c.callee),
208
- depth: c.depth,
209
- callSiteLine: line(c.callSiteLine),
210
- edgeKind: c.edgeKind,
211
- })),
212
- });
275
+ const callees = result.callees.map((c) => ({
276
+ callee: ref(c.callee),
277
+ depth: c.depth,
278
+ callSiteLine: line(c.callSiteLine),
279
+ edgeKind: c.edgeKind,
280
+ }));
281
+ return text(paged({ source: ref(result.source), count: callees.length, callees }, "callees", callees, {
282
+ max_tokens,
283
+ cursor,
284
+ }));
213
285
  });
214
286
  tool("trace_path", "Find call paths between two symbols. Follows static CALLS edges by default; set include_dynamic/include_advice to also route through virtual dispatch and aspect advice.", {
215
287
  source: z.string().describe("Starting symbol id or qualified name."),
@@ -223,8 +295,15 @@ export function createServer(store, config) {
223
295
  .optional()
224
296
  .describe("Also follow ADVISES edges (aspect advice methods)."),
225
297
  max_depth: z.number().int().min(1).max(50).optional().describe("Maximum path length."),
226
- limit: z.number().int().min(1).max(100).optional().describe("Max paths returned (default 10)."),
227
- }, async ({ source, target, include_dynamic, include_advice, max_depth, limit }) => {
298
+ limit: z
299
+ .number()
300
+ .int()
301
+ .min(1)
302
+ .max(100)
303
+ .optional()
304
+ .describe("Max paths returned (default 10)."),
305
+ ...budgetParams,
306
+ }, async ({ source, target, include_dynamic, include_advice, max_depth, limit, max_tokens, cursor, }) => {
228
307
  const paths = tracePath(store, source, target, {
229
308
  includeDynamic: include_dynamic,
230
309
  includeAdvice: include_advice,
@@ -233,21 +312,20 @@ export function createServer(store, config) {
233
312
  });
234
313
  if (!paths)
235
314
  return notFound(source);
236
- return text({
237
- source,
238
- target,
239
- pathCount: paths.length,
240
- paths: paths.map((p) => ({
241
- length: p.length,
242
- path: p.path,
243
- edges: p.edges.map((e) => ({
244
- source: e.source,
245
- target: e.target,
246
- kind: e.kind,
247
- callSiteLine: line(e.callSiteLine),
248
- })),
315
+ const mapped = paths.map((p) => ({
316
+ length: p.length,
317
+ path: p.path,
318
+ edges: p.edges.map((e) => ({
319
+ source: e.source,
320
+ target: e.target,
321
+ kind: e.kind,
322
+ callSiteLine: line(e.callSiteLine),
249
323
  })),
250
- });
324
+ }));
325
+ return text(paged({ source, target, pathCount: mapped.length, paths: mapped }, "paths", mapped, {
326
+ max_tokens,
327
+ cursor,
328
+ }));
251
329
  });
252
330
  tool("trace_execution", "Produce a dynamic execution trace from a symbol: follows static and virtual-dispatch calls and weaves in aspect advice. Steps show how each symbol was reached, including INTERCEPT hops where advice wraps a call.", {
253
331
  symbol: z.string().describe("Starting method or constructor id/qualified name."),
@@ -261,7 +339,8 @@ export function createServer(store, config) {
261
339
  .describe("Weave aspect advice into the trace (default true)."),
262
340
  max_depth: z.number().int().min(1).max(50).optional().describe("Traversal depth cap."),
263
341
  limit: z.number().int().min(1).max(1000).optional().describe("Max steps returned."),
264
- }, async ({ symbol, include_dynamic, include_advice, max_depth, limit }) => {
342
+ ...budgetParams,
343
+ }, async ({ symbol, include_dynamic, include_advice, max_depth, limit, max_tokens, cursor }) => {
265
344
  const result = traceExecution(store, symbol, {
266
345
  includeDynamic: include_dynamic,
267
346
  includeAdvice: include_advice,
@@ -270,51 +349,55 @@ export function createServer(store, config) {
270
349
  });
271
350
  if (!result)
272
351
  return notFound(symbol);
273
- return text({
274
- root: ref(result.root),
275
- stepCount: result.steps.length,
276
- steps: result.steps.map((s) => ({
277
- symbol: ref(s.symbol),
278
- depth: s.depth,
279
- via: s.via,
280
- callSiteLine: line(s.callSiteLine),
281
- })),
282
- });
352
+ const steps = result.steps.map((s) => ({
353
+ symbol: ref(s.symbol),
354
+ depth: s.depth,
355
+ via: s.via,
356
+ callSiteLine: line(s.callSiteLine),
357
+ }));
358
+ return text(paged({ root: ref(result.root), stepCount: steps.length, steps }, "steps", steps, {
359
+ max_tokens,
360
+ cursor,
361
+ }));
283
362
  });
284
363
  tool("find_implementations", "Find every class/interface that implements or extends a type, directly or transitively. Use this to understand virtual dispatch candidates.", {
285
364
  symbol: z.string().describe("Interface or superclass id/qualified name."),
286
365
  max_depth: z.number().int().min(1).max(20).optional().describe("Hierarchy depth cap."),
287
366
  limit: z.number().int().min(1).max(500).optional().describe("Max implementations returned."),
288
- }, async ({ symbol, max_depth, limit }) => {
367
+ ...budgetParams,
368
+ }, async ({ symbol, max_depth, limit, max_tokens, cursor }) => {
289
369
  const result = findImplementations(store, symbol, { maxDepth: max_depth, limit });
290
370
  if (!result)
291
371
  return notFound(symbol);
292
- return text({
372
+ const implementations = result.implementations.map((i) => ({
373
+ type: ref(i.type),
374
+ depth: i.depth,
375
+ via: i.via,
376
+ }));
377
+ return text(paged({
293
378
  target: ref(result.target),
294
- implementationCount: result.implementations.length,
295
- implementations: result.implementations.map((i) => ({
296
- type: ref(i.type),
297
- depth: i.depth,
298
- via: i.via,
299
- })),
300
- });
379
+ implementationCount: implementations.length,
380
+ implementations,
381
+ }, "implementations", implementations, { max_tokens, cursor }));
301
382
  });
302
383
  tool("find_aspects", "List aspects with their advice methods (@Before/@After/@Around/...), pointcuts, and the methods each advice intercepts.", {
303
384
  limit: z.number().int().min(1).max(200).optional().describe("Max aspects returned."),
304
- }, async ({ limit }) => {
385
+ ...budgetParams,
386
+ }, async ({ limit, max_tokens, cursor }) => {
305
387
  const aspects = getAspects(store, { limit });
306
- return text({
307
- aspectCount: aspects.length,
308
- aspects: aspects.map((a) => ({
309
- aspect: ref(a.aspect),
310
- advice: a.advice.map((adv) => ({
311
- method: ref(adv.method),
312
- adviceKind: adv.adviceKind,
313
- pointcut: adv.pointcut,
314
- advised: adv.advised.map(ref),
315
- })),
388
+ const mapped = aspects.map((a) => ({
389
+ aspect: ref(a.aspect),
390
+ advice: a.advice.map((adv) => ({
391
+ method: ref(adv.method),
392
+ adviceKind: adv.adviceKind,
393
+ pointcut: adv.pointcut,
394
+ advised: adv.advised.map(ref),
316
395
  })),
317
- });
396
+ }));
397
+ return text(paged({ aspectCount: mapped.length, aspects: mapped }, "aspects", mapped, {
398
+ max_tokens,
399
+ cursor,
400
+ }));
318
401
  });
319
402
  tool("get_decorators", "Get the annotations/decorators applied to a symbol. Set include_members to also list decorators on its methods, constructors, and fields.", {
320
403
  symbol: z.string().describe("Symbol id or qualified name."),
@@ -322,41 +405,46 @@ export function createServer(store, config) {
322
405
  .boolean()
323
406
  .optional()
324
407
  .describe("Include decorators on declared members (default false)."),
325
- }, async ({ symbol, include_members }) => {
408
+ ...budgetParams,
409
+ }, async ({ symbol, include_members, max_tokens, cursor }) => {
326
410
  const result = getDecorators(store, symbol, { includeMembers: include_members });
327
411
  if (!result)
328
412
  return notFound(symbol);
329
- return text({
413
+ const members = result.members.map((m) => ({
414
+ symbol: ref(m.symbol),
415
+ decorators: m.decorators.map((d) => ({ ...d, line: line(d.line) })),
416
+ }));
417
+ return text(paged({
330
418
  symbol: ref(result.symbol),
331
419
  decorators: result.decorators.map((d) => ({
332
420
  ...d,
333
421
  line: line(d.line),
334
422
  })),
335
- members: result.members.map((m) => ({
336
- symbol: ref(m.symbol),
337
- decorators: m.decorators.map((d) => ({ ...d, line: line(d.line) })),
338
- })),
339
- });
423
+ members,
424
+ }, "members", members, { max_tokens, cursor }));
340
425
  });
341
426
  tool("find_annotated", "Find every symbol annotated/decorated with a given annotation (simple name, @name, or FQN).", {
342
- annotation: z.string().describe("Annotation name, e.g. 'Transactional', '@Aspect' or an FQN."),
427
+ annotation: z
428
+ .string()
429
+ .describe("Annotation name, e.g. 'Transactional', '@Aspect' or an FQN."),
343
430
  kind: z
344
431
  .enum(NODE_KINDS)
345
432
  .optional()
346
433
  .describe("Restrict results to a node kind (class, method, field, ...)."),
347
434
  limit: z.number().int().min(1).max(500).optional().describe("Max symbols returned."),
348
- }, async ({ annotation, kind, limit }) => {
435
+ ...budgetParams,
436
+ }, async ({ annotation, kind, limit, max_tokens, cursor }) => {
349
437
  const results = findAnnotated(store, annotation, { kind, limit });
350
- return text({
351
- annotation,
352
- count: results.length,
353
- results: results.map((r) => ({
354
- symbol: ref(r.symbol),
355
- annotation: r.annotation,
356
- line: line(r.line),
357
- arguments: r.arguments,
358
- })),
359
- });
438
+ const symbols = results.map((r) => ({
439
+ symbol: ref(r.symbol),
440
+ annotation: r.annotation,
441
+ line: line(r.line),
442
+ arguments: r.arguments,
443
+ }));
444
+ return text(paged({ annotation, count: symbols.length, results: symbols }, "results", symbols, {
445
+ max_tokens,
446
+ cursor,
447
+ }));
360
448
  });
361
449
  tool("find_hotspots", "Rank methods/constructors by call volume (fan-in, fan-out, advice interceptions). Useful for prioritizing review or refactoring.", {
362
450
  limit: z.number().int().min(1).max(200).optional().describe("Max hotspots returned."),
@@ -365,44 +453,42 @@ export function createServer(store, config) {
365
453
  .boolean()
366
454
  .optional()
367
455
  .describe("Count dynamic-dispatch callers too (default true)."),
368
- }, async ({ limit, min_fan_in, include_dynamic }) => {
456
+ ...budgetParams,
457
+ }, async ({ limit, min_fan_in, include_dynamic, max_tokens, cursor }) => {
369
458
  const results = hotspots(store, {
370
459
  limit,
371
460
  minFanIn: min_fan_in,
372
461
  includeDynamic: include_dynamic,
373
462
  });
374
- return text({
375
- count: results.length,
376
- hotspots: results.map((h) => ({
377
- symbol: ref(h.symbol),
378
- fanIn: h.fanIn,
379
- fanOut: h.fanOut,
380
- adviceCount: h.adviceCount,
381
- })),
382
- });
463
+ const list = results.map((h) => ({
464
+ symbol: ref(h.symbol),
465
+ fanIn: h.fanIn,
466
+ fanOut: h.fanOut,
467
+ adviceCount: h.adviceCount,
468
+ }));
469
+ return text(paged({ count: list.length, hotspots: list }, "hotspots", list, { max_tokens, cursor }));
383
470
  });
384
471
  tool("find_dead_code", "Find methods/constructors with no incoming calls, dispatch, or advice edges. Excludes framework-invoked entry points (routes, listeners, schedulers, event handlers, `main`) and test sources by default; code invoked purely through reflection may still be reported.", {
385
472
  limit: z.number().int().min(1).max(500).optional().describe("Max symbols returned."),
386
- include_constructors: z.boolean().optional().describe("Include constructors (default false)."),
473
+ include_constructors: z
474
+ .boolean()
475
+ .optional()
476
+ .describe("Include constructors (default false)."),
387
477
  include_tests: z.boolean().optional().describe("Include test sources (default false)."),
388
478
  include_dynamic: z
389
479
  .boolean()
390
480
  .optional()
391
481
  .describe("Treat dynamic-dispatch edges as callers (default true)."),
392
- }, async ({ limit, include_constructors, include_tests, include_dynamic }) => {
482
+ ...budgetParams,
483
+ }, async ({ limit, include_constructors, include_tests, include_dynamic, max_tokens, cursor }) => {
393
484
  const results = deadCode(store, {
394
485
  limit,
395
486
  includeConstructors: include_constructors,
396
487
  includeTests: include_tests,
397
488
  includeDynamic: include_dynamic,
398
489
  });
399
- return text({
400
- count: results.length,
401
- unused: results.map((d) => ({
402
- symbol: ref(d.symbol),
403
- reason: d.reason,
404
- })),
405
- });
490
+ const unused = results.map((d) => ({ symbol: ref(d.symbol), reason: d.reason }));
491
+ return text(paged({ count: unused.length, unused }, "unused", unused, { max_tokens, cursor }));
406
492
  });
407
493
  tool("get_subgraph", "Extract the neighborhood around a symbol across any edge kinds — for visualization and understanding the local shape of the graph.", {
408
494
  symbol: z.string().describe("Center symbol id or qualified name."),
@@ -432,16 +518,18 @@ export function createServer(store, config) {
432
518
  .optional()
433
519
  .describe("Filter by entry-point kind: http, messaging, scheduler, event, cli, test, route."),
434
520
  limit: z.number().int().min(1).max(500).optional().describe("Max results (default 50)."),
435
- }, async ({ kind, limit }) => {
521
+ ...budgetParams,
522
+ }, async ({ kind, limit, max_tokens, cursor }) => {
436
523
  const results = findEntryPoints(store, { kind, limit });
437
- return text({
438
- count: results.length,
439
- entryPoints: results.map((e) => ({
440
- symbol: ref(e.symbol),
441
- entryPointKind: e.entryPointKind,
442
- ...(e.detail ? { detail: e.detail } : {}),
443
- })),
444
- });
524
+ const entryPoints = results.map((e) => ({
525
+ symbol: ref(e.symbol),
526
+ entryPointKind: e.entryPointKind,
527
+ ...(e.detail ? { detail: e.detail } : {}),
528
+ }));
529
+ return text(paged({ count: entryPoints.length, entryPoints }, "entryPoints", entryPoints, {
530
+ max_tokens,
531
+ cursor,
532
+ }));
445
533
  });
446
534
  tool("find_injections", "Find dependencies supplied by a DI container or framework rather than by an explicit `new`/import — Spring `@Autowired`, NestJS constructor injection, FastAPI `Depends`, and similar. Each result carries the injecting symbol, the injected type, and the framework.", {
447
535
  symbol: z
@@ -449,19 +537,21 @@ export function createServer(store, config) {
449
537
  .optional()
450
538
  .describe("Restrict to a single injecting or injected symbol id/qualified name."),
451
539
  limit: z.number().int().min(1).max(500).optional().describe("Max results (default 50)."),
452
- }, async ({ symbol, limit }) => {
540
+ ...budgetParams,
541
+ }, async ({ symbol, limit, max_tokens, cursor }) => {
453
542
  const results = findInjections(store, { symbol, limit });
454
- return text({
455
- count: results.length,
456
- injections: results.map((e) => ({
457
- source: ref(e.source),
458
- target: ref(e.target),
459
- ...(e.injectedName ? { injectedName: e.injectedName } : {}),
460
- ...(e.framework ? { framework: e.framework } : {}),
461
- ...(e.line !== undefined ? { line: line(e.line) } : {}),
462
- ...(e.external ? { external: true } : {}),
463
- })),
464
- });
543
+ const injections = results.map((e) => ({
544
+ source: ref(e.source),
545
+ target: ref(e.target),
546
+ ...(e.injectedName ? { injectedName: e.injectedName } : {}),
547
+ ...(e.framework ? { framework: e.framework } : {}),
548
+ ...(e.line !== undefined ? { line: line(e.line) } : {}),
549
+ ...(e.external ? { external: true } : {}),
550
+ }));
551
+ return text(paged({ count: injections.length, injections }, "injections", injections, {
552
+ max_tokens,
553
+ cursor,
554
+ }));
465
555
  });
466
556
  tool("find_reflections", "Find run-time indirection the static call graph cannot see through: Java reflection and `ServiceLoader`, Python `getattr`/`importlib`, JavaScript `require`/dynamic `import()`. The target is the resolved symbol when it could be determined statically.", {
467
557
  symbol: z
@@ -469,17 +559,19 @@ export function createServer(store, config) {
469
559
  .optional()
470
560
  .describe("Restrict to a single looking-up or looked-up symbol id/qualified name."),
471
561
  limit: z.number().int().min(1).max(500).optional().describe("Max results (default 50)."),
472
- }, async ({ symbol, limit }) => {
562
+ ...budgetParams,
563
+ }, async ({ symbol, limit, max_tokens, cursor }) => {
473
564
  const results = findReflections(store, { symbol, limit });
474
- return text({
475
- count: results.length,
476
- reflections: results.map((e) => ({
477
- source: ref(e.source),
478
- target: ref(e.target),
479
- ...(e.mechanism ? { mechanism: e.mechanism } : {}),
480
- ...(e.line !== undefined ? { line: line(e.line) } : {}),
481
- })),
482
- });
565
+ const reflections = results.map((e) => ({
566
+ source: ref(e.source),
567
+ target: ref(e.target),
568
+ ...(e.mechanism ? { mechanism: e.mechanism } : {}),
569
+ ...(e.line !== undefined ? { line: line(e.line) } : {}),
570
+ }));
571
+ return text(paged({ count: reflections.length, reflections }, "reflections", reflections, {
572
+ max_tokens,
573
+ cursor,
574
+ }));
483
575
  });
484
576
  tool("find_routes", "Map HTTP route registrations (`app.get`, `@GetMapping`, FastAPI decorators, Express routers) to the handler they invoke. Bridges framework configuration to source symbols.", {
485
577
  symbol: z
@@ -487,33 +579,34 @@ export function createServer(store, config) {
487
579
  .optional()
488
580
  .describe("Restrict to a single registering or handling symbol id/qualified name."),
489
581
  limit: z.number().int().min(1).max(500).optional().describe("Max results (default 50)."),
490
- }, async ({ symbol, limit }) => {
582
+ ...budgetParams,
583
+ }, async ({ symbol, limit, max_tokens, cursor }) => {
491
584
  const results = findRoutes(store, { symbol, limit });
492
- return text({
493
- count: results.length,
494
- routes: results.map((e) => ({
495
- source: ref(e.source),
496
- handler: ref(e.target),
497
- ...(e.httpMethod ? { httpMethod: e.httpMethod } : {}),
498
- ...(e.path ? { path: e.path } : {}),
499
- ...(e.framework ? { framework: e.framework } : {}),
500
- ...(e.line !== undefined ? { line: line(e.line) } : {}),
501
- })),
502
- });
585
+ const routes = results.map((e) => ({
586
+ source: ref(e.source),
587
+ handler: ref(e.target),
588
+ ...(e.httpMethod ? { httpMethod: e.httpMethod } : {}),
589
+ ...(e.path ? { path: e.path } : {}),
590
+ ...(e.framework ? { framework: e.framework } : {}),
591
+ ...(e.line !== undefined ? { line: line(e.line) } : {}),
592
+ }));
593
+ return text(paged({ count: routes.length, routes }, "routes", routes, { max_tokens, cursor }));
503
594
  });
504
595
  tool("find_external_dependencies", "Summarize third-party packages the crawled source imports, with reference and file counts. External libraries are modelled as package-level nodes: the crawler records the dependency, not the library's internals.", {
505
596
  limit: z.number().int().min(1).max(500).optional().describe("Max packages (default 50)."),
506
- }, async ({ limit }) => {
597
+ ...budgetParams,
598
+ }, async ({ limit, max_tokens, cursor }) => {
507
599
  const results = externalDependencies(store, { limit });
508
- return text({
509
- count: results.length,
510
- packages: results.map((p) => ({
511
- package: p.package,
512
- references: p.references,
513
- files: p.files,
514
- kinds: p.kinds,
515
- })),
516
- });
600
+ const packages = results.map((p) => ({
601
+ package: p.package,
602
+ references: p.references,
603
+ files: p.files,
604
+ kinds: p.kinds,
605
+ }));
606
+ return text(paged({ count: packages.length, packages }, "packages", packages, {
607
+ max_tokens,
608
+ cursor,
609
+ }));
517
610
  });
518
611
  tool("get_schema", "Describe the property-graph model: node kinds, edge kinds, and the id scheme (including annotations, aspects, and dynamic edges).", {}, async () => {
519
612
  return text({
@@ -548,6 +641,233 @@ export function createServer(store, config) {
548
641
  store.clearAll();
549
642
  return text({ ok: true, cleared: true });
550
643
  });
644
+ tool("find_unresolved", "List call sites the resolver could not bind to a symbol, grouped by reason, with the caller, source line and the callee text as written. This is the honest boundary of static analysis: dynamic receivers, untyped values, missing dependencies. Use it to judge how much of a project the index actually covers, and as the highest-signal input for improving a parser.", {
645
+ reason: z
646
+ .string()
647
+ .optional()
648
+ .describe("Case-insensitive substring filter on the resolver failure reason."),
649
+ caller_kind: z
650
+ .enum(NODE_KINDS)
651
+ .optional()
652
+ .describe("Restrict to unresolved calls made by callers of this node kind."),
653
+ limit: z
654
+ .number()
655
+ .int()
656
+ .min(1)
657
+ .max(5000)
658
+ .optional()
659
+ .describe("Max call sites scanned (default 2000)."),
660
+ ...budgetParams,
661
+ }, async ({ reason, caller_kind, limit, max_tokens, cursor }) => {
662
+ const report = findUnresolved(store, { reason, kind: caller_kind, limit });
663
+ const calls = report.calls.map((c) => ({
664
+ caller: ref(c.caller),
665
+ target: c.targetLabel,
666
+ reason: c.reason,
667
+ line: line(c.line),
668
+ }));
669
+ return text(paged({
670
+ count: calls.length,
671
+ total: report.total,
672
+ scanTruncated: report.scanTruncated,
673
+ byReason: report.byReason,
674
+ unresolved: calls,
675
+ }, "unresolved", calls, { max_tokens, cursor }));
676
+ });
677
+ tool("impact_analysis", "One call that answers 'what breaks if I change this?': reverse callers to a depth, framework entry points that reach the symbol, HTTP routes that serve it, and overriding implementations. Analysing a type aggregates the callers of all its methods. Returns a heuristic risk band: high when a framework entry point reaches the symbol, medium when it has direct callers, low otherwise.", {
678
+ symbol: z.string().describe("Symbol id or qualified name."),
679
+ max_depth: z
680
+ .number()
681
+ .int()
682
+ .min(1)
683
+ .max(50)
684
+ .optional()
685
+ .describe("Caller traversal depth (default 10)."),
686
+ include_dynamic: z
687
+ .boolean()
688
+ .optional()
689
+ .describe("Also follow DYNAMIC_DISPATCH edges (default true)."),
690
+ limit: z
691
+ .number()
692
+ .int()
693
+ .min(1)
694
+ .max(1000)
695
+ .optional()
696
+ .describe("Max callers returned (default 200)."),
697
+ }, async ({ symbol, max_depth, include_dynamic, limit }) => {
698
+ const result = impactAnalysis(store, symbol, {
699
+ maxDepth: max_depth,
700
+ includeDynamic: include_dynamic,
701
+ limit,
702
+ });
703
+ if (!result)
704
+ return notFound(symbol);
705
+ return text({
706
+ symbol: ref(result.symbol),
707
+ summary: result.summary,
708
+ callers: result.callers.map((c) => ({
709
+ caller: ref(c.caller),
710
+ depth: c.depth,
711
+ callSiteLine: line(c.callSiteLine),
712
+ edgeKind: c.edgeKind,
713
+ })),
714
+ entryPoints: result.entryPoints.map((e) => ({
715
+ symbol: ref(e.symbol),
716
+ entryPointKind: e.entryPointKind,
717
+ ...(e.detail ? { detail: e.detail } : {}),
718
+ viaDepth: e.viaDepth,
719
+ })),
720
+ routes: result.routes.map(wiring),
721
+ implementations: result.implementations.map((i) => ({
722
+ type: ref(i.type),
723
+ ...(i.method ? { method: ref(i.method) } : {}),
724
+ depth: i.depth,
725
+ via: i.via,
726
+ })),
727
+ affectedFiles: result.affectedFiles,
728
+ });
729
+ });
730
+ tool("explain_symbol", "One-shot digest of a symbol: kind, file and line, annotations/decorators, contained members with their decorators, direct callers and callees, overriding implementations, and the framework wiring that reaches it (routes, injections, reflections). Replaces a chain of five lookups with one round trip.", {
731
+ symbol: z.string().describe("Symbol id or qualified name."),
732
+ max_depth: z
733
+ .number()
734
+ .int()
735
+ .min(1)
736
+ .max(5)
737
+ .optional()
738
+ .describe("Caller/callee traversal depth (default 1)."),
739
+ include_dynamic: z
740
+ .boolean()
741
+ .optional()
742
+ .describe("Also follow DYNAMIC_DISPATCH edges (default true)."),
743
+ limit: z
744
+ .number()
745
+ .int()
746
+ .min(1)
747
+ .max(500)
748
+ .optional()
749
+ .describe("Max rows per section (default 50)."),
750
+ }, async ({ symbol, max_depth, include_dynamic, limit }) => {
751
+ const result = explainSymbol(store, symbol, {
752
+ maxDepth: max_depth,
753
+ includeDynamic: include_dynamic,
754
+ limit,
755
+ });
756
+ if (!result)
757
+ return notFound(symbol);
758
+ return text({
759
+ symbol: ref(result.symbol),
760
+ line: line(result.line),
761
+ isEntryPoint: result.isEntryPoint ?? false,
762
+ decorators: result.decorators.map((d) => ({ ...d, line: line(d.line) })),
763
+ members: result.members.map((m) => ({
764
+ symbol: ref(m.symbol),
765
+ line: line(m.line),
766
+ decorators: m.decorators.map((d) => ({ ...d, line: line(d.line) })),
767
+ })),
768
+ callers: result.callers.map((c) => ({
769
+ caller: ref(c.caller),
770
+ depth: c.depth,
771
+ callSiteLine: line(c.callSiteLine),
772
+ edgeKind: c.edgeKind,
773
+ })),
774
+ callees: result.callees.map((c) => ({
775
+ caller: ref(c.caller),
776
+ depth: c.depth,
777
+ callSiteLine: line(c.callSiteLine),
778
+ edgeKind: c.edgeKind,
779
+ })),
780
+ implementations: result.implementations.map((i) => ({
781
+ type: ref(i.type),
782
+ ...(i.method ? { method: ref(i.method) } : {}),
783
+ depth: i.depth,
784
+ via: i.via,
785
+ })),
786
+ routes: result.routes.map(wiring),
787
+ injections: result.injections.map(wiring),
788
+ reflections: result.reflections.map(wiring),
789
+ summary: result.summary,
790
+ });
791
+ });
792
+ tool("find_cycles", "Find call cycles (strongly connected components of the call graph) with a sample closed path for each. Direct recursion is reported separately in selfRecursive. Useful for spotting layer violations, accidental mutual recursion and hard-to-test tangles. Follows CALLS edges, plus DYNAMIC_DISPATCH unless disabled.", {
793
+ include_dynamic: z
794
+ .boolean()
795
+ .optional()
796
+ .describe("Include DYNAMIC_DISPATCH edges (default true)."),
797
+ min_size: z
798
+ .number()
799
+ .int()
800
+ .min(1)
801
+ .max(100)
802
+ .optional()
803
+ .describe("Minimum cycle size to report (default 2)."),
804
+ edge_kinds: z
805
+ .array(z.enum(EDGE_KINDS))
806
+ .optional()
807
+ .describe("Explicit edge kinds to follow, overriding include_dynamic."),
808
+ limit: z
809
+ .number()
810
+ .int()
811
+ .min(1)
812
+ .max(500)
813
+ .optional()
814
+ .describe("Max cycles returned (default 100)."),
815
+ ...budgetParams,
816
+ }, async ({ include_dynamic, min_size, edge_kinds, limit, max_tokens, cursor }) => {
817
+ const report = findCycles(store, {
818
+ includeDynamic: include_dynamic ?? true,
819
+ minSize: min_size,
820
+ edgeKinds: edge_kinds,
821
+ limit,
822
+ });
823
+ const cycles = report.cycles.map((c) => ({
824
+ size: c.size,
825
+ members: c.members.map(ref),
826
+ sample: c.sample,
827
+ }));
828
+ return text(paged({
829
+ count: cycles.length,
830
+ cycleCount: report.cycleCount,
831
+ edgeKinds: report.edgeKinds,
832
+ scannedNodes: report.scannedNodes,
833
+ scannedEdges: report.scannedEdges,
834
+ scanTruncated: report.scanTruncated,
835
+ selfRecursive: report.selfRecursive.map(ref),
836
+ cycles,
837
+ }, "cycles", cycles, { max_tokens, cursor }));
838
+ });
839
+ tool("export_graph", "Render a neighborhood, or a slice of the whole graph, as a Mermaid or Graphviz DOT diagram. Use it when showing the shape of the code beats listing edges. Center the export on a symbol and keep the radius small so the diagram stays readable.", {
840
+ symbol: z
841
+ .string()
842
+ .optional()
843
+ .describe("Center symbol id or qualified name; omit to export a slice of the whole graph."),
844
+ radius: z
845
+ .number()
846
+ .int()
847
+ .min(1)
848
+ .max(5)
849
+ .optional()
850
+ .describe("Neighborhood radius around the symbol (default 2)."),
851
+ format: z.enum(["mermaid", "dot"]).optional().describe("Diagram format (default mermaid)."),
852
+ direction: z
853
+ .enum(["LR", "TB", "RL", "BT"])
854
+ .optional()
855
+ .describe("Layout direction (default LR)."),
856
+ edge_kinds: z.array(z.enum(EDGE_KINDS)).optional().describe("Edge kinds to draw."),
857
+ limit: z.number().int().min(1).max(1000).optional().describe("Max nodes (default 200)."),
858
+ }, async ({ symbol, radius, format, direction, edge_kinds, limit }) => {
859
+ const result = exportGraph(store, {
860
+ symbol,
861
+ radius,
862
+ format,
863
+ direction,
864
+ edgeKinds: edge_kinds,
865
+ limit,
866
+ });
867
+ if (!result)
868
+ return notFound(symbol ?? "");
869
+ return text(result);
870
+ });
551
871
  // ------------------------------------------------------------- resources
552
872
  server.registerResource("graph-schema", "codecrawler://schema", {
553
873
  title: "Graph schema",
@@ -628,9 +948,7 @@ export function createServer(store, config) {
628
948
  content: {
629
949
  type: "text",
630
950
  text: `Here are the static call paths from \`${from}\` to \`${to}\`:\n\n` +
631
- paths
632
- .map((p, i) => `${i + 1}. ${p.path.join(" -> ")}`)
633
- .join("\n") +
951
+ paths.map((p, i) => `${i + 1}. ${p.path.join(" -> ")}`).join("\n") +
634
952
  `\n\nUse \`get_callers\` or \`get_callees\` on intermediate symbols to explore further.`,
635
953
  },
636
954
  },