@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
|
@@ -14,9 +14,7 @@ import type {
|
|
|
14
14
|
|
|
15
15
|
export async function handleReportIssue(args: ReportIssueArgs): Promise<ToolResult> {
|
|
16
16
|
const { title, description, context, labels = ['bug'] } = args;
|
|
17
|
-
|
|
18
|
-
const GRAFEMA_ISSUE_TOKEN = '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';
|
|
19
|
-
const githubToken = process.env.GITHUB_TOKEN || GRAFEMA_ISSUE_TOKEN;
|
|
17
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
20
18
|
const repo = 'Disentinel/grafema';
|
|
21
19
|
|
|
22
20
|
// Build issue body
|
|
@@ -25,13 +23,12 @@ ${description}
|
|
|
25
23
|
|
|
26
24
|
${context ? `## Context\n\`\`\`\n${context}\n\`\`\`\n` : ''}
|
|
27
25
|
## Environment
|
|
28
|
-
- Grafema version: 0.1.0-alpha.1
|
|
29
26
|
- Reported via: MCP tool
|
|
30
27
|
|
|
31
28
|
---
|
|
32
|
-
*This issue was
|
|
29
|
+
*This issue was created via Grafema MCP server.*`;
|
|
33
30
|
|
|
34
|
-
// Try GitHub API if token
|
|
31
|
+
// Try GitHub API if user has a token configured
|
|
35
32
|
if (githubToken) {
|
|
36
33
|
try {
|
|
37
34
|
const response = await fetch(`https://api.github.com/repos/${repo}/issues`, {
|
|
@@ -51,21 +48,18 @@ ${context ? `## Context\n\`\`\`\n${context}\n\`\`\`\n` : ''}
|
|
|
51
48
|
if (response.ok) {
|
|
52
49
|
const issue = await response.json() as { html_url: string; number: number };
|
|
53
50
|
return textResult(
|
|
54
|
-
|
|
55
|
-
`**Issue #${issue.number}**: ${issue.html_url}\n\n` +
|
|
56
|
-
`Thank you for reporting this issue.`
|
|
51
|
+
`Issue created: #${issue.number} ${issue.html_url}`
|
|
57
52
|
);
|
|
58
53
|
} else {
|
|
59
54
|
const error = await response.text();
|
|
60
55
|
throw new Error(`GitHub API error: ${response.status} - ${error}`);
|
|
61
56
|
}
|
|
62
57
|
} catch (error) {
|
|
63
|
-
// Fall through to manual template if API fails
|
|
64
58
|
console.error('[report_issue] GitHub API failed:', error);
|
|
65
59
|
}
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
//
|
|
62
|
+
// Return template for manual submission
|
|
69
63
|
const issueUrl = `https://github.com/${repo}/issues/new`;
|
|
70
64
|
const encodedTitle = encodeURIComponent(title);
|
|
71
65
|
const encodedBody = encodeURIComponent(body);
|
|
@@ -73,9 +67,10 @@ ${context ? `## Context\n\`\`\`\n${context}\n\`\`\`\n` : ''}
|
|
|
73
67
|
const directUrl = `${issueUrl}?title=${encodedTitle}&body=${encodedBody}&labels=${encodedLabels}`;
|
|
74
68
|
|
|
75
69
|
return textResult(
|
|
76
|
-
|
|
77
|
-
`**
|
|
78
|
-
|
|
79
|
-
`---\n**Title:** ${title}\n\n${body}\n
|
|
70
|
+
`Create issue manually:\n\n` +
|
|
71
|
+
`**Link:** ${directUrl}\n\n` +
|
|
72
|
+
`Or copy to ${issueUrl}:\n\n` +
|
|
73
|
+
`---\n**Title:** ${title}\n\n${body}\n---\n\n` +
|
|
74
|
+
`Tip: Set GITHUB_TOKEN env var to create issues automatically.`
|
|
80
75
|
);
|
|
81
76
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Knowledge Handlers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getOrCreateKnowledgeBase } from '../state.js';
|
|
6
|
+
import { textResult, errorResult } from '../utils.js';
|
|
7
|
+
import type {
|
|
8
|
+
ToolResult,
|
|
9
|
+
AddKnowledgeArgs,
|
|
10
|
+
QueryKnowledgeArgs,
|
|
11
|
+
QueryDecisionsArgs,
|
|
12
|
+
SupersedeFactArgs,
|
|
13
|
+
// Disabled: requires git-ingest (US-17). See US-17 in AI-AGENT-STORIES.md
|
|
14
|
+
// GitChurnArgs,
|
|
15
|
+
// GitCoChangeArgs,
|
|
16
|
+
// GitOwnershipArgs,
|
|
17
|
+
// GitArchaeologyArgs,
|
|
18
|
+
} from '../types.js';
|
|
19
|
+
import type { KBDecision, KBNodeType } from '@grafema/util';
|
|
20
|
+
// Disabled: requires git-ingest (US-17). See US-17 in AI-AGENT-STORIES.md
|
|
21
|
+
// import { getChurn, getCoChanges, getOwnership, getArchaeology } from '@grafema/util';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Add a new knowledge node.
|
|
25
|
+
*/
|
|
26
|
+
export async function handleAddKnowledge(args: AddKnowledgeArgs): Promise<ToolResult> {
|
|
27
|
+
try {
|
|
28
|
+
const kb = await getOrCreateKnowledgeBase();
|
|
29
|
+
|
|
30
|
+
const node = await kb.addNode({
|
|
31
|
+
type: args.type as KBNodeType,
|
|
32
|
+
content: args.content,
|
|
33
|
+
slug: args.slug,
|
|
34
|
+
subtype: args.subtype,
|
|
35
|
+
scope: args.scope as 'global' | 'project' | 'module' | undefined,
|
|
36
|
+
projections: args.projections,
|
|
37
|
+
relates_to: args.relates_to,
|
|
38
|
+
status: args.status as KBDecision['status'],
|
|
39
|
+
confidence: args.confidence as 'high' | 'medium' | 'low',
|
|
40
|
+
effective_from: args.effective_from,
|
|
41
|
+
applies_to: args.applies_to,
|
|
42
|
+
task_id: args.task_id,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return textResult(
|
|
46
|
+
`Created ${node.type} node: ${node.id}\n` +
|
|
47
|
+
`File: ${node.filePath}\n` +
|
|
48
|
+
`Lifecycle: ${node.lifecycle}\n` +
|
|
49
|
+
(node.content.length > 200
|
|
50
|
+
? `Content: ${node.content.slice(0, 200)}...`
|
|
51
|
+
: `Content: ${node.content}`)
|
|
52
|
+
);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
return errorResult(`Failed to add knowledge: ${message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Query knowledge nodes with filters.
|
|
61
|
+
*/
|
|
62
|
+
export async function handleQueryKnowledge(args: QueryKnowledgeArgs): Promise<ToolResult> {
|
|
63
|
+
try {
|
|
64
|
+
const kb = await getOrCreateKnowledgeBase();
|
|
65
|
+
|
|
66
|
+
const nodes = await kb.queryNodes({
|
|
67
|
+
type: args.type as KBNodeType | undefined,
|
|
68
|
+
projection: args.projection,
|
|
69
|
+
relates_to: args.relates_to,
|
|
70
|
+
text: args.text,
|
|
71
|
+
include_dangling_only: args.include_dangling_only,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (nodes.length === 0) {
|
|
75
|
+
return textResult('No matching knowledge nodes found.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const lines: string[] = [`Found ${nodes.length} node(s):\n`];
|
|
79
|
+
for (const node of nodes) {
|
|
80
|
+
lines.push(`## ${node.id}`);
|
|
81
|
+
lines.push(`Type: ${node.type} | Lifecycle: ${node.lifecycle}`);
|
|
82
|
+
if (node.projections.length > 0) lines.push(`Projections: ${node.projections.join(', ')}`);
|
|
83
|
+
if (node.type === 'DECISION') {
|
|
84
|
+
const d = node as KBDecision;
|
|
85
|
+
lines.push(`Status: ${d.status}`);
|
|
86
|
+
if (d.applies_to?.length) lines.push(`Applies to: ${d.applies_to.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Include resolution status for code references
|
|
90
|
+
const resolved = await kb.resolveReferences(node);
|
|
91
|
+
if (resolved.length > 0) {
|
|
92
|
+
lines.push('Code refs:');
|
|
93
|
+
for (const r of resolved) {
|
|
94
|
+
const icon = r.status === 'resolved' ? 'OK' : 'DANGLING';
|
|
95
|
+
lines.push(` [${icon}] ${r.address}${r.codeNodeId ? ` ā ${r.codeNodeId}` : ''}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
lines.push('');
|
|
100
|
+
lines.push(node.content.length > 500 ? node.content.slice(0, 500) + '...' : node.content);
|
|
101
|
+
lines.push('');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return textResult(lines.join('\n'));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
return errorResult(`Failed to query knowledge: ${message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Query decisions, optionally filtered by module or status.
|
|
113
|
+
*/
|
|
114
|
+
export async function handleQueryDecisions(args: QueryDecisionsArgs): Promise<ToolResult> {
|
|
115
|
+
try {
|
|
116
|
+
const kb = await getOrCreateKnowledgeBase();
|
|
117
|
+
|
|
118
|
+
let decisions: KBDecision[];
|
|
119
|
+
|
|
120
|
+
if (args.module) {
|
|
121
|
+
decisions = await kb.activeDecisionsFor(args.module);
|
|
122
|
+
if (args.status) {
|
|
123
|
+
decisions = decisions.filter(d => d.status === args.status);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
const nodes = await kb.queryNodes({
|
|
127
|
+
type: 'DECISION',
|
|
128
|
+
status: args.status,
|
|
129
|
+
});
|
|
130
|
+
decisions = nodes as KBDecision[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (decisions.length === 0) {
|
|
134
|
+
return textResult('No matching decisions found.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const lines: string[] = [`Found ${decisions.length} decision(s):\n`];
|
|
138
|
+
for (const d of decisions) {
|
|
139
|
+
lines.push(`## ${d.id} [${d.status}]`);
|
|
140
|
+
if (d.applies_to?.length) lines.push(`Applies to: ${d.applies_to.join(', ')}`);
|
|
141
|
+
if (d.effective_from) lines.push(`Effective from: ${d.effective_from}`);
|
|
142
|
+
if (d.superseded_by) lines.push(`Superseded by: ${d.superseded_by}`);
|
|
143
|
+
lines.push('');
|
|
144
|
+
lines.push(d.content);
|
|
145
|
+
lines.push('');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return textResult(lines.join('\n'));
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
return errorResult(`Failed to query decisions: ${message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Supersede an existing fact with a new version.
|
|
157
|
+
*/
|
|
158
|
+
export async function handleSupersedeFact(args: SupersedeFactArgs): Promise<ToolResult> {
|
|
159
|
+
try {
|
|
160
|
+
const kb = await getOrCreateKnowledgeBase();
|
|
161
|
+
|
|
162
|
+
const result = await kb.supersedeFact(args.old_id, args.new_content, args.new_slug);
|
|
163
|
+
|
|
164
|
+
return textResult(
|
|
165
|
+
`Superseded fact:\n` +
|
|
166
|
+
`Old: ${result.old.id} (now has superseded_by: ${result.old.superseded_by})\n` +
|
|
167
|
+
`New: ${result.new.id}\n` +
|
|
168
|
+
`File: ${result.new.filePath}`
|
|
169
|
+
);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
172
|
+
return errorResult(`Failed to supersede fact: ${message}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get knowledge base statistics.
|
|
178
|
+
*/
|
|
179
|
+
export async function handleGetKnowledgeStats(): Promise<ToolResult> {
|
|
180
|
+
try {
|
|
181
|
+
const kb = await getOrCreateKnowledgeBase();
|
|
182
|
+
const stats = await kb.getStats();
|
|
183
|
+
|
|
184
|
+
const lines: string[] = [
|
|
185
|
+
`## Knowledge Base Stats\n`,
|
|
186
|
+
`Total nodes: ${stats.totalNodes}`,
|
|
187
|
+
`Total edges: ${stats.totalEdges}`,
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
if (Object.keys(stats.byType).length > 0) {
|
|
191
|
+
lines.push('\n### By Type');
|
|
192
|
+
for (const [type, count] of Object.entries(stats.byType)) {
|
|
193
|
+
lines.push(`- ${type}: ${count}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (Object.keys(stats.byLifecycle).length > 0) {
|
|
198
|
+
lines.push('\n### By Lifecycle');
|
|
199
|
+
for (const [lifecycle, count] of Object.entries(stats.byLifecycle)) {
|
|
200
|
+
lines.push(`- ${lifecycle}: ${count}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (Object.keys(stats.edgesByType).length > 0) {
|
|
205
|
+
lines.push('\n### Edges by Type');
|
|
206
|
+
for (const [type, count] of Object.entries(stats.edgesByType)) {
|
|
207
|
+
lines.push(`- ${type}: ${count}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (stats.danglingRefs.length > 0) {
|
|
212
|
+
lines.push(`\n### Dangling KB References (${stats.danglingRefs.length})`);
|
|
213
|
+
for (const ref of stats.danglingRefs.slice(0, 10)) {
|
|
214
|
+
lines.push(`- ${ref}`);
|
|
215
|
+
}
|
|
216
|
+
if (stats.danglingRefs.length > 10) {
|
|
217
|
+
lines.push(`... and ${stats.danglingRefs.length - 10} more`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (stats.danglingCodeRefs.length > 0) {
|
|
222
|
+
lines.push(`\n### Dangling Code References (${stats.danglingCodeRefs.length})`);
|
|
223
|
+
for (const ref of stats.danglingCodeRefs.slice(0, 10)) {
|
|
224
|
+
lines.push(`- ${ref.nodeId} ā ${ref.address}`);
|
|
225
|
+
}
|
|
226
|
+
if (stats.danglingCodeRefs.length > 10) {
|
|
227
|
+
lines.push(`... and ${stats.danglingCodeRefs.length - 10} more`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return textResult(lines.join('\n'));
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
234
|
+
return errorResult(`Failed to get knowledge stats: ${message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Disabled: requires git-ingest (US-17). See US-17 in AI-AGENT-STORIES.md
|
|
239
|
+
// export async function handleGitChurn(args: GitChurnArgs): Promise<ToolResult> { ... }
|
|
240
|
+
// export async function handleGitCoChange(args: GitCoChangeArgs): Promise<ToolResult> { ... }
|
|
241
|
+
// export async function handleGitOwnership(args: GitOwnershipArgs): Promise<ToolResult> { ... }
|
|
242
|
+
// export async function handleGitArchaeology(args: GitArchaeologyArgs): Promise<ToolResult> { ... }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Notation Handlers ā describe tool
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ensureAnalyzed } from '../analysis.js';
|
|
6
|
+
import { renderNotation, extractSubgraph, PERSPECTIVES, generateLegend, isGrafemaUri, toCompactSemanticId } from '@grafema/util';
|
|
7
|
+
import type { DescribeOptions } from '@grafema/util';
|
|
8
|
+
import { textResult, errorResult } from '../utils.js';
|
|
9
|
+
import type { ToolResult, DescribeArgs } from '../types.js';
|
|
10
|
+
|
|
11
|
+
export async function handleDescribe(
|
|
12
|
+
args: DescribeArgs,
|
|
13
|
+
): Promise<ToolResult> {
|
|
14
|
+
const db = await ensureAnalyzed();
|
|
15
|
+
const { target, depth = 1, perspective } = args;
|
|
16
|
+
|
|
17
|
+
// Step 1: Resolve target ā node ID
|
|
18
|
+
let node = await db.getNode(target);
|
|
19
|
+
|
|
20
|
+
// If not found by semantic ID, try queryNodes for file path or name
|
|
21
|
+
if (!node) {
|
|
22
|
+
// Try as file path (MODULE node)
|
|
23
|
+
for await (const n of db.queryNodes({ file: target, type: 'MODULE' })) {
|
|
24
|
+
node = n;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!node) {
|
|
29
|
+
// Try by name (any type)
|
|
30
|
+
for await (const n of db.queryNodes({ name: target })) {
|
|
31
|
+
node = n;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!node) {
|
|
37
|
+
const displayTarget = isGrafemaUri(target) ? toCompactSemanticId(target) : target;
|
|
38
|
+
return errorResult(
|
|
39
|
+
`Target not found: "${displayTarget}"\n` +
|
|
40
|
+
`Try: semantic ID (from find_nodes), file path, or node name.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step 2: Extract subgraph
|
|
45
|
+
const subgraph = await extractSubgraph(db, node.id, depth);
|
|
46
|
+
|
|
47
|
+
// Step 3: Build options
|
|
48
|
+
const options: DescribeOptions = {
|
|
49
|
+
depth,
|
|
50
|
+
includeLocations: depth >= 2,
|
|
51
|
+
};
|
|
52
|
+
if (perspective && PERSPECTIVES[perspective]) {
|
|
53
|
+
options.archetypeFilter = PERSPECTIVES[perspective];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Step 4: Render
|
|
57
|
+
const notation = renderNotation(subgraph, options);
|
|
58
|
+
|
|
59
|
+
if (!notation.trim()) {
|
|
60
|
+
return textResult(
|
|
61
|
+
`[${node.type}] ${node.name ?? node.id}\nNo relationships found at depth=${depth}.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Append legend (generated from archetypes.ts ā single source of truth) + LOD hint
|
|
66
|
+
const depthHint = depth < 3
|
|
67
|
+
? `Showing depth=${depth}. Use depth=${depth + 1} for more detail.`
|
|
68
|
+
: `Showing depth=${depth}.`;
|
|
69
|
+
|
|
70
|
+
return textResult(`${notation}\n\n${generateLegend()}\n${depthHint}`);
|
|
71
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { getProjectPath } from '../state.js';
|
|
6
|
-
import { validateServices, validatePatterns, validateWorkspace, GRAFEMA_VERSION, getSchemaVersion } from '@grafema/
|
|
6
|
+
import { validateServices, validatePatterns, validateWorkspace, GRAFEMA_VERSION, getSchemaVersion } from '@grafema/util';
|
|
7
7
|
import { existsSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
8
|
import type { Dirent } from 'fs';
|
|
9
9
|
import { join, basename } from 'path';
|
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
guardResponseSize,
|
|
10
10
|
serializeBigInt,
|
|
11
11
|
findSimilarTypes,
|
|
12
|
+
extractQueriedTypes,
|
|
12
13
|
textResult,
|
|
13
14
|
errorResult,
|
|
14
15
|
} from '../utils.js';
|
|
16
|
+
import type { DatalogExplainResult, CypherResult } from '@grafema/types';
|
|
15
17
|
import type {
|
|
16
18
|
ToolResult,
|
|
17
19
|
QueryGraphArgs,
|
|
@@ -26,42 +28,143 @@ import type {
|
|
|
26
28
|
|
|
27
29
|
export async function handleQueryGraph(args: QueryGraphArgs): Promise<ToolResult> {
|
|
28
30
|
const db = await ensureAnalyzed();
|
|
29
|
-
const { query, limit: requestedLimit, offset: requestedOffset, format: _format, explain
|
|
31
|
+
const { query, language, limit: requestedLimit, offset: requestedOffset, format: _format, explain, count } = args;
|
|
30
32
|
|
|
31
33
|
const limit = normalizeLimit(requestedLimit);
|
|
32
34
|
const offset = Math.max(0, requestedOffset || 0);
|
|
33
35
|
|
|
34
36
|
try {
|
|
37
|
+
// Cypher query path
|
|
38
|
+
if (language === 'cypher') {
|
|
39
|
+
if (!('cypherQuery' in db)) {
|
|
40
|
+
return errorResult('Backend does not support Cypher queries');
|
|
41
|
+
}
|
|
42
|
+
const cypherFn = (db as unknown as { cypherQuery: (q: string) => Promise<CypherResult> }).cypherQuery;
|
|
43
|
+
const result = await cypherFn.call(db, query);
|
|
44
|
+
|
|
45
|
+
if (count) {
|
|
46
|
+
return textResult(`Count: ${result.rowCount}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (result.rowCount === 0) {
|
|
50
|
+
return textResult('Query returned no results.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const paginatedRows = result.rows.slice(offset, offset + limit);
|
|
54
|
+
const hasMore = offset + limit < result.rowCount;
|
|
55
|
+
|
|
56
|
+
const paginationInfo = formatPaginationInfo({
|
|
57
|
+
limit,
|
|
58
|
+
offset,
|
|
59
|
+
returned: paginatedRows.length,
|
|
60
|
+
total: result.rowCount,
|
|
61
|
+
hasMore,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Format as table
|
|
65
|
+
const lines: string[] = [];
|
|
66
|
+
lines.push(`Found ${result.rowCount} row(s):${paginationInfo}`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
|
|
69
|
+
// Column widths
|
|
70
|
+
const colWidths = result.columns.map((col, i) => {
|
|
71
|
+
let maxWidth = col.length;
|
|
72
|
+
for (const row of paginatedRows) {
|
|
73
|
+
const cellLen = String(row[i] ?? '').length;
|
|
74
|
+
if (cellLen > maxWidth) maxWidth = cellLen;
|
|
75
|
+
}
|
|
76
|
+
return Math.min(maxWidth, 60);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
lines.push(result.columns.map((col, i) => col.padEnd(colWidths[i])).join(' '));
|
|
80
|
+
lines.push(colWidths.map(w => '-'.repeat(w)).join(' '));
|
|
81
|
+
|
|
82
|
+
for (const row of paginatedRows) {
|
|
83
|
+
const line = row.map((cell, i) => {
|
|
84
|
+
const s = String(cell ?? '');
|
|
85
|
+
return s.length > colWidths[i] ? s.slice(0, colWidths[i] - 1) + '\u2026' : s.padEnd(colWidths[i]);
|
|
86
|
+
}).join(' ');
|
|
87
|
+
lines.push(line);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return textResult(guardResponseSize(lines.join('\n')));
|
|
91
|
+
}
|
|
92
|
+
|
|
35
93
|
// Check if backend supports Datalog queries
|
|
36
94
|
if (!('checkGuarantee' in db)) {
|
|
37
95
|
return errorResult('Backend does not support Datalog queries');
|
|
38
96
|
}
|
|
39
97
|
|
|
98
|
+
// Explain mode ā separate path with step-by-step trace
|
|
99
|
+
if (explain) {
|
|
100
|
+
const checkFn = (db as unknown as { checkGuarantee: (q: string, explain: true) => Promise<DatalogExplainResult> }).checkGuarantee;
|
|
101
|
+
const result = await checkFn.call(db, query, true);
|
|
102
|
+
return textResult(guardResponseSize(formatExplainOutput(result)));
|
|
103
|
+
}
|
|
104
|
+
|
|
40
105
|
const checkFn = (db as unknown as { checkGuarantee: (q: string) => Promise<Array<{ bindings: Array<{ name: string; value: string }> }>> }).checkGuarantee;
|
|
41
|
-
const results = await checkFn(query);
|
|
106
|
+
const results = await checkFn.call(db, query);
|
|
42
107
|
const total = results.length;
|
|
43
108
|
|
|
109
|
+
if (count) {
|
|
110
|
+
return textResult(`Count: ${total}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
44
113
|
if (total === 0) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
114
|
+
const { nodeTypes, edgeTypes } = extractQueriedTypes(query);
|
|
115
|
+
const hasQueriedTypes = nodeTypes.length > 0 || edgeTypes.length > 0;
|
|
47
116
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
117
|
+
const nodeCounts = await db.countNodesByType();
|
|
118
|
+
const edgeCounts = edgeTypes.length > 0 ? await db.countEdgesByType() : {};
|
|
119
|
+
const availableNodeTypes = Object.keys(nodeCounts);
|
|
120
|
+
const availableEdgeTypes = Object.keys(edgeCounts);
|
|
50
121
|
|
|
51
122
|
let hint = '';
|
|
52
|
-
if (
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
123
|
+
if (hasQueriedTypes) {
|
|
124
|
+
const hintLines: string[] = [];
|
|
125
|
+
|
|
126
|
+
if (nodeTypes.length > 0 && availableNodeTypes.length === 0) {
|
|
127
|
+
hintLines.push('Graph has no nodes');
|
|
128
|
+
} else {
|
|
129
|
+
for (const queriedType of nodeTypes) {
|
|
130
|
+
if (!nodeCounts[queriedType]) {
|
|
131
|
+
const similar = findSimilarTypes(queriedType, availableNodeTypes);
|
|
132
|
+
if (similar.length > 0) {
|
|
133
|
+
hintLines.push(`Did you mean: ${similar.join(', ')}? (node type)`);
|
|
134
|
+
} else {
|
|
135
|
+
const typeList = availableNodeTypes.slice(0, 10).join(', ');
|
|
136
|
+
const more = availableNodeTypes.length > 10 ? '...' : '';
|
|
137
|
+
hintLines.push(`Available node types: ${typeList}${more}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (edgeTypes.length > 0 && availableEdgeTypes.length === 0) {
|
|
144
|
+
hintLines.push('Graph has no edges');
|
|
57
145
|
} else {
|
|
58
|
-
|
|
146
|
+
for (const queriedType of edgeTypes) {
|
|
147
|
+
if (!edgeCounts[queriedType]) {
|
|
148
|
+
const similar = findSimilarTypes(queriedType, availableEdgeTypes);
|
|
149
|
+
if (similar.length > 0) {
|
|
150
|
+
hintLines.push(`Did you mean: ${similar.join(', ')}? (edge type)`);
|
|
151
|
+
} else {
|
|
152
|
+
const typeList = availableEdgeTypes.slice(0, 10).join(', ');
|
|
153
|
+
const more = availableEdgeTypes.length > 10 ? '...' : '';
|
|
154
|
+
hintLines.push(`Available edge types: ${typeList}${more}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (hintLines.length > 0) {
|
|
161
|
+
hint = '\n' + hintLines.map(l => `Hint: ${l}`).join('\n');
|
|
59
162
|
}
|
|
60
163
|
}
|
|
61
164
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
165
|
+
const totalNodes = Object.values(nodeCounts).reduce((a, b) => a + b, 0);
|
|
166
|
+
|
|
167
|
+
return textResult(`Query returned no results.${hint}\nGraph: ${totalNodes.toLocaleString()} nodes`);
|
|
65
168
|
}
|
|
66
169
|
|
|
67
170
|
const paginatedResults = results.slice(offset, offset + limit);
|
|
@@ -69,16 +172,23 @@ export async function handleQueryGraph(args: QueryGraphArgs): Promise<ToolResult
|
|
|
69
172
|
|
|
70
173
|
const enrichedResults: unknown[] = [];
|
|
71
174
|
for (const result of paginatedResults) {
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
const node = await db.getNode(
|
|
175
|
+
const xBinding = result.bindings?.find((b: DatalogBinding) => b.name === 'X');
|
|
176
|
+
if (xBinding) {
|
|
177
|
+
const node = await db.getNode(xBinding.value);
|
|
75
178
|
if (node) {
|
|
76
179
|
enrichedResults.push({
|
|
77
180
|
...node,
|
|
78
|
-
id:
|
|
181
|
+
id: xBinding.value,
|
|
79
182
|
file: node.file,
|
|
80
183
|
line: node.line,
|
|
81
184
|
});
|
|
185
|
+
} else {
|
|
186
|
+
// Non-node-ID binding (e.g. attr() string value) ā return raw bindings map
|
|
187
|
+
const bindingsMap: Record<string, string> = {};
|
|
188
|
+
for (const b of result.bindings!) {
|
|
189
|
+
bindingsMap[b.name] = b.value;
|
|
190
|
+
}
|
|
191
|
+
enrichedResults.push(bindingsMap);
|
|
82
192
|
}
|
|
83
193
|
}
|
|
84
194
|
}
|
|
@@ -106,7 +216,7 @@ export async function handleQueryGraph(args: QueryGraphArgs): Promise<ToolResult
|
|
|
106
216
|
|
|
107
217
|
export async function handleFindCalls(args: FindCallsArgs): Promise<ToolResult> {
|
|
108
218
|
const db = await ensureAnalyzed();
|
|
109
|
-
const {
|
|
219
|
+
const { name, limit: requestedLimit, offset: requestedOffset, className } = args;
|
|
110
220
|
|
|
111
221
|
const limit = normalizeLimit(requestedLimit);
|
|
112
222
|
const offset = Math.max(0, requestedOffset || 0);
|
|
@@ -116,8 +226,15 @@ export async function handleFindCalls(args: FindCallsArgs): Promise<ToolResult>
|
|
|
116
226
|
let totalMatched = 0;
|
|
117
227
|
|
|
118
228
|
for await (const node of db.queryNodes({ type: 'CALL' })) {
|
|
119
|
-
|
|
120
|
-
|
|
229
|
+
const nodeName = node.name ?? '';
|
|
230
|
+
// Method calls are stored as "receiver.method" (e.g., "kb.queryNodes", "<obj>.load").
|
|
231
|
+
// Extract the method part so find_calls("queryNodes") matches "kb.queryNodes".
|
|
232
|
+
const dotIdx = nodeName.lastIndexOf('.');
|
|
233
|
+
const methodPart = dotIdx >= 0 ? nodeName.slice(dotIdx + 1) : null;
|
|
234
|
+
const receiverPart = dotIdx >= 0 ? nodeName.slice(0, dotIdx) : null;
|
|
235
|
+
|
|
236
|
+
if (nodeName !== name && methodPart !== name) continue;
|
|
237
|
+
if (className && receiverPart !== className) continue;
|
|
121
238
|
|
|
122
239
|
totalMatched++;
|
|
123
240
|
|
|
@@ -180,6 +297,52 @@ export async function handleFindCalls(args: FindCallsArgs): Promise<ToolResult>
|
|
|
180
297
|
return textResult(guardResponseSize(responseText));
|
|
181
298
|
}
|
|
182
299
|
|
|
300
|
+
function formatExplainOutput(result: DatalogExplainResult): string {
|
|
301
|
+
const lines: string[] = [];
|
|
302
|
+
|
|
303
|
+
lines.push(`Query returned ${result.bindings.length} result(s).\n`);
|
|
304
|
+
|
|
305
|
+
if (result.explainSteps.length > 0) {
|
|
306
|
+
lines.push('Step-by-step execution:');
|
|
307
|
+
const maxSteps = 50;
|
|
308
|
+
const stepsToShow = result.explainSteps.slice(0, maxSteps);
|
|
309
|
+
for (const step of stepsToShow) {
|
|
310
|
+
const args = step.args.join(', ');
|
|
311
|
+
lines.push(` ${step.step}. [${step.operation}] ${step.predicate}(${args}) \u2192 ${step.resultCount} result(s) (${step.durationUs} \u00b5s)`);
|
|
312
|
+
if (step.details) {
|
|
313
|
+
lines.push(` ${step.details}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (result.explainSteps.length > maxSteps) {
|
|
317
|
+
lines.push(` ... ${result.explainSteps.length - maxSteps} more steps`);
|
|
318
|
+
}
|
|
319
|
+
lines.push('');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
lines.push('Statistics:');
|
|
323
|
+
lines.push(` Nodes visited: ${result.stats.nodesVisited}`);
|
|
324
|
+
lines.push(` Edges traversed: ${result.stats.edgesTraversed}`);
|
|
325
|
+
lines.push(` Rule evaluations: ${result.stats.ruleEvaluations}`);
|
|
326
|
+
lines.push(` Total results: ${result.stats.totalResults}`);
|
|
327
|
+
lines.push(` Duration: ${result.profile.totalDurationUs} \u00b5s`);
|
|
328
|
+
lines.push('');
|
|
329
|
+
|
|
330
|
+
if (result.bindings.length > 0) {
|
|
331
|
+
lines.push('Bindings:');
|
|
332
|
+
const maxBindings = 20;
|
|
333
|
+
const bindingsToShow = result.bindings.slice(0, maxBindings);
|
|
334
|
+
for (const row of bindingsToShow) {
|
|
335
|
+
const pairs = Object.entries(row).map(([k, v]) => `${k}=${v}`).join(', ');
|
|
336
|
+
lines.push(` { ${pairs} }`);
|
|
337
|
+
}
|
|
338
|
+
if (result.bindings.length > maxBindings) {
|
|
339
|
+
lines.push(` ... ${result.bindings.length - maxBindings} more results`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return lines.join('\n');
|
|
344
|
+
}
|
|
345
|
+
|
|
183
346
|
export async function handleFindNodes(args: FindNodesArgs): Promise<ToolResult> {
|
|
184
347
|
const db = await ensureAnalyzed();
|
|
185
348
|
const { type, name, file, limit: requestedLimit, offset: requestedOffset } = args;
|
|
@@ -191,6 +354,7 @@ export async function handleFindNodes(args: FindNodesArgs): Promise<ToolResult>
|
|
|
191
354
|
if (type) filter.type = type;
|
|
192
355
|
if (name) filter.name = name;
|
|
193
356
|
if (file) filter.file = file;
|
|
357
|
+
filter.substringMatch = true;
|
|
194
358
|
|
|
195
359
|
const nodes: GraphNode[] = [];
|
|
196
360
|
let skipped = 0;
|
package/src/prompts.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* MCP Prompts handler logic.
|
|
3
3
|
* Extracted for testability ā server.ts is a thin wrapper.
|
|
4
4
|
*/
|
|
5
|
-
import { getOnboardingInstruction } from '@grafema/
|
|
5
|
+
import { getOnboardingInstruction } from '@grafema/util';
|
|
6
6
|
|
|
7
7
|
export interface PromptDefinition {
|
|
8
8
|
name: string;
|