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.
- dlb_mcp-0.1.0/.github/workflows/ci.yml +40 -0
- dlb_mcp-0.1.0/.gitignore +31 -0
- dlb_mcp-0.1.0/LICENSE +21 -0
- dlb_mcp-0.1.0/PKG-INFO +102 -0
- dlb_mcp-0.1.0/README.md +88 -0
- dlb_mcp-0.1.0/pyproject.toml +44 -0
- dlb_mcp-0.1.0/src/dlb_mcp/__init__.py +6 -0
- dlb_mcp-0.1.0/src/dlb_mcp/__main__.py +34 -0
- dlb_mcp-0.1.0/src/dlb_mcp/server.py +200 -0
- dlb_mcp-0.1.0/src/dlb_mcp/store.py +562 -0
- dlb_mcp-0.1.0/tests/__init__.py +0 -0
- dlb_mcp-0.1.0/tests/conftest.py +17 -0
- dlb_mcp-0.1.0/tests/test_server_integration.py +178 -0
- dlb_mcp-0.1.0/tests/test_store.py +269 -0
- dlb_mcp-0.1.0/uv.lock +893 -0
|
@@ -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
|
dlb_mcp-0.1.0/.gitignore
ADDED
|
@@ -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.
|
dlb_mcp-0.1.0/README.md
ADDED
|
@@ -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,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")
|