code-graph-context 1.1.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.
- package/README.md +221 -101
- package/dist/core/config/fairsquare-framework-schema.js +47 -60
- package/dist/core/config/nestjs-framework-schema.js +11 -1
- package/dist/core/config/schema.js +1 -1
- package/dist/core/config/timeouts.js +27 -0
- package/dist/core/embeddings/embeddings.service.js +122 -2
- package/dist/core/embeddings/natural-language-to-cypher.service.js +416 -17
- package/dist/core/parsers/parser-factory.js +5 -3
- package/dist/core/parsers/typescript-parser.js +614 -45
- package/dist/core/parsers/workspace-parser.js +553 -0
- package/dist/core/utils/edge-factory.js +37 -0
- package/dist/core/utils/file-change-detection.js +105 -0
- package/dist/core/utils/file-utils.js +20 -0
- package/dist/core/utils/index.js +3 -0
- package/dist/core/utils/path-utils.js +75 -0
- package/dist/core/utils/progress-reporter.js +112 -0
- package/dist/core/utils/project-id.js +176 -0
- package/dist/core/utils/retry.js +41 -0
- package/dist/core/workspace/index.js +4 -0
- package/dist/core/workspace/workspace-detector.js +221 -0
- package/dist/mcp/constants.js +153 -5
- package/dist/mcp/handlers/cross-file-edge.helpers.js +19 -0
- package/dist/mcp/handlers/file-change-detection.js +105 -0
- package/dist/mcp/handlers/graph-generator.handler.js +97 -32
- package/dist/mcp/handlers/incremental-parse.handler.js +146 -0
- package/dist/mcp/handlers/streaming-import.handler.js +210 -0
- package/dist/mcp/handlers/traversal.handler.js +130 -71
- package/dist/mcp/mcp.server.js +45 -6
- package/dist/mcp/service-init.js +79 -0
- package/dist/mcp/services/job-manager.js +165 -0
- package/dist/mcp/services/watch-manager.js +376 -0
- package/dist/mcp/services.js +2 -2
- package/dist/mcp/tools/check-parse-status.tool.js +64 -0
- package/dist/mcp/tools/impact-analysis.tool.js +84 -18
- package/dist/mcp/tools/index.js +13 -1
- package/dist/mcp/tools/list-projects.tool.js +62 -0
- package/dist/mcp/tools/list-watchers.tool.js +51 -0
- package/dist/mcp/tools/natural-language-to-cypher.tool.js +34 -8
- package/dist/mcp/tools/parse-typescript-project.tool.js +318 -58
- package/dist/mcp/tools/search-codebase.tool.js +56 -16
- package/dist/mcp/tools/start-watch-project.tool.js +100 -0
- package/dist/mcp/tools/stop-watch-project.tool.js +49 -0
- package/dist/mcp/tools/traverse-from-node.tool.js +68 -9
- package/dist/mcp/utils.js +35 -13
- package/dist/mcp/workers/parse-worker.js +198 -0
- package/dist/storage/neo4j/neo4j.service.js +147 -48
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
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 =
|
|
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
|
|
166
|
-
|
|
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 =
|
|
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
|
|
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,7 @@ 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
|
},
|
|
332
424
|
// ============================================
|
|
@@ -339,13 +431,13 @@ export const QUERIES = {
|
|
|
339
431
|
CALL db.labels() YIELD label
|
|
340
432
|
CALL {
|
|
341
433
|
WITH label
|
|
342
|
-
MATCH (n) WHERE label IN labels(n)
|
|
434
|
+
MATCH (n) WHERE label IN labels(n) AND n.projectId = $projectId
|
|
343
435
|
WITH n LIMIT 1
|
|
344
436
|
RETURN keys(n) AS sampleProperties
|
|
345
437
|
}
|
|
346
438
|
CALL {
|
|
347
439
|
WITH label
|
|
348
|
-
MATCH (n) WHERE label IN labels(n)
|
|
440
|
+
MATCH (n) WHERE label IN labels(n) AND n.projectId = $projectId
|
|
349
441
|
RETURN count(n) AS nodeCount
|
|
350
442
|
}
|
|
351
443
|
RETURN label, nodeCount, sampleProperties
|
|
@@ -358,14 +450,14 @@ export const QUERIES = {
|
|
|
358
450
|
CALL db.relationshipTypes() YIELD relationshipType
|
|
359
451
|
CALL {
|
|
360
452
|
WITH relationshipType
|
|
361
|
-
MATCH (a)-[r]->(b) WHERE type(r) = relationshipType
|
|
453
|
+
MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId AND b.projectId = $projectId
|
|
362
454
|
WITH labels(a)[0] AS fromLabel, labels(b)[0] AS toLabel
|
|
363
455
|
RETURN fromLabel, toLabel
|
|
364
456
|
LIMIT 10
|
|
365
457
|
}
|
|
366
458
|
CALL {
|
|
367
459
|
WITH relationshipType
|
|
368
|
-
MATCH ()-[r]->() WHERE type(r) = relationshipType
|
|
460
|
+
MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId
|
|
369
461
|
RETURN count(r) AS relCount
|
|
370
462
|
}
|
|
371
463
|
RETURN relationshipType, relCount, collect(DISTINCT {from: fromLabel, to: toLabel}) AS connections
|
|
@@ -376,7 +468,7 @@ export const QUERIES = {
|
|
|
376
468
|
*/
|
|
377
469
|
DISCOVER_SEMANTIC_TYPES: `
|
|
378
470
|
MATCH (n)
|
|
379
|
-
WHERE n.semanticType IS NOT NULL
|
|
471
|
+
WHERE n.semanticType IS NOT NULL AND n.projectId = $projectId
|
|
380
472
|
WITH n.semanticType AS semanticType, count(*) AS count
|
|
381
473
|
ORDER BY count DESC
|
|
382
474
|
RETURN semanticType, count
|
|
@@ -386,6 +478,7 @@ export const QUERIES = {
|
|
|
386
478
|
*/
|
|
387
479
|
DISCOVER_COMMON_PATTERNS: `
|
|
388
480
|
MATCH (a)-[r]->(b)
|
|
481
|
+
WHERE a.projectId = $projectId AND b.projectId = $projectId
|
|
389
482
|
WITH labels(a)[0] AS fromType, type(r) AS relType, labels(b)[0] AS toType, count(*) AS count
|
|
390
483
|
WHERE count > 5
|
|
391
484
|
RETURN fromType, relType, toType, count
|
|
@@ -400,7 +493,7 @@ export const QUERIES = {
|
|
|
400
493
|
* Get node details by ID
|
|
401
494
|
*/
|
|
402
495
|
GET_NODE_BY_ID: `
|
|
403
|
-
MATCH (n) WHERE n.id = $nodeId
|
|
496
|
+
MATCH (n) WHERE n.id = $nodeId AND n.projectId = $projectId
|
|
404
497
|
RETURN n.id AS id,
|
|
405
498
|
n.name AS name,
|
|
406
499
|
labels(n) AS labels,
|
|
@@ -413,9 +506,9 @@ export const QUERIES = {
|
|
|
413
506
|
* Based on GET_CROSS_FILE_EDGES pattern but for a single node
|
|
414
507
|
*/
|
|
415
508
|
GET_NODE_IMPACT: `
|
|
416
|
-
MATCH (target) WHERE target.id = $nodeId
|
|
509
|
+
MATCH (target) WHERE target.id = $nodeId AND target.projectId = $projectId
|
|
417
510
|
MATCH (dependent)-[r]->(target)
|
|
418
|
-
WHERE dependent.id <> target.id
|
|
511
|
+
WHERE dependent.id <> target.id AND dependent.projectId = $projectId
|
|
419
512
|
RETURN DISTINCT
|
|
420
513
|
dependent.id AS nodeId,
|
|
421
514
|
dependent.name AS name,
|
|
@@ -431,12 +524,17 @@ export const QUERIES = {
|
|
|
431
524
|
* Directly reuses GET_CROSS_FILE_EDGES pattern
|
|
432
525
|
*/
|
|
433
526
|
GET_FILE_IMPACT: `
|
|
434
|
-
MATCH (sf:SourceFile
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
438
534
|
MATCH (dependent)-[r]->(n)
|
|
439
|
-
WHERE NOT dependent IN
|
|
535
|
+
WHERE NOT dependent IN entitiesInFile
|
|
536
|
+
AND dependent.projectId = $projectId
|
|
537
|
+
AND dependent.filePath <> sourceFilePath
|
|
440
538
|
RETURN DISTINCT
|
|
441
539
|
dependent.id AS nodeId,
|
|
442
540
|
dependent.name AS name,
|
|
@@ -453,8 +551,9 @@ export const QUERIES = {
|
|
|
453
551
|
* Get transitive dependents - nodes that depend on dependents (for deeper impact)
|
|
454
552
|
*/
|
|
455
553
|
GET_TRANSITIVE_DEPENDENTS: (maxDepth = 4) => `
|
|
456
|
-
MATCH (target) WHERE target.id = $nodeId
|
|
554
|
+
MATCH (target) WHERE target.id = $nodeId AND target.projectId = $projectId
|
|
457
555
|
MATCH path = (dependent)-[*2..${maxDepth}]->(target)
|
|
556
|
+
WHERE dependent.projectId = $projectId AND all(n IN nodes(path) WHERE n.projectId = $projectId)
|
|
458
557
|
WITH dependent,
|
|
459
558
|
length(path) AS depth,
|
|
460
559
|
[r IN relationships(path) | type(r)] AS relationshipPath
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-graph-context",
|
|
3
|
-
"version": "
|
|
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",
|