@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.
Files changed (34) hide show
  1. package/dist/definitions/query-tools.d.ts.map +1 -1
  2. package/dist/definitions/query-tools.js +102 -1
  3. package/dist/definitions/query-tools.js.map +1 -1
  4. package/dist/handlers/analysis-handlers.d.ts.map +1 -1
  5. package/dist/handlers/analysis-handlers.js +5 -1
  6. package/dist/handlers/analysis-handlers.js.map +1 -1
  7. package/dist/handlers/context-handlers.d.ts +4 -0
  8. package/dist/handlers/context-handlers.d.ts.map +1 -1
  9. package/dist/handlers/context-handlers.js +76 -1
  10. package/dist/handlers/context-handlers.js.map +1 -1
  11. package/dist/handlers/dataflow-handlers.d.ts +8 -1
  12. package/dist/handlers/dataflow-handlers.d.ts.map +1 -1
  13. package/dist/handlers/dataflow-handlers.js +143 -1
  14. package/dist/handlers/dataflow-handlers.js.map +1 -1
  15. package/dist/handlers/index.d.ts +3 -2
  16. package/dist/handlers/index.d.ts.map +1 -1
  17. package/dist/handlers/index.js +2 -2
  18. package/dist/handlers/index.js.map +1 -1
  19. package/dist/handlers/query-handlers.d.ts.map +1 -1
  20. package/dist/handlers/query-handlers.js +235 -7
  21. package/dist/handlers/query-handlers.js.map +1 -1
  22. package/dist/server.js +51 -15
  23. package/dist/server.js.map +1 -1
  24. package/dist/types.d.ts +10 -0
  25. package/dist/types.d.ts.map +1 -1
  26. package/package.json +4 -4
  27. package/src/definitions/query-tools.ts +102 -1
  28. package/src/handlers/analysis-handlers.ts +7 -1
  29. package/src/handlers/context-handlers.ts +80 -1
  30. package/src/handlers/dataflow-handlers.ts +164 -0
  31. package/src/handlers/index.ts +3 -2
  32. package/src/handlers/query-handlers.ts +239 -14
  33. package/src/server.ts +59 -14
  34. 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
- if (skipped < offset) {
367
- skipped++;
368
- continue;
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
- if (nodes.length < limit) {
372
- nodes.push(node);
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
- `Found ${totalMatched} node(s):${paginationInfo}\n\n${JSON.stringify(
391
- serializeBigInt(nodes),
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
- EXPLORATION WORKFLOW:
156
- 1. To understand a file get_file_overview (shows imports, exports, functions, classes with relationships)
157
- 2. To find functions/classes/modules find_nodes (filter by type, name, or file pattern)
158
- 3. To find who calls a function → find_calls (returns call sites with resolution status)
159
- 4. To understand data flow trace_dataflow (forward: where does this value go? backward: where does it come from?)
160
- 5. To understand full context of a node get_context (shows surrounding code, scope chain, relationships)
161
- 6. For complex pattern queriesquery_graph with Datalog (call get_documentation topic="queries" for syntax)
162
- 7. To query architectural decisions and facts query_knowledge, query_decisions, get_knowledge_stats
163
- 8. To get a compact visual summarydescribe (renders DSL notation with archetype-grouped operators)
164
- 9. For DSL syntax referenceget_documentation topic="notation"
165
-
166
- KEY INSIGHT: find_nodes supports partial matching on name and file fields.
167
- Example: find_nodes(file="auth/") returns all nodes in files matching "auth/".
168
- Example: find_nodes(name="redis", type="CALL") finds all calls containing "redis".`,
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;