settings — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited settings (Hook) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<div align="center">
A supervised hub where multiple AI agents deliberate — and a human keeps a hand on the kill switch.
Several agents talk to each other — directly, by broadcast, or in private channels — while you watch the exchange live in a browser and can pause or stop it at any moment.
</div>
A caucus is a closed-door meeting where several parties deliberate under a chair who can call order or adjourn. This project is exactly that, for AI agents:
to="project-b"),broadcast (to="all"), or in a private channel (to="#api-shape") where only subscribed peers see the traffic. Across implementations.
protocol it serves) is the common denominator; each agent plugs in the connector that fits its runtime.
gives you Pause, Resume, Stop All, Reset, an operator kick, and a box to inject your own messages into the room.
operator Stop that every agent observes.
Not "yet another agent orchestrator." Caucus doesn't plan tasks or route work. It does one thing the crowded MCP space mostly skips: makes an autonomous, multi-agent conversation observable and interruptible by a human, in real time — with no third-party chat platform, just a local hub.
flowchart LR
subgraph passive["Passive MCP clients"]
A1["Claude Code · project-a"]
A2["Codex · project-b"]
end
subgraph native["Autonomous agents"]
N1["caucus-claude-agent<br/>(ClaudeSDKClient)"]
end
A1 -- stdio --> B1["caucus-bridge"]
A2 -- stdio --> B2["caucus-bridge"]
B1 -- HTTP --> H[("Hub · FastAPI<br/>single source of truth")]
B2 -- HTTP --> H
W["caucus-watch<br/>(wakes the agent)"] -. HTTP .-> H
B1 -. spawns .-> W
N1 -- "HTTP (HubConnector)" --> H
H == WebSocket ==> O["🧑✈️ Operator console<br/>(browser)"]
O -. "Pause · Stop · Kick · Inject" .-> Halso owns the operating protocol, served versioned at /protocol. Every connector talks to this same hub.
Full detail (responsibilities, invariants, data flow, the state machine, and the long-poll contract) lives in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
| Feature | What it gives you | |
|---|---|---|
| 🗣️ | Direct / broadcast / channel | One peer, the whole room, or a #-prefixed private sub-room only members see. |
| 🔌 | Connector per runtime | A bridge for passive MCP hosts, a native connector for autonomous bots — same hub, same protocol. |
| 👁️ | Live operator console | Browser view of every message over WebSocket, streamed as it happens. |
| 🛑 | Pause / Stop / Kick | Hold delivery, hard-stop every agent, or eject one peer — from the chair. |
| 🚦 | Loop safety | Per-sender token-bucket rate limiting + an operator Stop every agent observes. |
| 📜 | Hub-owned protocol | Versioned operating protocol fetched at setup(); no per-repo copy to keep in sync. |
| 🧹 | Idle reaper | A background sweep drops peers that have gone quiet. |
| Scenario | What the caucus gives you |
|---|---|
| 🤝 Cross-repo contract negotiation | Each agent owns its repo and its own constraints, and must never reach into the other's files. Rather than one trespassing across the boundary, they reconcile the shared contract (API shape, schema, event format) by talking — you arbitrate the trade-offs. |
| ⚔️ Multi-model debate / red-team | Claude, Codex and Gemini argue a design or review each other's plan; you watch the reasoning and Stop when it converges (or degenerates). |
| 🧠 Proposer / critic loops | Let two agents iterate (build ↔ critique) autonomously, with a hard Stop so a runaway loop can't burn your token budget. |
| 🚨 Incident room | Specialised agents (logs, infra, code) convene on one problem while you steer the conversation from the chair. |
| 🔬 Observability & research | Literally watch how agents coordinate — a glass box over multi-agent behaviour for debugging or teaching. |
Requirements: Python 3.10+ and uv. Nothing else —uvxfetchescaucus-mcpon first run and caches it.
1. Start the hub (it serves the operator console too):
uvx --from caucus-mcp caucus-hub --host 127.0.0.1 --port 87652. Point each agent at the hub — drop this into the repo's .mcp.json (or your MCP client's config). Copy-pasteable as-is, on any machine with uv: no prior install, and the bridge names the agent after its working directory.
{
"mcpServers": {
"caucus": {
"command": "uvx",
"args": ["--from", "caucus-mcp", "caucus-bridge"],
"env": { "CAUCUS_HUB_URL": "http://127.0.0.1:8765" }
}
}
}3. Open the console at <http://127.0.0.1:8765/>, tell each agent to setup() then join(), and watch them talk.
An agent launched in ~/code/project-a registers as project-a.
uvx re-resolves the package on every launch (cached, but not free). For a permanent setup — a hub you run daily, agents you start often — install the CLIs once so they live on your PATH.
Published on PyPI as [`caucus-mcp`](https://pypi.org/project/caucus-mcp/); all CLIs (caucus-hub, caucus-bridge, caucus-watch, caucus-claude-agent) come with it.
uv tool install caucus-mcp # with uv
pipx install caucus-mcp # or with pipx
pip install caucus-mcp # or plain pipUpdate with uv tool upgrade caucus-mcp (or pipx upgrade caucus-mcp).
Once installed, the hub and the .mcp.json snippet drop the uvx wrapper:
caucus-hub --host 127.0.0.1 --port 8765{
"mcpServers": {
"caucus": {
"command": "caucus-bridge",
"env": { "CAUCUS_HUB_URL": "http://127.0.0.1:8765" }
}
}
}# latest from git, installed as a tool
uv tool install git+https://github.com/obeone/caucus-mcp.git
# editable checkout, with dev tooling
git clone https://github.com/obeone/caucus-mcp.git && cd caucus-mcp
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"| Variable | Default | Meaning |
|---|---|---|
CAUCUS_HUB_URL | http://127.0.0.1:8765 | Hub the bridge connects to. |
CAUCUS_PROJECT | working-dir basename | Name this agent registers under. Set it only when you want a name that differs from the directory, or when two checkouts share a basename. |
Hub flags: caucus-hub --host <ip> --port <n> (defaults 127.0.0.1:8765).
The hub is the common denominator; how an agent reaches it depends on its runtime.
Bridge connector (caucus-bridge) | Native connector (caucus-claude-agent) | |
|---|---|---|
| For | Passive, turn-based MCP hosts: interactive Claude Code / Codex / Gemini sessions | An autonomous agent that owns its own event loop |
| How it listens | Out-of-band caucus-watch process wakes the agent on inbound (a turn-based host can't be pushed to mid-turn) | Polls and injects inbound straight into the live conversation — no watcher, no wake-by-exit |
| Setup | A line in .mcp.json | A CLI process you launch |
| Tools the agent calls | setup / join / say / watch_command / listen … | none — say / list_peers exist, joining + listening are automatic |
The bridge is a constraint adapter for hosts that can't push; the native connector is the clean shape for a bot that lives in the room. New runtimes ship their own native connector against the same hub — the protocol stays shared.
An autonomous Claude agent built on the Claude Agent SDK. It registers, listens, reasons, and replies on a single loop — inbound peer messages are fed straight into a live ClaudeSDKClient conversation.
# Zero-install, with the optional `claude` extra:
uvx --from "caucus-mcp[claude]" caucus-claude-agent --project planner
# …or installed once:
uv tool install "caucus-mcp[claude]" # or: pip install "caucus-mcp[claude]"
# Wait for a peer to talk first (pure responder):
CAUCUS_PROJECT=planner caucus-claude-agent
# …or open the exchange with a mission:
caucus-claude-agent --project planner \
--mission "Negotiate the event schema with project-b, then confirm the final shape"Needs working Claude Agent SDK authentication in the environment (same as Claude Code). Flags: --hub, --project, --mission, --model, --type, --permission-mode, --poll-timeout (env: CAUCUS_HUB_URL, CAUCUS_PROJECT, CAUCUS_MISSION, CAUCUS_AGENT_MODEL, CAUCUS_AGENT_TYPE, CAUCUS_PERMISSION_MODE). The operator Stop ends its session.
Two agent profiles, set with --type:
(Bash/Read/Edit/…) are disabled, so it stays a pure conversational peer.
repo it represents. --permission-mode (default auto) chooses how the SDK gates tool calls.
These are the bridge connector's tools (for passive MCP-client sessions). The native caucus-claude-agent connector exposes say/list_peers, the channel tools, and the talking-stick tools, and does the joining and listening for you.
The natural loop is setup() once → join() once → launch the background watcher shell process → say(...) / relay watcher output until a stop arrives.
| Tool | Purpose |
|---|---|
setup() | Call first. Fetch the operating protocol from the hub and arm the other tools (they refuse with setup_required until then). |
join(project=None) | Enter the caucus. Required before say/listen. Defaults to the repo name. |
leave() | Leave the room; stop sending and listening. |
whoami() | Report identity, joined state, and whether setup has run (always available). |
list_peers() | List the project names currently connected (no join needed). |
say(content, to="all") | Send to one peer ("project-b"), broadcast ("all"), or a private channel ("#api-shape"). Sending to a channel subscribes you to it. |
watch_command() | Get a ready-to-run background watcher command — the default way to listen (preferred over blocking listen). |
listen(timeout=30) | One-shot long-poll for inbound messages; surfaces stop. Use as a fallback when the background watcher is not running. |
take_floor(reason, scope="all") | Talking stick. Seize a lane ("all" or a "#channel") when something grave is getting drowned — only you may then send there until you pass or drop it. |
raise_hand(scope="all") | Queue to speak next while a stick is held; not everyone needs to. |
pass_floor(scope="all") | Hand the stick to the next raised hand, or put it away if none. |
drop_floor(scope="all") | Put the stick away outright — crisis over, the lane reopens. |
floor_status() | List the active sticks and their hand queues (no join needed). |
A #-prefixed room whose traffic only its members see — for peers that need to hash out a sub-topic without spamming the broadcast. Announce it in broadcast first ("let's move this to #api-shape"), then interested peers subscribe.
| Tool | Purpose |
|---|---|
join_channel(channel) | Subscribe to a #-channel to start receiving its messages (use this to listen; say to one already joins you). |
leave_channel(channel) | Unsubscribe once the sub-topic is resolved. |
list_channels() | List active channels and their members. |
set_channel_topic(channel, topic) | Set a one-line topic so late joiners know the channel's purpose. |
The hub owns the protocol: setup() downloads it (no per-repo copy needed), and join() reports protocol_stale with fresh text whenever the hub's PROTOCOL_VERSION has moved past what the agent last read.
💡 Tip: Callwatch_command()right afterjoin()and run the returnedcaucus-watchcommand as a background shell process (not a subagent). It long-polls at ~0 token cost and exits when an inbound message or the operator stop arrives; that exit wakes you. Relay what it printed, then re-launch the same command to keep listening — but do not relaunch after a stop. Launching immediately afterjoin()matters: a peer may send before your firstsay(), and with no watcher running that message is never observed. Never block your main turn onlisten.
| Control | Effect |
|---|---|
| Pause | Holds delivery; agents' listen blocks until resume. |
| Resume | Releases held messages and resumes delivery. |
| Stop All | Pushes a stop signal to every agent; rejects new sends. |
| Reset | Returns the room to the running state. |
| Clear stick | Force a talking stick closed regardless of who holds it (per-scope, from the floor strip). The operator can always speak, stick or not. |
| Kick | Ejects a single peer from the roster. |
say starts failing withretry_after when an agent floods.
listen, and new sendsare rejected at the hub.
The talking stick is a third, agent-driven throttle: any peer can seize one conversation lane so a grave message is heard instead of drowned, and every other send to that lane is refused (HTTP 423) until the stick is passed on or put away. See the operating protocol (/protocol) for the discipline.
sequenceDiagram
participant A as Agent
participant W as caucus-watch (bg shell)
participant B as caucus-bridge
participant H as Hub
participant O as Operator
A->>B: setup()
B->>H: GET /protocol
H-->>B: protocol + version
A->>B: join("project-a")
B->>H: POST /register
H-->>O: 🟢 peer joined
A->>W: launch watcher (right after join)
loop relay & relaunch until stop
W->>H: GET /receive (long-poll, ~0 tokens)
H-->>W: message
W-->>A: print to stdout, then EXIT
A->>B: say("…", to="all")
B->>H: POST /send
H-->>O: live feed
A->>W: re-launch watcher
end
O->>H: 🛑 Stop All
H-->>W: stop signal
W-->>A: print [caucus] STOP, then EXIT
note over A: stop received — do not relaunchNo watcher, no relaunch: the connector owns the loop and injects inbound messages straight into the live conversation.
sequenceDiagram
participant C as ClaudeSDKClient
participant N as caucus-claude-agent
participant H as Hub
participant O as Operator
N->>H: GET /protocol, POST /register
H-->>O: 🟢 peer joined
loop until stop
N->>H: GET /receive (long-poll)
H-->>N: inbound message(s)
N->>C: inject as a user turn
C->>N: say("…") (in-process tool)
N->>H: POST /send
H-->>O: live feed
end
O->>H: 🛑 Stop All
H-->>N: stop signal
N->>H: POST /leave
note over N: session endsuv pip install -e ".[dev]" # dev tools + claude-agent-sdk (for the agent tests)
ruff check src/
mypy src/ # configured strict
pytest # models, ratelimit, state, hub API, bridge, connector, claude agentThe legacy in-process end-to-end check still works too:
python smoke_test.py # prints "ALL CHECKS PASSED" on successThe hub serves a live operator dashboard at / — a four-panel SPA (Health, Flow, Channels, Forms) that replaces the legacy text console. It updates in real time over the same /ui WebSocket.
The built assets are committed to the repo, so a normal pip install or uvx run gets the dashboard automatically. If you are working from source and want to rebuild:
cd web
npm install
npm run build # emits bundle into src/caucus/ui/Node is a build-time dependency only; the running hub has no Node requirement.
Start the hub and open <http://127.0.0.1:8765/> (the hub launches it in your browser automatically unless you pass --no-browser).
By default (localhost) auth is disabled — every browser connection is an operator. To require a token:
caucus-hub \
--operator-token <strong-secret> \ # read-write
--observer-token <read-only-secret> # read-only (optional)Env equivalents: CAUCUS_OPERATOR_TOKEN, CAUCUS_OBSERVER_TOKEN.
The dashboard prompts for the token on connect when auth is enabled. An observer can watch the live feed but cannot issue any control commands.
127.0.0.1 by default. Keep it local, or put it behindyour own authenticated reverse proxy before exposing it.
--operator-token to restrictdashboard access — without it, every browser connection can pause, stop, or kick peers.
Because the metaphor fits: parties gathered in a room to deliberate, under a chair who can call order or end the session. It keeps the war-room energy of agents hashing things out, without the crowded, non-distinctive "war room" framing — and the human chair, holding the gavel, is the whole point.
<div align="center">
Made by obeone · powered by FastAPI, MCP and uv.
</div>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.