stalwart-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@stalwart-mcpfind unread emails from this week and summarize them"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
stalwart-mcp
An MCP server for Stalwart mail servers, built on JMAP. It lets Claude (or any MCP client) search, read, write and organise mail, manage folders, Sieve filters and out-of-office replies — with safety rules for the parts where an AI assistant can do damage.
It runs in two ways:
As a sidecar behind an MCP hub (the T-0 MCP hub): the hub stores each person's Stalwart credential and sends it with every tool call. The hub manifests are generated into
service.yaml(mail) andmanifests/stalwart-admin/service.yaml(admin).Standalone next to your Stalwart server, added to Claude as a custom connector. (OAuth sign-in against Stalwart's own OAuth server is on the roadmap; see below.)
Tools
Tool | What it does | Kind |
| Accounts, sending identities, quota, vacation status, filters, server limits | read |
| Folder tree with roles and counts | read |
| Filters (text, from, to, subject, folder, dates, unread, flagged, attachments, size); | read |
| Up to 20 emails; body as text (HTML → Markdown, hidden elements removed), attachment list | read |
| A conversation in order, quoted history stripped | read |
| PDF/text → text, images → image, attached | read |
| Created/updated/deleted emails since a state token — for polling from n8n or agents | read |
| Sieve scripts with content | read |
| Draft only: new, reply, reply-all, forward (original attached); attachments from other mails or inline | write |
| Sends a draft; requires | sends mail |
| Move by folder path, role ( | write |
| Read/unread, flagged, answered | write |
| Junk / not junk, trains the spam filter | write |
| To Trash; permanent deletion only from Trash/Junk | destructive |
| Create, rename, move folders | write |
| Empty folders only; system folders protected | destructive |
| Validate and save a Sieve script, keep the previous version as | destructive |
| Remove a Sieve script | destructive |
| Out-of-office reply with period | write |
| List-Unsubscribe one-click (RFC 8058), mailto via draft, never opens web links | sends request |
Every tool carries MCP annotations (readOnlyHint, destructiveHint, openWorldHint), so clients like claude.ai
can ask for approval on writes.
Related MCP server: email-mcp
Admin tools (/admin/mcp)
A second MCP endpoint on the same server, for administrators, over Stalwart 0.16's JMAP management API
(urn:stalwart:jmap; reference with all quirks: docs/stalwart-management-api.md).
Credential: the administrator's own API key (Bearer API_…), ideally restricted to the server's IP.
Tool | What it does |
| Login, edition, management permissions of the credential |
| Accounts/groups with aliases, quota and usage, admin flag, disabled state, groups, credentials (never secrets) |
| Create (optional generated password, returned once), change aliases/quota/groups/admin/enabled, reset password; deletion needs |
| Domains, DKIM keys, the full DNS record set as BIND zone text; deletion needs |
| Distribution lists and their recipients |
| Outbound queue with per-recipient errors; retry now, reschedule, cancel, pause/resume |
| Blocked (incl. auto-bans) and allowed IPs; unblock/block/allow/unallow with the reload the server needs |
| DMARC aggregate / TLS-RPT reports summarised: volume, failing sources |
| Server log lines, newest first, anchor paging |
| Background tasks (DKIM, DNS, ACME, spam training, account maintenance), reloads and cache actions |
| DMARC evaluation and spam classification of a message |
| Generic read/write of any management object (expert tools) |
Safety model
Mail is the one data source where anyone on the internet can put text in front of the model. The server assumes that this text may contain instructions (prompt injection) and limits what such text can achieve:
Draft first.
write_emailnever sends.send_emailrefuses unlessconfirm_recipientslists exactly the draft's recipients, so the approval dialog shows who will receive the mail. Bcc is passed at send time and never written into the stored message.No silent forwarding.
save_filterrefuses Sieveredirect/notifytargets outside the account's own domains unlessallow_external_redirect=trueis passed explicitly.No outbound requests to internal networks. One-click unsubscribe only calls
httpsURLs whose host resolves to public addresses, does not follow redirects and ignores the response body.Hidden HTML is dropped before the model sees a body (
display:none, zero-size text, tracking pixels). This is damage control, not a boundary: every result with mail content is marked as untrusted data.Permanent deletion only for mails already in Trash or Junk; system folders cannot be renamed or deleted.
Operating against Stalwart
Rules learned against a production Stalwart (0.16), enforced in jmap.py:
Rejected credentials are never retried. Stalwart bans the source IP after failed logins — on some setups after a single one — for every account behind that IP. A rejected credential is quarantined for an hour; a new credential passes immediately.
One pacing budget per process (default 4 request starts/s, 4 concurrent). Stalwart counts per source IP, and behind a hub all users share one IP. Searches are batched into single JMAP requests with result references.
Put this server's IP on Stalwart's allowed list (Settings › Security › Allowed IPs) and then run the action Reload Settings — allowed-IP entries only take effect after a reload. Allowed IPs bypass rate limits and automatic bans, so one person's typo cannot lock everyone out; the quarantine above replaces Stalwart's brute-force protection for that IP. Unblocking a banned IP likewise needs Reload Blocked IPs (
manage_ipdoes both).Email/changesalways runs in its own HTTP request: a stale state token answers HTTP 400 for the whole request.Email/setis always written as a patch (mailboxIds/<id>,keywords/<kw>); keywords are lowercase.The
headercondition ofEmail/queryreturns no results on Stalwart instead of an error, so it is not offered.
Configuration
Variable | Default | |
| — | Base URL, e.g. |
|
| Request starts per second towards Stalwart, all users together |
|
| Parallel requests (Stalwart's |
| — | Extra domains Sieve filters may redirect to without confirmation |
|
| Largest attachment that is downloaded |
| — | Host header allow-list (DNS rebinding protection), e.g. |
|
|
Credentials arrive per request in the Authorization header: Bearer user@domain:app-password (what a hub sends;
forwarded as HTTP Basic), Bearer <token> (Stalwart API key or OAuth token) or Basic ….
Endpoints: POST /mcp (MCP, Streamable HTTP, stateless), GET /health, GET /auth-check (one session request
with the given credential — used by the hub's "test connection").
Running
docker run -p 8000:8000 -e STALWART_URL=https://mail.example.com ghcr.io/t-0-co/hub-service-stalwart:latestHub installation
The hub loads one service.yaml per repository, so there are two service repositories per hub:
Service | Manifest | Deploys | Users store |
|
| the sidecar container |
|
|
| nothing (uses the same sidecar) | their own admin API key |
Set STALWART_URL and STALWART_MCP_URL=http://stalwart-mcp:8000 for both. Give stalwart-admin only to the
admin group. Hub >= 2.28.0 is needed for mcpProxy.passthrough (images and error flags reach the client unchanged).
Development
uv sync
uv run pytest -q # unit tests + tests against an in-process fake Stalwart (mail + admin)
uv run pytest -m live tests/test_live.py # against a real throwaway mailbox, see the file header
uv run ruff check src scripts tests
uv run python scripts/gen_service_yaml.py # regenerate both manifests after tool changes (CI checks it)
STALWART_URL=https://mail.example.com uv run stalwart-mcpThe hub registers proxied tools from service.yaml, not from the sidecar, so the manifest is generated from the
server's own tool list and CI fails when they drift.
Roadmap
Standalone OAuth: the server as an OAuth resource server delegating sign-in to Stalwart's built-in OAuth (dynamic client registration + PKCE), so it can be added to Claude as a custom connector without app passwords.
This server cannot be deployed
Maintenance
Related MCP Connectors
- Lettio MCPOAutheu.lettio
Private, EU-hosted email for AI agents over JMAP: read, search, reply, organize, send.
Email infrastructure for AI agents — send, receive, search, and reply to email over MCP.
Programmable email inbox for AI agents — JMAP, PoW auth, stdio MCP server.
Connect any mailbox to Claude, ChatGPT & AI: read, send, reply, schedule & search emails.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceLocal IMAP/SMTP MCP server that lets Claude read, search, draft, send, flag, and move mail across multiple IMAP mailboxes. Credentials stay on your machine.-
- FlicenseNot gradedqualityBmaintenanceEnables Claude to read, search, draft, send, flag, and move email across multiple IMAP/SMTP mailboxes while keeping credentials local.-
- AlicenseAqualityCmaintenanceAn MCP server that lets Claude read, search, and send email over standard IMAP/SMTP.6MIT
- AlicenseNot gradedqualityCmaintenanceConnects AI agents to self-hosted Stalwart mail servers via a Cloudflare Worker and JMAP, enabling mailbox search, reading, listing, and two-step draft-and-send email operations through MCP.MIT