feat(tokens): MCP tool-schema deferral check + CLI-over-MCP lever (v5.10 B4)
By default Claude Code defers MCP tool schemas (names-only, ~120 tok; full
schemas on demand via tool search). CA-TOK-006 detects config-file signals that
force the FULL schemas into the always-loaded prefix every turn:
- settings.json env.ENABLE_TOOL_SEARCH="false" (high)
- "ToolSearch" in permissions.deny (high)
- configured model is a Haiku model (medium)
- per-server .mcp.json alwaysLoad:true (CC v2.1.121+) (high)
auto[:N] is threshold mode (info, not a trigger).
New engine lib/mcp-deferral.mjs: pure assessMcpDeferral({settings,mcpServers})
(unit-tested, no IO) + thin IO wrapper assessMcpDeferralForRepo shared by TOK and
GAP. Severity scales with aggregate forced-upfront tokens (medium-confidence
reasons cap at medium). feature-gap cliOverMcpLeverFinding fires only as a
companion to CA-TOK-006 (prefer gh/aws/gcloud over MCP for common ops).
Honest scoping (Verifiseringsplikt): triggers on config files ONLY — never
process.env shell vars. Vertex / custom ANTHROPIC_BASE_URL / runtime /model
switch are launch state (would flap snapshots machine-dependently), so they are
DISCLOSED in every finding, not triggered. Tool-level anthropic/alwaysLoad and
claude.ai connectors likewise disclosed. Mechanism verified 2026-06-23 against
code.claude.com/docs (context-window.md, mcp.md#configure-tool-search +
#exempt-a-server-from-deferral, costs.md); the prefix-cache invalidation claim
was NOT-CONFIRMED in docs and is not asserted.
alwaysLoad added to CA-MCP VALID_SERVER_FIELDS (no longer flagged as unknown).
active-config-reader surfaces per-server alwaysLoad. Byte-stable: CA-TOK-006
fires only on new conditions; frozen v5.0.0 + SC-5 snapshots untouched.
Tests 1215 -> 1239 (engine 16, integration 5, lever 2, mcp-field guard 1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
42e48514e4
commit
8f7e196046
13 changed files with 692 additions and 2 deletions
|
|
@ -28,6 +28,11 @@ import { finding, scannerResult } from './lib/output.mjs';
|
|||
import { SEVERITY } from './lib/severity.mjs';
|
||||
import { findImports, parseJson, parseFrontmatter } from './lib/yaml-parser.mjs';
|
||||
import { estimateTokens, readActiveConfig, deriveLoadPattern } from './lib/active-config-reader.mjs';
|
||||
import {
|
||||
assessMcpDeferralForRepo,
|
||||
severityForForcedSchemas,
|
||||
DEFERRAL_DISCLOSURE,
|
||||
} from './lib/mcp-deferral.mjs';
|
||||
|
||||
const SCANNER = 'TOK';
|
||||
|
||||
|
|
@ -580,6 +585,61 @@ export async function scan(targetPath, discovery) {
|
|||
}));
|
||||
}
|
||||
|
||||
// ── Pattern I: MCP tool-schema deferral (v5.10 B4, CA-TOK-006) ──
|
||||
// By default MCP tool schemas are DEFERRED (names-only, ~120 tok); certain
|
||||
// config signals force the FULL schemas into the always-loaded prefix every
|
||||
// turn. Scope: project-local .mcp.json servers (mirrors Pattern G — plugin /
|
||||
// global servers are the manifest's concern). Static config-file check only;
|
||||
// runtime conditions (Vertex / ANTHROPIC_BASE_URL / runtime /model switch) are
|
||||
// DISCLOSED (DEFERRAL_DISCLOSURE), not triggered, so the finding is deterministic.
|
||||
const mcpForDeferral = (activeConfig && Array.isArray(activeConfig.mcpServers))
|
||||
? activeConfig.mcpServers.filter(m => m && m.enabled && m.source === '.mcp.json')
|
||||
: [];
|
||||
if (mcpForDeferral.length > 0) {
|
||||
const a = await assessMcpDeferralForRepo(targetPath, { mcpServers: activeConfig.mcpServers });
|
||||
if (a.forcedUpfront) {
|
||||
const conf = a.confidence || 'high';
|
||||
const severity = severityForForcedSchemas(a.aggregateTokens, conf);
|
||||
const names = a.affectedServers.map(m => m.name).join(', ');
|
||||
const reasonText = {
|
||||
'enable-tool-search-false': 'ENABLE_TOOL_SEARCH is set to "false" in settings',
|
||||
'deny-tool-search': '"ToolSearch" is listed in permissions.deny',
|
||||
'haiku-model': 'the configured model is a Haiku model (Haiku has no tool-search support)',
|
||||
}[a.reason];
|
||||
const description = a.toolSearchDisabled
|
||||
? `Tool search is disabled (${reasonText}), so the full tool schemas of ` +
|
||||
`${a.affectedServers.length} active project MCP server${a.affectedServers.length === 1 ? '' : 's'} ` +
|
||||
`(~${a.aggregateTokens} tokens) load into the always-loaded prefix on every turn instead of ` +
|
||||
'being deferred (tool names only, ~120 tokens total). Every schema token is re-sent each turn ' +
|
||||
'whether or not a tool is used.'
|
||||
: `${a.alwaysLoadServers.length} project MCP server${a.alwaysLoadServers.length === 1 ? '' : 's'} ` +
|
||||
`marked alwaysLoad (${names}) load their full tool schemas (~${a.aggregateTokens} tokens) into ` +
|
||||
'the always-loaded prefix on every turn regardless of tool search, instead of deferring them ' +
|
||||
'(names only, ~120 tokens).';
|
||||
const evidence =
|
||||
`reason=${a.reason || 'alwaysLoad'}; confidence=${conf}; servers=${names}; ` +
|
||||
`forced_schema_tokens~${a.aggregateTokens} — ${CALIBRATION_NOTE}. ${DEFERRAL_DISCLOSURE}`;
|
||||
const recommendation = a.toolSearchDisabled
|
||||
? 'Re-enable tool search so MCP schemas defer (names-only) by default: remove ' +
|
||||
'ENABLE_TOOL_SEARCH="false" / the "ToolSearch" deny, or stop defaulting to a Haiku model. ' +
|
||||
'Also disable unused servers via /mcp, and prefer CLI tools (gh / aws / gcloud) over MCP for ' +
|
||||
'common operations — CLI adds zero context tokens until used.'
|
||||
: 'Drop alwaysLoad on large-schema servers so they defer (names-only) until a tool is needed; ' +
|
||||
'keep alwaysLoad only for small servers you call on most turns. Prefer CLI tools ' +
|
||||
'(gh / aws / gcloud) over MCP for common operations.';
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity,
|
||||
title: 'MCP tool schemas forced into the always-loaded prefix',
|
||||
file: null,
|
||||
evidence,
|
||||
description,
|
||||
recommendation,
|
||||
category: 'token-efficiency',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hotspots ranking ──
|
||||
const hotspots = await buildHotspots(discovery, targetPath, activeConfig);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue