@grafema/mcp 0.2.0-beta → 0.2.5-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.js +9 -5
- package/dist/analysis-worker.js.map +1 -0
- package/dist/analysis.d.ts.map +1 -1
- package/dist/analysis.js +1 -0
- package/dist/analysis.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -5
- package/dist/config.js.map +1 -0
- package/dist/definitions.d.ts +4 -4
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +89 -3
- package/dist/definitions.js.map +1 -0
- package/dist/handlers.d.ts +4 -2
- package/dist/handlers.d.ts.map +1 -1
- package/dist/handlers.js +172 -20
- package/dist/handlers.js.map +1 -0
- package/dist/prompts.d.ts +25 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +33 -0
- package/dist/prompts.js.map +1 -0
- package/dist/server.js +46 -20
- package/dist/server.js.map +1 -0
- package/dist/state.js +1 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +50 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.js +2 -1
- package/dist/utils.js.map +1 -0
- package/package.json +4 -3
- package/src/analysis-worker.ts +10 -7
- package/src/analysis.ts +2 -1
- package/src/config.ts +12 -4
- package/src/definitions.ts +92 -5
- package/src/handlers.ts +219 -26
- package/src/prompts.ts +56 -0
- package/src/server.ts +74 -20
- package/src/types.ts +53 -1
- package/src/utils.ts +2 -2
package/src/handlers.ts
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* MCP Tool Handlers
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { join } from 'path';
|
|
6
5
|
import { ensureAnalyzed } from './analysis.js';
|
|
7
6
|
import { getProjectPath, getAnalysisStatus, getOrCreateBackend, getGuaranteeManager, getGuaranteeAPI, isAnalysisRunning } from './state.js';
|
|
8
|
-
import { CoverageAnalyzer, findCallsInFunction, findContainingFunction } from '@grafema/core';
|
|
7
|
+
import { CoverageAnalyzer, findCallsInFunction, findContainingFunction, validateServices, validatePatterns, validateWorkspace, getOnboardingInstruction } from '@grafema/core';
|
|
9
8
|
import type { CallInfo, CallerInfo } from '@grafema/core';
|
|
9
|
+
import { existsSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'fs';
|
|
10
|
+
import type { Dirent } from 'fs';
|
|
11
|
+
import { join, basename } from 'path';
|
|
12
|
+
import { stringify as stringifyYAML } from 'yaml';
|
|
10
13
|
import {
|
|
11
14
|
normalizeLimit,
|
|
12
15
|
formatPaginationInfo,
|
|
@@ -34,8 +37,12 @@ import type {
|
|
|
34
37
|
FindGuardsArgs,
|
|
35
38
|
GuardInfo,
|
|
36
39
|
GetFunctionDetailsArgs,
|
|
37
|
-
GraphBackend,
|
|
38
40
|
GraphNode,
|
|
41
|
+
DatalogBinding,
|
|
42
|
+
CallResult,
|
|
43
|
+
ReportIssueArgs,
|
|
44
|
+
ReadProjectStructureArgs,
|
|
45
|
+
WriteConfigArgs,
|
|
39
46
|
} from './types.js';
|
|
40
47
|
import { isGuaranteeType } from '@grafema/core';
|
|
41
48
|
|
|
@@ -43,8 +50,7 @@ import { isGuaranteeType } from '@grafema/core';
|
|
|
43
50
|
|
|
44
51
|
export async function handleQueryGraph(args: QueryGraphArgs): Promise<ToolResult> {
|
|
45
52
|
const db = await ensureAnalyzed();
|
|
46
|
-
const { query, limit: requestedLimit, offset: requestedOffset, format } = args;
|
|
47
|
-
const explain = (args as any).explain;
|
|
53
|
+
const { query, limit: requestedLimit, offset: requestedOffset, format: _format, explain: _explain } = args;
|
|
48
54
|
|
|
49
55
|
const limit = normalizeLimit(requestedLimit);
|
|
50
56
|
const offset = Math.max(0, requestedOffset || 0);
|
|
@@ -87,7 +93,7 @@ export async function handleQueryGraph(args: QueryGraphArgs): Promise<ToolResult
|
|
|
87
93
|
|
|
88
94
|
const enrichedResults: unknown[] = [];
|
|
89
95
|
for (const result of paginatedResults) {
|
|
90
|
-
const nodeId = result.bindings?.find((b:
|
|
96
|
+
const nodeId = result.bindings?.find((b: DatalogBinding) => b.name === 'X')?.value;
|
|
91
97
|
if (nodeId) {
|
|
92
98
|
const node = await db.getNode(nodeId);
|
|
93
99
|
if (node) {
|
|
@@ -117,25 +123,25 @@ export async function handleQueryGraph(args: QueryGraphArgs): Promise<ToolResult
|
|
|
117
123
|
|
|
118
124
|
return textResult(guardResponseSize(responseText));
|
|
119
125
|
} catch (error) {
|
|
120
|
-
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
return errorResult(message);
|
|
121
128
|
}
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
export async function handleFindCalls(args: FindCallsArgs): Promise<ToolResult> {
|
|
125
132
|
const db = await ensureAnalyzed();
|
|
126
|
-
const { target: name, limit: requestedLimit, offset: requestedOffset } = args;
|
|
127
|
-
const className = (args as any).className;
|
|
133
|
+
const { target: name, limit: requestedLimit, offset: requestedOffset, className } = args;
|
|
128
134
|
|
|
129
135
|
const limit = normalizeLimit(requestedLimit);
|
|
130
136
|
const offset = Math.max(0, requestedOffset || 0);
|
|
131
137
|
|
|
132
|
-
const calls:
|
|
138
|
+
const calls: CallResult[] = [];
|
|
133
139
|
let skipped = 0;
|
|
134
140
|
let totalMatched = 0;
|
|
135
141
|
|
|
136
142
|
for await (const node of db.queryNodes({ type: 'CALL' })) {
|
|
137
|
-
if (
|
|
138
|
-
if (className &&
|
|
143
|
+
if (node.name !== name && node['method'] !== name) continue;
|
|
144
|
+
if (className && node['object'] !== className) continue;
|
|
139
145
|
|
|
140
146
|
totalMatched++;
|
|
141
147
|
|
|
@@ -155,7 +161,7 @@ export async function handleFindCalls(args: FindCallsArgs): Promise<ToolResult>
|
|
|
155
161
|
target = targetNode
|
|
156
162
|
? {
|
|
157
163
|
type: targetNode.type,
|
|
158
|
-
name: targetNode.name,
|
|
164
|
+
name: targetNode.name ?? '',
|
|
159
165
|
file: targetNode.file,
|
|
160
166
|
line: targetNode.line,
|
|
161
167
|
}
|
|
@@ -164,8 +170,8 @@ export async function handleFindCalls(args: FindCallsArgs): Promise<ToolResult>
|
|
|
164
170
|
|
|
165
171
|
calls.push({
|
|
166
172
|
id: node.id,
|
|
167
|
-
name:
|
|
168
|
-
object:
|
|
173
|
+
name: node.name,
|
|
174
|
+
object: node['object'] as string | undefined,
|
|
169
175
|
file: node.file,
|
|
170
176
|
line: node.line,
|
|
171
177
|
resolved: isResolved,
|
|
@@ -177,7 +183,7 @@ export async function handleFindCalls(args: FindCallsArgs): Promise<ToolResult>
|
|
|
177
183
|
return textResult(`No calls found for "${className ? className + '.' : ''}${name}"`);
|
|
178
184
|
}
|
|
179
185
|
|
|
180
|
-
const resolved = calls.filter(
|
|
186
|
+
const resolved = calls.filter(c => c.resolved).length;
|
|
181
187
|
const unresolved = calls.length - resolved;
|
|
182
188
|
const hasMore = offset + calls.length < totalMatched;
|
|
183
189
|
|
|
@@ -254,7 +260,7 @@ export async function handleFindNodes(args: FindNodesArgs): Promise<ToolResult>
|
|
|
254
260
|
export async function handleTraceAlias(args: TraceAliasArgs): Promise<ToolResult> {
|
|
255
261
|
const db = await ensureAnalyzed();
|
|
256
262
|
const { variableName, file } = args;
|
|
257
|
-
const
|
|
263
|
+
const _projectPath = getProjectPath();
|
|
258
264
|
|
|
259
265
|
let varNode: GraphNode | null = null;
|
|
260
266
|
|
|
@@ -419,7 +425,8 @@ export async function handleCheckInvariant(args: CheckInvariantArgs): Promise<To
|
|
|
419
425
|
)}${total > 20 ? `\n\n... and ${total - 20} more` : ''}`
|
|
420
426
|
);
|
|
421
427
|
} catch (error) {
|
|
422
|
-
|
|
428
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
429
|
+
return errorResult(message);
|
|
423
430
|
}
|
|
424
431
|
}
|
|
425
432
|
|
|
@@ -451,7 +458,8 @@ export async function handleAnalyzeProject(args: AnalyzeProjectArgs): Promise<To
|
|
|
451
458
|
`- Total time: ${status.timings.total || 'N/A'}s`
|
|
452
459
|
);
|
|
453
460
|
} catch (error) {
|
|
454
|
-
|
|
461
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
462
|
+
return errorResult(message);
|
|
455
463
|
}
|
|
456
464
|
}
|
|
457
465
|
|
|
@@ -576,7 +584,8 @@ export async function handleCreateGuarantee(args: CreateGuaranteeArgs): Promise<
|
|
|
576
584
|
);
|
|
577
585
|
}
|
|
578
586
|
} catch (error) {
|
|
579
|
-
|
|
587
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
588
|
+
return errorResult(`Failed to create guarantee: ${message}`);
|
|
580
589
|
}
|
|
581
590
|
}
|
|
582
591
|
|
|
@@ -622,7 +631,8 @@ export async function handleListGuarantees(): Promise<ToolResult> {
|
|
|
622
631
|
|
|
623
632
|
return textResult(results.join('\n'));
|
|
624
633
|
} catch (error) {
|
|
625
|
-
|
|
634
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
635
|
+
return errorResult(`Failed to list guarantees: ${message}`);
|
|
626
636
|
}
|
|
627
637
|
}
|
|
628
638
|
|
|
@@ -731,7 +741,8 @@ export async function handleCheckGuarantees(args: CheckGuaranteesArgs): Promise<
|
|
|
731
741
|
const summary = `\n---\nTotal: ${totalPassed + totalFailed} | ✅ Passed: ${totalPassed} | ❌ Failed: ${totalFailed}`;
|
|
732
742
|
return textResult(results.join('\n') + summary);
|
|
733
743
|
} catch (error) {
|
|
734
|
-
|
|
744
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
745
|
+
return errorResult(`Failed to check guarantees: ${message}`);
|
|
735
746
|
}
|
|
736
747
|
}
|
|
737
748
|
|
|
@@ -766,7 +777,8 @@ export async function handleDeleteGuarantee(args: DeleteGuaranteeArgs): Promise<
|
|
|
766
777
|
|
|
767
778
|
return errorResult(`Guarantee not found: ${name}`);
|
|
768
779
|
} catch (error) {
|
|
769
|
-
|
|
780
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
781
|
+
return errorResult(`Failed to delete guarantee: ${message}`);
|
|
770
782
|
}
|
|
771
783
|
}
|
|
772
784
|
|
|
@@ -807,7 +819,8 @@ export async function handleGetCoverage(args: GetCoverageArgs): Promise<ToolResu
|
|
|
807
819
|
|
|
808
820
|
return textResult(output);
|
|
809
821
|
} catch (error) {
|
|
810
|
-
|
|
822
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
823
|
+
return errorResult(`Failed to calculate coverage: ${message}`);
|
|
811
824
|
}
|
|
812
825
|
}
|
|
813
826
|
|
|
@@ -815,6 +828,7 @@ export async function handleGetDocumentation(args: GetDocumentationArgs): Promis
|
|
|
815
828
|
const { topic = 'overview' } = args;
|
|
816
829
|
|
|
817
830
|
const docs: Record<string, string> = {
|
|
831
|
+
onboarding: getOnboardingInstruction(),
|
|
818
832
|
overview: `
|
|
819
833
|
# Grafema Code Analysis
|
|
820
834
|
|
|
@@ -855,7 +869,7 @@ Find unresolved calls:
|
|
|
855
869
|
|
|
856
870
|
## Core Node Types
|
|
857
871
|
- MODULE, FUNCTION, CLASS, METHOD, VARIABLE
|
|
858
|
-
- CALL, IMPORT, EXPORT, PARAMETER
|
|
872
|
+
- CALL, PROPERTY_ACCESS, IMPORT, EXPORT, PARAMETER
|
|
859
873
|
|
|
860
874
|
## HTTP/Network
|
|
861
875
|
- http:route, http:request, db:query
|
|
@@ -1111,7 +1125,7 @@ function formatCallsForDisplay(calls: CallInfo[]): string[] {
|
|
|
1111
1125
|
|
|
1112
1126
|
// === BUG REPORTING ===
|
|
1113
1127
|
|
|
1114
|
-
export async function handleReportIssue(args:
|
|
1128
|
+
export async function handleReportIssue(args: ReportIssueArgs): Promise<ToolResult> {
|
|
1115
1129
|
const { title, description, context, labels = ['bug'] } = args;
|
|
1116
1130
|
// Use user's token if provided, otherwise fall back to project's issue-only token
|
|
1117
1131
|
const GRAFEMA_ISSUE_TOKEN = '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';
|
|
@@ -1178,3 +1192,182 @@ ${context ? `## Context\n\`\`\`\n${context}\n\`\`\`\n` : ''}
|
|
|
1178
1192
|
`---\n**Title:** ${title}\n\n${body}\n---`
|
|
1179
1193
|
);
|
|
1180
1194
|
}
|
|
1195
|
+
|
|
1196
|
+
// === PROJECT STRUCTURE (REG-173) ===
|
|
1197
|
+
|
|
1198
|
+
export async function handleReadProjectStructure(
|
|
1199
|
+
args: ReadProjectStructureArgs
|
|
1200
|
+
): Promise<ToolResult> {
|
|
1201
|
+
const projectPath = getProjectPath();
|
|
1202
|
+
const subPath = args.path || '.';
|
|
1203
|
+
const maxDepth = Math.min(Math.max(1, args.depth || 3), 5);
|
|
1204
|
+
const includeFiles = args.include_files !== false;
|
|
1205
|
+
|
|
1206
|
+
const targetPath = join(projectPath, subPath);
|
|
1207
|
+
|
|
1208
|
+
if (!existsSync(targetPath)) {
|
|
1209
|
+
return errorResult(`Path does not exist: ${subPath}`);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if (!statSync(targetPath).isDirectory()) {
|
|
1213
|
+
return errorResult(`Path is not a directory: ${subPath}`);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const EXCLUDED = new Set([
|
|
1217
|
+
'node_modules', '.git', 'dist', 'build', '.grafema',
|
|
1218
|
+
'coverage', '.next', '.nuxt', '.cache', '.output',
|
|
1219
|
+
'__pycache__', '.tox', 'target',
|
|
1220
|
+
]);
|
|
1221
|
+
|
|
1222
|
+
const lines: string[] = [];
|
|
1223
|
+
|
|
1224
|
+
function walk(dir: string, prefix: string, depth: number): void {
|
|
1225
|
+
if (depth > maxDepth) return;
|
|
1226
|
+
|
|
1227
|
+
let entries: Dirent[];
|
|
1228
|
+
try {
|
|
1229
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1230
|
+
} catch {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const dirs: string[] = [];
|
|
1235
|
+
const files: string[] = [];
|
|
1236
|
+
|
|
1237
|
+
for (const entry of entries) {
|
|
1238
|
+
if (EXCLUDED.has(entry.name)) continue;
|
|
1239
|
+
|
|
1240
|
+
if (entry.isDirectory()) {
|
|
1241
|
+
dirs.push(entry.name);
|
|
1242
|
+
} else if (includeFiles) {
|
|
1243
|
+
files.push(entry.name);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
dirs.sort();
|
|
1248
|
+
files.sort();
|
|
1249
|
+
|
|
1250
|
+
const allEntries = [
|
|
1251
|
+
...dirs.map(d => ({ name: d, isDir: true })),
|
|
1252
|
+
...files.map(f => ({ name: f, isDir: false })),
|
|
1253
|
+
];
|
|
1254
|
+
|
|
1255
|
+
for (let i = 0; i < allEntries.length; i++) {
|
|
1256
|
+
const entry = allEntries[i];
|
|
1257
|
+
const isLast = i === allEntries.length - 1;
|
|
1258
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
1259
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
1260
|
+
|
|
1261
|
+
if (entry.isDir) {
|
|
1262
|
+
lines.push(`${prefix}${connector}${entry.name}/`);
|
|
1263
|
+
walk(join(dir, entry.name), prefix + childPrefix, depth + 1);
|
|
1264
|
+
} else {
|
|
1265
|
+
lines.push(`${prefix}${connector}${entry.name}`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
lines.push(subPath === '.' ? basename(projectPath) + '/' : subPath + '/');
|
|
1271
|
+
walk(targetPath, '', 1);
|
|
1272
|
+
|
|
1273
|
+
if (lines.length === 1) {
|
|
1274
|
+
return textResult(`Directory is empty or contains only excluded entries: ${subPath}`);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return textResult(lines.join('\n'));
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// === WRITE CONFIG (REG-173) ===
|
|
1281
|
+
|
|
1282
|
+
export async function handleWriteConfig(
|
|
1283
|
+
args: WriteConfigArgs
|
|
1284
|
+
): Promise<ToolResult> {
|
|
1285
|
+
const projectPath = getProjectPath();
|
|
1286
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
1287
|
+
const configPath = join(grafemaDir, 'config.yaml');
|
|
1288
|
+
|
|
1289
|
+
try {
|
|
1290
|
+
if (args.services) {
|
|
1291
|
+
validateServices(args.services, projectPath);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (args.include !== undefined || args.exclude !== undefined) {
|
|
1295
|
+
const warnings: string[] = [];
|
|
1296
|
+
validatePatterns(args.include, args.exclude, {
|
|
1297
|
+
warn: (msg: string) => warnings.push(msg),
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (args.workspace) {
|
|
1302
|
+
validateWorkspace(args.workspace, projectPath);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const config: Record<string, unknown> = {};
|
|
1306
|
+
|
|
1307
|
+
if (args.services && args.services.length > 0) {
|
|
1308
|
+
config.services = args.services;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (args.plugins) {
|
|
1312
|
+
config.plugins = args.plugins;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (args.include) {
|
|
1316
|
+
config.include = args.include;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (args.exclude) {
|
|
1320
|
+
config.exclude = args.exclude;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (args.workspace) {
|
|
1324
|
+
config.workspace = args.workspace;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const yaml = stringifyYAML(config, { lineWidth: 0 });
|
|
1328
|
+
const content =
|
|
1329
|
+
'# Grafema Configuration\n' +
|
|
1330
|
+
'# Generated by Grafema onboarding\n' +
|
|
1331
|
+
'# Documentation: https://github.com/grafema/grafema#configuration\n\n' +
|
|
1332
|
+
yaml;
|
|
1333
|
+
|
|
1334
|
+
if (!existsSync(grafemaDir)) {
|
|
1335
|
+
mkdirSync(grafemaDir, { recursive: true });
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
writeFileSync(configPath, content);
|
|
1339
|
+
|
|
1340
|
+
const summary: string[] = ['Configuration written to .grafema/config.yaml'];
|
|
1341
|
+
|
|
1342
|
+
if (args.services && args.services.length > 0) {
|
|
1343
|
+
summary.push(`Services: ${args.services.map(s => s.name).join(', ')}`);
|
|
1344
|
+
} else {
|
|
1345
|
+
summary.push('Services: using auto-discovery (none explicitly configured)');
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (args.plugins) {
|
|
1349
|
+
summary.push('Plugins: custom configuration');
|
|
1350
|
+
} else {
|
|
1351
|
+
summary.push('Plugins: using defaults');
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (args.include) {
|
|
1355
|
+
summary.push(`Include patterns: ${args.include.join(', ')}`);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (args.exclude) {
|
|
1359
|
+
summary.push(`Exclude patterns: ${args.exclude.join(', ')}`);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if (args.workspace?.roots) {
|
|
1363
|
+
summary.push(`Workspace roots: ${args.workspace.roots.join(', ')}`);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
summary.push('\nNext step: run analyze_project to build the graph.');
|
|
1367
|
+
|
|
1368
|
+
return textResult(summary.join('\n'));
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1371
|
+
return errorResult(`Failed to write config: ${message}`);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Prompts handler logic.
|
|
3
|
+
* Extracted for testability — server.ts is a thin wrapper.
|
|
4
|
+
*/
|
|
5
|
+
import { getOnboardingInstruction } from '@grafema/core';
|
|
6
|
+
|
|
7
|
+
export interface PromptDefinition {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
arguments: Array<{ name: string; description: string; required?: boolean }>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PromptMessage {
|
|
14
|
+
role: 'user' | 'assistant';
|
|
15
|
+
content: { type: 'text'; text: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PromptResult {
|
|
19
|
+
[x: string]: unknown;
|
|
20
|
+
description: string;
|
|
21
|
+
messages: PromptMessage[];
|
|
22
|
+
_meta?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const PROMPTS: PromptDefinition[] = [
|
|
26
|
+
{
|
|
27
|
+
name: 'onboard_project',
|
|
28
|
+
description:
|
|
29
|
+
'Step-by-step instructions for studying a new project and ' +
|
|
30
|
+
'configuring Grafema for analysis. Use this when setting up ' +
|
|
31
|
+
'Grafema for the first time on a project.',
|
|
32
|
+
arguments: [],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function getPrompt(name: string): PromptResult {
|
|
37
|
+
if (name === 'onboard_project') {
|
|
38
|
+
const instruction = getOnboardingInstruction();
|
|
39
|
+
return {
|
|
40
|
+
description: PROMPTS[0].description,
|
|
41
|
+
messages: [
|
|
42
|
+
{
|
|
43
|
+
role: 'user',
|
|
44
|
+
content: {
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: instruction,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Unknown prompt: ${name}. Available prompts: ${PROMPTS.map(p => p.name).join(', ')}`
|
|
55
|
+
);
|
|
56
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -10,12 +10,15 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
10
10
|
import {
|
|
11
11
|
CallToolRequestSchema,
|
|
12
12
|
ListToolsRequestSchema,
|
|
13
|
+
ListPromptsRequestSchema,
|
|
14
|
+
GetPromptRequestSchema,
|
|
13
15
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
16
|
+
import { PROMPTS, getPrompt } from './prompts.js';
|
|
14
17
|
|
|
15
18
|
import { TOOLS } from './definitions.js';
|
|
16
19
|
import { initializeFromArgs, setupLogging, getProjectPath } from './state.js';
|
|
17
20
|
import { textResult, errorResult, log } from './utils.js';
|
|
18
|
-
import {
|
|
21
|
+
import { discoverServices } from './analysis.js';
|
|
19
22
|
import {
|
|
20
23
|
handleQueryGraph,
|
|
21
24
|
handleFindCalls,
|
|
@@ -36,8 +39,39 @@ import {
|
|
|
36
39
|
handleFindGuards,
|
|
37
40
|
handleReportIssue,
|
|
38
41
|
handleGetFunctionDetails,
|
|
42
|
+
handleReadProjectStructure,
|
|
43
|
+
handleWriteConfig,
|
|
39
44
|
} from './handlers.js';
|
|
40
|
-
import type {
|
|
45
|
+
import type {
|
|
46
|
+
ToolResult,
|
|
47
|
+
ReportIssueArgs,
|
|
48
|
+
GetDocumentationArgs,
|
|
49
|
+
GetFunctionDetailsArgs,
|
|
50
|
+
QueryGraphArgs,
|
|
51
|
+
FindCallsArgs,
|
|
52
|
+
FindNodesArgs,
|
|
53
|
+
TraceAliasArgs,
|
|
54
|
+
TraceDataFlowArgs,
|
|
55
|
+
CheckInvariantArgs,
|
|
56
|
+
AnalyzeProjectArgs,
|
|
57
|
+
GetSchemaArgs,
|
|
58
|
+
CreateGuaranteeArgs,
|
|
59
|
+
CheckGuaranteesArgs,
|
|
60
|
+
DeleteGuaranteeArgs,
|
|
61
|
+
GetCoverageArgs,
|
|
62
|
+
FindGuardsArgs,
|
|
63
|
+
ReadProjectStructureArgs,
|
|
64
|
+
WriteConfigArgs,
|
|
65
|
+
} from './types.js';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Type-safe argument casting helper.
|
|
69
|
+
* MCP SDK provides args as Record<string, unknown>, this helper
|
|
70
|
+
* casts them to the expected handler argument type.
|
|
71
|
+
*/
|
|
72
|
+
function asArgs<T>(args: Record<string, unknown> | undefined): T {
|
|
73
|
+
return (args ?? {}) as T;
|
|
74
|
+
}
|
|
41
75
|
|
|
42
76
|
// Initialize from command line args
|
|
43
77
|
initializeFromArgs();
|
|
@@ -55,6 +89,7 @@ const server = new Server(
|
|
|
55
89
|
{
|
|
56
90
|
capabilities: {
|
|
57
91
|
tools: {},
|
|
92
|
+
prompts: {},
|
|
58
93
|
},
|
|
59
94
|
}
|
|
60
95
|
);
|
|
@@ -64,6 +99,16 @@ server.setRequestHandler(ListToolsRequestSchema, async (_request, _extra) => {
|
|
|
64
99
|
return { tools: TOOLS };
|
|
65
100
|
});
|
|
66
101
|
|
|
102
|
+
// List available prompts
|
|
103
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
104
|
+
return { prompts: PROMPTS };
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Get prompt by name
|
|
108
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
109
|
+
return getPrompt(request.params.name);
|
|
110
|
+
});
|
|
111
|
+
|
|
67
112
|
// Handle tool calls
|
|
68
113
|
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
69
114
|
void extra; // suppress unused warning
|
|
@@ -78,27 +123,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
78
123
|
|
|
79
124
|
switch (name) {
|
|
80
125
|
case 'query_graph':
|
|
81
|
-
result = await handleQueryGraph(args
|
|
126
|
+
result = await handleQueryGraph(asArgs<QueryGraphArgs>(args));
|
|
82
127
|
break;
|
|
83
128
|
|
|
84
129
|
case 'find_calls':
|
|
85
|
-
result = await handleFindCalls(args
|
|
130
|
+
result = await handleFindCalls(asArgs<FindCallsArgs>(args));
|
|
86
131
|
break;
|
|
87
132
|
|
|
88
133
|
case 'find_nodes':
|
|
89
|
-
result = await handleFindNodes(args
|
|
134
|
+
result = await handleFindNodes(asArgs<FindNodesArgs>(args));
|
|
90
135
|
break;
|
|
91
136
|
|
|
92
137
|
case 'trace_alias':
|
|
93
|
-
result = await handleTraceAlias(args
|
|
138
|
+
result = await handleTraceAlias(asArgs<TraceAliasArgs>(args));
|
|
94
139
|
break;
|
|
95
140
|
|
|
96
141
|
case 'trace_dataflow':
|
|
97
|
-
result = await handleTraceDataFlow(args
|
|
142
|
+
result = await handleTraceDataFlow(asArgs<TraceDataFlowArgs>(args));
|
|
98
143
|
break;
|
|
99
144
|
|
|
100
145
|
case 'check_invariant':
|
|
101
|
-
result = await handleCheckInvariant(args
|
|
146
|
+
result = await handleCheckInvariant(asArgs<CheckInvariantArgs>(args));
|
|
102
147
|
break;
|
|
103
148
|
|
|
104
149
|
case 'discover_services':
|
|
@@ -107,7 +152,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
107
152
|
break;
|
|
108
153
|
|
|
109
154
|
case 'analyze_project':
|
|
110
|
-
result = await handleAnalyzeProject(args
|
|
155
|
+
result = await handleAnalyzeProject(asArgs<AnalyzeProjectArgs>(args));
|
|
111
156
|
break;
|
|
112
157
|
|
|
113
158
|
case 'get_analysis_status':
|
|
@@ -119,11 +164,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
119
164
|
break;
|
|
120
165
|
|
|
121
166
|
case 'get_schema':
|
|
122
|
-
result = await handleGetSchema(args
|
|
167
|
+
result = await handleGetSchema(asArgs<GetSchemaArgs>(args));
|
|
123
168
|
break;
|
|
124
169
|
|
|
125
170
|
case 'create_guarantee':
|
|
126
|
-
result = await handleCreateGuarantee(args
|
|
171
|
+
result = await handleCreateGuarantee(asArgs<CreateGuaranteeArgs>(args));
|
|
127
172
|
break;
|
|
128
173
|
|
|
129
174
|
case 'list_guarantees':
|
|
@@ -131,31 +176,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
131
176
|
break;
|
|
132
177
|
|
|
133
178
|
case 'check_guarantees':
|
|
134
|
-
result = await handleCheckGuarantees(args
|
|
179
|
+
result = await handleCheckGuarantees(asArgs<CheckGuaranteesArgs>(args));
|
|
135
180
|
break;
|
|
136
181
|
|
|
137
182
|
case 'delete_guarantee':
|
|
138
|
-
result = await handleDeleteGuarantee(args
|
|
183
|
+
result = await handleDeleteGuarantee(asArgs<DeleteGuaranteeArgs>(args));
|
|
139
184
|
break;
|
|
140
185
|
|
|
141
186
|
case 'get_coverage':
|
|
142
|
-
result = await handleGetCoverage(args
|
|
187
|
+
result = await handleGetCoverage(asArgs<GetCoverageArgs>(args));
|
|
143
188
|
break;
|
|
144
189
|
|
|
145
190
|
case 'get_documentation':
|
|
146
|
-
result = await handleGetDocumentation(args
|
|
191
|
+
result = await handleGetDocumentation(asArgs<GetDocumentationArgs>(args));
|
|
147
192
|
break;
|
|
148
193
|
|
|
149
194
|
case 'find_guards':
|
|
150
|
-
result = await handleFindGuards(args
|
|
195
|
+
result = await handleFindGuards(asArgs<FindGuardsArgs>(args));
|
|
151
196
|
break;
|
|
152
197
|
|
|
153
198
|
case 'report_issue':
|
|
154
|
-
result = await handleReportIssue(args
|
|
199
|
+
result = await handleReportIssue(asArgs<ReportIssueArgs>(args));
|
|
155
200
|
break;
|
|
156
201
|
|
|
157
202
|
case 'get_function_details':
|
|
158
|
-
result = await handleGetFunctionDetails(args
|
|
203
|
+
result = await handleGetFunctionDetails(asArgs<GetFunctionDetailsArgs>(args));
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case 'read_project_structure':
|
|
207
|
+
result = await handleReadProjectStructure(asArgs<ReadProjectStructureArgs>(args));
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
case 'write_config':
|
|
211
|
+
result = await handleWriteConfig(asArgs<WriteConfigArgs>(args));
|
|
159
212
|
break;
|
|
160
213
|
|
|
161
214
|
default:
|
|
@@ -170,8 +223,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
170
223
|
return result;
|
|
171
224
|
} catch (error) {
|
|
172
225
|
const duration = Date.now() - startTime;
|
|
173
|
-
|
|
174
|
-
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
227
|
+
log(`[Grafema MCP] ✗ ${name} FAILED after ${duration}ms: ${message}`);
|
|
228
|
+
return errorResult(message);
|
|
175
229
|
}
|
|
176
230
|
});
|
|
177
231
|
|