@grafema/mcp 0.2.12-beta → 0.3.0-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 (158) hide show
  1. package/dist/analysis-worker.d.ts +4 -3
  2. package/dist/analysis-worker.d.ts.map +1 -1
  3. package/dist/analysis-worker.js +8 -203
  4. package/dist/analysis-worker.js.map +1 -1
  5. package/dist/analysis.d.ts +10 -3
  6. package/dist/analysis.d.ts.map +1 -1
  7. package/dist/analysis.js +130 -62
  8. package/dist/analysis.js.map +1 -1
  9. package/dist/config.d.ts +5 -11
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +6 -128
  12. package/dist/config.js.map +1 -1
  13. package/dist/definitions/analysis-tools.d.ts +6 -0
  14. package/dist/definitions/analysis-tools.d.ts.map +1 -0
  15. package/dist/definitions/analysis-tools.js +125 -0
  16. package/dist/definitions/analysis-tools.js.map +1 -0
  17. package/dist/definitions/context-tools.d.ts +6 -0
  18. package/dist/definitions/context-tools.d.ts.map +1 -0
  19. package/dist/definitions/context-tools.js +144 -0
  20. package/dist/definitions/context-tools.js.map +1 -0
  21. package/dist/definitions/graph-tools.d.ts +7 -0
  22. package/dist/definitions/graph-tools.d.ts.map +1 -0
  23. package/dist/definitions/graph-tools.js +124 -0
  24. package/dist/definitions/graph-tools.js.map +1 -0
  25. package/dist/definitions/graphql-tools.d.ts +6 -0
  26. package/dist/definitions/graphql-tools.d.ts.map +1 -0
  27. package/dist/definitions/graphql-tools.js +62 -0
  28. package/dist/definitions/graphql-tools.js.map +1 -0
  29. package/dist/definitions/guarantee-tools.d.ts +6 -0
  30. package/dist/definitions/guarantee-tools.d.ts.map +1 -0
  31. package/dist/definitions/guarantee-tools.js +136 -0
  32. package/dist/definitions/guarantee-tools.js.map +1 -0
  33. package/dist/definitions/index.d.ts +7 -0
  34. package/dist/definitions/index.d.ts.map +1 -0
  35. package/dist/definitions/index.js +24 -0
  36. package/dist/definitions/index.js.map +1 -0
  37. package/dist/definitions/knowledge-tools.d.ts +10 -0
  38. package/dist/definitions/knowledge-tools.d.ts.map +1 -0
  39. package/dist/definitions/knowledge-tools.js +300 -0
  40. package/dist/definitions/knowledge-tools.js.map +1 -0
  41. package/dist/definitions/notation-tools.d.ts +9 -0
  42. package/dist/definitions/notation-tools.d.ts.map +1 -0
  43. package/dist/definitions/notation-tools.js +62 -0
  44. package/dist/definitions/notation-tools.js.map +1 -0
  45. package/dist/definitions/project-tools.d.ts +6 -0
  46. package/dist/definitions/project-tools.d.ts.map +1 -0
  47. package/dist/definitions/project-tools.js +181 -0
  48. package/dist/definitions/project-tools.js.map +1 -0
  49. package/dist/definitions/query-tools.d.ts +6 -0
  50. package/dist/definitions/query-tools.d.ts.map +1 -0
  51. package/dist/definitions/query-tools.js +245 -0
  52. package/dist/definitions/query-tools.js.map +1 -0
  53. package/dist/definitions/types.d.ts +21 -0
  54. package/dist/definitions/types.d.ts.map +1 -0
  55. package/dist/definitions/types.js +5 -0
  56. package/dist/definitions/types.js.map +1 -0
  57. package/dist/dev-proxy.d.ts +29 -0
  58. package/dist/dev-proxy.d.ts.map +1 -0
  59. package/dist/dev-proxy.js +267 -0
  60. package/dist/dev-proxy.js.map +1 -0
  61. package/dist/handlers/analysis-handlers.d.ts.map +1 -1
  62. package/dist/handlers/analysis-handlers.js +34 -4
  63. package/dist/handlers/analysis-handlers.js.map +1 -1
  64. package/dist/handlers/context-handlers.d.ts +5 -6
  65. package/dist/handlers/context-handlers.d.ts.map +1 -1
  66. package/dist/handlers/context-handlers.js +19 -16
  67. package/dist/handlers/context-handlers.js.map +1 -1
  68. package/dist/handlers/coverage-handlers.js +1 -1
  69. package/dist/handlers/dataflow-handlers.d.ts +2 -0
  70. package/dist/handlers/dataflow-handlers.d.ts.map +1 -1
  71. package/dist/handlers/dataflow-handlers.js +68 -46
  72. package/dist/handlers/dataflow-handlers.js.map +1 -1
  73. package/dist/handlers/documentation-handlers.d.ts.map +1 -1
  74. package/dist/handlers/documentation-handlers.js +56 -2
  75. package/dist/handlers/documentation-handlers.js.map +1 -1
  76. package/dist/handlers/graph-handlers.d.ts +23 -0
  77. package/dist/handlers/graph-handlers.d.ts.map +1 -0
  78. package/dist/handlers/graph-handlers.js +155 -0
  79. package/dist/handlers/graph-handlers.js.map +1 -0
  80. package/dist/handlers/graphql-handlers.d.ts +9 -0
  81. package/dist/handlers/graphql-handlers.d.ts.map +1 -0
  82. package/dist/handlers/graphql-handlers.js +57 -0
  83. package/dist/handlers/graphql-handlers.js.map +1 -0
  84. package/dist/handlers/guarantee-handlers.js +1 -1
  85. package/dist/handlers/guard-handlers.d.ts.map +1 -1
  86. package/dist/handlers/guard-handlers.js +6 -3
  87. package/dist/handlers/guard-handlers.js.map +1 -1
  88. package/dist/handlers/index.d.ts +4 -0
  89. package/dist/handlers/index.d.ts.map +1 -1
  90. package/dist/handlers/index.js +6 -0
  91. package/dist/handlers/index.js.map +1 -1
  92. package/dist/handlers/issue-handlers.d.ts.map +1 -1
  93. package/dist/handlers/issue-handlers.js +10 -15
  94. package/dist/handlers/issue-handlers.js.map +1 -1
  95. package/dist/handlers/knowledge-handlers.d.ts +25 -0
  96. package/dist/handlers/knowledge-handlers.d.ts.map +1 -0
  97. package/dist/handlers/knowledge-handlers.js +208 -0
  98. package/dist/handlers/knowledge-handlers.js.map +1 -0
  99. package/dist/handlers/notation-handlers.d.ts +6 -0
  100. package/dist/handlers/notation-handlers.d.ts.map +1 -0
  101. package/dist/handlers/notation-handlers.js +53 -0
  102. package/dist/handlers/notation-handlers.js.map +1 -0
  103. package/dist/handlers/project-handlers.js +1 -1
  104. package/dist/handlers/query-handlers.d.ts.map +1 -1
  105. package/dist/handlers/query-handlers.js +166 -20
  106. package/dist/handlers/query-handlers.js.map +1 -1
  107. package/dist/prompts.js +1 -1
  108. package/dist/server.d.ts +19 -1
  109. package/dist/server.d.ts.map +1 -1
  110. package/dist/server.js +93 -3
  111. package/dist/server.js.map +1 -1
  112. package/dist/state.d.ts +10 -1
  113. package/dist/state.d.ts.map +1 -1
  114. package/dist/state.js +61 -8
  115. package/dist/state.js.map +1 -1
  116. package/dist/types.d.ts +75 -3
  117. package/dist/types.d.ts.map +1 -1
  118. package/dist/utils.d.ts +4 -0
  119. package/dist/utils.d.ts.map +1 -1
  120. package/dist/utils.js +18 -1
  121. package/dist/utils.js.map +1 -1
  122. package/package.json +4 -3
  123. package/src/analysis-worker.ts +9 -301
  124. package/src/analysis.ts +151 -77
  125. package/src/config.ts +6 -193
  126. package/src/definitions/analysis-tools.ts +127 -0
  127. package/src/definitions/context-tools.ts +147 -0
  128. package/src/definitions/graph-tools.ts +126 -0
  129. package/src/definitions/graphql-tools.ts +64 -0
  130. package/src/definitions/guarantee-tools.ts +138 -0
  131. package/src/definitions/index.ts +28 -0
  132. package/src/definitions/knowledge-tools.ts +302 -0
  133. package/src/definitions/notation-tools.ts +64 -0
  134. package/src/definitions/project-tools.ts +183 -0
  135. package/src/definitions/query-tools.ts +247 -0
  136. package/src/definitions/types.ts +22 -0
  137. package/src/dev-proxy.ts +336 -0
  138. package/src/handlers/analysis-handlers.ts +35 -4
  139. package/src/handlers/context-handlers.ts +19 -15
  140. package/src/handlers/coverage-handlers.ts +1 -1
  141. package/src/handlers/dataflow-handlers.ts +74 -56
  142. package/src/handlers/documentation-handlers.ts +56 -2
  143. package/src/handlers/graph-handlers.ts +212 -0
  144. package/src/handlers/graphql-handlers.ts +70 -0
  145. package/src/handlers/guarantee-handlers.ts +1 -1
  146. package/src/handlers/guard-handlers.ts +7 -3
  147. package/src/handlers/index.ts +6 -0
  148. package/src/handlers/issue-handlers.ts +10 -15
  149. package/src/handlers/knowledge-handlers.ts +242 -0
  150. package/src/handlers/notation-handlers.ts +71 -0
  151. package/src/handlers/project-handlers.ts +1 -1
  152. package/src/handlers/query-handlers.ts +186 -22
  153. package/src/prompts.ts +1 -1
  154. package/src/server.ts +126 -2
  155. package/src/state.ts +68 -8
  156. package/src/types.ts +98 -3
  157. package/src/utils.ts +22 -1
  158. package/src/definitions.ts +0 -665
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * MCP Dataflow Handlers
3
+ *
4
+ * Delegates BFS tracing to @grafema/util's shared traceDataflow module.
3
5
  */
4
6
 
5
7
  import { ensureAnalyzed } from '../analysis.js';
@@ -9,6 +11,7 @@ import {
9
11
  textResult,
10
12
  errorResult,
11
13
  } from '../utils.js';
14
+ import { isGrafemaUri, toCompactSemanticId } from '@grafema/util';
12
15
  import type {
13
16
  ToolResult,
14
17
  TraceAliasArgs,
@@ -16,8 +19,14 @@ import type {
16
19
  CheckInvariantArgs,
17
20
  GraphNode,
18
21
  } from '../types.js';
22
+ import {
23
+ traceDataflow,
24
+ renderTraceNarrative,
25
+ type DataflowBackend,
26
+ type TraceDetail,
27
+ } from '@grafema/util';
19
28
 
20
- // === TRACE HANDLERS ===
29
+ // === TRACE ALIAS (unchanged) ===
21
30
 
22
31
  export async function handleTraceAlias(args: TraceAliasArgs): Promise<ToolResult> {
23
32
  const db = await ensureAnalyzed();
@@ -58,6 +67,16 @@ export async function handleTraceAlias(args: TraceAliasArgs): Promise<ToolResult
58
67
  }
59
68
  visited.add(current.id);
60
69
 
70
+ // Resolve REFERENCE → declaration transparently (don't add to chain)
71
+ if (current.type === 'REFERENCE') {
72
+ const resolveEdges = await db.getOutgoingEdges(current.id, ['READS_FROM']);
73
+ if (resolveEdges.length > 0) {
74
+ current = await db.getNode(resolveEdges[0].dst);
75
+ continue;
76
+ }
77
+ break;
78
+ }
79
+
61
80
  chain.push({
62
81
  type: current.type,
63
82
  name: current.name,
@@ -80,71 +99,64 @@ export async function handleTraceAlias(args: TraceAliasArgs): Promise<ToolResult
80
99
  );
81
100
  }
82
101
 
102
+ // === TRACE DATAFLOW ===
103
+
83
104
  export async function handleTraceDataFlow(args: TraceDataFlowArgs): Promise<ToolResult> {
84
105
  const db = await ensureAnalyzed();
85
- const { source, direction = 'forward', max_depth = 10 } = args;
106
+ const { source, file, direction = 'forward', max_depth = 10, limit = 50, detail } = args;
86
107
 
87
108
  // 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
109
+ let sourceNode: GraphNode | null = await db.getNode(source);
94
110
  if (!sourceNode) {
111
+ // Search by name, preferring nodes that match the file filter
112
+ let fallbackNode: GraphNode | null = null;
95
113
  for await (const node of db.queryNodes({ name: source })) {
114
+ if (file && !node.file?.includes(file)) {
115
+ // Keep first match as fallback in case no file-matching node is found
116
+ if (!fallbackNode) fallbackNode = node;
117
+ continue;
118
+ }
96
119
  sourceNode = node;
97
120
  break;
98
121
  }
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);
122
+ // Also try PARAMETER type nodes (often the real entry point for dataflow)
123
+ if (!sourceNode) {
124
+ for await (const node of db.queryNodes({ type: 'PARAMETER', name: source })) {
125
+ if (file && !node.file?.includes(file)) {
126
+ if (!fallbackNode) fallbackNode = node;
127
+ continue;
128
+ }
129
+ sourceNode = node;
130
+ break;
133
131
  }
134
132
  }
135
-
136
- if (depth > 0) {
137
- paths.push(newPath);
133
+ // Use fallback (first name match regardless of file) if no file-specific match
134
+ if (!sourceNode && fallbackNode) {
135
+ sourceNode = fallbackNode;
138
136
  }
139
137
  }
138
+ if (!sourceNode) {
139
+ const displaySource = isGrafemaUri(source) ? toCompactSemanticId(source) : source;
140
+ return errorResult(`Source "${displaySource}" not found`);
141
+ }
140
142
 
141
- await trace(sourceNode.id, 0, []);
143
+ // Cast db to DataflowBackend — runtime types are compatible
144
+ const dfDb = db as unknown as DataflowBackend;
142
145
 
143
- return textResult(
144
- `Data flow from "${source}" (${paths.length} paths):\n\n${JSON.stringify(paths, null, 2)}`
145
- );
146
+ const traceResults = await traceDataflow(dfDb, sourceNode.id, {
147
+ direction: direction as 'forward' | 'backward' | 'both',
148
+ maxDepth: max_depth,
149
+ limit,
150
+ });
151
+
152
+ const sourceName = sourceNode.name || source;
153
+ return textResult(renderTraceNarrative(traceResults, sourceName, {
154
+ detail: (detail as TraceDetail) || 'normal',
155
+ }));
146
156
  }
147
157
 
158
+ // === CHECK INVARIANT (unchanged) ===
159
+
148
160
  export async function handleCheckInvariant(args: CheckInvariantArgs): Promise<ToolResult> {
149
161
  const db = await ensureAnalyzed();
150
162
  const { rule, name: description } = args;
@@ -155,32 +167,38 @@ export async function handleCheckInvariant(args: CheckInvariantArgs): Promise<To
155
167
 
156
168
  try {
157
169
  const checkFn = (db as unknown as { checkGuarantee: (q: string) => Promise<Array<{ bindings: Array<{ name: string; value: string }> }>> }).checkGuarantee;
158
- const violations = await checkFn(rule);
170
+ const violations = await checkFn.call(db, rule);
159
171
  const total = violations.length;
160
172
 
161
173
  if (total === 0) {
162
- return textResult(`✅ Invariant holds: ${description || 'No violations found'}`);
174
+ return textResult(`Invariant holds: ${description || 'No violations found'}`);
163
175
  }
164
176
 
165
177
  const enrichedViolations: unknown[] = [];
166
178
  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);
179
+ const xBinding = v.bindings?.find((b: { name: string; value: string }) => b.name === 'X');
180
+ if (xBinding) {
181
+ const node = await db.getNode(xBinding.value);
170
182
  if (node) {
171
183
  enrichedViolations.push({
172
- id: nodeId,
184
+ id: xBinding.value,
173
185
  type: node.type,
174
186
  name: node.name,
175
187
  file: node.file,
176
188
  line: node.line,
177
189
  });
190
+ } else {
191
+ const bindingsMap: Record<string, string> = {};
192
+ for (const b of v.bindings!) {
193
+ bindingsMap[b.name] = b.value;
194
+ }
195
+ enrichedViolations.push(bindingsMap);
178
196
  }
179
197
  }
180
198
  }
181
199
 
182
200
  return textResult(
183
- `❌ ${total} violation(s) found:\n\n${JSON.stringify(
201
+ `${total} violation(s) found:\n\n${JSON.stringify(
184
202
  serializeBigInt(enrichedViolations),
185
203
  null,
186
204
  2
@@ -2,7 +2,7 @@
2
2
  * MCP Documentation Handlers
3
3
  */
4
4
 
5
- import { getOnboardingInstruction } from '@grafema/core';
5
+ import { getOnboardingInstruction } from '@grafema/util';
6
6
  import {
7
7
  textResult,
8
8
  } from '../utils.js';
@@ -41,7 +41,7 @@ Grafema is a static code analyzer that builds a graph of your codebase.
41
41
  violation(X) :- node(X, "TYPE"), attr(X, "name", "value").
42
42
 
43
43
  ## Available Predicates
44
- - node(Id, Type) - match nodes
44
+ - type(Id, Type) - match nodes (alias: node)
45
45
  - edge(Src, Dst, Type) - match edges
46
46
  - attr(Id, Name, Value) - match attributes
47
47
  - \\+ - negation (not)
@@ -81,6 +81,60 @@ Use check_guarantees to verify all guarantees.
81
81
  ## Example
82
82
  Name: no-eval
83
83
  Rule: violation(X) :- node(X, "CALL"), attr(X, "name", "eval").
84
+ `,
85
+ notation: `
86
+ # Grafema DSL — Compact Visual Notation
87
+
88
+ Grafema DSL renders graph structure as compact, readable notation.
89
+ Output-only — Datalog remains the query language.
90
+
91
+ ## Archetypes & Operators
92
+
93
+ | Archetype | Op | Meaning | Example edge types |
94
+ |------------|-------|---------------------------|---------------------------------------|
95
+ | contains | (nest)| structural containment | CONTAINS, HAS_MEMBER, DECLARES |
96
+ | depends | o- | dependency / import | DEPENDS_ON, IMPORTS_FROM, USES |
97
+ | flow_out | > | outward call / data flow | CALLS, ROUTES_TO, PASSES_ARGUMENT |
98
+ | flow_in | < | inward data / type flow | READS_FROM, ASSIGNED_FROM, EXTENDS |
99
+ | write | => | persistent side effect | WRITES_TO, LOGS_TO |
100
+ | exception | >x | error / rejection | THROWS, REJECTS, CATCHES_FROM |
101
+ | publishes | ~>> | event / message | EMITS_EVENT, PUBLISHES_TO, EXPOSED_VIA|
102
+ | gates | ?| | conditional guard | HAS_CONDITION, HAS_CASE |
103
+ | governs | |= | governance / invariant | GOVERNS, VIOLATES, MONITORED_BY |
104
+
105
+ ## LOD Levels (depth)
106
+
107
+ - **depth=0**: Node names only — minimal overview
108
+ - **depth=1** (default): Node + edges — shows all relationships with operators
109
+ - **depth=2**: Node + edges + nested children, **folded** — repetitive siblings compressed into exemplar + summary. Ideal for large modules (e.g., 36 handler imports → 1 exemplar + fold summary)
110
+ - **depth=3**: Node + edges + nested children, **exact** — every node expanded individually, no folding. Use when you need the precise bijective DSL output
111
+
112
+ ## Perspective Presets
113
+
114
+ | Preset | Archetypes shown | Use case |
115
+ |----------|-------------------------------|---------------------------|
116
+ | security | write, exception | Audit side effects & errors|
117
+ | data | flow_out, flow_in, write | Trace data movement |
118
+ | errors | exception | Error handling review |
119
+ | api | flow_out, publishes, depends | API surface analysis |
120
+ | events | publishes | Event flow mapping |
121
+
122
+ ## Special Modifiers
123
+
124
+ - \`??\` — uncertain/dynamic (unresolved call, dynamic import)
125
+ - \`[]\` — inside loop (edge occurs within iteration)
126
+
127
+ ## Budget
128
+
129
+ Default budget: 7 items per group. When exceeded, remaining items are
130
+ summarized as \`+N more\`. Override with budget parameter.
131
+
132
+ ## Usage
133
+
134
+ **MCP:** \`describe(target="src/app.ts->FUNCTION->main", depth=1, perspective="security")\`
135
+ **CLI:** \`grafema describe "src/app.ts->FUNCTION->main" -d 1 --perspective security\`
136
+
137
+ Target resolution order: semantic ID → file path (MODULE) → node name.
84
138
  `,
85
139
  };
86
140
 
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Graph traversal handlers: get_node, get_neighbors, traverse_graph
3
+ * REG-521: Add raw graph traversal primitives to MCP
4
+ */
5
+
6
+ import { ensureAnalyzed } from '../analysis.js';
7
+ import { textResult, errorResult } from '../utils.js';
8
+ import { isGrafemaUri, toCompactSemanticId } from '@grafema/util';
9
+ import type { ToolResult, GetNodeArgs, GetNeighborsArgs, TraverseGraphArgs } from '../types.js';
10
+ import type { EdgeType, EdgeRecord } from '@grafema/types';
11
+
12
+ const MAX_TRAVERSAL_RESULTS = 10_000;
13
+ const MAX_DEPTH = 20;
14
+
15
+ /**
16
+ * Minimal backend interface for graph-handler logic functions.
17
+ * Allows testing with mock backends without importing full GraphBackend.
18
+ */
19
+ interface GraphBackendLike {
20
+ getNode(id: string): Promise<Record<string, unknown> | null>;
21
+ getOutgoingEdges(nodeId: string, edgeTypes?: EdgeType[] | null): Promise<EdgeRecord[]>;
22
+ getIncomingEdges(nodeId: string, edgeTypes?: EdgeType[] | null): Promise<EdgeRecord[]>;
23
+ }
24
+
25
+ // === Shared helpers ===
26
+
27
+ async function groupEdgesByType(
28
+ edges: EdgeRecord[],
29
+ db: GraphBackendLike,
30
+ getNodeId: (edge: EdgeRecord) => string,
31
+ ): Promise<Record<string, Array<Record<string, unknown>>>> {
32
+ const grouped: Record<string, Array<Record<string, unknown>>> = {};
33
+
34
+ for (const edge of edges) {
35
+ const type = edge.type as string;
36
+ if (!grouped[type]) grouped[type] = [];
37
+ const nodeId = getNodeId(edge);
38
+ const node = await db.getNode(nodeId);
39
+ grouped[type].push({
40
+ id: nodeId,
41
+ ...(node ? { type: node.type, name: node.name, file: node.file, line: node.line } : { type: 'UNKNOWN' }),
42
+ ...(edge.metadata ? { edgeMetadata: edge.metadata } : {}),
43
+ });
44
+ }
45
+
46
+ return grouped;
47
+ }
48
+
49
+ // === Logic functions (testable, accept backend directly) ===
50
+
51
+ export async function getNodeLogic(db: GraphBackendLike, args: GetNodeArgs): Promise<ToolResult> {
52
+ const { semanticId } = args;
53
+
54
+ if (!semanticId || semanticId.trim() === '') {
55
+ return errorResult('semanticId must be a non-empty string');
56
+ }
57
+
58
+ // Accept both grafema:// URI and compact format
59
+ // If URI is passed, convert to compact for error messages
60
+ const displayId = isGrafemaUri(semanticId) ? toCompactSemanticId(semanticId) : semanticId;
61
+
62
+ const node = await db.getNode(semanticId);
63
+
64
+ if (!node) {
65
+ return errorResult(`Node not found: "${displayId}". Use find_nodes to search by type, name, or file.`);
66
+ }
67
+
68
+ return textResult(JSON.stringify(node, null, 2));
69
+ }
70
+
71
+ export async function getNeighborsLogic(db: GraphBackendLike, args: GetNeighborsArgs): Promise<ToolResult> {
72
+ const { semanticId, direction = 'both', edgeTypes } = args;
73
+
74
+ if (!semanticId || semanticId.trim() === '') {
75
+ return errorResult('semanticId must be a non-empty string');
76
+ }
77
+
78
+ if (edgeTypes !== undefined && edgeTypes.length === 0) {
79
+ return errorResult('edgeTypes must not be an empty array. Omit edgeTypes to get all edge types.');
80
+ }
81
+
82
+ // Accept both grafema:// URI and compact format
83
+ const displayId = isGrafemaUri(semanticId) ? toCompactSemanticId(semanticId) : semanticId;
84
+
85
+ const node = await db.getNode(semanticId);
86
+
87
+ if (!node) {
88
+ return errorResult(`Node not found: "${displayId}". Use find_nodes to search by type, name, or file.`);
89
+ }
90
+
91
+ const edgeFilter = (edgeTypes as EdgeType[] | undefined) ?? null;
92
+ const result: Record<string, unknown> = {};
93
+
94
+ if (direction === 'outgoing' || direction === 'both') {
95
+ const edges = await db.getOutgoingEdges(semanticId, edgeFilter);
96
+ result.outgoing = await groupEdgesByType(edges, db, (e) => e.dst);
97
+ }
98
+
99
+ if (direction === 'incoming' || direction === 'both') {
100
+ const edges = await db.getIncomingEdges(semanticId, edgeFilter);
101
+ result.incoming = await groupEdgesByType(edges, db, (e) => e.src);
102
+ }
103
+
104
+ return textResult(JSON.stringify(result, null, 2));
105
+ }
106
+
107
+ export async function traverseGraphLogic(db: GraphBackendLike, args: TraverseGraphArgs): Promise<ToolResult> {
108
+ const { startNodeIds, edgeTypes, maxDepth = 5, direction = 'outgoing' } = args;
109
+
110
+ // Validate inputs
111
+ if (!startNodeIds || startNodeIds.length === 0) {
112
+ return errorResult('startNodeIds must not be empty');
113
+ }
114
+ if (!edgeTypes || edgeTypes.length === 0) {
115
+ return errorResult('edgeTypes must not be empty. Use get_schema(type="edges") to see available types.');
116
+ }
117
+ if (!Number.isInteger(maxDepth) || maxDepth < 0) {
118
+ return errorResult('maxDepth must be a non-negative integer');
119
+ }
120
+ if (maxDepth > MAX_DEPTH) {
121
+ return errorResult(`maxDepth must be <= ${MAX_DEPTH} to prevent performance issues`);
122
+ }
123
+
124
+ // Deduplicate start nodes
125
+ const uniqueStartIds = [...new Set(startNodeIds)];
126
+
127
+ // Verify start nodes exist
128
+ for (const id of uniqueStartIds) {
129
+ const node = await db.getNode(id);
130
+ if (!node) {
131
+ // Accept both grafema:// URI and compact format for display
132
+ const displayId = isGrafemaUri(id) ? toCompactSemanticId(id) : id;
133
+ return errorResult(`Start node not found: "${displayId}". Use find_nodes to search by type, name, or file.`);
134
+ }
135
+ }
136
+
137
+ const edgeFilter = edgeTypes as EdgeType[];
138
+
139
+ // Manual BFS (works for both directions, provides depth info)
140
+ const visited = new Set<string>(uniqueStartIds);
141
+ const queue: Array<{ id: string; depth: number }> = uniqueStartIds.map(id => ({ id, depth: 0 }));
142
+ const results: Array<{ id: string; depth: number }> = uniqueStartIds.map(id => ({ id, depth: 0 }));
143
+
144
+ while (queue.length > 0) {
145
+ const current = queue.shift()!;
146
+ if (current.depth >= maxDepth) continue;
147
+
148
+ const edges: EdgeRecord[] = direction === 'outgoing'
149
+ ? await db.getOutgoingEdges(current.id, edgeFilter)
150
+ : await db.getIncomingEdges(current.id, edgeFilter);
151
+
152
+ for (const edge of edges) {
153
+ const neighborId = direction === 'outgoing' ? edge.dst : edge.src;
154
+ if (!visited.has(neighborId)) {
155
+ visited.add(neighborId);
156
+ const nextDepth = current.depth + 1;
157
+ queue.push({ id: neighborId, depth: nextDepth });
158
+ results.push({ id: neighborId, depth: nextDepth });
159
+
160
+ if (results.length >= MAX_TRAVERSAL_RESULTS) {
161
+ const nodes = await enrichResults(db, results);
162
+ return textResult(JSON.stringify({
163
+ count: nodes.length,
164
+ truncated: true,
165
+ message: `Traversal hit limit of ${MAX_TRAVERSAL_RESULTS} nodes. Use more specific edge types or lower maxDepth.`,
166
+ nodes,
167
+ }, null, 2));
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ const nodes = await enrichResults(db, results);
174
+ return textResult(JSON.stringify({
175
+ count: nodes.length,
176
+ truncated: false,
177
+ nodes,
178
+ }, null, 2));
179
+ }
180
+
181
+ async function enrichResults(
182
+ db: GraphBackendLike,
183
+ results: Array<{ id: string; depth: number }>
184
+ ): Promise<Array<Record<string, unknown>>> {
185
+ return Promise.all(
186
+ results.map(async ({ id, depth }) => {
187
+ const node = await db.getNode(id);
188
+ return {
189
+ id,
190
+ depth,
191
+ ...(node ? { type: node.type, name: node.name, file: node.file, line: node.line } : { type: 'UNKNOWN' }),
192
+ };
193
+ })
194
+ );
195
+ }
196
+
197
+ // === Public handlers (call ensureAnalyzed, used by MCP routing) ===
198
+
199
+ export async function handleGetNode(args: GetNodeArgs): Promise<ToolResult> {
200
+ const db = await ensureAnalyzed();
201
+ return getNodeLogic(db as unknown as GraphBackendLike, args);
202
+ }
203
+
204
+ export async function handleGetNeighbors(args: GetNeighborsArgs): Promise<ToolResult> {
205
+ const db = await ensureAnalyzed();
206
+ return getNeighborsLogic(db as unknown as GraphBackendLike, args);
207
+ }
208
+
209
+ export async function handleTraverseGraph(args: TraverseGraphArgs): Promise<ToolResult> {
210
+ const db = await ensureAnalyzed();
211
+ return traverseGraphLogic(db as unknown as GraphBackendLike, args);
212
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * GraphQL query handler — execute GraphQL queries against the code graph.
3
+ *
4
+ * Uses @grafema/api's schema and resolvers in-process via graphql-yoga.
5
+ * No HTTP server needed — yoga.fetch() accepts synthetic Request objects.
6
+ */
7
+
8
+ import { ensureAnalyzed } from '../analysis.js';
9
+ import { textResult, errorResult } from '../utils.js';
10
+ import type { ToolResult, GraphQLQueryArgs } from '../types.js';
11
+ import type { RFDBServerBackend } from '@grafema/util';
12
+
13
+ let yogaInstance: any = null;
14
+ let yogaBackend: RFDBServerBackend | null = null;
15
+
16
+ /**
17
+ * Get or create the yoga instance.
18
+ * Recreated if the backend changes (e.g., after re-analysis).
19
+ */
20
+ async function getYoga(backend: RFDBServerBackend) {
21
+ if (yogaInstance && yogaBackend === backend) {
22
+ return yogaInstance;
23
+ }
24
+
25
+ // Dynamic import to avoid loading graphql-yoga unless needed
26
+ const { createGraphQLServer } = await import('@grafema/api');
27
+ yogaInstance = createGraphQLServer({ backend });
28
+ yogaBackend = backend;
29
+ return yogaInstance;
30
+ }
31
+
32
+ export async function handleGraphQLQuery(args: GraphQLQueryArgs): Promise<ToolResult> {
33
+ const { query, variables, operationName } = args;
34
+
35
+ if (!query || query.trim() === '') {
36
+ return errorResult('query must be a non-empty GraphQL query string');
37
+ }
38
+
39
+ const db = await ensureAnalyzed();
40
+ const yoga = await getYoga(db as RFDBServerBackend);
41
+
42
+ // Build the GraphQL request body
43
+ const body: Record<string, unknown> = { query };
44
+ if (variables) body.variables = variables;
45
+ if (operationName) body.operationName = operationName;
46
+
47
+ // Execute via yoga.fetch() — no HTTP server needed
48
+ const response = await yoga.fetch('http://localhost/graphql', {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify(body),
52
+ });
53
+
54
+ const result = await response.json();
55
+
56
+ // Format output
57
+ if (result.errors && result.errors.length > 0) {
58
+ const errorMessages = result.errors.map((e: any) => e.message).join('\n');
59
+ if (result.data) {
60
+ // Partial success — return data with errors noted
61
+ return textResult(
62
+ `GraphQL partial result (with errors):\n${errorMessages}\n\n` +
63
+ JSON.stringify(result.data, null, 2)
64
+ );
65
+ }
66
+ return errorResult(`GraphQL errors:\n${errorMessages}`);
67
+ }
68
+
69
+ return textResult(JSON.stringify(result.data, null, 2));
70
+ }
@@ -13,7 +13,7 @@ import type {
13
13
  CheckGuaranteesArgs,
14
14
  DeleteGuaranteeArgs,
15
15
  } from '../types.js';
16
- import { isGuaranteeType } from '@grafema/core';
16
+ import { isGuaranteeType } from '@grafema/util';
17
17
 
18
18
  // === GUARANTEE HANDLERS ===
19
19
 
@@ -8,6 +8,7 @@ import {
8
8
  textResult,
9
9
  errorResult,
10
10
  } from '../utils.js';
11
+ import { isGrafemaUri, toCompactSemanticId } from '@grafema/util';
11
12
  import type {
12
13
  ToolResult,
13
14
  FindGuardsArgs,
@@ -28,10 +29,13 @@ export async function handleFindGuards(args: FindGuardsArgs): Promise<ToolResult
28
29
  const db = await getOrCreateBackend();
29
30
  const { nodeId } = args;
30
31
 
32
+ // Accept both grafema:// URI and compact format
33
+ const displayId = isGrafemaUri(nodeId) ? toCompactSemanticId(nodeId) : nodeId;
34
+
31
35
  // Verify target node exists
32
36
  const targetNode = await db.getNode(nodeId);
33
37
  if (!targetNode) {
34
- return errorResult(`Node not found: ${nodeId}`);
38
+ return errorResult(`Node not found: ${displayId}`);
35
39
  }
36
40
 
37
41
  const guards: GuardInfo[] = [];
@@ -79,7 +83,7 @@ export async function handleFindGuards(args: FindGuardsArgs): Promise<ToolResult
79
83
 
80
84
  if (guards.length === 0) {
81
85
  return textResult(
82
- `No guards found for node: ${nodeId}\n` +
86
+ `No guards found for node: ${displayId}\n` +
83
87
  `The node is not protected by any conditional scope (if/else/switch/etc.).`
84
88
  );
85
89
  }
@@ -91,7 +95,7 @@ export async function handleFindGuards(args: FindGuardsArgs): Promise<ToolResult
91
95
  }).join('\n');
92
96
 
93
97
  return textResult(
94
- `Found ${guards.length} guard(s) for node: ${nodeId}\n` +
98
+ `Found ${guards.length} guard(s) for node: ${displayId}\n` +
95
99
  `(inner to outer order)\n\n` +
96
100
  summary +
97
101
  `\n\n` +
@@ -12,3 +12,9 @@ export { handleGetCoverage } from './coverage-handlers.js';
12
12
  export { handleFindGuards } from './guard-handlers.js';
13
13
  export { handleGetDocumentation } from './documentation-handlers.js';
14
14
  export { handleReportIssue } from './issue-handlers.js';
15
+ export { handleGetNode, handleGetNeighbors, handleTraverseGraph } from './graph-handlers.js';
16
+ export { handleAddKnowledge, handleQueryKnowledge, handleQueryDecisions, handleSupersedeFact, handleGetKnowledgeStats } from './knowledge-handlers.js';
17
+ // Disabled: requires git-ingest (US-17). See US-17 in AI-AGENT-STORIES.md
18
+ // export { handleGitChurn, handleGitCoChange, handleGitOwnership, handleGitArchaeology } from './knowledge-handlers.js';
19
+ export { handleDescribe } from './notation-handlers.js';
20
+ export { handleGraphQLQuery } from './graphql-handlers.js';