@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/README.md +96 -4
- package/dist/crawler.d.ts +7 -0
- package/dist/crawler.js +126 -29
- package/dist/crawler.js.map +1 -1
- package/dist/graph/budget.d.ts +46 -0
- package/dist/graph/budget.js +72 -0
- package/dist/graph/budget.js.map +1 -0
- package/dist/graph/insights.d.ts +217 -0
- package/dist/graph/insights.js +612 -0
- package/dist/graph/insights.js.map +1 -0
- package/dist/graph/queries.d.ts +23 -6
- package/dist/graph/queries.js +106 -49
- package/dist/graph/queries.js.map +1 -1
- package/dist/graph/store.d.ts +19 -0
- package/dist/graph/store.js +170 -26
- package/dist/graph/store.js.map +1 -1
- package/dist/parser/cache.d.ts +28 -0
- package/dist/parser/cache.js +80 -0
- package/dist/parser/cache.js.map +1 -0
- package/dist/server.js +462 -163
- package/dist/server.js.map +1 -1
- package/package.json +3 -2
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
|
-
|
|
168
|
+
...budgetParams,
|
|
169
|
+
}, async ({ query, kind, limit, max_tokens, cursor }) => {
|
|
121
170
|
const results = searchSymbols(store, query, { kind, limit });
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
295
|
-
implementations
|
|
296
|
-
|
|
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
|
-
|
|
378
|
+
...budgetParams,
|
|
379
|
+
}, async ({ limit, max_tokens, cursor }) => {
|
|
305
380
|
const aspects = getAspects(store, { limit });
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
336
|
-
|
|
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
|
-
|
|
426
|
+
...budgetParams,
|
|
427
|
+
}, async ({ annotation, kind, limit, max_tokens, cursor }) => {
|
|
349
428
|
const results = findAnnotated(store, annotation, { kind, limit });
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
509
|
+
...budgetParams,
|
|
510
|
+
}, async ({ kind, limit, max_tokens, cursor }) => {
|
|
436
511
|
const results = findEntryPoints(store, { kind, limit });
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
525
|
+
...budgetParams,
|
|
526
|
+
}, async ({ symbol, limit, max_tokens, cursor }) => {
|
|
453
527
|
const results = findInjections(store, { symbol, limit });
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
547
|
+
...budgetParams,
|
|
548
|
+
}, async ({ symbol, limit, max_tokens, cursor }) => {
|
|
473
549
|
const results = findReflections(store, { symbol, limit });
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
567
|
+
...budgetParams,
|
|
568
|
+
}, async ({ symbol, limit, max_tokens, cursor }) => {
|
|
491
569
|
const results = findRoutes(store, { symbol, limit });
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
582
|
+
...budgetParams,
|
|
583
|
+
}, async ({ limit, max_tokens, cursor }) => {
|
|
507
584
|
const results = externalDependencies(store, { limit });
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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",
|