@grafema/mcp 0.1.0-alpha.1

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/src/server.ts ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Grafema MCP Server
4
+ *
5
+ * Provides code analysis tools via Model Context Protocol.
6
+ */
7
+
8
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import {
11
+ CallToolRequestSchema,
12
+ ListToolsRequestSchema,
13
+ } from '@modelcontextprotocol/sdk/types.js';
14
+
15
+ import { TOOLS } from './definitions.js';
16
+ import { initializeFromArgs, setupLogging, getProjectPath } from './state.js';
17
+ import { textResult, errorResult, log } from './utils.js';
18
+ import { ensureAnalyzed, discoverServices } from './analysis.js';
19
+ import {
20
+ handleQueryGraph,
21
+ handleFindCalls,
22
+ handleFindNodes,
23
+ handleTraceAlias,
24
+ handleTraceDataFlow,
25
+ handleCheckInvariant,
26
+ handleAnalyzeProject,
27
+ handleGetAnalysisStatus,
28
+ handleGetStats,
29
+ handleGetSchema,
30
+ handleCreateGuarantee,
31
+ handleListGuarantees,
32
+ handleCheckGuarantees,
33
+ handleDeleteGuarantee,
34
+ handleGetCoverage,
35
+ handleGetDocumentation,
36
+ handleReportIssue,
37
+ } from './handlers.js';
38
+ import type { ToolResult, ReportIssueArgs, GetDocumentationArgs } from './types.js';
39
+
40
+ // Initialize from command line args
41
+ initializeFromArgs();
42
+ setupLogging();
43
+
44
+ const projectPath = getProjectPath();
45
+ log(`[Grafema MCP] Starting server for project: ${projectPath}`);
46
+
47
+ // Create MCP server
48
+ const server = new Server(
49
+ {
50
+ name: 'grafema-mcp',
51
+ version: '0.1.0',
52
+ },
53
+ {
54
+ capabilities: {
55
+ tools: {},
56
+ },
57
+ }
58
+ );
59
+
60
+ // List available tools
61
+ server.setRequestHandler(ListToolsRequestSchema, async (_request, _extra) => {
62
+ return { tools: TOOLS };
63
+ });
64
+
65
+ // Handle tool calls
66
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
67
+ void extra; // suppress unused warning
68
+ const { name, arguments: args } = request.params;
69
+
70
+ const startTime = Date.now();
71
+ const argsPreview = args ? JSON.stringify(args).slice(0, 200) : '{}';
72
+ log(`[Grafema MCP] ▶ ${name} args=${argsPreview}`);
73
+
74
+ try {
75
+ let result: ToolResult;
76
+
77
+ switch (name) {
78
+ case 'query_graph':
79
+ result = await handleQueryGraph(args as any);
80
+ break;
81
+
82
+ case 'find_calls':
83
+ result = await handleFindCalls(args as any);
84
+ break;
85
+
86
+ case 'find_nodes':
87
+ result = await handleFindNodes(args as any);
88
+ break;
89
+
90
+ case 'trace_alias':
91
+ result = await handleTraceAlias(args as any);
92
+ break;
93
+
94
+ case 'trace_dataflow':
95
+ result = await handleTraceDataFlow(args as any);
96
+ break;
97
+
98
+ case 'check_invariant':
99
+ result = await handleCheckInvariant(args as any);
100
+ break;
101
+
102
+ case 'discover_services':
103
+ const services = await discoverServices();
104
+ result = textResult(`Found ${services.length} service(s):\n${JSON.stringify(services, null, 2)}`);
105
+ break;
106
+
107
+ case 'analyze_project':
108
+ result = await handleAnalyzeProject(args as any);
109
+ break;
110
+
111
+ case 'get_analysis_status':
112
+ result = await handleGetAnalysisStatus();
113
+ break;
114
+
115
+ case 'get_stats':
116
+ result = await handleGetStats();
117
+ break;
118
+
119
+ case 'get_schema':
120
+ result = await handleGetSchema(args as any);
121
+ break;
122
+
123
+ case 'create_guarantee':
124
+ result = await handleCreateGuarantee(args as any);
125
+ break;
126
+
127
+ case 'list_guarantees':
128
+ result = await handleListGuarantees();
129
+ break;
130
+
131
+ case 'check_guarantees':
132
+ result = await handleCheckGuarantees(args as any);
133
+ break;
134
+
135
+ case 'delete_guarantee':
136
+ result = await handleDeleteGuarantee(args as any);
137
+ break;
138
+
139
+ case 'get_coverage':
140
+ result = await handleGetCoverage(args as any);
141
+ break;
142
+
143
+ case 'get_documentation':
144
+ result = await handleGetDocumentation(args as GetDocumentationArgs);
145
+ break;
146
+
147
+ case 'report_issue':
148
+ result = await handleReportIssue(args as unknown as ReportIssueArgs);
149
+ break;
150
+
151
+ default:
152
+ result = errorResult(`Unknown tool: ${name}`);
153
+ }
154
+
155
+ const duration = Date.now() - startTime;
156
+ const resultSize = JSON.stringify(result).length;
157
+ const status = result.isError ? '✗' : '✓';
158
+ log(`[Grafema MCP] ${status} ${name} completed in ${duration}ms (${resultSize} bytes)`);
159
+
160
+ return result;
161
+ } catch (error) {
162
+ const duration = Date.now() - startTime;
163
+ log(`[Grafema MCP] ✗ ${name} FAILED after ${duration}ms: ${(error as Error).message}`);
164
+ return errorResult((error as Error).message);
165
+ }
166
+ });
167
+
168
+ // Main entry point
169
+ async function main(): Promise<void> {
170
+ const transport = new StdioServerTransport();
171
+ await server.connect(transport);
172
+ log('[Grafema MCP] Server connected via stdio');
173
+ }
174
+
175
+ main().catch((error) => {
176
+ log(`[Grafema MCP] Fatal error: ${error.message}`);
177
+ process.exit(1);
178
+ });
package/src/state.ts ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * MCP Server State Management
3
+ */
4
+
5
+ import { join } from 'path';
6
+ import { existsSync, mkdirSync } from 'fs';
7
+ import { RFDBServerBackend, GuaranteeManager, GuaranteeAPI } from '@grafema/core';
8
+ import type { GuaranteeGraphBackend, GuaranteeGraph } from '@grafema/core';
9
+ import { loadConfig } from './config.js';
10
+ import { log, initLogger } from './utils.js';
11
+ import type { AnalysisStatus } from './types.js';
12
+ import type { GraphBackend } from '@grafema/types';
13
+
14
+ // === GLOBAL STATE ===
15
+ let projectPath: string = process.cwd();
16
+ let backend: GraphBackend | null = null;
17
+ let isAnalyzed: boolean = false;
18
+ let backgroundPid: number | null = null;
19
+
20
+ // Guarantee managers
21
+ let guaranteeManager: GuaranteeManager | null = null;
22
+ let guaranteeAPI: GuaranteeAPI | null = null;
23
+
24
+ let analysisStatus: AnalysisStatus = {
25
+ running: false,
26
+ phase: null,
27
+ message: null,
28
+ servicesDiscovered: 0,
29
+ servicesAnalyzed: 0,
30
+ startTime: null,
31
+ endTime: null,
32
+ error: null,
33
+ timings: {
34
+ discovery: null,
35
+ indexing: null,
36
+ analysis: null,
37
+ enrichment: null,
38
+ validation: null,
39
+ total: null,
40
+ },
41
+ };
42
+
43
+ // === GETTERS ===
44
+ export function getProjectPath(): string {
45
+ return projectPath;
46
+ }
47
+
48
+ export function getIsAnalyzed(): boolean {
49
+ return isAnalyzed;
50
+ }
51
+
52
+ export function getAnalysisStatus(): AnalysisStatus {
53
+ return analysisStatus;
54
+ }
55
+
56
+ export function getBackgroundPid(): number | null {
57
+ return backgroundPid;
58
+ }
59
+
60
+ export function getGuaranteeManager(): GuaranteeManager | null {
61
+ return guaranteeManager;
62
+ }
63
+
64
+ export function getGuaranteeAPI(): GuaranteeAPI | null {
65
+ return guaranteeAPI;
66
+ }
67
+
68
+ // === SETTERS ===
69
+ export function setProjectPath(path: string): void {
70
+ projectPath = path;
71
+ }
72
+
73
+ export function setIsAnalyzed(value: boolean): void {
74
+ isAnalyzed = value;
75
+ }
76
+
77
+ export function setAnalysisStatus(status: Partial<AnalysisStatus>): void {
78
+ analysisStatus = { ...analysisStatus, ...status };
79
+ }
80
+
81
+ export function setBackgroundPid(pid: number | null): void {
82
+ backgroundPid = pid;
83
+ }
84
+
85
+ export function updateAnalysisTimings(timings: Partial<AnalysisStatus['timings']>): void {
86
+ analysisStatus.timings = { ...analysisStatus.timings, ...timings };
87
+ }
88
+
89
+ // === BACKEND ===
90
+ export async function getOrCreateBackend(): Promise<GraphBackend> {
91
+ if (backend) return backend;
92
+
93
+ const grafemaDir = join(projectPath, '.grafema');
94
+ const dbPath = join(grafemaDir, 'graph.rfdb');
95
+
96
+ if (!existsSync(grafemaDir)) {
97
+ mkdirSync(grafemaDir, { recursive: true });
98
+ }
99
+
100
+ const config = loadConfig(projectPath);
101
+ // Socket path from config, or let RFDBServerBackend derive it from dbPath
102
+ const socketPath = (config as any).analysis?.parallel?.socketPath;
103
+
104
+ log(`[Grafema MCP] Using RFDB server backend: socket=${socketPath || 'auto'}, db=${dbPath}`);
105
+
106
+ const rfdbBackend = new RFDBServerBackend({ socketPath, dbPath });
107
+ await rfdbBackend.connect();
108
+ backend = rfdbBackend as unknown as GraphBackend;
109
+
110
+ const nodeCount = await backend.nodeCount();
111
+ if (nodeCount > 0) {
112
+ isAnalyzed = true;
113
+ log(`[Grafema MCP] Connected to existing database: ${nodeCount} nodes`);
114
+ } else {
115
+ log(`[Grafema MCP] Empty database, analysis needed`);
116
+ }
117
+
118
+ // Initialize guarantee managers
119
+ initializeGuaranteeManagers(rfdbBackend);
120
+
121
+ return backend;
122
+ }
123
+
124
+ /**
125
+ * Initialize GuaranteeManager (Datalog-based) and GuaranteeAPI (contract-based)
126
+ */
127
+ function initializeGuaranteeManagers(rfdbBackend: RFDBServerBackend): void {
128
+ // GuaranteeManager for Datalog-based guarantees
129
+ // Cast to GuaranteeGraph interface expected by GuaranteeManager
130
+ const guaranteeGraph = rfdbBackend as unknown as GuaranteeGraph;
131
+ guaranteeManager = new GuaranteeManager(guaranteeGraph, projectPath);
132
+ log(`[Grafema MCP] GuaranteeManager initialized`);
133
+
134
+ // GuaranteeAPI for contract-based guarantees
135
+ const guaranteeGraphBackend = rfdbBackend as unknown as GuaranteeGraphBackend;
136
+ guaranteeAPI = new GuaranteeAPI(guaranteeGraphBackend);
137
+ log(`[Grafema MCP] GuaranteeAPI initialized`);
138
+ }
139
+
140
+ export function getBackendIfExists(): GraphBackend | null {
141
+ return backend;
142
+ }
143
+
144
+ // === LOGGING SETUP ===
145
+ export function setupLogging(): void {
146
+ const grafemaDir = join(projectPath, '.grafema');
147
+ if (!existsSync(grafemaDir)) {
148
+ mkdirSync(grafemaDir, { recursive: true });
149
+ }
150
+ initLogger(grafemaDir);
151
+ }
152
+
153
+ // === INITIALIZATION ===
154
+ export function initializeFromArgs(): void {
155
+ const args = process.argv.slice(2);
156
+ for (let i = 0; i < args.length; i++) {
157
+ if (args[i] === '--project' && args[i + 1]) {
158
+ projectPath = args[i + 1];
159
+ i++;
160
+ }
161
+ }
162
+ }
163
+
164
+ // === CLEANUP ===
165
+ export async function cleanup(): Promise<void> {
166
+ if (backend && 'close' in backend && typeof backend.close === 'function') {
167
+ await backend.close();
168
+ }
169
+ }
package/src/types.ts ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * MCP Server Types
3
+ */
4
+
5
+ import type { WriteStream } from 'fs';
6
+
7
+ // === ANALYSIS STATUS ===
8
+ export interface AnalysisTimings {
9
+ discovery: number | null;
10
+ indexing: number | null;
11
+ analysis: number | null;
12
+ enrichment: number | null;
13
+ validation: number | null;
14
+ total: number | null;
15
+ }
16
+
17
+ export interface AnalysisStatus {
18
+ running: boolean;
19
+ phase: string | null;
20
+ message: string | null;
21
+ servicesDiscovered: number;
22
+ servicesAnalyzed: number;
23
+ startTime: number | null;
24
+ endTime: number | null;
25
+ error: string | null;
26
+ timings: AnalysisTimings;
27
+ }
28
+
29
+ // === PAGINATION ===
30
+ export interface PaginationParams {
31
+ limit: number;
32
+ offset: number;
33
+ returned: number;
34
+ total?: number;
35
+ hasMore: boolean;
36
+ }
37
+
38
+ // === CONFIG ===
39
+ export interface GrafemaConfig {
40
+ project_path?: string;
41
+ backend?: 'local' | 'rfdb';
42
+ rfdb_socket?: string;
43
+ plugins?: string[];
44
+ ignore_patterns?: string[];
45
+ }
46
+
47
+ // === TOOL ARGUMENTS ===
48
+ export interface QueryGraphArgs {
49
+ query: string;
50
+ limit?: number;
51
+ offset?: number;
52
+ format?: 'table' | 'json' | 'tree';
53
+ }
54
+
55
+ export interface FindCallsArgs {
56
+ target: string;
57
+ limit?: number;
58
+ offset?: number;
59
+ include_indirect?: boolean;
60
+ }
61
+
62
+ export interface TraceAliasArgs {
63
+ identifier: string;
64
+ file?: string;
65
+ max_depth?: number;
66
+ }
67
+
68
+ export interface TraceDataFlowArgs {
69
+ source: string;
70
+ file?: string;
71
+ direction?: 'forward' | 'backward' | 'both';
72
+ max_depth?: number;
73
+ limit?: number;
74
+ }
75
+
76
+ export interface CheckInvariantArgs {
77
+ rule: string;
78
+ name?: string;
79
+ }
80
+
81
+ export interface GetSchemaArgs {
82
+ type?: 'nodes' | 'edges' | 'all';
83
+ }
84
+
85
+ export interface GetValueSetArgs {
86
+ node_id: string;
87
+ property?: string;
88
+ }
89
+
90
+ export interface FindNodesArgs {
91
+ type?: string;
92
+ name?: string;
93
+ file?: string;
94
+ limit?: number;
95
+ offset?: number;
96
+ }
97
+
98
+ export interface AnalyzeProjectArgs {
99
+ service?: string;
100
+ force?: boolean;
101
+ index_only?: boolean;
102
+ }
103
+
104
+ export interface GetCoverageArgs {
105
+ path?: string;
106
+ depth?: number;
107
+ }
108
+
109
+ export interface GetDocumentationArgs {
110
+ topic?: string;
111
+ }
112
+
113
+ // === GUARANTEE ARGS ===
114
+
115
+ // Priority levels for contract-based guarantees
116
+ export type GuaranteePriority = 'critical' | 'important' | 'observed' | 'tracked';
117
+
118
+ // Lifecycle status for contract-based guarantees
119
+ export type GuaranteeStatus = 'discovered' | 'reviewed' | 'active' | 'changing' | 'deprecated';
120
+
121
+ export interface CreateGuaranteeArgs {
122
+ name: string;
123
+ // Datalog-based guarantee fields (optional for contract-based)
124
+ rule?: string;
125
+ description?: string;
126
+ severity?: 'error' | 'warning' | 'info';
127
+ // Contract-based guarantee fields
128
+ type?: 'guarantee:queue' | 'guarantee:api' | 'guarantee:permission';
129
+ priority?: GuaranteePriority;
130
+ status?: GuaranteeStatus;
131
+ owner?: string;
132
+ schema?: Record<string, unknown>;
133
+ condition?: string;
134
+ governs?: string[]; // Node IDs that this guarantee governs
135
+ }
136
+
137
+ export interface CheckGuaranteesArgs {
138
+ names?: string[];
139
+ }
140
+
141
+ export interface DeleteGuaranteeArgs {
142
+ name: string;
143
+ }
144
+
145
+ export interface ExportGuaranteesArgs {
146
+ format?: 'json' | 'yaml';
147
+ }
148
+
149
+ export interface ImportGuaranteesArgs {
150
+ guarantees: Array<{
151
+ name: string;
152
+ rule: string;
153
+ description?: string;
154
+ severity?: string;
155
+ }>;
156
+ merge?: boolean;
157
+ }
158
+
159
+ export interface GuaranteeDriftArgs {
160
+ baseline?: string;
161
+ }
162
+
163
+ export interface CheckGuaranteeFeasibilityArgs {
164
+ rule: string;
165
+ }
166
+
167
+ // === TOOL RESULT ===
168
+ export interface ToolResult {
169
+ [x: string]: unknown;
170
+ content: Array<{
171
+ type: 'text';
172
+ text: string;
173
+ }>;
174
+ isError?: boolean;
175
+ _meta?: Record<string, unknown>;
176
+ }
177
+
178
+ // === BACKEND INTERFACE (minimal) ===
179
+ export interface GraphBackend {
180
+ nodeCount(): Promise<number>;
181
+ edgeCount(): Promise<number>;
182
+ countNodesByType(types?: string[] | null): Promise<Record<string, number>>;
183
+ countEdgesByType(types?: string[] | null): Promise<Record<string, number>>;
184
+ getNode(id: string): Promise<GraphNode | null>;
185
+ findByType(type: string): Promise<string[]>;
186
+ findByAttr(query: Record<string, unknown>): Promise<string[]>;
187
+ getOutgoingEdges(id: string, types?: string[] | null): Promise<GraphEdge[]>;
188
+ getIncomingEdges(id: string, types?: string[] | null): Promise<GraphEdge[]>;
189
+ queryNodes(filter: Record<string, unknown>): AsyncIterable<GraphNode>;
190
+ getAllNodes(filter?: Record<string, unknown>): Promise<GraphNode[]>;
191
+ runDatalogQuery?(query: string): Promise<unknown[]>;
192
+ close?(): Promise<void>;
193
+ }
194
+
195
+ export interface GraphNode {
196
+ id: string;
197
+ type: string;
198
+ name: string;
199
+ file?: string;
200
+ line?: number;
201
+ [key: string]: unknown;
202
+ }
203
+
204
+ export interface GraphEdge {
205
+ src: string;
206
+ dst: string;
207
+ type: string;
208
+ edgeType?: string;
209
+ [key: string]: unknown;
210
+ }
211
+
212
+ // === GLOBAL STATE ===
213
+ export interface MCPState {
214
+ projectPath: string;
215
+ backend: GraphBackend | null;
216
+ isAnalyzed: boolean;
217
+ analysisStatus: AnalysisStatus;
218
+ logStream: WriteStream | null;
219
+ backgroundPid: number | null;
220
+ }
221
+
222
+ // === FILE CLASSIFICATION ===
223
+ export interface FileClassification {
224
+ category: 'source' | 'config' | 'test' | 'doc' | 'asset' | 'generated' | 'other';
225
+ language?: string;
226
+ framework?: string;
227
+ }
228
+
229
+ export interface ExtensionGroup {
230
+ [ext: string]: string[];
231
+ }
232
+
233
+ export interface AnalyzerSuggestion {
234
+ name: string;
235
+ reason: string;
236
+ priority: number;
237
+ }
238
+
239
+ // === BUG REPORTING ===
240
+ export interface ReportIssueArgs {
241
+ title: string;
242
+ description: string;
243
+ context?: string;
244
+ labels?: string[];
245
+ }