code-graph-context 0.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.
@@ -0,0 +1,141 @@
1
+ /**
2
+ * MCP Server Constants
3
+ * All constants used throughout the MCP server implementation
4
+ */
5
+ // Server Configuration
6
+ export const MCP_SERVER_CONFIG = {
7
+ name: 'codebase-graph',
8
+ version: '1.0.0',
9
+ };
10
+ // File Paths
11
+ export const FILE_PATHS = {
12
+ debugLog: 'debug-search.log',
13
+ schemaOutput: 'neo4j-apoc-schema.json',
14
+ graphOutput: 'graph.json',
15
+ };
16
+ // Tool Names
17
+ export const TOOL_NAMES = {
18
+ hello: 'hello',
19
+ searchCodebase: 'search_codebase',
20
+ naturalLanguageToCypher: 'natural_language_to_cypher',
21
+ traverseFromNode: 'traverse_from_node',
22
+ parseTypescriptProject: 'parse_typescript_project',
23
+ testNeo4jConnection: 'test_neo4j_connection',
24
+ };
25
+ // Tool Metadata
26
+ export const TOOL_METADATA = {
27
+ [TOOL_NAMES.hello]: {
28
+ title: 'Hello Tool',
29
+ description: 'Test tool that says hello',
30
+ },
31
+ [TOOL_NAMES.searchCodebase]: {
32
+ title: 'Search Codebase',
33
+ description: `Search the codebase using semantic similarity to find relevant code, functions, classes, and implementations.
34
+
35
+ Returns normalized JSON with source code snippets. Uses JSON:API pattern to deduplicate nodes.
36
+
37
+ **Default Usage (Recommended)**:
38
+ Start with default parameters for richest context in a single call. Most queries complete successfully.
39
+
40
+ Parameters:
41
+ - query: Natural language description of what you're looking for
42
+ - limit (default: 10): Number of initial vector search results to consider
43
+
44
+ **Token Optimization (Only if needed)**:
45
+ Use these parameters ONLY if you encounter token limit errors (>25,000 tokens):
46
+
47
+ - maxDepth (default: 3): Reduce to 1-2 for shallow exploration
48
+ - maxNodesPerChain (default: 5): Limit chains shown per depth level
49
+ - includeCode (default: true): Set false to get structure only, fetch code separately
50
+ - snippetLength (default: 700): Reduce to 400-600 for smaller code snippets
51
+ - skip (default: 0): For pagination (skip N results)
52
+
53
+ **Progressive Strategy**:
54
+ 1. Try with defaults first
55
+ 2. If token error: Use maxDepth=1, includeCode=false for structure
56
+ 3. Then traverse deeper or Read specific files for full code`,
57
+ },
58
+ [TOOL_NAMES.naturalLanguageToCypher]: {
59
+ title: 'Natural Language to Cypher',
60
+ description: 'Convert natural language queries into Cypher queries for Neo4j. This tool is useful for generating specific queries based on user requests about the codebase.',
61
+ },
62
+ [TOOL_NAMES.traverseFromNode]: {
63
+ title: 'Traverse from Node',
64
+ description: `Traverse the graph starting from a specific node ID to explore its connections and relationships in detail.
65
+
66
+ Parameters:
67
+ - nodeId (required): The node ID to start traversal from (obtained from search_codebase)
68
+ - maxDepth (default: 3): How many relationship hops to traverse (1-10)
69
+ - skip (default: 0): Number of results to skip for pagination
70
+
71
+ Advanced options (use when needed):
72
+ - includeCode (default: true): Set to false for structure-only view without source code
73
+ - maxNodesPerChain (default: 5): Limit chains shown per depth level (applied independently at each depth)
74
+ - summaryOnly: Set to true for just file paths and statistics without detailed traversal
75
+
76
+ Best practices:
77
+ - Start with search_codebase to find initial nodes
78
+ - Default includes source code snippets for immediate context
79
+ - Set includeCode: false for high-level architecture view only
80
+ - Use summaryOnly: true for a quick overview of many connections`,
81
+ },
82
+ [TOOL_NAMES.parseTypescriptProject]: {
83
+ title: 'Parse TypeScript Project',
84
+ description: 'Parse a TypeScript/NestJS project and store in Neo4j graph',
85
+ },
86
+ [TOOL_NAMES.testNeo4jConnection]: {
87
+ title: 'Test Neo4j Connection & APOC',
88
+ description: 'Test connection to Neo4j database and verify APOC plugin is available',
89
+ },
90
+ };
91
+ // Default Values
92
+ export const DEFAULTS = {
93
+ searchLimit: 10,
94
+ traversalDepth: 3,
95
+ skipOffset: 0,
96
+ batchSize: 500,
97
+ maxResultsDisplayed: 30,
98
+ codeSnippetLength: 1000,
99
+ chainSnippetLength: 700,
100
+ };
101
+ // Messages
102
+ export const MESSAGES = {
103
+ errors: {
104
+ noRelevantCode: 'No relevant code found.',
105
+ serviceNotInitialized: 'ERROR: Natural Language to Cypher service is not initialized yet. Please try again in a few moments.',
106
+ connectionTestFailed: 'Connection test failed',
107
+ neo4jRequirement: 'Note: This server requires Neo4j with APOC plugin installed',
108
+ genericError: 'ERROR:',
109
+ },
110
+ success: {
111
+ hello: 'Hello from codebase MCP!',
112
+ parseSuccess: 'SUCCESS:',
113
+ partialSuccess: 'PARTIAL SUCCESS:',
114
+ },
115
+ queries: {
116
+ naturalLanguagePrefix: 'Natural Language Query:',
117
+ cypherQueryHeader: 'Generated Cypher Query',
118
+ queryResultsHeader: 'Query Results',
119
+ noResultsFound: 'No results found for this query.',
120
+ moreResultsIndicator: '_... and {} more results_',
121
+ summaryPrefix: '**Summary:** Executed query and found {} results.',
122
+ },
123
+ neo4j: {
124
+ connectionTest: 'RETURN "Connected!" as message, datetime() as timestamp',
125
+ apocTest: 'CALL apoc.help("apoc") YIELD name RETURN count(name) as apocFunctions',
126
+ connectionSuccess: 'Neo4j connected: {} at {}\nAPOC plugin available with {} functions',
127
+ },
128
+ server: {
129
+ starting: '=== MCP Server Starting ===',
130
+ connected: '=== MCP Server Connected and Running ===',
131
+ creatingTransport: 'Creating transport...',
132
+ connectingTransport: 'Connecting server to transport...',
133
+ startingServer: 'Starting MCP server...',
134
+ },
135
+ };
136
+ // Logging Configuration
137
+ export const LOG_CONFIG = {
138
+ timestampFormat: 'iso',
139
+ logSeparator: '---',
140
+ jsonIndentation: 2,
141
+ };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Graph Generator Handler
3
+ * Handles importing parsed graph data into Neo4j with embeddings
4
+ */
5
+ import fs from 'fs/promises';
6
+ import { QUERIES } from '../../storage/neo4j/neo4j.service.js';
7
+ import { DEFAULTS } from '../constants.js';
8
+ import { debugLog } from '../utils.js';
9
+ export class GraphGeneratorHandler {
10
+ neo4jService;
11
+ embeddingsService;
12
+ static EMBEDDED_LABEL = 'Embedded';
13
+ constructor(neo4jService, embeddingsService) {
14
+ this.neo4jService = neo4jService;
15
+ this.embeddingsService = embeddingsService;
16
+ }
17
+ async generateGraph(graphJsonPath, batchSize = DEFAULTS.batchSize, clearExisting = true) {
18
+ console.log(`Generating graph from JSON file: ${graphJsonPath}`);
19
+ await debugLog('Starting graph generation', { graphJsonPath, batchSize, clearExisting });
20
+ try {
21
+ const graphData = await this.loadGraphData(graphJsonPath);
22
+ const { nodes, edges, metadata } = graphData;
23
+ console.log(`Generating graph with ${nodes.length} nodes and ${edges.length} edges`);
24
+ await debugLog('Graph data loaded', { nodeCount: nodes.length, edgeCount: edges.length });
25
+ if (clearExisting) {
26
+ await this.clearExistingData();
27
+ }
28
+ await this.importNodes(nodes, batchSize);
29
+ await this.importEdges(edges, batchSize);
30
+ await this.createVectorIndexes();
31
+ const result = {
32
+ nodesImported: nodes.length,
33
+ edgesImported: edges.length,
34
+ metadata,
35
+ };
36
+ await debugLog('Graph generation completed', result);
37
+ return result;
38
+ }
39
+ catch (error) {
40
+ console.error('generateGraph error:', error);
41
+ await debugLog('Graph generation error', error);
42
+ throw error;
43
+ }
44
+ }
45
+ async loadGraphData(graphJsonPath) {
46
+ const fileContent = await fs.readFile(graphJsonPath, 'utf-8');
47
+ return JSON.parse(fileContent);
48
+ }
49
+ async clearExistingData() {
50
+ console.log('Clearing existing graph data...');
51
+ await this.neo4jService.run(QUERIES.CLEAR_DATABASE);
52
+ await debugLog('Existing graph data cleared');
53
+ }
54
+ async importNodes(nodes, batchSize) {
55
+ console.log(`Importing ${nodes.length} nodes with embeddings...`);
56
+ for (let i = 0; i < nodes.length; i += batchSize) {
57
+ const batch = await this.processNodeBatch(nodes.slice(i, i + batchSize));
58
+ const result = await this.neo4jService.run(QUERIES.CREATE_NODE, { nodes: batch });
59
+ const batchEnd = Math.min(i + batchSize, nodes.length);
60
+ console.log(`Created ${result[0].created} nodes in batch ${i + 1}-${batchEnd}`);
61
+ await debugLog('Node batch imported', {
62
+ batchStart: i + 1,
63
+ batchEnd,
64
+ created: result[0].created,
65
+ });
66
+ }
67
+ }
68
+ async processNodeBatch(nodes) {
69
+ return Promise.all(nodes.map(async (node) => {
70
+ const embedding = await this.embedNodeSourceCode(node);
71
+ return {
72
+ ...node,
73
+ labels: embedding ? [...node.labels, GraphGeneratorHandler.EMBEDDED_LABEL] : node.labels,
74
+ properties: {
75
+ ...this.flattenProperties(node.properties),
76
+ embedding,
77
+ },
78
+ };
79
+ }));
80
+ }
81
+ async importEdges(edges, batchSize) {
82
+ console.log(`Importing ${edges.length} edges using APOC...`);
83
+ for (let i = 0; i < edges.length; i += batchSize) {
84
+ const batch = edges.slice(i, i + batchSize).map((edge) => ({
85
+ ...edge,
86
+ properties: this.flattenProperties(edge.properties),
87
+ }));
88
+ const result = await this.neo4jService.run(QUERIES.CREATE_RELATIONSHIP, { edges: batch });
89
+ const batchEnd = Math.min(i + batchSize, edges.length);
90
+ console.log(`Created ${result[0].created} edges in batch ${i + 1}-${batchEnd}`);
91
+ await debugLog('Edge batch imported', {
92
+ batchStart: i + 1,
93
+ batchEnd,
94
+ created: result[0].created,
95
+ });
96
+ }
97
+ }
98
+ async createVectorIndexes() {
99
+ console.log('Creating vector indexes...');
100
+ await this.neo4jService.run(QUERIES.CREATE_EMBEDDED_VECTOR_INDEX);
101
+ await debugLog('Vector indexes created');
102
+ }
103
+ async embedNodeSourceCode(node) {
104
+ if (!node.properties?.sourceCode || node.skipEmbedding) {
105
+ return null;
106
+ }
107
+ try {
108
+ const sourceCode = node.properties.sourceCode;
109
+ const embedding = await this.embeddingsService.embedText(sourceCode);
110
+ await debugLog('Node embedded', { nodeId: node.id, codeLength: sourceCode.length });
111
+ return embedding;
112
+ }
113
+ catch (error) {
114
+ console.warn(`Failed to embed node ${node.id}:`, error);
115
+ await debugLog('Embedding failed', { nodeId: node.id, error });
116
+ return null;
117
+ }
118
+ }
119
+ flattenProperties(properties) {
120
+ const flattened = {};
121
+ for (const [key, value] of Object.entries(properties)) {
122
+ if (this.isComplexObject(value)) {
123
+ // Convert nested objects to JSON strings for Neo4j compatibility
124
+ flattened[key] = JSON.stringify(value);
125
+ }
126
+ else if (this.isComplexArray(value)) {
127
+ // Convert arrays with objects to JSON strings
128
+ flattened[key] = JSON.stringify(value);
129
+ }
130
+ else {
131
+ // Keep scalar values as-is
132
+ flattened[key] = value;
133
+ }
134
+ }
135
+ return flattened;
136
+ }
137
+ isComplexObject(value) {
138
+ return value && typeof value === 'object' && !Array.isArray(value);
139
+ }
140
+ isComplexArray(value) {
141
+ return Array.isArray(value) && value.some((item) => typeof item === 'object');
142
+ }
143
+ }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Traversal Handler
3
+ * Handles graph traversal operations with formatting and pagination
4
+ */
5
+ import { MAX_TRAVERSAL_DEPTH } from '../../constants.js';
6
+ import { QUERIES } from '../../storage/neo4j/neo4j.service.js';
7
+ import { DEFAULTS } from '../constants.js';
8
+ import { createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
9
+ export class TraversalHandler {
10
+ neo4jService;
11
+ static NODE_NOT_FOUND_QUERY = 'MATCH (n) WHERE n.id = $nodeId RETURN n';
12
+ constructor(neo4jService) {
13
+ this.neo4jService = neo4jService;
14
+ }
15
+ async traverseFromNode(nodeId, embedding, options = {}) {
16
+ const { maxDepth = DEFAULTS.traversalDepth, skip = DEFAULTS.skipOffset, direction = 'BOTH', relationshipTypes, includeStartNodeDetails = true, includeCode = false, maxNodesPerChain = 5, summaryOnly = false, title = `Node Traversal from: ${nodeId}`, snippetLength = DEFAULTS.codeSnippetLength, useWeightedTraversal = false, } = options;
17
+ try {
18
+ await debugLog('Starting node traversal', { nodeId, maxDepth, skip });
19
+ const startNode = await this.getStartNode(nodeId);
20
+ if (!startNode) {
21
+ return createErrorResponse(`Node with ID "${nodeId}" not found.`);
22
+ }
23
+ const maxNodesPerDepth = Math.ceil(maxNodesPerChain * 1.5);
24
+ const traversalData = useWeightedTraversal
25
+ ? await this.performTraversalByDepth(nodeId, embedding, maxDepth, maxNodesPerDepth, direction, relationshipTypes)
26
+ : await this.performTraversal(nodeId, embedding, maxDepth, skip, direction, relationshipTypes);
27
+ if (!traversalData) {
28
+ return createSuccessResponse(`No connections found for node "${nodeId}".`);
29
+ }
30
+ const result = summaryOnly
31
+ ? this.formatSummaryOnlyJSON(startNode, traversalData, title)
32
+ : this.formatTraversalJSON(startNode, traversalData, title, includeStartNodeDetails, includeCode, maxNodesPerChain, snippetLength);
33
+ await debugLog('Traversal completed', {
34
+ connectionsFound: traversalData.connections.length,
35
+ uniqueFiles: this.getUniqueFileCount(traversalData.connections),
36
+ });
37
+ return {
38
+ content: [{ type: 'text', text: JSON.stringify(result) }],
39
+ };
40
+ }
41
+ catch (error) {
42
+ console.error('Node traversal error:', error);
43
+ await debugLog('Node traversal error', { nodeId, error });
44
+ return createErrorResponse(error);
45
+ }
46
+ }
47
+ async getStartNode(nodeId) {
48
+ const startNodeResult = await this.neo4jService.run(TraversalHandler.NODE_NOT_FOUND_QUERY, { nodeId });
49
+ return startNodeResult.length > 0 ? startNodeResult[0].n : null;
50
+ }
51
+ async performTraversal(nodeId, embedding, maxDepth, skip, direction = 'BOTH', relationshipTypes) {
52
+ const traversal = await this.neo4jService.run(QUERIES.EXPLORE_ALL_CONNECTIONS(Math.min(maxDepth, MAX_TRAVERSAL_DEPTH), direction, relationshipTypes), {
53
+ nodeId,
54
+ skip: parseInt(skip.toString()),
55
+ });
56
+ if (traversal.length === 0) {
57
+ return null;
58
+ }
59
+ const result = traversal[0]?.result ?? {};
60
+ return {
61
+ connections: result.connections ?? [],
62
+ graph: result.graph ?? { nodes: [], relationships: [] },
63
+ };
64
+ }
65
+ async performTraversalByDepth(nodeId, embedding, maxDepth, maxNodesPerDepth, direction = 'BOTH', relationshipTypes) {
66
+ // Track visited nodes to avoid cycles
67
+ const visitedNodeIds = new Set([nodeId]);
68
+ // Track the path (chain of relationships) to reach each node
69
+ // Key: nodeId, Value: array of relationships from start node to this node
70
+ const pathsToNode = new Map();
71
+ pathsToNode.set(nodeId, []); // Start node has empty path
72
+ // Track which nodes to explore at each depth
73
+ let currentSourceIds = [nodeId];
74
+ // Result accumulators
75
+ const allConnections = [];
76
+ const nodeMap = new Map(); // Dedupe nodes
77
+ for (let depth = 1; depth <= maxDepth; depth++) {
78
+ if (currentSourceIds.length === 0) {
79
+ console.log(`No source nodes to explore at depth ${depth}`);
80
+ break;
81
+ }
82
+ const traversalResults = await this.neo4jService.run(QUERIES.EXPLORE_DEPTH_LEVEL(direction, maxNodesPerDepth), {
83
+ sourceNodeIds: currentSourceIds,
84
+ visitedNodeIds: Array.from(visitedNodeIds),
85
+ currentDepth: parseInt(depth.toString()),
86
+ queryEmbedding: embedding,
87
+ depthDecay: 0.85,
88
+ });
89
+ if (traversalResults.length === 0) {
90
+ console.log(`No connections found at depth ${depth}`);
91
+ break;
92
+ }
93
+ // Collect node IDs for next depth exploration
94
+ const nextSourceIds = [];
95
+ for (const row of traversalResults) {
96
+ const { node, relationship, sourceNodeId, scoring } = row.result;
97
+ const neighborId = node.id;
98
+ // Skip if already visited (safety check)
99
+ if (visitedNodeIds.has(neighborId))
100
+ continue;
101
+ // Mark as visited
102
+ visitedNodeIds.add(neighborId);
103
+ nextSourceIds.push(neighborId);
104
+ // Build the relationship chain:
105
+ // This node's chain = parent's chain + this relationship
106
+ const parentPath = pathsToNode.get(sourceNodeId) ?? [];
107
+ const thisPath = [
108
+ ...parentPath,
109
+ {
110
+ type: relationship.type,
111
+ start: relationship.startNodeId,
112
+ end: relationship.endNodeId,
113
+ properties: relationship.properties,
114
+ score: scoring.combinedScore,
115
+ },
116
+ ];
117
+ pathsToNode.set(neighborId, thisPath);
118
+ // Create connection with full relationship chain
119
+ const connection = {
120
+ depth,
121
+ node: node,
122
+ relationshipChain: thisPath,
123
+ };
124
+ allConnections.push(connection);
125
+ // Accumulate unique nodes
126
+ nodeMap.set(neighborId, node);
127
+ }
128
+ // Move to next depth with the newly discovered nodes
129
+ currentSourceIds = nextSourceIds;
130
+ }
131
+ return {
132
+ connections: allConnections,
133
+ };
134
+ }
135
+ groupConnectionsByDepth(connections) {
136
+ return connections.reduce((acc, conn) => {
137
+ const depth = conn.depth;
138
+ acc[depth] ??= [];
139
+ acc[depth].push(conn);
140
+ return acc;
141
+ }, {});
142
+ }
143
+ getRelationshipDirection(connection, startNodeId) {
144
+ // Check the first relationship in the chain to determine direction from start node
145
+ const firstRel = connection.relationshipChain?.[0];
146
+ if (!firstRel?.start || !firstRel.end) {
147
+ return 'UNKNOWN';
148
+ }
149
+ // If the start node is the source of the first relationship, it's OUTGOING
150
+ // If the start node is the target of the first relationship, it's INCOMING
151
+ if (firstRel.start === startNodeId) {
152
+ return 'OUTGOING';
153
+ }
154
+ else if (firstRel.end === startNodeId) {
155
+ return 'INCOMING';
156
+ }
157
+ return 'UNKNOWN';
158
+ }
159
+ getUniqueFileCount(connections) {
160
+ return new Set(connections.map((c) => c.node.properties.filePath).filter(Boolean)).size;
161
+ }
162
+ formatTraversalJSON(startNode, traversalData, title, includeStartNodeDetails, includeCode, maxNodesPerChain, snippetLength) {
163
+ // JSON:API normalization - collect all unique nodes
164
+ const nodeMap = new Map();
165
+ // Get common root path from all nodes
166
+ const allNodes = [startNode, ...traversalData.connections.map((c) => c.node)];
167
+ const projectRoot = this.getCommonRootPath(allNodes);
168
+ // Add start node to map
169
+ if (includeStartNodeDetails) {
170
+ const startNodeData = this.formatNodeJSON(startNode, includeCode, snippetLength, projectRoot);
171
+ nodeMap.set(startNode.properties.id, startNodeData);
172
+ }
173
+ // Collect all unique nodes from connections
174
+ traversalData.connections.forEach((conn) => {
175
+ const nodeId = conn.node.properties.id;
176
+ if (!nodeMap.has(nodeId)) {
177
+ nodeMap.set(nodeId, this.formatNodeJSON(conn.node, includeCode, snippetLength, projectRoot));
178
+ }
179
+ });
180
+ const byDepth = this.groupConnectionsByDepth(traversalData.connections);
181
+ return {
182
+ projectRoot,
183
+ totalConnections: traversalData.connections.length,
184
+ uniqueFiles: this.getUniqueFileCount(traversalData.connections),
185
+ maxDepth: Object.keys(byDepth).length > 0 ? Math.max(...Object.keys(byDepth).map((d) => parseInt(d))) : 0,
186
+ startNodeId: includeStartNodeDetails ? startNode.properties.id : undefined,
187
+ nodes: Object.fromEntries(nodeMap),
188
+ depths: this.formatConnectionsByDepthWithReferences(byDepth, maxNodesPerChain),
189
+ };
190
+ }
191
+ formatSummaryOnlyJSON(startNode, traversalData, title) {
192
+ const byDepth = this.groupConnectionsByDepth(traversalData.connections);
193
+ const totalConnections = traversalData.connections.length;
194
+ const maxDepthFound = Object.keys(byDepth).length > 0 ? Math.max(...Object.keys(byDepth).map((d) => parseInt(d))) : 0;
195
+ const uniqueFiles = this.getUniqueFileCount(traversalData.connections);
196
+ const allNodes = [startNode, ...traversalData.connections.map((c) => c.node)];
197
+ const projectRoot = this.getCommonRootPath(allNodes);
198
+ const fileMap = new Map();
199
+ traversalData.connections.forEach((conn) => {
200
+ const filePath = conn.node.properties.filePath;
201
+ if (filePath) {
202
+ const relativePath = this.makeRelativePath(filePath, projectRoot);
203
+ fileMap.set(relativePath, (fileMap.get(relativePath) ?? 0) + 1);
204
+ }
205
+ });
206
+ const connectedFiles = Array.from(fileMap.entries())
207
+ .sort((a, b) => b[1] - a[1])
208
+ .map(([file, count]) => ({ file, nodeCount: count }));
209
+ const maxSummaryFiles = DEFAULTS.maxResultsDisplayed;
210
+ return {
211
+ projectRoot,
212
+ startNodeId: startNode.properties.id,
213
+ nodes: {
214
+ [startNode.properties.id]: this.formatNodeJSON(startNode, false, 0, projectRoot),
215
+ },
216
+ totalConnections,
217
+ maxDepth: maxDepthFound,
218
+ uniqueFiles,
219
+ files: connectedFiles.slice(0, maxSummaryFiles),
220
+ ...(fileMap.size > maxSummaryFiles && { hasMore: fileMap.size - maxSummaryFiles }),
221
+ };
222
+ }
223
+ formatNodeJSON(node, includeCode, snippetLength, projectRoot) {
224
+ const result = {
225
+ id: node.properties.id,
226
+ type: node.properties.semanticType ?? node.labels.at(-1) ?? 'Unknown',
227
+ filePath: projectRoot ? this.makeRelativePath(node.properties.filePath, projectRoot) : node.properties.filePath,
228
+ };
229
+ if (node.properties.name) {
230
+ result.name = node.properties.name;
231
+ }
232
+ if (includeCode && node.properties.sourceCode && node.properties.coreType !== 'SourceFile') {
233
+ const code = node.properties.sourceCode;
234
+ const maxLength = snippetLength; // Use the provided snippet length
235
+ if (code.length <= maxLength) {
236
+ result.sourceCode = code;
237
+ }
238
+ else {
239
+ // Show first half and last half of the snippet
240
+ const half = Math.floor(maxLength / 2);
241
+ result.sourceCode =
242
+ code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
243
+ result.hasMore = true;
244
+ result.truncated = code.length - maxLength;
245
+ }
246
+ }
247
+ return result;
248
+ }
249
+ formatConnectionsByDepthWithReferences(byDepth, maxNodesPerChain) {
250
+ return Object.keys(byDepth)
251
+ .sort((a, b) => parseInt(a) - parseInt(b))
252
+ .map((depth) => {
253
+ const depthConnections = byDepth[parseInt(depth)];
254
+ const connectionsToShow = Math.min(depthConnections.length, maxNodesPerChain);
255
+ const chains = depthConnections.slice(0, connectionsToShow).map((conn) => {
256
+ return (conn.relationshipChain?.map((rel) => ({
257
+ type: rel.type,
258
+ from: rel.start,
259
+ to: rel.end,
260
+ })) ?? []);
261
+ });
262
+ return {
263
+ depth: parseInt(depth),
264
+ count: depthConnections.length,
265
+ chains,
266
+ ...(depthConnections.length > connectionsToShow && {
267
+ hasMore: depthConnections.length - connectionsToShow,
268
+ }),
269
+ };
270
+ });
271
+ }
272
+ getCommonRootPath(nodes) {
273
+ const filePaths = nodes.map((n) => n.properties.filePath).filter(Boolean);
274
+ if (filePaths.length === 0)
275
+ return process.cwd();
276
+ // Split all paths into parts
277
+ const pathParts = filePaths.map((p) => p.split('/'));
278
+ // Find common prefix
279
+ const commonParts = [];
280
+ const firstPath = pathParts[0];
281
+ for (let i = 0; i < firstPath.length; i++) {
282
+ const part = firstPath[i];
283
+ if (pathParts.every((p) => p[i] === part)) {
284
+ commonParts.push(part);
285
+ }
286
+ else {
287
+ break;
288
+ }
289
+ }
290
+ return commonParts.join('/') || '/';
291
+ }
292
+ makeRelativePath(absolutePath, projectRoot) {
293
+ if (!absolutePath)
294
+ return '';
295
+ if (!projectRoot || projectRoot === '/')
296
+ return absolutePath;
297
+ // Ensure both paths end consistently
298
+ const root = projectRoot.endsWith('/') ? projectRoot : projectRoot + '/';
299
+ if (absolutePath.startsWith(root)) {
300
+ return absolutePath.substring(root.length);
301
+ }
302
+ return absolutePath;
303
+ }
304
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ *
4
+ * MCP Server - Main Entry Point
5
+ * Clean, modular architecture for the Code Graph Context MCP Server
6
+ */
7
+ // Load environment variables from .env file
8
+ import dotenv from 'dotenv';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ // Go up two levels from dist/mcp/mcp.server.js to the root
14
+ const rootDir = join(__dirname, '..', '..');
15
+ dotenv.config({ path: join(rootDir, '.env') });
16
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
17
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
18
+ import { MCP_SERVER_CONFIG, MESSAGES } from './constants.js';
19
+ import { initializeServices } from './services.js';
20
+ import { registerAllTools } from './tools/index.js';
21
+ import { debugLog } from './utils.js';
22
+ /**
23
+ * Main server initialization and startup
24
+ */
25
+ const startServer = async () => {
26
+ console.error(JSON.stringify({ level: 'info', message: MESSAGES.server.starting }));
27
+ // Create MCP server instance
28
+ const server = new McpServer({
29
+ name: MCP_SERVER_CONFIG.name,
30
+ version: MCP_SERVER_CONFIG.version,
31
+ });
32
+ // Register all tools
33
+ registerAllTools(server);
34
+ // Initialize external services (non-blocking)
35
+ initializeServices().catch((error) => {
36
+ debugLog('Service initialization error', error);
37
+ });
38
+ // Create and connect transport
39
+ console.error(JSON.stringify({ level: 'info', message: MESSAGES.server.creatingTransport }));
40
+ const transport = new StdioServerTransport();
41
+ console.error(JSON.stringify({ level: 'info', message: MESSAGES.server.connectingTransport }));
42
+ await server.connect(transport);
43
+ console.error(JSON.stringify({ level: 'info', message: MESSAGES.server.connected }));
44
+ };
45
+ // Start the server
46
+ console.error(JSON.stringify({ level: 'info', message: MESSAGES.server.startingServer }));
47
+ await startServer();