@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
@@ -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
- // Use user's token if provided, otherwise fall back to project's issue-only token
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 automatically created via Grafema MCP server.*`;
29
+ *This issue was created via Grafema MCP server.*`;
33
30
 
34
- // Try GitHub API if token is available
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
- `āœ… Issue created successfully!\n\n` +
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
- // Fallback: return template for manual submission
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
- `āš ļø Failed to create issue automatically. Please create it manually:\n\n` +
77
- `**Quick link** (may truncate long descriptions):\n${directUrl}\n\n` +
78
- `**Or copy this template to** ${issueUrl}:\n\n` +
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/core';
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: _explain } = args;
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 nodeCounts = await db.countNodesByType();
46
- const totalNodes = Object.values(nodeCounts).reduce((a, b) => a + b, 0);
114
+ const { nodeTypes, edgeTypes } = extractQueriedTypes(query);
115
+ const hasQueriedTypes = nodeTypes.length > 0 || edgeTypes.length > 0;
47
116
 
48
- const typeMatch = query.match(/node\([^,]+,\s*"([^"]+)"\)/);
49
- const queriedType = typeMatch ? typeMatch[1] : null;
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 (queriedType && !nodeCounts[queriedType]) {
53
- const availableTypes = Object.keys(nodeCounts);
54
- const similar = findSimilarTypes(queriedType, availableTypes);
55
- if (similar.length > 0) {
56
- hint = `\nšŸ’” Did you mean: ${similar.join(', ')}?`;
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
- hint = `\nšŸ’” Available types: ${availableTypes.slice(0, 10).join(', ')}${availableTypes.length > 10 ? '...' : ''}`;
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
- return textResult(
63
- `Query returned no results.${hint}\nšŸ“Š Graph: ${totalNodes.toLocaleString()} nodes`
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 nodeId = result.bindings?.find((b: DatalogBinding) => b.name === 'X')?.value;
73
- if (nodeId) {
74
- const node = await db.getNode(nodeId);
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: nodeId,
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 { target: name, limit: requestedLimit, offset: requestedOffset, className } = args;
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
- if (node.name !== name && node['method'] !== name) continue;
120
- if (className && node['object'] !== className) continue;
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/core';
5
+ import { getOnboardingInstruction } from '@grafema/util';
6
6
 
7
7
  export interface PromptDefinition {
8
8
  name: string;