MCP server pro
The same product as the REST API, wearing the protocol an agent already speaks. Identical tokens, identical scopes, the same domain layer underneath.
Connecting
Streamable HTTP (preferred)
POST https://mail.example.com/api/mcp
Authorization: Bearer mimux_pat_...
The transport is stateless: no session table, no sticky routing, every POST self-contained. That is why it sits behind exactly the same auth middleware as the JSON API with nothing extra — the bearer token on each request is the whole session.
There is no standalone GET event stream; a client that opens one gets a
405. Configure your MCP client for streamable HTTP without the
separate SSE channel.
The stdio bridge
For clients that only speak stdio, the pro binary bundles a bridge. It speaks
MCP on stdin/stdout and forwards every call to a running mimux's
/api/mcp. It never opens the database, so it is safe to run while
the server is up — which is the point.
{
"mcpServers": {
"mimux": {
"command": "mimux",
"args": ["mcp"]
}
}
}
No env block is needed once you have run
mimux mail login: the bridge reads the same stored credentials.
Where nothing was stored — CI, containers — set MIMUX_URL
(default http://localhost:8083) and MIMUX_TOKEN in
the env map instead. The bridge mirrors whatever the remote
session exposes — tools and resources, schemas passed through untouched — so
it never drifts from the HTTP endpoint. See Command
line.
Scopes are the tool list
Each request builds a server exposing only the tools the presented
token's scopes allow. So tools/list is the
permission model: a missing tool tells the model that this credential cannot
do that, rather than letting it try and get an error back.
| Scope | Tools it adds |
|---|---|
accounts:read | list_accounts |
mail:read | list_folders, search_mail, read_message, and the mimux://inbox/summary resource |
mail:modify | mark_read, star_message, move_message |
mail:send | draft_reply, send_draft |
Nine tools in total. Mint a token with the narrowest set that does the job:
an agent given mail:read mail:modify is not "asked" not to send —
it is never shown a tool that can.
The tools
Reading
| Tool | What it does |
|---|---|
list_accounts | The configured accounts with live sync state (ok/syncing/error), last sync time and message/unread counts. Use it to learn account names, and to check whether mail is syncing before trusting freshness. |
list_folders | The folder tree per account: id, name, special role (inbox/sent/archive/spam/trash), unread count. Folder ids are what move_message and search filters take. Omit account for every tree. |
search_mail | Structured search results: id, account, folder_id, from, subject, date, snippet, flags. |
read_message | One message by id: sender, recipients, subject, date, flags, labels, attachment list, and body. headers=raw|parsed|both adds its headers. |
get_raw_message | The exact RFC 822 source as base64, including its MIME attachments. |
draft_forward_as_eml | Creates a reviewable draft with the original attached. It never sends; use send_draft only after human approval. |
Long bodies page. read_message returns a capped
chunk; when truncated is true, call again with
offset set to the returned next_offset. A truncated
body is never silently cut — the flag is always there to be checked.
Deep search is a job. search_mail defaults to the
local index, which is fast and usually enough. deep=true asks the
IMAP servers directly; the tool waits about 15 seconds and then, if the fan-out
has not finished, returns status: "running" with a
job_id. Poll by calling the tool again with the same query — a
finished job answers instantly from cache.
The query language is mimux's own:
from:, to:, subject:, body:,
is:unread, is:starred, has:attachment,
before:/after: with YYYY-MM-DD, quoted
phrases, and - to negate. Bare words match anywhere. Everything
ANDs.
Triage
| Tool | What it does |
|---|---|
mark_read | Mark a message read (default) or unread with read=false. |
star_message | Star (default) or unstar with starred=false. Starring is the polite way to flag something for the human without moving it. |
move_message | Move to a folder_id from list_folders, or to a special target: archive, spam or trash. |
Trash needs confirmation. move_message with
target=trash additionally requires confirm=true,
and the tool description tells the model to ask the human first. Archive is
the safe default for cleaning up an inbox — the worst realistic outcome of
an over-eager agent is one drag-and-drop to undo.
Sending, in two steps
Outbound mail is deliberately split across two tools, and only the second one sends.
-
draft_replycreates a normal mimux draft — a reply whenin_reply_tois a message id (recipients, subject and threading are derived from the original;reply_all=trueincludes everyone), or a fresh mail when it is omitted (thentois required). Nothing is sent. It returns the draft id and a full preview of exactly what would go out, plus a note instructing the model to show that preview to the human and get approval. -
send_draftsends a previously created draft by id, exactly as previewed, and then deletes the draft. This is the only tool that sends mail.
The split means "review before send" is a property of the surface, not a
prompt instruction that a model may or may not follow. If you never grant
mail:send, neither tool exists. If you want an agent to
propose mail without any chance of it going out, the pattern used by
the bundled example is to strip send_draft from the tool list
before the model ever sees it, and review the drafts in the mimux UI yourself.
The inbox summary resource
mimux://inbox/summary
JSON: per-account sync state and unread counts, the total unread across
accounts, and the newest unread messages. A cheap first call for an agent that
wants to know whether there is anything to do at all, without burning a search.
Requires mail:read.
A worked example
The repository ships a single-file triage agent at
examples/triage.
It connects over the streamable-HTTP endpoint, reads unread mail, stars what
needs a human, archives newsletters and notifications, and drafts — never
sends — replies where one is obvious. Everything it does is visible live in
the mimux UI while it runs.
export MIMUX_URL=http://localhost:8083
export MIMUX_TOKEN=mimux_pat_...
uv run triage.py
It needs a token with mail:read, mail:send and
mail:modify, and an OpenAI-compatible model endpoint. Read the
script — it is short, and the safety model is the interesting part.
Errors
The endpoint is behind the licence gate and the same per-token rate limit as
the REST API: a paused licence answers 402, and an exhausted
bucket answers 429 with Retry-After. The licence
is checked before the token, so a 402 never spends a request from your budget.
See The automation layer.