@grafema/mcp 0.2.1-beta → 0.2.6-beta

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 (96) hide show
  1. package/README.md +49 -25
  2. package/dist/analysis-worker.js +16 -8
  3. package/dist/analysis-worker.js.map +1 -0
  4. package/dist/analysis.d.ts.map +1 -1
  5. package/dist/analysis.js +1 -0
  6. package/dist/analysis.js.map +1 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +27 -7
  9. package/dist/config.js.map +1 -0
  10. package/dist/definitions.d.ts +4 -4
  11. package/dist/definitions.d.ts.map +1 -1
  12. package/dist/definitions.js +158 -3
  13. package/dist/definitions.js.map +1 -0
  14. package/dist/handlers/analysis-handlers.d.ts +9 -0
  15. package/dist/handlers/analysis-handlers.d.ts.map +1 -0
  16. package/dist/handlers/analysis-handlers.js +73 -0
  17. package/dist/handlers/analysis-handlers.js.map +1 -0
  18. package/dist/handlers/context-handlers.d.ts +21 -0
  19. package/dist/handlers/context-handlers.d.ts.map +1 -0
  20. package/dist/handlers/context-handlers.js +330 -0
  21. package/dist/handlers/context-handlers.js.map +1 -0
  22. package/dist/handlers/coverage-handlers.d.ts +6 -0
  23. package/dist/handlers/coverage-handlers.d.ts.map +1 -0
  24. package/dist/handlers/coverage-handlers.js +42 -0
  25. package/dist/handlers/coverage-handlers.js.map +1 -0
  26. package/dist/handlers/dataflow-handlers.d.ts +8 -0
  27. package/dist/handlers/dataflow-handlers.d.ts.map +1 -0
  28. package/dist/handlers/dataflow-handlers.js +140 -0
  29. package/dist/handlers/dataflow-handlers.js.map +1 -0
  30. package/dist/handlers/documentation-handlers.d.ts +6 -0
  31. package/dist/handlers/documentation-handlers.d.ts.map +1 -0
  32. package/dist/handlers/documentation-handlers.js +79 -0
  33. package/dist/handlers/documentation-handlers.js.map +1 -0
  34. package/dist/handlers/guarantee-handlers.d.ts +21 -0
  35. package/dist/handlers/guarantee-handlers.d.ts.map +1 -0
  36. package/dist/handlers/guarantee-handlers.js +251 -0
  37. package/dist/handlers/guarantee-handlers.js.map +1 -0
  38. package/dist/handlers/guard-handlers.d.ts +14 -0
  39. package/dist/handlers/guard-handlers.d.ts.map +1 -0
  40. package/dist/handlers/guard-handlers.js +77 -0
  41. package/dist/handlers/guard-handlers.js.map +1 -0
  42. package/dist/handlers/index.d.ts +14 -0
  43. package/dist/handlers/index.d.ts.map +1 -0
  44. package/dist/handlers/index.js +14 -0
  45. package/dist/handlers/index.js.map +1 -0
  46. package/dist/handlers/issue-handlers.d.ts +6 -0
  47. package/dist/handlers/issue-handlers.d.ts.map +1 -0
  48. package/dist/handlers/issue-handlers.js +66 -0
  49. package/dist/handlers/issue-handlers.js.map +1 -0
  50. package/dist/handlers/project-handlers.d.ts +7 -0
  51. package/dist/handlers/project-handlers.d.ts.map +1 -0
  52. package/dist/handlers/project-handlers.js +153 -0
  53. package/dist/handlers/project-handlers.js.map +1 -0
  54. package/dist/handlers/query-handlers.d.ts +8 -0
  55. package/dist/handlers/query-handlers.d.ts.map +1 -0
  56. package/dist/handlers/query-handlers.js +171 -0
  57. package/dist/handlers/query-handlers.js.map +1 -0
  58. package/dist/handlers.d.ts +6 -2
  59. package/dist/handlers.d.ts.map +1 -1
  60. package/dist/handlers.js +367 -20
  61. package/dist/handlers.js.map +1 -0
  62. package/dist/prompts.d.ts +25 -0
  63. package/dist/prompts.d.ts.map +1 -0
  64. package/dist/prompts.js +33 -0
  65. package/dist/prompts.js.map +1 -0
  66. package/dist/server.js +52 -20
  67. package/dist/server.js.map +1 -0
  68. package/dist/state.js +1 -0
  69. package/dist/state.js.map +1 -0
  70. package/dist/types.d.ts +59 -1
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js +1 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/utils.js +2 -1
  75. package/dist/utils.js.map +1 -0
  76. package/package.json +4 -3
  77. package/src/analysis-worker.ts +20 -9
  78. package/src/analysis.ts +2 -1
  79. package/src/config.ts +36 -4
  80. package/src/definitions.ts +162 -5
  81. package/src/handlers/analysis-handlers.ts +105 -0
  82. package/src/handlers/context-handlers.ts +410 -0
  83. package/src/handlers/coverage-handlers.ts +56 -0
  84. package/src/handlers/dataflow-handlers.ts +193 -0
  85. package/src/handlers/documentation-handlers.ts +89 -0
  86. package/src/handlers/guarantee-handlers.ts +278 -0
  87. package/src/handlers/guard-handlers.ts +100 -0
  88. package/src/handlers/index.ts +14 -0
  89. package/src/handlers/issue-handlers.ts +81 -0
  90. package/src/handlers/project-handlers.ts +200 -0
  91. package/src/handlers/query-handlers.ts +232 -0
  92. package/src/prompts.ts +56 -0
  93. package/src/server.ts +87 -21
  94. package/src/types.ts +68 -1
  95. package/src/utils.ts +2 -2
  96. package/src/handlers.ts +0 -1180
@@ -0,0 +1,410 @@
1
+ /**
2
+ * MCP Context Handlers
3
+ */
4
+
5
+ import { ensureAnalyzed } from '../analysis.js';
6
+ import { getProjectPath } from '../state.js';
7
+ import { findCallsInFunction, findContainingFunction, FileOverview, buildNodeContext, getNodeDisplayName, formatEdgeMetadata, STRUCTURAL_EDGE_TYPES } from '@grafema/core';
8
+ import type { CallInfo, CallerInfo, NodeContext } from '@grafema/core';
9
+ import { existsSync, readFileSync, realpathSync } from 'fs';
10
+ import { isAbsolute, join, relative } from 'path';
11
+ import {
12
+ serializeBigInt,
13
+ textResult,
14
+ errorResult,
15
+ } from '../utils.js';
16
+ import type {
17
+ ToolResult,
18
+ GetFunctionDetailsArgs,
19
+ GetContextArgs,
20
+ GetFileOverviewArgs,
21
+ GraphNode,
22
+ } from '../types.js';
23
+
24
+ // === GET FUNCTION DETAILS (REG-254) ===
25
+
26
+ /**
27
+ * Get comprehensive function details including calls made and callers.
28
+ *
29
+ * Graph structure:
30
+ * ```
31
+ * FUNCTION -[HAS_SCOPE]-> SCOPE -[CONTAINS]-> CALL/METHOD_CALL
32
+ * SCOPE -[CONTAINS]-> SCOPE (nested blocks)
33
+ * CALL -[CALLS]-> FUNCTION (target)
34
+ * ```
35
+ *
36
+ * This is the core tool for understanding function behavior.
37
+ * Use transitive=true to follow call chains (A -> B -> C).
38
+ */
39
+ export async function handleGetFunctionDetails(
40
+ args: GetFunctionDetailsArgs
41
+ ): Promise<ToolResult> {
42
+ const db = await ensureAnalyzed();
43
+ const { name, file, transitive = false } = args;
44
+
45
+ // Step 1: Find the function
46
+ const candidates: GraphNode[] = [];
47
+ for await (const node of db.queryNodes({ type: 'FUNCTION' })) {
48
+ if (node.name !== name) continue;
49
+ if (file && !node.file?.includes(file)) continue;
50
+ candidates.push(node);
51
+ }
52
+
53
+ if (candidates.length === 0) {
54
+ return errorResult(
55
+ `Function "${name}" not found.` +
56
+ (file ? ` (searched in files matching "${file}")` : '')
57
+ );
58
+ }
59
+
60
+ if (candidates.length > 1 && !file) {
61
+ const locations = candidates.map(f => `${f.file}:${f.line}`).join(', ');
62
+ return errorResult(
63
+ `Multiple functions named "${name}" found: ${locations}. ` +
64
+ `Use the "file" parameter to disambiguate.`
65
+ );
66
+ }
67
+
68
+ const targetFunction = candidates[0];
69
+
70
+ // Step 2: Find calls using shared utility
71
+ const calls = await findCallsInFunction(db, targetFunction.id, {
72
+ transitive,
73
+ transitiveDepth: 5,
74
+ });
75
+
76
+ // Step 3: Find callers
77
+ const calledBy: CallerInfo[] = [];
78
+ const incomingCalls = await db.getIncomingEdges(targetFunction.id, ['CALLS']);
79
+ const seenCallers = new Set<string>();
80
+
81
+ for (const edge of incomingCalls) {
82
+ const caller = await findContainingFunction(db, edge.src);
83
+ if (caller && !seenCallers.has(caller.id)) {
84
+ seenCallers.add(caller.id);
85
+ calledBy.push(caller);
86
+ }
87
+ }
88
+
89
+ // Step 4: Build result
90
+ const result = {
91
+ id: targetFunction.id,
92
+ name: targetFunction.name,
93
+ file: targetFunction.file,
94
+ line: targetFunction.line as number | undefined,
95
+ async: targetFunction.async as boolean | undefined,
96
+ calls,
97
+ calledBy,
98
+ };
99
+
100
+ // Format output
101
+ const summary = [
102
+ `Function: ${result.name}`,
103
+ `File: ${result.file || 'unknown'}:${result.line || '?'}`,
104
+ `Async: ${result.async || false}`,
105
+ `Transitive: ${transitive}`,
106
+ '',
107
+ `Calls (${calls.length}):`,
108
+ ...formatCallsForDisplay(calls),
109
+ '',
110
+ `Called by (${calledBy.length}):`,
111
+ ...calledBy.map(c => ` - ${c.name} (${c.file}:${c.line})`),
112
+ ].join('\n');
113
+
114
+ return textResult(
115
+ summary + '\n\n' +
116
+ JSON.stringify(serializeBigInt(result), null, 2)
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Format calls for display, grouped by depth if transitive
122
+ */
123
+ function formatCallsForDisplay(calls: CallInfo[]): string[] {
124
+ const directCalls = calls.filter(c => (c.depth || 0) === 0);
125
+ const transitiveCalls = calls.filter(c => (c.depth || 0) > 0);
126
+
127
+ const lines: string[] = [];
128
+
129
+ // Direct calls
130
+ for (const c of directCalls) {
131
+ const target = c.resolved
132
+ ? ` -> ${c.target?.name} (${c.target?.file}:${c.target?.line})`
133
+ : ' (unresolved)';
134
+ const prefix = c.type === 'METHOD_CALL' ? `${c.object}.` : '';
135
+ lines.push(` - ${prefix}${c.name}()${target}`);
136
+ }
137
+
138
+ // Transitive calls (grouped by depth)
139
+ if (transitiveCalls.length > 0) {
140
+ lines.push('');
141
+ lines.push(' Transitive calls:');
142
+
143
+ const byDepth = new Map<number, CallInfo[]>();
144
+ for (const c of transitiveCalls) {
145
+ const depth = c.depth || 1;
146
+ if (!byDepth.has(depth)) byDepth.set(depth, []);
147
+ byDepth.get(depth)!.push(c);
148
+ }
149
+
150
+ for (const [depth, depthCalls] of Array.from(byDepth.entries()).sort((a, b) => a[0] - b[0])) {
151
+ for (const c of depthCalls) {
152
+ const indent = ' '.repeat(depth + 1);
153
+ const prefix = c.type === 'METHOD_CALL' ? `${c.object}.` : '';
154
+ const target = c.resolved ? ` -> ${c.target?.name}` : '';
155
+ lines.push(`${indent}[depth=${depth}] ${prefix}${c.name}()${target}`);
156
+ }
157
+ }
158
+ }
159
+
160
+ return lines;
161
+ }
162
+
163
+ // === NODE CONTEXT (REG-406) ===
164
+
165
+ export async function handleGetContext(
166
+ args: GetContextArgs
167
+ ): Promise<ToolResult> {
168
+ const db = await ensureAnalyzed();
169
+ const { semanticId, contextLines: ctxLines = 3, edgeType } = args;
170
+
171
+ // 1. Look up node
172
+ const node = await db.getNode(semanticId);
173
+ if (!node) {
174
+ return errorResult(
175
+ `Node not found: "${semanticId}"\n` +
176
+ `Use find_nodes or query_graph to find the correct semantic ID.`
177
+ );
178
+ }
179
+
180
+ const edgeTypeFilter = edgeType
181
+ ? new Set(edgeType.split(',').map(t => t.trim().toUpperCase()))
182
+ : null;
183
+
184
+ // 2. Build context using shared logic
185
+ const projectPath = getProjectPath();
186
+ const ctx: NodeContext = await buildNodeContext(db, node, {
187
+ contextLines: ctxLines,
188
+ edgeTypeFilter,
189
+ readFileContent: (filePath: string) => {
190
+ const absPath = isAbsolute(filePath) ? filePath : join(projectPath, filePath);
191
+ if (!existsSync(absPath)) return null;
192
+ try { return readFileSync(absPath, 'utf-8'); } catch { return null; }
193
+ },
194
+ });
195
+
196
+ // 3. Format text output
197
+ const relFile = node.file ? (isAbsolute(node.file) ? relative(projectPath, node.file) : node.file) : undefined;
198
+ const lines: string[] = [];
199
+
200
+ lines.push(`[${node.type}] ${getNodeDisplayName(node)}`);
201
+ lines.push(` ID: ${node.id}`);
202
+ if (relFile) {
203
+ lines.push(` Location: ${relFile}${node.line ? `:${node.line}` : ''}`);
204
+ }
205
+
206
+ // Source
207
+ if (ctx.source) {
208
+ lines.push('');
209
+ lines.push(` Source (lines ${ctx.source.startLine}-${ctx.source.endLine}):`);
210
+ const maxLineNum = ctx.source.endLine;
211
+ const lineNumWidth = String(maxLineNum).length;
212
+ for (let i = 0; i < ctx.source.lines.length; i++) {
213
+ const lineNum = ctx.source.startLine + i;
214
+ const paddedNum = String(lineNum).padStart(lineNumWidth, ' ');
215
+ const prefix = lineNum === (node.line as number) ? '>' : ' ';
216
+ const displayLine = ctx.source.lines[i].length > 120
217
+ ? ctx.source.lines[i].slice(0, 117) + '...'
218
+ : ctx.source.lines[i];
219
+ lines.push(` ${prefix}${paddedNum} | ${displayLine}`);
220
+ }
221
+ }
222
+
223
+ const formatEdgeSection = (groups: NodeContext['outgoing'], dir: '->' | '<-') => {
224
+ for (const group of groups) {
225
+ const isStructural = STRUCTURAL_EDGE_TYPES.has(group.edgeType);
226
+ lines.push(` ${group.edgeType} (${group.edges.length}):`);
227
+ for (const { edge, node: connNode } of group.edges) {
228
+ if (!connNode) {
229
+ const danglingId = dir === '->' ? edge.dst : edge.src;
230
+ lines.push(` ${dir} [dangling] ${danglingId}`);
231
+ continue;
232
+ }
233
+ const nFile = connNode.file ? (isAbsolute(connNode.file) ? relative(projectPath, connNode.file) : connNode.file) : '';
234
+ const nLoc = nFile ? (connNode.line ? `${nFile}:${connNode.line}` : nFile) : '';
235
+ const locStr = nLoc ? ` (${nLoc})` : '';
236
+ const metaStr = formatEdgeMetadata(edge);
237
+ lines.push(` ${dir} [${connNode.type}] ${getNodeDisplayName(connNode)}${locStr}${metaStr}`);
238
+
239
+ // Code context for non-structural edges
240
+ if (!isStructural && connNode.file && connNode.line && ctxLines > 0) {
241
+ const absoluteConnFile = !isAbsolute(connNode.file) ? join(projectPath, connNode.file) : connNode.file;
242
+ if (existsSync(absoluteConnFile)) {
243
+ try {
244
+ const content = readFileSync(absoluteConnFile, 'utf-8');
245
+ const allFileLines = content.split('\n');
246
+ const nLine = connNode.line as number;
247
+ const sLine = Math.max(1, nLine - Math.min(ctxLines, 2));
248
+ const eLine = Math.min(allFileLines.length, nLine + Math.min(ctxLines, 2));
249
+ const w = String(eLine).length;
250
+ for (let i = sLine; i <= eLine; i++) {
251
+ const p = i === nLine ? '>' : ' ';
252
+ const ln = String(i).padStart(w, ' ');
253
+ const displayLn = allFileLines[i - 1].length > 120
254
+ ? allFileLines[i - 1].slice(0, 117) + '...'
255
+ : allFileLines[i - 1];
256
+ lines.push(` ${p}${ln} | ${displayLn}`);
257
+ }
258
+ } catch { /* ignore */ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ };
264
+
265
+ if (ctx.outgoing.length > 0) {
266
+ lines.push('');
267
+ lines.push(' Outgoing edges:');
268
+ formatEdgeSection(ctx.outgoing, '->');
269
+ }
270
+
271
+ if (ctx.incoming.length > 0) {
272
+ lines.push('');
273
+ lines.push(' Incoming edges:');
274
+ formatEdgeSection(ctx.incoming, '<-');
275
+ }
276
+
277
+ if (ctx.outgoing.length === 0 && ctx.incoming.length === 0) {
278
+ lines.push('');
279
+ lines.push(' No edges found.');
280
+ }
281
+
282
+ // Build JSON result alongside text
283
+ const jsonResult = {
284
+ node: { id: node.id, type: node.type, name: node.name, file: relFile, line: node.line },
285
+ source: ctx.source ? {
286
+ startLine: ctx.source.startLine,
287
+ endLine: ctx.source.endLine,
288
+ lines: ctx.source.lines,
289
+ } : null,
290
+ outgoing: Object.fromEntries(ctx.outgoing.map(g => [g.edgeType, g.edges])),
291
+ incoming: Object.fromEntries(ctx.incoming.map(g => [g.edgeType, g.edges])),
292
+ };
293
+
294
+ return textResult(
295
+ lines.join('\n') + '\n\n' + JSON.stringify(serializeBigInt(jsonResult), null, 2)
296
+ );
297
+ }
298
+
299
+ // === FILE OVERVIEW (REG-412) ===
300
+
301
+ export async function handleGetFileOverview(
302
+ args: GetFileOverviewArgs
303
+ ): Promise<ToolResult> {
304
+ const db = await ensureAnalyzed();
305
+ const projectPath = getProjectPath();
306
+ const { file, include_edges: includeEdges = true } = args;
307
+
308
+ let filePath = file;
309
+
310
+ if (!filePath.startsWith('/')) {
311
+ filePath = join(projectPath, filePath);
312
+ }
313
+
314
+ if (!existsSync(filePath)) {
315
+ return errorResult(
316
+ `File not found: ${file}\n` +
317
+ `Resolved to: ${filePath}\n` +
318
+ `Project root: ${projectPath}`
319
+ );
320
+ }
321
+
322
+ const absolutePath = realpathSync(filePath);
323
+ const relativePath = relative(projectPath, absolutePath);
324
+
325
+ try {
326
+ const overview = new FileOverview(db);
327
+ const result = await overview.getOverview(absolutePath, {
328
+ includeEdges,
329
+ });
330
+
331
+ result.file = relativePath;
332
+
333
+ if (result.status === 'NOT_ANALYZED') {
334
+ return textResult(
335
+ `File not analyzed: ${relativePath}\n` +
336
+ `Run analyze_project to build the graph.`
337
+ );
338
+ }
339
+
340
+ const lines: string[] = [];
341
+
342
+ lines.push(`Module: ${result.file}`);
343
+
344
+ if (result.imports.length > 0) {
345
+ const sources = result.imports.map(i => i.source);
346
+ lines.push(`Imports: ${sources.join(', ')}`);
347
+ }
348
+
349
+ if (result.exports.length > 0) {
350
+ const names = result.exports.map(e =>
351
+ e.isDefault ? `${e.name} (default)` : e.name
352
+ );
353
+ lines.push(`Exports: ${names.join(', ')}`);
354
+ }
355
+
356
+ if (result.classes.length > 0) {
357
+ lines.push('');
358
+ lines.push('Classes:');
359
+ for (const cls of result.classes) {
360
+ const ext = cls.extends ? ` extends ${cls.extends}` : '';
361
+ lines.push(` ${cls.name}${ext} (line ${cls.line ?? '?'})`);
362
+ for (const m of cls.methods) {
363
+ const calls = m.calls.length > 0
364
+ ? ` -> ${m.calls.join(', ')}`
365
+ : '';
366
+ const params = m.params
367
+ ? `(${m.params.join(', ')})`
368
+ : '()';
369
+ lines.push(` ${m.name}${params}${calls}`);
370
+ }
371
+ }
372
+ }
373
+
374
+ if (result.functions.length > 0) {
375
+ lines.push('');
376
+ lines.push('Functions:');
377
+ for (const fn of result.functions) {
378
+ const calls = fn.calls.length > 0
379
+ ? ` -> ${fn.calls.join(', ')}`
380
+ : '';
381
+ const params = fn.params
382
+ ? `(${fn.params.join(', ')})`
383
+ : '()';
384
+ const asyncStr = fn.async ? 'async ' : '';
385
+ lines.push(
386
+ ` ${asyncStr}${fn.name}${params}${calls} (line ${fn.line ?? '?'})`
387
+ );
388
+ }
389
+ }
390
+
391
+ if (result.variables.length > 0) {
392
+ lines.push('');
393
+ lines.push('Variables:');
394
+ for (const v of result.variables) {
395
+ const assign = v.assignedFrom ? ` = ${v.assignedFrom}` : '';
396
+ lines.push(
397
+ ` ${v.kind} ${v.name}${assign} (line ${v.line ?? '?'})`
398
+ );
399
+ }
400
+ }
401
+
402
+ return textResult(
403
+ lines.join('\n') + '\n\n' +
404
+ JSON.stringify(serializeBigInt(result), null, 2)
405
+ );
406
+ } catch (error) {
407
+ const message = error instanceof Error ? error.message : String(error);
408
+ return errorResult(`Failed to get file overview: ${message}`);
409
+ }
410
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * MCP Coverage Handlers
3
+ */
4
+
5
+ import { getOrCreateBackend, getProjectPath } from '../state.js';
6
+ import { CoverageAnalyzer } from '@grafema/core';
7
+ import {
8
+ textResult,
9
+ errorResult,
10
+ } from '../utils.js';
11
+ import type {
12
+ ToolResult,
13
+ GetCoverageArgs,
14
+ } from '../types.js';
15
+
16
+ // === COVERAGE ===
17
+
18
+ export async function handleGetCoverage(args: GetCoverageArgs): Promise<ToolResult> {
19
+ const db = await getOrCreateBackend();
20
+ const projectPath = getProjectPath();
21
+ const { path: targetPath = projectPath } = args;
22
+
23
+ try {
24
+ const analyzer = new CoverageAnalyzer(db, targetPath);
25
+ const result = await analyzer.analyze();
26
+
27
+ // Format output for AI agents
28
+ let output = `Analysis Coverage for ${targetPath}\n`;
29
+ output += `==============================\n\n`;
30
+
31
+ output += `File breakdown:\n`;
32
+ output += ` Total files: ${result.total}\n`;
33
+ output += ` Analyzed: ${result.analyzed.count} (${result.percentages.analyzed}%) - in graph\n`;
34
+ output += ` Unsupported: ${result.unsupported.count} (${result.percentages.unsupported}%) - no indexer available\n`;
35
+ output += ` Unreachable: ${result.unreachable.count} (${result.percentages.unreachable}%) - not imported from entrypoints\n`;
36
+
37
+ if (result.unsupported.count > 0) {
38
+ output += `\nUnsupported files by extension:\n`;
39
+ for (const [ext, files] of Object.entries(result.unsupported.byExtension)) {
40
+ output += ` ${ext}: ${files.length} files\n`;
41
+ }
42
+ }
43
+
44
+ if (result.unreachable.count > 0) {
45
+ output += `\nUnreachable source files:\n`;
46
+ for (const [ext, files] of Object.entries(result.unreachable.byExtension)) {
47
+ output += ` ${ext}: ${files.length} files\n`;
48
+ }
49
+ }
50
+
51
+ return textResult(output);
52
+ } catch (error) {
53
+ const message = error instanceof Error ? error.message : String(error);
54
+ return errorResult(`Failed to calculate coverage: ${message}`);
55
+ }
56
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * MCP Dataflow Handlers
3
+ */
4
+
5
+ import { ensureAnalyzed } from '../analysis.js';
6
+ import { getProjectPath } from '../state.js';
7
+ import {
8
+ serializeBigInt,
9
+ textResult,
10
+ errorResult,
11
+ } from '../utils.js';
12
+ import type {
13
+ ToolResult,
14
+ TraceAliasArgs,
15
+ TraceDataFlowArgs,
16
+ CheckInvariantArgs,
17
+ GraphNode,
18
+ } from '../types.js';
19
+
20
+ // === TRACE HANDLERS ===
21
+
22
+ export async function handleTraceAlias(args: TraceAliasArgs): Promise<ToolResult> {
23
+ const db = await ensureAnalyzed();
24
+ const { variableName, file } = args;
25
+ const _projectPath = getProjectPath();
26
+
27
+ let varNode: GraphNode | null = null;
28
+
29
+ for await (const node of db.queryNodes({ type: 'VARIABLE' })) {
30
+ if (node.name === variableName && node.file?.includes(file || '')) {
31
+ varNode = node;
32
+ break;
33
+ }
34
+ }
35
+
36
+ if (!varNode) {
37
+ for await (const node of db.queryNodes({ type: 'CONSTANT' })) {
38
+ if (node.name === variableName && node.file?.includes(file || '')) {
39
+ varNode = node;
40
+ break;
41
+ }
42
+ }
43
+ }
44
+
45
+ if (!varNode) {
46
+ return errorResult(`Variable "${variableName}" not found in ${file || 'project'}`);
47
+ }
48
+
49
+ const chain: unknown[] = [];
50
+ const visited = new Set<string>();
51
+ let current: GraphNode | null = varNode;
52
+ const MAX_DEPTH = 20;
53
+
54
+ while (current && chain.length < MAX_DEPTH) {
55
+ if (visited.has(current.id)) {
56
+ chain.push({ type: 'CYCLE_DETECTED', id: current.id });
57
+ break;
58
+ }
59
+ visited.add(current.id);
60
+
61
+ chain.push({
62
+ type: current.type,
63
+ name: current.name,
64
+ file: current.file,
65
+ line: current.line,
66
+ });
67
+
68
+ const edges = await db.getOutgoingEdges(current.id, ['ASSIGNED_FROM']);
69
+ if (edges.length === 0) break;
70
+
71
+ current = await db.getNode(edges[0].dst);
72
+ }
73
+
74
+ return textResult(
75
+ `Alias chain for "${variableName}" (${chain.length} steps):\n\n${JSON.stringify(
76
+ serializeBigInt(chain),
77
+ null,
78
+ 2
79
+ )}`
80
+ );
81
+ }
82
+
83
+ export async function handleTraceDataFlow(args: TraceDataFlowArgs): Promise<ToolResult> {
84
+ const db = await ensureAnalyzed();
85
+ const { source, direction = 'forward', max_depth = 10 } = args;
86
+
87
+ // Find source node
88
+ let sourceNode: GraphNode | null = null;
89
+
90
+ // Try to find by ID first
91
+ sourceNode = await db.getNode(source);
92
+
93
+ // If not found, search by name
94
+ if (!sourceNode) {
95
+ for await (const node of db.queryNodes({ name: source })) {
96
+ sourceNode = node;
97
+ break;
98
+ }
99
+ }
100
+
101
+ if (!sourceNode) {
102
+ return errorResult(`Source "${source}" not found`);
103
+ }
104
+
105
+ const visited = new Set<string>();
106
+ const paths: unknown[] = [];
107
+
108
+ async function trace(nodeId: string, depth: number, path: string[]): Promise<void> {
109
+ if (depth > max_depth || visited.has(nodeId)) return;
110
+ visited.add(nodeId);
111
+
112
+ const newPath = [...path, nodeId];
113
+
114
+ if (direction === 'forward' || direction === 'both') {
115
+ const outEdges = await db.getOutgoingEdges(nodeId, [
116
+ 'ASSIGNED_FROM',
117
+ 'DERIVES_FROM',
118
+ 'PASSES_ARGUMENT',
119
+ ]);
120
+ for (const edge of outEdges) {
121
+ await trace(edge.dst, depth + 1, newPath);
122
+ }
123
+ }
124
+
125
+ if (direction === 'backward' || direction === 'both') {
126
+ const inEdges = await db.getIncomingEdges(nodeId, [
127
+ 'ASSIGNED_FROM',
128
+ 'DERIVES_FROM',
129
+ 'PASSES_ARGUMENT',
130
+ ]);
131
+ for (const edge of inEdges) {
132
+ await trace(edge.src, depth + 1, newPath);
133
+ }
134
+ }
135
+
136
+ if (depth > 0) {
137
+ paths.push(newPath);
138
+ }
139
+ }
140
+
141
+ await trace(sourceNode.id, 0, []);
142
+
143
+ return textResult(
144
+ `Data flow from "${source}" (${paths.length} paths):\n\n${JSON.stringify(paths, null, 2)}`
145
+ );
146
+ }
147
+
148
+ export async function handleCheckInvariant(args: CheckInvariantArgs): Promise<ToolResult> {
149
+ const db = await ensureAnalyzed();
150
+ const { rule, name: description } = args;
151
+
152
+ if (!('checkGuarantee' in db)) {
153
+ return errorResult('Backend does not support Datalog queries');
154
+ }
155
+
156
+ try {
157
+ const checkFn = (db as unknown as { checkGuarantee: (q: string) => Promise<Array<{ bindings: Array<{ name: string; value: string }> }>> }).checkGuarantee;
158
+ const violations = await checkFn(rule);
159
+ const total = violations.length;
160
+
161
+ if (total === 0) {
162
+ return textResult(`✅ Invariant holds: ${description || 'No violations found'}`);
163
+ }
164
+
165
+ const enrichedViolations: unknown[] = [];
166
+ for (const v of violations.slice(0, 20)) {
167
+ const nodeId = v.bindings?.find((b: any) => b.name === 'X')?.value;
168
+ if (nodeId) {
169
+ const node = await db.getNode(nodeId);
170
+ if (node) {
171
+ enrichedViolations.push({
172
+ id: nodeId,
173
+ type: node.type,
174
+ name: node.name,
175
+ file: node.file,
176
+ line: node.line,
177
+ });
178
+ }
179
+ }
180
+ }
181
+
182
+ return textResult(
183
+ `❌ ${total} violation(s) found:\n\n${JSON.stringify(
184
+ serializeBigInt(enrichedViolations),
185
+ null,
186
+ 2
187
+ )}${total > 20 ? `\n\n... and ${total - 20} more` : ''}`
188
+ );
189
+ } catch (error) {
190
+ const message = error instanceof Error ? error.message : String(error);
191
+ return errorResult(message);
192
+ }
193
+ }