code-graph-context 0.1.0 → 1.1.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 +79 -0
- package/dist/constants.js +21 -0
- package/dist/core/config/nestjs-framework-schema.js +2 -1
- package/dist/core/config/schema.js +2 -1
- package/dist/core/embeddings/natural-language-to-cypher.service.js +12 -13
- package/dist/core/parsers/parser-factory.js +3 -2
- package/dist/core/parsers/typescript-parser.js +129 -39
- package/dist/mcp/constants.js +19 -2
- package/dist/mcp/mcp.server.js +1 -1
- package/dist/mcp/services.js +48 -127
- package/dist/mcp/tools/impact-analysis.tool.js +253 -0
- package/dist/mcp/tools/index.js +2 -0
- package/dist/mcp/tools/parse-typescript-project.tool.js +127 -42
- package/dist/mcp/tools/search-codebase.tool.js +4 -10
- package/dist/mcp/utils.js +4 -17
- package/dist/storage/neo4j/neo4j.service.js +201 -6
- package/dist/utils/file-utils.js +20 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Code Graph Context MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/code-graph-context)
|
|
3
4
|
[](LICENSE)
|
|
4
5
|
[](https://typescriptlang.org/)
|
|
5
6
|
[](https://neo4j.com/)
|
|
@@ -147,6 +148,84 @@ Then configure in your MCP config file (`~/.config/claude/config.json`):
|
|
|
147
148
|
|
|
148
149
|
**Note:** The env vars can be configured for any Neo4j instance - local, Docker, cloud (Aura), or enterprise.
|
|
149
150
|
|
|
151
|
+
### Verify Installation
|
|
152
|
+
|
|
153
|
+
After installation, verify everything is working:
|
|
154
|
+
|
|
155
|
+
1. **Check Neo4j is running:**
|
|
156
|
+
```bash
|
|
157
|
+
# Open Neo4j Browser
|
|
158
|
+
open http://localhost:7474
|
|
159
|
+
# Login: neo4j / PASSWORD
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
2. **Test APOC plugin:**
|
|
163
|
+
```cypher
|
|
164
|
+
CALL apoc.help("apoc")
|
|
165
|
+
```
|
|
166
|
+
Should return a list of APOC functions.
|
|
167
|
+
|
|
168
|
+
3. **Test MCP server connection:**
|
|
169
|
+
```bash
|
|
170
|
+
claude mcp list
|
|
171
|
+
```
|
|
172
|
+
Should show: `code-graph-context: ✓ Connected`
|
|
173
|
+
|
|
174
|
+
### Troubleshooting
|
|
175
|
+
|
|
176
|
+
**"APOC plugin not found"**
|
|
177
|
+
```bash
|
|
178
|
+
# Check Neo4j logs
|
|
179
|
+
docker logs code-graph-neo4j
|
|
180
|
+
|
|
181
|
+
# Verify APOC loaded
|
|
182
|
+
docker exec code-graph-neo4j cypher-shell -u neo4j -p PASSWORD "CALL apoc.help('apoc')"
|
|
183
|
+
|
|
184
|
+
# Restart if needed
|
|
185
|
+
docker restart code-graph-neo4j
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**"OPENAI_API_KEY environment variable is required"**
|
|
189
|
+
- Get your API key from: https://platform.openai.com/api-keys
|
|
190
|
+
- Add to Claude Code MCP config `env` section
|
|
191
|
+
- Verify with: `echo $OPENAI_API_KEY` (if using shell env)
|
|
192
|
+
|
|
193
|
+
**"Connection refused bolt://localhost:7687"**
|
|
194
|
+
```bash
|
|
195
|
+
# Check Neo4j is running
|
|
196
|
+
docker ps | grep neo4j
|
|
197
|
+
|
|
198
|
+
# Check ports are not in use
|
|
199
|
+
lsof -i :7687
|
|
200
|
+
|
|
201
|
+
# Start Neo4j if stopped
|
|
202
|
+
docker start code-graph-neo4j
|
|
203
|
+
|
|
204
|
+
# Check Neo4j logs
|
|
205
|
+
docker logs code-graph-neo4j
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**"Neo4j memory errors"**
|
|
209
|
+
```bash
|
|
210
|
+
# Increase memory in docker-compose.yml or docker run:
|
|
211
|
+
-e NEO4J_server_memory_heap_max__size=8G
|
|
212
|
+
-e NEO4J_dbms_memory_transaction_total_max=8G
|
|
213
|
+
|
|
214
|
+
docker restart code-graph-neo4j
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**"MCP server not responding"**
|
|
218
|
+
```bash
|
|
219
|
+
# Check Claude Code logs
|
|
220
|
+
cat ~/Library/Logs/Claude/mcp*.log
|
|
221
|
+
|
|
222
|
+
# Test server directly
|
|
223
|
+
node /path/to/code-graph-context/dist/mcp/mcp.server.js
|
|
224
|
+
|
|
225
|
+
# Rebuild if needed
|
|
226
|
+
npm run build
|
|
227
|
+
```
|
|
228
|
+
|
|
150
229
|
## Tool Usage Guide & Sequential Workflows
|
|
151
230
|
|
|
152
231
|
### Sequential Tool Usage Patterns
|
package/dist/constants.js
CHANGED
|
@@ -1 +1,22 @@
|
|
|
1
1
|
export const MAX_TRAVERSAL_DEPTH = 5;
|
|
2
|
+
// Shared exclude patterns for file parsing and change detection
|
|
3
|
+
// Regex patterns (escaped dots, anchored to end)
|
|
4
|
+
export const EXCLUDE_PATTERNS_REGEX = [
|
|
5
|
+
'node_modules/',
|
|
6
|
+
'dist/',
|
|
7
|
+
'build/',
|
|
8
|
+
'coverage/',
|
|
9
|
+
'\\.d\\.ts$',
|
|
10
|
+
'\\.spec\\.ts$',
|
|
11
|
+
'\\.test\\.ts$',
|
|
12
|
+
];
|
|
13
|
+
// Glob patterns for use with glob library
|
|
14
|
+
export const EXCLUDE_PATTERNS_GLOB = [
|
|
15
|
+
'node_modules/**',
|
|
16
|
+
'dist/**',
|
|
17
|
+
'build/**',
|
|
18
|
+
'coverage/**',
|
|
19
|
+
'**/*.d.ts',
|
|
20
|
+
'**/*.spec.ts',
|
|
21
|
+
'**/*.test.ts',
|
|
22
|
+
];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable prefer-arrow/prefer-arrow-functions */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import { EXCLUDE_PATTERNS_REGEX } from '../../constants.js';
|
|
3
4
|
import { CoreNodeType, SemanticNodeType, SemanticEdgeType, } from './schema.js';
|
|
4
5
|
// ============================================================================
|
|
5
6
|
// NESTJS HELPER FUNCTIONS
|
|
@@ -888,7 +889,7 @@ export const NESTJS_FRAMEWORK_SCHEMA = {
|
|
|
888
889
|
// ============================================================================
|
|
889
890
|
export const NESTJS_PARSE_OPTIONS = {
|
|
890
891
|
includePatterns: ['**/*.ts', '**/*.tsx'],
|
|
891
|
-
excludePatterns:
|
|
892
|
+
excludePatterns: EXCLUDE_PATTERNS_REGEX,
|
|
892
893
|
maxFiles: 1000,
|
|
893
894
|
frameworkSchemas: [NESTJS_FRAMEWORK_SCHEMA],
|
|
894
895
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
// graph.ts - Optimized for Neo4j performance with context-based framework properties
|
|
3
|
+
import { EXCLUDE_PATTERNS_REGEX } from '../../constants.js';
|
|
3
4
|
// ============================================================================
|
|
4
5
|
// CORE ENUMS
|
|
5
6
|
// ============================================================================
|
|
@@ -792,7 +793,7 @@ export const CORE_TYPESCRIPT_SCHEMA = {
|
|
|
792
793
|
};
|
|
793
794
|
export const DEFAULT_PARSE_OPTIONS = {
|
|
794
795
|
includePatterns: ['**/*.ts', '**/*.tsx'],
|
|
795
|
-
excludePatterns:
|
|
796
|
+
excludePatterns: EXCLUDE_PATTERNS_REGEX,
|
|
796
797
|
maxFiles: 1000,
|
|
797
798
|
coreSchema: CORE_TYPESCRIPT_SCHEMA,
|
|
798
799
|
frameworkSchemas: [],
|
|
@@ -7,10 +7,11 @@ export class NaturalLanguageToCypherService {
|
|
|
7
7
|
messageInstructions = `
|
|
8
8
|
The schema file (neo4j-apoc-schema.json) contains two sections:
|
|
9
9
|
1. rawSchema: Complete Neo4j APOC schema with all node labels, properties, and relationships in the graph
|
|
10
|
-
2.
|
|
11
|
-
- nodeTypes:
|
|
12
|
-
-
|
|
13
|
-
-
|
|
10
|
+
2. discoveredSchema: Dynamically discovered graph structure including:
|
|
11
|
+
- nodeTypes: Array of {label, count, properties} for each node type in the graph
|
|
12
|
+
- relationshipTypes: Array of {type, count, connections} showing relationship types and what they connect
|
|
13
|
+
- semanticTypes: Array of {type, count} showing semantic node classifications (e.g., Service, Controller)
|
|
14
|
+
- commonPatterns: Array of {from, relationship, to, count} showing frequent relationship patterns
|
|
14
15
|
|
|
15
16
|
Your response must be a valid JSON object with this exact structure:
|
|
16
17
|
{
|
|
@@ -20,19 +21,17 @@ Your response must be a valid JSON object with this exact structure:
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
Query Generation Process:
|
|
23
|
-
1. CHECK
|
|
24
|
-
2.
|
|
25
|
-
3. CHECK
|
|
26
|
-
4.
|
|
27
|
-
5.
|
|
24
|
+
1. CHECK NODE TYPES: Look at discoveredSchema.nodeTypes to see available node labels and their properties
|
|
25
|
+
2. CHECK RELATIONSHIPS: Look at discoveredSchema.relationshipTypes to understand how nodes connect
|
|
26
|
+
3. CHECK SEMANTIC TYPES: Look at discoveredSchema.semanticTypes for higher-level node classifications
|
|
27
|
+
4. REVIEW PATTERNS: Check discoveredSchema.commonPatterns for frequent relationship patterns in the graph
|
|
28
|
+
5. EXAMINE PROPERTIES: Use rawSchema for exact property names and types
|
|
28
29
|
6. GENERATE QUERY: Write the Cypher query using only node labels, relationships, and properties that exist in the schema
|
|
29
30
|
|
|
30
31
|
Critical Rules:
|
|
31
32
|
- Use the schema information from the file_search tool - do not guess node labels or relationships
|
|
32
|
-
- Use ONLY node labels and properties found in
|
|
33
|
-
- For nested JSON data in properties, use: apoc.convert.fromJsonMap(node.propertyName)
|
|
34
|
-
- Check domainContext for parsing instructions specific to certain node types (e.g., some nodes may store arrays of objects in JSON format)
|
|
35
|
-
- Follow the example queries in commonQueryPatterns for proper syntax patterns
|
|
33
|
+
- Use ONLY node labels and properties found in the schema
|
|
34
|
+
- For nested JSON data in properties, use: apoc.convert.fromJsonMap(node.propertyName)
|
|
36
35
|
- Use parameterized queries with $ syntax for any dynamic values
|
|
37
36
|
- Return only the data relevant to the user's request
|
|
38
37
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Parser Factory
|
|
3
3
|
* Creates TypeScript parsers with appropriate framework schemas
|
|
4
4
|
*/
|
|
5
|
+
import { EXCLUDE_PATTERNS_REGEX } from '../../constants.js';
|
|
5
6
|
import { FAIRSQUARE_FRAMEWORK_SCHEMA } from '../config/fairsquare-framework-schema.js';
|
|
6
7
|
import { NESTJS_FRAMEWORK_SCHEMA } from '../config/nestjs-framework-schema.js';
|
|
7
8
|
import { CORE_TYPESCRIPT_SCHEMA, CoreNodeType } from '../config/schema.js';
|
|
@@ -19,7 +20,7 @@ export class ParserFactory {
|
|
|
19
20
|
*/
|
|
20
21
|
static createParser(options) {
|
|
21
22
|
const { workspacePath, tsConfigPath = 'tsconfig.json', projectType = ProjectType.NESTJS, // Default to NestJS (use auto-detect for best results)
|
|
22
|
-
customFrameworkSchemas = [], excludePatterns =
|
|
23
|
+
customFrameworkSchemas = [], excludePatterns = EXCLUDE_PATTERNS_REGEX, excludedNodeTypes = [CoreNodeType.PARAMETER_DECLARATION], } = options;
|
|
23
24
|
// Select framework schemas based on project type
|
|
24
25
|
const frameworkSchemas = this.selectFrameworkSchemas(projectType, customFrameworkSchemas);
|
|
25
26
|
console.log(`📦 Creating parser for ${projectType} project`);
|
|
@@ -68,7 +69,7 @@ export class ParserFactory {
|
|
|
68
69
|
...packageJson.devDependencies,
|
|
69
70
|
};
|
|
70
71
|
const hasNestJS = '@nestjs/common' in deps || '@nestjs/core' in deps;
|
|
71
|
-
const hasFairSquare = '@fairsquare/core' in deps || '@fairsquare/server' in deps;
|
|
72
|
+
const hasFairSquare = '@fairsquare/core' in deps || '@fairsquare/server' in deps || packageJson.name === '@fairsquare/source';
|
|
72
73
|
if (hasFairSquare && hasNestJS) {
|
|
73
74
|
return ProjectType.BOTH;
|
|
74
75
|
}
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import fs from 'fs/promises';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
import { minimatch } from 'minimatch';
|
|
4
6
|
import { Project, Node } from 'ts-morph';
|
|
5
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Generate a deterministic node ID based on stable properties.
|
|
9
|
+
* This ensures the same node gets the same ID across reparses.
|
|
10
|
+
*
|
|
11
|
+
* Identity is based on: coreType + filePath + name (+ parentId for nested nodes)
|
|
12
|
+
* This is stable because when it matters (one side of edge not reparsed),
|
|
13
|
+
* names are guaranteed unchanged (or imports would break, triggering reparse).
|
|
14
|
+
*/
|
|
15
|
+
const generateDeterministicId = (coreType, filePath, name, parentId) => {
|
|
16
|
+
const parts = parentId ? [coreType, filePath, parentId, name] : [coreType, filePath, name];
|
|
17
|
+
const identity = parts.join('::');
|
|
18
|
+
const hash = crypto.createHash('sha256').update(identity).digest('hex').substring(0, 16);
|
|
19
|
+
return `${coreType}:${hash}`;
|
|
20
|
+
};
|
|
21
|
+
import { hashFile } from '../../utils/file-utils.js';
|
|
6
22
|
import { NESTJS_FRAMEWORK_SCHEMA } from '../config/nestjs-framework-schema.js';
|
|
7
23
|
import { CoreNodeType, CORE_TYPESCRIPT_SCHEMA, DEFAULT_PARSE_OPTIONS, CoreEdgeType, } from '../config/schema.js';
|
|
8
24
|
export class TypeScriptParser {
|
|
@@ -14,6 +30,7 @@ export class TypeScriptParser {
|
|
|
14
30
|
frameworkSchemas;
|
|
15
31
|
parsedNodes = new Map();
|
|
16
32
|
parsedEdges = new Map();
|
|
33
|
+
existingNodes = new Map(); // Nodes from Neo4j for edge target matching
|
|
17
34
|
deferredEdges = [];
|
|
18
35
|
sharedContext = new Map(); // Shared context for custom data
|
|
19
36
|
constructor(workspacePath, tsConfigPath = 'tsconfig.json', coreSchema = CORE_TYPESCRIPT_SCHEMA, frameworkSchemas = [NESTJS_FRAMEWORK_SCHEMA], parseConfig = DEFAULT_PARSE_OPTIONS) {
|
|
@@ -22,7 +39,6 @@ export class TypeScriptParser {
|
|
|
22
39
|
this.coreSchema = coreSchema;
|
|
23
40
|
this.frameworkSchemas = frameworkSchemas;
|
|
24
41
|
this.parseConfig = parseConfig;
|
|
25
|
-
// Initialize with proper compiler options for NestJS
|
|
26
42
|
this.project = new Project({
|
|
27
43
|
tsConfigFilePath: tsConfigPath,
|
|
28
44
|
skipAddingFilesFromTsConfig: false,
|
|
@@ -36,25 +52,53 @@ export class TypeScriptParser {
|
|
|
36
52
|
});
|
|
37
53
|
this.project.addSourceFilesAtPaths(path.join(workspacePath, '**/*.ts'));
|
|
38
54
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Set existing nodes from Neo4j for edge target matching during incremental parsing.
|
|
57
|
+
* These nodes will be available as targets for edge detection but won't be exported.
|
|
58
|
+
*/
|
|
59
|
+
setExistingNodes(nodes) {
|
|
60
|
+
this.existingNodes.clear();
|
|
61
|
+
for (const node of nodes) {
|
|
62
|
+
// Convert to ParsedNode format (without AST)
|
|
63
|
+
const parsedNode = {
|
|
64
|
+
id: node.id,
|
|
65
|
+
coreType: node.coreType,
|
|
66
|
+
semanticType: node.semanticType,
|
|
67
|
+
labels: node.labels,
|
|
68
|
+
properties: {
|
|
69
|
+
id: node.id,
|
|
70
|
+
name: node.name,
|
|
71
|
+
coreType: node.coreType,
|
|
72
|
+
filePath: node.filePath,
|
|
73
|
+
semanticType: node.semanticType,
|
|
74
|
+
},
|
|
75
|
+
// No sourceNode - these are from Neo4j, not parsed
|
|
76
|
+
};
|
|
77
|
+
this.existingNodes.set(node.id, parsedNode);
|
|
78
|
+
}
|
|
79
|
+
console.log(`📦 Loaded ${nodes.length} existing nodes for edge detection`);
|
|
80
|
+
}
|
|
81
|
+
async parseWorkspace(filesToParse) {
|
|
82
|
+
let sourceFiles;
|
|
83
|
+
if (filesToParse && filesToParse.length > 0) {
|
|
84
|
+
sourceFiles = filesToParse
|
|
85
|
+
.map((filePath) => this.project.getSourceFile(filePath))
|
|
86
|
+
.filter((sf) => sf !== undefined);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
sourceFiles = this.project.getSourceFiles();
|
|
90
|
+
}
|
|
42
91
|
for (const sourceFile of sourceFiles) {
|
|
43
92
|
if (this.shouldSkipFile(sourceFile))
|
|
44
93
|
continue;
|
|
45
94
|
await this.parseCoreTypeScriptV2(sourceFile);
|
|
46
95
|
}
|
|
47
|
-
// Phase 1.5: Resolve deferred relationship edges (EXTENDS, IMPLEMENTS)
|
|
48
96
|
this.resolveDeferredEdges();
|
|
49
|
-
// Phase 2: Apply context extractors
|
|
50
97
|
await this.applyContextExtractors();
|
|
51
|
-
// Phase 3: Framework enhancements
|
|
52
98
|
if (this.frameworkSchemas.length > 0) {
|
|
53
99
|
await this.applyFrameworkEnhancements();
|
|
54
100
|
}
|
|
55
|
-
// Phase 4: Edge enhancements
|
|
56
101
|
await this.applyEdgeEnhancements();
|
|
57
|
-
// Convert to Neo4j format
|
|
58
102
|
const neo4jNodes = Array.from(this.parsedNodes.values()).map(this.toNeo4jNode);
|
|
59
103
|
const neo4jEdges = Array.from(this.parsedEdges.values()).map(this.toNeo4jEdge);
|
|
60
104
|
return { nodes: neo4jNodes, edges: neo4jEdges };
|
|
@@ -77,17 +121,22 @@ export class TypeScriptParser {
|
|
|
77
121
|
return false;
|
|
78
122
|
}
|
|
79
123
|
async parseCoreTypeScriptV2(sourceFile) {
|
|
80
|
-
const
|
|
124
|
+
const filePath = sourceFile.getFilePath();
|
|
125
|
+
const stats = await fs.stat(filePath);
|
|
126
|
+
const fileTrackingProperties = {
|
|
127
|
+
size: stats.size,
|
|
128
|
+
mtime: stats.mtimeMs,
|
|
129
|
+
contentHash: await hashFile(filePath),
|
|
130
|
+
};
|
|
131
|
+
const sourceFileNode = this.createCoreNode(sourceFile, CoreNodeType.SOURCE_FILE, fileTrackingProperties);
|
|
81
132
|
this.addNode(sourceFileNode);
|
|
82
|
-
|
|
83
|
-
this.parseChildNodes(this.coreSchema.nodeTypes[CoreNodeType.SOURCE_FILE], sourceFileNode, sourceFile);
|
|
84
|
-
// Special handling: Parse variable declarations if framework schema specifies patterns
|
|
133
|
+
await this.parseChildNodes(this.coreSchema.nodeTypes[CoreNodeType.SOURCE_FILE], sourceFileNode, sourceFile);
|
|
85
134
|
if (this.shouldParseVariables(sourceFile.getFilePath())) {
|
|
86
135
|
for (const varStatement of sourceFile.getVariableStatements()) {
|
|
87
136
|
for (const varDecl of varStatement.getDeclarations()) {
|
|
88
137
|
if (this.shouldSkipChildNode(varDecl))
|
|
89
138
|
continue;
|
|
90
|
-
const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION);
|
|
139
|
+
const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION, {}, sourceFileNode.id);
|
|
91
140
|
this.addNode(variableNode);
|
|
92
141
|
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, variableNode.id);
|
|
93
142
|
this.addEdge(containsEdge);
|
|
@@ -118,10 +167,18 @@ export class TypeScriptParser {
|
|
|
118
167
|
for (const child of children) {
|
|
119
168
|
if (this.shouldSkipChildNode(child))
|
|
120
169
|
continue;
|
|
121
|
-
const coreNode = this.createCoreNode(child, type);
|
|
170
|
+
const coreNode = this.createCoreNode(child, type, {}, parentNode.id);
|
|
122
171
|
this.addNode(coreNode);
|
|
123
172
|
const coreEdge = this.createCoreEdge(edgeType, parentNode.id, coreNode.id);
|
|
124
173
|
this.addEdge(coreEdge);
|
|
174
|
+
const SKELETONIZE_TYPES = new Set([
|
|
175
|
+
CoreNodeType.METHOD_DECLARATION,
|
|
176
|
+
CoreNodeType.FUNCTION_DECLARATION,
|
|
177
|
+
CoreNodeType.PROPERTY_DECLARATION,
|
|
178
|
+
]);
|
|
179
|
+
if (SKELETONIZE_TYPES.has(type)) {
|
|
180
|
+
this.skeletonizeChildInParent(parentNode, coreNode);
|
|
181
|
+
}
|
|
125
182
|
const childNodeConfig = this.coreSchema.nodeTypes[type];
|
|
126
183
|
if (childNodeConfig) {
|
|
127
184
|
this.queueRelationshipNodes(childNodeConfig, coreNode, child);
|
|
@@ -130,6 +187,15 @@ export class TypeScriptParser {
|
|
|
130
187
|
}
|
|
131
188
|
}
|
|
132
189
|
}
|
|
190
|
+
skeletonizeChildInParent(parent, child) {
|
|
191
|
+
const childText = child.properties.sourceCode;
|
|
192
|
+
const bodyStart = childText.indexOf('{');
|
|
193
|
+
if (bodyStart > -1) {
|
|
194
|
+
const signature = childText.substring(0, bodyStart).trim();
|
|
195
|
+
const placeholder = `${signature} { /* NodeID: ${child.id} */ }`;
|
|
196
|
+
parent.properties.sourceCode = parent.properties.sourceCode.replace(childText, placeholder);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
133
199
|
/**
|
|
134
200
|
* Queue relationship edges for deferred processing
|
|
135
201
|
* These are resolved after all nodes are parsed since the target may not exist yet
|
|
@@ -169,8 +235,12 @@ export class TypeScriptParser {
|
|
|
169
235
|
return target.getName();
|
|
170
236
|
if (Node.isInterfaceDeclaration(target))
|
|
171
237
|
return target.getName();
|
|
172
|
-
if (Node.isExpressionWithTypeArguments(target))
|
|
173
|
-
|
|
238
|
+
if (Node.isExpressionWithTypeArguments(target)) {
|
|
239
|
+
const expression = target.getExpression();
|
|
240
|
+
const text = expression.getText();
|
|
241
|
+
const genericIndex = text.indexOf('<');
|
|
242
|
+
return genericIndex > 0 ? text.substring(0, genericIndex) : text;
|
|
243
|
+
}
|
|
174
244
|
return undefined;
|
|
175
245
|
}
|
|
176
246
|
/**
|
|
@@ -182,6 +252,11 @@ export class TypeScriptParser {
|
|
|
182
252
|
return node;
|
|
183
253
|
}
|
|
184
254
|
}
|
|
255
|
+
for (const node of this.existingNodes.values()) {
|
|
256
|
+
if (node.coreType === coreType && node.properties.name === name) {
|
|
257
|
+
return node;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
185
260
|
return undefined;
|
|
186
261
|
}
|
|
187
262
|
/**
|
|
@@ -205,14 +280,14 @@ export class TypeScriptParser {
|
|
|
205
280
|
this.addNode(sourceFileNode);
|
|
206
281
|
// Parse classes
|
|
207
282
|
for (const classDecl of sourceFile.getClasses()) {
|
|
208
|
-
const classNode = this.createCoreNode(classDecl, CoreNodeType.CLASS_DECLARATION);
|
|
283
|
+
const classNode = this.createCoreNode(classDecl, CoreNodeType.CLASS_DECLARATION, {}, sourceFileNode.id);
|
|
209
284
|
this.addNode(classNode);
|
|
210
285
|
// File contains class relationship
|
|
211
286
|
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, classNode.id);
|
|
212
287
|
this.addEdge(containsEdge);
|
|
213
288
|
// Parse class decorators
|
|
214
289
|
for (const decorator of classDecl.getDecorators()) {
|
|
215
|
-
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR);
|
|
290
|
+
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, classNode.id);
|
|
216
291
|
this.addNode(decoratorNode);
|
|
217
292
|
// Class decorated with decorator relationship
|
|
218
293
|
const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, classNode.id, decoratorNode.id);
|
|
@@ -220,14 +295,14 @@ export class TypeScriptParser {
|
|
|
220
295
|
}
|
|
221
296
|
// Parse methods
|
|
222
297
|
for (const method of classDecl.getMethods()) {
|
|
223
|
-
const methodNode = this.createCoreNode(method, CoreNodeType.METHOD_DECLARATION);
|
|
298
|
+
const methodNode = this.createCoreNode(method, CoreNodeType.METHOD_DECLARATION, {}, classNode.id);
|
|
224
299
|
this.addNode(methodNode);
|
|
225
300
|
// Class has method relationship
|
|
226
301
|
const hasMethodEdge = this.createCoreEdge(CoreEdgeType.HAS_MEMBER, classNode.id, methodNode.id);
|
|
227
302
|
this.addEdge(hasMethodEdge);
|
|
228
303
|
// Parse method decorators
|
|
229
304
|
for (const decorator of method.getDecorators()) {
|
|
230
|
-
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR);
|
|
305
|
+
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, methodNode.id);
|
|
231
306
|
this.addNode(decoratorNode);
|
|
232
307
|
// Method decorated with decorator relationship
|
|
233
308
|
const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, methodNode.id, decoratorNode.id);
|
|
@@ -235,14 +310,14 @@ export class TypeScriptParser {
|
|
|
235
310
|
}
|
|
236
311
|
// Parse method parameters
|
|
237
312
|
for (const param of method.getParameters()) {
|
|
238
|
-
const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION);
|
|
313
|
+
const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION, {}, methodNode.id);
|
|
239
314
|
this.addNode(paramNode);
|
|
240
315
|
// Method has parameter relationship
|
|
241
316
|
const hasParamEdge = this.createCoreEdge(CoreEdgeType.HAS_PARAMETER, methodNode.id, paramNode.id);
|
|
242
317
|
this.addEdge(hasParamEdge);
|
|
243
318
|
// Parse parameter decorators
|
|
244
319
|
for (const decorator of param.getDecorators()) {
|
|
245
|
-
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR);
|
|
320
|
+
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, paramNode.id);
|
|
246
321
|
this.addNode(decoratorNode);
|
|
247
322
|
// Parameter decorated with decorator relationship
|
|
248
323
|
const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, paramNode.id, decoratorNode.id);
|
|
@@ -252,14 +327,14 @@ export class TypeScriptParser {
|
|
|
252
327
|
}
|
|
253
328
|
// Parse properties
|
|
254
329
|
for (const property of classDecl.getProperties()) {
|
|
255
|
-
const propertyNode = this.createCoreNode(property, CoreNodeType.PROPERTY_DECLARATION);
|
|
330
|
+
const propertyNode = this.createCoreNode(property, CoreNodeType.PROPERTY_DECLARATION, {}, classNode.id);
|
|
256
331
|
this.addNode(propertyNode);
|
|
257
332
|
// Class has property relationship
|
|
258
333
|
const hasPropertyEdge = this.createCoreEdge(CoreEdgeType.HAS_MEMBER, classNode.id, propertyNode.id);
|
|
259
334
|
this.addEdge(hasPropertyEdge);
|
|
260
335
|
// Parse property decorators
|
|
261
336
|
for (const decorator of property.getDecorators()) {
|
|
262
|
-
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR);
|
|
337
|
+
const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, propertyNode.id);
|
|
263
338
|
this.addNode(decoratorNode);
|
|
264
339
|
// Property decorated with decorator relationship
|
|
265
340
|
const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, propertyNode.id, decoratorNode.id);
|
|
@@ -269,7 +344,7 @@ export class TypeScriptParser {
|
|
|
269
344
|
}
|
|
270
345
|
// Parse interfaces
|
|
271
346
|
for (const interfaceDecl of sourceFile.getInterfaces()) {
|
|
272
|
-
const interfaceNode = this.createCoreNode(interfaceDecl, CoreNodeType.INTERFACE_DECLARATION);
|
|
347
|
+
const interfaceNode = this.createCoreNode(interfaceDecl, CoreNodeType.INTERFACE_DECLARATION, {}, sourceFileNode.id);
|
|
273
348
|
this.addNode(interfaceNode);
|
|
274
349
|
// File contains interface relationship
|
|
275
350
|
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, interfaceNode.id);
|
|
@@ -277,14 +352,14 @@ export class TypeScriptParser {
|
|
|
277
352
|
}
|
|
278
353
|
// Parse functions
|
|
279
354
|
for (const funcDecl of sourceFile.getFunctions()) {
|
|
280
|
-
const functionNode = this.createCoreNode(funcDecl, CoreNodeType.FUNCTION_DECLARATION);
|
|
355
|
+
const functionNode = this.createCoreNode(funcDecl, CoreNodeType.FUNCTION_DECLARATION, {}, sourceFileNode.id);
|
|
281
356
|
this.addNode(functionNode);
|
|
282
357
|
// File contains function relationship
|
|
283
358
|
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, functionNode.id);
|
|
284
359
|
this.addEdge(containsEdge);
|
|
285
360
|
// Parse function parameters
|
|
286
361
|
for (const param of funcDecl.getParameters()) {
|
|
287
|
-
const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION);
|
|
362
|
+
const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION, {}, functionNode.id);
|
|
288
363
|
this.addNode(paramNode);
|
|
289
364
|
// Function has parameter relationship
|
|
290
365
|
const hasParamEdge = this.createCoreEdge(CoreEdgeType.HAS_PARAMETER, functionNode.id, paramNode.id);
|
|
@@ -293,7 +368,7 @@ export class TypeScriptParser {
|
|
|
293
368
|
}
|
|
294
369
|
// Parse imports
|
|
295
370
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
296
|
-
const importNode = this.createCoreNode(importDecl, CoreNodeType.IMPORT_DECLARATION);
|
|
371
|
+
const importNode = this.createCoreNode(importDecl, CoreNodeType.IMPORT_DECLARATION, {}, sourceFileNode.id);
|
|
297
372
|
this.addNode(importNode);
|
|
298
373
|
// File contains import relationship
|
|
299
374
|
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, importNode.id);
|
|
@@ -303,7 +378,7 @@ export class TypeScriptParser {
|
|
|
303
378
|
if (this.shouldParseVariables(sourceFile.getFilePath())) {
|
|
304
379
|
for (const varStatement of sourceFile.getVariableStatements()) {
|
|
305
380
|
for (const varDecl of varStatement.getDeclarations()) {
|
|
306
|
-
const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION);
|
|
381
|
+
const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION, {}, sourceFileNode.id);
|
|
307
382
|
this.addNode(variableNode);
|
|
308
383
|
// File contains variable relationship
|
|
309
384
|
const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, variableNode.id);
|
|
@@ -316,18 +391,21 @@ export class TypeScriptParser {
|
|
|
316
391
|
console.error(`Error parsing file ${sourceFile.getFilePath()}:`, error);
|
|
317
392
|
}
|
|
318
393
|
}
|
|
319
|
-
createCoreNode(astNode, coreType) {
|
|
320
|
-
const
|
|
394
|
+
createCoreNode(astNode, coreType, baseProperties = {}, parentId) {
|
|
395
|
+
const name = this.extractNodeName(astNode, coreType);
|
|
396
|
+
const filePath = astNode.getSourceFile().getFilePath();
|
|
397
|
+
const nodeId = generateDeterministicId(coreType, filePath, name, parentId);
|
|
321
398
|
// Extract base properties using schema
|
|
322
399
|
const properties = {
|
|
323
400
|
id: nodeId,
|
|
324
|
-
name
|
|
401
|
+
name,
|
|
325
402
|
coreType,
|
|
326
|
-
filePath
|
|
403
|
+
filePath,
|
|
327
404
|
startLine: astNode.getStartLineNumber(),
|
|
328
405
|
endLine: astNode.getEndLineNumber(),
|
|
329
406
|
sourceCode: astNode.getText(),
|
|
330
407
|
createdAt: new Date().toISOString(),
|
|
408
|
+
...baseProperties,
|
|
331
409
|
};
|
|
332
410
|
// Extract schema-defined properties
|
|
333
411
|
const coreNodeDef = this.coreSchema.nodeTypes[coreType];
|
|
@@ -386,8 +464,12 @@ export class TypeScriptParser {
|
|
|
386
464
|
// Get the weight from the core schema
|
|
387
465
|
const coreEdgeSchema = CORE_TYPESCRIPT_SCHEMA.edgeTypes[relationshipType];
|
|
388
466
|
const relationshipWeight = coreEdgeSchema?.relationshipWeight ?? 0.5;
|
|
467
|
+
// Generate deterministic edge ID based on type + source + target
|
|
468
|
+
const edgeIdentity = `${relationshipType}::${sourceNodeId}::${targetNodeId}`;
|
|
469
|
+
const edgeHash = crypto.createHash('sha256').update(edgeIdentity).digest('hex').substring(0, 16);
|
|
470
|
+
const edgeId = `${relationshipType}:${edgeHash}`;
|
|
389
471
|
return {
|
|
390
|
-
id:
|
|
472
|
+
id: edgeId,
|
|
391
473
|
relationshipType,
|
|
392
474
|
sourceNodeId,
|
|
393
475
|
targetNodeId,
|
|
@@ -517,8 +599,14 @@ export class TypeScriptParser {
|
|
|
517
599
|
}
|
|
518
600
|
async applyEdgeEnhancement(edgeEnhancement) {
|
|
519
601
|
try {
|
|
602
|
+
// Combine parsed nodes and existing nodes for target matching
|
|
603
|
+
// Sources must be parsed (have AST), targets can be either
|
|
604
|
+
const allTargetNodes = new Map([...this.parsedNodes, ...this.existingNodes]);
|
|
520
605
|
for (const [sourceId, sourceNode] of this.parsedNodes) {
|
|
521
|
-
|
|
606
|
+
// Skip if source doesn't have AST (shouldn't happen for parsedNodes, but be safe)
|
|
607
|
+
if (!sourceNode.sourceNode)
|
|
608
|
+
continue;
|
|
609
|
+
for (const [targetId, targetNode] of allTargetNodes) {
|
|
522
610
|
if (sourceId === targetId)
|
|
523
611
|
continue;
|
|
524
612
|
if (edgeEnhancement.detectionPattern(sourceNode, targetNode, this.parsedNodes, this.sharedContext)) {
|
|
@@ -538,7 +626,10 @@ export class TypeScriptParser {
|
|
|
538
626
|
}
|
|
539
627
|
}
|
|
540
628
|
createFrameworkEdge(semanticType, relationshipType, sourceNodeId, targetNodeId, context = {}, relationshipWeight = 0.5) {
|
|
541
|
-
|
|
629
|
+
// Generate deterministic edge ID based on type + source + target
|
|
630
|
+
const edgeIdentity = `${semanticType}::${sourceNodeId}::${targetNodeId}`;
|
|
631
|
+
const edgeHash = crypto.createHash('sha256').update(edgeIdentity).digest('hex').substring(0, 16);
|
|
632
|
+
const edgeId = `${semanticType}:${edgeHash}`;
|
|
542
633
|
const properties = {
|
|
543
634
|
coreType: semanticType, // This might need adjustment based on schema
|
|
544
635
|
semanticType,
|
|
@@ -652,7 +743,6 @@ export class TypeScriptParser {
|
|
|
652
743
|
const excludedPatterns = this.parseConfig.excludePatterns ?? [];
|
|
653
744
|
for (const pattern of excludedPatterns) {
|
|
654
745
|
if (filePath.includes(pattern) || filePath.match(new RegExp(pattern))) {
|
|
655
|
-
console.log(`⏭️ Skipping excluded file: ${filePath}`);
|
|
656
746
|
return true;
|
|
657
747
|
}
|
|
658
748
|
}
|
package/dist/mcp/constants.js
CHANGED
|
@@ -21,6 +21,7 @@ export const TOOL_NAMES = {
|
|
|
21
21
|
traverseFromNode: 'traverse_from_node',
|
|
22
22
|
parseTypescriptProject: 'parse_typescript_project',
|
|
23
23
|
testNeo4jConnection: 'test_neo4j_connection',
|
|
24
|
+
impactAnalysis: 'impact_analysis',
|
|
24
25
|
};
|
|
25
26
|
// Tool Metadata
|
|
26
27
|
export const TOOL_METADATA = {
|
|
@@ -39,7 +40,6 @@ Start with default parameters for richest context in a single call. Most queries
|
|
|
39
40
|
|
|
40
41
|
Parameters:
|
|
41
42
|
- query: Natural language description of what you're looking for
|
|
42
|
-
- limit (default: 10): Number of initial vector search results to consider
|
|
43
43
|
|
|
44
44
|
**Token Optimization (Only if needed)**:
|
|
45
45
|
Use these parameters ONLY if you encounter token limit errors (>25,000 tokens):
|
|
@@ -87,10 +87,27 @@ Best practices:
|
|
|
87
87
|
title: 'Test Neo4j Connection & APOC',
|
|
88
88
|
description: 'Test connection to Neo4j database and verify APOC plugin is available',
|
|
89
89
|
},
|
|
90
|
+
[TOOL_NAMES.impactAnalysis]: {
|
|
91
|
+
title: 'Impact Analysis',
|
|
92
|
+
description: `Analyze the impact of modifying a code node. Shows what depends on this node and helps assess risk before making changes.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
- Risk level (LOW/MEDIUM/HIGH/CRITICAL) based on dependency count and relationship types
|
|
96
|
+
- Direct dependents: nodes that directly reference the target
|
|
97
|
+
- Transitive dependents: nodes affected through dependency chains
|
|
98
|
+
- Affected files: list of files that would need review
|
|
99
|
+
- Critical paths: high-risk dependency chains
|
|
100
|
+
|
|
101
|
+
Parameters:
|
|
102
|
+
- nodeId: Node ID from search_codebase or traverse_from_node results
|
|
103
|
+
- filePath: Alternative - analyze all exports from a file
|
|
104
|
+
- maxDepth: How far to trace transitive dependencies (default: 4)
|
|
105
|
+
|
|
106
|
+
Use this before refactoring to understand blast radius of changes.`,
|
|
107
|
+
},
|
|
90
108
|
};
|
|
91
109
|
// Default Values
|
|
92
110
|
export const DEFAULTS = {
|
|
93
|
-
searchLimit: 10,
|
|
94
111
|
traversalDepth: 3,
|
|
95
112
|
skipOffset: 0,
|
|
96
113
|
batchSize: 500,
|
package/dist/mcp/mcp.server.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
// Load environment variables from .env file
|
|
8
8
|
import dotenv from 'dotenv';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
9
|
import { dirname, join } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
12
|
const __dirname = dirname(__filename);
|
|
13
13
|
// Go up two levels from dist/mcp/mcp.server.js to the root
|