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>
192 lines
6.9 KiB
JavaScript
192 lines
6.9 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
assessMcpDeferral,
|
|
severityForForcedSchemas,
|
|
FORCED_SCHEMA_TOKENS_HIGH,
|
|
FORCED_SCHEMA_TOKENS_MEDIUM,
|
|
} from '../../scanners/lib/mcp-deferral.mjs';
|
|
|
|
// A small helper to build server shapes the way active-config-reader exposes them.
|
|
const srv = (over = {}) => ({
|
|
name: 'srv',
|
|
source: '.mcp.json',
|
|
enabled: true,
|
|
toolCount: 10,
|
|
estimatedTokens: 2500,
|
|
alwaysLoad: false,
|
|
...over,
|
|
});
|
|
|
|
describe('assessMcpDeferral — default (deferred) case', () => {
|
|
it('no settings + a plain server → tool search NOT disabled, nothing forced upfront', () => {
|
|
const a = assessMcpDeferral({ settings: {}, mcpServers: [srv()] });
|
|
assert.equal(a.toolSearchDisabled, false);
|
|
assert.equal(a.reason, null);
|
|
assert.equal(a.confidence, null);
|
|
assert.equal(a.forcedUpfront, false);
|
|
assert.deepEqual(a.affectedServers, []);
|
|
assert.equal(a.aggregateTokens, 0);
|
|
});
|
|
|
|
it('empty inputs are tolerated', () => {
|
|
const a = assessMcpDeferral({});
|
|
assert.equal(a.toolSearchDisabled, false);
|
|
assert.equal(a.forcedUpfront, false);
|
|
});
|
|
});
|
|
|
|
describe('assessMcpDeferral — config-file disabling signals (HIGH confidence)', () => {
|
|
it('settings.env.ENABLE_TOOL_SEARCH="false" → disabled, high, all servers affected', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { env: { ENABLE_TOOL_SEARCH: 'false' } },
|
|
mcpServers: [srv({ name: 'a', estimatedTokens: 1000 }), srv({ name: 'b', estimatedTokens: 2000 })],
|
|
});
|
|
assert.equal(a.toolSearchDisabled, true);
|
|
assert.equal(a.reason, 'enable-tool-search-false');
|
|
assert.equal(a.confidence, 'high');
|
|
assert.equal(a.forcedUpfront, true);
|
|
assert.equal(a.affectedServers.length, 2);
|
|
assert.equal(a.aggregateTokens, 3000);
|
|
});
|
|
|
|
it('permissions.deny including bare "ToolSearch" → disabled, high', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { permissions: { deny: ['Read(./.env)', 'ToolSearch'] } },
|
|
mcpServers: [srv()],
|
|
});
|
|
assert.equal(a.toolSearchDisabled, true);
|
|
assert.equal(a.reason, 'deny-tool-search');
|
|
assert.equal(a.confidence, 'high');
|
|
});
|
|
|
|
it('explicit ENABLE_TOOL_SEARCH="false" wins over a haiku model for the reason label', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { env: { ENABLE_TOOL_SEARCH: 'false' }, model: 'claude-haiku-4-5' },
|
|
mcpServers: [srv()],
|
|
});
|
|
assert.equal(a.toolSearchDisabled, true);
|
|
assert.equal(a.reason, 'enable-tool-search-false');
|
|
assert.equal(a.confidence, 'high');
|
|
});
|
|
});
|
|
|
|
describe('assessMcpDeferral — configured Haiku model (MEDIUM confidence)', () => {
|
|
it('settings.model matching /haiku/ → disabled, medium, haiku-model', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { model: 'claude-haiku-4-5-20251001' },
|
|
mcpServers: [srv()],
|
|
});
|
|
assert.equal(a.toolSearchDisabled, true);
|
|
assert.equal(a.reason, 'haiku-model');
|
|
assert.equal(a.confidence, 'medium');
|
|
assert.equal(a.forcedUpfront, true);
|
|
});
|
|
|
|
it('haiku disables even when ENABLE_TOOL_SEARCH="true" (Haiku lacks tool_reference support)', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { env: { ENABLE_TOOL_SEARCH: 'true' }, model: 'haiku' },
|
|
mcpServers: [srv()],
|
|
});
|
|
assert.equal(a.toolSearchDisabled, true);
|
|
assert.equal(a.reason, 'haiku-model');
|
|
});
|
|
});
|
|
|
|
describe('assessMcpDeferral — non-disabling values', () => {
|
|
it('ENABLE_TOOL_SEARCH="true" (non-haiku) → NOT disabled', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { env: { ENABLE_TOOL_SEARCH: 'true' }, model: 'claude-sonnet-4-6' },
|
|
mcpServers: [srv()],
|
|
});
|
|
assert.equal(a.toolSearchDisabled, false);
|
|
assert.equal(a.forcedUpfront, false);
|
|
});
|
|
|
|
it('ENABLE_TOOL_SEARCH="auto" → threshold mode, NOT disabled', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { env: { ENABLE_TOOL_SEARCH: 'auto' } },
|
|
mcpServers: [srv()],
|
|
});
|
|
assert.equal(a.toolSearchDisabled, false);
|
|
assert.equal(a.thresholdMode, true);
|
|
assert.equal(a.forcedUpfront, false);
|
|
});
|
|
|
|
it('ENABLE_TOOL_SEARCH="auto:5" → threshold mode', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { env: { ENABLE_TOOL_SEARCH: 'auto:5' } },
|
|
mcpServers: [srv()],
|
|
});
|
|
assert.equal(a.thresholdMode, true);
|
|
assert.equal(a.toolSearchDisabled, false);
|
|
});
|
|
});
|
|
|
|
describe('assessMcpDeferral — per-server alwaysLoad', () => {
|
|
it('alwaysLoad:true server with tool search enabled → that server forced upfront only', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: {},
|
|
mcpServers: [
|
|
srv({ name: 'always', alwaysLoad: true, estimatedTokens: 1500 }),
|
|
srv({ name: 'deferred', alwaysLoad: false, estimatedTokens: 9000 }),
|
|
],
|
|
});
|
|
assert.equal(a.toolSearchDisabled, false);
|
|
assert.equal(a.forcedUpfront, true);
|
|
assert.equal(a.alwaysLoadServers.length, 1);
|
|
assert.equal(a.affectedServers.length, 1);
|
|
assert.equal(a.affectedServers[0].name, 'always');
|
|
assert.equal(a.aggregateTokens, 1500);
|
|
});
|
|
|
|
it('when tool search is disabled, ALL active servers are affected (not just alwaysLoad)', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { env: { ENABLE_TOOL_SEARCH: 'false' } },
|
|
mcpServers: [
|
|
srv({ name: 'always', alwaysLoad: true, estimatedTokens: 1500 }),
|
|
srv({ name: 'deferred', alwaysLoad: false, estimatedTokens: 2500 }),
|
|
],
|
|
});
|
|
assert.equal(a.affectedServers.length, 2);
|
|
assert.equal(a.aggregateTokens, 4000);
|
|
});
|
|
});
|
|
|
|
describe('assessMcpDeferral — server filtering', () => {
|
|
it('disabled servers (enabled:false) are excluded from affected + aggregate', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { env: { ENABLE_TOOL_SEARCH: 'false' } },
|
|
mcpServers: [
|
|
srv({ name: 'on', enabled: true, estimatedTokens: 1000 }),
|
|
srv({ name: 'off', enabled: false, estimatedTokens: 5000 }),
|
|
],
|
|
});
|
|
assert.equal(a.affectedServers.length, 1);
|
|
assert.equal(a.affectedServers[0].name, 'on');
|
|
assert.equal(a.aggregateTokens, 1000);
|
|
});
|
|
|
|
it('no active servers → not forced upfront even when tool search disabled', () => {
|
|
const a = assessMcpDeferral({
|
|
settings: { env: { ENABLE_TOOL_SEARCH: 'false' } },
|
|
mcpServers: [],
|
|
});
|
|
assert.equal(a.toolSearchDisabled, true);
|
|
assert.equal(a.forcedUpfront, false);
|
|
assert.equal(a.aggregateTokens, 0);
|
|
});
|
|
});
|
|
|
|
describe('severityForForcedSchemas', () => {
|
|
it('high confidence scales with aggregate tokens', () => {
|
|
assert.equal(severityForForcedSchemas(FORCED_SCHEMA_TOKENS_HIGH, 'high'), 'high');
|
|
assert.equal(severityForForcedSchemas(FORCED_SCHEMA_TOKENS_MEDIUM, 'high'), 'medium');
|
|
assert.equal(severityForForcedSchemas(100, 'high'), 'low');
|
|
});
|
|
|
|
it('medium confidence is capped at medium', () => {
|
|
assert.equal(severityForForcedSchemas(FORCED_SCHEMA_TOKENS_HIGH * 10, 'medium'), 'medium');
|
|
assert.equal(severityForForcedSchemas(100, 'medium'), 'low');
|
|
});
|
|
});
|