code-graph-context 1.0.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.
@@ -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. domainContext: Framework-specific semantics including:
11
- - nodeTypes: Descriptions and example queries for each node type
12
- - relationships: How nodes connect with context about relationship properties
13
- - commonQueryPatterns: Pre-built example queries for common use cases
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 DOMAIN CONTEXT: Look at domainContext.nodeTypes to understand available node types and their properties
24
- 2. REVIEW EXAMPLES: Check domainContext.commonQueryPatterns for similar query examples
25
- 3. CHECK RELATIONSHIPS: Look at domainContext.relationships to understand how nodes connect
26
- 4. EXAMINE NODE PROPERTIES: Use rawSchema to see exact property names and types
27
- 5. HANDLE JSON PROPERTIES: If properties or relationship context are stored as JSON strings, use apoc.convert.fromJsonMap() to parse them
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 rawSchema
33
- - For nested JSON data in properties, use: apoc.convert.fromJsonMap(node.propertyName) or apoc.convert.fromJsonMap(relationship.context)
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
 
@@ -69,9 +69,7 @@ export class ParserFactory {
69
69
  ...packageJson.devDependencies,
70
70
  };
71
71
  const hasNestJS = '@nestjs/common' in deps || '@nestjs/core' in deps;
72
- const hasFairSquare = '@fairsquare/core' in deps ||
73
- '@fairsquare/server' in deps ||
74
- packageJson.name === '@fairsquare/source';
72
+ const hasFairSquare = '@fairsquare/core' in deps || '@fairsquare/server' in deps || packageJson.name === '@fairsquare/source';
75
73
  if (hasFairSquare && hasNestJS) {
76
74
  return ProjectType.BOTH;
77
75
  }
@@ -171,6 +171,14 @@ export class TypeScriptParser {
171
171
  this.addNode(coreNode);
172
172
  const coreEdge = this.createCoreEdge(edgeType, parentNode.id, coreNode.id);
173
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
+ }
174
182
  const childNodeConfig = this.coreSchema.nodeTypes[type];
175
183
  if (childNodeConfig) {
176
184
  this.queueRelationshipNodes(childNodeConfig, coreNode, child);
@@ -179,6 +187,15 @@ export class TypeScriptParser {
179
187
  }
180
188
  }
181
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
+ }
182
199
  /**
183
200
  * Queue relationship edges for deferred processing
184
201
  * These are resolved after all nodes are parsed since the target may not exist yet
@@ -218,8 +235,12 @@ export class TypeScriptParser {
218
235
  return target.getName();
219
236
  if (Node.isInterfaceDeclaration(target))
220
237
  return target.getName();
221
- if (Node.isExpressionWithTypeArguments(target))
222
- return target.getExpression().getText();
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
+ }
223
244
  return undefined;
224
245
  }
225
246
  /**
@@ -231,6 +252,11 @@ export class TypeScriptParser {
231
252
  return node;
232
253
  }
233
254
  }
255
+ for (const node of this.existingNodes.values()) {
256
+ if (node.coreType === coreType && node.properties.name === name) {
257
+ return node;
258
+ }
259
+ }
234
260
  return undefined;
235
261
  }
236
262
  /**
@@ -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,
@@ -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
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import fs from 'fs/promises';
6
6
  import { join } from 'path';
7
- import { Neo4jService } from '../storage/neo4j/neo4j.service.js';
7
+ import { Neo4jService, QUERIES } from '../storage/neo4j/neo4j.service.js';
8
8
  import { FILE_PATHS, LOG_CONFIG } from './constants.js';
9
9
  import { initializeNaturalLanguageService } from './tools/natural-language-to-cypher.tool.js';
10
10
  import { debugLog } from './utils.js';
@@ -15,141 +15,62 @@ export const initializeServices = async () => {
15
15
  await Promise.all([initializeNeo4jSchema(), initializeNaturalLanguageService()]);
16
16
  };
17
17
  /**
18
- * Enrich raw Neo4j schema with FairSquare domain context
18
+ * Dynamically discover schema from the actual graph contents.
19
+ * This is framework-agnostic - it discovers what's actually in the graph.
19
20
  */
20
- const enrichSchemaWithDomainContext = (rawSchema) => {
21
- return {
22
- rawSchema,
23
- domainContext: {
24
- framework: 'FairSquare',
25
- description: 'Custom TypeScript framework for microservices with dependency injection and repository patterns',
26
- nodeTypes: {
27
- Controller: {
28
- description: 'HTTP request handlers that extend the Controller base class',
29
- purpose: 'Entry points for HTTP API endpoints',
30
- commonProperties: ['name', 'filePath', 'sourceCode'],
31
- exampleQuery: 'MATCH (c:Controller) WHERE c.name =~ ".*Credit.*" RETURN c',
32
- },
33
- Service: {
34
- description: 'Business logic layer with @Injectable decorator',
35
- purpose: 'Encapsulate business logic and orchestrate data operations',
36
- commonProperties: ['name', 'filePath', 'dependencies'],
37
- exampleQuery: 'MATCH (s:Service)-[:INJECTS]->(dep) RETURN s.name, collect(dep.name) as dependencies',
38
- },
39
- Repository: {
40
- description: 'Data access layer that extends Repository base class',
41
- purpose: 'Abstract database operations and provide data access interface',
42
- commonProperties: ['name', 'filePath', 'dals'],
43
- exampleQuery: 'MATCH (r:Repository)-[:USES_DAL]->(d:DAL) RETURN r.name, collect(d.name) as dals',
44
- },
45
- DAL: {
46
- description: 'Data Access Layer - direct database interaction classes',
47
- purpose: 'Execute database queries and manage data persistence',
48
- commonProperties: ['name', 'filePath'],
49
- exampleQuery: 'MATCH (d:DAL)<-[:USES_DAL]-(r:Repository) RETURN d.name, count(r) as usedByCount',
50
- },
51
- PermissionManager: {
52
- description: 'Security layer for authorization checks',
53
- purpose: 'Control access to resources and validate permissions',
54
- commonProperties: ['name', 'filePath'],
55
- exampleQuery: 'MATCH (c:Controller)-[:PROTECTED_BY]->(pm:PermissionManager) RETURN c.name, pm.name',
56
- },
57
- VendorClient: {
58
- description: 'External service integration clients',
59
- purpose: 'Interface with third-party APIs and services',
60
- commonProperties: ['name', 'filePath'],
61
- exampleQuery: 'MATCH (v:VendorClient)<-[:INJECTS]-(s) RETURN v.name, collect(s.name) as usedBy',
62
- },
63
- RouteDefinition: {
64
- description: 'Explicit route definitions from route files. CRITICAL: Individual route details (method, path, authenticated, handler, controllerName) are stored in the "context" property as a JSON string.',
65
- purpose: 'Map HTTP paths and methods to controller handlers',
66
- commonProperties: ['name', 'context', 'filePath', 'sourceCode'],
67
- contextStructure: 'The context property contains JSON with structure: {"routes": [{"method": "POST", "path": "/v1/endpoint", "controllerName": "SomeController", "handler": "methodName", "authenticated": true}]}',
68
- parsingInstructions: 'To get individual routes: (1) Parse JSON with apoc.convert.fromJsonMap(rd.context) (2) UNWIND the routes array (3) Access route.method, route.path, route.handler, route.authenticated, route.controllerName',
69
- exampleQuery: 'MATCH (rd:RouteDefinition) WITH rd, apoc.convert.fromJsonMap(rd.context) AS ctx UNWIND ctx.routes AS route RETURN route.method, route.path, route.controllerName, route.handler, route.authenticated ORDER BY route.path',
70
- },
71
- HttpEndpoint: {
72
- description: 'Methods that handle HTTP requests',
73
- purpose: 'Process incoming HTTP requests and return responses',
74
- commonProperties: ['name', 'filePath', 'sourceCode'],
75
- exampleQuery: 'MATCH (e:HttpEndpoint)<-[r:ROUTES_TO_HANDLER]-(rd) WHERE apoc.convert.fromJsonMap(r.context).authenticated = true RETURN e.name, apoc.convert.fromJsonMap(r.context).path as path',
76
- },
77
- },
78
- relationships: {
79
- INJECTS: {
80
- description: 'Dependency injection relationship from @Injectable decorator',
81
- direction: 'OUTGOING',
82
- example: 'Controller -[:INJECTS]-> Service',
83
- commonPatterns: ['Controller -> Service', 'Service -> Repository', 'Service -> VendorClient'],
84
- },
85
- USES_DAL: {
86
- description: 'Repository uses Data Access Layer for database operations',
87
- direction: 'OUTGOING',
88
- example: 'Repository -[:USES_DAL]-> DAL',
89
- commonPatterns: ['Repository -> DAL'],
90
- },
91
- ROUTES_TO: {
92
- description: 'Route definition points to a Controller',
93
- direction: 'OUTGOING',
94
- example: 'RouteDefinition -[:ROUTES_TO]-> Controller',
95
- commonPatterns: ['RouteDefinition -> Controller'],
96
- },
97
- ROUTES_TO_HANDLER: {
98
- description: 'Route definition points to a specific handler method',
99
- direction: 'OUTGOING',
100
- example: 'RouteDefinition -[:ROUTES_TO_HANDLER]-> HttpEndpoint',
101
- contextProperties: ['path', 'method', 'authenticated', 'handler', 'controllerName'],
102
- contextNote: 'IMPORTANT: context is stored as a JSON string. Access properties using apoc.convert.fromJsonMap(r.context).propertyName',
103
- commonPatterns: ['RouteDefinition -> HttpEndpoint (Method)'],
104
- },
105
- PROTECTED_BY: {
106
- description: 'Controller is protected by a PermissionManager',
107
- direction: 'OUTGOING',
108
- example: 'Controller -[:PROTECTED_BY]-> PermissionManager',
109
- commonPatterns: ['Controller -> PermissionManager'],
110
- },
111
- },
112
- commonQueryPatterns: [
113
- {
114
- intent: 'Find all HTTP endpoints',
115
- query: 'MATCH (e:HttpEndpoint) RETURN e.name, e.filePath',
116
- },
117
- {
118
- intent: 'Find service dependency chain',
119
- query: 'MATCH path = (c:Controller)-[:INJECTS*1..3]->(s) RETURN [n in nodes(path) | n.name] as chain',
120
- },
121
- {
122
- intent: 'Find all authenticated routes',
123
- query: 'MATCH (rd:RouteDefinition)-[r:ROUTES_TO_HANDLER]->(m) WHERE apoc.convert.fromJsonMap(r.context).authenticated = true RETURN apoc.convert.fromJsonMap(r.context).path as path, apoc.convert.fromJsonMap(r.context).method as method, m.name',
124
- },
125
- {
126
- intent: 'Find controllers without permission managers',
127
- query: 'MATCH (c:Controller) WHERE NOT (c)-[:PROTECTED_BY]->(:PermissionManager) RETURN c.name',
128
- },
129
- {
130
- intent: 'Find what services a controller uses',
131
- query: 'MATCH (c:Controller {name: $controllerName})-[:INJECTS]->(s:Service) RETURN s.name',
132
- },
133
- {
134
- intent: 'Find complete execution path from controller to database',
135
- query: 'MATCH path = (c:Controller)-[:INJECTS*1..3]->(r:Repository)-[:USES_DAL]->(d:DAL) WHERE c.name = $controllerName RETURN [n in nodes(path) | n.name] as executionPath',
136
- },
137
- ],
138
- },
139
- };
21
+ const discoverSchemaFromGraph = async (neo4jService) => {
22
+ try {
23
+ // Discover actual node types, relationships, and patterns from the graph
24
+ const [nodeTypes, relationshipTypes, semanticTypes, commonPatterns] = await Promise.all([
25
+ neo4jService.run(QUERIES.DISCOVER_NODE_TYPES),
26
+ neo4jService.run(QUERIES.DISCOVER_RELATIONSHIP_TYPES),
27
+ neo4jService.run(QUERIES.DISCOVER_SEMANTIC_TYPES),
28
+ neo4jService.run(QUERIES.DISCOVER_COMMON_PATTERNS),
29
+ ]);
30
+ return {
31
+ nodeTypes: nodeTypes.map((r) => ({
32
+ label: r.label,
33
+ count: typeof r.nodeCount === 'object' ? r.nodeCount.toNumber() : r.nodeCount,
34
+ properties: r.sampleProperties || [],
35
+ })),
36
+ relationshipTypes: relationshipTypes.map((r) => ({
37
+ type: r.relationshipType,
38
+ count: typeof r.relCount === 'object' ? r.relCount.toNumber() : r.relCount,
39
+ connections: r.connections || [],
40
+ })),
41
+ semanticTypes: semanticTypes.map((r) => ({
42
+ type: r.semanticType,
43
+ count: typeof r.count === 'object' ? r.count.toNumber() : r.count,
44
+ })),
45
+ commonPatterns: commonPatterns.map((r) => ({
46
+ from: r.fromType,
47
+ relationship: r.relType,
48
+ to: r.toType,
49
+ count: typeof r.count === 'object' ? r.count.toNumber() : r.count,
50
+ })),
51
+ };
52
+ }
53
+ catch (error) {
54
+ await debugLog('Failed to discover schema from graph', error);
55
+ return null;
56
+ }
140
57
  };
141
58
  /**
142
- * Initialize Neo4j schema by fetching and caching it locally
59
+ * Initialize Neo4j schema by fetching from APOC and discovering actual graph structure
143
60
  */
144
61
  const initializeNeo4jSchema = async () => {
145
62
  try {
146
63
  const neo4jService = new Neo4jService();
147
64
  const rawSchema = await neo4jService.getSchema();
148
- // Enrich schema with FairSquare domain context
149
- const enrichedSchema = enrichSchemaWithDomainContext(rawSchema);
65
+ // Dynamically discover what's actually in the graph
66
+ const discoveredSchema = await discoverSchemaFromGraph(neo4jService);
67
+ const schema = {
68
+ rawSchema,
69
+ discoveredSchema,
70
+ };
150
71
  const schemaPath = join(process.cwd(), FILE_PATHS.schemaOutput);
151
- await fs.writeFile(schemaPath, JSON.stringify(enrichedSchema, null, LOG_CONFIG.jsonIndentation));
152
- await debugLog('Neo4j schema cached successfully with domain context', { schemaPath });
72
+ await fs.writeFile(schemaPath, JSON.stringify(schema, null, LOG_CONFIG.jsonIndentation));
73
+ await debugLog('Neo4j schema cached successfully', { schemaPath });
153
74
  }
154
75
  catch (error) {
155
76
  await debugLog('Failed to initialize Neo4j schema', error);
@@ -0,0 +1,253 @@
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 } 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
+ nodeId: z
39
+ .string()
40
+ .optional()
41
+ .describe('The node ID to analyze impact for (from search_codebase or traverse_from_node results)'),
42
+ filePath: z
43
+ .string()
44
+ .optional()
45
+ .describe('Alternatively, provide a file path to analyze all exports from that file'),
46
+ maxDepth: z
47
+ .number()
48
+ .int()
49
+ .min(1)
50
+ .max(6)
51
+ .optional()
52
+ .describe('Maximum depth to traverse for transitive dependents (default: 4)')
53
+ .default(4),
54
+ 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.'),
55
+ },
56
+ }, async ({ nodeId, filePath, maxDepth = 4, frameworkConfig }) => {
57
+ try {
58
+ if (!nodeId && !filePath) {
59
+ return createErrorResponse('Either nodeId or filePath must be provided');
60
+ }
61
+ await debugLog('Impact analysis started', { nodeId, filePath, maxDepth, frameworkConfig });
62
+ const neo4jService = new Neo4jService();
63
+ // Merge default weights with framework-specific weights
64
+ const weights = { ...DEFAULT_RELATIONSHIP_WEIGHTS, ...frameworkConfig?.relationshipWeights };
65
+ const highRiskTypes = new Set(frameworkConfig?.highRiskTypes ?? []);
66
+ let targetInfo;
67
+ let directDependents;
68
+ if (nodeId) {
69
+ // Get target node info
70
+ const targetResult = await neo4jService.run(QUERIES.GET_NODE_BY_ID, { nodeId });
71
+ if (targetResult.length === 0) {
72
+ return createErrorResponse(`Node with ID "${nodeId}" not found`);
73
+ }
74
+ const target = targetResult[0];
75
+ targetInfo = {
76
+ id: target.id,
77
+ name: target.name ?? 'Unknown',
78
+ type: target.semanticType ?? target.coreType ?? target.labels?.[0] ?? 'Unknown',
79
+ filePath: target.filePath ?? '',
80
+ };
81
+ // Get direct dependents using cross-file edge pattern
82
+ const directResult = await neo4jService.run(QUERIES.GET_NODE_IMPACT, { nodeId });
83
+ directDependents = normalizeDependents(directResult);
84
+ }
85
+ else {
86
+ // File-based analysis
87
+ targetInfo = {
88
+ id: filePath,
89
+ name: filePath.split('/').pop() ?? filePath,
90
+ type: 'SourceFile',
91
+ filePath: filePath,
92
+ };
93
+ // Get all external dependents on this file
94
+ const fileResult = await neo4jService.run(QUERIES.GET_FILE_IMPACT, { filePath });
95
+ directDependents = normalizeDependents(fileResult);
96
+ }
97
+ // Get transitive dependents if nodeId provided
98
+ let transitiveDependents = [];
99
+ if (nodeId && maxDepth > 1) {
100
+ const transitiveResult = await neo4jService.run(QUERIES.GET_TRANSITIVE_DEPENDENTS(maxDepth), { nodeId });
101
+ transitiveDependents = normalizeTransitiveDependents(transitiveResult);
102
+ // Filter out direct dependents from transitive
103
+ const directIds = new Set(directDependents.map((d) => d.nodeId));
104
+ transitiveDependents = transitiveDependents.filter((d) => !directIds.has(d.nodeId));
105
+ }
106
+ // Calculate risk score
107
+ const riskScore = calculateRiskScore(directDependents, transitiveDependents, weights, highRiskTypes);
108
+ const riskLevel = getRiskLevel(riskScore);
109
+ // Group dependents by type
110
+ const directByType = groupByType(directDependents);
111
+ const directByRelationship = groupByRelationship(directDependents);
112
+ const transitiveByType = groupByType(transitiveDependents);
113
+ // Get affected files
114
+ const affectedFiles = getAffectedFiles([...directDependents, ...transitiveDependents]);
115
+ // Find critical paths (high-weight relationships)
116
+ const criticalPaths = findCriticalPaths(directDependents, targetInfo, weights);
117
+ // Build summary
118
+ const summary = buildSummary(targetInfo, directDependents.length, transitiveDependents.length, affectedFiles.length, riskLevel);
119
+ const result = {
120
+ target: targetInfo,
121
+ riskLevel,
122
+ riskScore: Math.round(riskScore * 100) / 100,
123
+ summary,
124
+ directDependents: {
125
+ count: directDependents.length,
126
+ byType: directByType,
127
+ byRelationship: directByRelationship,
128
+ },
129
+ transitiveDependents: {
130
+ count: transitiveDependents.length,
131
+ maxDepth: getMaxDepth(transitiveDependents),
132
+ byType: transitiveByType,
133
+ },
134
+ affectedFiles,
135
+ criticalPaths,
136
+ };
137
+ await debugLog('Impact analysis complete', {
138
+ nodeId: nodeId ?? filePath,
139
+ riskLevel,
140
+ directCount: directDependents.length,
141
+ transitiveCount: transitiveDependents.length,
142
+ });
143
+ return createSuccessResponse(JSON.stringify(result, null, 2));
144
+ }
145
+ catch (error) {
146
+ console.error('Impact analysis error:', error);
147
+ await debugLog('Impact analysis error', { nodeId, filePath, error });
148
+ return createErrorResponse(error);
149
+ }
150
+ });
151
+ };
152
+ // Helper functions
153
+ const normalizeDependents = (results) => {
154
+ return results.map((r) => ({
155
+ nodeId: r.nodeId,
156
+ name: r.name ?? 'Unknown',
157
+ labels: r.labels ?? [],
158
+ semanticType: r.semanticType,
159
+ coreType: r.coreType,
160
+ filePath: r.filePath ?? '',
161
+ relationshipType: r.relationshipType ?? 'UNKNOWN',
162
+ weight: typeof r.weight === 'object'
163
+ ? r.weight.toNumber()
164
+ : (r.weight ?? 0.5),
165
+ }));
166
+ };
167
+ const normalizeTransitiveDependents = (results) => {
168
+ return results.map((r) => ({
169
+ nodeId: r.nodeId,
170
+ name: r.name ?? 'Unknown',
171
+ labels: r.labels ?? [],
172
+ semanticType: r.semanticType,
173
+ coreType: r.coreType,
174
+ filePath: r.filePath ?? '',
175
+ relationshipType: r.relationshipPath?.[0] ?? 'UNKNOWN',
176
+ weight: 0.5,
177
+ depth: typeof r.depth === 'object' ? r.depth.toNumber() : r.depth,
178
+ relationshipPath: r.relationshipPath,
179
+ }));
180
+ };
181
+ const calculateRiskScore = (directDependents, transitiveDependents, weights, highRiskTypes) => {
182
+ if (directDependents.length === 0)
183
+ return 0;
184
+ let score = 0;
185
+ // Factor 1: Number of direct dependents (logarithmic, max 0.3)
186
+ score += Math.min(Math.log10(directDependents.length + 1) / 2, 0.3);
187
+ // Factor 2: Average relationship weight of direct deps (max 0.3)
188
+ const avgWeight = directDependents.reduce((sum, d) => sum + (weights[d.relationshipType] ?? d.weight), 0) / directDependents.length;
189
+ score += avgWeight * 0.3;
190
+ // Factor 3: High-risk types affected (max 0.2)
191
+ const highRiskCount = directDependents.filter((d) => highRiskTypes.has(d.semanticType ?? '') || highRiskTypes.has(d.coreType ?? '')).length;
192
+ if (highRiskTypes.size > 0) {
193
+ score += Math.min(highRiskCount / Math.max(highRiskTypes.size, 3), 1) * 0.2;
194
+ }
195
+ // Factor 4: Transitive impact (max 0.2)
196
+ score += Math.min(Math.log10(transitiveDependents.length + 1) / 3, 0.2);
197
+ return Math.min(score, 1);
198
+ };
199
+ const getRiskLevel = (score) => {
200
+ if (score >= 0.75)
201
+ return 'CRITICAL';
202
+ if (score >= 0.5)
203
+ return 'HIGH';
204
+ if (score >= 0.25)
205
+ return 'MEDIUM';
206
+ return 'LOW';
207
+ };
208
+ const groupByType = (dependents) => {
209
+ const groups = {};
210
+ for (const dep of dependents) {
211
+ const type = dep.semanticType ?? dep.coreType ?? dep.labels?.[0] ?? 'Unknown';
212
+ groups[type] = (groups[type] ?? 0) + 1;
213
+ }
214
+ return groups;
215
+ };
216
+ const groupByRelationship = (dependents) => {
217
+ const groups = {};
218
+ for (const dep of dependents) {
219
+ groups[dep.relationshipType] = (groups[dep.relationshipType] ?? 0) + 1;
220
+ }
221
+ return groups;
222
+ };
223
+ const getAffectedFiles = (dependents) => {
224
+ const files = new Set();
225
+ for (const dep of dependents) {
226
+ if (dep.filePath)
227
+ files.add(dep.filePath);
228
+ }
229
+ return Array.from(files).sort();
230
+ };
231
+ const getMaxDepth = (dependents) => {
232
+ if (dependents.length === 0)
233
+ return 0;
234
+ return Math.max(...dependents.map((d) => d.depth ?? 1));
235
+ };
236
+ const findCriticalPaths = (directDependents, target, weights) => {
237
+ const paths = [];
238
+ for (const dep of directDependents) {
239
+ const relWeight = weights[dep.relationshipType] ?? 0.5;
240
+ // Only include high-weight relationships
241
+ if (relWeight >= 0.6) {
242
+ const depType = dep.semanticType ?? dep.coreType ?? '';
243
+ paths.push(`${dep.name} (${depType}) -[${dep.relationshipType}]-> ${target.name} (${target.type})`);
244
+ }
245
+ }
246
+ return paths.slice(0, 10);
247
+ };
248
+ const buildSummary = (target, directCount, transitiveCount, fileCount, riskLevel) => {
249
+ if (directCount === 0) {
250
+ return `${target.name} (${target.type}) has no external dependents - safe to modify`;
251
+ }
252
+ return `Modifying ${target.name} (${target.type}) affects ${directCount} direct and ${transitiveCount} transitive dependents across ${fileCount} files. Risk: ${riskLevel}`;
253
+ };
@@ -3,6 +3,7 @@
3
3
  * Centralized tool creation and registration
4
4
  */
5
5
  import { createHelloTool } from './hello.tool.js';
6
+ import { createImpactAnalysisTool } from './impact-analysis.tool.js';
6
7
  import { createNaturalLanguageToCypherTool } from './natural-language-to-cypher.tool.js';
7
8
  import { createParseTypescriptProjectTool } from './parse-typescript-project.tool.js';
8
9
  import { createSearchCodebaseTool } from './search-codebase.tool.js';
@@ -19,6 +20,7 @@ export const registerAllTools = (server) => {
19
20
  createSearchCodebaseTool(server);
20
21
  createTraverseFromNodeTool(server);
21
22
  createNaturalLanguageToCypherTool(server);
23
+ createImpactAnalysisTool(server);
22
24
  // Register project parsing tool
23
25
  createParseTypescriptProjectTool(server);
24
26
  };
@@ -63,7 +63,9 @@ export const createParseTypescriptProjectTool = (server) => {
63
63
  // Recreate cross-file edges after incremental parse
64
64
  if (!clearExisting && savedCrossFileEdges.length > 0) {
65
65
  await debugLog('Recreating cross-file edges', { edgesToRecreate: savedCrossFileEdges.length });
66
- const recreateResult = await neo4jService.run(QUERIES.RECREATE_CROSS_FILE_EDGES, { edges: savedCrossFileEdges });
66
+ const recreateResult = await neo4jService.run(QUERIES.RECREATE_CROSS_FILE_EDGES, {
67
+ edges: savedCrossFileEdges,
68
+ });
67
69
  const recreatedCount = recreateResult[0]?.recreatedCount ?? 0;
68
70
  await debugLog('Cross-file edges recreated', { recreatedCount, expected: savedCrossFileEdges.length });
69
71
  }
@@ -112,7 +114,10 @@ const parseProject = async (options) => {
112
114
  await deleteSourceFileSubgraphs(neo4jService, filesToRemoveFromGraph);
113
115
  }
114
116
  if (filesToReparse.length > 0) {
115
- await debugLog('Incremental parse starting', { filesChanged: filesToReparse.length, filesDeleted: filesToDelete.length });
117
+ await debugLog('Incremental parse starting', {
118
+ filesChanged: filesToReparse.length,
119
+ filesDeleted: filesToDelete.length,
120
+ });
116
121
  // Load existing nodes from Neo4j for edge target matching
117
122
  const existingNodes = await loadExistingNodesForEdgeDetection(neo4jService, filesToRemoveFromGraph);
118
123
  await debugLog('Loaded existing nodes for edge detection', { count: existingNodes.length });
@@ -14,12 +14,6 @@ export const createSearchCodebaseTool = (server) => {
14
14
  description: TOOL_METADATA[TOOL_NAMES.searchCodebase].description,
15
15
  inputSchema: {
16
16
  query: z.string().describe('Natural language query to search the codebase'),
17
- limit: z
18
- .number()
19
- .int()
20
- .optional()
21
- .describe(`Maximum number of results to return (default: ${DEFAULTS.searchLimit})`)
22
- .default(DEFAULTS.searchLimit),
23
17
  maxDepth: z
24
18
  .number()
25
19
  .int()
@@ -50,19 +44,19 @@ export const createSearchCodebaseTool = (server) => {
50
44
  .describe('Use weighted traversal strategy that scores each node for relevance (default: false)')
51
45
  .default(true),
52
46
  },
53
- }, async ({ query, limit = DEFAULTS.searchLimit, maxDepth = DEFAULTS.traversalDepth, maxNodesPerChain = 5, skip = 0, includeCode = true, snippetLength = DEFAULTS.codeSnippetLength, useWeightedTraversal = true, }) => {
47
+ }, async ({ query, maxDepth = DEFAULTS.traversalDepth, maxNodesPerChain = 5, skip = 0, includeCode = true, snippetLength = DEFAULTS.codeSnippetLength, useWeightedTraversal = true, }) => {
54
48
  try {
55
- await debugLog('Search codebase started', { query, limit });
49
+ await debugLog('Search codebase started', { query });
56
50
  const neo4jService = new Neo4jService();
57
51
  const embeddingsService = new EmbeddingsService();
58
52
  const traversalHandler = new TraversalHandler(neo4jService);
59
53
  const embedding = await embeddingsService.embedText(query);
60
54
  const vectorResults = await neo4jService.run(QUERIES.VECTOR_SEARCH, {
61
- limit: parseInt(limit.toString()),
55
+ limit: 1,
62
56
  embedding,
63
57
  });
64
58
  if (vectorResults.length === 0) {
65
- await debugLog('No relevant code found', { query, limit });
59
+ await debugLog('No relevant code found', { query });
66
60
  return createSuccessResponse(MESSAGES.errors.noRelevantCode);
67
61
  }
68
62
  const startNode = vectorResults[0].node;
package/dist/mcp/utils.js CHANGED
@@ -55,7 +55,8 @@ export const formatNodeInfo = (value, key) => {
55
55
  else {
56
56
  // Show first 500 and last 500 characters
57
57
  const half = Math.floor(maxLength / 2);
58
- result.sourceCode = code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
58
+ result.sourceCode =
59
+ code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
59
60
  result.hasMore = true;
60
61
  result.truncated = code.length - maxLength;
61
62
  }
@@ -329,4 +329,144 @@ export const QUERIES = {
329
329
  LIMIT ${maxNodesPerDepth}
330
330
  `;
331
331
  },
332
+ // ============================================
333
+ // DYNAMIC SCHEMA DISCOVERY QUERIES
334
+ // ============================================
335
+ /**
336
+ * Get all distinct node labels with counts and sample properties
337
+ */
338
+ DISCOVER_NODE_TYPES: `
339
+ CALL db.labels() YIELD label
340
+ CALL {
341
+ WITH label
342
+ MATCH (n) WHERE label IN labels(n)
343
+ WITH n LIMIT 1
344
+ RETURN keys(n) AS sampleProperties
345
+ }
346
+ CALL {
347
+ WITH label
348
+ MATCH (n) WHERE label IN labels(n)
349
+ RETURN count(n) AS nodeCount
350
+ }
351
+ RETURN label, nodeCount, sampleProperties
352
+ ORDER BY nodeCount DESC
353
+ `,
354
+ /**
355
+ * Get all distinct relationship types with counts and which node types they connect
356
+ */
357
+ DISCOVER_RELATIONSHIP_TYPES: `
358
+ CALL db.relationshipTypes() YIELD relationshipType
359
+ CALL {
360
+ WITH relationshipType
361
+ MATCH (a)-[r]->(b) WHERE type(r) = relationshipType
362
+ WITH labels(a)[0] AS fromLabel, labels(b)[0] AS toLabel
363
+ RETURN fromLabel, toLabel
364
+ LIMIT 10
365
+ }
366
+ CALL {
367
+ WITH relationshipType
368
+ MATCH ()-[r]->() WHERE type(r) = relationshipType
369
+ RETURN count(r) AS relCount
370
+ }
371
+ RETURN relationshipType, relCount, collect(DISTINCT {from: fromLabel, to: toLabel}) AS connections
372
+ ORDER BY relCount DESC
373
+ `,
374
+ /**
375
+ * Get sample nodes of each semantic type for context
376
+ */
377
+ DISCOVER_SEMANTIC_TYPES: `
378
+ MATCH (n)
379
+ WHERE n.semanticType IS NOT NULL
380
+ WITH n.semanticType AS semanticType, count(*) AS count
381
+ ORDER BY count DESC
382
+ RETURN semanticType, count
383
+ `,
384
+ /**
385
+ * Get example query patterns based on actual graph structure
386
+ */
387
+ DISCOVER_COMMON_PATTERNS: `
388
+ MATCH (a)-[r]->(b)
389
+ WITH labels(a)[0] AS fromType, type(r) AS relType, labels(b)[0] AS toType, count(*) AS count
390
+ WHERE count > 5
391
+ RETURN fromType, relType, toType, count
392
+ ORDER BY count DESC
393
+ LIMIT 20
394
+ `,
395
+ // ============================================
396
+ // IMPACT ANALYSIS QUERIES
397
+ // Reuses cross-file edge pattern to find dependents
398
+ // ============================================
399
+ /**
400
+ * Get node details by ID
401
+ */
402
+ GET_NODE_BY_ID: `
403
+ MATCH (n) WHERE n.id = $nodeId
404
+ RETURN n.id AS id,
405
+ n.name AS name,
406
+ labels(n) AS labels,
407
+ n.semanticType AS semanticType,
408
+ n.coreType AS coreType,
409
+ n.filePath AS filePath
410
+ `,
411
+ /**
412
+ * Get impact of changing a node - finds all external nodes that depend on it
413
+ * Based on GET_CROSS_FILE_EDGES pattern but for a single node
414
+ */
415
+ GET_NODE_IMPACT: `
416
+ MATCH (target) WHERE target.id = $nodeId
417
+ MATCH (dependent)-[r]->(target)
418
+ WHERE dependent.id <> target.id
419
+ RETURN DISTINCT
420
+ dependent.id AS nodeId,
421
+ dependent.name AS name,
422
+ labels(dependent) AS labels,
423
+ dependent.semanticType AS semanticType,
424
+ dependent.coreType AS coreType,
425
+ dependent.filePath AS filePath,
426
+ type(r) AS relationshipType,
427
+ coalesce(r.relationshipWeight, 0.5) AS weight
428
+ `,
429
+ /**
430
+ * Get impact of changing a file - finds all external nodes that depend on nodes in this file
431
+ * Directly reuses GET_CROSS_FILE_EDGES pattern
432
+ */
433
+ GET_FILE_IMPACT: `
434
+ MATCH (sf:SourceFile {filePath: $filePath})
435
+ OPTIONAL MATCH (sf)-[*]->(child)
436
+ WITH collect(DISTINCT sf) + collect(DISTINCT child) AS nodesInFile
437
+ UNWIND nodesInFile AS n
438
+ MATCH (dependent)-[r]->(n)
439
+ WHERE NOT dependent IN nodesInFile
440
+ RETURN DISTINCT
441
+ dependent.id AS nodeId,
442
+ dependent.name AS name,
443
+ labels(dependent) AS labels,
444
+ dependent.semanticType AS semanticType,
445
+ dependent.coreType AS coreType,
446
+ dependent.filePath AS filePath,
447
+ type(r) AS relationshipType,
448
+ coalesce(r.relationshipWeight, 0.5) AS weight,
449
+ n.id AS targetNodeId,
450
+ n.name AS targetNodeName
451
+ `,
452
+ /**
453
+ * Get transitive dependents - nodes that depend on dependents (for deeper impact)
454
+ */
455
+ GET_TRANSITIVE_DEPENDENTS: (maxDepth = 4) => `
456
+ MATCH (target) WHERE target.id = $nodeId
457
+ MATCH path = (dependent)-[*2..${maxDepth}]->(target)
458
+ WITH dependent,
459
+ length(path) AS depth,
460
+ [r IN relationships(path) | type(r)] AS relationshipPath
461
+ RETURN DISTINCT
462
+ dependent.id AS nodeId,
463
+ dependent.name AS name,
464
+ labels(dependent) AS labels,
465
+ dependent.semanticType AS semanticType,
466
+ dependent.coreType AS coreType,
467
+ dependent.filePath AS filePath,
468
+ depth,
469
+ relationshipPath
470
+ ORDER BY depth ASC
471
+ `,
332
472
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-context",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",