Skip to content

Commit 3acbf1d

Browse files
committed
refactor: improve graph traversal output and configuration
- Reduce max traversal depth from 10 to 5 for better performance - Add comprehensive parameter controls to search_codebase tool: - maxDepth, maxNodesPerChain, includeCode, snippetLength, skip - Update tool descriptions with progressive optimization strategy - Change chain format to show relationship paths instead of grouping - Add projectRoot to output for relative path context - Improve node formatting with semanticType and relative paths - Standardize maxNodesPerChain to 5 (applied per depth level) - Update snippet defaults: 700 chars, 30 max results displayed - Remove unused groupConnectionsByRelationshipChain method - Add helper methods for common root path calculation
1 parent 6a882a5 commit 3acbf1d

File tree

5 files changed

+146
-67
lines changed

5 files changed

+146
-67
lines changed

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const MAX_TRAVERSAL_DEPTH = 10;
1+
export const MAX_TRAVERSAL_DEPTH = 5;

src/mcp/constants.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,28 @@ export const TOOL_METADATA = {
3636
title: 'Search Codebase',
3737
description: `Search the codebase using semantic similarity to find relevant code, functions, classes, and implementations.
3838
39-
Returns normalized JSON with source code snippets (maxDepth: 3, maxNodesPerChain: 4). Uses JSON:API pattern to deduplicate nodes.
39+
Returns normalized JSON with source code snippets. Uses JSON:API pattern to deduplicate nodes.
40+
41+
**Default Usage (Recommended)**:
42+
Start with default parameters for richest context in a single call. Most queries complete successfully.
4043
4144
Parameters:
4245
- query: Natural language description of what you're looking for
4346
- limit (default: 10): Number of initial vector search results to consider
4447
45-
Response includes both relationship chains and actual source code for immediate understanding.`,
48+
**Token Optimization (Only if needed)**:
49+
Use these parameters ONLY if you encounter token limit errors (>25,000 tokens):
50+
51+
- maxDepth (default: 3): Reduce to 1-2 for shallow exploration
52+
- maxNodesPerChain (default: 5): Limit chains shown per depth level
53+
- includeCode (default: true): Set false to get structure only, fetch code separately
54+
- snippetLength (default: 700): Reduce to 400-600 for smaller code snippets
55+
- skip (default: 0): For pagination (skip N results)
56+
57+
**Progressive Strategy**:
58+
1. Try with defaults first
59+
2. If token error: Use maxDepth=1, includeCode=false for structure
60+
3. Then traverse deeper or Read specific files for full code`,
4661
},
4762
[TOOL_NAMES.naturalLanguageToCypher]: {
4863
title: 'Natural Language to Cypher',
@@ -60,7 +75,7 @@ Parameters:
6075
6176
Advanced options (use when needed):
6277
- includeCode (default: true): Set to false for structure-only view without source code
63-
- maxNodesPerChain: Limit nodes shown per relationship chain (default: 8)
78+
- maxNodesPerChain (default: 5): Limit chains shown per depth level (applied independently at each depth)
6479
- summaryOnly: Set to true for just file paths and statistics without detailed traversal
6580
6681
Best practices:
@@ -85,9 +100,9 @@ export const DEFAULTS = {
85100
traversalDepth: 3,
86101
skipOffset: 0,
87102
batchSize: 500,
88-
maxResultsDisplayed: 20,
89-
codeSnippetLength: 800,
90-
chainSnippetLength: 800,
103+
maxResultsDisplayed: 30,
104+
codeSnippetLength: 700,
105+
chainSnippetLength: 700,
91106
} as const;
92107

93108
// Messages

src/mcp/handlers/traversal.handler.ts

Lines changed: 78 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ interface Connection {
3535

3636
export class TraversalHandler {
3737
private static readonly NODE_NOT_FOUND_QUERY = 'MATCH (n) WHERE n.id = $nodeId RETURN n';
38-
private static readonly MAX_NODES_PER_CHAIN = 8;
3938

4039
constructor(private neo4jService: Neo4jService) {}
4140

@@ -47,7 +46,7 @@ export class TraversalHandler {
4746
relationshipTypes,
4847
includeStartNodeDetails = true,
4948
includeCode = false,
50-
maxNodesPerChain = TraversalHandler.MAX_NODES_PER_CHAIN,
49+
maxNodesPerChain = 5,
5150
summaryOnly = false,
5251
title = `Node Traversal from: ${nodeId}`,
5352
snippetLength = DEFAULTS.codeSnippetLength,
@@ -84,7 +83,7 @@ export class TraversalHandler {
8483
});
8584

8685
return {
87-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
86+
content: [{ type: 'text', text: JSON.stringify(result) }],
8887
};
8988
} catch (error) {
9089
console.error('Node traversal error:', error);
@@ -137,25 +136,6 @@ export class TraversalHandler {
137136
);
138137
}
139138

140-
private groupConnectionsByRelationshipChain(connections: Connection[]): Record<string, Connection[]> {
141-
return connections.reduce(
142-
(acc, conn) => {
143-
// Build chain with direction arrows
144-
const chain =
145-
conn.relationshipChain
146-
?.map((rel: any) => {
147-
// rel has: { type, start, end, properties }
148-
return rel.type;
149-
})
150-
.join(' → ') ?? 'Unknown';
151-
acc[chain] ??= [];
152-
acc[chain].push(conn);
153-
return acc;
154-
},
155-
{} as Record<string, Connection[]>,
156-
);
157-
}
158-
159139
private getRelationshipDirection(connection: Connection, startNodeId: string): 'OUTGOING' | 'INCOMING' | 'UNKNOWN' {
160140
// Check the first relationship in the chain to determine direction from start node
161141
const firstRel = connection.relationshipChain?.[0] as any;
@@ -190,29 +170,34 @@ export class TraversalHandler {
190170
// JSON:API normalization - collect all unique nodes
191171
const nodeMap = new Map<string, any>();
192172

173+
// Get common root path from all nodes
174+
const allNodes = [startNode, ...traversalData.connections.map((c) => c.node)];
175+
const projectRoot = this.getCommonRootPath(allNodes);
176+
193177
// Add start node to map
194178
if (includeStartNodeDetails) {
195-
const startNodeData = this.formatNodeJSON(startNode, includeCode, snippetLength);
179+
const startNodeData = this.formatNodeJSON(startNode, includeCode, snippetLength, projectRoot);
196180
nodeMap.set(startNode.properties.id, startNodeData);
197181
}
198182

199183
// Collect all unique nodes from connections
200184
traversalData.connections.forEach((conn) => {
201185
const nodeId = conn.node.properties.id;
202186
if (!nodeMap.has(nodeId)) {
203-
nodeMap.set(nodeId, this.formatNodeJSON(conn.node, includeCode, snippetLength));
187+
nodeMap.set(nodeId, this.formatNodeJSON(conn.node, includeCode, snippetLength, projectRoot));
204188
}
205189
});
206190

207191
const byDepth = this.groupConnectionsByDepth(traversalData.connections);
208192

209193
return {
194+
projectRoot,
210195
totalConnections: traversalData.connections.length,
211196
uniqueFiles: this.getUniqueFileCount(traversalData.connections),
212197
maxDepth: Object.keys(byDepth).length > 0 ? Math.max(...Object.keys(byDepth).map((d) => parseInt(d))) : 0,
213198
startNodeId: includeStartNodeDetails ? startNode.properties.id : undefined,
214199
nodes: Object.fromEntries(nodeMap),
215-
depths: this.formatConnectionsByDepthWithReferences(byDepth, maxNodesPerChain, startNode.properties.id),
200+
depths: this.formatConnectionsByDepthWithReferences(byDepth, maxNodesPerChain),
216201
};
217202
}
218203

@@ -227,36 +212,43 @@ export class TraversalHandler {
227212
Object.keys(byDepth).length > 0 ? Math.max(...Object.keys(byDepth).map((d) => parseInt(d))) : 0;
228213
const uniqueFiles = this.getUniqueFileCount(traversalData.connections);
229214

215+
const allNodes = [startNode, ...traversalData.connections.map((c) => c.node)];
216+
const projectRoot = this.getCommonRootPath(allNodes);
217+
230218
const fileMap = new Map<string, number>();
231219
traversalData.connections.forEach((conn) => {
232220
const filePath = conn.node.properties.filePath;
233221
if (filePath) {
234-
fileMap.set(filePath, (fileMap.get(filePath) ?? 0) + 1);
222+
const relativePath = this.makeRelativePath(filePath, projectRoot);
223+
fileMap.set(relativePath, (fileMap.get(relativePath) ?? 0) + 1);
235224
}
236225
});
237226

238227
const connectedFiles = Array.from(fileMap.entries())
239228
.sort((a, b) => b[1] - a[1])
240229
.map(([file, count]) => ({ file, nodeCount: count }));
241230

231+
const maxSummaryFiles = DEFAULTS.maxResultsDisplayed;
232+
242233
return {
234+
projectRoot,
243235
startNodeId: startNode.properties.id,
244236
nodes: {
245-
[startNode.properties.id]: this.formatNodeJSON(startNode, false, 0),
237+
[startNode.properties.id]: this.formatNodeJSON(startNode, false, 0, projectRoot),
246238
},
247239
totalConnections,
248240
maxDepth: maxDepthFound,
249241
uniqueFiles,
250-
files: connectedFiles.slice(0, 20),
251-
...(fileMap.size > 20 && { hasMore: fileMap.size - 20 }),
242+
files: connectedFiles.slice(0, maxSummaryFiles),
243+
...(fileMap.size > maxSummaryFiles && { hasMore: fileMap.size - maxSummaryFiles }),
252244
};
253245
}
254246

255-
private formatNodeJSON(node: Neo4jNode, includeCode: boolean, snippetLength: number): any {
247+
private formatNodeJSON(node: Neo4jNode, includeCode: boolean, snippetLength: number, projectRoot?: string): any {
256248
const result: any = {
257249
id: node.properties.id,
258-
type: node.labels[0] ?? 'Unknown',
259-
filePath: node.properties.filePath,
250+
type: node.properties.semanticType ?? node.labels.at(-1) ?? 'Unknown',
251+
filePath: projectRoot ? this.makeRelativePath(node.properties.filePath, projectRoot) : node.properties.filePath,
260252
};
261253

262254
if (node.properties.name) {
@@ -265,14 +257,15 @@ export class TraversalHandler {
265257

266258
if (includeCode && node.properties.sourceCode && node.properties.coreType !== 'SourceFile') {
267259
const code = node.properties.sourceCode;
268-
const maxLength = 1000; // Show max 1000 chars total
260+
const maxLength = snippetLength; // Use the provided snippet length
269261

270262
if (code.length <= maxLength) {
271263
result.sourceCode = code;
272264
} else {
273-
// Show first 500 and last 500 characters
265+
// Show first half and last half of the snippet
274266
const half = Math.floor(maxLength / 2);
275-
result.sourceCode = code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
267+
result.sourceCode =
268+
code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
276269
result.hasMore = true;
277270
result.truncated = code.length - maxLength;
278271
}
@@ -284,38 +277,70 @@ export class TraversalHandler {
284277
private formatConnectionsByDepthWithReferences(
285278
byDepth: Record<number, Connection[]>,
286279
maxNodesPerChain: number,
287-
startNodeId: string,
288280
): any[] {
289281
return Object.keys(byDepth)
290282
.sort((a, b) => parseInt(a) - parseInt(b))
291283
.map((depth) => {
292284
const depthConnections = byDepth[parseInt(depth)];
293-
const byRelChain = this.groupConnectionsByRelationshipChain(depthConnections);
294-
295-
const chains = Object.entries(byRelChain).map(([chain, nodes]) => {
296-
const firstNode = nodes[0];
297-
const direction = this.getRelationshipDirection(firstNode, startNodeId);
298-
const displayNodes = nodes.slice(0, maxNodesPerChain);
299-
300-
const chainResult: any = {
301-
via: chain,
302-
direction,
303-
count: nodes.length,
304-
nodeIds: displayNodes.map((conn) => conn.node.properties.id),
305-
};
306285

307-
if (nodes.length > maxNodesPerChain) {
308-
chainResult.hasMore = nodes.length - maxNodesPerChain;
309-
}
286+
const connectionsToShow = Math.min(depthConnections.length, maxNodesPerChain);
310287

311-
return chainResult;
288+
const chains = depthConnections.slice(0, connectionsToShow).map((conn) => {
289+
return (
290+
conn.relationshipChain?.map((rel: any) => ({
291+
type: rel.type,
292+
from: rel.start,
293+
to: rel.end,
294+
})) ?? []
295+
);
312296
});
313297

314298
return {
315299
depth: parseInt(depth),
316300
count: depthConnections.length,
317301
chains,
302+
...(depthConnections.length > connectionsToShow && {
303+
hasMore: depthConnections.length - connectionsToShow,
304+
}),
318305
};
319306
});
320307
}
308+
309+
private getCommonRootPath(nodes: Neo4jNode[]): string {
310+
const filePaths = nodes.map((n) => n.properties.filePath).filter(Boolean) as string[];
311+
312+
if (filePaths.length === 0) return process.cwd();
313+
314+
// Split all paths into parts
315+
const pathParts = filePaths.map((p) => p.split('/'));
316+
317+
// Find common prefix
318+
const commonParts: string[] = [];
319+
const firstPath = pathParts[0];
320+
321+
for (let i = 0; i < firstPath.length; i++) {
322+
const part = firstPath[i];
323+
if (pathParts.every((p) => p[i] === part)) {
324+
commonParts.push(part);
325+
} else {
326+
break;
327+
}
328+
}
329+
330+
return commonParts.join('/') || '/';
331+
}
332+
333+
private makeRelativePath(absolutePath: string | undefined, projectRoot: string): string {
334+
if (!absolutePath) return '';
335+
if (!projectRoot || projectRoot === '/') return absolutePath;
336+
337+
// Ensure both paths end consistently
338+
const root = projectRoot.endsWith('/') ? projectRoot : projectRoot + '/';
339+
340+
if (absolutePath.startsWith(root)) {
341+
return absolutePath.substring(root.length);
342+
}
343+
344+
return absolutePath;
345+
}
321346
}

src/mcp/tools/search-codebase.tool.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,41 @@ export const createSearchCodebaseTool = (server: McpServer): void => {
2626
.optional()
2727
.describe(`Maximum number of results to return (default: ${DEFAULTS.searchLimit})`)
2828
.default(DEFAULTS.searchLimit),
29+
maxDepth: z
30+
.number()
31+
.int()
32+
.optional()
33+
.describe(`Maximum depth to traverse relationships (default: ${DEFAULTS.traversalDepth}, max: 10)`)
34+
.default(DEFAULTS.traversalDepth),
35+
maxNodesPerChain: z
36+
.number()
37+
.int()
38+
.optional()
39+
.describe('Maximum chains to show per depth level (default: 5, applied independently at each depth)')
40+
.default(5),
41+
skip: z.number().int().optional().describe('Number of results to skip for pagination (default: 0)').default(0),
42+
includeCode: z
43+
.boolean()
44+
.optional()
45+
.describe('Include source code snippets in results (default: true)')
46+
.default(true),
47+
snippetLength: z
48+
.number()
49+
.int()
50+
.optional()
51+
.describe(`Length of code snippets to include (default: ${DEFAULTS.codeSnippetLength})`)
52+
.default(DEFAULTS.codeSnippetLength),
2953
},
3054
},
31-
async ({ query, limit = DEFAULTS.searchLimit }) => {
55+
async ({
56+
query,
57+
limit = DEFAULTS.searchLimit,
58+
maxDepth = DEFAULTS.traversalDepth,
59+
maxNodesPerChain = 5,
60+
skip = 0,
61+
includeCode = true,
62+
snippetLength = DEFAULTS.codeSnippetLength,
63+
}) => {
3264
try {
3365
await debugLog('Search codebase started', { query, limit });
3466

@@ -54,14 +86,21 @@ export const createSearchCodebaseTool = (server: McpServer): void => {
5486
await debugLog('Vector search completed, starting traversal', {
5587
nodeId,
5688
resultsCount: vectorResults.length,
89+
maxDepth,
90+
maxNodesPerChain,
91+
skip,
92+
includeCode,
93+
snippetLength,
5794
});
5895

5996
return await traversalHandler.traverseFromNode(nodeId, {
60-
maxDepth: 3,
97+
maxDepth,
6198
direction: 'BOTH', // Show both incoming (who calls this) and outgoing (what this calls)
62-
includeCode: true,
63-
maxNodesPerChain: 4,
99+
includeCode,
100+
maxNodesPerChain,
101+
skip,
64102
summaryOnly: false,
103+
snippetLength,
65104
title: `Exploration from Node: ${nodeId}`,
66105
});
67106
} catch (error) {

src/mcp/tools/traverse-from-node.tool.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ export const createTraverseFromNodeTool = (server: McpServer): void => {
5454
.number()
5555
.int()
5656
.optional()
57-
.describe('Maximum nodes to show per relationship chain (default: 8)')
58-
.default(8),
57+
.describe('Maximum chains to show per depth level (default: 5, applied independently at each depth)')
58+
.default(5),
5959
summaryOnly: z
6060
.boolean()
6161
.optional()
@@ -76,7 +76,7 @@ export const createTraverseFromNodeTool = (server: McpServer): void => {
7676
direction = 'BOTH',
7777
relationshipTypes,
7878
includeCode = true,
79-
maxNodesPerChain = 8,
79+
maxNodesPerChain = 5,
8080
summaryOnly = false,
8181
snippetLength = DEFAULTS.codeSnippetLength,
8282
}) => {

0 commit comments

Comments
 (0)