For agents
mxr is built so an LLM agent can run it directly. The CLI emits structured JSON, the first-party MCP server exposes typed tools over stdio, every risky mutation has a dry-run or preview path, and the HTTP bridge exposes the same daemon for non-shell clients. There is no provider-specific SDK to wrap, no headless browser, no DOM scraping — the agent uses the local mxr daemon.
This page is the practical guide. For the comprehensive list of what’s safe to script, see the automation contract. For the field-level JSON shape, see JSON output schemas.
Safety primitives, all the time
Section titled “Safety primitives, all the time”- Read first.
mxr search,mxr cat,mxr stale,mxr sender,mxr summarizenever mutate. Use them to understand the situation before acting. - Dry-run everything.
--dry-runworks on every mutation; show the user the affected count before you run the real thing. --yesis opt-in. Without--yes, mutations prompt. When stdin isn’t a TTY (i.e. piped from an agent), pass--yesexplicitly so the user has a clear “I authorise this batch” moment in the loop.- Carry account scope through the whole loop. If the user says “work account” or “personal account”, resolve it with
mxr accounts, then use--account <selector>on CLI search, dry-run, mutation, and verification reads. MCP tools takeaccount_idwhere they can select an account; daemon profiles also enforceallowed_accountsforagentandmcpIPC origins. - Use
mxr history/mxr activity. Every mutation gets amutation_id. Capture it; offermxr undo <id>within ~60 seconds. Activity rows include the request origin (cli,agent,mcp, etc.) and stay local.
Worked example 1 — Newsletter prune
Section titled “Worked example 1 — Newsletter prune”Goal: unsubscribe from low-engagement subscriptions, archive the residue.
mxr subscriptions --rank --format json \ | jq '.[] | { sender_email, message_count, opened_count, replied_count, archived_unread_count, unsubscribe }'The agent gets:
[ { "sender_email": "newsletter@example.com", "message_count": 12, "opened_count": 0, "replied_count": 0, "archived_unread_count": 9, "unsubscribe": { "OneClick": { "url": "https://..." } } } /* ... */]The agent picks candidates with opened_count == 0 and message_count >= 4,
presents them to the user, then dry-runs. opened_count is the number of
messages from that sender with the local READ flag set, not a tracking-pixel
or distinct-open count; opened_count == message_count means every message in
that sender bucket is already read locally.
mxr unsubscribe newsletter@example.com --dry-runmxr archive --search 'from:newsletter@example.com' --dry-runUser confirms. Agent runs:
mxr unsubscribe newsletter@example.com --yesmxr archive --search 'from:newsletter@example.com' --yesAgent verifies and reports:
mxr history --category mutation --limit 3 --format jsonFor account-specific requests, keep the selector on every command in the sequence:
mxr subscriptions --account work --rank --format jsonmxr unsubscribe --account work newsletter@example.com --dry-runmxr unsubscribe --account work newsletter@example.com --yesWorked example 2 — Meeting prep
Section titled “Worked example 2 — Meeting prep”Goal: for tomorrow’s 1:1 with Sarah, gather the relevant threads from the last two weeks and draft an agenda.
mxr search 'from:sarah@example.com OR to:sarah@example.com after:2026-04-23' --account work --format jsonThe agent gets compact search rows with message_id, from, subject, date, read, starred, and score. When it needs thread context, it exports the matching search directly as markdown:
for tid in 01JFQ7K3M2X8N5R0VYZA9CTBPF 01JFQ8...; do mxr export "$tid" --format markdowndoneOr in one call with --search:
mxr export --account work --search 'from:sarah@example.com OR to:sarah@example.com after:2026-04-23' --format markdown > /tmp/sarah-context.mdAgent feeds the markdown into its summariser, then uses mxr draft-assist to generate a suggested reply body on stdout. Draft assist can use local relationship context when available and JSON output includes humanizer/voice-match metadata. The agent can show the body to the user or pass it into mxr compose --body-stdin / mxr reply --body-stdin after approval:
mxr draft-assist <thread_id> "Build a 1:1 agenda. Group by open question, decision needed, status update."The agent never sends from draft-assist. The user reviews the generated body, saves a draft, or sends only after explicit approval. For MCP, mxr_send_draft also requires confirm=true; the daemon can still block the send if the active mcp profile has allow_send = false.
Worked example 3 — CI failure cleanup
Section titled “Worked example 3 — CI failure cleanup”Goal: archive every CI failure email from last week whose underlying test has since been fixed.
mxr search 'from:noreply@github.com subject:"failed" after:2026-04-30' --format jsonFor each failure, the agent extracts the commit SHA and test name from the body (using mxr cat <id> --view reader). It cross-references against the local repo:
git log --since=1.week --pretty='%H %s' | grep -i 'fix.*test'It builds a list of message IDs to archive. Dry-run:
echo 01JFQ... 01JFQ... | xargs mxr archive --dry-runUser confirms. Apply:
echo 01JFQ... 01JFQ... | xargs mxr archive --yesCapture the mutation_id in the output. If the user notices an over-archive, the agent runs mxr undo <mutation_id> within 60 seconds.
What stays local, what doesn’t
Section titled “What stays local, what doesn’t”- Embeddings (semantic search) — local, with locally-stored model weights. Never sent off-device.
mxr summarizeandmxr draft-assist— call your configured[llm]endpoint. That can be a local server (Ollama, LM Studio) or a remote provider. Configure inconfig.toml. The thread content goes wherever the LLM is.- Provider mail content — passes through mxr to whatever provider the account is connected to (Gmail, IMAP). mxr never proxies through third parties.
If you want a strict local-only setup: set [llm].base_url = "http://localhost:11434/v1" for Ollama and [search.semantic].enabled = true. No third-party calls beyond your own provider.
Token-budget tips
Section titled “Token-budget tips”- Use
--limitaggressively.mxr search 'is:unread' --format json --limit 20is plenty for triage. - Use
--format idswhen you only need to drive a mutation. Saves tokens vs. full envelopes. - Use
mxr summarize <thread_id>for long threads instead of feedingmxr catinto the model. - Use
mxr export <thread_id> --format llmfor thread context formatted for an LLM (omits redundant headers, strips signatures).
MCP quick start
Section titled “MCP quick start”Run the server under an MCP client as a stdio command:
mxr mcp serveRequired daemon config is explicit. If source = "mcp" requests arrive without an [agents.profiles.mcp] profile, the daemon rejects them before handlers touch mail providers:
[agents.profiles.mcp]safety_policy = "draft-only" # read-only | restricted | draft-only | fullallowed_accounts = ["work"] # account key, email, or account idallow_send = falseallow_destructive = false# Optional: restrict to specific destructive actions even when# allow_destructive = true. Omit for all-or-nothing.# allowed_destructive_actions = ["archive", "unsubscribe"]Use safety_policy = "full", allow_send = true, and allow_destructive = true only for a client/session where the human approval loop is strong enough. To let an agent tidy the inbox but never trash or delete, keep allow_destructive = true and set allowed_destructive_actions = ["archive", "unsubscribe"] (see the config reference for the full action list). MCP mutation and send tools still require confirm=true.
IPC bucket model (skim)
Section titled “IPC bucket model (skim)”Behind the CLI and MCP server, every request lands in one of four IPC buckets: core-mail, mxr-platform, admin-maintenance, client-specific. The first three are stable; the fourth is per-client view-shape and not part of the daemon contract. If you’re scripting against the HTTP bridge or MCP, think in those buckets — they’re the contract surface.
Current limits (be honest)
Section titled “Current limits (be honest)”- MCP is stdio-only today; run
mxr mcp serveunder your client. There is no hosted MCP endpoint. - Agent/MCP profiles enforce daemon requests by IPC origin, account allowlist, safety policy, send gate, and destructive gate. They do not sandbox the rest of the OS; a coding agent can still run any shell command you allowed outside mxr.
- Account scope must still be carried in prompts and commands. The daemon blocks out-of-profile accounts, but the best UX is to include account selectors in every search/read/mutation step.
If you need stronger OS sandboxing, run the agent in a separate user/session and give it only the mxr config/profile you intend.
See also
Section titled “See also”- Automation contract — exhaustive table of
--format,--dry-run, stdin support - JSON output schemas — field names for
jq - Unsubscribe — header methods, body-link fallback, and safe cleanup flow
- Recipes — pipelines for common tasks
- Agent skill — install the mxr skill into Claude Code, Cursor, Continue, Aider
- MCP server — first-party stdio MCP tools and profile gates
- HTTP bridge — same surface over HTTP
- API explorer — interactive Scalar reference