@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
@@ -113,7 +113,7 @@ Use this when you need to:
113
113
 
114
114
  Returns semantic IDs that you can pass to get_context, get_node, get_neighbors, or find_guards.
115
115
 
116
- Supports partial matches on name and file. Use limit/offset for pagination.`,
116
+ Supports partial matches on name and file. When a name filter returns no exact matches, automatically falls back to fuzzy name matching using token similarity (CamelCase/snake_case aware). Use limit/offset for pagination.`,
117
117
  inputSchema: {
118
118
  type: 'object',
119
119
  properties: {
@@ -214,6 +214,107 @@ Tip: Start with max_depth=5, increase if needed.`,
214
214
  required: ['source'],
215
215
  },
216
216
  },
217
+ {
218
+ name: 'trace_calls',
219
+ description: `Trace call chains from or to a function/method, following CALLS and CALLS_REMOTE edges transitively.
220
+
221
+ Use this when you need to:
222
+ - "What does this function eventually call?" (forward) — full call tree including cross-language hops
223
+ - "Who calls this function?" (backward) — all callers up the stack
224
+ - "Show the full call chain from handler to database" (forward with depth)
225
+
226
+ Unlike trace_dataflow (which follows data assignments), this follows function CALLS edges:
227
+ - CALLS: same-language function/method invocation
228
+ - CALLS_REMOTE: cross-process/language boundary (IPC, HTTP, socket)
229
+
230
+ Returns: Indented call tree showing each hop with file:line location.`,
231
+ inputSchema: {
232
+ type: 'object',
233
+ properties: {
234
+ source: {
235
+ type: 'string',
236
+ description: 'Function/method name or semantic ID to trace from',
237
+ },
238
+ file: {
239
+ type: 'string',
240
+ description: 'File path to disambiguate (optional)',
241
+ },
242
+ direction: {
243
+ type: 'string',
244
+ description: 'forward (callees), backward (callers), or both (default: forward)',
245
+ enum: ['forward', 'backward', 'both'],
246
+ },
247
+ max_depth: {
248
+ type: 'number',
249
+ description: 'Maximum chain depth (default: 10)',
250
+ },
251
+ },
252
+ required: ['source'],
253
+ },
254
+ },
255
+ {
256
+ name: 'get_shape',
257
+ description: `Get the shape (methods + properties) of a CLASS, INTERFACE, or typed variable.
258
+
259
+ Shows all members including inherited ones via EXTENDS chain. For variables,
260
+ follows INSTANCE_OF to find the type, then returns its shape.
261
+
262
+ Use this to understand:
263
+ - "What methods does GraphBackend have?" → get_shape(target="GraphBackend")
264
+ - "What can I call on this variable?" → get_shape(target="db", file="handlers.ts")
265
+ - "What does this interface require?" → get_shape(target="NodeRecord")
266
+
267
+ Returns: members (methods + properties), extends chain, implements list.`,
268
+ inputSchema: {
269
+ type: 'object',
270
+ properties: {
271
+ target: {
272
+ type: 'string',
273
+ description: 'CLASS, INTERFACE, or variable name (or semantic ID)',
274
+ },
275
+ file: {
276
+ type: 'string',
277
+ description: 'File path to disambiguate (optional)',
278
+ },
279
+ },
280
+ required: ['target'],
281
+ },
282
+ },
283
+ {
284
+ name: 'explain',
285
+ description: `Explain a code element using graph data — returns structured context + prompt for the LLM to summarize.
286
+
287
+ Unlike other tools that return raw data, this tool returns graph query results
288
+ PLUS a natural-language prompt asking the calling LLM to explain the results
289
+ to the user. The LLM uses its own reasoning to produce a human-readable summary.
290
+
291
+ No extra API calls needed — the calling model (Claude, GPT, etc.) does the summarization.
292
+
293
+ Use cases:
294
+ - "Explain where this value comes from" → dataflow trace + summarization prompt
295
+ - "What does this function do?" → structure + calls + prompt to describe
296
+ - "How is this variable used?" → forward trace + prompt to explain usage patterns
297
+
298
+ The question parameter guides what graph data to fetch and how to frame the summary.`,
299
+ inputSchema: {
300
+ type: 'object',
301
+ properties: {
302
+ target: {
303
+ type: 'string',
304
+ description: 'Variable, function, or node name to explain',
305
+ },
306
+ file: {
307
+ type: 'string',
308
+ description: 'File path to narrow scope',
309
+ },
310
+ question: {
311
+ type: 'string',
312
+ description: 'What to explain: "where does this value come from?", "what does this function do?", "how is this used?" (default: general explanation)',
313
+ },
314
+ },
315
+ required: ['target'],
316
+ },
317
+ },
217
318
  {
218
319
  name: 'check_invariant',
219
320
  description: `Check a one-off code invariant using a Datalog rule. Returns violations if broken.
@@ -99,13 +99,19 @@ export async function handleGetStats(): Promise<ToolResult> {
99
99
  }
100
100
  }
101
101
 
102
+ const actionHint =
103
+ nodeCount > 0
104
+ ? `\n\nGraph is ready — use find_nodes, find_calls, trace_dataflow to query it.`
105
+ : `\n\nGraph is empty — call analyze_project to build the graph first.`;
106
+
102
107
  return textResult(
103
108
  `Graph Statistics:\n\n` +
104
109
  `Total nodes: ${nodeCount.toLocaleString()}\n` +
105
110
  `Total edges: ${edgeCount.toLocaleString()}\n\n` +
106
111
  `Nodes by type:\n${JSON.stringify(nodesByType, null, 2)}\n\n` +
107
112
  `Edges by type:\n${JSON.stringify(edgesByType, null, 2)}` +
108
- shardSection
113
+ shardSection +
114
+ actionHint
109
115
  );
110
116
  }
111
117
 
@@ -4,7 +4,8 @@
4
4
 
5
5
  import { ensureAnalyzed } from '../analysis.js';
6
6
  import { getProjectPath } from '../state.js';
7
- import { findCallsInFunction, findContainingFunction, FileOverview, buildNodeContext, getNodeDisplayName, formatEdgeMetadata, STRUCTURAL_EDGE_TYPES, isGrafemaUri, toCompactSemanticId } from '@grafema/util';
7
+ import { findCallsInFunction, findContainingFunction, FileOverview, buildNodeContext, getNodeDisplayName, formatEdgeMetadata, STRUCTURAL_EDGE_TYPES, isGrafemaUri, toCompactSemanticId, getShape } from '@grafema/util';
8
+ import type { ClassIndex } from '@grafema/util';
8
9
  import type { CallInfo, CallerInfo, NodeContext } from '@grafema/util';
9
10
  import { existsSync, readFileSync, realpathSync } from 'fs';
10
11
  import { isAbsolute, join, relative } from 'path';
@@ -433,3 +434,81 @@ export async function handleGetFileOverview(
433
434
  return errorResult(`Failed to get file overview: ${message}`);
434
435
  }
435
436
  }
437
+
438
+ // === GET SHAPE ===
439
+
440
+ export async function handleGetShape(args: { target: string; file?: string }): Promise<ToolResult> {
441
+ const db = await ensureAnalyzed();
442
+ const { target, file } = args;
443
+
444
+ // Find the target node
445
+ let targetNode: GraphNode | null = await db.getNode(target);
446
+ if (!targetNode) {
447
+ // Search by name, preferring CLASS/INTERFACE
448
+ for (const type of ['CLASS', 'INTERFACE', 'VARIABLE', 'CONSTANT', 'PARAMETER']) {
449
+ for await (const node of db.queryNodes({ type, name: target })) {
450
+ if (file && !node.file?.includes(file)) continue;
451
+ targetNode = node;
452
+ break;
453
+ }
454
+ if (targetNode) break;
455
+ }
456
+ }
457
+ if (!targetNode) {
458
+ return errorResult(`Target "${target}" not found. Provide a CLASS, INTERFACE, or typed variable name.`);
459
+ }
460
+
461
+ // Build class index for EXTENDS chain walking
462
+ const classIndex: ClassIndex = new Map();
463
+ for await (const node of db.queryNodes({ type: 'CLASS' })) {
464
+ if (node.name) classIndex.set(node.name, String(node.id));
465
+ }
466
+ for await (const node of db.queryNodes({ type: 'INTERFACE' })) {
467
+ if (node.name) classIndex.set(node.name, String(node.id));
468
+ }
469
+
470
+ const backend = {
471
+ getNode: (id: string) => db.getNode(id),
472
+ getOutgoingEdges: (id: string) => db.getOutgoingEdges(id),
473
+ getIncomingEdges: (id: string) => db.getIncomingEdges(id),
474
+ };
475
+
476
+ const shape = await getShape(backend as never, String(targetNode.id), classIndex);
477
+ if (!shape) {
478
+ return errorResult(`Could not determine shape for "${target}". It may not be a CLASS, INTERFACE, or typed variable.`);
479
+ }
480
+
481
+ // Format output
482
+ const lines: string[] = [];
483
+ lines.push(`## Shape: ${shape.name} (${shape.nodeType})`);
484
+ if (shape.file) lines.push(`File: ${shape.file}`);
485
+ if (shape.extends.length > 0) lines.push(`Extends: ${shape.extends.join(' → ')}`);
486
+ if (shape.implements.length > 0) lines.push(`Implements: ${shape.implements.join(', ')}`);
487
+ lines.push(`Confidence: ${shape.confidence}`);
488
+ lines.push('');
489
+
490
+ // Group members by source
491
+ const bySource = new Map<string, typeof shape.members>();
492
+ for (const m of shape.members) {
493
+ if (!bySource.has(m.from)) bySource.set(m.from, []);
494
+ bySource.get(m.from)!.push(m);
495
+ }
496
+
497
+ for (const [source, members] of bySource) {
498
+ const label = source === shape.name ? 'Own members' : `Inherited from ${source}`;
499
+ lines.push(`### ${label} (${members.length})`);
500
+ const methods = members.filter(m => m.kind === 'method' || m.kind === 'method_signature');
501
+ const props = members.filter(m => m.kind === 'property' || m.kind === 'property_signature');
502
+ if (methods.length > 0) {
503
+ lines.push(`Methods: ${methods.map(m => m.name).join(', ')}`);
504
+ }
505
+ if (props.length > 0) {
506
+ lines.push(`Properties: ${props.map(m => m.name).join(', ')}`);
507
+ }
508
+ lines.push('');
509
+ }
510
+
511
+ lines.push(`Total: ${shape.members.length} members`);
512
+
513
+ return textResult(lines.join('\n'));
514
+ }
@@ -16,12 +16,14 @@ import type {
16
16
  ToolResult,
17
17
  TraceAliasArgs,
18
18
  TraceDataFlowArgs,
19
+ TraceCallChainArgs,
19
20
  CheckInvariantArgs,
20
21
  GraphNode,
21
22
  } from '../types.js';
22
23
  import {
23
24
  traceDataflow,
24
25
  renderTraceNarrative,
26
+ traceCallChain,
25
27
  type DataflowBackend,
26
28
  type TraceDetail,
27
29
  } from '@grafema/util';
@@ -155,6 +157,168 @@ export async function handleTraceDataFlow(args: TraceDataFlowArgs): Promise<Tool
155
157
  }));
156
158
  }
157
159
 
160
+ // === TRACE CALL CHAIN ===
161
+
162
+ export async function handleTraceCallChain(args: TraceCallChainArgs): Promise<ToolResult> {
163
+ const db = await ensureAnalyzed();
164
+ const { source, file, direction = 'forward', max_depth = 10 } = args;
165
+
166
+ // Find source node (same resolution logic as trace_dataflow)
167
+ let sourceNode: GraphNode | null = await db.getNode(source);
168
+ if (!sourceNode) {
169
+ let fallbackNode: GraphNode | null = null;
170
+ for (const type of ['FUNCTION', 'METHOD']) {
171
+ for await (const node of db.queryNodes({ type, name: source })) {
172
+ if (file && !node.file?.includes(file)) {
173
+ if (!fallbackNode) fallbackNode = node;
174
+ continue;
175
+ }
176
+ sourceNode = node;
177
+ break;
178
+ }
179
+ if (sourceNode) break;
180
+ }
181
+ if (!sourceNode && fallbackNode) {
182
+ sourceNode = fallbackNode;
183
+ }
184
+ }
185
+ if (!sourceNode) {
186
+ const displaySource = isGrafemaUri(source) ? toCompactSemanticId(source) : source;
187
+ return errorResult(`Source "${displaySource}" not found. Provide a FUNCTION or METHOD name.`);
188
+ }
189
+
190
+ const results = await traceCallChain(db as never, sourceNode.id, {
191
+ direction: direction as 'forward' | 'backward' | 'both',
192
+ maxDepth: max_depth,
193
+ limit: 100,
194
+ });
195
+
196
+ // Render as indented call tree
197
+ const lines: string[] = [];
198
+ const sourceName = sourceNode.name || source;
199
+ const sourceFile = sourceNode.file ? sourceNode.file.split('/').pop() : '';
200
+
201
+ for (const result of results) {
202
+ lines.push(`## ${result.direction === 'forward' ? 'Outgoing calls from' : 'Incoming callers of'} ${sourceName} (${sourceFile}:${sourceNode.line ?? '?'})`);
203
+ lines.push('');
204
+
205
+ if (result.chain.length === 0) {
206
+ lines.push(' (no calls found)');
207
+ continue;
208
+ }
209
+
210
+ for (const hop of result.chain) {
211
+ const indent = ' '.repeat(hop.depth + 1);
212
+ const prefix = hop.remote ? '> calls_remote' : '> calls';
213
+ const shortFile = hop.file ? hop.file.split('/').pop() : '?';
214
+ const loc = hop.line ? `${shortFile}:${hop.line}` : shortFile;
215
+ const unresolvedTag = hop.resolved ? '' : ' [unresolved]';
216
+ lines.push(`${indent}${prefix} ${hop.name} (${loc})${unresolvedTag}`);
217
+ }
218
+
219
+ lines.push('');
220
+ lines.push(`${result.totalFound} hop(s) found`);
221
+ }
222
+
223
+ lines.push('');
224
+ lines.push('Legend: > calls = local call, > calls_remote = cross-process/language boundary');
225
+
226
+ return textResult(lines.join('\n'));
227
+ }
228
+
229
+ // === EXPLAIN (graph data + LLM prompt injection) ===
230
+
231
+ export interface ExplainArgs {
232
+ target: string;
233
+ file?: string;
234
+ question?: string;
235
+ }
236
+
237
+ export async function handleExplain(args: ExplainArgs): Promise<ToolResult> {
238
+ const db = await ensureAnalyzed();
239
+ const { target, file, question } = args;
240
+
241
+ // 1. Find the target node
242
+ let targetNode: GraphNode | null = await db.getNode(target);
243
+ if (!targetNode) {
244
+ for await (const node of db.queryNodes({ name: target })) {
245
+ if (file && !node.file?.includes(file)) continue;
246
+ targetNode = node;
247
+ break;
248
+ }
249
+ }
250
+ if (!targetNode) {
251
+ // Try PARAMETER, CONSTANT
252
+ for (const type of ['PARAMETER', 'CONSTANT', 'IMPORT_BINDING']) {
253
+ for await (const node of db.queryNodes({ type, name: target })) {
254
+ if (file && !node.file?.includes(file)) continue;
255
+ targetNode = node;
256
+ break;
257
+ }
258
+ if (targetNode) break;
259
+ }
260
+ }
261
+ if (!targetNode) {
262
+ return errorResult(`Target "${target}" not found`);
263
+ }
264
+
265
+ // 2. Gather graph context based on question type
266
+ const dfDb = db as unknown as DataflowBackend;
267
+ const sections: string[] = [];
268
+
269
+ const nodeType = targetNode.nodeType || targetNode.type;
270
+ const nodeName = targetNode.name || target;
271
+ const nodeFile = targetNode.file || '';
272
+
273
+ sections.push(`## ${nodeType} "${nodeName}" in ${nodeFile}`);
274
+
275
+ // Always include dataflow (both directions)
276
+ const trace = await traceDataflow(dfDb, targetNode.id, {
277
+ direction: 'both',
278
+ maxDepth: 8,
279
+ limit: 30,
280
+ });
281
+ const traceText = renderTraceNarrative(trace, nodeName, { detail: 'normal' });
282
+ sections.push(`### Dataflow\n${traceText}`);
283
+
284
+ // Include structure context if it's a function
285
+ if (nodeType === 'FUNCTION') {
286
+ const allOutEdges = await db.getOutgoingEdges(targetNode.id);
287
+ const allInEdges = await db.getIncomingEdges(targetNode.id);
288
+ const outSummary: string[] = [];
289
+ const inSummary: string[] = [];
290
+ for (const edge of allOutEdges.slice(0, 15)) {
291
+ if (['CALLS', 'CONTAINS', 'HAS_SCOPE'].includes(edge.type)) {
292
+ const dst = await db.getNode(edge.dst);
293
+ if (dst) outSummary.push(`${edge.type} → ${dst.nodeType} "${dst.name}"`);
294
+ }
295
+ }
296
+ for (const edge of allInEdges.slice(0, 15)) {
297
+ if (['CALLS', 'CONTAINS'].includes(edge.type)) {
298
+ const src = await db.getNode(edge.src);
299
+ if (src) inSummary.push(`${edge.type} ← ${src.nodeType} "${src.name}"`);
300
+ }
301
+ }
302
+ if (outSummary.length) sections.push(`### Outgoing\n${outSummary.join('\n')}`);
303
+ if (inSummary.length) sections.push(`### Incoming\n${inSummary.join('\n')}`);
304
+ }
305
+
306
+ // 3. Prompt injection — instruct the calling LLM to summarize
307
+ const questionText = question || `what is "${nodeName}" and how is it used?`;
308
+ const prompt = `
309
+ ---
310
+ INSTRUCTION: Based on the graph data above, answer the user's question in 2-3 concise sentences.
311
+ Question: "${questionText}"
312
+ Focus on: data origins, transformations, and where the value ends up.
313
+ Use concrete names from the trace (function names, variable names, file paths).
314
+ Do NOT repeat the raw graph data — synthesize it into a human-readable explanation.
315
+ ---`;
316
+
317
+ sections.push(prompt);
318
+
319
+ return textResult(sections.join('\n\n'));
320
+ }
321
+
158
322
  // === CHECK INVARIANT (unchanged) ===
159
323
 
160
324
  export async function handleCheckInvariant(args: CheckInvariantArgs): Promise<ToolResult> {
@@ -3,10 +3,11 @@
3
3
  */
4
4
 
5
5
  export { handleQueryGraph, handleFindCalls, handleFindNodes } from './query-handlers.js';
6
- export { handleTraceAlias, handleTraceDataFlow, handleCheckInvariant } from './dataflow-handlers.js';
6
+ export { handleTraceAlias, handleTraceDataFlow, handleTraceCallChain, handleCheckInvariant, handleExplain } from './dataflow-handlers.js';
7
+ export type { ExplainArgs } from './dataflow-handlers.js';
7
8
  export { handleAnalyzeProject, handleGetAnalysisStatus, handleGetStats, handleGetSchema } from './analysis-handlers.js';
8
9
  export { handleCreateGuarantee, handleListGuarantees, handleCheckGuarantees, handleDeleteGuarantee } from './guarantee-handlers.js';
9
- export { handleGetFunctionDetails, handleGetContext, handleGetFileOverview } from './context-handlers.js';
10
+ export { handleGetFunctionDetails, handleGetContext, handleGetFileOverview, handleGetShape } from './context-handlers.js';
10
11
  export { handleReadProjectStructure, handleWriteConfig } from './project-handlers.js';
11
12
  export { handleGetCoverage } from './coverage-handlers.js';
12
13
  export { handleFindGuards } from './guard-handlers.js';