12 KiB
CodeBurn MCP Server — Design Spec
- Date: 2026-06-02
- Status: Approved design (pre-implementation)
- Author: brainstormed + validator/devil's-advocate hardened
1. Context & Goal
CodeBurn already aggregates rich AI-coding usage/cost data (by task, model, project, provider; retry tax; routing waste; optimize findings; 365-day history). This spec adds an MCP server that exposes that data to AI agents (Claude Code, Cursor, Claude Desktop) so an agent can answer "where did my tokens go?" and "how do I spend less?" mid-conversation.
Why (and why not): This is a product / differentiation play, not a downloads play. We measured that MCP is a niche download lever (@ccusage/mcp ≈ 1.1% of ccusage's downloads; competitor tokscale ships no MCP), and the real download lever is npx-first CLI positioning — tracked separately. The MCP's value is agent-facing utility on data competitors don't expose (retry tax, routing waste, one-shot rate, task attribution).
2. Decisions (from brainstorming)
- Use case: unified — serves both live self-optimization and historical analysis from one tool set.
- Output contract: every tool returns a ready-to-display markdown table and the same data as structured JSON.
- Architecture: Approach A — a
codeburn mcpsubcommand on the existing CLI (no second package), reusing the existing aggregation; run as a long-lived in-process stdio server. - Tool surface: 2 tools —
get_usageandget_savings. (compare_periodscut — no reusable backend, overlap-incoherent.) - Privacy: project/session names hashed by default (
project-<6hex>); real names only wheninclude_project_names: true. Absolute paths never exposed.
3. Architecture
Process model
codeburn mcpstarts a resident, in-process MCP server over stdio (StdioServerTransport). Not exec-per-call — a resident process is required so the existing in-process session cache (180s TTL,src/parser.ts) amortizes across tool calls. Measured cost otherwise:--period allis ~17.6s even warm when the process exits between calls.- First line of the
mcpaction:console.log = console.error. The aggregation path is stdout-clean today (writes go to stderr), but the globalpreActionhook andrunOptimizecontain stdoutconsole.logs; reassigning immunizes the JSON-RPC stream against any present or future stdout write. (Verified:scanAndDetect,parseAllSessions, providers, caches,buildMenubarPayloaddo not write to stdout.) - Pre-warm
todayusage on boot so the first interactive call is fast.
Modules
src/usage-aggregator.ts(new) — extracted from the inline logic in thestatushandler (src/main.ts~476–760):buildUsage(period, opts): Promise<MenubarPayload>— cheap path, no optimize pass.buildSavings(period, opts): Promise<MenubarPayload>— addsscanAndDetect(optimize findings + retry tax + routing waste).opts: { provider?: string; project?: string[]; exclude?: string[]; range?: ResolvedRange }. Theproviderand project/range filters are required for parity — thestatusbody forks onisAllProvidersand resolves a range fromday/days/from/tobeforeperiod.status --format menubar-jsonis refactored to call these (one shared path).
src/mcp/server.ts(new) — buildsMcpServer, registers the 2 tools, connectsStdioServerTransport, owns the in-flight coalescing map and the empty-state messages.src/mcp/tables.ts(new) — compact markdown renderers per slice, reusingformat.tsandmodels-report.tswhere they already render tables.src/mcp/redact.ts(new) — stable pseudonym hashing for project/session names; applied unless the caller passesinclude_project_names: true.src/main.ts— add.command('mcp')that dynamic-import()s./mcp/server.jsand starts it.
Dependencies & build
- Add to
dependencies:@modelcontextprotocol/sdk@^1.29.0(v1 line; import paths@modelcontextprotocol/sdk/server/mcp.jsand/server/stdio.js) andzod@^3.25(NOT transitive — it's a peer dep of the SDK and absent from the lockfile today). - Add to
tsup.config.ts:external: ['@modelcontextprotocol/sdk', 'zod']. The current config bundles all deps (splitting: false, noexternal), so without this a dynamic import would inline the SDK (~MBs) intodist/main.js. Externalizing keepsdistsmall and makes the dynamic import a real lazy load fromnode_modules. (files: ["dist"]means externalized deps must be runtimedependencies.) - Pin the SDK major: a separately-named v2 package exists with different import paths;
^1.29.0keeps the v1 paths valid.
4. Tool Surface
Period enum (LLM-clear names, mapped internally): today→today, last_7_days→week, last_30_days→30days, month_to_date→month, last_6_months→all. Documented: last_6_months is the maximum window (codeburn's "all" = ~6 months); history is summarized, not dumped.
Both tools carry annotations { readOnlyHint: true, openWorldHint: false, idempotentHint: true, title }, a zod inputSchema and outputSchema, and return { content: [{ type:'text', text: <markdown table> }], structuredContent: <object matching outputSchema> }.
4.1 get_usage
- title: "CodeBurn — usage & cost"
- description (agent-facing): "Show AI coding token spend and usage for a period. Omit
byfor a headline summary; setbyto break it down by project, model, task, or provider. Fast (does not run the deeper savings analysis). Data is local to this machine and current as of the last scan." - inputSchema:
period?: enum(defaulttoday)by?: "project" | "model" | "task" | "provider"limit?: number(default 20, max 100) — row cap for breakdownsinclude_project_names?: boolean(defaultfalse)
- Behavior: no
by→ headline (cost, calls, sessions, input/output tokens, cache-hit %, one-shot rate) + top-N models/projects/tasks. Withby→ one ranked table for that dimension (project→topProjects,model→topModels,task→topActivities,provider→providerscost map). UsesbuildUsage(cheap path). - outputSchema: the relevant subset of
MenubarPayload.current(typed).
4.2 get_savings
- title: "CodeBurn — savings opportunities"
- description (agent-facing): "Find ways to reduce AI coding cost for a period: optimization findings, retry tax (money spent re-doing work), and routing waste (what you'd have saved on a cheaper model). Runs a deeper analysis, so it is slower than get_usage."
- inputSchema:
period?: enum(defaultlast_7_days— never default to the slowlast_6_months),include_project_names?: boolean(defaultfalse). - Behavior: runs
buildSavings; returns optimize findings (title, impact, $ saved), retry tax (total + by model), routing waste (total savings, baseline model, by model). - outputSchema:
{ optimize, retryTax, routingWaste }(typed).
4.3 Server instructions
"CodeBurn exposes local AI-coding spend data. Use
get_usagefor spend/usage and breakdowns (fast); useget_savingsto find cost reductions (slower). Project names are pseudonymized unless you passinclude_project_names: true. All data is read locally from this machine;last_6_monthsis the widest window. Numbers reflect the most recent scan and may lag the current in-flight session by a short interval."
5. Data Flow
agent tool call
→ zod inputSchema validation (invalid params → protocol error, auto)
→ in-flight coalesce on {kind, period, opts-hash} (parallel callers share one scan)
→ buildUsage / buildSavings(period, {provider:'all', ...})
→ parseAllSessions with PER-PROVIDER allSettled isolation → degraded[]
→ existing 180s in-process session cache amortizes
→ redact project/session names unless include_project_names
→ render markdown table + build structuredContent (validated vs outputSchema)
→ return { content:[{type:'text',text}], structuredContent } (isError:true on failure)
6. Privacy / Redaction
- Name-bearing fields —
topProjects[].name,topSessions[].project,topProjects[].sessionDetails— are replaced with stable pseudonymsproject-<6hex(sha256(name))>unlessinclude_project_names: true. - Absolute paths are never emitted (they aren't in the payload today; the MCP must not enrich with them).
- Rationale: an MCP is an egress surface to possibly-remote/cloud agents; codeburn's brand is "data never leaves your machine." Hashing keeps breakdowns coherent (stable pseudonyms) without leaking identity; local users who want real names opt in per call.
7. Performance
- Two paths:
get_usageuses the cheap aggregation (--no-optimizeseam already exists; ~3stoday,5s+13s) — isolated to the one tool that needs it. Without this split, every tool paid the 13s tax.all).get_savingsruns the optimize pass ( - Resident process + existing 180s session cache → warm calls are cheap.
- In-flight coalescing: concurrent calls for the same
{kind, period, opts}await a single scan (agents fire tools in parallel). - Pre-warm
todayusage on boot. - Token discipline: breakdowns capped at
limit(default 20); history is summarized (totals + top-N), never the full 365-day array; tables are compact.
8. Error Handling
- Invalid params → zod → MCP protocol error (auto).
- No data for period (fresh install) →
isError: falsewith a friendly "no usage recorded for yet — run some coding sessions" message (not a zero-filled table). - One provider fails to parse → isolate via
allSettled, return partial data, list skipped providers indegraded[]and a table footer note. - Aggregation throw / payload-shape mismatch →
isError: truewith a clear message (incl. version-skew hint); never hang the transport.
9. Testing
- Parity:
buildUsage('today', {provider:'all'})deep-equals currentstatus --format menubar-json --period today(plus one more period). Guards the extraction. - Per-tool: fixture payload → expected markdown table (snapshot) + structuredContent keys;
byeach dimension; redaction on/off (no real names/paths leak when off; pseudonyms stable); empty-state message. - MCP protocol smoke: in-memory transport —
listToolsreturns the 2 tools with annotations + outputSchema; call each; assert result shape (content+structuredContent) andisErrorpaths; concurrent identical calls coalesce to one scan. - Build: assert SDK + zod are externalized (absent from
dist) and declared independencies.
10. Out of Scope (v1 — YAGNI)
- HTTP/SSE transport (stdio only).
compare_periods(agent can diff twoget_usagecalls; backend is net-new and overlap-incoherent).- Write/mutating tools, auth, multi-machine/remote data.
- MCP prompts and resources primitives (tools-only v1; revisit if a "cost-review" prompt template proves useful).
- README "MCP" section and MCP-registry listings (lobehub/Smithery/mcp.so) — follow-up tasks, not code.
11. Provenance — review findings incorporated
Hardened by a validator + devil's-advocate pass:
- BLOCKER in-process resident server (caches don't help exec-per-call) — §3, §7.
- BLOCKER split cheap vs optimize path (optimize ≈ 70% of cost) — §7.
- BLOCKER redact names by default (raw repo names + dated spend would egress) — §6.
- MAJOR
buildUsage/buildSavingssignature carries provider/project/range for parity — §3. - MAJOR tsup
external+ deps independencies; pin SDK^1.29.0; addzodexplicitly — §3. - MAJOR per-provider
allSettledisolation +degraded[]— §5, §8. - MAJOR in-flight coalescing — §5, §7.
- MAJOR drop
compare_periods; rename period enums; defaulttoday— §4, §10. - MINOR
console.log→console.errorstdout guard; friendly empty-state; run compileddistnottsx; validate payload shape;last_6_monthsis the real max — §3, §8, §4.