feat(config-audit): CA-TOK-005 MCP tool-schema budget (v5 N1) [skip-docs]
Adds detectMcpToolBudget detection block in TOK scanner. Tiered severity
per project-local .mcp.json server based on toolCount:
- < 20: no finding
- 20-49: low
- 50-99: medium
- 100+: high
- null (manifest unparseable): low + "tool count unknown" message
Scoped to source==='.mcp.json' to keep findings actionable for the
audited path; plugin/user-level MCP servers are surfaced by the
manifest scanner (Step 19 / N2).
5 fixtures (mcp-budget/{14,25,60,120,unknown}-tools) use inline `tools`
arrays in .mcp.json — no node_modules needed for these tests.
Tests assert title+severity (not exact ID) since TOK IDs are sequential
per scan, not semantic per pattern.
[skip-docs] reason: v5 plan fences off README/CLAUDE.md badge updates
to Session 5; Forgejo pre-commit-docs-gate hook requires this tag on
feat commits without doc changes.
Tests: 586 → 593 (+7).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
dd0d4bf738
commit
b2407a09b3
7 changed files with 164 additions and 0 deletions
|
|
@ -222,6 +222,69 @@ describe('TOK scanner — M4 cascade > 10k tokens (v5)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('TOK scanner — N1 MCP tool-schema budget (v5 CA-TOK-005)', () => {
|
||||
// readActiveConfig pulls in ambient ~/.claude.json plugin MCP servers; tests
|
||||
// filter to the fixture's own server name (budget-srv-<count>) to avoid
|
||||
// user-state leakage. Findings identified by title (not exact ID) — TOK IDs
|
||||
// are sequential per scan.
|
||||
const findFixtureBudget = (result, count) =>
|
||||
result.findings.find(f =>
|
||||
/MCP tool-schema budget/i.test(f.title || '') &&
|
||||
(f.title || '').includes(`budget-srv-${count}`)
|
||||
);
|
||||
|
||||
it('14 tools → no budget finding (under 20-tool floor)', async () => {
|
||||
const result = await runScanner('mcp-budget/14-tools');
|
||||
const f = findFixtureBudget(result, 14);
|
||||
assert.equal(f, undefined,
|
||||
`expected no budget finding for budget-srv-14 under 20 tools; got: ${f?.title}`);
|
||||
});
|
||||
|
||||
it('25 tools → low severity', async () => {
|
||||
const result = await runScanner('mcp-budget/25-tools');
|
||||
const f = findFixtureBudget(result, 25);
|
||||
assert.ok(f, `expected budget finding for budget-srv-25; got: ${result.findings.map(x => x.title).join(' | ')}`);
|
||||
assert.equal(f.severity, 'low', `expected low for 25 tools, got ${f.severity}`);
|
||||
});
|
||||
|
||||
it('60 tools → medium severity', async () => {
|
||||
const result = await runScanner('mcp-budget/60-tools');
|
||||
const f = findFixtureBudget(result, 60);
|
||||
assert.ok(f, `expected budget finding for budget-srv-60`);
|
||||
assert.equal(f.severity, 'medium', `expected medium for 60 tools, got ${f.severity}`);
|
||||
});
|
||||
|
||||
it('120 tools → high severity', async () => {
|
||||
const result = await runScanner('mcp-budget/120-tools');
|
||||
const f = findFixtureBudget(result, 120);
|
||||
assert.ok(f, `expected budget finding for budget-srv-120`);
|
||||
assert.equal(f.severity, 'high', `expected high for 120 tools, got ${f.severity}`);
|
||||
});
|
||||
|
||||
it('unknown toolCount → low severity with "unknown" in evidence', async () => {
|
||||
const result = await runScanner('mcp-budget/unknown-tools');
|
||||
const f = findFixtureBudget(result, 'unknown');
|
||||
assert.ok(f, `expected budget finding for budget-srv-unknown`);
|
||||
assert.equal(f.severity, 'low', `expected low for unknown toolCount, got ${f.severity}`);
|
||||
assert.match(String(f.evidence || ''), /unknown/i,
|
||||
`expected "unknown" in evidence, got: ${f.evidence}`);
|
||||
});
|
||||
|
||||
it('finding ID matches CA-TOK-NNN format', async () => {
|
||||
const result = await runScanner('mcp-budget/120-tools');
|
||||
const f = findFixtureBudget(result, 120);
|
||||
assert.ok(f);
|
||||
assert.match(f.id, /^CA-TOK-\d{3}$/);
|
||||
});
|
||||
|
||||
it('finding evidence carries calibration_note', async () => {
|
||||
const result = await runScanner('mcp-budget/60-tools');
|
||||
const f = findFixtureBudget(result, 60);
|
||||
assert.ok(f);
|
||||
assert.match(String(f.evidence || ''), /severity reflects estimated tokens\/turn/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TOK scanner — F7 severity recalibration (v5)', () => {
|
||||
// Findings identified by title pattern, not finding ID — TOK IDs are
|
||||
// sequential per scan run, not semantic per pattern (output.mjs:31).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue