feat(config-audit): MCP tool-count detection with manifest fallback (v5 M1) [skip-docs]

readActiveMcpServers now resolves tool count via:
  1. In-config tools array
  2. Cached tools/list at \$HOME/.claude/config-audit/mcp-cache/<name>.json
  3. node_modules/<pkg>/package.json (resolved from npx <pkg>)
  4. Fallback: { toolCount: null, toolCountUnknown: true }

estimateTokens uses detected toolCount (heavy server > light server).

New fixture: mcp-tool-heavy/ with mocked node_modules/mcp-heavy/package.json (20 tools).

576 → 580 tests, all green.
This commit is contained in:
Kjell Tore Guttormsen 2026-05-01 07:02:08 +02:00
commit 1422daf895
3 changed files with 148 additions and 7 deletions

View file

@ -0,0 +1,6 @@
{
"mcpServers": {
"heavy": { "command": "npx", "args": ["mcp-heavy"] },
"light": { "command": "npx", "args": ["mcp-light"] }
}
}

View file

@ -539,6 +539,70 @@ describe('readActiveMcpServers', () => {
});
});
// ─────────────────────────────────────────────────────────────────────────
// readActiveMcpServers — tool-count detection (v5 M1)
// ─────────────────────────────────────────────────────────────────────────
describe('readActiveMcpServers — tool-count detection (v5 M1)', () => {
it('detects toolCount from project node_modules/<pkg>/package.json', async () => {
const fixturePath = resolve(import.meta.dirname || dirname(new URL(import.meta.url).pathname),
'..', 'fixtures', 'mcp-tool-heavy');
const servers = await readActiveMcpServers(fixturePath);
const heavy = servers.find(s => s.name === 'heavy');
assert.ok(heavy, 'expected heavy server from fixture');
assert.equal(heavy.toolCount, 20, `expected toolCount=20, got ${heavy.toolCount}`);
assert.equal(heavy.toolCountUnknown, false);
});
it('falls back to toolCount: null + toolCountUnknown: true when manifest missing', async () => {
const fixturePath = resolve(import.meta.dirname || dirname(new URL(import.meta.url).pathname),
'..', 'fixtures', 'mcp-tool-heavy');
const servers = await readActiveMcpServers(fixturePath);
const light = servers.find(s => s.name === 'light');
assert.ok(light, 'expected light server from fixture');
assert.equal(light.toolCount, null);
assert.equal(light.toolCountUnknown, true);
});
it('detects toolCount from cache file in $HOME/.claude/config-audit/mcp-cache/', async () => {
const fakeHome = uniqueDir('mcp-cache');
const repoRoot = uniqueDir('mcp-cache-repo');
await mkdir(repoRoot, { recursive: true });
await writeFile(
join(repoRoot, '.mcp.json'),
JSON.stringify({ mcpServers: { cached: { command: 'npx', args: ['unknown-pkg'] } } }, null, 2),
);
await mkdir(join(fakeHome, '.claude', 'config-audit', 'mcp-cache'), { recursive: true });
await writeFile(
join(fakeHome, '.claude', 'config-audit', 'mcp-cache', 'cached.json'),
JSON.stringify({ tools: Array.from({ length: 12 }, (_, i) => ({ name: `t${i}` })) }, null, 2),
);
const originalHome = process.env.HOME;
process.env.HOME = fakeHome;
try {
const servers = await readActiveMcpServers(repoRoot);
const cached = servers.find(s => s.name === 'cached');
assert.ok(cached, 'expected cached server');
assert.equal(cached.toolCount, 12, `expected toolCount=12 from cache, got ${cached.toolCount}`);
assert.equal(cached.toolCountUnknown, false);
} finally {
process.env.HOME = originalHome;
await rm(fakeHome, { recursive: true, force: true });
await rm(repoRoot, { recursive: true, force: true });
}
});
it('toolCount drives estimateTokens (heavy > light)', async () => {
const fixturePath = resolve(import.meta.dirname || dirname(new URL(import.meta.url).pathname),
'..', 'fixtures', 'mcp-tool-heavy');
const servers = await readActiveMcpServers(fixturePath);
const heavy = servers.find(s => s.name === 'heavy');
const light = servers.find(s => s.name === 'light');
assert.ok(heavy.estimatedTokens > light.estimatedTokens,
`expected heavy (${heavy.estimatedTokens}) > light (${light.estimatedTokens})`);
});
});
// ─────────────────────────────────────────────────────────────────────────
// readActiveConfig (integration)
// ─────────────────────────────────────────────────────────────────────────