Liking cljdoc? Tell your friends :D

bb-mcp

Clojars Project cljdoc release License: MIT

Lightweight MCP (Model Context Protocol) server that bridges Claude Code and Codex to the hive-mcp JVM over nREPL. It runs on ClojureWasm (cljw, the default) or on Babashka (bb), from the same source.

OpenClaw ACP

bb-mcp.acp is a portable .cljc JSON-RPC client for OpenClaw's ACP methods: initialize, session/new, session/load, session/prompt, and session/cancel. It accepts an injected exchange! function, so the same message layer works over bb-mcp stdio, the cljw TCP adapter, JVM sockets, or a ClojureScript transport. hive-claw should consume this seam instead of inventing a second ACP wire format.

Why bb-mcp?

The Problem: Running multiple Claude Code instances (e.g., swarm agents) each with their own JVM-based MCP server consumes massive resources.

The Solution: bb-mcp is a lightweight multiplexer: every client gets a small bb-mcp head, and all heads share ONE hive-mcp JVM. With N clients you pay for one JVM plus N heads instead of N JVMs.

Key Metrics

One head, measured 2026-10-22 on Linux x86_64 (cljw v1.15.3, babashka v1.13.224), three runs each against a running hive-mcp, on a loaded host, so read the ratios rather than the exact times:

Per headcljw (default)bb
Peak memory (RSS)~54MB~129MB
Process start to initialize reply (connects to hive-mcp, loads its tools)164-506ms, median 288ms320-383ms, median 370ms
tools/list (73KB of JSON)9-16ms4ms
clojure_eval (+ 1 2) round trip, median37-59ms39-47ms
clojure_eval returning ~110KB44-92ms70-163ms

Per-call time is dominated by the nREPL round trip and hive-mcp itself, so the two runtimes feel the same in use. Memory is where they differ, and the RSS column overstates bb's cost for a fleet:

Fresh headcljwbb
Binary9.4MB90.5MB
Runtime alone (-e nil), peak RSS13.7MB63.2MB
Private (anonymous) memory~46MB~75MB
File-backed, shared by every process of that binary~1MB~51MB

bb is a GraalVM native image: its binary carries a pre-built heap of every bundled namespace, and about 51MB of it is mapped from the file and shared between bb heads. cljw's binary is code only and its runtime loads into a compact private heap. So ten fresh heads cost ~0.5GB on cljw against ~0.8GB on bb (10 × 75MB + 51MB once), on top of the one shared JVM.

cljw's memory is nearly all private and grows with traffic: four long-lived cljw heads measured 32, 68, 88 and 219MB of private memory, the largest after 3 hours of a busy session. The saving is largest for many short-lived heads.

Use Case: Claude Swarm

When running swarm agents (multiple Claudes working in parallel), each agent needs an MCP connection to hive. Without bb-mcp, you'd need a JVM per agent. With bb-mcp:

  • 1 hive-mcp (JVM) serves memory, kanban, swarm, git and the Emacs integration
  • N bb-mcp heads (cljw by default, or bb) multiplex to it
  • All agents share tools, memory, kanban and git through the single JVM

Architecture

                    bb-mcp Instances              Shared JVM
                    ┌──────────────┐
  Claude 1 ───────▶ │   bb-mcp     │──┐
                    └──────────────┘  │
                    ┌──────────────┐  │     ┌─────────────────┐
  Claude 2 ───────▶ │   bb-mcp     │──┼────▶│   hive-mcp     │
                    └──────────────┘  │     │   (nREPL:7910)  │
                    ┌──────────────┐  │     └────────┬────────┘
  Claude 3 ───────▶ │   bb-mcp     │──┘              │
                    └──────────────┘                 ▼
                   ~54MB each (cljw)           ┌─────────────┐
                                               │    Emacs    │
                                               └─────────────┘

Data Flow:

  1. Claude Code connects to bb-mcp via MCP protocol (stdio)
  2. bb-mcp serves its native tools itself: bash and clojure_eval (on cljw, bash runs inside hive-mcp; see Runtimes)
  3. Every other tool is forwarded to hive-mcp via nREPL on port 7910
  4. hive-mcp executes elisp in Emacs via emacsclient where a tool needs it

Runtimes: cljw and bb

start-bb-mcp.sh picks the runtime from BB_MCP_RUNTIME, cljw by default:

  1. It looks for a cljw binary in CLJW_BIN, then config.edn :runtimes :cljw :binary, then every cljw on PATH, and takes the first one that provides cljw.net/connect (the TCP client the nREPL connection uses).
  2. It runs cljw -cp src -m bb-mcp.core.
  3. With no usable cljw it falls back to bb -m bb-mcp.core, unless you asked for cljw explicitly via CLJW_BIN or babashka is missing (then it exits 3).

Install cljw with brew install clojurewasm/tap/cljw, or take a release tarball from ClojureWasm releases (each one ships SHA256SUMS and a signed build attestation). v1.15.3 is the version measured above.

What differs on cljw:

cljwbb
nREPL socketcljw.net/connectjava.net.Socket
JSONcljw's clojure.data.jsoncheshire
bash toolruns inside hive-mcp: cljw has no subprocess, so the command goes over the nREPL socket the head already holds. tools/list is the same on both, but on cljw bash needs the backend up.runs locally in the head
Release noticea detached bb bb-mcp notice beside the server (it needs git ls-remote)in process
bb-mcp setup, bb test, bb doctorrun on bbrun on bb

So babashka stays a prerequisite even when every head runs on cljw.

Prerequisites

  • ClojureWasm (cljw), the default runtime (see Runtimes)
  • Babashka v1.3+ (required: bb-mcp setup, the release notice, tests, and the bb runtime)
  • ripgrep (for the grep tool)
  • hive-mcp, the JVM backend bb-mcp talks to, serving nREPL (default port 7910)

Installation

Install with curl -fsSL https://hive-mcp.com/install.sh | sh then hive setup. hive setup installs bb-mcp, publishes it at ~/.local/share/hive-mcp/bb-mcp, and registers hive with your MCP clients (Claude Code, Codex) through bb-mcp. Already have bb-mcp checked out somewhere? Run <that checkout>/bb-mcp setup: wherever bb-mcp lives becomes the reference. Verify with bb-mcp setup --check.

The reference: one anchor for every client

Wherever bb-mcp is checked out is the reference. bb-mcp setup publishes it at one well-known anchor, a symlink:

~/.local/share/hive-mcp/bb-mcp  ->  <your bb-mcp checkout>

Every MCP client launches hive through the anchor, never through a user-specific checkout path, so moving or switching the checkout is one rerun of bb-mcp setup and no client config changes.

bb-mcp setup

<checkout>/bb-mcp setup [--client claude|codex|all] [--scope user|project] \
                        [--port N] [--dry-run] [--check] [--json]
# or, from the checkout:
bb setup [same options]

Every step is idempotent and reported as a row (ok, changed, planned, skipped, refused, failed):

StepWhat it does
anchorCreates ~/.local/share/hive-mcp/ and the symlink to this checkout. A symlink pointing elsewhere is replaced and its previous target reported. A real directory or file at the anchor is refused, never deleted.
claude:userDefault when the claude CLI is on PATH: claude mcp remove --scope user hive, then claude mcp add --scope user hive -- ~/.local/share/hive-mcp/bb-mcp/start-bb-mcp.sh (with -e BB_MCP_NREPL_PORT=N under --port).
claude:projectWith --scope project, run from a project dir: writes or merges .mcp.json in the cwd with the literal ${HOME}/.local/share/hive-mcp/bb-mcp/start-bb-mcp.sh command, keeping every other server.
codexWhen ~/.codex exists (or --client codex): upserts [mcp_servers.hive] in ~/.codex/config.toml with the expanded anchor path, keeping every other table and comment, after backing the file up once as config.toml.bak-<timestamp>.

Your own env vars on an existing hive entry (for example BB_MCP_RUNTIME) are kept. HIVE_MCP_DIR is removed (bb-mcp never reads it), and BB_MCP_NREPL_PORT is set only when you pass --port. --dry-run prints the plan, ops included, without writing anything.

bb-mcp setup --check verifies that the anchor resolves to a directory with an executable start-bb-mcp.sh, that babashka is on PATH, and that each detected client's hive entry points at the anchor (flagging stale paths such as the real checkout, another user's home, leftover HIVE_MCP_DIR or pinned args). It also reports whether an nREPL answers on the resolved port (a warning only), and whether a newer bb-mcp release exists (an update row, also a warning only). It exits non-zero when a check fails.

Updates

Clients launch bb-mcp through the anchor, so an update is a fast-forward of the checkout it points at: hive update (refuses a checkout with local changes or off its tracked branch, then runs bb-mcp setup --check), or git pull in the checkout. On startup bb-mcp compares its VERSION with the newest v* release tag of https://github.com/hive-agi/bb-mcp and, when behind, prints one line on stderr (never stdout, the MCP channel):

bb-mcp 1.2.29 -> 1.2.30 available: run hive update (or git pull in <checkout>)

The lookup (git ls-remote, bounded to 2 s) runs in the background and never delays or breaks startup; offline it says nothing. The answer is cached for 24 h in ${XDG_CACHE_HOME:-~/.cache}/hive-mcp/bb-mcp-latest.edn. On the cljw runtime start-bb-mcp.sh runs it as bb-mcp notice beside the server. BB_MCP_NO_UPDATE_CHECK=1 turns it off.

Manual configuration

For a client that expands env vars in its JSON config (Claude Code's .mcp.json does):

{
  "mcpServers": {
    "hive": {
      "command": "${HOME}/.local/share/hive-mcp/bb-mcp/start-bb-mcp.sh"
    }
  }
}

For a client without expansion, write the expanded absolute path of the anchor (/home/<you>/.local/share/hive-mcp/bb-mcp/start-bb-mcp.sh), never the path of the real checkout. No args are needed, and env only when non-default. See examples/mcp.json.example.

The backend

bb-mcp is a client: it connects to an nREPL the hive-mcp JVM is already serving, and it does not spawn or respawn one. Start the hive-mcp backend (nREPL on 7910 by default) yourself; hive setup documents how. When it is down, every tool call fails with could not connect to 'localhost:7910' until the backend is started. That message means the backend, not the arguments of the call that reported it.

Configuration

Project and nREPL port resolution

start-bb-mcp.sh takes no arguments. The project directory defaults to the client's cwd, and the nREPL port is resolved in this order:

  1. Explicit parameter: port in a tool call
  2. Environment variable: BB_MCP_NREPL_PORT
  3. .nrepl-port file: in BB_MCP_PROJECT_DIR (the client's cwd by default)
  4. Default: port 7910 (hive-mcp)

Environment Variables

VariableDescriptionDefault
BB_MCP_RUNTIMEcljw (ClojureWasm) or bb (babashka). cljw falls back to bb when no usable cljw is found.cljw
BB_MCP_NREPL_PORTnREPL port of the hive-mcp backend<project>/.nrepl-port, else 7910
BB_MCP_HIVE_NREPL_PORTnREPL port the forwarded hive tools, tool loading and MCP resources dial. Never read from .nrepl-port or BB_MCP_NREPL_PORT, so a project's own REPL is not mistaken for the hive host7910
BB_MCP_PROJECT_DIRProject directory (scopes tools; holds .nrepl-port)the client's cwd
BB_MCP_TOOL_SCHEMAcompact advertises each tool's core parameters and still accepts the rest (per-command params on demand via command='carto describe' of='<cmd>'); full advertises every tool parametercompact
BB_MCP_NO_UPDATE_CHECK1 turns off the startup release notice (see Updates)off
CLJW_BINcljw binary for the cljw runtimeconfig.edn :runtimes :cljw :binary, then cljw on PATH

Set these in the client's server entry only when you need a non-default value. Never put HIVE_MCP_DIR in an MCP client config: bb-mcp does not read it.

Caller identity

bb-mcp stamps every tool call it forwards to the hive with:

ArgumentSource
_caller_idcoordinator:<instance> for a coordinator session, <slave-id>:<instance> for a ling (BB_MCP_AGENT_ID, BB_MCP_CLIENT_ID or CLAUDE_SWARM_SLAVE_ID). Self-asserted; always overwrites a value the model put in the arguments.
_caller_credentialHIVE_AGENT_CREDENTIAL, the signed spawn credential the hive sets in the environment of a child it spawns. Opaque: bb-mcp never parses, logs or prints it. Present only when the variable is set and non-blank. A value the model put in the arguments is always dropped; the environment is the only source.

Do not set HIVE_AGENT_CREDENTIAL by hand or in an MCP client config: the hive mints it per spawn.

Sixth-sense channel receptor

With channels on, bb-mcp polls hive-agent's sixth sense over the same nREPL link and pushes each sense (a ling's ask, block, completion, truncation, error...) to Claude Code as a notifications/claude/channel event. The event's meta carries agent, sense_class, project, parent, sense_id and, when the sense has them, termination, error_type and reason; the content ends with the same detail compactly ([termination=max-turns]).

VariableDescriptionDefault
BB_MCP_CHANNELS1 arms the receptor (opt-in)off
BB_MCP_SENSE_PARENTParent whose lings this session hears; * or all hears every swarm on the hostThis session's caller id, the parent hive-mcp records verbatim for lings it spawns (coordinator:<instance> for a coordinator, <slave-id>:<instance> for a ling); CLAUDE_SWARM_SLAVE_ID only when no caller id is known
BB_MCP_SENSE_PROJECTSComma list of project idsall
BB_MCP_SENSE_CLASSESComma list of classes, e.g. ask,blocked,completed,truncated,errorall
BB_MCP_SENSE_POLL_MSPoll interval1000
BB_MCP_SENSE_REPLAY1 delivers the backlog from before the sessionoff

When the hive has hive-agent's per-consumer port (sense-port), the drain cursor is kept in the hive under its own consumer id, <caller-id>#channel, apart from the session's manual ss drain calls (which default to the bare caller id and would otherwise share one consume-once cursor), so a restarted bb-mcp resumes where it left off; a consumer the hive has never seen starts at the log head. On an older hive-agent it falls back to the stateless api/drain with a cursor held in bb-mcp, dropping the first backlog and re-reading from 0 whenever the hive's sense log restarts (epoch change or a cursor that went backwards).

Tools

bb-mcp serves two native tools and forwards everything else hive-mcp exposes. hive-mcp's tools are consolidated, one tool per domain with a command parameter, so the list is short: against hive-mcp on 2026-10-22, tools/list returned 11.

Native Tools (2)

Served by the head itself:

ToolDescription
bashExecute shell commands. Local on bb; on cljw it runs inside hive-mcp (cljw has no subprocess)
clojure_evalEvaluate Clojure on an nREPL: hive-mcp's by default, or any port

hive-mcp Tools (dynamic)

Loaded from hive-mcp at startup. On 2026-10-22: bb-mcp, code (carto navigation and structural editing, CIDER, analysis), memory (memory, the knowledge graph, ingestion), swarm (lings, hivemind, sixth-sense), project (kanban, workflows, transcripts), git (status, commits, ship), fs, web and multi (batched calls). Each one lists its commands with command='help'.

Dynamic Tool Loading

At startup, bb-mcp queries hive-mcp via nREPL for all available tools and creates forwarding handlers automatically. This ensures bb-mcp always has automatic parity with hive-mcp - no manual synchronization needed.

When hive-mcp adds new tools, bb-mcp picks them up automatically on next startup.

Usage

As MCP Server

MCP clients run ~/.local/share/hive-mcp/bb-mcp/start-bb-mcp.sh (see Installation). By hand, from the checkout:

# The launcher clients use (cljw, falling back to bb)
./start-bb-mcp.sh

# On cljw directly
cljw -cp src -m bb-mcp.core

# On bb
bb mcp            # or: bb -m bb-mcp.core

Project Structure

bb-mcp/
├── bb-mcp                    # Entry: MCP server, or `bb-mcp setup`
├── start-bb-mcp.sh           # Launcher every MCP client runs (via the anchor)
├── bb.edn                    # Tasks: mcp, setup, test
├── src/bb_mcp/
│   ├── core.clj              # Main entry, MCP message loop
│   ├── protocol.clj          # JSON-RPC over stdio
│   ├── setup.clj             # `bb-mcp setup` pipeline (CLI)
│   ├── setup/                # model (pure plan), clients (registry), toml, io (boundary)
│   ├── host/                 # Runtime ports (bb, cljw)
│   ├── sense/                # Sixth-sense channel receptor
│   ├── wire/                 # bencode
│   └── tools/                # bash, nrepl client, hive (dynamic tools from hive-mcp)
└── test/                     # Tests (bb test)

All hive tools are loaded dynamically from hive-mcp at runtime, so there is no static copy of them here.

Development

# Run tests
bb test

# Start an nREPL server for development
bb nrepl-server

Adding New Tools

Native tools (no JVM needed):

  1. Add to appropriate file in src/bb_mcp/tools/ (bash, file, grep)
  2. Register in src/bb_mcp/core.clj native-tools vector

Emacs tools: Add to hive-mcp instead - bb-mcp picks them up automatically via dynamic loading.

nREPL Implementation

The nREPL client (tools/nrepl.clj) uses byte-based bencode for proper UTF-8 handling. Key functions:

  • eval-code - Evaluate Clojure on remote nREPL
  • bencode-to-bytes / bdecode-from-stream - Binary-safe bencode

License

MIT

Can you improve this documentation? These fine people already did:
Pedro Gomes Branquinho & blw
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close