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
@@ -1,5 +1,6 @@
1
1
  import neo4j from 'neo4j-driver';
2
2
  import { MAX_TRAVERSAL_DEPTH } from '../../constants.js';
3
+ import { getTimeoutConfig } from '../../core/config/timeouts.js';
3
4
  export class Neo4jService {
4
5
  driver;
5
6
  constructor() {
@@ -9,20 +10,39 @@ export class Neo4jService {
9
10
  const uri = process.env.NEO4J_URI ?? 'bolt://localhost:7687';
10
11
  const user = process.env.NEO4J_USER ?? 'neo4j';
11
12
  const password = process.env.NEO4J_PASSWORD ?? 'PASSWORD';
12
- return neo4j.driver(uri, neo4j.auth.basic(user, password));
13
+ const timeoutConfig = getTimeoutConfig();
14
+ return neo4j.driver(uri, neo4j.auth.basic(user, password), {
15
+ connectionTimeout: timeoutConfig.neo4j.connectionTimeoutMs,
16
+ maxTransactionRetryTime: timeoutConfig.neo4j.queryTimeoutMs,
17
+ });
13
18
  }
14
19
  async run(query, params = {}) {
15
20
  const session = this.driver.session();
21
+ const timeoutConfig = getTimeoutConfig();
16
22
  try {
17
- const result = await session.run(query, params);
23
+ const result = await session.run(query, params, {
24
+ timeout: timeoutConfig.neo4j.queryTimeoutMs,
25
+ });
18
26
  return result.records.map((record) => record.toObject());
19
27
  }
20
28
  catch (error) {
29
+ // Provide helpful error message for timeout
30
+ if (error.code === 'Neo.TransientError.Transaction.Terminated') {
31
+ throw new Error(`Neo4j query timed out after ${timeoutConfig.neo4j.queryTimeoutMs}ms. ` +
32
+ 'Consider simplifying the query or increasing NEO4J_QUERY_TIMEOUT_MS.');
33
+ }
21
34
  console.error('Error running query:', error);
22
35
  throw error;
23
36
  }
24
37
  finally {
25
- await session.close();
38
+ // Wrap session close in try-catch to avoid masking the original error
39
+ try {
40
+ await session.close();
41
+ }
42
+ catch (closeError) {
43
+ // Log but don't re-throw to preserve original error
44
+ console.warn('Error closing Neo4j session:', closeError);
45
+ }
26
46
  }
27
47
  }
28
48
  getDriver() {
@@ -30,15 +50,34 @@ export class Neo4jService {
30
50
  }
31
51
  async getSchema() {
32
52
  const session = this.driver.session();
53
+ const timeoutConfig = getTimeoutConfig();
33
54
  try {
34
- return await session.run(QUERIES.APOC_SCHEMA);
55
+ return await session.run(QUERIES.APOC_SCHEMA, {}, {
56
+ timeout: timeoutConfig.neo4j.queryTimeoutMs,
57
+ });
35
58
  }
36
59
  catch (error) {
37
60
  console.error('Error fetching schema:', error);
38
61
  throw error;
39
62
  }
40
63
  finally {
41
- await session.close();
64
+ // Wrap session close in try-catch to avoid masking the original error
65
+ try {
66
+ await session.close();
67
+ }
68
+ catch (closeError) {
69
+ // Log but don't re-throw to preserve original error
70
+ console.warn('Error closing Neo4j session:', closeError);
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * Close the Neo4j driver connection.
76
+ * Should be called when the service is no longer needed to release resources.
77
+ */
78
+ async close() {
79
+ if (this.driver) {
80
+ await this.driver.close();
42
81
  }
43
82
  }
44
83
  }
@@ -47,7 +86,34 @@ export const QUERIES = {
47
86
  CALL apoc.meta.schema() YIELD value
48
87
  RETURN value as schema
49
88
  `,
50
- CLEAR_DATABASE: 'MATCH (n) DETACH DELETE n',
89
+ // Project-scoped deletion - only deletes nodes for the specified project
90
+ // Uses APOC batched deletion to avoid transaction memory limits on large projects
91
+ CLEAR_PROJECT: `
92
+ CALL apoc.periodic.iterate(
93
+ 'MATCH (n) WHERE n.projectId = $projectId RETURN n',
94
+ 'DETACH DELETE n',
95
+ {batchSize: 1000, params: {projectId: $projectId}}
96
+ )
97
+ YIELD batches, total
98
+ RETURN batches, total
99
+ `,
100
+ // Full database clear - use with caution, clears ALL projects
101
+ // Uses APOC batched deletion to avoid transaction memory limits
102
+ CLEAR_DATABASE: `
103
+ CALL apoc.periodic.iterate(
104
+ 'MATCH (n) RETURN n',
105
+ 'DETACH DELETE n',
106
+ {batchSize: 1000}
107
+ )
108
+ YIELD batches, total
109
+ RETURN batches, total
110
+ `,
111
+ // Create indexes on projectId for efficient filtering across key node types
112
+ CREATE_PROJECT_INDEX_EMBEDDED: 'CREATE INDEX project_embedded_idx IF NOT EXISTS FOR (n:Embedded) ON (n.projectId)',
113
+ CREATE_PROJECT_INDEX_SOURCEFILE: 'CREATE INDEX project_sourcefile_idx IF NOT EXISTS FOR (n:SourceFile) ON (n.projectId)',
114
+ // Create composite indexes on projectId + id for efficient lookups
115
+ CREATE_PROJECT_ID_INDEX_EMBEDDED: 'CREATE INDEX project_id_embedded_idx IF NOT EXISTS FOR (n:Embedded) ON (n.projectId, n.id)',
116
+ CREATE_PROJECT_ID_INDEX_SOURCEFILE: 'CREATE INDEX project_id_sourcefile_idx IF NOT EXISTS FOR (n:SourceFile) ON (n.projectId, n.id)',
51
117
  CREATE_NODE: `
52
118
  UNWIND $nodes AS nodeData
53
119
  CALL apoc.create.node(nodeData.labels, nodeData.properties) YIELD node
@@ -55,16 +121,17 @@ export const QUERIES = {
55
121
  `,
56
122
  CREATE_RELATIONSHIP: `
57
123
  UNWIND $edges AS edgeData
58
- MATCH (start) WHERE start.id = edgeData.startNodeId
59
- MATCH (end) WHERE end.id = edgeData.endNodeId
124
+ MATCH (start) WHERE start.id = edgeData.startNodeId AND start.projectId = $projectId
125
+ MATCH (end) WHERE end.id = edgeData.endNodeId AND end.projectId = $projectId
60
126
  WITH start, end, edgeData
61
127
  CALL apoc.create.relationship(start, edgeData.type, edgeData.properties, end) YIELD rel
62
128
  RETURN count(*) as created
63
129
  `,
64
130
  CREATE_INDEX: (label, property) => `CREATE INDEX IF NOT EXISTS FOR (n:${label}) ON (n.${property})`,
65
131
  GET_STATS: `
66
- MATCH (n)
67
- RETURN labels(n)[0] as nodeType, count(*) as count
132
+ MATCH (n)
133
+ WHERE n.projectId = $projectId
134
+ RETURN labels(n)[0] as nodeType, count(*) as count
68
135
  ORDER BY count DESC
69
136
  `,
70
137
  CREATE_EMBEDDED_VECTOR_INDEX: `
@@ -75,9 +142,16 @@ export const QUERIES = {
75
142
  \`vector.similarity_function\`: 'cosine'
76
143
  }}
77
144
  `,
145
+ // Vector search with configurable fetch multiplier for project filtering.
146
+ // fetchMultiplier (default: 10) controls how many extra results to fetch before filtering by projectId.
147
+ // minSimilarity (default: 0.3) filters out low-confidence matches for nonsense queries.
148
+ // Higher values = more accurate results but slower; lower values = faster but may miss results.
78
149
  VECTOR_SEARCH: `
79
- CALL db.index.vector.queryNodes('embedded_nodes_idx', $limit, $embedding)
150
+ CALL db.index.vector.queryNodes('embedded_nodes_idx', toInteger($limit * coalesce($fetchMultiplier, 10)), $embedding)
80
151
  YIELD node, score
152
+ WHERE node.projectId = $projectId AND score >= coalesce($minSimilarity, 0.3)
153
+ WITH node, score
154
+ LIMIT toInteger($limit)
81
155
  RETURN {
82
156
  id: node.id,
83
157
  labels: labels(node),
@@ -93,18 +167,22 @@ export const QUERIES = {
93
167
  `,
94
168
  GET_SOURCE_FILE_TRACKING_INFO: `
95
169
  MATCH (sf:SourceFile)
96
- RETURN sf.filePath AS filePath, sf.mtime AS mtime, sf.size AS size, sf.contentHash AS contentHash
170
+ WHERE sf.projectId = $projectId
171
+ RETURN sf.filePath AS filePath,
172
+ COALESCE(sf.mtime, 0) AS mtime,
173
+ COALESCE(sf.size, 0) AS size,
174
+ COALESCE(sf.contentHash, '') AS contentHash
97
175
  `,
98
176
  // Get cross-file edges before deletion (edges where one endpoint is outside the subgraph)
99
177
  // These will be recreated after import using deterministic IDs
100
178
  GET_CROSS_FILE_EDGES: `
101
179
  MATCH (sf:SourceFile)
102
- WHERE sf.filePath IN $filePaths
180
+ WHERE sf.filePath IN $filePaths AND sf.projectId = $projectId
103
181
  OPTIONAL MATCH (sf)-[*]->(child)
104
182
  WITH collect(DISTINCT sf) + collect(DISTINCT child) AS nodesToDelete
105
183
  UNWIND nodesToDelete AS n
106
184
  MATCH (n)-[r]-(other)
107
- WHERE NOT other IN nodesToDelete
185
+ WHERE NOT other IN nodesToDelete AND other.projectId = $projectId
108
186
  RETURN DISTINCT
109
187
  startNode(r).id AS startNodeId,
110
188
  endNode(r).id AS endNodeId,
@@ -114,7 +192,7 @@ export const QUERIES = {
114
192
  // Delete source file subgraphs (nodes and all their edges)
115
193
  DELETE_SOURCE_FILE_SUBGRAPHS: `
116
194
  MATCH (sf:SourceFile)
117
- WHERE sf.filePath IN $filePaths
195
+ WHERE sf.filePath IN $filePaths AND sf.projectId = $projectId
118
196
  OPTIONAL MATCH (sf)-[*]->(child)
119
197
  DETACH DELETE sf, child
120
198
  `,
@@ -122,23 +200,21 @@ export const QUERIES = {
122
200
  RECREATE_CROSS_FILE_EDGES: `
123
201
  UNWIND $edges AS edge
124
202
  MATCH (startNode {id: edge.startNodeId})
203
+ WHERE startNode.projectId = $projectId
125
204
  MATCH (endNode {id: edge.endNodeId})
205
+ WHERE endNode.projectId = $projectId
126
206
  CALL apoc.create.relationship(startNode, edge.edgeType, edge.edgeProperties, endNode) YIELD rel
127
207
  RETURN count(rel) AS recreatedCount
128
208
  `,
129
- // Clean up dangling edges (edges pointing to non-existent nodes)
130
- // Run after incremental parse to remove edges to renamed/deleted nodes
131
- CLEANUP_DANGLING_EDGES: `
132
- MATCH ()-[r]->()
133
- WHERE startNode(r) IS NULL OR endNode(r) IS NULL
134
- DELETE r
135
- RETURN count(r) AS deletedCount
136
- `,
209
+ // Note: Dangling edge cleanup is not needed because:
210
+ // 1. DETACH DELETE removes all edges when deleting nodes
211
+ // 2. Edges cannot exist without both endpoints in Neo4j
212
+ // The previous query (WHERE startNode(r) IS NULL OR endNode(r) IS NULL) could never match anything
137
213
  // Get existing nodes (excluding files being reparsed) for edge target matching
138
214
  // Returns minimal info needed for edge detection: id, name, coreType, semanticType
139
215
  GET_EXISTING_NODES_FOR_EDGE_DETECTION: `
140
216
  MATCH (sf:SourceFile)-[*]->(n)
141
- WHERE NOT sf.filePath IN $excludeFilePaths
217
+ WHERE NOT sf.filePath IN $excludeFilePaths AND sf.projectId = $projectId
142
218
  RETURN n.id AS id,
143
219
  n.name AS name,
144
220
  n.coreType AS coreType,
@@ -149,29 +225,45 @@ export const QUERIES = {
149
225
  EXPLORE_ALL_CONNECTIONS: (maxDepth = MAX_TRAVERSAL_DEPTH, direction = 'BOTH', relationshipTypes) => {
150
226
  const safeMaxDepth = Math.min(Math.max(maxDepth, 1), MAX_TRAVERSAL_DEPTH);
151
227
  // Build relationship pattern based on direction
228
+ // For INCOMING, we reverse the match order: (connected)-[*]->(start) instead of (start)<-[*]-(connected)
229
+ // This is because Neo4j variable-length patterns like <-[*1..N]- require ALL edges to point toward start,
230
+ // but in multi-hop paths (A→B→C), intermediate edges (A→B) don't point toward C, causing 0 results.
152
231
  let relPattern = '';
232
+ let isReversed = false;
153
233
  if (direction === 'OUTGOING') {
154
234
  relPattern = `-[*1..${safeMaxDepth}]->`;
155
235
  }
156
236
  else if (direction === 'INCOMING') {
157
- relPattern = `<-[*1..${safeMaxDepth}]-`;
237
+ relPattern = `-[*1..${safeMaxDepth}]->`; // Same pattern as OUTGOING
238
+ isReversed = true; // But we'll reverse start/connected in MATCH
158
239
  }
159
240
  else {
160
241
  relPattern = `-[*1..${safeMaxDepth}]-`;
161
242
  }
162
243
  // Build relationship type filter if specified
244
+ // SECURITY: Validate relationship types to prevent Cypher injection
245
+ // Only allow uppercase letters and underscores (valid Neo4j relationship type format)
163
246
  let relTypeFilter = '';
164
247
  if (relationshipTypes && relationshipTypes.length > 0) {
165
- const types = relationshipTypes.map((t) => `'${t}'`).join(', ');
166
- relTypeFilter = `AND all(rel in relationships(path) WHERE type(rel) IN [${types}])`;
248
+ const validRelTypePattern = /^[A-Z_]+$/;
249
+ const validatedTypes = relationshipTypes.filter((t) => validRelTypePattern.test(t));
250
+ if (validatedTypes.length !== relationshipTypes.length) {
251
+ console.warn('Some relationship types were filtered out due to invalid format. Valid format: uppercase letters and underscores only.');
252
+ }
253
+ if (validatedTypes.length > 0) {
254
+ const types = validatedTypes.map((t) => `'${t}'`).join(', ');
255
+ relTypeFilter = `AND all(rel in relationships(path) WHERE type(rel) IN [${types}])`;
256
+ }
167
257
  }
258
+ // For INCOMING, reverse the match: (connected)-[*]->(start) finds nodes that can REACH start
259
+ const matchPattern = isReversed ? `(connected)${relPattern}(start)` : `(start)${relPattern}(connected)`;
168
260
  return `
169
- MATCH (start) WHERE start.id = $nodeId
261
+ MATCH (start) WHERE start.id = $nodeId AND start.projectId = $projectId
170
262
 
171
263
  CALL {
172
264
  WITH start
173
- MATCH path = (start)${relPattern}(connected)
174
- WHERE connected <> start
265
+ MATCH path = ${matchPattern}
266
+ WHERE connected <> start AND connected.projectId = $projectId
175
267
  ${relTypeFilter}
176
268
  WITH path, connected, length(path) as depth
177
269
 
@@ -267,13 +359,13 @@ export const QUERIES = {
267
359
  return `
268
360
  // Unwind the source nodes we're exploring from
269
361
  UNWIND $sourceNodeIds AS sourceId
270
- MATCH (source) WHERE source.id = sourceId
362
+ MATCH (source) WHERE source.id = sourceId AND source.projectId = $projectId
271
363
 
272
364
  // Find immediate neighbors (exactly 1 hop)
273
365
  MATCH (source)${relPattern}(neighbor)
274
366
 
275
- // Filter: skip already visited nodes to avoid cycles
276
- WHERE NOT neighbor.id IN $visitedNodeIds
367
+ // Filter: skip already visited nodes and ensure same project
368
+ WHERE NOT neighbor.id IN $visitedNodeIds AND neighbor.projectId = $projectId
277
369
 
278
370
  // Calculate the three scoring components
279
371
  WITH source, neighbor, rel,
@@ -326,7 +418,154 @@ export const QUERIES = {
326
418
 
327
419
  // Sort by score and limit to top N per depth
328
420
  ORDER BY combinedScore DESC
329
- LIMIT ${maxNodesPerDepth}
421
+ LIMIT toInteger(${maxNodesPerDepth})
330
422
  `;
331
423
  },
424
+ // ============================================
425
+ // DYNAMIC SCHEMA DISCOVERY QUERIES
426
+ // ============================================
427
+ /**
428
+ * Get all distinct node labels with counts and sample properties
429
+ */
430
+ DISCOVER_NODE_TYPES: `
431
+ CALL db.labels() YIELD label
432
+ CALL {
433
+ WITH label
434
+ MATCH (n) WHERE label IN labels(n) AND n.projectId = $projectId
435
+ WITH n LIMIT 1
436
+ RETURN keys(n) AS sampleProperties
437
+ }
438
+ CALL {
439
+ WITH label
440
+ MATCH (n) WHERE label IN labels(n) AND n.projectId = $projectId
441
+ RETURN count(n) AS nodeCount
442
+ }
443
+ RETURN label, nodeCount, sampleProperties
444
+ ORDER BY nodeCount DESC
445
+ `,
446
+ /**
447
+ * Get all distinct relationship types with counts and which node types they connect
448
+ */
449
+ DISCOVER_RELATIONSHIP_TYPES: `
450
+ CALL db.relationshipTypes() YIELD relationshipType
451
+ CALL {
452
+ WITH relationshipType
453
+ MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId AND b.projectId = $projectId
454
+ WITH labels(a)[0] AS fromLabel, labels(b)[0] AS toLabel
455
+ RETURN fromLabel, toLabel
456
+ LIMIT 10
457
+ }
458
+ CALL {
459
+ WITH relationshipType
460
+ MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId
461
+ RETURN count(r) AS relCount
462
+ }
463
+ RETURN relationshipType, relCount, collect(DISTINCT {from: fromLabel, to: toLabel}) AS connections
464
+ ORDER BY relCount DESC
465
+ `,
466
+ /**
467
+ * Get sample nodes of each semantic type for context
468
+ */
469
+ DISCOVER_SEMANTIC_TYPES: `
470
+ MATCH (n)
471
+ WHERE n.semanticType IS NOT NULL AND n.projectId = $projectId
472
+ WITH n.semanticType AS semanticType, count(*) AS count
473
+ ORDER BY count DESC
474
+ RETURN semanticType, count
475
+ `,
476
+ /**
477
+ * Get example query patterns based on actual graph structure
478
+ */
479
+ DISCOVER_COMMON_PATTERNS: `
480
+ MATCH (a)-[r]->(b)
481
+ WHERE a.projectId = $projectId AND b.projectId = $projectId
482
+ WITH labels(a)[0] AS fromType, type(r) AS relType, labels(b)[0] AS toType, count(*) AS count
483
+ WHERE count > 5
484
+ RETURN fromType, relType, toType, count
485
+ ORDER BY count DESC
486
+ LIMIT 20
487
+ `,
488
+ // ============================================
489
+ // IMPACT ANALYSIS QUERIES
490
+ // Reuses cross-file edge pattern to find dependents
491
+ // ============================================
492
+ /**
493
+ * Get node details by ID
494
+ */
495
+ GET_NODE_BY_ID: `
496
+ MATCH (n) WHERE n.id = $nodeId AND n.projectId = $projectId
497
+ RETURN n.id AS id,
498
+ n.name AS name,
499
+ labels(n) AS labels,
500
+ n.semanticType AS semanticType,
501
+ n.coreType AS coreType,
502
+ n.filePath AS filePath
503
+ `,
504
+ /**
505
+ * Get impact of changing a node - finds all external nodes that depend on it
506
+ * Based on GET_CROSS_FILE_EDGES pattern but for a single node
507
+ */
508
+ GET_NODE_IMPACT: `
509
+ MATCH (target) WHERE target.id = $nodeId AND target.projectId = $projectId
510
+ MATCH (dependent)-[r]->(target)
511
+ WHERE dependent.id <> target.id AND dependent.projectId = $projectId
512
+ RETURN DISTINCT
513
+ dependent.id AS nodeId,
514
+ dependent.name AS name,
515
+ labels(dependent) AS labels,
516
+ dependent.semanticType AS semanticType,
517
+ dependent.coreType AS coreType,
518
+ dependent.filePath AS filePath,
519
+ type(r) AS relationshipType,
520
+ coalesce(r.relationshipWeight, 0.5) AS weight
521
+ `,
522
+ /**
523
+ * Get impact of changing a file - finds all external nodes that depend on nodes in this file
524
+ * Directly reuses GET_CROSS_FILE_EDGES pattern
525
+ */
526
+ GET_FILE_IMPACT: `
527
+ MATCH (sf:SourceFile)
528
+ WHERE sf.projectId = $projectId
529
+ AND (sf.filePath = $filePath OR sf.filePath ENDS WITH '/' + $filePath)
530
+ MATCH (sf)-[:CONTAINS]->(entity)
531
+ WHERE entity:Class OR entity:Function OR entity:Interface
532
+ WITH collect(DISTINCT entity) AS entitiesInFile, sf.filePath AS sourceFilePath
533
+ UNWIND entitiesInFile AS n
534
+ MATCH (dependent)-[r]->(n)
535
+ WHERE NOT dependent IN entitiesInFile
536
+ AND dependent.projectId = $projectId
537
+ AND dependent.filePath <> sourceFilePath
538
+ RETURN DISTINCT
539
+ dependent.id AS nodeId,
540
+ dependent.name AS name,
541
+ labels(dependent) AS labels,
542
+ dependent.semanticType AS semanticType,
543
+ dependent.coreType AS coreType,
544
+ dependent.filePath AS filePath,
545
+ type(r) AS relationshipType,
546
+ coalesce(r.relationshipWeight, 0.5) AS weight,
547
+ n.id AS targetNodeId,
548
+ n.name AS targetNodeName
549
+ `,
550
+ /**
551
+ * Get transitive dependents - nodes that depend on dependents (for deeper impact)
552
+ */
553
+ GET_TRANSITIVE_DEPENDENTS: (maxDepth = 4) => `
554
+ MATCH (target) WHERE target.id = $nodeId AND target.projectId = $projectId
555
+ MATCH path = (dependent)-[*2..${maxDepth}]->(target)
556
+ WHERE dependent.projectId = $projectId AND all(n IN nodes(path) WHERE n.projectId = $projectId)
557
+ WITH dependent,
558
+ length(path) AS depth,
559
+ [r IN relationships(path) | type(r)] AS relationshipPath
560
+ RETURN DISTINCT
561
+ dependent.id AS nodeId,
562
+ dependent.name AS name,
563
+ labels(dependent) AS labels,
564
+ dependent.semanticType AS semanticType,
565
+ dependent.coreType AS coreType,
566
+ dependent.filePath AS filePath,
567
+ depth,
568
+ relationshipPath
569
+ ORDER BY depth ASC
570
+ `,
332
571
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-context",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "MCP server that builds code graphs to provide rich context to LLMs",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/drewdrewH/code-graph-context#readme",
@@ -49,12 +49,15 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@modelcontextprotocol/sdk": "^1.15.1",
52
+ "@parcel/watcher": "^2.5.1",
52
53
  "commander": "^14.0.0",
53
54
  "dotenv": "^17.2.3",
54
55
  "glob": "^11.0.3",
55
56
  "neo4j": "^2.0.0-RC2",
56
57
  "neo4j-driver": "^5.28.1",
57
58
  "openai": "^5.10.1",
59
+ "ts-morph": "^26.0.0",
60
+ "yaml": "^2.8.2",
58
61
  "zod": "^3.25.76"
59
62
  },
60
63
  "devDependencies": {
@@ -73,7 +76,6 @@
73
76
  "globals": "^16.2.0",
74
77
  "prettier": "^3.5.3",
75
78
  "reflect-metadata": "^0.2.2",
76
- "ts-morph": "^26.0.0",
77
79
  "ts-node": "^10.9.2",
78
80
  "typescript": "^5.8.3",
79
81
  "typescript-eslint": "^8.34.1",