MCP Agent Mail (Rust)
"It's like Gmail for your coding agents!"
A mail-like coordination layer for AI coding agents, exposed as an MCP server with 45 tools and 25 resources, Git-backed archive, SQLite indexing, an interactive 16-screen TUI, a server-rendered web UI, and an agent-first robot CLI. The Rust rewrite of the original Python project (1,700+ stars).
Supported agents: Claude Code, Codex CLI, Gemini CLI, Oh My Pi (OMP), GitHub Copilot CLI, and any MCP-compatible client.
Watch the 23-minute walkthrough to see seven AI coding agents send over 1,000 messages to each other while implementing a development plan over two days.
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/mcp_agent_mail_rust/main/install.sh?$(date +%s)" | bash
Table of Contents
- TL;DR
- Why This Exists
- What People Are Saying
- Design Philosophy
- Rust vs. Python: Stress Test Results
- Installation
- Quick Start
- Agent Configuration
- Server Modes
- Operator CLI Surface
- The 45 MCP Tools
- TUI Operations Console
- Robot Mode (
am robot) - File Reservations
- Multi-Agent Coordination Workflows
- Web UI
- Deployment Validation
- Configuration
- Architecture
- ATC Learning Implementation Map
- Core Data Model
- How the System Works
- Search Architecture
- Coordination Algorithms and Safety Invariants
- Consistency and Recovery Model
- Comparison vs. Alternatives
- Development
- Performance and Benchmarking
- Mailbox Diagnostics (
am doctor) - Troubleshooting
- Limitations
- FAQ
- Appendix: Protocol Transcript
- Appendix: TUI vs. Web vs. Robot
- Documentation
- About Contributions
- License
TL;DR
The Problem: Modern projects often run multiple coding agents at once (backend, frontend, scripts, infra). Without a shared coordination fabric, agents overwrite each other's edits, miss critical context from parallel workstreams, and require humans to relay messages across tools and teams.
The Solution: Agent Mail gives every coding agent a persistent identity (e.g., GreenCastle), an inbox/outbox, searchable threaded conversations, and advisory file reservations (leases) to signal editing intent. Everything is backed by Git for human-auditable artifacts and SQLite for fast indexing and search.
Why Use Agent Mail?
Quick Example
# Start the server (the installer already configured your detected coding agents;
# `am setup run` re-runs that setup at any time)
am
# That's it. Server starts on 127.0.0.1:8765 with the interactive TUI.
# Agents coordinate through MCP tools:
# ensure_project(human_key="/abs/path")
# register_agent(project_key="/abs/path", program="claude-code", model="opus-4.6")
# file_reservation_paths(project_key="/abs/path", agent_name="BlueLake", paths=["src/**"], ttl_seconds=3600, exclusive=true)
# send_message(project_key="/abs/path", sender_name="BlueLake", to=["GreenCastle"], subject="Starting refactor", body_md="Taking src/**", thread_id="FEAT-123")
# fetch_inbox(project_key="/abs/path", agent_name="BlueLake")
# Or use the robot CLI for non-interactive agent workflows:
am robot status --project /abs/path --agent BlueLake
am robot inbox --project /abs/path --agent BlueLake --urgent --format json
am robot reservations --project /abs/path --agent BlueLake --conflicts
What Agent Conversations Look Like
Example exchange between two agents coordinating a refactor:
┌──────────────────────────────────────────────────────────────────────────────┐
│ Thread: FEAT-123 - Auth module refactor │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ GreenCastle -> BlueLake 2026-02-16 10:03 │ │
│ │ Subject: Starting auth refactor │ │
│ ├──────────────────────────────────────────────────────────────────────────┤ │
│ │ I'm reserving src/auth/** for the next hour. Can you focus on the API │ │
│ │ tests in tests/api/** instead? │ │
│ │ [ack_required: true] │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ BlueLake -> GreenCastle 2026-02-16 10:04 │ │
│ │ Subject: Re: Starting auth refactor │ │
│ ├──────────────────────────────────────────────────────────────────────────┤ │
│ │ Confirmed. Releasing my reservation on src/auth/** and taking │ │
│ │ tests/api/** exclusively. Will sync when I hit the auth middleware │ │
│ │ boundary. │ │
│ │ [ack: OK] │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ BlueLake -> GreenCastle 2026-02-16 10:31 │ │
│ │ Subject: Re: Starting auth refactor │ │
│ ├──────────────────────────────────────────────────────────────────────────┤ │
│ │ Found a broken assertion in tests/api/auth_test.rs:142 -- the expected │ │
│ │ token format changed. Heads up if you're touching the JWT issuer. │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ GreenCastle -> BlueLake 2026-02-16 10:33 │ │
│ │ Subject: Re: Starting auth refactor │ │
│ ├──────────────────────────────────────────────────────────────────────────┤ │
│ │ Good catch. I just changed the claims struct. Updated the test fixture │ │
│ │ in my commit. Releasing src/auth/** now -- all yours if you need it. │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
No human relay needed. Agents negotiate file ownership, flag breaking changes in real time, and hand off work through structured, threaded messages stored in Git.
Why This Exists
Modern projects often run multiple coding agents at once (backend, frontend, scripts, infra). Without a shared coordination fabric, agents overwrite each other's edits, miss critical context from parallel workstreams, and require humans to relay messages across tools and teams.
Agent Mail has been available since October 2025 and was designed around real multi-agent coding workloads across providers such as Claude Code, Codex CLI, and Gemini CLI. The adjacent Beads and bv tools make it more useful as a full coordination stack: Beads tracks work, bv helps pick the right next work, and Agent Mail carries the coordination traffic.
The Footguns Agent Mail Avoids
No "broadcast to all" mode. Given the option, many agents will overuse broadcast-style messaging. That is the equivalent of default reply-all in email: lots of irrelevant noise and wasted context.
Carefully refined API ergonomics. Bad MCP documentation and poor agent ergonomics quietly wreck reliability. Agent Mail's 45 tool definitions have gone through repeated real-world iteration so they work predictably without wasting tokens.
No git worktrees. Worktrees can slow development velocity and create reconciliation debt when agents diverge. Agent Mail takes the opposite approach: keep agents in one shared space, surface conflicts quickly, and give them tools to coordinate through them.
Advisory file reservations instead of hard locks. For this problem, advisory reservations fit better than hard locks. Agents can temporarily claim files while they work, reservations expire automatically, and stale claims can be reclaimed. That makes the system robust to crashed or reset agents; hard locks would not.
Semi-persistent identity. An identity that can last for the duration of a discrete task (for the purpose of coordination), but one that can also vanish without a trace and not break things. You don't want ringleader agents whose death takes down the whole system. Agent Mail identities are memorable (e.g., GreenCastle), but ephemeral by design.
Graph-aware task selection. If you have 200-500 tasks, you don't want agents randomly choosing them or wasting context communicating about what to do. There's usually a "right answer" for what each agent should work on, and that right answer comes from the dependency structure of the tasks. That's what bv computes using graph theory, like a compass that tells each agent which direction will unlock the most work overall.
What Agent Mail Gives You
- Prevents conflicts: Explicit file reservations (leases) for files/globs prevent agents from overwriting each other
- Reduces human relay work: Agents send messages directly to each other with threaded conversations, acknowledgments, and priority levels
- Keeps communication off the token budget: Messages stored in per-project Git archive, not consuming agent context windows
- Offers quick reads:
resource://inbox/{Agent}?project=<abs-path>,resource://thread/{id}?project=<abs-path>, and 23 other MCP resources - Provides full audit trails: Every instruction, lease, message, and attachment is in Git for human review
- Scales across repos: Frontend and backend agents in different repos coordinate through the product bus and contact system
Typical Use Cases
- Multiple agents splitting a large refactor across services while staying in sync
- Frontend and backend agent teams coordinating thread-by-thread across repositories
- Protecting critical migrations with exclusive file reservations and pre-commit guards
- Searching and summarizing long technical discussions as threads evolve
- Running agent swarms with Beads task tracking for dependency-aware work selection
Productivity Math
Parallel agent work changes the economics of supervision. One human operator can spend an hour steering several agents while those agents produce many hours of implementation work in parallel. The exact multiplier depends on the task and on how disciplined the workflow is, but the point is straightforward: coordination overhead matters, and Agent Mail is built to keep that overhead low.
What People Are Saying
"Agent Mail and Beads feel like the first 'agent-native' tooling." — @jefftangx
"Agent mail is a truly brain-melting experience the first time. Thanks for building it." — @quastora
"Between Claude Code, Codex CLI, and Gemini; Beads; and Agent Mail — basically already 80% the way to the autonomous corporation. It blows my mind this all works now!" — @curious_vii
"Use it with agent mail == holy grail." — @metapog
"The only correct answer to this is mcp agent mail." — @skillcreatorai
"GPT 5.2 suggesting beads + agent mail for agent co-ordination (of course, I am already using them)." — @jjpcodes
Design Philosophy
Mail metaphor, not chat. Agents send discrete messages with subjects, recipients, and thread IDs. Work coordination is structured communication with clear intent, not a firehose. Imagine if your email system at work defaulted to reply-all every time; that's what chat-based coordination does, and it burns context fast.
SQLite for live state, Git for the durable ledger. The accepted write path mutates SQLite first so inboxes, resources, TUI, web, and robot views see one fresh operational state. The same write then emits human-readable message, profile, and reservation artifacts into the per-project Git archive for audit and recovery.
Advisory, not mandatory. File reservations are advisory leases, not hard locks. The pre-commit guard enforces them at commit time, but agents can override if needed. Acquiring a reservation reports conflicts instead of waiting for another agent to release its lease. Reservations expire on a TTL; they do not prevent deadlocks in the storage or runtime layers.
Resilient to agent death. Agents die all the time: context windows overflow, sessions crash, memory gets wiped. Any agent can vanish without breaking the system. No ringleader agents, no single points of failure. Semi-persistent identities exist for coordination but don't create hard dependencies.
Dual persistence. Human-readable Markdown in Git for auditability; SQLite for live indexing plus Search V3 for fast lexical/semantic retrieval. Both stay in sync through the write pipeline.
Structured concurrency, no Tokio. The entire async stack uses asupersync with Cx-threaded structured concurrency. No orphan tasks, cancel-correct channels, and deterministic testing with virtual time.
Rust vs. Python: Stress Test Results
The Python implementation had three recurring failure modes under real multi-agent workloads: Git lock file contention from concurrent writes, SQLite pool exhaustion under sustained load, and cascading failures when many agents hit the server simultaneously. The Rust rewrite addresses these through batching, bounded pools, and backpressure. The measurements below are retained results from the original stress campaign, not a passing verdict for the current release candidate or a guarantee for every workload (the heavy multi-agent stress tests in crates/mcp-agent-mail-storage/tests/stress_pipeline.rs are marked #[ignore] for explicit -- --ignored stress runs and currently enforce a 120s release-host ceiling, measured at 38–47s after durable archive-fsync hardening).
The 10-Test Gauntlet
Python Problem → Rust Fix
What Makes the Difference
- Fewer Git lock acquisitions. The commit coalescer batches rapid-fire writes into fewer git commits (9.1x reduction in the recorded run). Git plumbing commits avoid
index.lockin the normal archive path; other Git operations can still contend. - Pool exhaustion handled gracefully. Even with 4x more threads than pool connections (60 vs 15), all 600 operations succeeded with 0 timeouts. WAL mode + a bounded 20s
busy_timeoutlets writers queue rather than fail (and give up before the 30s client deadline). - Stale lock recovery works. Crashed-process lock files are detected via PID checking and cleaned up automatically, so a dead agent never holds the archive hostage.
- Write-behind queue backpressure is clean. 2000 rapid-fire enqueues from 20 threads — all accepted with 0 fallbacks or errors.
- Read/write concurrency is solid. Concurrent inbox reads and message writes produce 0 errors. WAL mode allows unlimited readers alongside writers.
The stress tests live in crates/mcp-agent-mail-storage/tests/stress_pipeline.rs (Rust unit tests targeting the DB+Git pipeline) and tests/e2e/test_stress_load.sh (HTTP E2E tests hammering a live server through the full network→server→DB→git pipeline).
Installation
One-Liner (recommended)
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/mcp_agent_mail_rust/main/install.sh?$(date +%s)" | bash
Prerequisites: the installer verifies the release with minisign and refuses to install without it, and minimal Linux images also lack xz. macOS: brew install minisign. Debian 12+ and Ubuntu 24.04+: apt install minisign xz-utils. Ubuntu 22.04 and Debian 10 have no minisign package: use the static binary from the minisign releases.
Downloads the right binary for your platform, installs to ~/.local/bin, optionally updates your PATH, and auto-configures detected Codex CLI configs for HTTP MCP URL mode. Downloaded release archives are verified before extraction by default. The requested version is normalized to the release tag vX.Y.Z (or vX.Y.Z-prerelease), and the trust model depends on the release generation:
- Releases v0.3.31 and later (the current model): releases are built and published by the maintainer's own release infrastructure rather than GitHub Actions, and every release ships a
SHA256SUMSmanifest plus a detached minisign signature (SHA256SUMS.minisig) made with the maintainer-held release key (id1BBD79B28BF718D0; the public key is pinned inside the installer). The installer verifies the signature over the exact manifest bytes withminisign, then verifies the archive's SHA-256 against the authenticated manifest. A missingminisignexecutable, manifest, signature, or checksum entry aborts the install;cosignis not required or consulted for these releases. This replaced the earlier keyless GitHub-Actions Sigstore requirement, which had become unsatisfiable once releases stopped being built by Actions — the trust anchor is now a key the maintainer controls (the same model as the maintainer's other released tools), and verification remains fail-closed.am self-updateapplies the same model with a built-in minisign verifier (the release public key is embedded in the binary; nominisignexecutable is needed) and refuses unsigned or tampered manifests. See SECURITY.md. - Releases before v0.3.31 (legacy): the installer requires a SHA-256 witness plus a non-empty modern Sigstore bundle that
cosignvalidates against the literal workflow identityhttps://github.com/Dicklesworthstone/mcp_agent_mail_rust/.github/workflows/dist.yml@refs/tags/<requested-tag>and OIDC issuerhttps://token.actions.githubusercontent.com, so a valid archive and bundle from any other tag are rejected. A stablecosignv3.1.3 or newer in the v3 line must be onPATH; older versions are rejected because of GHSA-fx35-mq7g-6g98, and v4 is not accepted until its verifier contract is reviewed. Verification forces the modern bundle parser and ignores caller-suppliedSIGSTORE_ROOT_FILE,SIGSTORE_REKOR_PUBLIC_KEY, andSIGSTORE_CT_LOG_PUBLIC_KEY_FILEoverrides.
In both models, a missing checksum, SHA-256 implementation, signature witness, or required verifier executable—and any malformed or mismatched signature evidence—aborts the install.
Options: --version vX.Y.Z, --dest DIR, --system (installs to /usr/local/bin), --from-source, --verify (run an additional post-install self-test), --no-verify (unsafe explicit escape that skips the checksum and signature checks), --easy-mode (auto-update PATH), --force, --no-service (never install/modify/restart the background service; also implied automatically by a non-default --dest), --uninstall, --yes, --purge. --no-verify allows unauthenticated downloaded binaries to execute during mandatory version probes before installation; malicious archive bytes can therefore run arbitrary code as the installer user. Archive verification applies to downloaded release archives, not source builds. Every downloaded archive must still contain exactly the two expected flat, non-empty regular-file members, and both staged binaries and both installed binaries must report the exact requested version. Those member/version checks are mandatory even with --no-verify; the flag also does not disable the optional broader --verify post-install self-test. A failed archive download never silently changes into a source build. Explicit --from-source checks out the exact requested tag, obtains immutable sibling revisions from that tag's release workflow, builds with cargo --locked, and subjects the staged and installed pair to the same exact-version and byte-preserving transactional replacement gates.
A successful source install prints the path of a mode-private source-receipt retained inside its committed transaction-history directory. The hash-witnessed receipt binds the normalized release tag, the exact Agent Mail/frankensearch/fast_cmaes/beads_rust commits, and both installed binary SHA-256 digests. On current source, the Beads commit records an additional installer checkout; the compiled beads_rust package comes from the registry version and checksum in Cargo.lock. Release tags that predate the immutable dependency-pin record fail closed rather than building mutable sibling branches. Interrupted or rolled-back transactions retain their evidence under an explicitly non-committed history name and are never reported as installed source provenance.
Windows One-Liner (PowerShell)
iwr -useb "https://raw.githubusercontent.com/Dicklesworthstone/mcp_agent_mail_rust/main/install.ps1?$(Get-Random)" | iex
PowerShell enforces the same per-release trust contract before Expand-Archive: for releases v0.3.31 and later it verifies the minisign-signed SHA256SUMS manifest with the same pinned maintainer key (a minisign executable on PATH is required; cosign is not consulted), and for older releases it requires the same stable cosign v3.1.3-or-newer-in-v3 verifier, forces modern-bundle parsing, and isolates verification from the three custom Sigstore trust environment variables named above. The ZIP must contain exactly flat am.exe and mcp-agent-mail.exe, and their staged and post-install version lines must exactly match the requested release. Both executables are replaced under one per-destination installer mutex and rollback domain; backups remain until byte-for-byte installed-digest and version checks pass. Options: -Version vX.Y.Z, -Dest PATH, -Force, -Verify (explicitly request the already-default cryptographic checks), and -NoVerify (the unsafe escape that skips only those checksum and signature checks). Archive-member and exact-version checks still run with -NoVerify, which means unauthenticated downloaded executables run during version probes and may execute arbitrary code as the installer user.
Container (Docker or Podman)
Each release also publishes a linux/amd64 + linux/arm64 image to GHCR, tagged vX.Y.Z, X.Y.Z, 0.3 and latest:
docker run -d --name agent-mail -p 127.0.0.1:8765:8765 \
-e HTTP_BEARER_TOKEN="$(openssl rand -hex 32)" \
-v agent-mail-data:/data/mailbox \
ghcr.io/dicklesworthstone/mcp_agent_mail_rust:latest
The image runs mcp-agent-mail serve --no-tui as the non-root appuser, serving MCP on port 8765 at /mcp/, with the whole mailbox (Git archive and SQLite) under /data/mailbox. Mount a volume there so mail survives replacing the container. Every request needs the token (Authorization: Bearer <token>); clients connect to http://127.0.0.1:8765/mcp/ with it. Keep the published port on loopback unless remote agents should reach it. Docker's healthcheck polls /health/liveness. The image packages the release's Linux glibc binaries unchanged (Dockerfile.release).
From Source
git clone https://github.com/Dicklesworthstone/mcp_agent_mail_rust
cd mcp_agent_mail_rust
./install-local.sh # builds release, installs to ~/.local/bin
# DEST=/usr/local/bin ./install-local.sh # custom destination
The script resolves the correct Cargo target directory via cargo metadata, so the
installed binary always matches the freshly-built artifact regardless of
CARGO_TARGET_DIR overrides or workspace settings. Do not manually copy from
target/release/am -- if CARGO_TARGET_DIR is set, that path may be stale.
Requires Rust nightly (see rust-toolchain.toml). On current main, Cargo fetches FrankenSearch 0.6.1 at revision 616c9a7a6bdada97d81f760808996911ba3be294, recorded in Cargo.toml and Cargo.lock and aligned with FRANKENSEARCH_COMMIT in .github/workflows/dist.yml. This revision shares FastMCP's rustix 1.1.5 requirement; the enabled search features retain local Model2Vec and native reranking without FastEmbed or Tokio. A manual build needs no FrankenSearch, fast_cmaes, or Beads sibling checkout. Use cargo build --locked to retain the checked-in dependency graph. Earlier release tags may still require the gated ../frankensearch-rel-0332 and ../fast_cmaes checkouts; install.sh --from-source provisions their recorded revisions. The installer and container recipes retain those additional checkouts for provenance compatibility, while current Cargo builds consume the pinned git source. The mailbox uses registry FrankenSQLite =0.4.10 through SQLModel 0.5.0 on Asupersync 0.5.0. FastMCP 0.10.0 remains pinned to an immutable revision preserving protocol negotiation. Registry beads_rust =0.6.0 retains its separate FrankenSQLite 0.3.18 engine, patched to an immutable Git revision, with default features disabled. See UPGRADE_LOG.md for engine qualification results.
Platforms
Quick Start
1. Start the server
am
Starts the HTTP server on 127.0.0.1:8765 with the interactive TUI. It leaves existing MCP client configuration untouched: the curl installer configures detected coding agents (Claude Code, Codex CLI, Gemini CLI, OMP, etc.), and am setup run (or am serve-http --setup) configures them explicitly afterwards.
2. Agents register and coordinate
Once the server is running, agents use MCP tools to coordinate:
Terminology note: ensure_project takes a human_key, which must be the absolute repo path. Most follow-on tools take project_key, which can be that same absolute path or the project's computed slug.
# Register identity
ensure_project(human_key="/abs/path/to/repo")
register_agent(project_key="/abs/path/to/repo", program="claude-code", model="opus-4.6")
# Reserve files before editing
file_reservation_paths(project_key="/abs/path/to/repo", agent_name="GreenCastle", paths=["src/**"], ttl_seconds=3600, exclusive=true)
# Send a message
send_message(project_key="/abs/path/to/repo", sender_name="GreenCastle", to=["BlueLake"],
subject="Starting auth refactor", body_md="Taking src/auth/**",
thread_id="FEAT-123", ack_required=true)
# Check inbox
fetch_inbox(project_key="/abs/path/to/repo", agent_name="BlueLake")
acknowledge_message(project_key="/abs/path/to/repo", agent_name="BlueLake", message_id=123)
3. Use macros for common flows
# Boot a full session (ensure project + register agent + reserve files + fetch inbox)
macro_start_session(human_key="/abs/path/to/repo", program="claude-code", model="opus-4.6")
# Prepare for a thread (fetch context + recent messages)
macro_prepare_thread(project_key="/abs/path/to/repo", thread_id="FEAT-123",
program="claude-code", model="opus-4.6")
# Reserve, work, release cycle
macro_file_reservation_cycle(project_key="/abs/path/to/repo", agent_name="GreenCastle",
paths=["src/auth/**"], ttl_seconds=3600, auto_release=true)
# Contact handshake between agents in different projects: approves the link and
# delivers the welcome into the target's project (without auto_accept the link
# stays pending, no welcome is sent, and the response says why; auto_accept never
# overrides a link the target has blocked)
macro_contact_handshake(project_key="/abs/path/to/repo", requester="GreenCastle",
target="BlueLake", to_project="/abs/path/to/other/repo",
auto_accept=true, welcome_subject="Coordination channel",
welcome_body="Use thread FEAT-123 for the cutover")
Agent Configuration
The installer and am command auto-detect installed agents and write their configs in HTTP URL mode. The examples below are the manual fallback.
Claude Code
Add to your project's .mcp.json or ~/.claude/settings.json:
{
"mcpServers": {
"agent-mail": {
"command": "mcp-agent-mail",
"args": []
}
}
}
Or for HTTP transport (when the server is already running):
{
"mcpServers": {
"agent-mail": {
"type": "url",
"url": "http://127.0.0.1:8765/mcp/"
}
}
}
Codex CLI
The curl installer writes this automatically for detected Codex CLI installs. For source installs, manual setup, or custom endpoint overrides, add this to ~/.codex/config.toml:
[mcp_servers.mcp_agent_mail]
url = "http://127.0.0.1:8765/mcp/"
# Add this when HTTP bearer auth is enabled:
http_headers = { Authorization = "Bearer <HTTP_BEARER_TOKEN>" }
Gemini CLI
Add to ~/.gemini/settings.json:
{
"mcpServers": {
"agent-mail": {
"command": "mcp-agent-mail",
"args": []
}
}
}
For a project-local Gemini setup plus Agent Mail identity registration:
scripts/register_gemini.sh /abs/path/to/repo
AGENT_NAME=BlueLake AGENT_MODEL=gemini-2.5-pro scripts/register_gemini.sh /abs/path/to/repo
Oh My Pi (OMP)
am setup run --agent omp writes OMP's native project config at .omp/mcp.json
and the active profile's user config. The default profile uses
~/.omp/agent/mcp.json; named profiles selected through OMP_PROFILE (or the
legacy PI_PROFILE) use ~/.omp/profiles/<name>/agent/mcp.json. Profile names
must use OMP's lowercase [a-z0-9][a-z0-9._-]{0,63} syntax; an invalid
explicit profile fails closed instead of redirecting setup to the default.
Setup also honors OMP's PI_CONFIG_DIR and default-profile PI_CODING_AGENT_DIR
overrides, with a stricter path-safety contract: neither may contain ..
components. PI_CONFIG_DIR is rooted beneath the user's home directory;
a relative PI_CODING_AGENT_DIR is resolved against the working directory,
and an absolute path is accepted. For example, use agent instead of
foo/../agent. Invalid overrides cause setup to fail before writing config
files; setup also refuses symlink traversal when accessing those files.
The project config is profile-independent and applies under every named OMP
profile.
am setup run --agent omp --no-user-config leaves active-user bytes untouched,
but setup and status still read the authorities that decide whether the
project entry can run. They check the active canonical mcp.json
disabledServers list against all three accepted Agent Mail aliases
(mcp-agent-mail, mcp_agent_mail, and agent-mail). OMP also discovers the
read-only .mcp.json sibling after mcp.json in both project and active-user
scopes. An exact repeated server name is shadowed by the earlier source, while a
different Agent Mail alias remains a separate MCP key; setup status reports and
fingerprints that conflict without mirror-writing the secondary file. Status
also evaluates
mcp.enableProjectConfig with OMP's persistent settings precedence: the active
profile's config.yml (falling back to config.yaml only when it is absent).
When both main YAML names are absent, an existing legacy settings.json or
agent.db remains a migration authority and setup fails closed until OMP has
migrated it or an operator has inspected it. Project settings providers then
merge in runtime order: .omp/settings.json,
.omp/config.yml, .claude/settings.json, .codex/config.toml,
.gemini/settings.json, project-root opencode.json and opencode.jsonc,
.opencode/opencode.json and .opencode/opencode.jsonc, and
.cursor/settings.json. Ordered ambient PI_CONFIG_FILES overlays apply last.
Foreign-provider parse failures are skipped as OMP skips them. Unsafe or
unreadable paths still fail closed: OMP may follow a symlink or otherwise read
bytes that Agent Mail deliberately refuses to trust, so treating that authority
as absent could produce a false-green verdict. The active-profile YAML, native
.omp/config.yml, and explicit overlays are also strict authorities. An
effective false, an unsupported authority, or a missing explicit overlay is
reported as runtime-relevant drift instead of success. OpenCode {env:...} and
{file:...} substitutions are also reported
unsupported because their external dependencies cannot be proven by a cache of
the config bytes alone. Remediation deliberately omits --no-user-config,
because a project-only write cannot overcome an authority that excludes every
project MCP source.
This check is bound to the persistent files and the PI_CONFIG_FILES value in
the am process environment. A one-shot OMP --config argument or a
programmatic runtime settings override is launch-specific and is outside this
static setup/status boundary; run am setup status under the same ambient
overlay environment used to launch OMP.
To configure it manually:
{
"mcpServers": {
"mcp-agent-mail": {
"type": "http",
"url": "http://127.0.0.1:8765/mcp/"
}
}
}
When HTTP bearer authentication is enabled, add an Authorization entry under
headers; am setup run does this automatically and adds .omp/mcp.json to
the project .gitignore so the credential is not committed.
Any MCP-Compatible Client
Agent Mail supports both stdio and HTTP transports:
- stdio: Run
mcp-agent-mailas a subprocess (the default for most MCP clients) - HTTP: Connect to
http://127.0.0.1:8765/mcp/when the server is running viaamormcp-agent-mail serve
One server process owns a mailbox at a time. While am (or mcp-agent-mail serve) is serving a storage root, a stdio mcp-agent-mail started for the same root refuses to start ("another Agent Mail server is already serving this storage root") instead of contending for it, so configure clients with the HTTP URL whenever am runs; the installer and am setup run write HTTP URL entries.
- Examples: Token-free client templates live under
docs/examples/mcp/
Server Modes
MCP Server (default)
mcp-agent-mail # stdio transport (for MCP client integration)
mcp-agent-mail serve # HTTP server with TUI (default 127.0.0.1:8765)
mcp-agent-mail serve --no-tui # Headless server (CI/daemon mode)
mcp-agent-mail serve --reuse-running # Reuse existing server on same port
CLI Operator Tool
am # Start server + TUI (non-interactive: robot status)
am serve-http --port 9000 # Different port
am serve-http --setup # Explicitly update detected MCP clients before serving
am serve-http --host 0.0.0.0 # Bind to all interfaces
am serve-http --no-auth # Skip authentication (local dev)
am serve-http --path api # Use /api/ transport instead of /mcp/
am --help # Full operator CLI
Server startup preserves existing MCP client configuration by default, including
project and user-level URLs. Use am setup run to configure clients separately,
or am serve-http --setup to update detected clients to that launch's endpoint.
The setup flag can rewrite existing entries; omit it for temporary test servers.
For CLI deployments that require an explicitly supplied sending credential, set
AGENT_MAIL_REQUIRE_EXPLICIT_SENDER_TOKEN=1. am mail send then requires
--sender-token, --sender-token-file, or AGENT_MAIL_SENDER_TOKEN; it refuses
to borrow a token from persisted agent identity state. The same resolver applies
to queued-send replay and contact handshakes (use the environment variable where
the command has no token flag). The default retains automatic token reuse.
This controls CLI token selection; it does not establish MCP session ownership
or isolate hostile processes sharing the same OS account. Server-side verified
send enforcement remains MESSAGING_FAIL_CLOSED_SEND_PROFILE=true.
Session-bound agent identity (opt-in)
Available since v0.3.39 (see CHANGELOG.md).
Set MESSAGING_SESSION_IDENTITY=true on the server to let an MCP session act as
the identities it established without a registration token in the model-visible
transcript:
- The session. Over stdio, the session is the client's server process. Over
Streamable HTTP, a successful
initializereturns anMcp-Session-Idheader; requests that send it back (over any connection) share the session, andDELETEon the MCP endpoint with the header ends it. A session is tied to theAuthorizationit was created under: another credential, an unknown id or an expired id (24 h idle) gets a stateless request. Sessions live in memory, so a restart ends them. Bearer/JWT authorization is still checked on every request: this is identity context inside an authorized connection, not authentication. - Binding.
create_agent_identity(usereturn_registration_token: falseto keep the token out of the transcript) binds the new agent, and so does aregister_agentthat creates its agent. Naming an existing agent never binds it. - Acting as a bound agent.
send_message/reply_messagefrom the session as a bound agent are verified withoutsender_token("verified_sender": true, "sender_verification": "session"), including underMESSAGING_FAIL_CLOSED_SEND_PROFILE. - No borrowed names. A session that holds an identity in a project cannot,
in that project, send/reply, read an inbox (
fetch_inbox,fetch_inbox_events, or that project's view infetch_inbox_product), mark read or acknowledge, reserve/renew/release files, manage build slots, request a forced reservation release, or re-register (rewriting the profile and rotating the token) as any other agent: those calls fail withSESSION_IDENTITY_MISMATCH(a send or reply that carries the other agent's ownsender_tokenis still accepted). A session holding no identity in the project keeps the trusted-local behavior. - Database outages. ACKs and reservation releases can be queued durably after the acting identity is authorized. If an outage prevents that check and the session holds a potentially relevant binding, the call returns the database error without queuing an intent, even for its own supplied name. Retry after database access recovers. Already authorized intents remain replayable when another session triggers recovery.
- Lifecycle.
retire_agent/deregister_agentdrop the identity from the calling session; lifecycle authorization itself is unchanged (registration token, or a bound tmux pane over stdio).
This does not authenticate the model, provider or OS process, and it cannot isolate hostile clients that share one bearer credential and can read each other's session ids.
When an interactive am finds a healthy Agent Mail service already serving the
configured endpoint, it attaches a read-only terminal view to that service's
/mail/ws-state snapshot. It does not stop or restart the service, acquire a
mutation lock, or run service-control commands. --takeover remains the
explicit opt-in for a local replacement.
Dual-Mode Interface
This project keeps MCP server and CLI command surfaces separate:
Running CLI-only commands via the MCP binary produces a deterministic denial on stderr with exit code 2, and vice versa, preventing accidental mode confusion in automated workflows.
Operator CLI Surface
am is more than a launcher. It is the operator surface for runtime control, diagnostics, migration, exports, benchmarking, and agent-facing non-interactive workflows. In non-interactive contexts, bare am automatically falls back to robot output instead of trying to launch a blocking TUI.
Command Families
Setup Drift Reports
am setup status is read-only. It inventories supported MCP clients, reports the
current redacted server entry beside the expected entry, and labels drift such
as missing_file, legacy_stdio, stale_http_path, wrong_bearer_header,
wrong_startup_timeout, duplicate_server_entries, and unsupported_config.
Use the reported remediation in two steps:
am setup status --format json
am setup run --dry-run --project-dir "$PWD" --format toon
am setup run --yes --project-dir "$PWD" --format toon
For bearer-header checks, pass --token to am setup status or expose the
expected token through HTTP_BEARER_TOKEN / config.env; status output redacts
token values.
Family Detail
Durable Inbox Event Cursors
am inbox-events is the CLI for restart-safe inbox monitors. It reads
append-only, body-free delivery events oldest first; its --after value is a
delivery cursor, never a message ID. Persist next_cursor only after the
corresponding events have been processed.
# Establish a durable starting position without consuming events.
am inbox-events --project /abs/path --agent BlueLake --position-now --format json
# Resume from the last processed delivery cursor.
am inbox-events --project /abs/path --agent BlueLake --after 42 --limit 100 --format json
Each page includes events, next_cursor, has_more,
oldest_available_cursor, and tail_cursor. Retention records durable loss
markers for the affected recipient. Deleting another recipient's mail does
not expire your cursor, and deleting the newest or every retained event does
not move your durable tail backwards.
If retention removed a delivery after your saved position, the read fails
with code cursor_expired (the fetch_inbox_events tool reports
CURSOR_EXPIRED). This also detects holes inside retained history, even when
older events remain. The error's oldest_available_cursor identifies the first
position after the lost history; after handling the loss according to your
recovery policy, resume with --after set to that value minus one. The page's
oldest_available_cursor still describes its oldest retained event. To
intentionally start monitoring only future mail, use --position-now.
A cursor beyond the durable tail fails with cursor_ahead (CURSOR_AHEAD);
check that the cursor belongs to this mailbox and recipient before choosing a
new position. The migration preserves the database's existing issued-cursor
ceiling, including when all events were already deleted. It cannot reconstruct
which recipients lost events before durable retention markers were introduced.
The 45 MCP Tools
9 Clusters
fetch_inbox_events is the MCP equivalent of am inbox-events: it is a
durable, recipient-scoped delivery feed for monitors, not a replacement for
message-body inbox reads.
file_reservation_paths accepts an optional idempotency_key for an exact
retry after a timeout. The same key and normalized request replay the original
grant without a second lease or archive write and mark the response
idempotent_replay: true. Reusing a key with different arguments returns
IDEMPOTENCY_KEY_CONFLICT. Keys are scoped to (project, tool), expire after
the configured retention window, and are optional; this is not a blanket
idempotency claim for every mutating tool.
On replay, granted contains the original IDs and expiries, even after release
or expiry; it does not reacquire those paths. conflicts is a current snapshot.
A partial or empty original grant stays partial or empty; use a new key for a
new acquisition attempt. Fresh reacquisition of an active lease reuses its ID
and applies the requested exclusive/shared mode and reason after checking peers.
Keys recorded before the normalized path-set fingerprint update can return
IDEMPOTENCY_KEY_CONFLICT after an upgrade, even within their retention window.
The original grants remain unchanged; inspect them before making a new attempt.
25 MCP Resources
Read-only resources span environment/config inspection, project and agent discovery, inbox and thread views, reservation views, and tooling diagnostics. They are there so agents can fetch state cheaply without mutating anything.
resource://inbox/{Agent}?project=<abs-path>&limit=20
resource://thread/{id}?project=<abs-path>&include_bodies=true
resource://mailbox/{Agent}?project=<abs-path>
resource://views/ack-overdue/{Agent}?project=<abs-path>
resource://agents/<project-slug>
resource://file_reservations/<project-slug>?active_only=true
resource://tooling/metrics
resource://config/environment
resource://agents/... and resource://file_reservations/... take the project in the path segment. Inbox, mailbox, thread, and message resources put the agent or thread in the path and the project in the query string.
Macros vs. Granular Tools
- Prefer macros when you want speed or are on a smaller model:
macro_start_session,macro_prepare_thread,macro_file_reservation_cycle,macro_contact_handshake - Use granular tools when you need control:
register_agent,file_reservation_paths,send_message,fetch_inbox,acknowledge_message
TUI Operations Console
The interactive TUI has 16 screens. Jump directly with 1-9, 0 (screen 10), and shifted digits !, @, #, $, %, ^ (screens 11-16). Use Tab/Shift+Tab to cycle in order.
Global keys: ? help, Ctrl+P/: command palette, / the screen's own filter (global search focus on screens without one), . contextual action menu, Ctrl+N compose overlay, Ctrl+Y toast-focus mode, Ctrl+T/Shift+T cycle theme, m toggle MCP/API transport, q quit.
ATC keys: d decision detail, r retention report, i toggle detail pane, Tab switch agents/decisions.
Screen-specific highlights: Messages uses g for Local/Global inbox; Threads uses e/c for expand/collapse-all in conversation view; Timeline uses V for Events/Commits/Combined/Log and v for visual selection; Search uses f + facet rail navigation for scope/sort/field controls; Contacts uses n to cycle Table/Graph/Matrix (sender × recipient heatmap); batch-capable screens share Space/v/A/C; preset-enabled screens use Ctrl+S/Ctrl+L.
Command palette: Press Ctrl+P (or : outside text-entry) to open a searchable action launcher that includes screen navigation, transport/layout controls, and dynamic entities (agents/projects/threads/tools/reservations).
Themes: 42 named palettes are in the Ctrl+T/Shift+T cycle (Cyberpunk Aurora, Darcula, Lumen Light, Nordic Frost, High Contrast, Dracula, Monokai, Nord, and more; see NAMED_THEMES in crates/mcp-agent-mail-server/src/tui_theme.rs). The five config-level themes are selectable through TUI_THEME. Accessibility support includes high-contrast mode and reduced motion.
Archive Browser note: use Enter to expand/preview, Tab to switch tree vs preview pane, / to filter filenames, and Ctrl+D/U for preview paging.
If an interactive am attaches to an already running service, this terminal is
read-only and detaches with Ctrl-C; the service keeps serving MCP/API traffic.
Use an explicit --takeover only when replacement is intended.
Robot Mode (am robot)
Non-interactive, agent-first CLI surface for TUI-equivalent situational awareness. Use it when you need structured snapshots quickly, especially in automated loops and when tokens matter.
19 Subcommands
Output Formats
toon(default at TTY): Token-efficient, compact, optimized for agent parsingjson(default when piped): Machine-readable envelope with_meta,_alerts,_actionsmd(thread/message-focused): Human-readable narrative for deep context
am robot overview summarizes the whole mailbox; the global --project and
--agent flags do not scope this command. Message counts use one grouped query,
and active reservations use one candidate scan with release-ledger filtering.
--counts returns totals from the same aggregation. The snapshot cache is
process-local, so separate CLI invocations each read the database.
am robot atc reads the live ATC snapshot over /mail/ws-state when the local server is running and falls back to a local SQLite rollup/liveness view when that snapshot is unavailable. Use --since to trim recent decisions/executions, --stratum to focus open-stratum counts, and --summary-only for the compact health view.
am robot handoff correlates in-progress beads with Agent Mail activity, active file reservations, thread mail, and recent comments. It is always read-only: reopen/takeover rows contain proposed br update ... --status open --json commands, but agents must inspect reservations, peer dirty work, and the related thread before running them.
Agent Workflow Recipes
# Startup triage
am robot status --project /abs/path --agent AgentName
# Immediate urgency pass
am robot inbox --project /abs/path --agent AgentName --urgent --format json
# Incremental monitoring loop
am robot timeline --project /abs/path --agent AgentName --since 2026-02-16T10:00:00Z
# Deep thread drill-down
am robot thread br-123 --project /abs/path --agent AgentName --format md
# Reservation safety check before edits
am robot reservations --project /abs/path --agent AgentName --conflicts --expiring 30
# Heavy verification recommendation with build-slot admission and rch
am robot status --project /abs/path --agent AgentName --format json \
| jq '.recommendations[] | select(.category == "verification_lane")'
File Reservations for Multi-Agent Editing
Before editing, agents reserve file paths to avoid conflicts:
file_reservation_paths(project_key, agent_name, paths=["src/**"], ttl_seconds=3600, exclusive=true)
The pre-commit guard (mcp-agent-mail-guard) installs as a Git hook and blocks commits that touch files reserved by other agents. Reservations are advisory, TTL-based, and support glob patterns.
New reservation artifacts are named
id-<reservation-id>-g<db-generation>.json and carry the same generation in
their JSON. This prevents a re-created SQLite database from overwriting an
older generation's row-id artifact. Readers still recognize legacy
id-<reservation-id>.json artifacts; parity reports stamped artifacts from a
different live generation as foreign-generation debris rather than current
reservation drift.
The guard has narrow operational exemptions: .beads/** metadata is always
exempt so shared task-state commits cannot be stranded, and an agent's own
reservation does not conflict with its commit. Source files reserved by another
agent remain protected. When no matching mailbox archive can be found (including
a proven slug collision), the hook emits a warning and allows the commit rather
than turning stale/missing mailbox state into a universal commit gate.
The pre-push hook lists the paths a push touches with one streamed
git rev-list | git diff-tree --stdin pipeline per pushed ref, so its cost no
longer grows with a git process per commit. The scan is bounded by
AGENT_MAIL_GUARD_PUSH_MAX_COMMITS (default 2000 commits per ref, newest
first), AGENT_MAIL_GUARD_PUSH_MAX_PATHS (default 100000 path records per
ref) and AGENT_MAIL_GUARD_PUSH_TIMEOUT_SECS (default 120 seconds for the
whole push); 0 removes a bound. Reaching a bound truncates the scan: the
paths already read are still checked, and if none of them conflicts while
another agent holds an active lease, the hook fails closed (exit 2) naming what
was skipped and which variable to raise, the same way every other unfinished
inspection does. AGENT_MAIL_GUARD_MODE=warn turns that into a warning.
Complete Reservation Reads Through the Server
Use --server to read reservations through the configured authenticated MCP
endpoint while the server owns the mailbox:
am reservations --project /abs/path --all --json --server
am file_reservations active /abs/path --server
am file_reservations list /abs/path --all --server
These reads now use a complete database snapshot and can return more than the resource's normal 250-row page. Project identity, holder identities, lease rows, and the active-lease cutoff come from the same read snapshot. Historical leases retain their holder names even when those agents have retired. Navigation still applies the requested ordering and page after obtaining the complete set.
MCP clients can request the same envelope with
resource://file_reservations/<slug>?snapshot=true&active_only=true&format=json.
It includes schema_version, complete, project, agents, captured_ts,
active_only, count, and reservations. Set active_only=false to include
released and expired leases. Snapshot mode accepts no limit or offset and
refuses results exceeding 10,000 reservations, 10,000 holder identities, or
4 MiB of JSON. The CLI rejects incomplete snapshots and older servers that only
return pages; it does not display a truncated set as complete. Explicit resource
reads without snapshot=true retain their existing pagination behavior.
Multi-Agent Coordination Workflows
How Agents Interact (Protocol Flow)
sequenceDiagram
participant A as Agent A (GreenCastle)
participant S as Agent Mail Server
participant B as Agent B (BlueLake)
A->>S: ensure_project(human_key="/abs/path")
A->>S: register_agent(project_key="/abs/path", program="claude-code")
S-->>A: identity: GreenCastle
B->>S: register_agent(project_key="/abs/path", program="codex-cli")
S-->>B: identity: BlueLake
A->>S: file_reservation_paths(project_key="/abs/path", agent_name="GreenCastle", paths=["src/auth/**"], exclusive=true)
S-->>A: reserved ✓
B->>S: file_reservation_paths(project_key="/abs/path", agent_name="BlueLake", paths=["src/auth/**"], exclusive=true)
S-->>B: CONFLICT — reserved by GreenCastle
B->>S: file_reservation_paths(project_key="/abs/path", agent_name="BlueLake", paths=["tests/api/**"], exclusive=true)
S-->>B: reserved ✓
A->>S: send_message(project_key="/abs/path", sender_name="GreenCastle", to=["BlueLake"], thread_id="FEAT-123", ack_required=true)
S-->>B: new message in inbox
B->>S: fetch_inbox(project_key="/abs/path", agent_name="BlueLake")
S-->>B: [message from GreenCastle]
B->>S: acknowledge_message(project_key="/abs/path", agent_name="BlueLake", message_id=1482)
S-->>A: ack received ✓
A->>S: release_file_reservations(project_key="/abs/path", agent_name="GreenCastle", paths=["src/auth/**"])
S-->>A: released ✓
Same Repository
-
Register identity:
ensure_project(human_key=<abs-path>)+register_agent(project_key=<abs-path>, ...) -
Reserve files before editing:
file_reservation_paths(project_key, agent_name, paths=["src/**"], ttl_seconds=3600, exclusive=true) -
Communicate with threads:
send_message(..., thread_id="FEAT-123"), check withfetch_inbox, acknowledge withacknowledge_message -
Quick reads:
resource://inbox/{Agent}?project=<abs-path>&limit=20 -
Tell the whole project once: address the project's shared mailbox as
project:<slug>(orproject:<human_key>). Available since v0.3.39 (see CHANGELOG.md):send_message(project_key="/abs/path", sender_name="GreenCastle", to=["project:abs-path"], subject="Schema freeze until 18:00", body_md="Do not touch migrations/ until the release is cut.", ack_required=true)- The message is stored once, with one project delivery: no per-agent copies, rows, signals or archive inbox files. This is not
broadcast(which stays rejected). - Every other non-retired agent of the project that was registered when the message was sent sees it in
fetch_inbox(andresource://inbox/...,am robot inbox) with"via": "project". An agent that joins later is not handed the backlog; project history stays readable through threads,fetch_topicand search. An agent whosecontact_policyisblock_allopts out, and a direct to/cc/bcc delivery to the same agent takes precedence so nothing appears twice. - Read and acknowledgement state is per agent and recorded lazily:
fetch_inboxread receipts,mark_message_read,acknowledge_messageandmark_all_readrecord only the calling agent's state.get_message_delivery_receiptreports the delivery underproject_mailboxwith each visible agent's read/ack state. - It can be combined with named recipients in
to/cc, cannot bebcc, and can only name the sender's own project. Replies may address it too. - Retention treats a project message as settled only after every agent that can see it has read it (and acknowledged it when required). Archive reconstruction restores the delivery from the canonical message's
to/ccenvelope, and salvage restores the per-agent receipts. - Restart-safe monitors see it too: a project delivery appends one event to the durable
fetch_inbox_events/am inbox-eventscursor ledger, and each visible agent's page includes it ("kind": "project") under the same single cursor. am robot overviewandam robot overview --countsinclude each visible agent's unread, urgent-unread, and overdue acknowledgement obligations. One shared message can therefore count for several agents, while a named recipient is counted only once; generating the overview never creates receipts or marks mail read.
- The message is stored once, with one project delivery: no per-agent copies, rows, signals or archive inbox files. This is not
Across Different Repos
-
Option A (single project bus): Register both repos under the same
project_key. Keep reservation patterns specific (frontend/**vsbackend/**). -
Option B (separate projects): Each repo has its own
project_key. Link two agents with a contact handshake, then address the other agent with a project-qualified name:macro_contact_handshake(project_key="/abs/path/frontend", requester="BlueLake", target="GreenCastle", to_project="/abs/path/backend", auto_accept=true) send_message(project_key="/abs/path/frontend", sender_name="BlueLake", to=["GreenCastle@/abs/path/backend"], subject="API change", body_md="Please bump /v2.", thread_id="br-124", ack_required=true)Name@projecttakes the project's human key or its slug (as returned byensure_projectand listed byresource://projects), and so does the olderproject:<project>#Nameform. The qualified name is resolved in that project only and is never auto-registered.- The message is delivered only while an approved, unexpired contact link exists between the two agents (in either direction). Without one the send fails with
CONTACT_REQUIREDand a readymacro_contact_handshakecall; a blocked link or ablock_allrecipient fails withCONTACT_BLOCKED. This holds even when the recipient's own contact policy isopen. - The message is stored in the recipient's project, so the recipient reads it with its ordinary
fetch_inbox, acknowledges it withacknowledge_message, and answers withreply_messagein its own project. A default reply to a message from another project goes back to the original sender in that sender's project over the same link. - One message lives in one project: a send may address agents of one other project, or local agents, but not both (
INVALID_ARGUMENT; send twice instead). - A plain name still addresses only the sender's project. Naming a contact from another project, or an agent registered in another project of the same product, is refused with
CROSS_PROJECT_RECIPIENT(the error carries the qualified address to use); no same-name placeholder is created.
The product bus (
fetch_inbox_product,search_messages_product,summarize_thread_product) aggregates an agent's inboxes and threads across the linked projects.
External Git Coordination (opt-in)
mcp-agent-mail serializes its own in-process git shell-outs via
per-repo flock on <repo>/.git/am.git-serialize.lock. External tools
(wrapper scripts, editor integrations, CI) can honor the same sentinel
by invoking git through scripts/git-with-amlock.sh:
# one-time: put the wrapper on PATH
install -m755 scripts/git-with-amlock.sh ~/.local/bin/git-with-amlock
# per-shell: resolve an "am-aware" git for automation
export GIT_BIN="$(command -v git-with-amlock || command -v git)"
alias gitam='"$GIT_BIN"'
# your automation invokes gitam instead of git:
gitam status
gitam commit -m 'synced'
Scope of the mitigation:
- mcp-agent-mail's own in-process calls → always coordinated (automatic).
- External tools that opt into the wrapper → coordinated.
- Your IDE running
git commitdirectly, with no wrapper → still races. The only complete mitigation for that case is to setAM_GIT_BINARYto a safe git binary, or upgrade/downgrade the system git. See the Known-bad git versions section.
The wrapper honors AM_GIT_BINARY for binary selection and
AM_GIT_FLOCK_TIMEOUT_SECS (default 60s) for the bounded wait cap.
Exit code 75 (EX_TEMPFAIL) signals a flock timeout.
With Beads Task Tracking
Agent Mail pairs with Beads (br) for dependency-aware task selection:
- Pick ready work:
br ready --json(choose highest priority, no blockers) - Reserve edit surface:
file_reservation_paths(..., reason="br-123") - Announce start:
send_message(..., thread_id="br-123", subject="[br-123] Start: <title>", ack_required=true) - Work and update: Reply in-thread with progress
- Complete and release:
br close 123,release_file_reservations(...), final Mail reply
Use the Beads issue ID (br-123) as the Mail thread_id and prefix message subjects with [br-123] to keep everything linked.
Web UI
The server includes a lightweight, server-rendered web UI for humans at /mail/. Agents should continue using MCP tools and resources; the web UI is for human review and oversight. Use /mail/* for mailbox and task oversight. The authenticated live browser TUI mirror at /web-dashboard remains deferred and is not a supported surface.
The project website separately embeds the real production DashboardScreen through
crates/mcp-agent-mail-dashboard-wasm/ and FrankenTUI's browser renderer. That surface
is an interactive, read-only public replay: six SQLite aggregate counts are real, while
all names, paths, messages, and replay events are synthetic. It does not connect to a
visitor's mailbox or expose the deferred /mail/ws-input path. The deterministic opening
frame carries 192 synthetic history events, 500 agent rows, 41 project rows, and 200
contact rows so the production dashboard opens at the same busy visual density as a real
operator session without publishing mailbox content. See
docs/SPEC-browser-parity-contract-deferred.md
for the shipped replay boundary and the still-deferred live parity contract.