code-graph-context 1.0.0 → 2.0.0

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 (47) hide show
  1. package/README.md +221 -101
  2. package/dist/core/config/fairsquare-framework-schema.js +47 -60
  3. package/dist/core/config/nestjs-framework-schema.js +11 -1
  4. package/dist/core/config/schema.js +1 -1
  5. package/dist/core/config/timeouts.js +27 -0
  6. package/dist/core/embeddings/embeddings.service.js +122 -2
  7. package/dist/core/embeddings/natural-language-to-cypher.service.js +428 -30
  8. package/dist/core/parsers/parser-factory.js +6 -6
  9. package/dist/core/parsers/typescript-parser.js +639 -44
  10. package/dist/core/parsers/workspace-parser.js +553 -0
  11. package/dist/core/utils/edge-factory.js +37 -0
  12. package/dist/core/utils/file-change-detection.js +105 -0
  13. package/dist/core/utils/file-utils.js +20 -0
  14. package/dist/core/utils/index.js +3 -0
  15. package/dist/core/utils/path-utils.js +75 -0
  16. package/dist/core/utils/progress-reporter.js +112 -0
  17. package/dist/core/utils/project-id.js +176 -0
  18. package/dist/core/utils/retry.js +41 -0
  19. package/dist/core/workspace/index.js +4 -0
  20. package/dist/core/workspace/workspace-detector.js +221 -0
  21. package/dist/mcp/constants.js +172 -7
  22. package/dist/mcp/handlers/cross-file-edge.helpers.js +19 -0
  23. package/dist/mcp/handlers/file-change-detection.js +105 -0
  24. package/dist/mcp/handlers/graph-generator.handler.js +97 -32
  25. package/dist/mcp/handlers/incremental-parse.handler.js +146 -0
  26. package/dist/mcp/handlers/streaming-import.handler.js +210 -0
  27. package/dist/mcp/handlers/traversal.handler.js +130 -71
  28. package/dist/mcp/mcp.server.js +46 -7
  29. package/dist/mcp/service-init.js +79 -0
  30. package/dist/mcp/services/job-manager.js +165 -0
  31. package/dist/mcp/services/watch-manager.js +376 -0
  32. package/dist/mcp/services.js +48 -127
  33. package/dist/mcp/tools/check-parse-status.tool.js +64 -0
  34. package/dist/mcp/tools/impact-analysis.tool.js +319 -0
  35. package/dist/mcp/tools/index.js +15 -1
  36. package/dist/mcp/tools/list-projects.tool.js +62 -0
  37. package/dist/mcp/tools/list-watchers.tool.js +51 -0
  38. package/dist/mcp/tools/natural-language-to-cypher.tool.js +34 -8
  39. package/dist/mcp/tools/parse-typescript-project.tool.js +325 -60
  40. package/dist/mcp/tools/search-codebase.tool.js +57 -23
  41. package/dist/mcp/tools/start-watch-project.tool.js +100 -0
  42. package/dist/mcp/tools/stop-watch-project.tool.js +49 -0
  43. package/dist/mcp/tools/traverse-from-node.tool.js +68 -9
  44. package/dist/mcp/utils.js +35 -12
  45. package/dist/mcp/workers/parse-worker.js +198 -0
  46. package/dist/storage/neo4j/neo4j.service.js +273 -34
  47. package/package.json +4 -2
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Impact Analysis Tool
3
+ * Analyzes what would be affected if a node is modified
4
+ * Reuses cross-file edge pattern from incremental parsing
5
+ */
6
+ import { z } from 'zod';
7
+ import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
8
+ import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
9
+ import { createErrorResponse, createSuccessResponse, debugLog, resolveProjectIdOrError } from '../utils.js';
10
+ // Default relationship weights for core AST relationships
11
+ const DEFAULT_RELATIONSHIP_WEIGHTS = {
12
+ // Critical - inheritance/interface contracts
13
+ EXTENDS: 0.95,
14
+ IMPLEMENTS: 0.95,
15
+ // High - direct code dependencies
16
+ CALLS: 0.75,
17
+ HAS_MEMBER: 0.65,
18
+ TYPED_AS: 0.6,
19
+ // Medium - module dependencies
20
+ IMPORTS: 0.5,
21
+ EXPORTS: 0.5,
22
+ // Lower - structural
23
+ CONTAINS: 0.3,
24
+ HAS_PARAMETER: 0.3,
25
+ DECORATED_WITH: 0.4,
26
+ };
27
+ // Schema for framework-specific configuration
28
+ const FrameworkConfigSchema = z.object({
29
+ relationshipWeights: z.record(z.string(), z.number().min(0).max(1)).optional(),
30
+ highRiskTypes: z.array(z.string()).optional(),
31
+ name: z.string().optional(),
32
+ });
33
+ export const createImpactAnalysisTool = (server) => {
34
+ server.registerTool(TOOL_NAMES.impactAnalysis, {
35
+ title: TOOL_METADATA[TOOL_NAMES.impactAnalysis].title,
36
+ description: TOOL_METADATA[TOOL_NAMES.impactAnalysis].description,
37
+ inputSchema: {
38
+ projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
39
+ nodeId: z
40
+ .string()
41
+ .optional()
42
+ .describe('The node ID to analyze impact for (from search_codebase or traverse_from_node results)'),
43
+ filePath: z
44
+ .string()
45
+ .optional()
46
+ .describe('Alternatively, provide a file path to analyze all exports from that file'),
47
+ maxDepth: z
48
+ .number()
49
+ .int()
50
+ .min(1)
51
+ .max(6)
52
+ .optional()
53
+ .describe('Maximum depth to traverse for transitive dependents (default: 4)')
54
+ .default(4),
55
+ frameworkConfig: FrameworkConfigSchema.optional().describe('Framework-specific configuration for risk scoring. Includes relationshipWeights (e.g., {"INJECTS": 0.9}), highRiskTypes (e.g., ["Controller", "Service"]), and optional name.'),
56
+ },
57
+ }, async ({ projectId, nodeId, filePath, maxDepth = 4, frameworkConfig }) => {
58
+ const neo4jService = new Neo4jService();
59
+ try {
60
+ // Resolve project ID from name, path, or ID
61
+ const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
62
+ if (!projectResult.success)
63
+ return projectResult.error;
64
+ const resolvedProjectId = projectResult.projectId;
65
+ if (!nodeId && !filePath) {
66
+ return createErrorResponse('Either nodeId or filePath must be provided');
67
+ }
68
+ await debugLog('Impact analysis started', {
69
+ projectId: resolvedProjectId,
70
+ nodeId,
71
+ filePath,
72
+ maxDepth,
73
+ frameworkConfig,
74
+ });
75
+ // Merge default weights with framework-specific weights
76
+ const weights = { ...DEFAULT_RELATIONSHIP_WEIGHTS, ...frameworkConfig?.relationshipWeights };
77
+ const highRiskTypes = new Set(frameworkConfig?.highRiskTypes ?? []);
78
+ let targetInfo;
79
+ let directDependents;
80
+ if (nodeId) {
81
+ // Get target node info
82
+ const targetResult = await neo4jService.run(QUERIES.GET_NODE_BY_ID, { nodeId, projectId: resolvedProjectId });
83
+ if (targetResult.length === 0) {
84
+ return createErrorResponse(`Node with ID "${nodeId}" not found in project "${resolvedProjectId}"`);
85
+ }
86
+ const target = targetResult[0];
87
+ targetInfo = {
88
+ id: target.id,
89
+ name: target.name ?? 'Unknown',
90
+ type: target.semanticType ?? target.coreType ?? target.labels?.[0] ?? 'Unknown',
91
+ filePath: target.filePath ?? '',
92
+ };
93
+ // Get direct dependents using cross-file edge pattern
94
+ const directResult = await neo4jService.run(QUERIES.GET_NODE_IMPACT, {
95
+ nodeId,
96
+ projectId: resolvedProjectId,
97
+ });
98
+ directDependents = normalizeDependents(directResult);
99
+ }
100
+ else {
101
+ // File-based analysis - find all Class/Function/Interface entities in the file
102
+ // and aggregate their impact analysis results
103
+ const entitiesQuery = `
104
+ MATCH (n)
105
+ WHERE n.projectId = $projectId
106
+ AND (n.filePath = $filePath OR n.filePath ENDS WITH '/' + $filePath)
107
+ AND (n:Class OR n:Function OR n:Interface)
108
+ RETURN n.id AS nodeId, n.name AS name, labels(n) AS labels,
109
+ n.semanticType AS semanticType, n.coreType AS coreType
110
+ `;
111
+ const entities = await neo4jService.run(entitiesQuery, {
112
+ filePath,
113
+ projectId: resolvedProjectId,
114
+ });
115
+ if (entities.length === 0) {
116
+ // No exportable entities found
117
+ targetInfo = {
118
+ id: filePath,
119
+ name: filePath.split('/').pop() ?? filePath,
120
+ type: 'SourceFile',
121
+ filePath: filePath,
122
+ };
123
+ directDependents = [];
124
+ }
125
+ else {
126
+ // Use first entity as the primary target for display
127
+ const primaryEntity = entities[0];
128
+ targetInfo = {
129
+ id: primaryEntity.nodeId,
130
+ name: primaryEntity.name ?? filePath.split('/').pop() ?? filePath,
131
+ type: primaryEntity.semanticType ?? primaryEntity.coreType ?? 'Class',
132
+ filePath: filePath,
133
+ };
134
+ // Aggregate impact from all entities in the file
135
+ const allDependentsMap = new Map();
136
+ for (const entity of entities) {
137
+ const entityResult = await neo4jService.run(QUERIES.GET_NODE_IMPACT, {
138
+ nodeId: entity.nodeId,
139
+ projectId: resolvedProjectId,
140
+ });
141
+ for (const dep of normalizeDependents(entityResult)) {
142
+ // Dedupe by nodeId, keeping highest weight
143
+ const existing = allDependentsMap.get(dep.nodeId);
144
+ if (!existing || dep.weight > existing.weight) {
145
+ allDependentsMap.set(dep.nodeId, dep);
146
+ }
147
+ }
148
+ }
149
+ directDependents = Array.from(allDependentsMap.values());
150
+ // Update nodeId for transitive analysis if we have dependents
151
+ if (directDependents.length > 0 && entities.length > 0) {
152
+ // Use first entity's nodeId for transitive analysis
153
+ nodeId = primaryEntity.nodeId;
154
+ }
155
+ }
156
+ }
157
+ // Get transitive dependents if nodeId provided
158
+ let transitiveDependents = [];
159
+ if (nodeId && maxDepth > 1) {
160
+ const transitiveResult = await neo4jService.run(QUERIES.GET_TRANSITIVE_DEPENDENTS(maxDepth), {
161
+ nodeId,
162
+ projectId: resolvedProjectId,
163
+ });
164
+ transitiveDependents = normalizeTransitiveDependents(transitiveResult);
165
+ // Filter out direct dependents from transitive
166
+ const directIds = new Set(directDependents.map((d) => d.nodeId));
167
+ transitiveDependents = transitiveDependents.filter((d) => !directIds.has(d.nodeId));
168
+ }
169
+ // Calculate risk score
170
+ const riskScore = calculateRiskScore(directDependents, transitiveDependents, weights, highRiskTypes);
171
+ const riskLevel = getRiskLevel(riskScore);
172
+ // Group dependents by type
173
+ const directByType = groupByType(directDependents);
174
+ const directByRelationship = groupByRelationship(directDependents);
175
+ const transitiveByType = groupByType(transitiveDependents);
176
+ // Get affected files
177
+ const affectedFiles = getAffectedFiles([...directDependents, ...transitiveDependents]);
178
+ // Find critical paths (high-weight relationships)
179
+ const criticalPaths = findCriticalPaths(directDependents, targetInfo, weights);
180
+ // Build summary
181
+ const summary = buildSummary(targetInfo, directDependents.length, transitiveDependents.length, affectedFiles.length, riskLevel);
182
+ const result = {
183
+ target: targetInfo,
184
+ riskLevel,
185
+ riskScore: Math.round(riskScore * 100) / 100,
186
+ summary,
187
+ directDependents: {
188
+ count: directDependents.length,
189
+ byType: directByType,
190
+ byRelationship: directByRelationship,
191
+ },
192
+ transitiveDependents: {
193
+ count: transitiveDependents.length,
194
+ maxDepth: getMaxDepth(transitiveDependents),
195
+ byType: transitiveByType,
196
+ },
197
+ affectedFiles,
198
+ criticalPaths,
199
+ };
200
+ await debugLog('Impact analysis complete', {
201
+ nodeId: nodeId ?? filePath,
202
+ riskLevel,
203
+ directCount: directDependents.length,
204
+ transitiveCount: transitiveDependents.length,
205
+ });
206
+ return createSuccessResponse(JSON.stringify(result, null, 2));
207
+ }
208
+ catch (error) {
209
+ console.error('Impact analysis error:', error);
210
+ await debugLog('Impact analysis error', { nodeId, filePath, error });
211
+ return createErrorResponse(error);
212
+ }
213
+ finally {
214
+ await neo4jService.close();
215
+ }
216
+ });
217
+ };
218
+ // Helper functions
219
+ const normalizeDependents = (results) => {
220
+ return results.map((r) => ({
221
+ nodeId: r.nodeId,
222
+ name: r.name ?? 'Unknown',
223
+ labels: r.labels ?? [],
224
+ semanticType: r.semanticType,
225
+ coreType: r.coreType,
226
+ filePath: r.filePath ?? '',
227
+ relationshipType: r.relationshipType ?? 'UNKNOWN',
228
+ weight: typeof r.weight === 'object'
229
+ ? r.weight.toNumber()
230
+ : (r.weight ?? 0.5),
231
+ }));
232
+ };
233
+ const normalizeTransitiveDependents = (results) => {
234
+ return results.map((r) => ({
235
+ nodeId: r.nodeId,
236
+ name: r.name ?? 'Unknown',
237
+ labels: r.labels ?? [],
238
+ semanticType: r.semanticType,
239
+ coreType: r.coreType,
240
+ filePath: r.filePath ?? '',
241
+ relationshipType: r.relationshipPath?.[0] ?? 'UNKNOWN',
242
+ weight: 0.5,
243
+ depth: typeof r.depth === 'object' ? r.depth.toNumber() : r.depth,
244
+ relationshipPath: r.relationshipPath,
245
+ }));
246
+ };
247
+ const calculateRiskScore = (directDependents, transitiveDependents, weights, highRiskTypes) => {
248
+ if (directDependents.length === 0)
249
+ return 0;
250
+ let score = 0;
251
+ // Factor 1: Number of direct dependents (logarithmic, max 0.3)
252
+ score += Math.min(Math.log10(directDependents.length + 1) / 2, 0.3);
253
+ // Factor 2: Average relationship weight of direct deps (max 0.3)
254
+ const avgWeight = directDependents.reduce((sum, d) => sum + (weights[d.relationshipType] ?? d.weight), 0) / directDependents.length;
255
+ score += avgWeight * 0.3;
256
+ // Factor 3: High-risk types affected (max 0.2)
257
+ const highRiskCount = directDependents.filter((d) => highRiskTypes.has(d.semanticType ?? '') || highRiskTypes.has(d.coreType ?? '')).length;
258
+ if (highRiskTypes.size > 0) {
259
+ score += Math.min(highRiskCount / Math.max(highRiskTypes.size, 3), 1) * 0.2;
260
+ }
261
+ // Factor 4: Transitive impact (max 0.2)
262
+ score += Math.min(Math.log10(transitiveDependents.length + 1) / 3, 0.2);
263
+ return Math.min(score, 1);
264
+ };
265
+ const getRiskLevel = (score) => {
266
+ if (score >= 0.75)
267
+ return 'CRITICAL';
268
+ if (score >= 0.5)
269
+ return 'HIGH';
270
+ if (score >= 0.25)
271
+ return 'MEDIUM';
272
+ return 'LOW';
273
+ };
274
+ const groupByType = (dependents) => {
275
+ const groups = {};
276
+ for (const dep of dependents) {
277
+ const type = dep.semanticType ?? dep.coreType ?? dep.labels?.[0] ?? 'Unknown';
278
+ groups[type] = (groups[type] ?? 0) + 1;
279
+ }
280
+ return groups;
281
+ };
282
+ const groupByRelationship = (dependents) => {
283
+ const groups = {};
284
+ for (const dep of dependents) {
285
+ groups[dep.relationshipType] = (groups[dep.relationshipType] ?? 0) + 1;
286
+ }
287
+ return groups;
288
+ };
289
+ const getAffectedFiles = (dependents) => {
290
+ const files = new Set();
291
+ for (const dep of dependents) {
292
+ if (dep.filePath)
293
+ files.add(dep.filePath);
294
+ }
295
+ return Array.from(files).sort();
296
+ };
297
+ const getMaxDepth = (dependents) => {
298
+ if (dependents.length === 0)
299
+ return 0;
300
+ return Math.max(...dependents.map((d) => d.depth ?? 1));
301
+ };
302
+ const findCriticalPaths = (directDependents, target, weights) => {
303
+ const paths = [];
304
+ for (const dep of directDependents) {
305
+ const relWeight = weights[dep.relationshipType] ?? 0.5;
306
+ // Only include high-weight relationships
307
+ if (relWeight >= 0.6) {
308
+ const depType = dep.semanticType ?? dep.coreType ?? '';
309
+ paths.push(`${dep.name} (${depType}) -[${dep.relationshipType}]-> ${target.name} (${target.type})`);
310
+ }
311
+ }
312
+ return paths.slice(0, 10);
313
+ };
314
+ const buildSummary = (target, directCount, transitiveCount, fileCount, riskLevel) => {
315
+ if (directCount === 0) {
316
+ return `${target.name} (${target.type}) has no external dependents - safe to modify`;
317
+ }
318
+ return `Modifying ${target.name} (${target.type}) affects ${directCount} direct and ${transitiveCount} transitive dependents across ${fileCount} files. Risk: ${riskLevel}`;
319
+ };
@@ -2,10 +2,16 @@
2
2
  * MCP Tool Factory
3
3
  * Centralized tool creation and registration
4
4
  */
5
+ import { createCheckParseStatusTool } from './check-parse-status.tool.js';
5
6
  import { createHelloTool } from './hello.tool.js';
7
+ import { createImpactAnalysisTool } from './impact-analysis.tool.js';
8
+ import { createListProjectsTool } from './list-projects.tool.js';
9
+ import { createListWatchersTool } from './list-watchers.tool.js';
6
10
  import { createNaturalLanguageToCypherTool } from './natural-language-to-cypher.tool.js';
7
11
  import { createParseTypescriptProjectTool } from './parse-typescript-project.tool.js';
8
12
  import { createSearchCodebaseTool } from './search-codebase.tool.js';
13
+ import { createStartWatchProjectTool } from './start-watch-project.tool.js';
14
+ import { createStopWatchProjectTool } from './stop-watch-project.tool.js';
9
15
  import { createTestNeo4jConnectionTool } from './test-neo4j-connection.tool.js';
10
16
  import { createTraverseFromNodeTool } from './traverse-from-node.tool.js';
11
17
  /**
@@ -19,6 +25,14 @@ export const registerAllTools = (server) => {
19
25
  createSearchCodebaseTool(server);
20
26
  createTraverseFromNodeTool(server);
21
27
  createNaturalLanguageToCypherTool(server);
22
- // Register project parsing tool
28
+ createImpactAnalysisTool(server);
29
+ // Register project parsing tools
23
30
  createParseTypescriptProjectTool(server);
31
+ createCheckParseStatusTool(server);
32
+ // Register project management tools
33
+ createListProjectsTool(server);
34
+ // Register file watch tools
35
+ createStartWatchProjectTool(server);
36
+ createStopWatchProjectTool(server);
37
+ createListWatchersTool(server);
24
38
  };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * List Projects Tool
3
+ * Lists all parsed projects in the database
4
+ */
5
+ import { LIST_PROJECTS_QUERY } from '../../core/utils/project-id.js';
6
+ import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
7
+ import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
8
+ import { createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
9
+ export const createListProjectsTool = (server) => {
10
+ server.registerTool(TOOL_NAMES.listProjects, {
11
+ title: TOOL_METADATA[TOOL_NAMES.listProjects].title,
12
+ description: TOOL_METADATA[TOOL_NAMES.listProjects].description,
13
+ inputSchema: {},
14
+ }, async () => {
15
+ const neo4jService = new Neo4jService();
16
+ try {
17
+ await debugLog('Listing projects');
18
+ const results = await neo4jService.run(LIST_PROJECTS_QUERY, {});
19
+ if (results.length === 0) {
20
+ return createSuccessResponse('No projects found. Use parse_typescript_project to add a project first.');
21
+ }
22
+ const projects = results.map((r) => ({
23
+ projectId: r.projectId,
24
+ name: r.name,
25
+ path: r.path,
26
+ status: r.status ?? 'unknown',
27
+ nodeCount: r.nodeCount,
28
+ edgeCount: r.edgeCount,
29
+ updatedAt: r.updatedAt?.toString() ?? 'Unknown',
30
+ }));
31
+ await debugLog('Projects listed', { count: projects.length });
32
+ // Format output for readability
33
+ const header = `Found ${projects.length} project(s):\n\n`;
34
+ const formatStats = (p) => {
35
+ if (p.status === 'complete' && p.nodeCount !== null) {
36
+ return ` Stats: ${p.nodeCount} nodes, ${p.edgeCount ?? 0} edges`;
37
+ }
38
+ return '';
39
+ };
40
+ const projectList = projects
41
+ .map((p) => `- ${p.name} [${p.status}]\n` +
42
+ ` ID: ${p.projectId}\n` +
43
+ ` Path: ${p.path}\n` +
44
+ formatStats(p) +
45
+ (formatStats(p) ? '\n' : '') +
46
+ ` Updated: ${p.updatedAt}`)
47
+ .join('\n\n');
48
+ const tip = '\n\nTip: Use the project name (e.g., "' +
49
+ projects[0].name +
50
+ '") in other tools instead of the full projectId.';
51
+ return createSuccessResponse(header + projectList + tip);
52
+ }
53
+ catch (error) {
54
+ console.error('List projects error:', error);
55
+ await debugLog('List projects error', { error });
56
+ return createErrorResponse(error);
57
+ }
58
+ finally {
59
+ await neo4jService.close();
60
+ }
61
+ });
62
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * List Watchers Tool
3
+ * Lists all active file watchers
4
+ */
5
+ import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
6
+ import { watchManager } from '../services/watch-manager.js';
7
+ import { createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
8
+ export const createListWatchersTool = (server) => {
9
+ server.registerTool(TOOL_NAMES.listWatchers, {
10
+ title: TOOL_METADATA[TOOL_NAMES.listWatchers].title,
11
+ description: TOOL_METADATA[TOOL_NAMES.listWatchers].description,
12
+ inputSchema: {},
13
+ }, async () => {
14
+ try {
15
+ await debugLog('Listing watchers');
16
+ const watchers = watchManager.listWatchers();
17
+ if (watchers.length === 0) {
18
+ return createSuccessResponse('No active file watchers.\n\n' +
19
+ 'To start watching a project:\n' +
20
+ '- Use start_watch_project with a projectId\n' +
21
+ '- Or use parse_typescript_project with watch: true (requires async: false)');
22
+ }
23
+ await debugLog('Watchers listed', { count: watchers.length });
24
+ const header = `Found ${watchers.length} active watcher(s):\n\n`;
25
+ const watcherList = watchers
26
+ .map((w) => {
27
+ const lines = [
28
+ `- ${w.projectId} [${w.status}]`,
29
+ ` Path: ${w.projectPath}`,
30
+ ` Debounce: ${w.debounceMs}ms`,
31
+ ` Pending changes: ${w.pendingChanges}`,
32
+ ];
33
+ if (w.lastUpdateTime) {
34
+ lines.push(` Last update: ${w.lastUpdateTime}`);
35
+ }
36
+ if (w.errorMessage) {
37
+ lines.push(` Error: ${w.errorMessage}`);
38
+ }
39
+ return lines.join('\n');
40
+ })
41
+ .join('\n\n');
42
+ const tip = '\n\nUse stop_watch_project with a project ID to stop watching.';
43
+ return createSuccessResponse(header + watcherList + tip);
44
+ }
45
+ catch (error) {
46
+ console.error('List watchers error:', error);
47
+ await debugLog('List watchers error', { error });
48
+ return createErrorResponse(error instanceof Error ? error : new Error(String(error)));
49
+ }
50
+ });
51
+ };
@@ -6,7 +6,7 @@ import { z } from 'zod';
6
6
  import { NaturalLanguageToCypherService } from '../../core/embeddings/natural-language-to-cypher.service.js';
7
7
  import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
8
8
  import { TOOL_NAMES, TOOL_METADATA, MESSAGES } from '../constants.js';
9
- import { createErrorResponse, createSuccessResponse, formatQueryResults, debugLog } from '../utils.js';
9
+ import { createErrorResponse, createSuccessResponse, formatQueryResults, debugLog, resolveProjectIdOrError, } from '../utils.js';
10
10
  // Service instance - initialized asynchronously
11
11
  let naturalLanguageToCypherService = null;
12
12
  /**
@@ -30,20 +30,43 @@ export const createNaturalLanguageToCypherTool = (server) => {
30
30
  title: TOOL_METADATA[TOOL_NAMES.naturalLanguageToCypher].title,
31
31
  description: TOOL_METADATA[TOOL_NAMES.naturalLanguageToCypher].description,
32
32
  inputSchema: {
33
+ projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
33
34
  query: z.string().describe('Natural language query to convert to Cypher'),
34
35
  },
35
- }, async ({ query }) => {
36
+ }, async ({ projectId, query }) => {
37
+ const neo4jService = new Neo4jService();
36
38
  try {
39
+ // Resolve project ID from name, path, or ID
40
+ const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
41
+ if (!projectResult.success)
42
+ return projectResult.error;
43
+ const resolvedProjectId = projectResult.projectId;
37
44
  if (!naturalLanguageToCypherService) {
38
- await debugLog('Natural language service not available', { query });
45
+ await debugLog('Natural language service not available', { projectId: resolvedProjectId, query });
39
46
  return createSuccessResponse(MESSAGES.errors.serviceNotInitialized);
40
47
  }
41
- await debugLog('Natural language to Cypher conversion started', { query });
42
- const cypherResult = await naturalLanguageToCypherService.promptToQuery(query);
43
- const neo4jService = new Neo4jService();
44
- // Execute the generated Cypher query
45
- const results = await neo4jService.run(cypherResult.cypher, cypherResult.parameters ?? {});
48
+ await debugLog('Natural language to Cypher conversion started', { projectId: resolvedProjectId, query });
49
+ const cypherResult = await naturalLanguageToCypherService.promptToQuery(query, resolvedProjectId);
50
+ // Validate Cypher syntax using EXPLAIN (no execution, just parse)
51
+ const parameters = { ...cypherResult.parameters, projectId: resolvedProjectId };
52
+ try {
53
+ await neo4jService.run(`EXPLAIN ${cypherResult.cypher}`, parameters);
54
+ }
55
+ catch (validationError) {
56
+ const message = validationError instanceof Error ? validationError.message : String(validationError);
57
+ await debugLog('Generated Cypher validation failed', {
58
+ cypher: cypherResult.cypher,
59
+ error: message,
60
+ });
61
+ return createErrorResponse(`Generated Cypher query has syntax errors:\n\n` +
62
+ `Query: ${cypherResult.cypher}\n\n` +
63
+ `Error: ${message}\n\n` +
64
+ `Try rephrasing your request or use a simpler query.`);
65
+ }
66
+ // Execute the validated query
67
+ const results = await neo4jService.run(cypherResult.cypher, parameters);
46
68
  await debugLog('Cypher query executed', {
69
+ projectId: resolvedProjectId,
47
70
  cypher: cypherResult.cypher,
48
71
  resultsCount: results.length,
49
72
  });
@@ -55,5 +78,8 @@ export const createNaturalLanguageToCypherTool = (server) => {
55
78
  await debugLog('Natural language to Cypher error', { query, error });
56
79
  return createErrorResponse(error);
57
80
  }
81
+ finally {
82
+ await neo4jService.close();
83
+ }
58
84
  });
59
85
  };