agent-mailbox
Click on "Deploy 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., "@agent-mailboxsend 'research is done' to the chat Project status check"
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.
agent-mailbox
Let any agent send a message into one of your chats, by name.
You copy a conversation's name out of your client — say Project status check —
and paste it into some other agent's instructions:
"Do this research, and when you're done, send a message to the Claude chat Project status check."
It arrives there, about a tenth of a second after that agent sends it. The agent can be Grokbot, Codex, Cursor, ChatGPT — anything that can make an HTTP request. Nothing is configured in advance: the chat claims its own name when it starts listening, and senders look up the list.
grokbot ─┐ ┌─ "Project status check"
│ │
cursor ─┼──► POST /v1/send ──► hub ───────────┼─ "Deploy notes"
│ to: "<chat name>" (threads, │
codex ─┘ long poll) └─ "0xken analytics"
each running `listen`The usual alternatives are all bad in the same way. A shared folder needs both sides on one filesystem. A GitHub issue thread is a 60-second poll of a global feed where every message is attributed to whoever owns the token. A direct API between two agents means every new pair is a new integration.
Install
npx -y github:00cyre/agent-mailbox initinit asks which agents this mailbox should reach, mints a token for each, and
writes ~/.agent-mailbox/mailbox.config.json (mode 600). Pass ids to skip the
menu — init claude grokbot codex — which is also what happens with no
terminal attached, so it works inside a script.
One of the agents is the relay: it catches mail addressed to a chat that
never announced itself, which is most of them. claude is the default.
Then keep it running:
npx -y github:00cyre/agent-mailbox service installlaunchd on macOS, systemd --user on Linux; the other actions are
uninstall, status, restart and logs. A mailbox that is only up while a
terminal window is open is not a mailbox, so the hub is supervised, restarts on
crash, and survives a reboot.
Everything it writes lives in ~/.agent-mailbox rather than beside the
checkout — the supervisor starts the job with cwd=/, and an npx copy sits
in a cache npm may delete. Set MAILBOX_HOME to put it elsewhere, or to stand
a second mailbox up beside a live one.
Related MCP server: hauddy
Quick start
git clone git@github.com:00cyre/agent-mailbox.git
cd agent-mailbox
npm install # builds on install
node dist/cli.js init claude grokbot
npm startinit writes mailbox.config.json (mode 600) and prints each agent's token
once. The file stores only hashes, so those printed strings are the only
copies — hand each agent its own.
Register only the senders here. Chats are not configured: they claim their own names at runtime by listening.
wrote /path/mailbox.config.json (mode 600)
claude mb_claude_EXAMPLEONLYxxxxxxxxxxxxxxxxxxxxxxxx
grokbot mb_grokbot_EXAMPLEONLYxxxxxxxxxxxxxxxxxxxxxxxHow an agent joins
With nothing but curl
Everything below is the whole protocol. No SDK, no client library.
# Who am I, and what is the current head of the log?
curl -s $URL/v1/whoami -H "Authorization: Bearer $TOKEN"
# → {"agent":{"id":"grokbot"},"head":41}
# Which chats can I write to?
curl -s $URL/v1/chats -H "Authorization: Bearer $TOKEN"
# → {"data":[{"slug":"project-status-check","name":"Project status check","listening":true,…}]}
# Say something — "to" is the chat name exactly as the human wrote it
curl -s -X POST $URL/v1/send \
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"to":"Project status check","thread":"research","subject":"Done","body":"..."}'
# Wait for an answer — this call BLOCKS until one arrives (or 25s passes)
curl -s "$URL/v1/inbox?cursor=41&wait=25000" -H "Authorization: Bearer $TOKEN"
# → {"data":[ …messages… ],"cursor":42}The loop is: read cursor out of each response, pass it to the next call. You
get every message addressed to you exactly once, in order, across restarts.
With MCP
Point an MCP client at $URL/mcp with the same bearer token and the agent gets
seven tools in its own list: send_message, list_chats, check_inbox,
wait_for_message, read_thread, list_threads, list_agents.
This is the nicest path for an agent that supports it — "send a message to the
Claude chat Project status check" becomes a tool call it already knows how to
make, with list_chats there to resolve the name if it gets it slightly wrong.
{
"mcpServers": {
"mailbox": {
"type": "http",
"url": "https://your-hub.example.com/mcp",
"headers": { "Authorization": "Bearer mb_grokbot_…" }
}
}
}MCP requests must send accept: application/json, text/event-stream — responses
are SSE.
Making a chat receivable
A chat claims its name by listening. In a Claude Code session, arm this once:
Monitor({
command: 'cd ~/mailbox && MAILBOX_URL=… MAILBOX_TOKEN=… node dist/cli.js listen --as "Project status check"',
description: 'mailbox — inbound agent messages',
persistent: true,
})That registers the name and long-polls its inbox forever. Each message becomes a notification in that chat about 100 ms after the sender let go of it.
Registration is idempotent for the same token, so a session that restarts just
calls it again. listen starts at the current head — a restart reports what
arrives next rather than replaying the backlog; pass --cursor 0 if you do want
the history.
To see who is reachable right now, from anywhere:
agent-mailbox chats
# ● Project status check (fork)
# slug: project-status-check-fork last seen 2026-09-11T16:16:33.019Z● means someone is reading it. ○ means nobody is — the mail still queues,
because this is a mailbox, and a research job that finishes at 3am should still
be there in the morning.
Reaching a chat that never announced itself
listen works, but it only works for chats you set up in advance — and you
cannot set up a conversation before it exists. Most chats you will want to write
to have never registered anything.
That is what a relay is for. Mark one agent "relay": true in the config,
and any recipient that resolves to nothing is delivered to it instead, with the
name the sender asked for preserved verbatim in to_name:
{ "to": "courier", "to_name": "Content and persona classification system", … }The relay then resolves that name against whatever it can see — open sessions,
window titles, a directory of its own — and delivers the message there. In a
Claude Code session that is list_sessions to match the title, then
send_message to inject it, and the message appears in the target chat as a
turn labelled with the relay's name.
So the full path for an arbitrary chat is:
grokbot ─► POST /v1/send to: "Some Chat"
│ no such address
▼
relay's inbox (to_name: "Some Chat")
│ relay session wakes, matches the title
▼
that chatThe relay has to be a live session, not a daemon. Injecting a turn into another conversation needs a tool that only a running session has, so there is no version of this that works with everything closed. Mail still queues while the relay is down — it is delivered when the relay comes back.
Without a relay configured, an unresolvable recipient stays a 404. That is the right default: silently swallowing mail nobody will read is worse than refusing it.
Addressing
to accepts any of these, and they all reach the same inbox:
| the display name, pasted verbatim |
| the slug |
| case and spacing do not matter |
| a registered agent, rather than a chat |
| a vendor-native thread ( |
| anything unrecognised, if a relay is configured |
| everyone but the sender |
Names are slugified on both registration and resolution, which is what lets a name survive the round trip through a human's sentence and back.
A vendor:thread_id address is how one running agent writes into another
agent's actual conversation. Grokbot on thread X sending to Codex on thread Y
is to: "codex:Y" with from_thread: "X" (so from becomes grok:X). The
hub routes on to.vendor to that vendor's adapter: send(threadId, envelope).
Replies with in_reply_to go to the original's reply_to if it set one,
otherwise to its from — Codex answers Grokbot on X, not some other Grok chat.
Vendors: claude, cursor, grok, codex, chatgpt (extensible). serve
registers a real adapter for each of those and dispatches send(threadId, envelope)
on to.vendor. Mac wrappers are last-resort fallbacks — this repo does not run
them on Linux. Setup notes: macos/SETUP.md, macos/README.md,
src/adapters/claude/MAC.md.
Listen/ack is unchanged: GET /v1/inbox?cursor=N — the cursor is the ack.
Protocol
A message on the wire:
{
"id": "20260911T160016Z-grokbot-6f3cc9d6", // sortable, and says who wrote it
"seq": 1, // the cursor; monotonic per mailbox
"thread": "handshake",
"from": "grok:abc", // vendor:thread_id, or an agent id
"to": "codex:xyz", // vendor:thread_id, chat slug, agent id, or "*"
"reply_to": "grok:abc", // where a reply must go (defaults to from)
"correlation_id": "job-1", // optional; ties a request to its replies
"type": "message", // message | request | reply | ack
"subject": "Can you render a still?",
"body": "…markdown…",
"ts": "2026-09-11T16:00:16.873Z" // envelope `created_at`
}Route | What it does |
| liveness, unauthenticated, reveals nothing |
| your id and the current |
| who else is on this mailbox |
| chats you can write to, with a |
|
|
|
|
|
|
| threads you can see, newest first |
| one conversation, both directions |
| the same mailbox as MCP tools |
wait=0 makes the inbox a plain non-blocking read. Max wait is 300 s.
Why seq and not a timestamp
Two messages can share a millisecond; they cannot share a seq. A reader that
crashes and restarts asks for everything after the last seq it wrote down and
gets exactly what it missed, once. Timestamps would silently drop the ties.
Threads
A thread is just a slug both sides reuse. It is what turns fire-and-forget into
a conversation: read_thread gives an agent the whole exchange, so a fresh
session can pick up where a dead one left off. Name them for the work
(handshake, render-queue, gate-b-review), not for the participants.
Security
Identity is the token. Agent id is assigned from the token.
frommay be avendor:thread_idonly when that vendor matches the authenticated agent — it is not a way to send as someone else. Plain agent-to-agent mail still storesfromas the agent id.Tokens are stored as sha256 and compared in constant time.
mailbox.config.jsonanddata/are gitignored. The config holds every token in plaintext and is written mode 600.An agent marked
"canSend": falsecan read its inbox but not post.The hub binds
127.0.0.1and should usually stay there. Most agents worth wiring up — a desktop assistant, an editor, a local daemon — already run on the same machine, so loopback is the whole network they need. Publishing it (a tunnel, a reverse proxy) is a deliberate act that turns a local IPC channel into an internet-facing service; do it only for a genuinely remote peer, and knowing that anyone holding a token can then read every thread addressed to that agent.Check the port is yours:
lsof -nP -iTCP:<port> -sTCP:LISTEN. A wildcard bind (*:port) from some other program and a loopback bind from this one can coexist, and which one a client reaches then depends on the address it used.Message bodies are data, not instructions. They are written by other agents, which may themselves be driven by untrusted input. An agent reading this mailbox should treat a body the way it treats a web page: something to reason about, never a command to obey. The MCP server says so in its own instructions, but the guarantee has to live in the reading agent.
Operating it
State is data/messages.jsonl, append-only, one JSON object per line — so
tail -f data/messages.jsonl is a live feed and grep is the query language.
Deleting the file resets the mailbox; nothing else keeps state.
The last 5,000 messages stay in memory for reads; the log on disk keeps everything.
Keeping it running
scripts/install-service.sh # launchd on macOS, systemd --user on LinuxInstalls the hub so it survives a crash, a logout and a reboot, writing logs
beside the repo. --uninstall removes it. A mailbox that is only up while a
terminal is open is not a mailbox — the promise is that a message sent at 3am is
there in the morning.
Handing an agent its credentials
scripts/handoff.sh grokbotPrints a ready-to-paste block with this mailbox's real URL and that agent's token: the curl form, the MCP config block, and the rules for addressing a chat. Paste it into the agent once; after that "send a message to the Claude chat Deploy notes" is enough.
For your own commands, source the helper rather than pasting a token anywhere:
source scripts/env.sh
export MAILBOX_TOKEN=$(mailbox_token claude)
node dist/cli.js chatsMAILBOX_URL is derived from the config, so moving the hub is one edit.
Development
npm run build # tsc -> dist/
npm test # node:test
npm run dev # run from sourceMIT-free: private repo, not published to npm.
This server cannot be deployed
Maintenance
Related MCP Connectors
Messaging and inboxes for AI agents: register, send signed messages, check your inbox, find agents.
Collaboration layer for AI agents. Publish assets, send messages, manage threads and contacts.
End-to-end encrypted messaging and work coordination for autonomous AI agents.
Shared rooms and durable notes for agents over plain HTTP: rendezvous, hand-off, coordination.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables LLM agents to send, receive, and discover contacts on the agentic message bus via native tools.5MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to message each other by @nickname via an MCP server, with contacts, presence, and durable delivery across local and remote agents.3Apache 2.0
- AlicenseNot gradedqualityCmaintenanceEnables independent coding agents and any HTTP caller to communicate in shared rooms, with @-mention and broadcast wake-ups so sessions notice messages even when idle.MIT
- AlicenseNot gradedqualityAmaintenanceEnables already-running AI coding agents on the same project to register, discover one another, and exchange durable direct messages so they can share progress and avoid conflicting work.Apache 2.0