@grafema/mcp 0.3.21 → 0.3.23
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/definitions/query-tools.d.ts.map +1 -1
- package/dist/definitions/query-tools.js +102 -1
- package/dist/definitions/query-tools.js.map +1 -1
- package/dist/handlers/analysis-handlers.d.ts.map +1 -1
- package/dist/handlers/analysis-handlers.js +5 -1
- package/dist/handlers/analysis-handlers.js.map +1 -1
- package/dist/handlers/context-handlers.d.ts +4 -0
- package/dist/handlers/context-handlers.d.ts.map +1 -1
- package/dist/handlers/context-handlers.js +76 -1
- package/dist/handlers/context-handlers.js.map +1 -1
- package/dist/handlers/dataflow-handlers.d.ts +8 -1
- package/dist/handlers/dataflow-handlers.d.ts.map +1 -1
- package/dist/handlers/dataflow-handlers.js +143 -1
- package/dist/handlers/dataflow-handlers.js.map +1 -1
- package/dist/handlers/index.d.ts +3 -2
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +2 -2
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/query-handlers.d.ts.map +1 -1
- package/dist/handlers/query-handlers.js +235 -7
- package/dist/handlers/query-handlers.js.map +1 -1
- package/dist/server.js +51 -15
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/definitions/query-tools.ts +102 -1
- package/src/handlers/analysis-handlers.ts +7 -1
- package/src/handlers/context-handlers.ts +80 -1
- package/src/handlers/dataflow-handlers.ts +164 -0
- package/src/handlers/index.ts +3 -2
- package/src/handlers/query-handlers.ts +239 -14
- package/src/server.ts +59 -14
- package/src/types.ts +12 -0
|
@@ -272,6 +272,36 @@ export async function handleFindCalls(args: FindCallsArgs): Promise<ToolResult>
|
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
// Also find callback usages: where the function is passed as argument
|
|
276
|
+
// Pattern: CALL "action" → PASSES_ARGUMENT → REFERENCE "analyzeAction" → READS_FROM → FUNCTION
|
|
277
|
+
// Search: REFERENCE nodes with matching name that have incoming PASSES_ARGUMENT
|
|
278
|
+
if (totalMatched === 0 || calls.length < limit) {
|
|
279
|
+
for await (const ref of db.queryNodes({ type: 'REFERENCE', name })) {
|
|
280
|
+
const inEdges = await db.getIncomingEdges(ref.id, ['PASSES_ARGUMENT' as any]);
|
|
281
|
+
if (inEdges.length === 0) continue;
|
|
282
|
+
|
|
283
|
+
totalMatched++;
|
|
284
|
+
if (skipped < offset) { skipped++; continue; }
|
|
285
|
+
if (calls.length >= limit) continue;
|
|
286
|
+
|
|
287
|
+
const callerNode = await db.getNode(inEdges[0].src);
|
|
288
|
+
calls.push({
|
|
289
|
+
id: ref.id,
|
|
290
|
+
name: `${callerNode?.name ?? '?'}(${name})`,
|
|
291
|
+
object: undefined,
|
|
292
|
+
file: ref.file,
|
|
293
|
+
line: ref.line,
|
|
294
|
+
resolved: true,
|
|
295
|
+
target: callerNode ? {
|
|
296
|
+
type: callerNode.type,
|
|
297
|
+
name: callerNode.name ?? '',
|
|
298
|
+
file: callerNode.file,
|
|
299
|
+
line: callerNode.line,
|
|
300
|
+
} : null,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
275
305
|
if (totalMatched === 0) {
|
|
276
306
|
return textResult(`No calls found for "${className ? className + '.' : ''}${name}"`);
|
|
277
307
|
}
|
|
@@ -355,6 +385,7 @@ export async function handleFindNodes(args: FindNodesArgs): Promise<ToolResult>
|
|
|
355
385
|
if (name) filter.name = name;
|
|
356
386
|
if (file) filter.file = file;
|
|
357
387
|
filter.substringMatch = true;
|
|
388
|
+
filter.fuzzyNameFallback = true;
|
|
358
389
|
|
|
359
390
|
const nodes: GraphNode[] = [];
|
|
360
391
|
let skipped = 0;
|
|
@@ -362,14 +393,39 @@ export async function handleFindNodes(args: FindNodesArgs): Promise<ToolResult>
|
|
|
362
393
|
|
|
363
394
|
for await (const node of db.queryNodes(filter)) {
|
|
364
395
|
totalMatched++;
|
|
396
|
+
if (skipped < offset) { skipped++; continue; }
|
|
397
|
+
if (nodes.length < limit) nodes.push(node);
|
|
398
|
+
}
|
|
365
399
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
400
|
+
// === Progressive fallback chain ===
|
|
401
|
+
let fallbackLevel = 'exact';
|
|
402
|
+
|
|
403
|
+
// Level 2: type relaxation ("did you mean?")
|
|
404
|
+
if (totalMatched === 0 && type && name) {
|
|
405
|
+
fallbackLevel = 'type_relaxed';
|
|
406
|
+
const relaxedFilter: Record<string, unknown> = {};
|
|
407
|
+
if (name) relaxedFilter.name = name;
|
|
408
|
+
if (file) relaxedFilter.file = file;
|
|
409
|
+
relaxedFilter.substringMatch = true;
|
|
410
|
+
relaxedFilter.fuzzyNameFallback = true;
|
|
411
|
+
|
|
412
|
+
for await (const node of db.queryNodes(relaxedFilter)) {
|
|
413
|
+
totalMatched++;
|
|
414
|
+
if (nodes.length < 5) nodes.push(node);
|
|
369
415
|
}
|
|
416
|
+
}
|
|
370
417
|
|
|
371
|
-
|
|
372
|
-
|
|
418
|
+
// Level 3: grep fallback — search source files, enrich with nearest graph nodes
|
|
419
|
+
if (totalMatched === 0 && name) {
|
|
420
|
+
fallbackLevel = 'grep_enriched';
|
|
421
|
+
const grepResults = await grepAndEnrich(db, name, file);
|
|
422
|
+
if (grepResults.length > 0) {
|
|
423
|
+
const typeList = [...new Set(grepResults.map(r => r.type).filter(Boolean))].join(', ');
|
|
424
|
+
return textResult(
|
|
425
|
+
`No graph nodes matched "${name}"${type ? ` (type: ${type})` : ''}. ` +
|
|
426
|
+
`Found ${grepResults.length} match(es) via text search, enriched with graph context (${typeList || 'unresolved'}):\n\n` +
|
|
427
|
+
JSON.stringify(serializeBigInt(grepResults), null, 2)
|
|
428
|
+
);
|
|
373
429
|
}
|
|
374
430
|
}
|
|
375
431
|
|
|
@@ -377,20 +433,189 @@ export async function handleFindNodes(args: FindNodesArgs): Promise<ToolResult>
|
|
|
377
433
|
return textResult('No nodes found matching criteria');
|
|
378
434
|
}
|
|
379
435
|
|
|
436
|
+
// === Rich context enrichment ===
|
|
437
|
+
// For each found node, add structural context (methods, calls, imports)
|
|
438
|
+
// Only enrich when result set is small enough (≤10 nodes) to avoid latency
|
|
439
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
440
|
+
const enriched = nodes.length <= 10
|
|
441
|
+
? await enrichNodes(db as any, nodes)
|
|
442
|
+
: nodes;
|
|
443
|
+
|
|
380
444
|
const hasMore = offset + nodes.length < totalMatched;
|
|
381
445
|
const paginationInfo = formatPaginationInfo({
|
|
382
|
-
limit,
|
|
383
|
-
offset,
|
|
384
|
-
returned: nodes.length,
|
|
385
|
-
total: totalMatched,
|
|
386
|
-
hasMore,
|
|
446
|
+
limit, offset, returned: nodes.length, total: totalMatched, hasMore,
|
|
387
447
|
});
|
|
388
448
|
|
|
449
|
+
const fallbackNote = fallbackLevel === 'type_relaxed'
|
|
450
|
+
? `No ${type} nodes found matching "${name}". Showing results without type filter:\n`
|
|
451
|
+
: '';
|
|
452
|
+
|
|
389
453
|
return textResult(
|
|
390
|
-
|
|
391
|
-
serializeBigInt(
|
|
392
|
-
null,
|
|
393
|
-
2
|
|
454
|
+
`${fallbackNote}Found ${totalMatched} node(s):${paginationInfo}\n\n${JSON.stringify(
|
|
455
|
+
serializeBigInt(enriched), null, 2
|
|
394
456
|
)}`
|
|
395
457
|
);
|
|
396
458
|
}
|
|
459
|
+
|
|
460
|
+
/** Enrich nodes with structural context from graph edges */
|
|
461
|
+
async function enrichNodes(
|
|
462
|
+
db: { getOutgoingEdges(id: string, types?: string[] | null): Promise<Array<Record<string, unknown>>>; getIncomingEdges(id: string, types?: string[] | null): Promise<Array<Record<string, unknown>>> },
|
|
463
|
+
nodes: GraphNode[]
|
|
464
|
+
): Promise<Array<GraphNode & { _context?: Record<string, unknown> }>> {
|
|
465
|
+
const result = [];
|
|
466
|
+
for (const node of nodes) {
|
|
467
|
+
const nodeId = node.id;
|
|
468
|
+
if (!nodeId) { result.push(node); continue; }
|
|
469
|
+
|
|
470
|
+
const context: Record<string, unknown> = {};
|
|
471
|
+
try {
|
|
472
|
+
// Get key outgoing edges (what this node uses/calls/contains)
|
|
473
|
+
const outEdges = await db.getOutgoingEdges(nodeId, null);
|
|
474
|
+
const edgeType = (e: Record<string, unknown>) => String(e.type || '');
|
|
475
|
+
const edgeSrc = (e: Record<string, unknown>) => String(e.src || '');
|
|
476
|
+
|
|
477
|
+
const containsCount = outEdges.filter(e => edgeType(e) === 'CONTAINS').length;
|
|
478
|
+
const callsOut = outEdges.filter(e => edgeType(e) === 'CALLS');
|
|
479
|
+
const imports = outEdges.filter(e => edgeType(e) === 'IMPORTS_FROM');
|
|
480
|
+
|
|
481
|
+
const inEdges = await db.getIncomingEdges(nodeId, null);
|
|
482
|
+
const callsIn = inEdges.filter(e => edgeType(e) === 'CALLS');
|
|
483
|
+
const containedBy = inEdges.filter(e => edgeType(e) === 'CONTAINS');
|
|
484
|
+
|
|
485
|
+
if (containsCount > 0) context.contains = containsCount;
|
|
486
|
+
if (callsOut.length > 0) context.calls_out = callsOut.length;
|
|
487
|
+
if (callsIn.length > 0) {
|
|
488
|
+
context.called_by = callsIn.length;
|
|
489
|
+
context.callers = callsIn.slice(0, 3).map(e => humanReadableId(edgeSrc(e)));
|
|
490
|
+
}
|
|
491
|
+
if (imports.length > 0) context.imports = imports.length;
|
|
492
|
+
if (containedBy.length > 0) {
|
|
493
|
+
context.parent = humanReadableId(edgeSrc(containedBy[0]));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// For CLASS/INTERFACE nodes: list methods and properties
|
|
497
|
+
const nodeType = String(node.type || '');
|
|
498
|
+
if (nodeType === 'CLASS' || nodeType === 'INTERFACE') {
|
|
499
|
+
const edgeDst = (e: Record<string, unknown>) => String(e.dst || '');
|
|
500
|
+
const methodEdges = outEdges.filter(e => edgeType(e) === 'HAS_METHOD');
|
|
501
|
+
const containsEdges = outEdges.filter(e => edgeType(e) === 'CONTAINS');
|
|
502
|
+
const memberEdges = methodEdges.length > 0 ? methodEdges : containsEdges;
|
|
503
|
+
if (memberEdges.length > 0 && memberEdges.length <= 30) {
|
|
504
|
+
const members = memberEdges
|
|
505
|
+
.map(e => humanReadableId(edgeDst(e)))
|
|
506
|
+
.filter(n => n !== '?');
|
|
507
|
+
if (members.length > 0) context.members = members.slice(0, 15);
|
|
508
|
+
} else if (memberEdges.length > 30) {
|
|
509
|
+
context.member_count = memberEdges.length;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
// Edge queries may fail for some node types — skip enrichment silently
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (Object.keys(context).length > 0) {
|
|
517
|
+
result.push({ ...node, _context: context });
|
|
518
|
+
} else {
|
|
519
|
+
result.push(node);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Convert a semantic ID to human-readable "name in file.ts" format.
|
|
526
|
+
* Handles: grafema://host/path/file.ts#TYPE->name[scope], path/file.ts#TYPE->name,
|
|
527
|
+
* TYPE-%3Ename (URL-encoded, no file prefix), or raw node IDs. */
|
|
528
|
+
function humanReadableId(semanticId: string): string {
|
|
529
|
+
if (!semanticId) return '?';
|
|
530
|
+
// Decode URI components first
|
|
531
|
+
let id = semanticId;
|
|
532
|
+
try { id = decodeURIComponent(id); } catch { /* keep as-is */ }
|
|
533
|
+
|
|
534
|
+
// Find the # separator between file path and node descriptor
|
|
535
|
+
const hashIdx = id.lastIndexOf('#');
|
|
536
|
+
let filePart = '';
|
|
537
|
+
let nodePart = id;
|
|
538
|
+
if (hashIdx !== -1) {
|
|
539
|
+
filePart = id.slice(0, hashIdx);
|
|
540
|
+
nodePart = id.slice(hashIdx + 1);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const fileName = filePart ? (filePart.split('/').pop() || '') : '';
|
|
544
|
+
|
|
545
|
+
// Extract name from TYPE->name or TYPE->name[in:scope,h:hash]
|
|
546
|
+
const arrowIdx = nodePart.indexOf('->');
|
|
547
|
+
if (arrowIdx === -1) return fileName || nodePart;
|
|
548
|
+
|
|
549
|
+
let name = nodePart.slice(arrowIdx + 2);
|
|
550
|
+
// Strip scope/hash info [in:xxx,h:xxx]
|
|
551
|
+
const bracketIdx = name.indexOf('[');
|
|
552
|
+
let scope = '';
|
|
553
|
+
if (bracketIdx > 0) {
|
|
554
|
+
const scopeStr = name.slice(bracketIdx + 1, -1);
|
|
555
|
+
const inMatch = scopeStr.match(/in:([^,]+)/);
|
|
556
|
+
if (inMatch) scope = inMatch[1];
|
|
557
|
+
name = name.slice(0, bracketIdx);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Build readable string: "name" or "name (in scope)" or "name in file.ts"
|
|
561
|
+
if (scope && scope !== name) {
|
|
562
|
+
return fileName ? `${name} (in ${scope}) in ${fileName}` : `${name} (in ${scope})`;
|
|
563
|
+
}
|
|
564
|
+
return fileName ? `${name} in ${fileName}` : name || '?';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Grep source files for a name, then find nearest graph nodes for each match */
|
|
568
|
+
async function grepAndEnrich(
|
|
569
|
+
db: { queryNodes(filter: Record<string, unknown>): AsyncIterable<GraphNode> },
|
|
570
|
+
name: string,
|
|
571
|
+
filePattern?: string
|
|
572
|
+
): Promise<Array<{ file: string; line: number; match: string; type?: string; node_name?: string; node_id?: string }>> {
|
|
573
|
+
const { execFileSync } = await import('child_process');
|
|
574
|
+
const { getProjectPath } = await import('../state.js');
|
|
575
|
+
const projectRoot = getProjectPath();
|
|
576
|
+
if (!projectRoot) return [];
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const args = ['-rn', '--include=*.ts', '--include=*.js', '--include=*.tsx', '-l', name];
|
|
580
|
+
if (filePattern) args.push(filePattern);
|
|
581
|
+
else args.push(projectRoot);
|
|
582
|
+
|
|
583
|
+
const output = execFileSync('grep', args, {
|
|
584
|
+
timeout: 5000,
|
|
585
|
+
maxBuffer: 50000,
|
|
586
|
+
encoding: 'utf-8',
|
|
587
|
+
}).trim();
|
|
588
|
+
if (!output) return [];
|
|
589
|
+
|
|
590
|
+
const files = output.split('\n').slice(0, 5);
|
|
591
|
+
const results = [];
|
|
592
|
+
|
|
593
|
+
for (const matchFile of files) {
|
|
594
|
+
// Find graph nodes in this file
|
|
595
|
+
const relPath = matchFile.replace(projectRoot + '/', '');
|
|
596
|
+
const fileNodes: GraphNode[] = [];
|
|
597
|
+
for await (const node of db.queryNodes({ file: relPath, substringMatch: true })) {
|
|
598
|
+
if (fileNodes.length < 3) fileNodes.push(node);
|
|
599
|
+
else break;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (fileNodes.length > 0) {
|
|
603
|
+
for (const n of fileNodes) {
|
|
604
|
+
results.push({
|
|
605
|
+
file: relPath,
|
|
606
|
+
line: n.line || 0,
|
|
607
|
+
match: name,
|
|
608
|
+
type: n.type,
|
|
609
|
+
node_name: n.name,
|
|
610
|
+
node_id: n.id,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
} else {
|
|
614
|
+
results.push({ file: relPath, line: 0, match: name });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return results;
|
|
618
|
+
} catch {
|
|
619
|
+
return [];
|
|
620
|
+
}
|
|
621
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
handleFindNodes,
|
|
45
45
|
handleTraceAlias,
|
|
46
46
|
handleTraceDataFlow,
|
|
47
|
+
handleTraceCallChain,
|
|
47
48
|
handleCheckInvariant,
|
|
48
49
|
handleAnalyzeProject,
|
|
49
50
|
handleGetAnalysisStatus,
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
handleReadProjectStructure,
|
|
63
64
|
handleWriteConfig,
|
|
64
65
|
handleGetFileOverview,
|
|
66
|
+
handleGetShape,
|
|
65
67
|
handleGetNode,
|
|
66
68
|
handleGetNeighbors,
|
|
67
69
|
handleTraverseGraph,
|
|
@@ -78,7 +80,9 @@ import {
|
|
|
78
80
|
handleDescribe,
|
|
79
81
|
handleGraphQLQuery,
|
|
80
82
|
handleQueryRegistry,
|
|
83
|
+
handleExplain,
|
|
81
84
|
} from './handlers/index.js';
|
|
85
|
+
import type { ExplainArgs } from './handlers/index.js';
|
|
82
86
|
import type {
|
|
83
87
|
ToolResult,
|
|
84
88
|
ReportIssueArgs,
|
|
@@ -90,6 +94,7 @@ import type {
|
|
|
90
94
|
FindNodesArgs,
|
|
91
95
|
TraceAliasArgs,
|
|
92
96
|
TraceDataFlowArgs,
|
|
97
|
+
TraceCallChainArgs,
|
|
93
98
|
CheckInvariantArgs,
|
|
94
99
|
AnalyzeProjectArgs,
|
|
95
100
|
GetSchemaArgs,
|
|
@@ -101,6 +106,7 @@ import type {
|
|
|
101
106
|
ReadProjectStructureArgs,
|
|
102
107
|
WriteConfigArgs,
|
|
103
108
|
GetFileOverviewArgs,
|
|
109
|
+
GetShapeArgs,
|
|
104
110
|
GetNodeArgs,
|
|
105
111
|
GetNeighborsArgs,
|
|
106
112
|
TraverseGraphArgs,
|
|
@@ -152,20 +158,47 @@ const server = new Server(
|
|
|
152
158
|
START HERE: call get_stats to check if the graph is loaded (nodeCount > 0).
|
|
153
159
|
If nodeCount is 0, call analyze_project first.
|
|
154
160
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
161
|
+
IMPORTANT: for structural questions about code (who calls what, where is something defined,
|
|
162
|
+
how does data flow), avoid using Grep — it only does text matching and misses calls through
|
|
163
|
+
aliases, re-exports, and dynamic dispatch. Use graph tools instead.
|
|
164
|
+
|
|
165
|
+
TOOL ROUTING — use the right tool for the task:
|
|
166
|
+
- "Where is X defined?" → find_nodes(name="X") or find_nodes(name="X", type="CLASS")
|
|
167
|
+
- "Who calls function X?" → find_calls(name="X")
|
|
168
|
+
- "What does file X contain?" → get_file_overview(file="X")
|
|
169
|
+
- "How does data flow from A to B?" → trace_dataflow(source="A", direction="forward")
|
|
170
|
+
- "What's the structure of class X?" → describe(nodeId="X")
|
|
171
|
+
- "Find all classes in directory Y" → find_nodes(type="CLASS", file="Y/")
|
|
172
|
+
- For text search in comments or strings → Grep
|
|
173
|
+
- For reading exact source code → Read
|
|
174
|
+
|
|
175
|
+
EXAMPLES — how to answer common questions using graph tools:
|
|
176
|
+
|
|
177
|
+
Example 1: "Where is the drag and drop handler for the file explorer?"
|
|
178
|
+
→ find_nodes(name="DragAndDrop", type="CLASS")
|
|
179
|
+
→ Result: FileDragAndDrop in src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
|
|
180
|
+
→ Then: get_file_overview(file="explorerViewer.ts") to see related classes
|
|
181
|
+
|
|
182
|
+
Example 2: "Who calls the createTerminal method?"
|
|
183
|
+
→ find_calls(name="createTerminal")
|
|
184
|
+
→ Result: 12 call sites across 5 files with file:line locations
|
|
185
|
+
→ Then: Read specific call sites for implementation details
|
|
186
|
+
|
|
187
|
+
Example 3: "What is the lifecycle of a terminal instance?"
|
|
188
|
+
→ find_nodes(name="Terminal", type="CLASS") to find key classes
|
|
189
|
+
→ find_calls(name="createTerminal") to find entry points
|
|
190
|
+
→ trace_dataflow(source="TerminalInstance", direction="forward") to trace the flow
|
|
191
|
+
→ Combine graph results with targeted Read for implementation details
|
|
192
|
+
|
|
193
|
+
find_nodes supports partial matching: find_nodes(file="auth/") matches all files in auth/.
|
|
194
|
+
find_nodes(name="redis", type="CALL") finds all calls containing "redis".
|
|
195
|
+
|
|
196
|
+
TIP: If unsure about the type, omit it — find_nodes(name="Foo") searches all types.
|
|
197
|
+
Results include _context with callers, members, and parent — often no follow-up needed.
|
|
198
|
+
|
|
199
|
+
BUG FIX PATTERN: After identifying a bug in a method, use find_calls(name="method") to check
|
|
200
|
+
ALL callers. Other components may call the same method without the guard your fix adds.
|
|
201
|
+
This catches "same bug, different caller" patterns common in large codebases.`,
|
|
169
202
|
}
|
|
170
203
|
);
|
|
171
204
|
|
|
@@ -217,6 +250,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
217
250
|
result = await handleTraceDataFlow(asArgs<TraceDataFlowArgs>(args));
|
|
218
251
|
break;
|
|
219
252
|
|
|
253
|
+
case 'trace_calls':
|
|
254
|
+
result = await handleTraceCallChain(asArgs<TraceCallChainArgs>(args));
|
|
255
|
+
break;
|
|
256
|
+
|
|
257
|
+
case 'explain':
|
|
258
|
+
result = await handleExplain(asArgs<ExplainArgs>(args));
|
|
259
|
+
break;
|
|
260
|
+
|
|
220
261
|
case 'check_invariant':
|
|
221
262
|
result = await handleCheckInvariant(asArgs<CheckInvariantArgs>(args));
|
|
222
263
|
break;
|
|
@@ -286,6 +327,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
286
327
|
result = await handleGetFileOverview(asArgs<GetFileOverviewArgs>(args));
|
|
287
328
|
break;
|
|
288
329
|
|
|
330
|
+
case 'get_shape':
|
|
331
|
+
result = await handleGetShape(asArgs<GetShapeArgs>(args));
|
|
332
|
+
break;
|
|
333
|
+
|
|
289
334
|
case 'read_project_structure':
|
|
290
335
|
result = await handleReadProjectStructure(asArgs<ReadProjectStructureArgs>(args));
|
|
291
336
|
break;
|
package/src/types.ts
CHANGED
|
@@ -74,6 +74,18 @@ export interface TraceDataFlowArgs {
|
|
|
74
74
|
detail?: 'summary' | 'normal' | 'full';
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export interface TraceCallChainArgs {
|
|
78
|
+
source: string;
|
|
79
|
+
file?: string;
|
|
80
|
+
direction?: string;
|
|
81
|
+
max_depth?: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface GetShapeArgs {
|
|
85
|
+
target: string;
|
|
86
|
+
file?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
77
89
|
export interface CheckInvariantArgs {
|
|
78
90
|
rule: string;
|
|
79
91
|
name?: string;
|