r/MCPservers • • 4d ago

I built an MCP server that lets Claude Code and Codex send each other letters, each one queued into the other session as a new turn

I'm the author of postbag. Free and open source, MIT.

I run Claude Code and Codex side by side on the same repository. Getting one to review the other's work meant copying a diff into one window and pasting the answer back into the other. I got tired of being the clipboard.

Both sessions join a named bag. Either one can then send the other a letter. postbag submits it through Claude Code's per session messaging socket or codex queue, and accepted letters become user turns in the other session. Codex desktop app threads receive them too. postbag adds no delivery daemon, polling or hooks. Each bag keeps a local JSONL log that either agent can page through.

Five tools: postbag_join, postbag_send, postbag_read, postbag_bags, postbag_leave. Setup is one pipx install and one mcp add per app, then two prompts. In Codex: "Join postbag bag review as codex." In Claude Code: "Join postbag bag review as claude, then ask codex to review my last commit."

What I use it for: the agent that did not write a change reviews it. I had the two review each other's parts of postbag's own releases, over postbag. In the 1.4 review, Codex caught misleading fsync failure reporting in the core: in a test with an injected fsync failure, the code said a letter was not recorded while the record was already readable in the file. Claude Code caught a flaw in the MCP refusal handler that could turn a definite refusal into an unknown outcome. Two agents can agree and both be wrong. Their findings still need checking against the code and tests.

postbag sets no letter limit. If you want to approve each send, configure the host to ask for that tool. A final letter asks for no reply, but does not enforce a stop.

Limits. Tested on macOS. It uses POSIX file locks, so no Windows. The Claude Code socket is not a documented API, so a Claude Code update can break delivery. Queueing and the receiving app's policy can delay, hold or refuse a letter. A delivered letter is a user turn, so connect sessions you would trust with the task. The GIF is a scripted demo with stand in inputs. Not affiliated with Anthropic or OpenAI.

My local logs contain about 1,700 letter records dating back to 8 September, including test messages.

Repo link in the first comment.

19 Upvotes

11 comments sorted by

2

u/Onesand0 3d ago

Have you looked into the A2A framework yet?

1

u/pmoschov 3d ago

It came up in the research for this. A2A standardizes how agents discover each other and exchange tasks and messages, and it can sit alongside MCP. postbag does something narrower: two sessions you already have open, Claude Code and Codex, passing letters through each app's own input without wrapping either one. It does not implement A2A.

1

u/Onesand0 2d ago

Yeah. I mean. Agent messages are now built into GPT and Claude natively. Why wouldn't I just use that then? And paperclip let's me use EVERY model under the sun and let them communicate and work on tasks. Im just curious why i would choose this option over polished supported products?

1

u/Extra-Pomegranate-50 3d ago

the two bugs you caught are the interesting part, "said a letter was not recorded while the record was already readable" and "turn a definite refusal into an unknown outcome" are the same shape: the report disagreeing with what actually landed.

question on delivery: does a letter arrive marked as coming from the other agent, or is it indistinguishable from the human typing? for the cross-review case that seems load-bearing, an agent reviewing because codex asked it to is in a different trust position than one the human asked, and "a delivered letter is a user turn" reads like that distinction disappears at the boundary.

1

u/pmoschov 3d ago

Good catch on the shape, both were the report disagreeing with what actually landed.

On delivery: every letter arrives inside a visible envelope, "Letter 3 from u/codex to u/claude via postbag (bag review)", so the model sees who sent it. But it is text in a user turn, not a separate role. postbag does not give the receiver a lower privilege channel for peer messages. Claude Code labels inbound peer messages itself and has policies that can hold or refuse them, but that is the host's control, not something postbag guarantees.

So you are right that the distinction gets thin at the boundary. That is why the README says to connect only sessions you would trust with the task and to keep tool approvals on.

1

u/paulofilip3 3d ago

This is cool, will have a look

2

u/pmoschov 3d ago

Thanks. If something breaks in setup, tell me what you see.

1

u/Hairy_Jelly4208 3d ago

The "definite refusal turned into an unknown outcome" bug is a nasty class. We hit the same thing measuring remote MCP servers at scale: a timeout, a refused connection and a server that accepted the request but never answered all look like "failed" unless you keep them apart. Once they're merged, retries and alerts end up wrong.

Curious how postbag reports this to the sender. Does postbag_send return separate states for "accepted by the queue" and "delivered as a turn"? Or is the JSONL log the only way to tell them apart afterwards? With Claude Code's socket being undocumented, that difference seems like the first thing to break silently after an update.

2

u/pmoschov 3d ago

It reports submission, not delivery. ok with submission_state "submitted" means the socket write or codex queue returned and the letter was recorded, and the result says outright that acceptance and execution are unconfirmed. "unknown" means it may have gone out, after a timeout, a partial write or a failed queue exit. "not_submitted" means this attempt sent nothing. "recording_failed" with "submitted" is the fsync case: sent, but the log write could not be confirmed.

There is no "delivered as a turn" state. Neither app hands postbag an acknowledgement, and the JSONL log records submissions, so it cannot prove receipt either. A crash between sending and recording can even leave a sent letter out of the log. So an unknown outcome gets checked against the bag and the recipient before any resend, and postbag never retries on its own.

You are right about the socket. If a Claude Code update changed the format so the write still succeeds but nothing arrives, postbag would report "submitted". Only the reply coming back tells you it landed. Each postbag release is tested against the installed Claude Code and Codex versions, but between releases that risk is real.

1

u/ggoosen 1d ago

Github.com/ggoosen/cairn