@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.
- package/dist/analysis-worker.d.ts +4 -3
- package/dist/analysis-worker.d.ts.map +1 -1
- package/dist/analysis-worker.js +8 -203
- package/dist/analysis-worker.js.map +1 -1
- package/dist/analysis.d.ts +10 -3
- package/dist/analysis.d.ts.map +1 -1
- package/dist/analysis.js +130 -62
- package/dist/analysis.js.map +1 -1
- package/dist/config.d.ts +5 -11
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -128
- package/dist/config.js.map +1 -1
- package/dist/definitions/analysis-tools.d.ts +6 -0
- package/dist/definitions/analysis-tools.d.ts.map +1 -0
- package/dist/definitions/analysis-tools.js +125 -0
- package/dist/definitions/analysis-tools.js.map +1 -0
- package/dist/definitions/context-tools.d.ts +6 -0
- package/dist/definitions/context-tools.d.ts.map +1 -0
- package/dist/definitions/context-tools.js +144 -0
- package/dist/definitions/context-tools.js.map +1 -0
- package/dist/definitions/graph-tools.d.ts +7 -0
- package/dist/definitions/graph-tools.d.ts.map +1 -0
- package/dist/definitions/graph-tools.js +124 -0
- package/dist/definitions/graph-tools.js.map +1 -0
- package/dist/definitions/graphql-tools.d.ts +6 -0
- package/dist/definitions/graphql-tools.d.ts.map +1 -0
- package/dist/definitions/graphql-tools.js +62 -0
- package/dist/definitions/graphql-tools.js.map +1 -0
- package/dist/definitions/guarantee-tools.d.ts +6 -0
- package/dist/definitions/guarantee-tools.d.ts.map +1 -0
- package/dist/definitions/guarantee-tools.js +136 -0
- package/dist/definitions/guarantee-tools.js.map +1 -0
- package/dist/definitions/index.d.ts +7 -0
- package/dist/definitions/index.d.ts.map +1 -0
- package/dist/definitions/index.js +24 -0
- package/dist/definitions/index.js.map +1 -0
- package/dist/definitions/knowledge-tools.d.ts +10 -0
- package/dist/definitions/knowledge-tools.d.ts.map +1 -0
- package/dist/definitions/knowledge-tools.js +300 -0
- package/dist/definitions/knowledge-tools.js.map +1 -0
- package/dist/definitions/notation-tools.d.ts +9 -0
- package/dist/definitions/notation-tools.d.ts.map +1 -0
- package/dist/definitions/notation-tools.js +62 -0
- package/dist/definitions/notation-tools.js.map +1 -0
- package/dist/definitions/project-tools.d.ts +6 -0
- package/dist/definitions/project-tools.d.ts.map +1 -0
- package/dist/definitions/project-tools.js +181 -0
- package/dist/definitions/project-tools.js.map +1 -0
- package/dist/definitions/query-tools.d.ts +6 -0
- package/dist/definitions/query-tools.d.ts.map +1 -0
- package/dist/definitions/query-tools.js +245 -0
- package/dist/definitions/query-tools.js.map +1 -0
- package/dist/definitions/types.d.ts +21 -0
- package/dist/definitions/types.d.ts.map +1 -0
- package/dist/definitions/types.js +5 -0
- package/dist/definitions/types.js.map +1 -0
- package/dist/dev-proxy.d.ts +29 -0
- package/dist/dev-proxy.d.ts.map +1 -0
- package/dist/dev-proxy.js +267 -0
- package/dist/dev-proxy.js.map +1 -0
- package/dist/handlers/analysis-handlers.d.ts.map +1 -1
- package/dist/handlers/analysis-handlers.js +34 -4
- package/dist/handlers/analysis-handlers.js.map +1 -1
- package/dist/handlers/context-handlers.d.ts +5 -6
- package/dist/handlers/context-handlers.d.ts.map +1 -1
- package/dist/handlers/context-handlers.js +19 -16
- package/dist/handlers/context-handlers.js.map +1 -1
- package/dist/handlers/coverage-handlers.js +1 -1
- package/dist/handlers/dataflow-handlers.d.ts +2 -0
- package/dist/handlers/dataflow-handlers.d.ts.map +1 -1
- package/dist/handlers/dataflow-handlers.js +68 -46
- package/dist/handlers/dataflow-handlers.js.map +1 -1
- package/dist/handlers/documentation-handlers.d.ts.map +1 -1
- package/dist/handlers/documentation-handlers.js +56 -2
- package/dist/handlers/documentation-handlers.js.map +1 -1
- package/dist/handlers/graph-handlers.d.ts +23 -0
- package/dist/handlers/graph-handlers.d.ts.map +1 -0
- package/dist/handlers/graph-handlers.js +155 -0
- package/dist/handlers/graph-handlers.js.map +1 -0
- package/dist/handlers/graphql-handlers.d.ts +9 -0
- package/dist/handlers/graphql-handlers.d.ts.map +1 -0
- package/dist/handlers/graphql-handlers.js +57 -0
- package/dist/handlers/graphql-handlers.js.map +1 -0
- package/dist/handlers/guarantee-handlers.js +1 -1
- package/dist/handlers/guard-handlers.d.ts.map +1 -1
- package/dist/handlers/guard-handlers.js +6 -3
- package/dist/handlers/guard-handlers.js.map +1 -1
- package/dist/handlers/index.d.ts +4 -0
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/issue-handlers.d.ts.map +1 -1
- package/dist/handlers/issue-handlers.js +10 -15
- package/dist/handlers/issue-handlers.js.map +1 -1
- package/dist/handlers/knowledge-handlers.d.ts +25 -0
- package/dist/handlers/knowledge-handlers.d.ts.map +1 -0
- package/dist/handlers/knowledge-handlers.js +208 -0
- package/dist/handlers/knowledge-handlers.js.map +1 -0
- package/dist/handlers/notation-handlers.d.ts +6 -0
- package/dist/handlers/notation-handlers.d.ts.map +1 -0
- package/dist/handlers/notation-handlers.js +53 -0
- package/dist/handlers/notation-handlers.js.map +1 -0
- package/dist/handlers/project-handlers.js +1 -1
- package/dist/handlers/query-handlers.d.ts.map +1 -1
- package/dist/handlers/query-handlers.js +166 -20
- package/dist/handlers/query-handlers.js.map +1 -1
- package/dist/prompts.js +1 -1
- package/dist/server.d.ts +19 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +93 -3
- package/dist/server.js.map +1 -1
- package/dist/state.d.ts +10 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +61 -8
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +75 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +18 -1
- package/dist/utils.js.map +1 -1
- package/package.json +4 -3
- package/src/analysis-worker.ts +9 -301
- package/src/analysis.ts +151 -77
- package/src/config.ts +6 -193
- package/src/definitions/analysis-tools.ts +127 -0
- package/src/definitions/context-tools.ts +147 -0
- package/src/definitions/graph-tools.ts +126 -0
- package/src/definitions/graphql-tools.ts +64 -0
- package/src/definitions/guarantee-tools.ts +138 -0
- package/src/definitions/index.ts +28 -0
- package/src/definitions/knowledge-tools.ts +302 -0
- package/src/definitions/notation-tools.ts +64 -0
- package/src/definitions/project-tools.ts +183 -0
- package/src/definitions/query-tools.ts +247 -0
- package/src/definitions/types.ts +22 -0
- package/src/dev-proxy.ts +336 -0
- package/src/handlers/analysis-handlers.ts +35 -4
- package/src/handlers/context-handlers.ts +19 -15
- package/src/handlers/coverage-handlers.ts +1 -1
- package/src/handlers/dataflow-handlers.ts +74 -56
- package/src/handlers/documentation-handlers.ts +56 -2
- package/src/handlers/graph-handlers.ts +212 -0
- package/src/handlers/graphql-handlers.ts +70 -0
- package/src/handlers/guarantee-handlers.ts +1 -1
- package/src/handlers/guard-handlers.ts +7 -3
- package/src/handlers/index.ts +6 -0
- package/src/handlers/issue-handlers.ts +10 -15
- package/src/handlers/knowledge-handlers.ts +242 -0
- package/src/handlers/notation-handlers.ts +71 -0
- package/src/handlers/project-handlers.ts +1 -1
- package/src/handlers/query-handlers.ts +186 -22
- package/src/prompts.ts +1 -1
- package/src/server.ts +126 -2
- package/src/state.ts +68 -8
- package/src/types.ts +98 -3
- package/src/utils.ts +22 -1
- 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
|
|
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 =
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 (
|
|
137
|
-
|
|
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
|
-
|
|
143
|
+
// Cast db to DataflowBackend — runtime types are compatible
|
|
144
|
+
const dfDb = db as unknown as DataflowBackend;
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
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
|
|
168
|
-
if (
|
|
169
|
-
const node = await db.getNode(
|
|
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:
|
|
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
|
-
|
|
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/
|
|
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
|
-
-
|
|
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
|
+
}
|
|
@@ -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: ${
|
|
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: ${
|
|
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: ${
|
|
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` +
|
package/src/handlers/index.ts
CHANGED
|
@@ -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';
|