@nithin554/code-crawler 0.1.54 → 0.1.56

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
@@ -3,6 +3,8 @@ import { z } from "zod";
3
3
  import { NODE_KINDS, EDGE_KINDS } from "./graph/schema.js";
4
4
  import { crawlProject } from "./crawler.js";
5
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
+ import { applyBudget, pageMeta } from "./graph/budget.js";
7
+ import { explainSymbol, exportGraph, findCycles, findUnresolved, impactAnalysis, unresolvedForNode, } from "./graph/insights.js";
6
8
  import { loadConfig } from "./config.js";
7
9
  /**
8
10
  * MCP server surface for code-crawler.
@@ -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.
@@ -117,15 +165,16 @@ export function createServer(store, config) {
117
165
  .optional()
118
166
  .describe("Restrict results to a node kind (class, method, field, ...)."),
119
167
  limit: z.number().int().min(1).max(200).optional().describe("Max results (default 50)."),
120
- }, async ({ query, kind, limit }) => {
168
+ ...budgetParams,
169
+ }, async ({ query, kind, limit, max_tokens, cursor }) => {
121
170
  const results = searchSymbols(store, query, { kind, limit });
122
- return text({
123
- query,
124
- count: results.length,
125
- results: results.map(ref),
126
- });
171
+ const refs = results.map(ref);
172
+ return text(paged({ query, count: refs.length, results: refs }, "results", refs, {
173
+ max_tokens,
174
+ cursor,
175
+ }));
127
176
  });
128
- tool("get_node", "Get metadata and source location for a single symbol by id or qualified name.", {
177
+ 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
178
  qualified_name: z
130
179
  .string()
131
180
  .describe("Symbol id or qualified name, e.g. 'com.acme.OrderService#place'."),
@@ -133,6 +182,7 @@ export function createServer(store, config) {
133
182
  const node = store.getNode(qualified_name) ?? store.getNodeByQualifiedName(qualified_name);
134
183
  if (!node)
135
184
  return notFound(qualified_name);
185
+ const unresolved = unresolvedForNode(store, node.id);
136
186
  return text({
137
187
  id: node.id,
138
188
  kind: node.kind,
@@ -143,6 +193,23 @@ export function createServer(store, config) {
143
193
  startLine: line(node.properties.startLine),
144
194
  endLine: line(node.properties.endLine),
145
195
  },
196
+ ...(unresolved
197
+ ? {
198
+ unresolved: {
199
+ callerCount: unresolved.outgoing.length,
200
+ referencedByCount: unresolved.incoming.length,
201
+ calls: unresolved.outgoing.map((c) => ({
202
+ target: c.targetLabel,
203
+ reason: c.reason,
204
+ line: line(c.line),
205
+ })),
206
+ referencedBy: unresolved.incoming.map((i) => ({
207
+ caller: ref(i.caller),
208
+ line: line(i.line),
209
+ })),
210
+ },
211
+ }
212
+ : {}),
146
213
  });
147
214
  });
148
215
  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 +225,8 @@ export function createServer(store, config) {
158
225
  .describe("Also follow ADVISES edges (aspect advice methods)."),
159
226
  max_depth: z.number().int().min(1).max(50).optional().describe("Traversal depth cap."),
160
227
  limit: z.number().int().min(1).max(500).optional().describe("Max callers returned."),
161
- }, async ({ symbol, transitive, include_dynamic, include_advice, max_depth, limit }) => {
228
+ ...budgetParams,
229
+ }, async ({ symbol, transitive, include_dynamic, include_advice, max_depth, limit, max_tokens, cursor }) => {
162
230
  const result = getCallers(store, symbol, {
163
231
  transitive,
164
232
  includeDynamic: include_dynamic,
@@ -168,15 +236,16 @@ export function createServer(store, config) {
168
236
  });
169
237
  if (!result)
170
238
  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
- });
239
+ const callers = result.callers.map((c) => ({
240
+ caller: ref(c.caller),
241
+ depth: c.depth,
242
+ callSiteLine: line(c.callSiteLine),
243
+ edgeKind: c.edgeKind,
244
+ }));
245
+ return text(paged({ target: ref(result.target), count: callers.length, callers }, "callers", callers, {
246
+ max_tokens,
247
+ cursor,
248
+ }));
180
249
  });
181
250
  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
251
  symbol: z.string().describe("Method/constructor id or qualified name."),
@@ -191,7 +260,8 @@ export function createServer(store, config) {
191
260
  .describe("Also follow ADVISES edges (aspect advice methods)."),
192
261
  max_depth: z.number().int().min(1).max(50).optional().describe("Traversal depth cap."),
193
262
  limit: z.number().int().min(1).max(500).optional().describe("Max callees returned."),
194
- }, async ({ symbol, transitive, include_dynamic, include_advice, max_depth, limit }) => {
263
+ ...budgetParams,
264
+ }, async ({ symbol, transitive, include_dynamic, include_advice, max_depth, limit, max_tokens, cursor }) => {
195
265
  const result = getCallees(store, symbol, {
196
266
  transitive,
197
267
  includeDynamic: include_dynamic,
@@ -201,15 +271,16 @@ export function createServer(store, config) {
201
271
  });
202
272
  if (!result)
203
273
  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
- });
274
+ const callees = result.callees.map((c) => ({
275
+ callee: ref(c.callee),
276
+ depth: c.depth,
277
+ callSiteLine: line(c.callSiteLine),
278
+ edgeKind: c.edgeKind,
279
+ }));
280
+ return text(paged({ source: ref(result.source), count: callees.length, callees }, "callees", callees, {
281
+ max_tokens,
282
+ cursor,
283
+ }));
213
284
  });
214
285
  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
286
  source: z.string().describe("Starting symbol id or qualified name."),
@@ -224,7 +295,8 @@ export function createServer(store, config) {
224
295
  .describe("Also follow ADVISES edges (aspect advice methods)."),
225
296
  max_depth: z.number().int().min(1).max(50).optional().describe("Maximum path length."),
226
297
  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
+ ...budgetParams,
299
+ }, async ({ source, target, include_dynamic, include_advice, max_depth, limit, max_tokens, cursor }) => {
228
300
  const paths = tracePath(store, source, target, {
229
301
  includeDynamic: include_dynamic,
230
302
  includeAdvice: include_advice,
@@ -233,21 +305,20 @@ export function createServer(store, config) {
233
305
  });
234
306
  if (!paths)
235
307
  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
- })),
308
+ const mapped = paths.map((p) => ({
309
+ length: p.length,
310
+ path: p.path,
311
+ edges: p.edges.map((e) => ({
312
+ source: e.source,
313
+ target: e.target,
314
+ kind: e.kind,
315
+ callSiteLine: line(e.callSiteLine),
249
316
  })),
250
- });
317
+ }));
318
+ return text(paged({ source, target, pathCount: mapped.length, paths: mapped }, "paths", mapped, {
319
+ max_tokens,
320
+ cursor,
321
+ }));
251
322
  });
252
323
  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
324
  symbol: z.string().describe("Starting method or constructor id/qualified name."),
@@ -261,7 +332,8 @@ export function createServer(store, config) {
261
332
  .describe("Weave aspect advice into the trace (default true)."),
262
333
  max_depth: z.number().int().min(1).max(50).optional().describe("Traversal depth cap."),
263
334
  limit: z.number().int().min(1).max(1000).optional().describe("Max steps returned."),
264
- }, async ({ symbol, include_dynamic, include_advice, max_depth, limit }) => {
335
+ ...budgetParams,
336
+ }, async ({ symbol, include_dynamic, include_advice, max_depth, limit, max_tokens, cursor }) => {
265
337
  const result = traceExecution(store, symbol, {
266
338
  includeDynamic: include_dynamic,
267
339
  includeAdvice: include_advice,
@@ -270,51 +342,55 @@ export function createServer(store, config) {
270
342
  });
271
343
  if (!result)
272
344
  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
- });
345
+ const steps = result.steps.map((s) => ({
346
+ symbol: ref(s.symbol),
347
+ depth: s.depth,
348
+ via: s.via,
349
+ callSiteLine: line(s.callSiteLine),
350
+ }));
351
+ return text(paged({ root: ref(result.root), stepCount: steps.length, steps }, "steps", steps, {
352
+ max_tokens,
353
+ cursor,
354
+ }));
283
355
  });
284
356
  tool("find_implementations", "Find every class/interface that implements or extends a type, directly or transitively. Use this to understand virtual dispatch candidates.", {
285
357
  symbol: z.string().describe("Interface or superclass id/qualified name."),
286
358
  max_depth: z.number().int().min(1).max(20).optional().describe("Hierarchy depth cap."),
287
359
  limit: z.number().int().min(1).max(500).optional().describe("Max implementations returned."),
288
- }, async ({ symbol, max_depth, limit }) => {
360
+ ...budgetParams,
361
+ }, async ({ symbol, max_depth, limit, max_tokens, cursor }) => {
289
362
  const result = findImplementations(store, symbol, { maxDepth: max_depth, limit });
290
363
  if (!result)
291
364
  return notFound(symbol);
292
- return text({
365
+ const implementations = result.implementations.map((i) => ({
366
+ type: ref(i.type),
367
+ depth: i.depth,
368
+ via: i.via,
369
+ }));
370
+ return text(paged({
293
371
  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
- });
372
+ implementationCount: implementations.length,
373
+ implementations,
374
+ }, "implementations", implementations, { max_tokens, cursor }));
301
375
  });
302
376
  tool("find_aspects", "List aspects with their advice methods (@Before/@After/@Around/...), pointcuts, and the methods each advice intercepts.", {
303
377
  limit: z.number().int().min(1).max(200).optional().describe("Max aspects returned."),
304
- }, async ({ limit }) => {
378
+ ...budgetParams,
379
+ }, async ({ limit, max_tokens, cursor }) => {
305
380
  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
- })),
381
+ const mapped = aspects.map((a) => ({
382
+ aspect: ref(a.aspect),
383
+ advice: a.advice.map((adv) => ({
384
+ method: ref(adv.method),
385
+ adviceKind: adv.adviceKind,
386
+ pointcut: adv.pointcut,
387
+ advised: adv.advised.map(ref),
316
388
  })),
317
- });
389
+ }));
390
+ return text(paged({ aspectCount: mapped.length, aspects: mapped }, "aspects", mapped, {
391
+ max_tokens,
392
+ cursor,
393
+ }));
318
394
  });
319
395
  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
396
  symbol: z.string().describe("Symbol id or qualified name."),
@@ -322,21 +398,23 @@ export function createServer(store, config) {
322
398
  .boolean()
323
399
  .optional()
324
400
  .describe("Include decorators on declared members (default false)."),
325
- }, async ({ symbol, include_members }) => {
401
+ ...budgetParams,
402
+ }, async ({ symbol, include_members, max_tokens, cursor }) => {
326
403
  const result = getDecorators(store, symbol, { includeMembers: include_members });
327
404
  if (!result)
328
405
  return notFound(symbol);
329
- return text({
406
+ const members = result.members.map((m) => ({
407
+ symbol: ref(m.symbol),
408
+ decorators: m.decorators.map((d) => ({ ...d, line: line(d.line) })),
409
+ }));
410
+ return text(paged({
330
411
  symbol: ref(result.symbol),
331
412
  decorators: result.decorators.map((d) => ({
332
413
  ...d,
333
414
  line: line(d.line),
334
415
  })),
335
- members: result.members.map((m) => ({
336
- symbol: ref(m.symbol),
337
- decorators: m.decorators.map((d) => ({ ...d, line: line(d.line) })),
338
- })),
339
- });
416
+ members,
417
+ }, "members", members, { max_tokens, cursor }));
340
418
  });
341
419
  tool("find_annotated", "Find every symbol annotated/decorated with a given annotation (simple name, @name, or FQN).", {
342
420
  annotation: z.string().describe("Annotation name, e.g. 'Transactional', '@Aspect' or an FQN."),
@@ -345,18 +423,19 @@ export function createServer(store, config) {
345
423
  .optional()
346
424
  .describe("Restrict results to a node kind (class, method, field, ...)."),
347
425
  limit: z.number().int().min(1).max(500).optional().describe("Max symbols returned."),
348
- }, async ({ annotation, kind, limit }) => {
426
+ ...budgetParams,
427
+ }, async ({ annotation, kind, limit, max_tokens, cursor }) => {
349
428
  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
- });
429
+ const symbols = results.map((r) => ({
430
+ symbol: ref(r.symbol),
431
+ annotation: r.annotation,
432
+ line: line(r.line),
433
+ arguments: r.arguments,
434
+ }));
435
+ return text(paged({ annotation, count: symbols.length, results: symbols }, "results", symbols, {
436
+ max_tokens,
437
+ cursor,
438
+ }));
360
439
  });
361
440
  tool("find_hotspots", "Rank methods/constructors by call volume (fan-in, fan-out, advice interceptions). Useful for prioritizing review or refactoring.", {
362
441
  limit: z.number().int().min(1).max(200).optional().describe("Max hotspots returned."),
@@ -365,21 +444,20 @@ export function createServer(store, config) {
365
444
  .boolean()
366
445
  .optional()
367
446
  .describe("Count dynamic-dispatch callers too (default true)."),
368
- }, async ({ limit, min_fan_in, include_dynamic }) => {
447
+ ...budgetParams,
448
+ }, async ({ limit, min_fan_in, include_dynamic, max_tokens, cursor }) => {
369
449
  const results = hotspots(store, {
370
450
  limit,
371
451
  minFanIn: min_fan_in,
372
452
  includeDynamic: include_dynamic,
373
453
  });
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
- });
454
+ const list = results.map((h) => ({
455
+ symbol: ref(h.symbol),
456
+ fanIn: h.fanIn,
457
+ fanOut: h.fanOut,
458
+ adviceCount: h.adviceCount,
459
+ }));
460
+ return text(paged({ count: list.length, hotspots: list }, "hotspots", list, { max_tokens, cursor }));
383
461
  });
384
462
  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
463
  limit: z.number().int().min(1).max(500).optional().describe("Max symbols returned."),
@@ -389,20 +467,16 @@ export function createServer(store, config) {
389
467
  .boolean()
390
468
  .optional()
391
469
  .describe("Treat dynamic-dispatch edges as callers (default true)."),
392
- }, async ({ limit, include_constructors, include_tests, include_dynamic }) => {
470
+ ...budgetParams,
471
+ }, async ({ limit, include_constructors, include_tests, include_dynamic, max_tokens, cursor }) => {
393
472
  const results = deadCode(store, {
394
473
  limit,
395
474
  includeConstructors: include_constructors,
396
475
  includeTests: include_tests,
397
476
  includeDynamic: include_dynamic,
398
477
  });
399
- return text({
400
- count: results.length,
401
- unused: results.map((d) => ({
402
- symbol: ref(d.symbol),
403
- reason: d.reason,
404
- })),
405
- });
478
+ const unused = results.map((d) => ({ symbol: ref(d.symbol), reason: d.reason }));
479
+ return text(paged({ count: unused.length, unused }, "unused", unused, { max_tokens, cursor }));
406
480
  });
407
481
  tool("get_subgraph", "Extract the neighborhood around a symbol across any edge kinds — for visualization and understanding the local shape of the graph.", {
408
482
  symbol: z.string().describe("Center symbol id or qualified name."),
@@ -432,16 +506,15 @@ export function createServer(store, config) {
432
506
  .optional()
433
507
  .describe("Filter by entry-point kind: http, messaging, scheduler, event, cli, test, route."),
434
508
  limit: z.number().int().min(1).max(500).optional().describe("Max results (default 50)."),
435
- }, async ({ kind, limit }) => {
509
+ ...budgetParams,
510
+ }, async ({ kind, limit, max_tokens, cursor }) => {
436
511
  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
- });
512
+ const entryPoints = results.map((e) => ({
513
+ symbol: ref(e.symbol),
514
+ entryPointKind: e.entryPointKind,
515
+ ...(e.detail ? { detail: e.detail } : {}),
516
+ }));
517
+ return text(paged({ count: entryPoints.length, entryPoints }, "entryPoints", entryPoints, { max_tokens, cursor }));
445
518
  });
446
519
  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
520
  symbol: z
@@ -449,19 +522,21 @@ export function createServer(store, config) {
449
522
  .optional()
450
523
  .describe("Restrict to a single injecting or injected symbol id/qualified name."),
451
524
  limit: z.number().int().min(1).max(500).optional().describe("Max results (default 50)."),
452
- }, async ({ symbol, limit }) => {
525
+ ...budgetParams,
526
+ }, async ({ symbol, limit, max_tokens, cursor }) => {
453
527
  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
- });
528
+ const injections = results.map((e) => ({
529
+ source: ref(e.source),
530
+ target: ref(e.target),
531
+ ...(e.injectedName ? { injectedName: e.injectedName } : {}),
532
+ ...(e.framework ? { framework: e.framework } : {}),
533
+ ...(e.line !== undefined ? { line: line(e.line) } : {}),
534
+ ...(e.external ? { external: true } : {}),
535
+ }));
536
+ return text(paged({ count: injections.length, injections }, "injections", injections, {
537
+ max_tokens,
538
+ cursor,
539
+ }));
465
540
  });
466
541
  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
542
  symbol: z
@@ -469,17 +544,19 @@ export function createServer(store, config) {
469
544
  .optional()
470
545
  .describe("Restrict to a single looking-up or looked-up symbol id/qualified name."),
471
546
  limit: z.number().int().min(1).max(500).optional().describe("Max results (default 50)."),
472
- }, async ({ symbol, limit }) => {
547
+ ...budgetParams,
548
+ }, async ({ symbol, limit, max_tokens, cursor }) => {
473
549
  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
- });
550
+ const reflections = results.map((e) => ({
551
+ source: ref(e.source),
552
+ target: ref(e.target),
553
+ ...(e.mechanism ? { mechanism: e.mechanism } : {}),
554
+ ...(e.line !== undefined ? { line: line(e.line) } : {}),
555
+ }));
556
+ return text(paged({ count: reflections.length, reflections }, "reflections", reflections, {
557
+ max_tokens,
558
+ cursor,
559
+ }));
483
560
  });
484
561
  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
562
  symbol: z
@@ -487,33 +564,34 @@ export function createServer(store, config) {
487
564
  .optional()
488
565
  .describe("Restrict to a single registering or handling symbol id/qualified name."),
489
566
  limit: z.number().int().min(1).max(500).optional().describe("Max results (default 50)."),
490
- }, async ({ symbol, limit }) => {
567
+ ...budgetParams,
568
+ }, async ({ symbol, limit, max_tokens, cursor }) => {
491
569
  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
- });
570
+ const routes = results.map((e) => ({
571
+ source: ref(e.source),
572
+ handler: ref(e.target),
573
+ ...(e.httpMethod ? { httpMethod: e.httpMethod } : {}),
574
+ ...(e.path ? { path: e.path } : {}),
575
+ ...(e.framework ? { framework: e.framework } : {}),
576
+ ...(e.line !== undefined ? { line: line(e.line) } : {}),
577
+ }));
578
+ return text(paged({ count: routes.length, routes }, "routes", routes, { max_tokens, cursor }));
503
579
  });
504
580
  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
581
  limit: z.number().int().min(1).max(500).optional().describe("Max packages (default 50)."),
506
- }, async ({ limit }) => {
582
+ ...budgetParams,
583
+ }, async ({ limit, max_tokens, cursor }) => {
507
584
  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
- });
585
+ const packages = results.map((p) => ({
586
+ package: p.package,
587
+ references: p.references,
588
+ files: p.files,
589
+ kinds: p.kinds,
590
+ }));
591
+ return text(paged({ count: packages.length, packages }, "packages", packages, {
592
+ max_tokens,
593
+ cursor,
594
+ }));
517
595
  });
518
596
  tool("get_schema", "Describe the property-graph model: node kinds, edge kinds, and the id scheme (including annotations, aspects, and dynamic edges).", {}, async () => {
519
597
  return text({
@@ -548,6 +626,227 @@ export function createServer(store, config) {
548
626
  store.clearAll();
549
627
  return text({ ok: true, cleared: true });
550
628
  });
629
+ 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.", {
630
+ reason: z
631
+ .string()
632
+ .optional()
633
+ .describe("Case-insensitive substring filter on the resolver failure reason."),
634
+ caller_kind: z
635
+ .enum(NODE_KINDS)
636
+ .optional()
637
+ .describe("Restrict to unresolved calls made by callers of this node kind."),
638
+ limit: z
639
+ .number()
640
+ .int()
641
+ .min(1)
642
+ .max(5000)
643
+ .optional()
644
+ .describe("Max call sites scanned (default 2000)."),
645
+ ...budgetParams,
646
+ }, async ({ reason, caller_kind, limit, max_tokens, cursor }) => {
647
+ const report = findUnresolved(store, { reason, kind: caller_kind, limit });
648
+ const calls = report.calls.map((c) => ({
649
+ caller: ref(c.caller),
650
+ target: c.targetLabel,
651
+ reason: c.reason,
652
+ line: line(c.line),
653
+ }));
654
+ return text(paged({
655
+ count: calls.length,
656
+ total: report.total,
657
+ scanTruncated: report.scanTruncated,
658
+ byReason: report.byReason,
659
+ unresolved: calls,
660
+ }, "unresolved", calls, { max_tokens, cursor }));
661
+ });
662
+ 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.", {
663
+ symbol: z.string().describe("Symbol id or qualified name."),
664
+ max_depth: z
665
+ .number()
666
+ .int()
667
+ .min(1)
668
+ .max(50)
669
+ .optional()
670
+ .describe("Caller traversal depth (default 10)."),
671
+ include_dynamic: z
672
+ .boolean()
673
+ .optional()
674
+ .describe("Also follow DYNAMIC_DISPATCH edges (default true)."),
675
+ limit: z
676
+ .number()
677
+ .int()
678
+ .min(1)
679
+ .max(1000)
680
+ .optional()
681
+ .describe("Max callers returned (default 200)."),
682
+ }, async ({ symbol, max_depth, include_dynamic, limit }) => {
683
+ const result = impactAnalysis(store, symbol, {
684
+ maxDepth: max_depth,
685
+ includeDynamic: include_dynamic,
686
+ limit,
687
+ });
688
+ if (!result)
689
+ return notFound(symbol);
690
+ return text({
691
+ symbol: ref(result.symbol),
692
+ summary: result.summary,
693
+ callers: result.callers.map((c) => ({
694
+ caller: ref(c.caller),
695
+ depth: c.depth,
696
+ callSiteLine: line(c.callSiteLine),
697
+ edgeKind: c.edgeKind,
698
+ })),
699
+ entryPoints: result.entryPoints.map((e) => ({
700
+ symbol: ref(e.symbol),
701
+ entryPointKind: e.entryPointKind,
702
+ ...(e.detail ? { detail: e.detail } : {}),
703
+ viaDepth: e.viaDepth,
704
+ })),
705
+ routes: result.routes.map(wiring),
706
+ implementations: result.implementations.map((i) => ({
707
+ type: ref(i.type),
708
+ ...(i.method ? { method: ref(i.method) } : {}),
709
+ depth: i.depth,
710
+ via: i.via,
711
+ })),
712
+ affectedFiles: result.affectedFiles,
713
+ });
714
+ });
715
+ 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.", {
716
+ symbol: z.string().describe("Symbol id or qualified name."),
717
+ max_depth: z
718
+ .number()
719
+ .int()
720
+ .min(1)
721
+ .max(5)
722
+ .optional()
723
+ .describe("Caller/callee traversal depth (default 1)."),
724
+ include_dynamic: z
725
+ .boolean()
726
+ .optional()
727
+ .describe("Also follow DYNAMIC_DISPATCH edges (default true)."),
728
+ limit: z
729
+ .number()
730
+ .int()
731
+ .min(1)
732
+ .max(500)
733
+ .optional()
734
+ .describe("Max rows per section (default 50)."),
735
+ }, async ({ symbol, max_depth, include_dynamic, limit }) => {
736
+ const result = explainSymbol(store, symbol, {
737
+ maxDepth: max_depth,
738
+ includeDynamic: include_dynamic,
739
+ limit,
740
+ });
741
+ if (!result)
742
+ return notFound(symbol);
743
+ return text({
744
+ symbol: ref(result.symbol),
745
+ line: line(result.line),
746
+ isEntryPoint: result.isEntryPoint ?? false,
747
+ decorators: result.decorators.map((d) => ({ ...d, line: line(d.line) })),
748
+ members: result.members.map((m) => ({
749
+ symbol: ref(m.symbol),
750
+ line: line(m.line),
751
+ decorators: m.decorators.map((d) => ({ ...d, line: line(d.line) })),
752
+ })),
753
+ callers: result.callers.map((c) => ({
754
+ caller: ref(c.caller),
755
+ depth: c.depth,
756
+ callSiteLine: line(c.callSiteLine),
757
+ edgeKind: c.edgeKind,
758
+ })),
759
+ callees: result.callees.map((c) => ({
760
+ caller: ref(c.caller),
761
+ depth: c.depth,
762
+ callSiteLine: line(c.callSiteLine),
763
+ edgeKind: c.edgeKind,
764
+ })),
765
+ implementations: result.implementations.map((i) => ({
766
+ type: ref(i.type),
767
+ ...(i.method ? { method: ref(i.method) } : {}),
768
+ depth: i.depth,
769
+ via: i.via,
770
+ })),
771
+ routes: result.routes.map(wiring),
772
+ injections: result.injections.map(wiring),
773
+ reflections: result.reflections.map(wiring),
774
+ summary: result.summary,
775
+ });
776
+ });
777
+ 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.", {
778
+ include_dynamic: z
779
+ .boolean()
780
+ .optional()
781
+ .describe("Include DYNAMIC_DISPATCH edges (default true)."),
782
+ min_size: z
783
+ .number()
784
+ .int()
785
+ .min(1)
786
+ .max(100)
787
+ .optional()
788
+ .describe("Minimum cycle size to report (default 2)."),
789
+ edge_kinds: z
790
+ .array(z.enum(EDGE_KINDS))
791
+ .optional()
792
+ .describe("Explicit edge kinds to follow, overriding include_dynamic."),
793
+ limit: z.number().int().min(1).max(500).optional().describe("Max cycles returned (default 100)."),
794
+ ...budgetParams,
795
+ }, async ({ include_dynamic, min_size, edge_kinds, limit, max_tokens, cursor }) => {
796
+ const report = findCycles(store, {
797
+ includeDynamic: include_dynamic ?? true,
798
+ minSize: min_size,
799
+ edgeKinds: edge_kinds,
800
+ limit,
801
+ });
802
+ const cycles = report.cycles.map((c) => ({
803
+ size: c.size,
804
+ members: c.members.map(ref),
805
+ sample: c.sample,
806
+ }));
807
+ return text(paged({
808
+ count: cycles.length,
809
+ cycleCount: report.cycleCount,
810
+ edgeKinds: report.edgeKinds,
811
+ scannedNodes: report.scannedNodes,
812
+ scannedEdges: report.scannedEdges,
813
+ scanTruncated: report.scanTruncated,
814
+ selfRecursive: report.selfRecursive.map(ref),
815
+ cycles,
816
+ }, "cycles", cycles, { max_tokens, cursor }));
817
+ });
818
+ 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.", {
819
+ symbol: z
820
+ .string()
821
+ .optional()
822
+ .describe("Center symbol id or qualified name; omit to export a slice of the whole graph."),
823
+ radius: z
824
+ .number()
825
+ .int()
826
+ .min(1)
827
+ .max(5)
828
+ .optional()
829
+ .describe("Neighborhood radius around the symbol (default 2)."),
830
+ format: z.enum(["mermaid", "dot"]).optional().describe("Diagram format (default mermaid)."),
831
+ direction: z
832
+ .enum(["LR", "TB", "RL", "BT"])
833
+ .optional()
834
+ .describe("Layout direction (default LR)."),
835
+ edge_kinds: z.array(z.enum(EDGE_KINDS)).optional().describe("Edge kinds to draw."),
836
+ limit: z.number().int().min(1).max(1000).optional().describe("Max nodes (default 200)."),
837
+ }, async ({ symbol, radius, format, direction, edge_kinds, limit }) => {
838
+ const result = exportGraph(store, {
839
+ symbol,
840
+ radius,
841
+ format,
842
+ direction,
843
+ edgeKinds: edge_kinds,
844
+ limit,
845
+ });
846
+ if (!result)
847
+ return notFound(symbol ?? "");
848
+ return text(result);
849
+ });
551
850
  // ------------------------------------------------------------- resources
552
851
  server.registerResource("graph-schema", "codecrawler://schema", {
553
852
  title: "Graph schema",