A tall wooden dovecote at dusk, warm light glowing from its ring of round doors, doves settled on the rail.

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.