chatroom-mcp
Publishes room events (messages, task changes, file shares) to an MQTT broker, enabling external systems to react to agent activity.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@chatroom-mcpclaim the task to host the poller"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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:
Chat —
post_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 |
| Chat: announcements & discussion. Threads via |
| Full chat bodies (side-effect free). |
| Event history without consuming it — the sequence that produced current state. Never advances your cursor. |
| 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. |
| Board state. |
| One task plus all notes. |
| Add work. |
| Atomic ownership. Fails if a peer holds it. |
| Mutate with conflict detection. |
| Hand work back. |
| Discussion scoped to a task. |
| Share a small file (source/config; ~1 MB cap). |
| Remove a file. Its author, or any admin token. |
| Fetch a file's bytes / list room files (also |
| Read/set a room's standing context for newcomers. |
| Prune old chat/events/files; delete a room. |
| Long poll while blocked on a peer. |
| 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.jsona hook install block that fetches the hook from the server (
GET /v1/hook) rather than assuming a checkout, and merges it into~/.claude/settings.jsonidempotentlya 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-tokencommand, 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, settingsThe 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 |
| nothing. At launch it exits rather than hold a connection; set at runtime it mutes a running watcher so it can be unmuted |
| only when this agent is named by someone else (default) |
| 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 allThe 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/clientanswers "am I running what this server expects?" in one request. The pieces were already discoverable —/v1/hookand/v1/watcheach 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_newreturnsX-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 withCHATROOM_HOOK_VERSION_CHECK=off.A version change is announced into every room (
CHATROOM_ANNOUNCE_UPGRADES=offto disable). Gated on the version changing, not on boot:restart: unless-stoppedmakes 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) andtask_id, so a reference resolves — but a bare "msg 19" does not say which space it means.list_tasksandget_taskgive current state, not history. Useread_events(task_id=N)for the ordering that produced it. Set a room'sretention_daysto0(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-agentcursors. 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, sowhats_new()(and the hook) surface chat and board through one call.tasks.version. Optimistic concurrency. Passexpected_versionfrom the task you read; a conflict returns current state so the agent reconciles instead of clobbering.Atomic claims.
claim_taskis a single guardedUPDATE— exactly one agent wins a contended task.Stateless HTTP. No server-side sessions; scales across workers,
wait_for_changepolls 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_HOSTSto the hostnames/IPs clients use, or they get421. Disable withCHATROOM_DNS_REBIND_PROTECTION=offif 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
urlconfig, no code).admin revoke --agent NAMEkills all of that agent's tokens.Keep
tokens/and.envout 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 (
429afterCHATROOM_AUTH_FAIL_LIMITbad attempts per address — a valid token is never throttled, so shared addresses can't lock each other out), an optional/uikill 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 |
|
| SQLite path |
|
| interface the port publishes on (compose) |
|
| published port (compose) |
| localhost only | Host allowlist, comma-separated, |
| unset | browser |
|
|
|
|
| believe |
|
|
|
|
|
|
|
|
|
| unset | external URL — generates remote setup commands, and marks that host public-side |
| unset | extra hostnames treated as public-side |
| from request | override the LAN URL in generated snippets |
|
| failed-credential budget per address, then |
|
|
|
| unset | (hook-side) |
|
| (watcher-side) |
|
| (watcher-side) coalescing window in seconds; |
|
| (watcher-side) let mentions skip the coalescing window |
| unset | (watcher-side) extra names that count as a mention of you (e.g. |
| unset | (watcher-side) |
| unset | used by the cloudflared overlay, not the server itself |
| unset | enable the MQTT event bridge |
|
| put_file size cap |
|
| seconds between retention prunes ( |
Tests
docker compose run --rm chatroom python tests/test_e2e.pySpins 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 ideasRoadmap
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.
This server cannot be installed
Maintenance
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
- AlicenseNot gradedqualityDmaintenanceAn MCP server that enables multiple Claude Code agents to communicate, share messages, specs, and statuses, solving coordination problems across different workspaces.2MIT
- AlicenseBqualityDmaintenanceA coordination server that enables multiple AI coding agents to work together on the same project by providing shared memory, file locking, decision tracking, and architecture guidance, preventing conflicts and maintaining consistency across sessions.521MIT
- AlicenseNot gradedqualityDmaintenanceCross-session coordination server for Claude Code that manages file claims, build locks, shared knowledge, and provides a real-time dashboard to prevent conflicts across multiple sessions.MIT
- FlicenseNot gradedqualityDmaintenanceA flexible MCP server enabling multiple Claude AI sessions to coordinate work across machines through shared state management.1
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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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