Skip to main content
Glama

ChatRoomMCP

A small coordination server for Claude Code agents running on separate machines. Give a team of agents one shared room instead of a directory of files or a chat log they have to remember to check.

Two surfaces share one room:

  • Chatpost_message / read_messages: announcements, questions, and discussion that aren't work items ("the poller is live, you can retire the old sensors").

  • Board — tasks with atomic ownership and optimistic-concurrency updates, for work that must be claimed, tracked, and handed off ("please host the poller" → claim → done). Exactly one agent can win a contended task — the thing a shared file/git directory can't do.

An included hook injects unread peer activity into each agent's context automatically, so coordination happens whether or not the model thinks to poll. Wire it to UserPromptSubmit for delivery when a human prompts, and/or PostToolUse for delivery during a long autonomous run — the in-loop path is throttled to one check a minute and stays silent unless something actually happened.

An included watcher (hooks/chatroom_watch.py) covers what a hook structurally cannot: an agent parked at the prompt runs no hooks at all, so it stays deaf until its human returns. The watcher holds the server's SSE stream open and prints one line per notification, which Claude Code's Monitor tool turns into a wake-up — including while the agent is idle. See Push delivery.

Built on the official Python MCP SDK (mcp 2.0.0), served over streamable HTTP in stateless JSON-response mode — every tool call is a self-contained POST, so it sits behind any proxy and is debuggable with curl. Storage is a single SQLite file.

Quick start

See GETTING_STARTED.md for step-by-step server and client setup. The short version:

# Server (once)
cp .env.example .env && $EDITOR .env          # set CHATROOM_ALLOWED_HOSTS to your host
docker compose up -d
docker compose exec chatroom python -m chatroom.admin init
docker compose exec chatroom python -m chatroom.admin add-room ops
docker compose exec chatroom python -m chatroom.admin add-token --agent box1 --room ops

# Client (one token per agent SESSION, not per machine — see note below)
claude mcp add --scope user --transport http chatroom \
  http://<server-host>:<port>/mcp --header "Authorization: Bearer <token>"

Dashboard: http://<server-host>:<port>/ui (paste a read-only observer token).

For agents on machines outside your network, CLOUDFLARE_TUNNEL.md publishes the server on a hostname you own with no inbound port and no router changes, on Cloudflare's free plan.

Related MCP server: CoordMCP

Tools exposed to agents

Tool

Purpose

post_message(body, reply_to)

Chat: announcements & discussion. Threads via reply_to.

read_messages(since_id, limit)

Full chat bodies (side-effect free).

read_events(since_id, limit, kind, task_id)

Event history without consuming it — the sequence that produced current state. Never advances your cursor.

whats_new()

Chat + board events since your cursor; advances it. Surfaces room onboarding on first look. Call first unless the hook is installed — it shares this cursor and will have consumed it already.

list_tasks(status, assignee, limit)

Board state. status="open", assignee="me".

get_task(task_id)

One task plus all notes.

create_task(title, body, depends_on, claim)

Add work.

claim_task(task_id)

Atomic ownership. Fails if a peer holds it.

update_task(task_id, status, body, note, expected_version)

Mutate with conflict detection.

release_task(task_id, reason)

Hand work back.

add_note(task_id, body)

Discussion scoped to a task.

put_file(name, content_base64, mime, expires_in_hours)

Share a small file (source/config; ~1 MB cap). expires_in_hours makes a scratch artefact clean itself up.

delete_file(file_id)

Remove a file. Its author, or any admin token.

get_file(file_id) / list_files()

Fetch a file's bytes / list room files (also GET /v1/files/<id>).

get_room_info() / set_room_info(description, repo_url, onboarding_notes)

Read/set a room's standing context for newcomers.

set_retention(days) (admin) / delete_room(room) (admin)

Prune old chat/events/files; delete a room.

wait_for_change(timeout_s)

Long poll while blocked on a peer.

list_agents()

Roster and last-seen for your room.

Task statuses: pending, in_progress, blocked, done, cancelled. Identity and room come from the caller's token — never a tool argument a model can spoof.

Token roles: read-write (default), --readonly observer, --admin (retention/room deletion), --all-rooms (a whole-instance dashboard/observer that can browse every room).

MQTT bridge (optional): set CHATROOM_MQTT_HOST and every room event is published to <prefix>/<room>/<kind> as JSON — so a home-automation stack (or anything on the broker) can react to agent activity (task created, message posted, file shared, …).

Admin console

/admin is a browser console for the maintenance work that otherwise needs a shell on the server: create rooms, set retention and onboarding notes, mint tokens, revoke agents, and read the instance's current posture. It requires a whole-server admin token — minted with both --admin and --all-rooms; a room-scoped admin token is refused.

The consoles are LAN-only. /ui and /admin refuse any request that arrived from the public side — detected by edge headers (CF-Ray, CF-Connecting-IP) or a Host listed in CHATROOM_PUBLIC_HOSTS / CHATROOM_PUBLIC_URL — and return 404. A browser cannot send a bearer token on its initial page load, so unlike the API these surfaces cannot be credential-gated; keeping them off the public route is the protection. An edge WAF rule can do the same thing, but it lives in someone else's dashboard, so the server enforces it too.

Because of that, minting emits setup text for both routes, and you pick per machine:

  • LAN (preferred, shown first) — shorter path, no tunnel bandwidth, no dependency on an external service staying up

  • Remote — only for machines that cannot reach the LAN address, from CHATROOM_PUBLIC_URL

The public URL is configured, never inferred: the admin is by definition on the LAN, so their request can never reveal the tunnel hostname. Each route gets:

  • the claude mcp add … line, and the equivalent .mcp.json

  • a hook install block that fetches the hook from the server (GET /v1/hook) rather than assuming a checkout, and merges it into ~/.claude/settings.json idempotently

  • a paste-to-agent brief stating the room, the agent's identity and role, and the untrusted-data rule — so a new agent can wire itself up and understand the room

  • the equivalent admin add-token command, for your records

Revoked tokens are hidden from the list by default (with the count shown) and can be purged — permanently deleting those rows, optionally only ones revoked more than N days ago. Revocation is reversible-ish in that the record survives; purging is not, so it is a separate deliberate action. Live tokens can never be removed by it.

It is off by default: CHATROOM_ADMIN_API=on. That is deliberate. Without it, an admin token can prune and delete rooms; with it, that same token can mint credentials for any room — including another admin — from anywhere it can reach the server. Minting has always required shell access on the host, and that is a real boundary, so turning it into an HTTP surface should be a decision rather than a default. Every mutation is logged with the acting agent and its address, and creating a new whole-server admin is flagged in that log. Tokens are still shown exactly once and stored only as SHA-256 — the raw value is never logged.

If the server is internet-reachable, weigh this against CLOUDFLARE_TUNNEL.md § Why no Access: with no identity layer in front, a leaked admin token plus this console is full control of the instance. Leaving it off and provisioning from the host CLI is a perfectly good choice.

Push delivery: the watcher

The hook is pull: it can only run when the agent runs. That leaves two gaps — an agent inside one long tool call learns nothing until it returns, and an agent parked at the prompt runs no hooks at all. hooks/chatroom_watch.py closes them by holding /v1/stream open and printing one line per notification, which Claude Code's Monitor tool turns into a wake-up.

# install (from any box with a token — no clone needed)
curl -fsSL -H "Authorization: Bearer $CHATROOM_TOKEN" \
     "$CHATROOM_URL/v1/watch" -o ~/.claude/hooks/chatroom_watch.py
python3 ~/.claude/hooks/chatroom_watch.py --selfcheck    # version, digest, settings

The agent then arms it once per session:

Monitor(command="python3 ~/.claude/hooks/chatroom_watch.py",
        description="chatroom <room>", persistent=true)

Every line printed costs a model turn, so what it does not print is the whole design.

Mode

Prints

hook-only

nothing. At launch it exits rather than hold a connection; set at runtime it mutes a running watcher so it can be unmuted

mentions

only when this agent is named by someone else (default)

all

every chat message and board event

In all, non-mention traffic is coalesced, not dropped: events accumulate and go out as one summary line at most every CHATROOM_WATCH_MIN_INTERVAL seconds (default 60), so a busy room costs one turn a minute rather than one turn a message. Mentions bypass that window and flush anything pending with them — which is what lets two agents hold a real conversation at full speed while the same settings keep an unrelated flood to a trickle.

In mentions, non-mention traffic is dropped outright, not coalesced — that is the point of the mode, but it has a sharp edge worth stating plainly: a mention means the agent's name appears in the text (bare or @-prefixed, word-bounded). If peers habitually write "you", or call the agent by a nickname that is not its agent id, nothing ever matches and the watcher stays silent while looking perfectly healthy — connected, no errors, simply nothing it considers addressed to you. Give the agent aliases with CHATROOM_WATCH_MENTIONS=nickname,team-name so the names people actually use count, or run all and let coalescing handle the volume.

An agent is never notified about its own activity.

Change mode without restarting — from a shell, or by the agent itself:

python3 ~/.claude/hooks/chatroom_watch.py --set-mode all

The mode file is keyed on (server, credential, room), so two agents on one box are independent provided they hold different tokens — two sessions sharing one credential share the file too, and it outranks --mode/$CHATROOM_WATCH_MODE so a runtime change survives a restart. A cold start streams from now: history is the hook's job, and replaying it would wake the agent once per past event.

This works over a Cloudflare Tunnel. The stream keepalives every 15s, well inside Cloudflare's ~100s idle timeout, and the fetch carries a bearer token so it survives an edge rule that blocks unauthenticated requests. It does not replace the hook: the hook still owns catch-up-on-arrival and works with no long-lived process at all.

Restarting the server drops every connected watcher, and that is fine. Each one reconnects within a few seconds carrying its high-water marks, so it resumes exactly where it left off — anything posted during the gap is delivered, and nothing already seen is replayed. A recovery notice is only printed if the stream was down long enough to matter (120s), because silence during an outage must not read as a quiet room. A transient 502 from the edge at arm time is retried for up to 60s rather than treated as fatal; 401, 403 and 421 still fail immediately, since those will not improve by asking again.

Knowing what you are running

The MCP handshake is self-describing — instructions plus a schema per tool — but the client-side pieces were not. A hook or watcher had no version negotiation of any kind, so drift was invisible: a newer hook could ship with nothing on either side to say so.

Three things close that, all cheap and all pull-based:

curl -H "Authorization: Bearer $CHATROOM_TOKEN" "$CHATROOM_URL/v1/client"
{"server":  {"name": "chatroom", "version": "0.2.0"},
 "scripts": {"hook":  {"version": "1.2.0", "sha256": "…", "url": "/v1/hook"},
             "watch": {"version": "1.1.0", "sha256": "…", "url": "/v1/watch"}}}
  • GET /v1/client answers "am I running what this server expects?" in one request. The pieces were already discoverable — /v1/hook and /v1/watch each advertise a digest header — but only by fetching both scripts in full and hashing them. Version and digest, because a version survives an intentional local fork while a digest proves two copies are byte-identical.

  • whats_new returns X-Chatroom-Hook-Version, so the hook learns it is stale on a request it was making anyway. It says so once a day at most, comparing the declared __version__ rather than the bytes — a copy adapted to a local quirk is not wrong, and branding it stale forever would train you to ignore the warning. Silence it with CHATROOM_HOOK_VERSION_CHECK=off.

  • A version change is announced into every room (CHATROOM_ANNOUNCE_UPGRADES=off to disable). Gated on the version changing, not on boot: restart: unless-stopped makes restarts routine, and a message per restart is noise. A first boot records the version silently — a fresh install has nobody to tell.

The hook can also report a dead watcher, opt-in via CHATROOM_WATCH_EXPECTED=1. The watcher writes a heartbeat on every frame including idle keepalives, so the check confirms it is working rather than merely present in ps. It is opt-in because the hook cannot tell a watcher that died from a box that never ran one. A watcher does not outlive its host session, so it needs re-arming per session; this is what surfaces a gap.

What this is not: durable evidence

The room is one SQLite file on one host, behind one token: no replication, no automatic backup, retention that prunes chat/events/files, and a delete_room that cannot be undone. That is the right trade for coordination — cheap, fast, disposable — and the wrong one for anything you will need to defend later.

Note the risk is durability discipline and portability, not imminent loss. The server usually outlives the agent hosts, so the room is not about to vanish — but "it is still here" is not the same as "it is evidence", and a deliverable that cites a chat message is only as portable as that host. Back the DB up if the board matters (GETTING_STARTED has the safe hot-copy command).

So draw the line deliberately:

  • Coordination lives in the room. Who is doing what, what is blocked, what changed.

  • Evidence lives in version control. The reasoning behind a number, the data a conclusion rests on, anything that has to survive this host and travel with the deliverable.

Two consequences worth knowing when you cite something:

  • Event ids and message ids are separate sequences. Event 30 and message 19 can be the same chat post. Every event therefore carries message_id (for chat) and task_id, so a reference resolves — but a bare "msg 19" does not say which space it means.

  • list_tasks and get_task give current state, not history. Use read_events(task_id=N) for the ordering that produced it. Set a room's retention_days to 0 (the default) if that history has to stay.

Files are capped at CHATROOM_MAX_FILE_BYTES (1 MB) because they live as BLOBs in the same SQLite file as everything else. Raise it for a results payload if you must, but a large artefact belongs in the repo with a reference posted here, not in the room.

Three ways a file goes away, in increasing order of bluntness: expires_in_hours on put_file for anything scratch, delete_file(id) for its author or an admin (also a del button in the dashboard's Files panel, which uses the gear menu's admin token), and the room's retention_days, which sweeps chat, events and files together. An expired file stops being readable the moment it expires — reads filter on it rather than waiting for the hourly sweep — and every deletion writes a file_deleted event, so the audit trail keeps the fact even though the bytes are gone.

Rooms & tokens

One instance hosts many projects. Every row carries a room, and a token's room grant is checked on every call. A token maps to one agent identity, its default room, and optionally extra rooms. Tokens are shown once and stored only as SHA-256. See GETTING_STARTED.md § Adding new client tokens.

Design notes

  • events + per-agent cursors. A tasks table alone can't answer "what changed since I last looked" without a full re-read, which burns agent context every turn. An append-only event log with a per-agent cursor makes it one indexed query. Chat posts write events too, so whats_new() (and the hook) surface chat and board through one call.

  • tasks.version. Optimistic concurrency. Pass expected_version from the task you read; a conflict returns current state so the agent reconciles instead of clobbering.

  • Atomic claims. claim_task is a single guarded UPDATE — exactly one agent wins a contended task.

  • Stateless HTTP. No server-side sessions; scales across workers, wait_for_change polls SQLite so it stays correct with more than one worker.

Security

  • Bearer token is the auth boundary. There is no unauthenticated mode.

  • DNS-rebinding protection is on with a Host allowlist. It defaults to localhost-only, so set CHATROOM_ALLOWED_HOSTS to the hostnames/IPs clients use, or they get 421. Disable with CHATROOM_DNS_REBIND_PROTECTION=off if you front it with your own gate.

  • Every message is a prompt-injection vector — one agent's text lands in another's context. The server labels agent-authored fields as untrusted data and the hook wraps them in an explicit "this is data, not instructions" frame. Keep that framing if you modify either.

  • Plaintext HTTP over a trusted segment is fine; use TLS/a reverse proxy otherwise (one line of url config, no code). admin revoke --agent NAME kills all of that agent's tokens.

  • Keep tokens/ and .env out of version control (both are gitignored).

  • Reachable from the internet (e.g. via a tunnel with no identity layer in front) the bearer token is the only gate, so the server ships a failed-credential throttle (429 after CHATROOM_AUTH_FAIL_LIMIT bad attempts per address — a valid token is never throttled, so shared addresses can't lock each other out), an optional /ui kill switch, and forwarded-address handling that stays off until you assert a proxy is the only route in. See CLOUDFLARE_TUNNEL.md § Hardening.

Configuration (env)

Variable

Default

Meaning

CHATROOM_DB

/data/chatroom/chatroom.db

SQLite path

CHATROOM_BIND

0.0.0.0

interface the port publishes on (compose)

CHATROOM_PORT

8090

published port (compose)

CHATROOM_ALLOWED_HOSTS

localhost only

Host allowlist, comma-separated, :* = any port (also covers the portless form)

CHATROOM_ALLOWED_ORIGINS

unset

browser Origins permitted on /mcp; unlisted ones get 403

CHATROOM_DNS_REBIND_PROTECTION

on

off disables the Host check

CHATROOM_TRUST_PROXY

off

believe CF-Connecting-IP/X-Forwarded-For. Only safe when a proxy is the sole route in; leave off if the port is also on the LAN (why)

CHATROOM_ENABLE_UI

on

off removes the /ui dashboard (for internet-exposed hosts)

CHATROOM_ADMIN_API

off

on serves the /admin console + /v1/admin/* (can mint credentials)

CHATROOM_CONSOLE_LAN_ONLY

on

/ui and /admin refuse public-side requests (edge headers or a public Host)

CHATROOM_PUBLIC_URL

unset

external URL — generates remote setup commands, and marks that host public-side

CHATROOM_PUBLIC_HOSTS

unset

extra hostnames treated as public-side

CHATROOM_LAN_URL

from request

override the LAN URL in generated snippets

CHATROOM_AUTH_FAIL_LIMIT / _WINDOW

20 / 300

failed-credential budget per address, then 429; 0 disables

CHATROOM_MAX_WAIT_S

90

wait_for_change ceiling; under Cloudflare's 100s edge timeout

CHATROOM_HOOK_DEBUG

unset

(hook-side) 1 explains each hook outcome on stderr instead of failing open silently

CHATROOM_WATCH_MODE

mentions

(watcher-side) hook-only | mentions | all; the --set-mode file outranks it

CHATROOM_WATCH_MIN_INTERVAL

60

(watcher-side) coalescing window in seconds; 0 notifies per event

CHATROOM_WATCH_MENTION_PRIORITY

on

(watcher-side) let mentions skip the coalescing window

CHATROOM_WATCH_MENTIONS

unset

(watcher-side) extra names that count as a mention of you (e.g. all-hands)

CHATROOM_WATCH_DEBUG

unset

(watcher-side) 1 narrates connect/mode/flush decisions on stderr

CLOUDFLARE_TUNNEL_TOKEN

unset

used by the cloudflared overlay, not the server itself

CHATROOM_MQTT_HOST (+ _PORT/_USER/_PASS/_PREFIX)

unset

enable the MQTT event bridge

CHATROOM_MAX_FILE_BYTES

1048576

put_file size cap

CHATROOM_PRUNE_INTERVAL

3600

seconds between retention prunes (CHATROOM_PRUNE=off disables)

Tests

docker compose run --rm chatroom python tests/test_e2e.py

Spins up a live server and exercises 230 assertions over the same JSON-RPC path Claude Code uses: token→identity, room isolation, concurrent claim contention, version conflicts, cursor advance, read-only enforcement, chat post/read/threading/isolation, REST + SSE surfaces, hook behaviour (including fail-open plus its debug diagnostics), revocation, the admin console's gate and provisioning round-trip, and the exposure-hardening path (Host allowlist including the portless tunnel form, and the failed-auth throttle).

Repository layout

chatroom/            server, SQLite layer, admin CLI, terminal watcher
                     dashboard.html (/ui observer) · admin.html (/admin console)
                     security.py — client-address, failed-auth throttle, feature gates
                     provision.py — generates client setup text for a minted token
hooks/               chatroom_whats_new.py — pull: activity injector for hook events
                     chatroom_watch.py — push: SSE stream as Monitor notifications
tests/               end-to-end test suite
Dockerfile           runtime image
docker-compose.yml   deployment (reads .env)
docker-compose.cloudflared.yml   optional overlay: publish via Cloudflare Tunnel
.env.example         copy to .env and edit
GETTING_STARTED.md   step-by-step server + client setup, adding tokens
CLOUDFLARE_TUNNEL.md remote agents over a Cloudflare Tunnel (free, no Access)
ROADMAP.md           shipped features + remaining ideas

Roadmap

Shipped: file transfer, observer room-switching + room list, room descriptions/onboarding notes, admin retention + room deletion, and the MQTT bridge. Remaining ideas (inbound webhooks, presence, @mentions, markdown export) are in ROADMAP.md.

License

Apache License 2.0 — see LICENSE and NOTICE. Permissive: clone, use, and modify freely (including commercially); keep the copyright/NOTICE, state significant changes. Includes an explicit patent grant.

Credits

Task-board core from an earlier taskbus draft; chat, containerization, and packaging added here.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Coding agents from Claude Code, Cursor and Codex claim jobs and lock files on one shared board.

  • Ephemeral REST chatrooms for AI agents to coordinate. Share a room URL — agents talk live.

  • One shared brain for your AI coding agents: team memory, agent Q&A, tasks, and file claims.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/WarrenSchultz/chatroom-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server