Skip to content

Commit 2c565fa

Browse files
feat: add file limit to structure tool to reduce token usage
The MCP structure tool returned all files with full metrics for every directory, easily exceeding 12k tokens. Add a global file limit (default 25) that caps per-file listings across all directories. Directory-level summaries always appear. When files are suppressed, a warning recommends --full or narrowing with --directory. - structureData(): add full and fileLimit options - formatStructure(): append warning when files are omitted - MCP: add full boolean to structure tool schema - CLI: add --full flag to structure command Impact: 3 functions changed, 2 affected
1 parent 1b97fb9 commit 2c565fa

4 files changed

Lines changed: 69 additions & 1 deletion

File tree

src/cli.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ program
502502
.option('-d, --db <path>', 'Path to graph.db')
503503
.option('--depth <n>', 'Max directory depth')
504504
.option('--sort <metric>', 'Sort by: cohesion | fan-in | fan-out | density | files', 'files')
505+
.option('--full', 'Show all files without limit')
505506
.option('-T, --no-tests', 'Exclude test/spec files')
506507
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
507508
.option('-j, --json', 'Output as JSON')
@@ -511,6 +512,7 @@ program
511512
directory: dir,
512513
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
513514
sort: opts.sort,
515+
full: opts.full,
514516
noTests: resolveNoTests(opts),
515517
});
516518
if (opts.json) {

src/mcp.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ const BASE_TOOLS = [
259259
{
260260
name: 'structure',
261261
description:
262-
'Show project structure with directory hierarchy, cohesion scores, and per-file metrics',
262+
'Show project structure with directory hierarchy, cohesion scores, and per-file metrics. Per-file details are capped at 25 files by default; use full=true to show all.',
263263
inputSchema: {
264264
type: 'object',
265265
properties: {
@@ -270,6 +270,11 @@ const BASE_TOOLS = [
270270
enum: ['cohesion', 'fan-in', 'fan-out', 'density', 'files'],
271271
description: 'Sort directories by metric',
272272
},
273+
full: {
274+
type: 'boolean',
275+
description: 'Return all files without limit',
276+
default: false,
277+
},
273278
},
274279
},
275280
},
@@ -571,6 +576,7 @@ export async function startMCPServer(customDbPath, options = {}) {
571576
directory: args.directory,
572577
depth: args.depth,
573578
sort: args.sort,
579+
full: args.full,
574580
});
575581
break;
576582
}

src/structure.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ export function structureData(customDbPath, opts = {}) {
330330
const maxDepth = opts.depth || null;
331331
const sortBy = opts.sort || 'files';
332332
const noTests = opts.noTests || false;
333+
const full = opts.full || false;
334+
const fileLimit = opts.fileLimit || 25;
333335

334336
// Get all directory nodes with their metrics
335337
let dirs = db
@@ -403,6 +405,33 @@ export function structureData(customDbPath, opts = {}) {
403405
});
404406

405407
db.close();
408+
409+
// Apply global file limit unless full mode
410+
if (!full) {
411+
const totalFiles = result.reduce((sum, d) => sum + d.files.length, 0);
412+
if (totalFiles > fileLimit) {
413+
let shown = 0;
414+
for (const d of result) {
415+
const remaining = fileLimit - shown;
416+
if (remaining <= 0) {
417+
d.files = [];
418+
} else if (d.files.length > remaining) {
419+
d.files = d.files.slice(0, remaining);
420+
shown = fileLimit;
421+
} else {
422+
shown += d.files.length;
423+
}
424+
}
425+
const suppressed = totalFiles - fileLimit;
426+
return {
427+
directories: result,
428+
count: result.length,
429+
suppressed,
430+
warning: `${suppressed} files omitted (showing ${fileLimit}/${totalFiles}). Use --full to show all files, or narrow with --directory.`,
431+
};
432+
}
433+
}
434+
406435
return { directories: result, count: result.length };
407436
}
408437

@@ -539,6 +568,10 @@ export function formatStructure(data) {
539568
);
540569
}
541570
}
571+
if (data.warning) {
572+
lines.push('');
573+
lines.push(`⚠ ${data.warning}`);
574+
}
542575
return lines.join('\n');
543576
}
544577

tests/integration/structure.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,33 @@ describe('structureData', () => {
133133
});
134134
});
135135

136+
describe('structureData file limit', () => {
137+
test('default fileLimit truncates files and includes warning when exceeded', () => {
138+
// Use a very low fileLimit to trigger truncation on the small fixture
139+
const data = structureData(dbPath, { fileLimit: 2 });
140+
const shownFiles = data.directories.reduce((sum, d) => sum + d.files.length, 0);
141+
expect(shownFiles).toBeLessThanOrEqual(2);
142+
expect(data.suppressed).toBeGreaterThan(0);
143+
expect(data.warning).toMatch(/files omitted/);
144+
expect(data.warning).toMatch(/--full/);
145+
});
146+
147+
test('full: true returns all files without warning', () => {
148+
const data = structureData(dbPath, { full: true });
149+
const totalFiles = data.directories.reduce((sum, d) => sum + d.files.length, 0);
150+
expect(totalFiles).toBeGreaterThan(0);
151+
expect(data.suppressed).toBeUndefined();
152+
expect(data.warning).toBeUndefined();
153+
});
154+
155+
test('no truncation when total files are within limit', () => {
156+
// fileLimit higher than total files should not add warning
157+
const data = structureData(dbPath, { fileLimit: 100 });
158+
expect(data.suppressed).toBeUndefined();
159+
expect(data.warning).toBeUndefined();
160+
});
161+
});
162+
136163
describe('hotspotsData', () => {
137164
test('returns file hotspots ranked by fan-in', () => {
138165
const data = hotspotsData(dbPath, { metric: 'fan-in', level: 'file', limit: 5 });

0 commit comments

Comments
 (0)