dlb-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ name: test (Python ${{ matrix.python-version }})
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ python-version: ["3.11", "3.12", "3.13"]
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v3
22
+ with:
23
+ enable-cache: true
24
+
25
+ - name: Set up Python ${{ matrix.python-version }}
26
+ run: uv python install ${{ matrix.python-version }}
27
+
28
+ - name: Create venv and sync deps
29
+ run: |
30
+ uv venv -p ${{ matrix.python-version }}
31
+ uv sync
32
+
33
+ - name: Lint with ruff
34
+ run: uv run ruff check .
35
+
36
+ - name: Format check with ruff
37
+ run: uv run ruff format --check .
38
+
39
+ - name: Run tests
40
+ run: uv run pytest -v
@@ -0,0 +1,31 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ *.egg-info/
8
+ .eggs/
9
+ dist/
10
+ build/
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .mypy_cache/
14
+
15
+ # uv
16
+ .venv/
17
+ # uv.lock IS committed — gives reproducible CI + dev environments.
18
+ # It only affects local installs / CI, not what consumers get via pip install.
19
+
20
+ # IDE
21
+ .vscode/
22
+ .idea/
23
+
24
+ # OS
25
+ .DS_Store
26
+ Thumbs.db
27
+
28
+ # DLB local store (for any accidental runs in repo dir)
29
+ store.sqlite3
30
+ store.sqlite3-wal
31
+ store.sqlite3-shm
dlb_mcp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jordan S Pilgrim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
dlb_mcp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: dlb-mcp
3
+ Version: 0.1.0
4
+ Summary: Dead Letter Box: a tiny MCP server for independent agent sessions to leave each other notes. Six tools, no daemon, real dead-letter semantics.
5
+ Project-URL: Homepage, https://github.com/jordanspilgrim/dlb-mcp
6
+ Project-URL: Repository, https://github.com/jordanspilgrim/dlb-mcp
7
+ Project-URL: Issues, https://github.com/jordanspilgrim/dlb-mcp/issues
8
+ Author: Jordan S Pilgrim
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: mcp[cli]>=1.2.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # DLB — Dead Letter Box
16
+
17
+ 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.
18
+
19
+ ## Why
20
+
21
+ 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.
22
+
23
+ DLB does one thing: messages between agent sessions. It does NOT do orchestration, contact handshakes, file reservations, push notifications, web hosting, or auto-name-generation. If you want those, you want a different tool.
24
+
25
+ ## Five differentiators (vs. mcp_agent_mail and friends)
26
+
27
+ 1. **6 tools** — fits cleanly in Claude Code's tool list without crowding out the builtins.
28
+ 2. **No daemon** — each MCP call opens SQLite, runs one transaction, closes. No `server.lock`, no port management.
29
+ 3. **Names accepted as-is** — call yourself `alpha`, `ThreadBeta`, `worker-1`. DLB will not rename you.
30
+ 4. **Real dead-letter semantics** — `send(to="ghost")` succeeds and queues the message; if/when someone registers as `ghost`, the messages are waiting.
31
+ 5. **Zero ceremony** — `send` works on call one. Registration is optional and observational.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ uv tool install dlb-mcp
37
+ ```
38
+
39
+ Or zero-install:
40
+
41
+ ```bash
42
+ uvx dlb-mcp
43
+ ```
44
+
45
+ ## Wire into Claude Code
46
+
47
+ Add to `~/.claude.json`:
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "dlb": {
53
+ "type": "stdio",
54
+ "command": "uvx",
55
+ "args": ["dlb-mcp"]
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ Multiple sessions can coexist — they all share `~/.dlb/store.sqlite3` via SQLite WAL mode.
62
+
63
+ ## The 6 tools
64
+
65
+ | Tool | What it does |
66
+ |---|---|
67
+ | `register(name, working_on=None, force=False)` | Declare a name + status. Returns `session_token`. Conflict on existing active name → error with suggestion. |
68
+ | `list_threads(active_within="24h")` | See who's around. No auth. |
69
+ | `send(to, body, subject=None, from_=None)` | Drop a message. Always succeeds — even if `to` doesn't exist yet. |
70
+ | `read(name, session_token, unread_only=True, limit=20)` | Read inbox. Requires session_token for registered names. |
71
+ | `ack(message_id, session_token)` | Explicit "I saw this and acted on it". Optional. |
72
+ | `unregister(name, session_token)` | Release the name. Messages preserved for re-registration. |
73
+
74
+ That's the entire API.
75
+
76
+ ## Configuration
77
+
78
+ | Env var | Default | What |
79
+ |---|---|---|
80
+ | `DLB_STORE` | `~/.dlb/store.sqlite3` | Path to the SQLite store |
81
+ | `DLB_MESSAGE_TTL_DAYS` | `7` | Days before unread messages expire |
82
+
83
+ ## Security model
84
+
85
+ - **Local-only.** File permissions (`~/.dlb/` is `chmod 700`) are the boundary.
86
+ - **Send is open.** Anyone can drop messages in any inbox. By design — DLB is a relay.
87
+ - **Read is gated.** Once a name is registered, only the holder of that session's token can read it.
88
+ - **No TLS, no accounts, no cross-host.** If you need those, you want a different tool.
89
+
90
+ ## What DLB is NOT
91
+
92
+ - Not an orchestrator. Use a script + your LLM SDK if you need to spawn agents.
93
+ - Not a web service. Local only.
94
+ - Not Gmail. No threading, replies, CC/BCC, attachments, contacts, importance flags.
95
+ - Not a file-coordination tool. No file leases or advisory locks.
96
+ - Not push. Polling-only — call `read` when you want to check.
97
+
98
+ If you find yourself wishing for any of these, that's a signal to use a different tool, not to ask DLB to grow.
99
+
100
+ ## License
101
+
102
+ MIT.
@@ -0,0 +1,88 @@
1
+ # DLB — Dead Letter Box
2
+
3
+ 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.
4
+
5
+ ## Why
6
+
7
+ 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.
8
+
9
+ DLB does one thing: messages between agent sessions. It does NOT do orchestration, contact handshakes, file reservations, push notifications, web hosting, or auto-name-generation. If you want those, you want a different tool.
10
+
11
+ ## Five differentiators (vs. mcp_agent_mail and friends)
12
+
13
+ 1. **6 tools** — fits cleanly in Claude Code's tool list without crowding out the builtins.
14
+ 2. **No daemon** — each MCP call opens SQLite, runs one transaction, closes. No `server.lock`, no port management.
15
+ 3. **Names accepted as-is** — call yourself `alpha`, `ThreadBeta`, `worker-1`. DLB will not rename you.
16
+ 4. **Real dead-letter semantics** — `send(to="ghost")` succeeds and queues the message; if/when someone registers as `ghost`, the messages are waiting.
17
+ 5. **Zero ceremony** — `send` works on call one. Registration is optional and observational.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ uv tool install dlb-mcp
23
+ ```
24
+
25
+ Or zero-install:
26
+
27
+ ```bash
28
+ uvx dlb-mcp
29
+ ```
30
+
31
+ ## Wire into Claude Code
32
+
33
+ Add to `~/.claude.json`:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "dlb": {
39
+ "type": "stdio",
40
+ "command": "uvx",
41
+ "args": ["dlb-mcp"]
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ Multiple sessions can coexist — they all share `~/.dlb/store.sqlite3` via SQLite WAL mode.
48
+
49
+ ## The 6 tools
50
+
51
+ | Tool | What it does |
52
+ |---|---|
53
+ | `register(name, working_on=None, force=False)` | Declare a name + status. Returns `session_token`. Conflict on existing active name → error with suggestion. |
54
+ | `list_threads(active_within="24h")` | See who's around. No auth. |
55
+ | `send(to, body, subject=None, from_=None)` | Drop a message. Always succeeds — even if `to` doesn't exist yet. |
56
+ | `read(name, session_token, unread_only=True, limit=20)` | Read inbox. Requires session_token for registered names. |
57
+ | `ack(message_id, session_token)` | Explicit "I saw this and acted on it". Optional. |
58
+ | `unregister(name, session_token)` | Release the name. Messages preserved for re-registration. |
59
+
60
+ That's the entire API.
61
+
62
+ ## Configuration
63
+
64
+ | Env var | Default | What |
65
+ |---|---|---|
66
+ | `DLB_STORE` | `~/.dlb/store.sqlite3` | Path to the SQLite store |
67
+ | `DLB_MESSAGE_TTL_DAYS` | `7` | Days before unread messages expire |
68
+
69
+ ## Security model
70
+
71
+ - **Local-only.** File permissions (`~/.dlb/` is `chmod 700`) are the boundary.
72
+ - **Send is open.** Anyone can drop messages in any inbox. By design — DLB is a relay.
73
+ - **Read is gated.** Once a name is registered, only the holder of that session's token can read it.
74
+ - **No TLS, no accounts, no cross-host.** If you need those, you want a different tool.
75
+
76
+ ## What DLB is NOT
77
+
78
+ - Not an orchestrator. Use a script + your LLM SDK if you need to spawn agents.
79
+ - Not a web service. Local only.
80
+ - Not Gmail. No threading, replies, CC/BCC, attachments, contacts, importance flags.
81
+ - Not a file-coordination tool. No file leases or advisory locks.
82
+ - Not push. Polling-only — call `read` when you want to check.
83
+
84
+ If you find yourself wishing for any of these, that's a signal to use a different tool, not to ask DLB to grow.
85
+
86
+ ## License
87
+
88
+ MIT.
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "dlb-mcp"
3
+ version = "0.1.0"
4
+ description = "Dead Letter Box: a tiny MCP server for independent agent sessions to leave each other notes. Six tools, no daemon, real dead-letter semantics."
5
+ readme = "README.md"
6
+ authors = [{ name = "Jordan S Pilgrim" }]
7
+ license = { text = "MIT" }
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "mcp[cli]>=1.2.0",
11
+ ]
12
+
13
+ [project.urls]
14
+ Homepage = "https://github.com/jordanspilgrim/dlb-mcp"
15
+ Repository = "https://github.com/jordanspilgrim/dlb-mcp"
16
+ Issues = "https://github.com/jordanspilgrim/dlb-mcp/issues"
17
+
18
+ [project.scripts]
19
+ dlb-mcp = "dlb_mcp.__main__:main"
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/dlb_mcp"]
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "pytest>=8.0",
31
+ "pytest-asyncio>=0.24",
32
+ "ruff>=0.7",
33
+ ]
34
+
35
+ [tool.ruff]
36
+ line-length = 100
37
+ target-version = "py311"
38
+
39
+ [tool.ruff.lint]
40
+ select = ["E", "F", "I", "UP", "B", "SIM"]
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ asyncio_mode = "auto"
@@ -0,0 +1,6 @@
1
+ """Dead Letter Box — a tiny MCP server for inter-agent messaging.
2
+
3
+ Six tools, no daemon, real dead-letter semantics. See README.md.
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1,34 @@
1
+ """Entry point: ``dlb-mcp`` or ``python -m dlb_mcp``.
2
+
3
+ Single subcommand for v1: run the stdio MCP server. We don't bother with a
4
+ real CLI parser — there's nothing else to do.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+ from .server import serve_stdio
12
+
13
+
14
+ def main() -> None:
15
+ # Accept --help / -h politely so users probing the binary aren't confused
16
+ if len(sys.argv) > 1 and sys.argv[1] in ("--help", "-h"):
17
+ sys.stdout.write(
18
+ "dlb-mcp — Dead Letter Box MCP server (stdio transport).\n"
19
+ "\n"
20
+ "Usage: dlb-mcp\n"
21
+ "\n"
22
+ "Run this command as your MCP server entry; it speaks the MCP\n"
23
+ "protocol over stdin/stdout. Not interactive.\n"
24
+ "\n"
25
+ "Env vars:\n"
26
+ " DLB_STORE Path to SQLite store (default ~/.dlb/store.sqlite3)\n"
27
+ " DLB_MESSAGE_TTL_DAYS Days before unread messages expire (default 7)\n"
28
+ )
29
+ return
30
+ serve_stdio()
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()
@@ -0,0 +1,200 @@
1
+ """FastMCP server exposing DLB's six tools over stdio.
2
+
3
+ This module is a thin façade — all real logic lives in store.py. The tools
4
+ here exist only to:
5
+ 1. Receive MCP arguments and dispatch to store functions
6
+ 2. Convert store dataclasses to plain dicts for JSON-RPC serialization
7
+ 3. Translate store exceptions into MCP-friendly error responses
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import timedelta
13
+ from typing import Any
14
+
15
+ from mcp.server.fastmcp import FastMCP
16
+
17
+ from . import store
18
+
19
+ mcp = FastMCP(
20
+ name="dlb",
21
+ instructions=(
22
+ "DLB — Dead Letter Box. Six tools for inter-agent messaging:\n"
23
+ " register, list_threads, send, read, ack, unregister.\n"
24
+ "Send is open (no auth). Read/ack/unregister on a registered name "
25
+ "require its session_token.\n"
26
+ "Sends to nonexistent recipients are queued and delivered on later registration."
27
+ ),
28
+ )
29
+
30
+
31
+ # ── Serialization helpers ────────────────────────────────────────────────────
32
+
33
+
34
+ def _agent_summary_dict(s: store.AgentSummary) -> dict[str, Any]:
35
+ return {
36
+ "name": s.name,
37
+ "working_on": s.working_on,
38
+ "last_seen": s.last_seen.isoformat(),
39
+ "unread_count": s.unread_count,
40
+ "stale": s.stale,
41
+ }
42
+
43
+
44
+ def _message_dict(m: store.Message) -> dict[str, Any]:
45
+ return {
46
+ "id": m.id,
47
+ "recipient_name": m.recipient_name,
48
+ "sender_name": m.sender_name,
49
+ "subject": m.subject,
50
+ "body": m.body,
51
+ "sent_at": m.sent_at.isoformat(),
52
+ "read_at": m.read_at.isoformat() if m.read_at else None,
53
+ "expires_at": m.expires_at.isoformat(),
54
+ }
55
+
56
+
57
+ # ── Tools ────────────────────────────────────────────────────────────────────
58
+
59
+
60
+ @mcp.tool()
61
+ def register(
62
+ name: str,
63
+ working_on: str | None = None,
64
+ force: bool = False,
65
+ ) -> dict[str, Any]:
66
+ """Declare or refresh an agent identity.
67
+
68
+ Returns the agent record plus a `session_token` you must keep — it's
69
+ required for `read`, `ack`, and `unregister` on this name.
70
+
71
+ Conflict behavior: if the name is already registered to an active
72
+ session and you don't pass `force=True`, raises an error with up to
73
+ three suggested alternatives (e.g., `alpha-2`, `alpha-3`).
74
+
75
+ Args:
76
+ name: Stable identifier you'll be addressed by. Accepted as-given
77
+ (no rename, no auto-suffix). Examples: "alpha", "ThreadBeta",
78
+ "worker-1".
79
+ working_on: Free-text description of what this agent is doing.
80
+ Shown to other agents via `list_threads`. Optional.
81
+ force: If True, evict the prior session holding this name and
82
+ take it over. Use with care — silently kicks the previous owner.
83
+ """
84
+ return store.register(name, working_on=working_on, force=force)
85
+
86
+
87
+ @mcp.tool()
88
+ def list_threads(active_within_hours: int = 24) -> list[dict[str, Any]]:
89
+ """List all known agents with their unread counts and staleness flags.
90
+
91
+ No auth required. Use this to discover who's around before sending.
92
+ Agents whose `last_seen` is older than `active_within_hours` are marked
93
+ `stale=True` but still returned (so you can see "alpha was here last week").
94
+
95
+ Args:
96
+ active_within_hours: How many hours back to consider 'active' for
97
+ the staleness flag. Default 24.
98
+ """
99
+ items = store.list_threads(active_within=timedelta(hours=active_within_hours))
100
+ return [_agent_summary_dict(s) for s in items]
101
+
102
+
103
+ @mcp.tool()
104
+ def send(
105
+ to: str,
106
+ body: str,
107
+ subject: str | None = None,
108
+ from_: str | None = None,
109
+ ) -> dict[str, Any]:
110
+ """Drop a message into `to`'s inbox.
111
+
112
+ ALWAYS succeeds, even if `to` is not registered yet. This is DLB's
113
+ namesake feature — messages queue under the name, and will be in the
114
+ recipient's inbox when they register (or whenever someone reads under
115
+ that name).
116
+
117
+ No auth required. The sender is identified by the `from_` field you
118
+ pass; if omitted, it's the literal string "anonymous". DLB does not
119
+ try to auto-identify the caller.
120
+
121
+ Args:
122
+ to: Recipient name.
123
+ body: Message body (free text / Markdown).
124
+ subject: Optional short subject line.
125
+ from_: Sender label. Defaults to "anonymous".
126
+ """
127
+ msg = store.send(to=to, body=body, subject=subject, from_=from_)
128
+ return _message_dict(msg)
129
+
130
+
131
+ @mcp.tool()
132
+ def read(
133
+ name: str,
134
+ session_token: str | None = None,
135
+ unread_only: bool = True,
136
+ limit: int = 20,
137
+ ) -> list[dict[str, Any]]:
138
+ """Read messages in `name`'s inbox. Auto-marks them read.
139
+
140
+ Auth rules:
141
+ - If `name` is registered, `session_token` must match.
142
+ - If `name` is NOT registered, anyone can read (no owner to protect).
143
+ You can use this to peek at the dead-letter queue before claiming
144
+ a name.
145
+
146
+ Args:
147
+ name: The inbox to read.
148
+ session_token: Required if `name` is registered; ignored otherwise.
149
+ unread_only: If True (default), only returns messages with read_at
150
+ still NULL at the time of this call.
151
+ limit: Max messages to return (1-1000). Default 20.
152
+ """
153
+ msgs = store.read(
154
+ name=name,
155
+ session_token=session_token,
156
+ unread_only=unread_only,
157
+ limit=limit,
158
+ )
159
+ return [_message_dict(m) for m in msgs]
160
+
161
+
162
+ @mcp.tool()
163
+ def ack(message_id: int, session_token: str) -> dict[str, Any]:
164
+ """Explicitly acknowledge a message ("I saw this and acted on it").
165
+
166
+ Optional — `read` already marks messages as read. Use ack to record a
167
+ stronger signal. v1 treats ack as "definitely read" (sets read_at if
168
+ not already set). The read/ack distinction is reserved for future use.
169
+
170
+ Args:
171
+ message_id: The message ID from `send` or `read`.
172
+ session_token: Token for the message's recipient. Required.
173
+ """
174
+ ok = store.ack(message_id, session_token)
175
+ return {"ok": ok, "message_id": message_id}
176
+
177
+
178
+ @mcp.tool()
179
+ def unregister(name: str, session_token: str) -> dict[str, Any]:
180
+ """Release a name. Messages addressed to it are PRESERVED.
181
+
182
+ Re-registering the same name (by anyone) restores access to the
183
+ waiting messages. Use this when an agent session is exiting and
184
+ won't be back, OR when you want to hand the name to someone else
185
+ cleanly (instead of letting them `force` it).
186
+
187
+ Args:
188
+ name: The name to release.
189
+ session_token: Must match the current holder.
190
+ """
191
+ ok = store.unregister(name, session_token)
192
+ return {"ok": ok, "name": name}
193
+
194
+
195
+ # ── Stdio entry point ────────────────────────────────────────────────────────
196
+
197
+
198
+ def serve_stdio() -> None:
199
+ """Run the MCP server over stdio. Used by Claude Code / Cursor / Codex."""
200
+ mcp.run("stdio")