Skip to main content

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.

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.6.0.tar.gz (118.4 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.6.0-py3-none-any.whl (43.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: dlb_mcp-0.6.0.tar.gz
  • Upload date:
  • Size: 118.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for dlb_mcp-0.6.0.tar.gz
Algorithm Hash digest
SHA256 a7bd7b8e06ec87cd6dae044da2029d5133a804a049283de9c13c529c59e2251a
MD5 19f5391d20868a357927a23395b25f33
BLAKE2b-256 ac4c68669bc26cfcfe542387469ea3c203dfbff0f7e97ff85c13b7032f6690ac

See more details on using hashes here.

Provenance

The following attestation bundles were made for dlb_mcp-0.6.0.tar.gz:

Publisher: publish.yml on jordanspilgrim/dlb-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

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

File metadata

  • Download URL: dlb_mcp-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 43.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for dlb_mcp-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4c9c491fb2ed060112bdd4652961edd8f9c8de68dd2cce74e2d69851b85341c4
MD5 9110fb56cdfdcb57881f73db624d0ccd
BLAKE2b-256 570e866a0fb03bcdbd65647842d20c321ccffe8d062c2628acb53d86522c8b5d

See more details on using hashes here.

Provenance

The following attestation bundles were made for dlb_mcp-0.6.0-py3-none-any.whl:

Publisher: publish.yml on jordanspilgrim/dlb-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.6.5

2 files

0.6.4

2 files

0.6.3

2 files

0.6.2

2 files

0.6.1

2 files

This release

0.6.0 This release

2 files

0.5.2

2 files

0.5.1

2 files

0.3.0

2 files

0.2.1

2 files

0.1.0

2 files

Supported by

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