Skip to content

mcp: refactor @relayburn/mcp as a thin wrapper over burn <verb> --json so the MCP surface tracks the CLI automatically #210

Description

@willwashburn

Summary

Today @relayburn/mcp exposes only two hand-authored tools (burn__sessionCost, burn__currentBlock) — a tiny subset of the CLI's read surface. As the CLI grows (#152 hotspots/overhead, #156 summary --by-tool, #164 budget, #165 state, etc.), MCP falls further behind. Refactor the MCP package so each tool is a thin wrapper that shells out to burn <verb> --json internally. New CLI verbs become MCP tools "for free" — drift becomes structurally impossible.

Background

Today's MCP surface (packages/cli/src/commands/mcp-server.ts:64-67, @relayburn/mcp package):

  • burn__sessionCost — total USD / tokens / turns / models for a session
  • burn__currentBlock — 5-hour OAuth window % + local burn-rate forecast
  • 668 LOC across packages/mcp/src/ (config, server, tools, types)
  • Each tool is hand-authored — query logic duplicated from CLI commands

Today's CLI surface (post-cleanup epic): 10+ verbs, every read verb supports --json, output schemas are stable.

The drift: every new CLI verb requires a hand-authored MCP tool to stay relevant. That has not happened. The two MCP tools are subsets of burn summary --session $X --json and burn budget --json — already replicable through the CLI alone if the agent has shell access.

Why MCP still earns its keep: in-session agent self-query has real value over Bash invocation —

  • Tool-schema discoverability (agent sees the tools listed, doesn't need to know burn exists)
  • Session id baked into the server invocation (no env-var dance)
  • Lower per-query overhead in tokens (structured tool definitions vs. shell command + JSON parse)
  • Works in restricted-shell environments
  • Works across all major harnesses today (Claude Code, Codex, OpenCode all support MCP)

The right fix isn't to drop MCP — it's to make MCP's surface a derivative of the CLI's surface so they stay in lockstep without coordination.

Proposed shape

Each MCP tool becomes a thin registration that:

  1. Declares its input schema (the subset of CLI flags it accepts)
  2. Maps tool input to CLI args
  3. Spawns burn <verb> --json [flags] as a subprocess
  4. Parses stdout, returns the JSON as the MCP tool result
  5. Handles non-zero exit codes / non-JSON stderr as MCP tool errors
// packages/mcp/src/tools/cli-derive.ts (new)exportinterfaceCliDerivedToolDef{verb: string;// 'summary' | 'hotspots' | ...toolName: string;// 'burn__summary'description: string;inputSchema: ZodSchema;// tool-input shapebuildArgs: (input: unknown,defaults: ToolDefaults)=>string[];// CLI flag list}exportfunctioncreateCliDerivedTool(def: CliDerivedToolDef,defaults: ToolDefaults): Tool{return{name: def.toolName,description: def.description,inputSchema: def.inputSchema,handler: async(input)=>{constargs=def.buildArgs(input,defaults);const{ stdout, stderr, exitCode }=awaitexecFile('burn',[def.verb,'--json', ...args]);if(exitCode!==0){thrownewError(`burn ${def.verb} failed: ${stderr||stdout}`);}returnJSON.parse(stdout);},};}

Tool catalog (initial)

MCP toolCLI invocation
burn__summaryburn summary --json [flags]
burn__hotspotsburn hotspots --json [flags]
burn__compareburn compare <models> --json [flags]
burn__overheadburn overhead --json [flags]
burn__overheadTrimburn overhead trim --json [flags] (after #204)
burn__budgetburn budget --json [flags]

The two existing tools (burn__sessionCost, burn__currentBlock) are subsets of the new generic tools. Two paths:

  • Drop them — agents use burn__summary --session $defaultSessionId and burn__budget --no-forecast. Cleaner long-term.
  • Keep as named aliases — preserves the convenience surface for existing config consumers. One-line registrations that internally call the same CLI subprocess.

Recommend drop since this is internal-API churn and there are no users yet.

Default session id flow

The --session-id <uuid> passed to burn mcp-server (today's behavior — commands/mcp-server.ts:30-31) flows into ToolDefaults.sessionId. Tools that scope by session (burn__summary, burn__hotspots) inject it as a default --session flag if the tool input doesn't override.

Implementation Steps

No users yet — clean refactor, no compatibility shims for the existing tool names.

  1. Add the cli-derive helper (packages/mcp/src/tools/cli-derive.ts):
    • execFile-based subprocess wrapper. Use node:child_process's execFile (not exec) to avoid shell injection.
    • Resolve the burn binary path: prefer process.execPath-relative resolution if running from the same monorepo install; otherwise rely on $PATH. Document the assumption.
    • JSON parse + error handling. If stdout isn't valid JSON (e.g. command error), bubble stderr as the tool error.
  2. Build the tool catalog (packages/mcp/src/tools/):
    • One file per tool: summary.ts, hotspots.ts, compare.ts, overhead.ts, overhead-trim.ts, budget.ts. Each exports a CliDerivedToolDef with the tool's input schema and arg-builder.
    • Input schemas use Zod (already a dependency of @relayburn/mcp). Cover the read flags: since, project, session, workflow, agent, provider, plus mode flags for summary (byProvider, byTool, bySubagentType, subagentTree) and the patterns flag for hotspots.
    • compare requires the model-list positional (per cli: burn compare should take models as a required positional, not a --models flag #159). Schema: { models: string[] } (length ≥ 2).
  3. Update mcp-server.ts (packages/cli/src/commands/mcp-server.ts:64-67):
    • Replace the hand-authored createSessionCostTool / createCurrentBlockTool with the new tool catalog.
    • Pass defaults: { sessionId: defaultSessionId } through to every tool that scopes by session.
    • Update help text (MCP_HELP constant at mcp-server.ts:11-23) to list the new tool catalog.
  4. Delete the old hand-authored tools:
    • packages/mcp/src/tools/sessionCost.ts (and equivalents) — gone.
    • packages/mcp/src/index.ts exports updated.
  5. Update @relayburn/mcp README and packages/mcp/CHANGELOG.md to reflect the new shape and tool catalog. Bump major version (1.0.0 → 2.0.0) since the tool names change.
  6. Tests:
    • packages/mcp/src/tools/cli-derive.test.ts: stub execFile, assert correct args are passed for various tool inputs, assert JSON parse round-trip, assert error-path behavior.
    • packages/mcp/src/end-to-end.test.ts: spawn the real burn binary via cli-derive, exercise each tool against a fixture ledger, assert the structured output matches the CLI's --json shape.
  7. Docs sweep: README mention of MCP tools, AGENTS.md if present, CHANGELOG [Unreleased] on @relayburn/mcp and @relayburn/cli.

Files touched (scope)

  • packages/mcp/src/tools/cli-derive.ts — new helper
  • packages/mcp/src/tools/{summary,hotspots,compare,overhead,overhead-trim,budget}.ts — new (one per tool)
  • packages/mcp/src/tools/{sessionCost,currentBlock}.ts — deleted
  • packages/mcp/src/index.ts — export updates
  • packages/cli/src/commands/mcp-server.ts — uses the new catalog
  • packages/mcp/{README.md,CHANGELOG.md} — version bump + tool catalog
  • packages/mcp/src/end-to-end.test.ts and new test file
  • Root README, AGENTS.md if needed

No CLI surface changes. No @relayburn/analyze / @relayburn/ledger API changes.

Risks / questions

  • Subprocess overhead. Per-call cost is ~ms (Node spawn + JSON parse). Negligible against typical model latency. Document the assumption — if it ever becomes a hot path, the same tools can grow an in-process implementation later.
  • burn on PATH. The MCP server must resolve the burn binary. Today's MCP setup already implicitly requires this (the harness's MCP config invokes burn mcp-server by name). Document the requirement; consider BURN_BIN env override for non-standard installs.
  • Error-shape parity. CLI commands exit non-zero with stderr text on error. MCP tools must surface those as structured errors ({ error: string }). Define a uniform error-translation policy in cli-derive.
  • Tool input schema vs CLI flag drift. Adding a new flag to a CLI verb won't automatically appear in the MCP tool's input schema. Schemas are still hand-authored, but they're per-tool and small. Worth tracking — if drift becomes a problem, the long-term play is to have the CLI export its flag schemas in a structured format MCP can import.
  • Tool naming.burn__summary vs summary (no namespace). Today's tools use the burn__ prefix; preserve it. Some harnesses present tools without namespacing, so the prefix gives users a "where is this from" hint.
  • Major bump. Renaming burn__sessionCost → drop and replace with burn__summary is a breaking API change for any consumer importing @relayburn/mcp directly. No users yet, so the major bump is documentation, not a migration.

Out of scope

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions