Local MCP server for letting multiple coding agents in the same repository coordinate through a shared chat log.
It is a small stdio server written in Python. Each client runs its own process. Shared state lives on disk in .chatroom/, so agents can discover each other, send direct or broadcast messages, and read the conversation history.
- Install the only dependency:
python3 -m pip install mcp-
Configure your MCP client to launch
chatroom_mcp_server.py. See Configure Claude Code or Configure Codex CLI. -
Start your agents in the same repository.
-
Have each agent call
joinwith a unique stable name such asclaudeorcodex. -
On resumed sessions, call
get_handoff(name=...)before replaying chat history.
- Live participant names are exclusive. If one running process has joined as
codex, a second running process cannot also join ascodex. - Process-exit cleanup is best-effort. Hard-killed agents can leave stale participants behind.
- By default, the server and monitor resolve the chatroom root from the directory containing their script files. If your client launches elsewhere, use an absolute path for
chatroom_mcp_server.pyor setCHATROOM_ROOT=/absolute/path/to/repo. joinonly registers presence. It does not instruct other agents how to behave, subscribe them to updates, or make them automatically respond.send_messageonly appends to the shared log. There is no push delivery, interrupt, or wake-up mechanism; another agent sees the message only when it reads viaget_handoff,read_unread, orread_messages.- Messages sent to a named participant are a visibility convention, not a security boundary. The log is still stored locally on disk, and unfiltered reads can still see those entries.
send_messagedoes not require the sender or recipient to be currently joined. A message toto="alice"is still appended even ifaliceis not active.
- registers active participants
- sends broadcast messages to
all - sends direct messages to a named participant
- reads messages after a given message ID
- tracks per-participant unread cursors
- stores explicit handoff summaries
- returns compact handoff state for resumed sessions
- lists active participants
- reports basic chatroom status
This is local-only. No network transport, auth, or encryption.
- Python 3
mcpPython package
Runtime state is created lazily on first use:
.chatroom/messages.jsonl.chatroom/participants.json.chatroom/cursors.json.chatroom/summaries.jsonl
The server also adds .chatroom/ to .gitignore if it is not already present.
Shared repository guidance lives in AGENTS.md.
If you want private, operator-specific agent instructions, copy AGENTS.local.example to AGENTS.local.md. That local file is gitignored and should not contain secrets.
Create a .mcp.json file in the project root:
{
"mcpServers": {
"chatroom": {
"command": "python3",
"args": ["chatroom_mcp_server.py"]
}
}
}Note: Do not put MCP server config in
.claude/settings.json— that file is committed to the repo and shared across collaborators..mcp.jsonis the standard per-project MCP configuration file for Claude Code.
Add this to .codex/config.toml:
[mcp_servers.chatroom]
command = "python3"
args = ["chatroom_mcp_server.py"]If you install this MCP server into some other repository, agents in that repository will not automatically read this repository's AGENTS.md. They will follow the consuming repository's own instruction files.
For downstream use:
- Install the MCP server in the consuming repository.
- Add a short chatroom workflow section to that repository's agent instructions.
Suggested AGENTS.md snippet for a consuming repository:
## Chatroom Coordination
When using the `chatroom` MCP server for multi-agent work:
- Call `join(name=...)` when starting work.
- On resumed work, call `get_handoff(name=...)`.
- Prefer `read_unread(name=...)` or the latest summary over replaying the full log.
- Send coordination updates with `send_message(...)`.
- Write a handoff with `write_summary(...)` before handing work across sessions.
- Call `leave(name=...)` when done.
- Instruct every participating agent to check the chatroom and act on relevant messages; joining alone does not create an automatic relay or response loop.
Use the chatroom for ownership, status, blockers, and handoffs. Keep messages compact.Suggested CLAUDE.md snippet for a consuming repository:
## Chatroom Coordination
When the `chatroom` MCP server is available:
- Join the chatroom at the start of work with `join`.
- Use `get_handoff` and `read_unread` to resume context efficiently.
- Send concise progress or blocker updates with `send_message`.
- Write `write_summary` before handoff or session end when continuity matters.
- Leave the chatroom with `leave` when work is complete.
- Do not assume another agent will see a message unless that agent is also instructed to read from the chatroom.Tool semantics live in SPEC.md. Use the consuming repository's own instruction files to tell agents when to use the tools.
For direct debugging from the repository root:
python3 chatroom_mcp_server.pyThe server uses stdio transport, so you normally do not run it by hand for long. Your MCP client should launch it as a subprocess.
Run the read-only terminal monitor from the repository root:
python3 chatroom_monitor.pyUseful options:
python3 chatroom_monitor.py --limit 50
python3 chatroom_monitor.py --participant codex
python3 chatroom_monitor.py --interval 0.5
python3 chatroom_monitor.py --onceThe monitor reads .chatroom/messages.jsonl and .chatroom/participants.json under shared locks and redraws the terminal in place. It does not modify chatroom state.
Syntax check:
python3 -m py_compile chatroom_mcp_server.py chatroom_monitor.pyRegression tests:
python3 -m pytest -qManual run check:
python3 -c "import chatroom_mcp_server as s; print(s.join('alice')); print(s.send_message('alice', 'hello')); print(s.read_unread('alice')); print(s.write_summary('alice', 'Initial handoff')); print(s.get_handoff('alice')); print(s.leave('alice'))"Authoritative tool behavior lives in SPEC.md. The summaries below are quick reference.
Registers an agent as active.
Parameters:
name: strrole: str = "general"
Returns the current participant list.
Removes an active participant and writes a system message like "alice left the chatroom".
Parameters:
name: str
Returns:
{"left": true}Appends a message to the shared log.
Parameters:
name: strcontent: strto: str = "all"
Returns:
{"id": 12}Reads messages with id > since_id. If participant is set, only messages addressed to that participant or to all are returned.
Parameters:
since_id: int = 0limit: int = 50participant: str = ""
The server rejects limit values above 100.
Returns all active participants.
Returns:
{
"participant_count": 2,
"message_count": 14,
"last_activity_ts": "2026-04-11T13:00:00Z"
}Returns the stored unread cursor for a participant.
Example:
{"name": "codex", "last_read_id": 12}Sets the unread cursor for a participant to a specific message ID.
Cursor updates are monotonic: the stored cursor never moves backwards.
Reads messages visible to a participant after that participant's cursor.
Parameters:
name: strlimit: int = 50mark_read: bool = true
If mark_read is true, the cursor advances to the highest returned message ID and never regresses.
Appends a summary record intended for cross-session handoff.
Parameters:
name: strcontent: strscope: str = "all"
Returns the latest summary for an exact scope.
Returns the preferred compact orientation payload for a new or resumed session:
- active participants
- latest message ID
- current cursor for a named participant
- unread count for that participant
- latest relevant summary
- recent visible messages
- Agent starts and calls
join. - On a resumed session, the agent calls
get_handoff(name=...). - The agent reads the latest summary or uses
read_unread(name=...)instead of replaying the full log. - The agent sends status updates with
send_message. - The agent writes a summary with
write_summarywhen handing work off across sessions. - The agent calls
leavewhen done.
- It does not orchestrate agents by itself. The server is shared state plus tools; agent behavior still has to come from each agent's instructions.
- It does not push messages to running agents. Reading is explicit and pull-based.
- It does not guarantee a directed message will be read, acknowledged, or acted on.
- It does not make named messages private from other local readers of the underlying log.
- It does not validate that a named recipient is currently active before accepting a message.
- Message IDs are assigned under an exclusive file lock, so concurrent writers still get unique sequential IDs.
- Cursor writes are monotonic, so stale callers cannot move unread state backwards.
- Participant names are treated as single-owner identities across live processes; duplicate live joins are rejected.
- Participant cleanup on process exit is best-effort via
atexit. Hard kills can leave stale participants behind. get_statusis not an atomic snapshot across both files. Counts can reflect slightly different moments.- Input strings are trimmed before validation and storage.
- Read-oriented tools enforce a hard maximum of
100messages per call. - Context control is handled by cursors, summaries, and
get_handoff. The message log is still durable on disk. - This version does not rotate
messages.jsonl; rotation would require changing the current ID-allocation contract.