Shared MCP Hub

Roost

One roost. Every session.

Explore Features

Screenshots

See It in Action

Features

One Daemon, Not Twenty-Two Processes Per Session

Every Claude Code session used to spawn its own private copy of all 22 stdio MCP servers configured across the fleet — measured at 92 processes and 5.85 GB of RSS with just 4 concurrent sessions open, extrapolating to roughly 270 processes and 17 GB at a dozen-session working set. Roost replaces that with a single daemon, roostd, that serves every logical server at http://127.0.0.1:3462/<server>/mcp over Streamable HTTP. Sessions carry plain URL entries instead of spawn commands, and tool names (mcp__<server>__*) never change, because the prefix comes from the config key, not the transport. Behind the router, roostd spawns at most one child per backend — lazily, shared by every session, reaped after idle — and answers tools/list from a persisted catalog snapshot without waking anything at all.

The Problem

22 Servers, Spawned Once Per Session, Forever

  • 92 Processes, 5.85 GB, Four Sessions

    Measured on the founder's Mac with just 4 concurrent Claude Code sessions open: 92 MCP server processes, 5.85 GB of total RSS, 43–94 MB per server. At the routine working set of a dozen sessions, that extrapolates to roughly 270 processes and 17 GB — doing nothing but holding identical idle copies of the same wrappers.

  • All Stateless, All Byte-Identical

    Every fleet MCP server is a thin wrapper that translates tool calls into REST calls against the tool's own backend — auth lives in env/config, not per-connection. Twelve copies of the same server hold zero session-specific state between them; they are byte-identical processes wasting byte-identical memory.

  • 22 Interpreter Startups Before a Single Tool Works

    Before Roost, every new session paid the cost of spawning all 22 stdio servers fresh — 22 Python interpreter startups before the first tool call could even resolve. Deferred tool loading had already solved the context-window half of the many-servers problem; the process half remained.

Architecture

One Daemon, Path-Per-Server Routing

  • roostd — A Single Shared Hub

    One asyncio process (uvicorn + Starlette), binding 127.0.0.1 only. Per configured server, roostd mounts an independent Streamable HTTP endpoint at /<server>/mcp, backed by a relay whose handlers forward to that backend's one shared client session.

  • Tool Names Never Change

    Each stdio stanza in ~/.claude.json becomes a plain URL entry, but the config key — and therefore the mcp__<server>__* prefix every session and every CLAUDE.md already references — is untouched. roostctl init lifts every stanza verbatim and keeps a .mcp.direct.json escape hatch alongside it.

  • Lazy Spawn, Never on tools/list

    A backend child spawns only on its first real tools/call, with exponential backoff and a circuit breaker if it keeps failing. Cold tools/list is served from the catalog snapshot instead — the invariant that keeps a fresh session's tool listing fast.

  • Catalog Snapshot Cache

    Every server's initialize result plus its tools/resources/prompts lists are persisted to disk and served in microseconds. The snapshot invalidates on a real spawn diff, an explicit roostctl refresh, or a staleness check against each server's watch_paths.

  • Shared-Child Multiplexing, No Hand-Rolled ID Remapping

    All forwards for a backend funnel through its single SDK ClientSession, which assigns its own monotonically increasing request IDs — cross-session collisions are impossible by construction, not merely unlikely. Progress notifications relay back only to the session that made the call.

Reliability

Self-Healing by Default, Reversible in Seconds

  • launchd KeepAlive

    The default start mode runs roostd as a launchd user agent with KeepAlive — it restarts itself on crash and survives a reboot without a human re-running a script. A kill -9 against a live daemon was verified to auto-respawn in about six seconds.

  • Per-Backend Circuit Breaker

    A child that dies mid-request fails those in-flight calls with the child's exit code and last stderr lines, then respawns on the next call — every other backend keeps running untouched. Repeated failures trip a circuit breaker rather than retrying forever.

  • roostctl verify — The Parity Gate

    Before any session config is ever flipped onto the hub, roostctl verify spawns each server directly and diffs its tools/list byte-for-byte against what roostd serves. The rule is absolute: every configured server must pass before a single Claude config changes.

  • One-Command Revert

    roostctl revert restores the pre-flip direct-spawn configs from a timestamped backup in seconds. Because only new sessions read the flipped config, any session already open when something goes wrong was never affected in the first place.

Observability

A Quiet Hub Is Still a Legible Hub

  • JSONL Request Log, INFO on Every Transition

    Every call is logged — timestamp, session, server, method, tool, duration, outcome — and every spawn, reap, refresh, and circuit open/close emits an INFO milestone. A watcher polling the log should never see minutes of silence while the hub is active.

  • roostctl status / top

    A live per-server table: mode, PID, RSS, warm-or-cold, last-used timestamp, call count, and p50 latency — the same view whether you're checking one backend or auditing the whole fleet's memory footprint at a glance.

  • roostctl doctor

    One command checks port availability, launchd registration, catalog snapshot ages, and file-descriptor headroom against the socket count a dozen sessions times 22 endpoints actually needs — the fast way to rule out infrastructure before debugging a tool call.

Validation

Measured, Not Estimated

  • 22/22 Parity, Verified Three Times

    roostctl verify passed 22/22 across two integration runs plus once more inside the live config flip — tool names and input schemas identical to direct spawn for every server, combining to more than 1,000 tools total (courier alone contributes 128, trellis 111, beacon 86).

  • ~95% Memory Reduction, Measured

    Pre-flip baseline on the same machine: 92 processes and 5.85 GB RSS at 4 sessions. Post-flip, roostd itself runs 59–83 MB RSS with a sample warm child (fulcrum) at 75 MB — one daemon plus a small warm set, at any session count, instead of 22 processes multiplied by every open session.

  • Recovery Verified: kill -9, Twice

    Killing a warm child mid-call returns a clean JSON-RPC error and the very next call respawns it successfully. Killing roostd itself gets it back via launchd KeepAlive with no session restart required — both recovery paths were exercised directly, not assumed.

How It Works

Handshake, Spawn, Relay, Reap

  1. Step 1: Handshake

    A new session's tools/list for all ~22 servers is served straight from the catalog snapshot — no child process wakes up, no interpreter starts. Cold listings resolve in milliseconds instead of after 22 live handshakes.

  2. Step 2: Spawn

    The first real tools/call to a backend spawns exactly one child for it, with exponential backoff and a circuit breaker if it keeps failing. Every other session already connected shares that same child from this point forward.

  3. Step 3: Relay

    Requests multiplex through the backend's single SDK ClientSession, which owns its own monotonic request IDs — cross-session collisions are impossible by construction. Progress notifications route back only to the session that made the call.

  4. Step 4: Reap

    A backend idle past its TTL (30 minutes by default; longer for a keep-warm set like lattice and cortex) is reaped automatically. The next call respawns it — steady-state memory tracks actual usage, not configuration.

Technical Specifications

Under the Hood

  • Daemon (roostd)

    • Python 3.12 + uv, built on the official mcp SDK on both sides
    • Streamable HTTP frontend — one session manager per configured server, mounted at /<server>/mcp
    • Loopback-only: binds 127.0.0.1:3462, nothing exposed beyond the workstation
    • One shared ClientSession per backend relays every forwarded call to its single child
    • Reserved paths: /healthz, /status, loopback-only /admin/refresh and /admin/restart
  • Lifecycle

    • Lazy spawn on first tools/call only — never on tools/list
    • Idle reap per server (default 30-minute TTL; keep_warm override for the hot set)
    • Exponential backoff plus a circuit breaker on repeated spawn failures
    • Catalog snapshot per server, invalidated by real-spawn diff, manual refresh, or watch-path staleness
    • Per-backend concurrency semaphore (default 8) with a FIFO queue
  • CLI (roostctl)

    • init — imports existing stdio stanzas from ~/.claude.json and the project .mcp.json
    • status / top — per-server mode, PID, RSS, warm/cold, last used, calls, p50
    • verify — the parity gate: diffs live tools/list against a direct spawn, server by server
    • flip / revert — swap Claude configs onto Roost, or restore the originals in seconds
    • refresh / restart / doctor — re-snapshot a server, bounce a child, or check port/launchd/fd health
  • Deployment

    • Managed host process, not a container — must exec the fleet's macOS venv interpreters directly
    • Default start: a launchd user agent with KeepAlive — self-heals on crash, survives reboot
    • --background nohup-plus-PID fallback for machines without the Full Disk Access grant
    • PID file and JSONL logs under the stack's standard managed-services convention
    • Escape hatch: roostctl revert restores direct-spawn configs from a timestamped fallback backup

Development

100% Built by Claude

Every tool in the Renkara fleet was built by Claude (Anthropic) working alongside a single human supervisor. Every line of code, every test, every deployment: AI-authored with human direction. The leverage factor across the fleet runs in the 20x–50x range, with individual sessions regularly exceeding 100x.

See the daily leverage records for per-task numbers across the full build history.