Skip to main content

Dead Letter Box: a tiny MCP server for independent agent sessions to leave each other notes. Eight tools, no daemon, real dead-letter semantics + task lifecycle. Includes dlb-monitor for push-like wake via Claude Code's Monitor tool.

Project description

DLB — Dead Letter Box

PyPI CI Python License

A tiny MCP server that lets independent AI-agent sessions leave each other notes. Fire-and-forget. Queued for non-existent recipients. No daemon. Six tools + an optional dlb-monitor wake source.

Why

If you've ever had two Claude Code / Cursor / Codex sessions running in different terminals and wished they could coordinate, you've hit the gap DLB fills. Existing options (mcp_agent_mail, agent frameworks like CrewAI/AutoGen) either bundle too much (40+ tools, contact policies, file leases) or only work inside a single parent process.

DLB does one thing: messages between agent sessions. It does NOT do orchestration, contact handshakes, file reservations, web hosting, or auto-name-generation. If you want those, you want a different tool.

Five differentiators (vs. mcp_agent_mail and friends)

  1. 6 tools — fits cleanly in Claude Code's tool list without crowding out the builtins.
  2. No daemon — each MCP call opens SQLite, runs one transaction, closes. No server.lock, no port management.
  3. Names accepted as-is — call yourself alpha, ThreadBeta, worker-1. DLB will not rename you.
  4. Real dead-letter semanticssend(to="ghost") succeeds and queues the message; if/when someone registers as ghost, the messages are waiting.
  5. Zero ceremonysend works on call one. Registration is optional and observational.

Install

Zero-install (recommended — uvx fetches and runs on demand):

uvx dlb-mcp

Or install once:

uv tool install dlb-mcp

Wire into Claude Code

Add to ~/.claude.json:

{
  "mcpServers": {
    "dlb": {
      "type": "stdio",
      "command": "uvx",
      "args": ["dlb-mcp"]
    }
  }
}

Multiple sessions can coexist — they all share ~/.dlb/store.sqlite3 via SQLite WAL mode.

The 8 tools

Tool What it does
register(name, working_on=None, force=False, prior_token=None) Declare a name + status. Returns session_token. Conflict on existing active name → error with suggestion. force=True requires either prior_token matching the holder, or the holder being stale > DLB_TAKEOVER_AFTER_SECONDS.
list_threads(active_within="24h") See who's around. No auth.
send(to, body, subject=None, from_=None, session_token=None, msg_type=None, in_reply_to=None) Drop a message. Always succeeds (subject to size cap) — even if to doesn't exist yet. Pass session_token to bind from_ to your registered name (otherwise from_ is unverified free text). Pass msg_type="task" for messages the recipient must acknowledge; pass in_reply_to=<id> to thread a reply to an earlier message.
read(name, session_token, unread_only=True, limit=20) Read inbox. Requires session_token for registered names. Returns full lifecycle fields on each message.
ack(message_id, session_token) Explicit "I saw this and acted on it". Optional; superseded by update_status for the common case.
unregister(name, session_token) Release the name. Messages preserved for re-registration.
update_status(message_id, status, session_token, note=None) (v0.3.0+) Recipient signals lifecycle state on a message they received. Convention: queued/accepted/running/done/blocked (any string accepted). Also sets read_at if the message was still unread.
get_task_status(message_id) (v0.3.0+) Sender-side probe. No auth — returns the current status, status_note, msg_type, in_reply_to, and read_at of any message id. Lightweight (does NOT return body). Fixes the "did my task get picked up?" gap.

That's the entire API.

Task lifecycle (v0.3.0+) — the pattern that closes the "delivered ≠ acted on" gap

DLB 0.2.x guaranteed delivery and read-receipts, but a message the recipient had read yet not acted on was indistinguishable (to the sender) from a task in progress. A documented incident (2026-07-01) had a security-review task sit unexecuted for ~1 hour with no signal.

v0.3.0 adds the minimum vocabulary to close that gap as a convention, not enforcement — DLB stays a small file store; the state machine lives in the CLAUDE.md DLB Inbox Protocol.

Sender pattern:

task = send(to="worker", body="run /security-review", msg_type="task")
# ... later, without holding worker's session_token:
status = get_task_status(task.id)
# → {"status": "running", "status_note": "scanning src/", "read_at": "..."}

Recipient pattern (as required by the DLB Inbox Protocol in ~/.claude/CLAUDE.md):

inbox = read(name="worker", session_token=my_token)
for msg in inbox:
    if msg.msg_type == "task":
        # STEP 1: reply IMMEDIATELY (before starting work)
        send(to=msg.sender_name, body="accepted, ETA 20m",
             session_token=my_token, in_reply_to=msg.id)
        update_status(msg.id, "accepted", my_token, note="ETA 20m")

        # STEP 2: do the work
        result = do_work(msg.body)
        update_status(msg.id, "running", my_token, note="in progress")

        # STEP 3: report completion
        send(to=msg.sender_name, body=f"done: {result}",
             session_token=my_token, in_reply_to=msg.id)
        update_status(msg.id, "done", my_token, note=result)

What DLB does NOT do:

  • No enforcement — nothing forces status values to a fixed set.
  • No SLA nudges — DLB won't automatically re-notify a stale task. That's a job for a task-orchestration product built on top of DLB.
  • No heartbeat beacons — the recipient calls update_status explicitly when the state changes.
  • No history — only the LATEST status per message is stored; note-per-status is not preserved.

If you need any of those, you want a full task-orchestration layer, not more features on DLB.

Push-like wake — dlb-monitor + Claude Code's Monitor tool

DLB itself is polling-only (request-response MCP, no push). But Claude Code has a Monitor tool that streams stdout from a long-running process into the conversation as notifications — each line wakes the LLM mid-idle. dlb-monitor is a tiny CLI that polls the same SQLite store and emits one line per new message, designed to be wrapped by Monitor:

# Run this at session start (or have the LLM call it after registering):
Monitor({
  command: "dlb-monitor --name alpha",
  description: "DLB inbox: alpha",
  persistent: true
})

Each new message addressed to alpha produces one stdout line:

2026-06-30T21:30:14Z bravo: "ping — can you look at the reskin route?"

Filters:

dlb-monitor --name alpha --include-senders bob,carol      # allowlist
dlb-monitor --name alpha --exclude-senders bot,system     # denylist
dlb-monitor --name alpha --interval 1                     # tick frequency (default 2s)

When dlb-monitor is the right answer vs. dlb-launcher:

Surface Use
Claude Code (terminal or app) dlb-monitor via Monitor tool — native notification path, no PTY mechanics
Codex CLI / Gemini CLI dlb-launcher PTY wrap — Monitor tool doesn't exist there, PTY injection is the only path
Web (claude.ai) Neither — manually call read per turn

They're complementary, not competitive.

Configuration

Env var Default What
DLB_STORE ~/.dlb/store.sqlite3 Path to the SQLite store
DLB_MESSAGE_TTL_DAYS 7 Days before unread messages expire
DLB_MAX_BODY_BYTES 262144 (256 KiB) Reject send with bodies exceeding this UTF-8 byte length
DLB_TAKEOVER_AFTER_SECONDS 86400 (24h) How long a holder must be silent before force=True without prior_token can evict them
DLB_MONITOR_INTERVAL 2.0 Default poll interval for dlb-monitor (overridable via --interval)

Trust model — coordination, not confidentiality

DLB is for cooperating agents under the same OS user, not against adversarial ones. Specifically:

  • session_token gates the DLB tool API, not the underlying SQLite file. Any process running as the same OS user can open ~/.dlb/store.sqlite3 directly and read every body, every token. Tightening file perms (0600) raises the bar against accidental leakage, but does not change the threat model.
  • Send is open by default. Anyone running as your user can drop messages into any inbox. Passing session_token on send makes provenance trustworthy (binds from_ to the token's name); tokenless sends keep the unverified free-text from_ field.
  • force=True is stale-gated. Without a matching prior_token or a stale holder (> DLB_TAKEOVER_AFTER_SECONDS), name takeover is rejected — closes the casual-hijack hole of v0.1.
  • No TLS, no accounts, no cross-host. If you need a real adversarial boundary between agent sessions, you want a broker-process design (DLB rejects this — it would break the "no daemon" promise) or a different tool entirely.

What DLB is NOT

  • Not an orchestrator. Use a script + your LLM SDK if you need to spawn agents.
  • Not a web service. Local only.
  • Not Gmail. No threading, replies, CC/BCC, attachments, contacts, importance flags.
  • Not a file-coordination tool. No file leases or advisory locks.
  • Not push by itself — but dlb-monitor + Claude Code's Monitor tool gets you there (see above).

If you find yourself wishing for any of these, that's a signal to use a different tool, not to ask DLB to grow.

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dlb_mcp-0.3.0.tar.gz (90.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dlb_mcp-0.3.0-py3-none-any.whl (27.1 kB view details)

Uploaded Python 3

File details

Details for the file dlb_mcp-0.3.0.tar.gz.

File metadata

  • Download URL: dlb_mcp-0.3.0.tar.gz
  • Upload date:
  • Size: 90.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for dlb_mcp-0.3.0.tar.gz
Algorithm Hash digest
SHA256 46dce5714ed7811733167306d7cc6ae15f650f732360941b05808dfcc67f535e
MD5 6c1d988a90e2a29b18b03b214b50ac4a
BLAKE2b-256 0883f14651a51333c1265374583ccce48cd3410b803b6ec0815e82e33880e615

See more details on using hashes here.

File details

Details for the file dlb_mcp-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: dlb_mcp-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 27.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for dlb_mcp-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 15651894c01e91260f7d1cf78eee9626bedc628e14127ddec5943bb9ea14b9f4
MD5 bcf21245b8a55e173769ed8b4905f7b8
BLAKE2b-256 2f453e4a19aa1f2567061874c9379c0c211ec4ddea72378bb87d6ee8c242b4f1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page