Skip to content

Commit bb8511c

Browse files
committed
feat(mcp): add dev_map tool for codebase overview
Create MapAdapter that provides the dev_map MCP tool: - Shows directory tree with component counts - Displays exported symbols per directory - Configurable depth (1-5 levels) - Focus on specific directory path - Token budget with automatic depth reduction - Uses startTimer and estimateTokensForText utilities Registered in MCP server alongside existing adapters. Part of #81
1 parent 45f023b commit bb8511c

File tree

3 files changed

+256
-0
lines changed

3 files changed

+256
-0
lines changed

packages/mcp-server/bin/dev-agent-mcp.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
ExploreAdapter,
2222
GitHubAdapter,
2323
HealthAdapter,
24+
MapAdapter,
2425
PlanAdapter,
2526
RefsAdapter,
2627
SearchAdapter,
@@ -186,6 +187,12 @@ async function main() {
186187
defaultLimit: 20,
187188
});
188189

190+
const mapAdapter = new MapAdapter({
191+
repositoryIndexer: indexer,
192+
defaultDepth: 2,
193+
defaultTokenBudget: 2000,
194+
});
195+
189196
// Create MCP server with coordinator
190197
const server = new MCPServer({
191198
serverInfo: {
@@ -205,6 +212,7 @@ async function main() {
205212
githubAdapter,
206213
healthAdapter,
207214
refsAdapter,
215+
mapAdapter,
208216
],
209217
coordinator,
210218
});

packages/mcp-server/src/adapters/built-in/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
export { ExploreAdapter, type ExploreAdapterConfig } from './explore-adapter';
77
export { GitHubAdapter, type GitHubAdapterConfig } from './github-adapter';
88
export { HealthAdapter, type HealthCheckConfig } from './health-adapter';
9+
export { MapAdapter, type MapAdapterConfig } from './map-adapter';
910
export { PlanAdapter, type PlanAdapterConfig } from './plan-adapter';
1011
export { RefsAdapter, type RefsAdapterConfig } from './refs-adapter';
1112
export { SearchAdapter, type SearchAdapterConfig } from './search-adapter';
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/**
2+
* Map Adapter
3+
* Provides codebase structure overview via the dev_map tool
4+
*/
5+
6+
import {
7+
formatCodebaseMap,
8+
generateCodebaseMap,
9+
type MapOptions,
10+
type RepositoryIndexer,
11+
} from '@lytics/dev-agent-core';
12+
import { estimateTokensForText, startTimer } from '../../formatters/utils';
13+
import { ToolAdapter } from '../tool-adapter';
14+
import type { AdapterContext, ToolDefinition, ToolExecutionContext, ToolResult } from '../types';
15+
16+
/**
17+
* Map adapter configuration
18+
*/
19+
export interface MapAdapterConfig {
20+
/**
21+
* Repository indexer instance
22+
*/
23+
repositoryIndexer: RepositoryIndexer;
24+
25+
/**
26+
* Default depth for map generation
27+
*/
28+
defaultDepth?: number;
29+
30+
/**
31+
* Default token budget
32+
*/
33+
defaultTokenBudget?: number;
34+
}
35+
36+
/**
37+
* Map Adapter
38+
* Implements the dev_map tool for codebase structure overview
39+
*/
40+
export class MapAdapter extends ToolAdapter {
41+
readonly metadata = {
42+
name: 'map-adapter',
43+
version: '1.0.0',
44+
description: 'Codebase structure overview adapter',
45+
author: 'Dev-Agent Team',
46+
};
47+
48+
private indexer: RepositoryIndexer;
49+
private config: Required<MapAdapterConfig>;
50+
51+
constructor(config: MapAdapterConfig) {
52+
super();
53+
this.indexer = config.repositoryIndexer;
54+
this.config = {
55+
repositoryIndexer: config.repositoryIndexer,
56+
defaultDepth: config.defaultDepth ?? 2,
57+
defaultTokenBudget: config.defaultTokenBudget ?? 2000,
58+
};
59+
}
60+
61+
async initialize(context: AdapterContext): Promise<void> {
62+
context.logger.info('MapAdapter initialized', {
63+
defaultDepth: this.config.defaultDepth,
64+
defaultTokenBudget: this.config.defaultTokenBudget,
65+
});
66+
}
67+
68+
getToolDefinition(): ToolDefinition {
69+
return {
70+
name: 'dev_map',
71+
description:
72+
'Get a high-level overview of the codebase structure. Shows directories, component counts, and exported symbols.',
73+
inputSchema: {
74+
type: 'object',
75+
properties: {
76+
depth: {
77+
type: 'number',
78+
description: `Directory depth to show (1-5, default: ${this.config.defaultDepth})`,
79+
minimum: 1,
80+
maximum: 5,
81+
default: this.config.defaultDepth,
82+
},
83+
focus: {
84+
type: 'string',
85+
description: 'Focus on a specific directory path (e.g., "packages/core/src")',
86+
},
87+
includeExports: {
88+
type: 'boolean',
89+
description: 'Include exported symbols in output (default: true)',
90+
default: true,
91+
},
92+
tokenBudget: {
93+
type: 'number',
94+
description: `Maximum tokens for output (default: ${this.config.defaultTokenBudget})`,
95+
minimum: 500,
96+
maximum: 10000,
97+
default: this.config.defaultTokenBudget,
98+
},
99+
},
100+
required: [],
101+
},
102+
};
103+
}
104+
105+
async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<ToolResult> {
106+
const {
107+
depth = this.config.defaultDepth,
108+
focus,
109+
includeExports = true,
110+
tokenBudget = this.config.defaultTokenBudget,
111+
} = args as {
112+
depth?: number;
113+
focus?: string;
114+
includeExports?: boolean;
115+
tokenBudget?: number;
116+
};
117+
118+
// Validate depth
119+
if (typeof depth !== 'number' || depth < 1 || depth > 5) {
120+
return {
121+
success: false,
122+
error: {
123+
code: 'INVALID_DEPTH',
124+
message: 'Depth must be a number between 1 and 5',
125+
},
126+
};
127+
}
128+
129+
// Validate focus if provided
130+
if (focus !== undefined && typeof focus !== 'string') {
131+
return {
132+
success: false,
133+
error: {
134+
code: 'INVALID_FOCUS',
135+
message: 'Focus must be a string path',
136+
},
137+
};
138+
}
139+
140+
// Validate tokenBudget
141+
if (typeof tokenBudget !== 'number' || tokenBudget < 500 || tokenBudget > 10000) {
142+
return {
143+
success: false,
144+
error: {
145+
code: 'INVALID_TOKEN_BUDGET',
146+
message: 'Token budget must be a number between 500 and 10000',
147+
},
148+
};
149+
}
150+
151+
try {
152+
const timer = startTimer();
153+
context.logger.debug('Generating codebase map', {
154+
depth,
155+
focus,
156+
includeExports,
157+
tokenBudget,
158+
});
159+
160+
const mapOptions: MapOptions = {
161+
depth,
162+
focus: focus || '',
163+
includeExports,
164+
tokenBudget,
165+
};
166+
167+
// Generate the map
168+
const map = await generateCodebaseMap(this.indexer, mapOptions);
169+
170+
// Format the output
171+
let content = formatCodebaseMap(map, mapOptions);
172+
173+
// Check token budget and truncate if needed
174+
let tokens = estimateTokensForText(content);
175+
let truncated = false;
176+
177+
if (tokens > tokenBudget) {
178+
// Try reducing depth
179+
let reducedDepth = depth;
180+
while (tokens > tokenBudget && reducedDepth > 1) {
181+
reducedDepth--;
182+
const reducedMap = await generateCodebaseMap(this.indexer, {
183+
...mapOptions,
184+
depth: reducedDepth,
185+
});
186+
content = formatCodebaseMap(reducedMap, { ...mapOptions, depth: reducedDepth });
187+
tokens = estimateTokensForText(content);
188+
truncated = true;
189+
}
190+
191+
if (truncated) {
192+
content += `\n\n*Note: Depth reduced to ${reducedDepth} to fit token budget*`;
193+
}
194+
}
195+
196+
const duration_ms = timer.elapsed();
197+
198+
context.logger.info('Codebase map generated', {
199+
depth,
200+
focus,
201+
totalComponents: map.totalComponents,
202+
totalDirectories: map.totalDirectories,
203+
tokens,
204+
truncated,
205+
duration_ms,
206+
});
207+
208+
return {
209+
success: true,
210+
data: {
211+
content,
212+
totalComponents: map.totalComponents,
213+
totalDirectories: map.totalDirectories,
214+
depth,
215+
focus: focus || null,
216+
truncated,
217+
},
218+
metadata: {
219+
tokens,
220+
duration_ms,
221+
timestamp: new Date().toISOString(),
222+
cached: false,
223+
},
224+
};
225+
} catch (error) {
226+
context.logger.error('Map generation failed', { error });
227+
return {
228+
success: false,
229+
error: {
230+
code: 'MAP_FAILED',
231+
message: error instanceof Error ? error.message : 'Unknown error',
232+
details: error,
233+
},
234+
};
235+
}
236+
}
237+
238+
estimateTokens(args: Record<string, unknown>): number {
239+
const { depth = this.config.defaultDepth, tokenBudget = this.config.defaultTokenBudget } = args;
240+
241+
// Estimate based on depth - each level roughly doubles the output
242+
const baseTokens = 100;
243+
const depthMultiplier = 2 ** ((depth as number) - 1);
244+
245+
return Math.min(baseTokens * depthMultiplier, tokenBudget as number);
246+
}
247+
}

0 commit comments

Comments
 (0)