@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.
- package/README.md +24 -2
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/crawler.d.ts +1 -1
- package/dist/crawler.js +42 -15
- 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 +616 -0
- package/dist/graph/insights.js.map +1 -0
- package/dist/graph/queries.d.ts +15 -1
- package/dist/graph/queries.js +25 -28
- package/dist/graph/queries.js.map +1 -1
- package/dist/graph/store.d.ts +1 -1
- package/dist/graph/store.js +4 -10
- package/dist/graph/store.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/parser/cache.js.map +1 -1
- package/dist/parser/controlFlow.js +1 -3
- package/dist/parser/controlFlow.js.map +1 -1
- package/dist/parser/languages/java.js +22 -7
- package/dist/parser/languages/java.js.map +1 -1
- package/dist/parser/languages/python.js +50 -18
- package/dist/parser/languages/python.js.map +1 -1
- package/dist/parser/languages/typescript.js +58 -19
- package/dist/parser/languages/typescript.js.map +1 -1
- package/dist/parser/moduleId.js +2 -1
- package/dist/parser/moduleId.js.map +1 -1
- package/dist/parser/treeSitter.d.ts +1 -1
- package/dist/parser/treeSitter.js +2 -2
- package/dist/parser/treeSitter.js.map +1 -1
- package/dist/resolver/common.js +89 -23
- package/dist/resolver/common.js.map +1 -1
- package/dist/resolver/dynamic.d.ts +1 -1
- package/dist/resolver/dynamic.js +4 -2
- package/dist/resolver/dynamic.js.map +1 -1
- package/dist/resolver/genericResolver.js +61 -12
- package/dist/resolver/genericResolver.js.map +1 -1
- package/dist/resolver/javaResolver.js +7 -4
- package/dist/resolver/javaResolver.js.map +1 -1
- package/dist/resolver/types.js +1 -1
- package/dist/resolver/types.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +491 -173
- package/dist/server.js.map +1 -1
- 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 &&
|
|
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
|
-
|
|
169
|
+
...budgetParams,
|
|
170
|
+
}, async ({ query, kind, limit, max_tokens, cursor }) => {
|
|
121
171
|
const results = searchSymbols(store, query, { kind, limit });
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
})),
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
295
|
-
implementations
|
|
296
|
-
|
|
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
|
-
|
|
385
|
+
...budgetParams,
|
|
386
|
+
}, async ({ limit, max_tokens, cursor }) => {
|
|
305
387
|
const aspects = getAspects(store, { limit });
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
336
|
-
|
|
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
|
|
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
|
-
|
|
435
|
+
...budgetParams,
|
|
436
|
+
}, async ({ annotation, kind, limit, max_tokens, cursor }) => {
|
|
349
437
|
const results = findAnnotated(store, annotation, { kind, limit });
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
521
|
+
...budgetParams,
|
|
522
|
+
}, async ({ kind, limit, max_tokens, cursor }) => {
|
|
436
523
|
const results = findEntryPoints(store, { kind, limit });
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
540
|
+
...budgetParams,
|
|
541
|
+
}, async ({ symbol, limit, max_tokens, cursor }) => {
|
|
453
542
|
const results = findInjections(store, { symbol, limit });
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
562
|
+
...budgetParams,
|
|
563
|
+
}, async ({ symbol, limit, max_tokens, cursor }) => {
|
|
473
564
|
const results = findReflections(store, { symbol, limit });
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
582
|
+
...budgetParams,
|
|
583
|
+
}, async ({ symbol, limit, max_tokens, cursor }) => {
|
|
491
584
|
const results = findRoutes(store, { symbol, limit });
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
597
|
+
...budgetParams,
|
|
598
|
+
}, async ({ limit, max_tokens, cursor }) => {
|
|
507
599
|
const results = externalDependencies(store, { limit });
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
},
|