Relay is a local-first, AI-friendly task manager. AI and humans "relay" tasks between each other, and the CLI/MCP interfaces are first-class — equal to any GUI.
- Local-first. Everything runs offline against a single SQLite-compatible database in your data directory. No account, and no Relay-operated server.
- AI-friendly. The MCP server exposes the same capabilities as the CLI, so an agent can create, search, update, and complete tasks programmatically.
- Zero-config. Works immediately after install.
- Optional sync. Off by default. Opt in to sync with infrastructure you own — S3-compatible storage (R2/S3/MinIO) or a libSQL remote (Turso/sqld). See Sync.
Status: Phase 0 (MVP) — Rust CLI + TUI + MCP server on macOS. License: MIT.
cargo build --workspace # build all crates
cargo run -p relay-cli -- <args> # run a CLI commandThe binary is rly.
rly add "Write the report" --priority high # create a task
rly list # list active tasks
rly search "report" # full-text search
rly move <id> --status in_progress # change status
rly done <id> # complete a task
rly tui # launch the TUI
rly serve # start the MCP server (stdio)Run rly --help for the full command set (projects, tags, comments, links,
dependencies, recurrence, export/import).
Relay speaks the Model Context Protocol, so agents (Claude Code, Claude Desktop, etc.) get the same capabilities as the CLI. There are two transports — pick based on how many clients share one database.
The simplest setup. The MCP client launches its own rly serve process. Add to
your MCP client config (e.g. Claude Code ~/.claude.json / Claude Desktop
claude_desktop_config.json):
The default turso backend is single-process: it locks the database file, so
only one rly serve can hold it at a time. If you open Relay from several
projects/windows at once (each spawning its own stdio server), all but the first
fail to connect. Run one shared daemon instead and point every client at it
over HTTP:
rly serve --http # listens on http://127.0.0.1:7777/mcp (loopback only)
# rly serve --http --port 8000 # or set `mcp_http_port` in config.toml{
"mcpServers": {
"relay": { "type": "http", "url": "http://127.0.0.1:7777/mcp" }
}
}One daemon holds a single database connection and serves all clients
concurrently. It binds to loopback only and rejects non-loopback Host headers,
so no authentication is needed for local use.
To keep the daemon running, register it with your OS service manager:
macOS — Relay.app (recommended)
macos/install.sh builds a release Relay.app, bundles the rly binary
inside it (Relay.app/Contents/MacOS/rly), installs the app to
/Applications, and registers a launchd LaunchAgent that supervises the daemon
from that bundled binary:
./macos/install.shThe bundled path is stable across cargo clean / rebuilds in this checkout, so
the daemon (and every client's HTTP connection) survives day-to-day development.
The app's Setup window can re-detect the binary and toggle the daemon too.
macOS — launchd LaunchAgent (manual)
~/Library/LaunchAgents/com.example.relay-mcp.plist (replace the binary path
with your rly, e.g. the output of which rly):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.example.relay-mcp</string>
<key>ProgramArguments</key>
<array><string>/usr/local/bin/rly</string><string>serve</string><string>--http</string></array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict></plist>launchctl load ~/Library/LaunchAgents/com.example.relay-mcp.plistLinux — systemd user service
~/.config/systemd/user/relay-mcp.service:
[Unit]
Description=Relay shared MCP daemon
[Service]
ExecStart=%h/.cargo/bin/rly serve --http
Restart=always
[Install]
WantedBy=default.targetsystemctl --user enable --now relay-mcp.serviceThe native macOS desktop app (gui/ crate, binary relay-gui) is built with
gpui and
gpui-component. It is a 3-pane
Linear-style client (sidebar / virtualized task list / detail) that drives data
entirely through the shared rly serve --http daemon over MCP.
# Run a daemon first (or install the LaunchAgent):
rly serve --http
# Dev run (debug build):
./gui/run.sh
# Build a distributable Relay.app (bundles the rly binary inside, ad-hoc signed):
./gui/bundle.sh # -> target/Relay.app
./gui/bundle.sh /Applications
# Point at a non-default daemon:
RELAY_MCP_ENDPOINT=http://127.0.0.1:7777/mcp ./gui/run.shRequires Rust 1.95 (pinned in rust-toolchain.toml) and, on macOS, the Metal
Toolchain component (xcodebuild -downloadComponent MetalToolchain). Full design
and the phased build plan are in docs/GUI_GPUI.md.
Note on
macos/(SwiftUI): the original SwiftUI desktop app undermacos/is the predecessor ofgui/and is being superseded by the gpui app. It still builds and runs; it will be removed once the gpui app reaches full parity.
The desktop GUI is a three-pane, Linear-style shell:
- Left — sidebar: built-in views (All Tasks, Inbox, Review, Completed) and your projects, each with a live count.
- Center — work area: the task List (grouped by status) or Board (Kanban), switched from the top-right toggle.
- Right / overlay — detail: the selected task, or a project overview.
- Title bar: a global search + command bar (
⌘K) and New task (⌘N), available from anywhere. A status dot shows the live connection to the daemon.
Every screenshot below runs against a throwaway sample database.
Task list — all tasks grouped by status, with priority, tags, due dates, and
AI · Needs review badges for work an agent finished and handed back.
Board — the same tasks as a Kanban board, one column per status (Backlog → Todo → In Progress → Review → Completed).
Relay stores data in ~/Library/Application Support/relay/ (macOS) /
$XDG_DATA_HOME/relay/ (Linux). Two backends are available, selected by the
RELAY_STORAGE_BACKEND environment variable:
| Backend | Default | DB file | Full-text search |
|---|---|---|---|
turso (Rust) |
✅ yes | tasks.turso.db |
Tantivy (ngram) |
rusqlite |
no | tasks.db |
SQLite FTS5 |
turso is the default (a pure-Rust, SQLite-compatible engine; currently BETA).
rusqlite remains as a stable escape hatch. Because the two engines use
incompatible on-disk FTS, each backend keeps its own database file and never
touches the other. To move data from a legacy rusqlite database into turso:
RELAY_STORAGE_BACKEND=rusqlite rly export --output backup.json
rly import backup.jsonRelay is local-only until you opt in. Add a [sync] section to config.toml
pointing at storage you control, then:
rly sync status # dry-run: what would transfer
rly sync push # upload local changes
rly sync pull # download remote changesTwo providers, selected at runtime (no rebuild):
- S3-compatible (R2 / AWS S3 / MinIO) — diff-based object sync with 3-way
conflict detection (
local/remote/newer/interactive). - libSQL (Turso Cloud / self-hosted
sqld) — row-level sync via turso.
Secrets come from the environment (RELAY_S3_ACCESS_KEY_ID,
RELAY_S3_SECRET_ACCESS_KEY, RELAY_LIBSQL_AUTH_TOKEN). Full setup, the conflict
model, and limitations are in docs/SYNC.md.
Presentation: CLI (clap) / TUI (ratatui) / MCP server (rmcp; stdio or shared HTTP)
Desktop GUI (gpui → MCP/HTTP client; gui/ crate)
Service: Task / Project / Tag / Comment / Link services (pure CRUD)
Storage: Storage trait → RusqliteStorage | TursoStorage
Database: SQLite-compatible (turso default, rusqlite fallback)
The desktop GUI is a separate MCP client: it never opens the database, it
connects to the shared rly serve --http daemon over MCP — the same daemon the
CLI and Claude Code use. See Desktop GUI and
docs/GUI_GPUI.md.
Entity timestamps (created_at / updated_at / deleted_at / completed_at)
are RFC3339 (UTC) strings; deletes are logical (deleted_at set, never physical).
See docs/SPEC.md for the full specification.
MIT — see LICENSE.







{ "mcpServers": { "relay": { "type": "stdio", "command": "rly", "args": ["serve"] } } }