@grafema/mcp 0.2.5-beta → 0.2.7

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 (79) hide show
  1. package/README.md +49 -25
  2. package/dist/analysis-worker.js +8 -4
  3. package/dist/analysis-worker.js.map +1 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +15 -3
  6. package/dist/config.js.map +1 -1
  7. package/dist/definitions.d.ts.map +1 -1
  8. package/dist/definitions.js +69 -0
  9. package/dist/definitions.js.map +1 -1
  10. package/dist/handlers/analysis-handlers.d.ts +9 -0
  11. package/dist/handlers/analysis-handlers.d.ts.map +1 -0
  12. package/dist/handlers/analysis-handlers.js +73 -0
  13. package/dist/handlers/analysis-handlers.js.map +1 -0
  14. package/dist/handlers/context-handlers.d.ts +21 -0
  15. package/dist/handlers/context-handlers.d.ts.map +1 -0
  16. package/dist/handlers/context-handlers.js +330 -0
  17. package/dist/handlers/context-handlers.js.map +1 -0
  18. package/dist/handlers/coverage-handlers.d.ts +6 -0
  19. package/dist/handlers/coverage-handlers.d.ts.map +1 -0
  20. package/dist/handlers/coverage-handlers.js +42 -0
  21. package/dist/handlers/coverage-handlers.js.map +1 -0
  22. package/dist/handlers/dataflow-handlers.d.ts +8 -0
  23. package/dist/handlers/dataflow-handlers.d.ts.map +1 -0
  24. package/dist/handlers/dataflow-handlers.js +140 -0
  25. package/dist/handlers/dataflow-handlers.js.map +1 -0
  26. package/dist/handlers/documentation-handlers.d.ts +6 -0
  27. package/dist/handlers/documentation-handlers.d.ts.map +1 -0
  28. package/dist/handlers/documentation-handlers.js +79 -0
  29. package/dist/handlers/documentation-handlers.js.map +1 -0
  30. package/dist/handlers/guarantee-handlers.d.ts +21 -0
  31. package/dist/handlers/guarantee-handlers.d.ts.map +1 -0
  32. package/dist/handlers/guarantee-handlers.js +251 -0
  33. package/dist/handlers/guarantee-handlers.js.map +1 -0
  34. package/dist/handlers/guard-handlers.d.ts +14 -0
  35. package/dist/handlers/guard-handlers.d.ts.map +1 -0
  36. package/dist/handlers/guard-handlers.js +77 -0
  37. package/dist/handlers/guard-handlers.js.map +1 -0
  38. package/dist/handlers/index.d.ts +14 -0
  39. package/dist/handlers/index.d.ts.map +1 -0
  40. package/dist/handlers/index.js +14 -0
  41. package/dist/handlers/index.js.map +1 -0
  42. package/dist/handlers/issue-handlers.d.ts +6 -0
  43. package/dist/handlers/issue-handlers.d.ts.map +1 -0
  44. package/dist/handlers/issue-handlers.js +66 -0
  45. package/dist/handlers/issue-handlers.js.map +1 -0
  46. package/dist/handlers/project-handlers.d.ts +7 -0
  47. package/dist/handlers/project-handlers.d.ts.map +1 -0
  48. package/dist/handlers/project-handlers.js +153 -0
  49. package/dist/handlers/project-handlers.js.map +1 -0
  50. package/dist/handlers/query-handlers.d.ts +8 -0
  51. package/dist/handlers/query-handlers.d.ts.map +1 -0
  52. package/dist/handlers/query-handlers.js +171 -0
  53. package/dist/handlers/query-handlers.js.map +1 -0
  54. package/dist/handlers.d.ts +3 -1
  55. package/dist/handlers.d.ts.map +1 -1
  56. package/dist/handlers.js +199 -4
  57. package/dist/handlers.js.map +1 -1
  58. package/dist/server.js +7 -1
  59. package/dist/server.js.map +1 -1
  60. package/dist/types.d.ts +9 -0
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +3 -3
  63. package/src/analysis-worker.ts +10 -2
  64. package/src/config.ts +24 -0
  65. package/src/definitions.ts +70 -0
  66. package/src/handlers/analysis-handlers.ts +105 -0
  67. package/src/handlers/context-handlers.ts +410 -0
  68. package/src/handlers/coverage-handlers.ts +56 -0
  69. package/src/handlers/dataflow-handlers.ts +193 -0
  70. package/src/handlers/documentation-handlers.ts +89 -0
  71. package/src/handlers/guarantee-handlers.ts +278 -0
  72. package/src/handlers/guard-handlers.ts +100 -0
  73. package/src/handlers/index.ts +14 -0
  74. package/src/handlers/issue-handlers.ts +81 -0
  75. package/src/handlers/project-handlers.ts +200 -0
  76. package/src/handlers/query-handlers.ts +232 -0
  77. package/src/server.ts +13 -1
  78. package/src/types.ts +15 -0
  79. package/src/handlers.ts +0 -1373
@@ -0,0 +1,105 @@
1
+ /**
2
+ * MCP Analysis Handlers
3
+ */
4
+
5
+ import { ensureAnalyzed } from '../analysis.js';
6
+ import { getAnalysisStatus, getOrCreateBackend, isAnalysisRunning } from '../state.js';
7
+ import {
8
+ textResult,
9
+ errorResult,
10
+ } from '../utils.js';
11
+ import type {
12
+ ToolResult,
13
+ AnalyzeProjectArgs,
14
+ GetSchemaArgs,
15
+ } from '../types.js';
16
+
17
+ // === ANALYSIS HANDLERS ===
18
+
19
+ export async function handleAnalyzeProject(args: AnalyzeProjectArgs): Promise<ToolResult> {
20
+ const { service, force } = args;
21
+
22
+ // Early check: return error for force=true if analysis is already running
23
+ // This provides immediate feedback instead of waiting or causing corruption
24
+ if (force && isAnalysisRunning()) {
25
+ return errorResult(
26
+ 'Cannot force re-analysis: analysis is already in progress. ' +
27
+ 'Use get_analysis_status to check current status, or wait for completion.'
28
+ );
29
+ }
30
+
31
+ // Note: setIsAnalyzed(false) is now handled inside ensureAnalyzed() within the lock
32
+ // to prevent race conditions where multiple calls could both clear the database
33
+
34
+ try {
35
+ await ensureAnalyzed(service || null, force || false);
36
+ const status = getAnalysisStatus();
37
+
38
+ return textResult(
39
+ `Analysis complete!\n` +
40
+ `- Services discovered: ${status.servicesDiscovered}\n` +
41
+ `- Services analyzed: ${status.servicesAnalyzed}\n` +
42
+ `- Total time: ${status.timings.total || 'N/A'}s`
43
+ );
44
+ } catch (error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ return errorResult(message);
47
+ }
48
+ }
49
+
50
+ export async function handleGetAnalysisStatus(): Promise<ToolResult> {
51
+ const status = getAnalysisStatus();
52
+
53
+ return textResult(
54
+ `Analysis Status:\n` +
55
+ `- Running: ${status.running}\n` +
56
+ `- Phase: ${status.phase || 'N/A'}\n` +
57
+ `- Message: ${status.message || 'N/A'}\n` +
58
+ `- Services discovered: ${status.servicesDiscovered}\n` +
59
+ `- Services analyzed: ${status.servicesAnalyzed}\n` +
60
+ (status.error ? `- Error: ${status.error}\n` : '')
61
+ );
62
+ }
63
+
64
+ export async function handleGetStats(): Promise<ToolResult> {
65
+ const db = await getOrCreateBackend();
66
+
67
+ const nodeCount = await db.nodeCount();
68
+ const edgeCount = await db.edgeCount();
69
+ const nodesByType = await db.countNodesByType();
70
+ const edgesByType = await db.countEdgesByType();
71
+
72
+ return textResult(
73
+ `Graph Statistics:\n\n` +
74
+ `Total nodes: ${nodeCount.toLocaleString()}\n` +
75
+ `Total edges: ${edgeCount.toLocaleString()}\n\n` +
76
+ `Nodes by type:\n${JSON.stringify(nodesByType, null, 2)}\n\n` +
77
+ `Edges by type:\n${JSON.stringify(edgesByType, null, 2)}`
78
+ );
79
+ }
80
+
81
+ export async function handleGetSchema(args: GetSchemaArgs): Promise<ToolResult> {
82
+ const db = await getOrCreateBackend();
83
+ const { type = 'all' } = args;
84
+
85
+ const nodesByType = await db.countNodesByType();
86
+ const edgesByType = await db.countEdgesByType();
87
+
88
+ let output = '';
89
+
90
+ if (type === 'nodes' || type === 'all') {
91
+ output += `Node Types (${Object.keys(nodesByType).length}):\n`;
92
+ for (const [t, count] of Object.entries(nodesByType)) {
93
+ output += ` - ${t}: ${count}\n`;
94
+ }
95
+ }
96
+
97
+ if (type === 'edges' || type === 'all') {
98
+ output += `\nEdge Types (${Object.keys(edgesByType).length}):\n`;
99
+ for (const [t, count] of Object.entries(edgesByType)) {
100
+ output += ` - ${t}: ${count}\n`;
101
+ }
102
+ }
103
+
104
+ return textResult(output);
105
+ }
@@ -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
+ }