Skip to content

relayburn-cli: burn ingest + burn mcp-server (#248 D8, closes #210)#319

Merged
willwashburn merged 3 commits intomainfrom
rust-port/cli-ingest-mcp
May 7, 2026
Merged

relayburn-cli: burn ingest + burn mcp-server (#248 D8, closes #210)#319
willwashburn merged 3 commits intomainfrom
rust-port/cli-ingest-mcp

Conversation

@willwashburn
Copy link
Copy Markdown
Member

Summary

  • Wire burn ingest (no-flag scan, --watch poll loop, --hook claude --quiet) as a thin presenter over relayburn_sdk::ingest_all + start_watch_loop. TS source-of-truth: packages/cli/src/commands/ingest.ts.
  • Wire burn mcp-server as a hand-rolled JSON-RPC 2.0 stdio server exposing burn__sessionCost. Mirrors packages/mcp/src/server.ts + packages/mcp/src/tools/session-cost.ts 1:1; closes mcp: refactor @relayburn/mcp as a thin wrapper over burn <verb> --json so the MCP surface tracks the CLI automatically #210 (the standalone-MCP-server question).
  • Drop ingest and mcp-server from UNIMPLEMENTED_SUBCOMMANDS in the CLI smoke test. (run stays — D5 owns its removal.)

Notes

  • The MCP server hand-rolls JSON-RPC framing rather than depending on rmcp. The on-wire surface is tiny (initialize / ping / tools/list / tools/call), the TS sibling already hand-rolls the same shape, and freezing a heavy SDK version buys nothing for the read-only surface this command exposes today. If/when we need richer transports, this module is localized enough to swap in one place.
  • Tool surface for D8 is intentionally minimal: burn__sessionCost only. burn__summary / burn__hotspots are tracked as follow-ups so the scope of D8 stays tight.
  • --hook claude mode validates the payload shape and (today) drives a full ingest_all sweep — the SDK does not yet expose a single-transcript verb. The cursor short-circuit makes this no slower than the TS hook in practice; a single-transcript SDK verb is a clean follow-up if needed.
  • Adds signal to tokio features in relayburn-cli/Cargo.toml so the --watch mode can trap SIGINT / SIGTERM and drain in-flight ticks before exiting.

Test plan

  • cargo build --workspace clean.
  • cargo test --workspace green (8 CLI smoke tests, 610 SDK unit tests, doc-tests).
  • burn ingest --help lists --watch, --interval, --hook, --quiet.
  • burn ingest (no flags, isolated HOME) prints [burn] ingest: ingested 0 sessions (+0 turns) and exits 0.
  • burn ingest --watch --interval 500 --quiet accepts SIGINT, exits 0.
  • burn ingest --watch --hook claude rejects with exit 2 + the canonical message.
  • burn ingest --hook codex rejects with exit 2 (unsupported harness).
  • burn ingest --hook claude with valid stdin payload exits 0; empty stdin emits the nothing to do breadcrumb and exits 0.
  • burn mcp-server --help lists --session-id, --debug, --ledger-path.
  • MCP stdio handshake: piped initialize + tools/list + tools/call burn__sessionCost + ping over stdin returns the expected JSON-RPC frames; tools/call with no sessionId and no --session-id returns the descriptive "no session id provided and server was not registered with one" note.

Wire `burn ingest` as a thin presenter over `relayburn_sdk::ingest_all`
plus the SDK's `start_watch_loop` controller. Three modes mirror the TS
sibling at `packages/cli/src/commands/ingest.ts`:

- No flags: one full sweep, then exit. Logs
  `[burn] ingest: ingested N session(s) (+M turn(s))` on stderr.
- `--watch [--interval MS]`: foreground poll loop. Skips empty-tick
  log lines (TS shape); SIGINT / SIGTERM stop drains in-flight ticks
  before returning.
- `--hook claude [--quiet]`: stdin-driven Claude hook entrypoint.
  Validates the `session_id` / `transcript_path` payload shape, then
  drives a full sweep (cursor short-circuit means cost is bounded by
  new turns, not the hook payload). Failure paths log + exit 0 to
  avoid blocking the parent Claude tool call.

Wire `burn mcp-server` as a hand-rolled JSON-RPC 2.0 stdio server,
mirroring `packages/mcp/src/server.ts` rather than depending on
`rmcp`. The on-wire surface (`initialize`, `ping`, `tools/list`,
`tools/call`) is small enough that a tight in-tree implementation
beats freezing a heavy SDK version. Single tool ships in this PR:
`burn__sessionCost`, a 1:1 port of `packages/mcp/src/tools/session-cost.ts`
delegating to `LedgerHandle::session_cost`. Future tools (summary,
hotspots, …) land separately. Closes #210.

Smoke test drops `ingest` and `mcp-server` from
`UNIMPLEMENTED_SUBCOMMANDS`; `--help` for both subcommands still passes
the existing help-shape assertion. Manual stdio handshake verified:
`initialize` echoes the client's `protocolVersion`, `tools/list`
returns `burn__sessionCost`, and `tools/call burn__sessionCost`
returns the SDK payload as both stringified `text` content and a
`structuredContent` mirror.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a9e1caa-0f3d-4076-977f-fe7f9b4c8c19

📥 Commits

Reviewing files that changed from the base of the PR and between a1b4660 and 3ed58fa.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • crates/relayburn-cli/src/cli.rs
  • crates/relayburn-cli/src/commands/ingest.rs
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/relayburn-cli/src/commands/ingest.rs

📝 Walkthrough

Walkthrough

The PR implements burn ingest (one-shot, --watch polling with signal handling, and --hook stdin modes) and a stdio JSON-RPC 2.0 MCP server exposing burn__sessionCost. It updates CLI argument types, dispatch wiring, adds Tokio signal feature, fills in command implementations, and adjusts tests and changelog.

Changes

CLI & Command Implementation

Layer / File(s) Summary
CLI Interface
crates/relayburn-cli/src/cli.rs
Command enum: added Ingest(IngestArgs); changed McpServer to McpServer(McpServerArgs); added IngestArgs (--watch, --interval, --hook, --quiet) and McpServerArgs (--session-id, --debug, etc.).
Dependency Setup
crates/relayburn-cli/Cargo.toml
Added signal feature to the workspace tokio dependency (alongside sync, rt) to enable SIGINT/SIGTERM handling for watch loops.
Ingest Implementation
crates/relayburn-cli/src/commands/ingest.rs
Implements run(globals, args) with three modes: one-shot ingest (runs ingest_all), --watch polling loop (interval validation, persistent LedgerHandle via Arc<Mutex>, logs only on appended work, waits for SIGINT/SIGTERM, stops controller), and --hook claude (reads stdin when non-TTY, JSON validation of session_id/transcript_path, always exits 0 for hook errors/no-op). Adds ledger-opening, stdin, signal helpers, pluralized ingest-line rendering, and a unit test for pluralization.
MCP Server Implementation
crates/relayburn-cli/src/commands/mcp_server.rs
Implements stdio-based JSON-RPC 2.0 server: reads line-delimited frames on a blocking thread into an async loop, handles initialize, ping, tools/list, tools/call, implements burn__sessionCost (calls SDK session_cost using locked shared LedgerHandle), returns tool errors as MCP success payloads with isError: true and protocol errors as JSON-RPC errors. Includes response helpers and a unit test for JSON-RPC error envelope formatting.
Dispatch Wiring
crates/relayburn-cli/src/main.rs
Dispatch updated to pass per-subcommand args into commands::ingest::run and commands::mcp_server::run.
Tests / Changelog
crates/relayburn-cli/tests/smoke.rs, CHANGELOG.md
Removed ingest and mcp-server from unimplemented-stub list in smoke test; added [Unreleased] changelog entry documenting the new ingest wiring and mcp-server stdio tool with issue references.

Sequence Diagrams

sequenceDiagram
    participant CLI as CLI (burn ingest)
    participant Dispatcher as Dispatcher
    participant Ingest as Ingest Handler
    participant Ledger as Ledger
    participant SDK as SDK
    participant SignalHandler as Signal Handler
    participant Output as Stderr

    CLI->>Dispatcher: Command::Ingest(IngestArgs)
    Dispatcher->>Ingest: run(&globals, args)
    alt Watch Mode
        Ingest->>Ledger: open_handle()
        Ledger-->>Ingest: LedgerHandle
        Ingest->>SDK: start_watch_loop(ledger, interval)
        loop Each tick
            SDK->>SDK: scan_sessions() + ingest_all()
            SDK-->>Ingest: {appended_turns, ...}
            alt appended_turns > 0
                Ingest->>Output: log "[burn] ingest: ingested X (+Y)"
            end
        end
        Ingest->>SignalHandler: wait_for_signal()
        SignalHandler-->>Ingest: SIGINT/SIGTERM
        Ingest->>SDK: stop_controller()
        Ingest->>Output: exit 0
    else Hook Mode
        Ingest->>Ingest: read_stdin()
        Ingest->>Ingest: validate JSON {session_id, transcript_path}
        Ingest->>SDK: ingest_all(ledger, session)
        Ingest->>Output: log report (if appended_turns > 0)
        Ingest->>Output: exit 0
    else One-Shot Mode
        Ingest->>Ledger: open_handle()
        Ingest->>SDK: ingest_all(ledger)
        Ingest->>Output: log "[burn] ingest: ingested X (+Y)"
        Ingest->>Output: exit 0 or non-zero on error
    end
Loading
sequenceDiagram
    participant Harness as Harness Process
    participant MCP as MCP Server
    participant Stdin as Stdin Reader (blocking)
    participant Channel as Async Channel
    participant Handler as Request Handler
    participant SDK as SDK (Ledger)
    participant Stdout as Stdout Writer

    Harness->>MCP: spawn burn mcp-server
    MCP->>SDK: open_handle()
    SDK-->>MCP: LedgerHandle(Arc<Mutex>)
    MCP->>MCP: build current-thread Tokio runtime

    par Stdin Reading
        Stdin->>Channel: send line-delimited JSON-RPC frames
    and Request Dispatch
        Handler->>Channel: recv frame
        alt Valid JSON-RPC Request
            Handler->>Handler: parse & validate
            alt Known Method
                alt tools/call (burn__sessionCost)
                    Handler->>SDK: session_cost(args)
                    SDK-->>Handler: {result} or tool-error
                    Handler->>Stdout: write_response(success with payload / isError)
                end
                Handler->>Stdout: write_response(success)
            else Unknown Method
                Handler->>Stdout: write_response(JSON-RPC error -32601)
            end
        else Invalid JSON
            Handler->>Stdout: write_response(JSON-RPC error -32700)
        else Notification (no id)
            Handler->>Handler: suppress response
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #210 — Proposes making MCP a thin wrapper over CLI verbs; this PR implements a stdio MCP server foundation and exposes burn__sessionCost, aligning with that objective.
  • #248 — Tracks the relayburn-cli Rust rewrite; this PR implements the ingest and mcp-server subcommands referenced there.
  • #237 — Requests expansion of the MCP tool catalog; this PR adds the MCP server dispatch and burn__sessionCost that future tools can reuse.

Possibly related PRs

Poem

A rabbit hops through the CLI's door,
With ingest and watch loops and much, much more!
The MCP server hums JSON lines at night,
Ledgers and signals dancing in soft light. 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive Issue #210 requires a broader MCP refactor using CLI-derived tools and cli-derive helper, but this PR implements only a hand-rolled burn__sessionCost server. Clarify whether this PR (#248 D8) is an interim implementation and if issue #210's full requirements are addressed in a different PR or phase.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: implementing burn ingest and burn mcp-server subcommands with appropriate issue references.
Description check ✅ Passed The description comprehensively covers the implementation of both burn ingest and burn mcp-server commands, test plan, and design decisions.
Out of Scope Changes check ✅ Passed All changes directly support implementing burn ingest and burn mcp-server subcommands as described in the PR objectives without introducing unrelated modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rust-port/cli-ingest-mcp

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1b4660e40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if quiet && report.appended_turns == 0 {
return;
}
eprint!("{}", render_ingest_line(report));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route one-shot ingest report to stdout

burn ingest in non-watch/non-hook mode now emits its summary via stderr (log_report), but the TS source-of-truth prints that report to stdout in runIngestOnce (packages/cli/src/commands/ingest.ts, lines 121-126). This breaks parity for callers that capture stdout (e.g., shell pipelines/automation that parse the ingest summary) and makes the Rust command behave differently from the existing CLI contract in the default mode.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3ed58fa. The one-shot path now routes its [burn] ingest: ingested N session(s) (+M turn(s)) line to stdout via a new log_report_oneshot helper that wraps print! (mirroring TS runIngestOnce at packages/cli/src/commands/ingest.ts:121-126 which uses process.stdout.write). --watch keeps its existing eprint!-driven banner + tick lines on stderr (TS sibling logs the same surface to stderr), and --hook claude keeps its breadcrumbs on stderr (matches TS hook behavior + the pending-stamp protocol).

Smoke-tested in an isolated HOME:

  • burn ingest > /tmp/out 2>/tmp/err/tmp/out has [burn] ingest: ingested 0 sessions (+0 turns), /tmp/err is empty.
  • burn ingest --watch --interval 500 (SIGINT) → foreground ingest every 500ms; Ctrl-C to stop lands on stderr, stdout empty.
  • burn ingest --hook claude with valid stdin → breadcrumb stays on stderr.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/relayburn-cli/src/cli.rs`:
- Around line 160-164: The --quiet flag is currently declared as a global pub
quiet field in the CLI struct (cli.rs) and is being parsed for one-shot and
--watch invocations; remove that global pub quiet and instead add a hook-scoped
flag on the hook-specific arguments (e.g., the struct or enum variant used for
the "hook" path under the ingest command—add a quiet: bool field to the
HookOptions/HookArgs or the claude hook variant). Update the code paths that
read the flag (the hook invocation handler / run_hook / ingest_hook function) to
read the new hook-scoped field and remove any usage that depended on the removed
global field so only burn ingest --hook [--quiet] affects hook logging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1d445569-3529-46f5-a125-b5ae31fa158f

📥 Commits

Reviewing files that changed from the base of the PR and between 067c096 and a1b4660.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • CHANGELOG.md
  • crates/relayburn-cli/Cargo.toml
  • crates/relayburn-cli/src/cli.rs
  • crates/relayburn-cli/src/commands/ingest.rs
  • crates/relayburn-cli/src/commands/mcp_server.rs
  • crates/relayburn-cli/src/main.rs
  • crates/relayburn-cli/tests/smoke.rs

Comment thread crates/relayburn-cli/src/cli.rs
…et hook scope)

- Merge origin/main (D6 codex landed); CHANGELOG additive, registry/mod
  auto-merged with codex slot from main.
- Codex P2: route burn ingest one-shot summary to stdout (matches TS
  runIngestOnce at packages/cli/src/commands/ingest.ts:121-126);
  --watch banner + --hook breadcrumbs stay on stderr.
- CodeRabbit Minor: scope --quiet to --hook via clap requires = "hook"
  (mirrors the --reingest requires = "force" pattern from #313).
  Standalone --quiet and --watch --quiet are now rejected at parse time.
@willwashburn willwashburn merged commit 33a5f86 into main May 7, 2026
8 checks passed
@willwashburn willwashburn deleted the rust-port/cli-ingest-mcp branch May 7, 2026 00:40
willwashburn added a commit that referenced this pull request May 7, 2026
Absorbs #319 (Wave 2 D8 — burn ingest + burn mcp-server) on top of D5's
prior merge with origin/main.

Resolutions:

- crates/relayburn-cli/src/cli.rs — auto-merged: keep all eight typed
  Command variants (D5's Run + D8's Ingest/McpServer + the four already
  typed by D1-D4 + State from D4).
- crates/relayburn-cli/src/main.rs — auto-merged: dispatch arms for all
  eight subcommands route to typed handlers; no more not_yet_implemented
  fallbacks anywhere.
- crates/relayburn-cli/Cargo.toml — union the tokio feature flags from
  both branches: D5 needed `process` (tokio::process::Command::status
  for the run driver), D8 needed `signal` (SIGINT/SIGTERM trap in
  --watch). Final feature set: ["sync", "rt", "process", "signal"].
- crates/relayburn-cli/tests/smoke.rs — every Wave 2 subcommand is now
  wired, so UNIMPLEMENTED_SUBCOMMANDS becomes [] and the
  each_stub_exits_one_with_not_yet_implemented_message iteration helper
  becomes a no-op pass over an empty slice (constant kept so a future
  scaffold has somewhere to land). Also pivoted
  json_mode_emits_error_envelope_on_unimplemented to use `compare`'s
  no-models error path (still routes through report_error so the JSON
  envelope contract is exercised); renamed it
  json_mode_emits_error_envelope_on_argument_failure.
- crates/relayburn-cli/src/commands/mod.rs — `not_yet_implemented` is no
  longer called by any subcommand; added `#[allow(dead_code)]` so the
  placeholder helper survives without warning.
- CHANGELOG.md — additive: keep both D5 and D8 bullets under
  [Unreleased].
- Cargo.lock — refreshed via `git checkout origin/main -- Cargo.lock &&
  cargo build --workspace`. Tokio's `signal` feature pulled in
  signal-hook-registry / mio extensions cleanly.

Verified: cargo build --workspace clean; cargo test --workspace green
(610 SDK + 9 sdk-node + 10 cli smoke + 5 cli golden); BURN_GOLDEN=1
cargo test --test golden -p relayburn-cli passes byte-for-byte.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant