codeburn/tests/mcp-coverage.test.ts
Resham Joshi 95651189e2
fix(optimize): price deferred MCP tools as name lines, once per call (#1720)
The low-coverage and project-scope findings only see servers whose tools were
deferred, yet charged 400 tokens per tool and counted every call as both a
cache write and a cache read. A deferred tool is one name line (~9 tokens),
and each call now pays it as a read, or as a write only when it read no cache.
2026-10-10 07:05:50 +02:00

1078 lines
42 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
import {
aggregateMcpCoverage,
buildOptimizeJsonReport,
classTotals,
findingClass,
detectMcpProfileAdvisor,
detectMcpToolCoverage,
estimateMcpSchemaCost,
runOptimize,
} from '../src/optimize.js'
import type {
ClassifiedTurn,
ParsedApiCall,
ProjectSummary,
SessionSummary,
TaskCategory,
TokenUsage,
} from '../src/types.js'
// ---------------------------------------------------------------------------
// Test fixtures
// ---------------------------------------------------------------------------
const ZERO_USAGE: TokenUsage = {
inputTokens: 0,
outputTokens: 0,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
}
function makeCall(opts: {
tools?: string[]
cacheCreation?: number
cacheRead?: number
cost?: number
} = {}): ParsedApiCall {
const tools = opts.tools ?? []
return {
provider: 'claude',
model: 'Opus 4.7',
usage: {
...ZERO_USAGE,
cacheCreationInputTokens: opts.cacheCreation ?? 0,
cacheReadInputTokens: opts.cacheRead ?? 0,
},
costUSD: opts.cost ?? 0,
tools,
mcpTools: tools.filter(t => t.startsWith('mcp__')),
skills: [],
hasAgentSpawn: false,
hasPlanMode: false,
speed: 'standard',
timestamp: '2026-05-04T00:00:00Z',
bashCommands: [],
deduplicationKey: 'k',
}
}
function makeTurn(calls: ParsedApiCall[]): ClassifiedTurn {
return {
userMessage: '',
assistantCalls: calls,
timestamp: '2026-05-04T00:00:00Z',
sessionId: 's1',
category: 'coding',
retries: 0,
hasEdits: false,
}
}
function makeSession(opts: {
sessionId?: string
inventory?: string[]
turns?: ClassifiedTurn[]
mcpBreakdown?: Record<string, { calls: number }>
}): SessionSummary {
const turns = opts.turns ?? []
const apiCalls = turns.reduce((s, t) => s + t.assistantCalls.length, 0)
const emptyCategoryBreakdown = {} as Record<TaskCategory, { turns: number; costUSD: number; retries: number; editTurns: number; oneShotTurns: number }>
return {
sessionId: opts.sessionId ?? 's1',
project: 'p',
firstTimestamp: '2026-05-04T00:00:00Z',
lastTimestamp: '2026-05-04T00:00:00Z',
totalCostUSD: 0,
totalInputTokens: 0,
totalOutputTokens: 0,
totalCacheReadTokens: 0,
totalCacheWriteTokens: 0,
apiCalls,
turns,
modelBreakdown: {},
toolBreakdown: {},
mcpBreakdown: opts.mcpBreakdown ?? {},
bashBreakdown: {},
categoryBreakdown: emptyCategoryBreakdown,
skillBreakdown: {},
...(opts.inventory ? { mcpInventory: opts.inventory } : {}),
}
}
function project(sessions: SessionSummary[]): ProjectSummary {
return projectNamed('p', sessions)
}
function projectNamed(name: string, sessions: SessionSummary[]): ProjectSummary {
return {
project: name,
projectPath: `/tmp/${name}`,
sessions,
totalCostUSD: 0,
totalApiCalls: sessions.reduce((s, ses) => s + ses.apiCalls, 0),
}
}
// ---------------------------------------------------------------------------
// aggregateMcpCoverage
// ---------------------------------------------------------------------------
describe('aggregateMcpCoverage', () => {
it('returns empty list when no session has MCP inventory', () => {
const projects = [project([makeSession({})])]
expect(aggregateMcpCoverage(projects)).toEqual([])
})
it('reports per-server tools available, invoked, and unused', () => {
const inventory = [
'mcp__hf__hub_repo_search',
'mcp__hf__paper_search',
'mcp__hf__hf_doc_search',
]
const turns = [
makeTurn([makeCall({ tools: ['mcp__hf__hub_repo_search'] })]),
]
const sessions = [
makeSession({ inventory, turns, mcpBreakdown: { hf: { calls: 1 } } }),
]
const result = aggregateMcpCoverage([project(sessions)])
expect(result).toHaveLength(1)
expect(result[0]!.server).toBe('hf')
expect(result[0]!.toolsAvailable).toBe(3)
expect(result[0]!.toolsInvoked).toBe(1)
expect(result[0]!.unusedTools).toEqual([
'mcp__hf__hf_doc_search',
'mcp__hf__paper_search',
])
expect(result[0]!.coverageRatio).toBeCloseTo(1 / 3, 5)
expect(result[0]!.invocations).toBe(1)
expect(result[0]!.loadedSessions).toBe(1)
})
it('unions inventory across multiple sessions for the same server', () => {
const sessions = [
makeSession({ sessionId: 'a', inventory: ['mcp__x__a', 'mcp__x__b'] }),
makeSession({ sessionId: 'b', inventory: ['mcp__x__b', 'mcp__x__c'] }),
]
const result = aggregateMcpCoverage([project(sessions)])
expect(result[0]!.toolsAvailable).toBe(3)
expect(result[0]!.loadedSessions).toBe(2)
})
it('separates servers with similar names', () => {
const sessions = [
makeSession({ inventory: ['mcp__hf__a', 'mcp__hugface__a'] }),
]
const result = aggregateMcpCoverage([project(sessions)])
expect(result.map(r => r.server).sort()).toEqual(['hf', 'hugface'])
})
it('skips invocations without inventory (foreign server, no inventory observed)', () => {
// A server can show up only via a call. We still report it so the
// operator knows it was invoked, but coverage is 0/0 and it is not a
// candidate for the unused-coverage finding.
const turns = [makeTurn([makeCall({ tools: ['mcp__ghost__t1'] })])]
const sessions = [
makeSession({ turns, mcpBreakdown: { ghost: { calls: 1 } } }),
]
const result = aggregateMcpCoverage([project(sessions)])
// No inventory entry -> aggregator drops the server from the report
// because we cannot reason about coverage without an inventory baseline.
expect(result).toEqual([])
})
})
// ---------------------------------------------------------------------------
// estimateMcpSchemaCost — cache-aware accounting
// ---------------------------------------------------------------------------
describe('estimateMcpSchemaCost', () => {
it('charges first cacheCreation turn at full price, subsequent turns at cache-read', () => {
const turns = [
makeTurn([makeCall({ cacheCreation: 50_000 })]), // first turn: write
makeTurn([makeCall({ cacheRead: 60_000 })]), // ongoing: read
makeTurn([makeCall({ cacheRead: 60_000 })]),
]
const sessions = [makeSession({
inventory: Array.from({ length: 30 }, (_, i) => `mcp__svc__t${i}`),
turns,
mcpBreakdown: { svc: { calls: 0 } },
})]
// 30 deferred tools * 9 token name lines = 270 tokens
const cost = estimateMcpSchemaCost(30, [project(sessions)], 'svc')
expect(cost.cacheWriteTokens).toBe(270)
expect(cost.cacheReadTokens).toBe(540) // 270 + 270 across two ongoing turns
// effective = write * 1.25 + read * 0.10 (cache pricing)
expect(cost.effectiveInputTokens).toBeCloseTo(270 * 1.25 + 540 * 0.10, 5)
})
it('caps by available cache bucket so we never overclaim', () => {
const turns = [makeTurn([makeCall({ cacheCreation: 100 })])]
const sessions = [makeSession({
inventory: Array.from({ length: 30 }, (_, i) => `mcp__svc__t${i}`),
turns,
mcpBreakdown: { svc: { calls: 0 } },
})]
// 30*9 = 270 tokens, but the call only had 100 cache-creation,
// so we should not claim more than 100 of overhead for that turn.
const cost = estimateMcpSchemaCost(30, [project(sessions)], 'svc')
expect(cost.cacheWriteTokens).toBe(100)
})
it('returns zero when no unused tools', () => {
const sessions = [makeSession({
inventory: ['mcp__svc__t1'],
turns: [makeTurn([makeCall({ cacheCreation: 5000 })])],
})]
const cost = estimateMcpSchemaCost(0, [project(sessions)], 'svc')
expect(cost).toEqual({ cacheWriteTokens: 0, cacheReadTokens: 0, effectiveInputTokens: 0 })
})
it('charges a call that read the cached prefix once, as a read', () => {
// Agent loops write new tool output on nearly every call, so a cache
// write next to a cache read is not a prefix rebuild: the tool lines
// were read, not written again.
const turns = [makeTurn([
makeCall({ cacheCreation: 50_000, cacheRead: 30_000 }),
])]
const sessions = [makeSession({
inventory: Array.from({ length: 30 }, (_, i) => `mcp__svc__t${i}`),
turns,
mcpBreakdown: { svc: { calls: 0 } },
})]
const cost = estimateMcpSchemaCost(30, [project(sessions)], 'svc')
expect(cost.cacheWriteTokens).toBe(0)
expect(cost.cacheReadTokens).toBe(270)
})
it('counts every cache rebuild, not just the first one', () => {
// Sessions that span more than 5 minutes can rebuild the cache
// multiple times. The estimator should treat every cacheCreation
// bucket as another write.
const turns = [makeTurn([
makeCall({ cacheCreation: 50_000 }),
makeCall({ cacheCreation: 50_000 }), // rebuild after cache TTL
makeCall({ cacheRead: 60_000 }),
])]
const sessions = [makeSession({
inventory: Array.from({ length: 30 }, (_, i) => `mcp__svc__t${i}`),
turns,
mcpBreakdown: { svc: { calls: 0 } },
})]
const cost = estimateMcpSchemaCost(30, [project(sessions)], 'svc')
expect(cost.cacheWriteTokens).toBe(540) // both rebuilds counted
expect(cost.cacheReadTokens).toBe(270)
})
it('skips sessions where the server was never loaded', () => {
const turns = [makeTurn([makeCall({ cacheCreation: 100_000 })])]
const sessions = [makeSession({
inventory: ['mcp__other__t1'],
turns,
})]
const cost = estimateMcpSchemaCost(10, [project(sessions)], 'svc')
expect(cost.cacheWriteTokens).toBe(0)
})
it('requires observed inventory for the server, not just invocations', () => {
// Session invoked the server (mcpBreakdown set, mcpTools called) but
// never reported a deferred_tools_delta for it. Cost should be 0 to
// stay consistent with aggregateMcpCoverage's loadedSessions rule.
const turns = [makeTurn([
makeCall({ tools: ['mcp__svc__t1'], cacheCreation: 100_000 }),
])]
const sessions = [makeSession({
// No inventory at all
turns,
mcpBreakdown: { svc: { calls: 1 } },
})]
const cost = estimateMcpSchemaCost(10, [project(sessions)], 'svc')
expect(cost.cacheWriteTokens).toBe(0)
expect(cost.cacheReadTokens).toBe(0)
})
it('caps combined unused-schema budget across multiple flagged servers', () => {
// Two flagged servers, each with 30 unused tools (270 tokens each =
// 540 combined). One call has a 300 cache-creation bucket. The
// combined cap means total write tokens reported is min(540, 300) =
// 300, not min(270, 300) per server = 540.
const inventory = [
...Array.from({ length: 30 }, (_, i) => `mcp__a__t${i}`),
...Array.from({ length: 30 }, (_, i) => `mcp__b__t${i}`),
]
const turns = [makeTurn([makeCall({ cacheCreation: 300 })])]
const sessions = [makeSession({ inventory, turns })]
const cost = estimateMcpSchemaCost(
{ a: 30, b: 30 },
[project(sessions)],
['a', 'b'],
)
expect(cost.cacheWriteTokens).toBe(300)
})
it('does not count a duplicated server identifier twice', () => {
const inventory = Array.from({ length: 20 }, (_, i) => `mcp__svc__t${i}`)
const sessions = [makeSession({
inventory,
turns: [makeTurn([makeCall({ cacheCreation: 50_000 })])],
})]
const cost = estimateMcpSchemaCost(
{ svc: 20 },
[project(sessions)],
['svc', 'svc'],
)
expect(cost.cacheWriteTokens).toBe(180)
expect(cost.effectiveInputTokens).toBe(225)
})
it('still works with the single-server signature (backward compat)', () => {
const turns = [makeTurn([makeCall({ cacheCreation: 50_000 })])]
const sessions = [makeSession({
inventory: Array.from({ length: 30 }, (_, i) => `mcp__svc__t${i}`),
turns,
})]
const cost = estimateMcpSchemaCost(30, [project(sessions)], 'svc')
expect(cost.cacheWriteTokens).toBe(270)
})
})
// ---------------------------------------------------------------------------
// detectMcpToolCoverage — finding emission with thresholds
// ---------------------------------------------------------------------------
describe('detectMcpToolCoverage', () => {
it('returns null when no inventory exists at all', () => {
expect(detectMcpToolCoverage([project([makeSession({})])])).toBeNull()
})
it('keeps claude.ai connector evidence but emits manual guidance instead of a local remove command', () => {
const server = 'claude_ai_Netlify'
const inventory = Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`)
const turns = [makeTurn([makeCall({ cacheCreation: 50_000 })])]
const sessions = [
makeSession({ sessionId: 'a', inventory, turns }),
makeSession({ sessionId: 'b', inventory, turns }),
]
const finding = detectMcpToolCoverage([project(sessions)])
expect(finding).not.toBeNull()
expect(finding!.tokensSaved).toBe(450)
// Keep the transcript namespace as evidence, but name the connector the
// way users actually see it in /mcp and claude.ai Settings.
expect(finding!.explanation).toContain(server)
expect(finding!.explanation).toContain('claude.ai Netlify')
expect(finding!.explanation).toContain('/mcp')
expect(finding!.explanation).toContain('claude.ai Settings > Connectors')
expect(finding!.fix.type).toBe('paste')
if (finding!.fix.type === 'paste') {
expect(finding!.fix.destination).toBe('manual')
expect(finding!.fix.text).toContain('/mcp')
expect(finding!.fix.text).toContain('claude.ai Netlify')
expect(finding!.fix.text).not.toContain(server)
expect(finding!.fix.text).toContain('claude.ai Settings > Connectors')
}
expect(JSON.stringify(finding)).not.toContain('claude mcp remove')
expect(finding!.apply).toBeUndefined()
})
it('renders connector-only remediation as a manual action, never an Ask Claude prompt', async () => {
const server = 'claude_ai_Google_Calendar'
const inventory = Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`)
const turns = [makeTurn([makeCall({ cacheCreation: 50_000 })])]
const projects = [project([
makeSession({ sessionId: 'a', inventory, turns }),
makeSession({ sessionId: 'b', inventory, turns }),
])]
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined)
try {
await runOptimize(projects, 'Test period')
const output = log.mock.calls.map(args => args.join(' ')).join('\n')
expect(output).toContain('Manual action')
expect(output).toContain('claude.ai Google Calendar')
expect(output).not.toContain('Ask Claude in the current session')
} finally {
log.mockRestore()
}
})
it('keeps the public optimize JSON envelope while marking connector guidance manual', () => {
const server = 'claude_ai_Slack'
const coverage = [{
server,
toolsAvailable: 20,
toolsInvoked: 0,
unusedTools: Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`),
invocations: 0,
loadedSessions: 2,
coverageRatio: 0,
}]
const finding = detectMcpToolCoverage([], coverage)!
const report = buildOptimizeJsonReport([], 'Test period', {
findings: [finding],
costRate: 0,
healthScore: 90,
healthGrade: 'A',
})
expect(report.findings[0]).toMatchObject({
id: 'mcp-low-coverage',
tokensSaved: 0,
fix: {
type: 'paste',
destination: 'manual',
text: expect.stringContaining('claude.ai Slack'),
},
})
expect(report.findings[0]).not.toHaveProperty('apply')
expect(report.findings[0]).not.toHaveProperty('applyTokensSaved')
expect(report.findings[0]).not.toHaveProperty('applyTokensSavedByServer')
expect(report.findings[0]).not.toHaveProperty('manualFollowUp')
})
it('intersects globally unused tool identities with each session inventory', () => {
const server = 'filesystem'
const coverage = [{
server,
toolsAvailable: 20,
toolsInvoked: 0,
unusedTools: Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`),
invocations: 0,
loadedSessions: 2,
coverageRatio: 0,
}]
const sessions = [5, 20].map((count, index) => makeSession({
sessionId: `s${index}`,
inventory: Array.from({ length: count }, (_, i) => `mcp__${server}__t${i}`),
turns: [makeTurn([makeCall({ cacheCreation: 50_000 })])],
}))
const finding = detectMcpToolCoverage([project(sessions)], coverage)
// 5*9 and 20*9 name-line tokens, each at 1.25x cache-write pricing.
expect(finding).toMatchObject({ tokensSaved: 281 })
expect(finding!.applyTokensSavedByServer?.filesystem).toBe(281.25)
})
it('conserves a capped cache-read bucket with fractional shares', () => {
const inventory = [
...Array.from({ length: 15 }, (_, i) => `mcp__filesystem__t${i}`),
...Array.from({ length: 11 }, (_, i) => `mcp__claude_ai_Slack__t${i}`),
]
const coverage: McpServerCoverage[] = [
{
server: 'filesystem', toolsAvailable: 15, toolsInvoked: 0,
unusedTools: inventory.slice(0, 15), invocations: 0, loadedSessions: 2, coverageRatio: 0,
},
{
server: 'claude_ai_Slack', toolsAvailable: 11, toolsInvoked: 0,
unusedTools: inventory.slice(15), invocations: 0, loadedSessions: 2, coverageRatio: 0,
},
]
// Duplicate inventory entries must not increase the schema share.
const sessionInventory = [...inventory, inventory[0]!, inventory[15]!]
const sessions = ['a', 'b'].map(sessionId => makeSession({
sessionId,
inventory: sessionInventory,
turns: [makeTurn([makeCall({ cacheCreation: 5_001, cacheRead: 101 })])],
}))
const finding = detectMcpToolCoverage([project(sessions)], coverage)!
// 26 tools * 9 = 234 tokens, capped at the 101-token read; the call read
// the prefix, so its cache write is new content, not the tool lines.
const total = 2 * 101 * 0.10
const local = total * (15 / 26)
expect(finding.tokensSaved).toBe(Math.round(total))
expect(finding.applyTokensSaved).toBe(Math.round(local))
expect(finding.applyTokensSavedByServer?.filesystem).toBeCloseTo(local, 8)
})
it('pluralises manual guidance when only claude.ai connectors are flagged', () => {
const coverage = ['claude_ai_Slack', 'claude_ai_Google_Calendar'].map(server => ({
server,
toolsAvailable: 20,
toolsInvoked: 0,
unusedTools: Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`),
invocations: 0,
loadedSessions: 2,
coverageRatio: 0,
}))
const finding = detectMcpToolCoverage([], coverage)
expect(finding).not.toBeNull()
expect(finding!.fix).toMatchObject({
type: 'paste',
destination: 'manual',
label: 'Manage the underused claude.ai connectors where they load:',
})
if (finding!.fix.type === 'paste') {
expect(finding!.fix.text).toContain('manage them in claude.ai Settings > Connectors')
}
expect(finding!.apply).toBeUndefined()
})
it('does not flag a server with healthy coverage', () => {
const inventory = Array.from({ length: 20 }, (_, i) => `mcp__svc__t${i}`)
const turns = [makeTurn(
Array.from({ length: 8 }, (_, i) => makeCall({ tools: [`mcp__svc__t${i}`] })),
)]
const sessions = [
makeSession({ sessionId: 'a', inventory, turns }),
makeSession({ sessionId: 'b', inventory, turns }),
]
// 8/20 = 40% coverage, above the 20% threshold -> no finding
expect(detectMcpToolCoverage([project(sessions)])).toBeNull()
})
it('does not flag a server with too few tools (signal too noisy)', () => {
// Below MCP_COVERAGE_MIN_TOOLS=10
const inventory = ['mcp__svc__a', 'mcp__svc__b']
const sessions = [
makeSession({ sessionId: 'a', inventory }),
makeSession({ sessionId: 'b', inventory }),
]
expect(detectMcpToolCoverage([project(sessions)])).toBeNull()
})
it('does not flag if seen in only one session (insufficient evidence)', () => {
const inventory = Array.from({ length: 20 }, (_, i) => `mcp__svc__t${i}`)
const sessions = [makeSession({ inventory })]
expect(detectMcpToolCoverage([project(sessions)])).toBeNull()
})
it('flags a large server with low coverage across multiple sessions', () => {
const inventory = Array.from({ length: 30 }, (_, i) => `mcp__hf__t${i}`)
const turns = [makeTurn([
makeCall({ tools: ['mcp__hf__t0'], cacheCreation: 100_000 }),
])]
const sessions = [
makeSession({ sessionId: 'a', inventory, turns, mcpBreakdown: { hf: { calls: 1 } } }),
makeSession({ sessionId: 'b', inventory, turns, mcpBreakdown: { hf: { calls: 1 } } }),
]
const finding = detectMcpToolCoverage([project(sessions)])
expect(finding).not.toBeNull()
expect(finding!.title).toContain('1 MCP server')
expect(finding!.title).toContain('low tool coverage')
expect(finding!.explanation).toContain('hf')
expect(finding!.explanation).toContain('1/30')
expect(finding!.fix.type).toBe('command')
expect((finding!.fix as { text: string }).text).toContain("claude mcp remove 'hf'")
expect(finding!.apply).toEqual({ kind: 'mcp-remove', servers: ['hf'] })
expect(finding!.tokensSaved).toBeGreaterThan(0)
})
it('keeps mixed connector guidance visible while making only the local server executable', () => {
const inventory = ['filesystem', 'claude_ai_Slack'].flatMap(server =>
Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`),
)
const sessions: SessionSummary[] = [
makeSession({ sessionId: 'mixed-a', inventory, turns: [makeTurn([makeCall({ cacheCreation: 50_000 })])] }),
makeSession({ sessionId: 'mixed-b', inventory, turns: [makeTurn([makeCall({ cacheCreation: 50_000 })])] }),
]
const finding = detectMcpToolCoverage([project(sessions)])
expect(finding).not.toBeNull()
// The finding describes both opportunities: 40 unused tool lines across
// two sessions = 900 effective tokens. The automatic mutation owns only
// the 20 local lines = 450; the connector portion remains manual.
expect(finding).toMatchObject({ tokensSaved: 900, applyTokensSaved: 450 })
expect(finding!.explanation).toContain('claude_ai_Slack')
expect(finding!.explanation).toContain('/mcp')
expect(finding!.explanation).toContain('claude.ai Settings > Connectors')
expect(finding!.fix).toEqual({
type: 'command',
label: 'Remove the underused local server, or trim its tools in your MCP config:',
text: "claude mcp remove 'filesystem'",
})
expect(finding!.apply).toEqual({ kind: 'mcp-remove', servers: ['filesystem'] })
})
it('attributes a capped mixed cache bucket proportionally to the local action', () => {
const inventory = ['filesystem', 'claude_ai_Slack'].flatMap(server =>
Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`),
)
const sessions = ['a', 'b'].map(sessionId => makeSession({
sessionId,
inventory,
turns: [makeTurn([makeCall({ cacheCreation: 200 })])],
}))
const finding = detectMcpToolCoverage([project(sessions)])
// Each call's 200-token cache bucket is shared evenly by two 180-token
// tool lists. Total: 2 * 200 * 1.25 = 500. The local mutation owns half.
expect(finding).toMatchObject({ tokensSaved: 500, applyTokensSaved: 250 })
})
it('charges only the flagged servers actually loaded in each session', () => {
const sessions = ['filesystem', 'claude_ai_Slack'].flatMap(server =>
['a', 'b'].map(suffix => makeSession({
sessionId: `${server}-${suffix}`,
inventory: Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`),
turns: [makeTurn([makeCall({ cacheCreation: 50_000 })])],
})),
)
const finding = detectMcpToolCoverage([project(sessions)])
// Four sessions each load one 180-token tool list. The combined finding
// must not charge both to every session merely because both are flagged.
expect(finding).toMatchObject({ tokensSaved: 900, applyTokensSaved: 450 })
})
it('disambiguates a claude.ai connector from a similarly named local server', () => {
const sessions: SessionSummary[] = []
for (const server of ['claude_ai_Netlify', 'netlify']) {
const inventory = Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`)
sessions.push(
makeSession({ sessionId: `${server}-a`, inventory }),
makeSession({ sessionId: `${server}-b`, inventory }),
)
}
const finding = detectMcpToolCoverage([project(sessions)])
expect(finding).not.toBeNull()
expect(finding!.explanation).toContain('claude_ai_Netlify')
expect(finding!.explanation).toContain('separate from any similarly named local MCP server')
expect(finding!.fix.type).toBe('command')
if (finding!.fix.type === 'command') {
expect(finding!.fix.text).toBe("claude mcp remove 'netlify'")
expect(finding!.fix.text).not.toContain('claude_ai_Netlify')
}
expect(finding!.apply).toEqual({ kind: 'mcp-remove', servers: ['netlify'] })
})
it('escalates impact to high when token waste crosses the threshold', () => {
const inventory = Array.from({ length: 200 }, (_, i) => `mcp__big__t${i}`)
// 200 tools * 9 tokens = 1,800 per call: one write (2,250 effective) and
// 100 reads (18,000 effective) is ~20k per session, so ten sessions clear
// the 200k high-impact threshold.
const turns = [makeTurn([
makeCall({ tools: ['mcp__big__t0'], cacheCreation: 50_000 }),
...Array.from({ length: 100 }, () => makeCall({ cacheRead: 60_000 })),
])]
const sessions = Array.from({ length: 10 }, (_, i) =>
makeSession({ sessionId: `s${i}`, inventory, turns, mcpBreakdown: { big: { calls: 1 } } }),
)
const finding = detectMcpToolCoverage([project(sessions)])
expect(finding).not.toBeNull()
expect(finding!.impact).toBe('high')
})
it('does not count invocation-only sessions toward loadedSessions', () => {
// Server `svc` has inventory in only one session, but is invoked in
// a second session that never observed the schema. Pre-fix this
// would have satisfied the >=2 session threshold; it must not now.
const inventory = Array.from({ length: 20 }, (_, i) => `mcp__svc__t${i}`)
const turns = [makeTurn([
makeCall({ tools: ['mcp__svc__t0'], cacheCreation: 50_000 }),
])]
const sessions = [
makeSession({ sessionId: 'a', inventory, turns, mcpBreakdown: { svc: { calls: 1 } } }),
// No inventory — this shouldn't be considered a "loaded" session.
makeSession({ sessionId: 'b', turns, mcpBreakdown: { svc: { calls: 1 } } }),
]
expect(detectMcpToolCoverage([project(sessions)])).toBeNull()
})
it('does not let invocations of un-inventoried tools inflate coverage', () => {
// Inventory has 20 tools, none invoked. Calls hit a 21st tool that
// never appeared in any deferred_tools_delta (could be a renamed/
// removed tool from an older session config). Coverage must stay 0%
// and unusedCount must not go negative.
const inventory = Array.from({ length: 20 }, (_, i) => `mcp__svc__t${i}`)
const turns = [makeTurn([makeCall({ tools: ['mcp__svc__ghost'] })])]
const sessions = [
makeSession({ sessionId: 'a', inventory, turns, mcpBreakdown: { svc: { calls: 1 } } }),
makeSession({ sessionId: 'b', inventory, turns, mcpBreakdown: { svc: { calls: 1 } } }),
]
const result = aggregateMcpCoverage([project(sessions)])
expect(result[0]!.toolsAvailable).toBe(20)
expect(result[0]!.toolsInvoked).toBe(0)
expect(result[0]!.coverageRatio).toBe(0)
expect(result[0]!.unusedTools).toHaveLength(20)
})
it('handles multiple flagged servers and pluralises the title', () => {
const sessions: SessionSummary[] = []
for (const server of ['svc1', 'svc2']) {
const inventory = Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`)
const turns = [makeTurn([
makeCall({ tools: [`mcp__${server}__t0`], cacheCreation: 50_000 }),
])]
sessions.push(
makeSession({ sessionId: `${server}-a`, inventory, turns, mcpBreakdown: { [server]: { calls: 1 } } }),
makeSession({ sessionId: `${server}-b`, inventory, turns, mcpBreakdown: { [server]: { calls: 1 } } }),
)
}
const finding = detectMcpToolCoverage([project(sessions)])
expect(finding).not.toBeNull()
expect(finding!.title).toContain('2 MCP servers')
expect((finding!.fix as { text: string }).text.split('\n')).toHaveLength(2)
})
})
// ---------------------------------------------------------------------------
// detectMcpProfileAdvisor — project-scoping recommendations
// ---------------------------------------------------------------------------
describe('detectMcpProfileAdvisor', () => {
const smallInventory = Array.from({ length: 4 }, (_, i) => `mcp__github__t${i}`)
it('flags a server loaded across projects but invoked only in one hot project', () => {
const hotTurns = [makeTurn([
makeCall({ tools: ['mcp__github__t0'], cacheCreation: 10_000 }),
makeCall({ tools: ['mcp__github__t1'], cacheCreation: 10_000 }),
])]
const coldTurns = [makeTurn([makeCall({ cacheCreation: 10_000 })])]
const projects = [
projectNamed('api', [
makeSession({ inventory: smallInventory, turns: hotTurns, mcpBreakdown: { github: { calls: 2 } } }),
]),
projectNamed('web', [
makeSession({ inventory: smallInventory, turns: coldTurns, mcpBreakdown: { github: { calls: 0 } } }),
]),
projectNamed('docs', [
makeSession({ inventory: smallInventory, turns: coldTurns, mcpBreakdown: { github: { calls: 0 } } }),
]),
]
const finding = detectMcpProfileAdvisor(projects)
expect(finding).not.toBeNull()
expect(finding!.title).toContain('project-scoped')
expect(finding!.explanation).toContain('github')
expect(finding!.explanation).toContain('/tmp/api')
expect(finding!.explanation).toContain('/tmp/web')
expect(finding!.explanation).toContain('/tmp/docs')
expect(finding!.tokensSaved).toBe(90)
expect(finding!.fix.type).toBe('paste')
if (finding!.fix.type === 'paste') {
expect(finding!.fix.destination).toBe('prompt')
expect(finding!.fix.text).toContain('Keep github available for /tmp/api')
expect(finding!.fix.text).toContain('/tmp/web')
expect(finding!.fix.text).toContain('/tmp/docs')
}
})
it('scopes the remediation label to --provider codex', () => {
const hotTurns = [makeTurn([
makeCall({ tools: ['mcp__github__t0'], cacheCreation: 10_000 }),
makeCall({ tools: ['mcp__github__t1'], cacheCreation: 10_000 }),
])]
const coldTurns = [makeTurn([makeCall({ cacheCreation: 10_000 })])]
const projects = [
projectNamed('api', [
makeSession({ inventory: smallInventory, turns: hotTurns, mcpBreakdown: { github: { calls: 2 } } }),
]),
projectNamed('web', [
makeSession({ inventory: smallInventory, turns: coldTurns, mcpBreakdown: { github: { calls: 0 } } }),
]),
projectNamed('docs', [
makeSession({ inventory: smallInventory, turns: coldTurns, mcpBreakdown: { github: { calls: 0 } } }),
]),
]
const finding = detectMcpProfileAdvisor(projects, undefined, 'codex')
expect(finding!.fix.label).toBe('Ask Codex to turn this into a project-scoped MCP profile:')
expect(finding!.fix.label).not.toContain('Claude')
expect(finding!.fix.label).not.toContain('CLAUDE.md')
})
it('does not flag servers used evenly across loaded projects', () => {
const projects = ['api', 'web', 'docs'].map(name => projectNamed(name, [
makeSession({
inventory: smallInventory,
turns: [makeTurn([makeCall({ tools: ['mcp__github__t0'], cacheCreation: 10_000 })])],
mcpBreakdown: { github: { calls: 2 } },
}),
]))
expect(detectMcpProfileAdvisor(projects)).toBeNull()
})
it('allows a hot profile shared by two projects', () => {
const projects = [
projectNamed('api', [
makeSession({
inventory: smallInventory,
turns: [makeTurn([
makeCall({ tools: ['mcp__github__t0'], cacheCreation: 10_000 }),
makeCall({ tools: ['mcp__github__t1'], cacheCreation: 10_000 }),
])],
mcpBreakdown: { github: { calls: 2 } },
}),
]),
projectNamed('web', [
makeSession({
inventory: smallInventory,
turns: [makeTurn([
makeCall({ tools: ['mcp__github__t0'], cacheCreation: 10_000 }),
makeCall({ tools: ['mcp__github__t1'], cacheCreation: 10_000 }),
])],
mcpBreakdown: { github: { calls: 2 } },
}),
]),
projectNamed('docs', [
makeSession({
inventory: smallInventory,
turns: [makeTurn([makeCall({ cacheCreation: 10_000 })])],
mcpBreakdown: { github: { calls: 0 } },
}),
]),
projectNamed('playground', [
makeSession({
inventory: smallInventory,
turns: [makeTurn([makeCall({ cacheCreation: 10_000 })])],
mcpBreakdown: { github: { calls: 0 } },
}),
]),
]
const finding = detectMcpProfileAdvisor(projects)
expect(finding).not.toBeNull()
expect(finding!.explanation).toContain('/tmp/api')
expect(finding!.explanation).toContain('/tmp/web')
expect(finding!.explanation).toContain('/tmp/docs')
expect(finding!.explanation).toContain('/tmp/playground')
})
it('caps profile savings once when multiple candidate servers share cold sessions', () => {
const githubInventory = Array.from({ length: 4 }, (_, i) => `mcp__github__t${i}`)
const slackInventory = Array.from({ length: 4 }, (_, i) => `mcp__slack__t${i}`)
const inventory = [...githubInventory, ...slackInventory]
const projects = [
projectNamed('api', [
makeSession({
inventory,
turns: [makeTurn([
makeCall({ tools: ['mcp__github__t0'] }),
makeCall({ tools: ['mcp__github__t1'] }),
makeCall({ tools: ['mcp__slack__t0'] }),
makeCall({ tools: ['mcp__slack__t1'] }),
])],
mcpBreakdown: { github: { calls: 2 }, slack: { calls: 2 } },
}),
]),
projectNamed('web', [
makeSession({
inventory,
turns: [makeTurn([makeCall({ cacheCreation: 50 })])],
mcpBreakdown: { github: { calls: 0 }, slack: { calls: 0 } },
}),
]),
projectNamed('docs', [
makeSession({
inventory,
turns: [makeTurn([makeCall({ cacheCreation: 50 })])],
mcpBreakdown: { github: { calls: 0 }, slack: { calls: 0 } },
}),
]),
]
const finding = detectMcpProfileAdvisor(projects)
expect(finding).not.toBeNull()
expect(finding!.title).toContain('2 MCP servers')
expect(finding!.explanation).toContain('github')
expect(finding!.explanation).toContain('slack')
// 2 * 36 tool-line tokens capped once at the 50-token bucket, per cold
// session: 2 * 50 * 1.25.
expect(finding!.tokensSaved).toBe(125)
})
it('requires at least three loaded projects before recommending a profile', () => {
const projects = [
projectNamed('api', [
makeSession({
inventory: smallInventory,
turns: [makeTurn([makeCall({ tools: ['mcp__github__t0'], cacheCreation: 10_000 })])],
mcpBreakdown: { github: { calls: 2 } },
}),
]),
projectNamed('web', [
makeSession({
inventory: smallInventory,
turns: [makeTurn([makeCall({ cacheCreation: 10_000 })])],
mcpBreakdown: { github: { calls: 0 } },
}),
]),
]
expect(detectMcpProfileAdvisor(projects)).toBeNull()
})
it('does not duplicate low tool coverage findings for the same server', () => {
const inventory = Array.from({ length: 12 }, (_, i) => `mcp__huge__t${i}`)
const projects = [
projectNamed('api', [
makeSession({
inventory,
turns: [makeTurn([makeCall({ tools: ['mcp__huge__t0'], cacheCreation: 20_000 })])],
mcpBreakdown: { huge: { calls: 3 } },
}),
]),
projectNamed('web', [
makeSession({
inventory,
turns: [makeTurn([makeCall({ cacheCreation: 20_000 })])],
mcpBreakdown: { huge: { calls: 0 } },
}),
]),
projectNamed('docs', [
makeSession({
inventory,
turns: [makeTurn([makeCall({ cacheCreation: 20_000 })])],
mcpBreakdown: { huge: { calls: 0 } },
}),
]),
]
const coverage = [{
server: 'huge',
toolsAvailable: 12,
toolsInvoked: 1,
unusedTools: Array.from({ length: 11 }, (_, i) => `mcp__huge__t${i + 1}`),
invocations: 3,
loadedSessions: 3,
coverageRatio: 1 / 12,
}]
expect(detectMcpProfileAdvisor(projects, coverage)).toBeNull()
})
})
// ---------------------------------------------------------------------------
// Connector findings under the fix/nudge/keep classification (#1019)
// ---------------------------------------------------------------------------
describe('connector findings and finding class', () => {
const inventoryFor = (servers: string[]) => servers.flatMap(server =>
Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`),
)
const twoSessions = (servers: string[]) => ['a', 'b'].map(sessionId => makeSession({
sessionId,
inventory: inventoryFor(servers),
turns: [makeTurn([makeCall({ cacheCreation: 50_000 })])],
}))
it('classifies a connector-only finding as a nudge, since nothing is appliable', () => {
const finding = detectMcpToolCoverage([project(twoSessions(['claude_ai_Gmail']))])
expect(finding).not.toBeNull()
expect(finding!.apply).toBeUndefined()
expect(findingClass(finding!)).toBe('nudge')
expect(finding!.tokensSaved).toBeGreaterThan(0)
// Never lands in the "apply-able" subtotal.
expect(classTotals([finding!], 0.00002).fix).toEqual({ tokensSaved: 0, savingsUSD: 0, count: 0 })
})
it('counts only the local subset of a mixed finding towards the apply-able subtotal', () => {
const finding = detectMcpToolCoverage([project(twoSessions(['filesystem', 'claude_ai_Slack']))])
expect(finding).not.toBeNull()
expect(findingClass(finding!)).toBe('fix')
expect(finding).toMatchObject({ tokensSaved: 900, applyTokensSaved: 450 })
expect(classTotals([finding!], 0.00002).fix).toEqual({ tokensSaved: 450, savingsUSD: expect.closeTo(0.009, 10), count: 1 })
})
it("leaves a local-only finding's subtotal at its full estimate", () => {
const finding = detectMcpToolCoverage([project(twoSessions(['filesystem']))])
expect(finding).not.toBeNull()
expect(finding!.applyTokensSaved).toBeUndefined()
expect(classTotals([finding!], 0.00002).fix.tokensSaved).toBe(finding!.tokensSaved)
})
it('charges each session only for the local schemas it actually loaded', () => {
// Same per-session scoping the connector split relies on, with no
// connector in play: two flagged local servers in disjoint sessions are
// charged one schema each, not both schemas everywhere.
const sessions = ['filesystem', 'playwright'].flatMap(server =>
['a', 'b'].map(suffix => makeSession({
sessionId: `${server}-${suffix}`,
inventory: inventoryFor([server]),
turns: [makeTurn([makeCall({ cacheCreation: 50_000 })])],
})),
)
const finding = detectMcpToolCoverage([project(sessions)])
expect(finding).toMatchObject({ tokensSaved: 900 })
expect(finding!.applyTokensSaved).toBeUndefined()
expect(classTotals([finding!], 0.00002).fix.tokensSaved).toBe(900)
})
it('treats a claude_ai_* name owned by local config as a local server', () => {
const finding = detectMcpToolCoverage(
[project(twoSessions(['claude_ai_homegrown']))],
undefined,
new Set(['claude_ai_homegrown']),
)
expect(finding!.fix).toEqual({
type: 'command',
label: 'Remove the underused local server, or trim its tools in your MCP config:',
text: "claude mcp remove 'claude_ai_homegrown'",
})
expect(finding!.apply).toEqual({ kind: 'mcp-remove', servers: ['claude_ai_homegrown'] })
expect(findingClass(finding!)).toBe('fix')
// Local config owns the name, so the finding must not claim it is a connector.
expect(finding!.explanation).not.toContain('is a claude.ai connector namespace')
// ...but the transcript cannot rule out a same-name connector.
expect(finding!.explanation).toContain('If you also use a claude.ai connector named claude_ai_homegrown')
expect(finding!.manualFollowUp?.label).toBe('Check for a same-name claude.ai connector:')
// The whole estimate is appliable: nothing is reserved for a connector.
expect(finding!.applyTokensSaved).toBeUndefined()
expect(classTotals([finding!], 0.00002).fix.tokensSaved).toBe(finding!.tokensSaved)
})
it('keeps a claude_ai_* name absent from local config a connector', () => {
const finding = detectMcpToolCoverage(
[project(twoSessions(['claude_ai_Gmail']))],
undefined,
new Set(['filesystem', 'playwright']),
)
expect(finding!.fix.type).toBe('paste')
expect(finding!.apply).toBeUndefined()
expect(findingClass(finding!)).toBe('nudge')
expect(finding!.explanation).toContain('is a claude.ai connector namespace')
})
it('falls back to prefix-only when no local config could be read', () => {
// Unreadable or absent config contributes no names, which leaves every
// claude_ai_* namespace on the conservative connector path.
const finding = detectMcpToolCoverage([project(twoSessions(['claude_ai_homegrown']))])
expect(finding!.fix.type).toBe('paste')
expect(finding!.apply).toBeUndefined()
expect(findingClass(finding!)).toBe('nudge')
})
it('applies the local entry and notes the connector when a name collides', () => {
const finding = detectMcpToolCoverage(
[project(twoSessions(['filesystem', 'claude_ai_Slack']))],
undefined,
new Set(['filesystem', 'claude_ai_Slack']),
)
// Both are local: the removal owns both entries and nothing is deferred.
expect(finding!.apply).toEqual({ kind: 'mcp-remove', servers: ['filesystem', 'claude_ai_Slack'] })
expect(finding!.applyTokensSaved).toBeUndefined()
expect(classTotals([finding!], 0.00002).fix.tokensSaved).toBe(900)
expect(finding!.explanation).not.toContain('is a claude.ai connector namespace')
expect(finding!.manualFollowUp?.text)
.toBe('If you also use a claude.ai connector named claude_ai_Slack, manage it with /mcp or in claude.ai Settings > Connectors.')
})
})