mcp-agent-mail
Allows AI agents to read, send, and reply to GPG-encrypted email through a Gmail account via IMAP/SMTP, with PGP key management, contact book with key provenance, and email archiving/search.
Integrates with GnuPG for PGP operations: encrypting and verifying messages, listing keyring keys, exporting public keys, and managing agent key status, enabling end-to-end encrypted email workflows.
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., "@mcp-agent-mailcheck my inbox for new encrypted emails 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.
mcp-agent-mail
Encrypted email + PGP for AI agents, as a local MCP server. Read, send, and reply to GPG-encrypted mail; manage a contact book with key provenance; archive and search everything you read.
Built to be called by any MCP-aware agent harness (Claude, opencode, etc.) over stdio. Works with Gmail and any IMAP/SMTP provider.
Standalone rewrite. This project is a clean, hardened rebuild of the earlier prototype, built around one hard-won invariant: consequence-critical bytes must never enter the model's context window. The prototype earned that invariant the hard way — a low-parameter local model reading and repeating high-bit key material corrupted it. This rewrite carries the invariant over verbatim; several deliberately-added behavioral changes are flagged with ⚠ below.
Why this design
The model is the least reliable component in the pipeline. Small local models (the ones that run on consumer hardware) occasionally mangle byte-exact data — a quoting battle, a garbled regex, a corrupted key block. For ordinary text that is noise; for private keys, passphrases, and ciphertext it is unacceptable. So the design rule is:
Anything whose bytes must not change never enters the model's context.
That rule, not a threat model, is the primary reason for every invariant below. They are reliability controls: they keep high-consequence data on the side of the boundary the model cannot corrupt. The same controls also happen to harden the tool against an untrusted model and a partially-trusted host — useful, but derived. The primary enemy here is entropy, not malice.
There's a third, pragmatic face of the same rule: context is the scarcest resource on a local model, and high-entropy bytes are its worst possible consumer. A key block, an armored ciphertext, a base64 payload is nearly incompressible noise — it burns tokens at maximal density and returns zero usable signal. Keeping it out of the context window doesn't just protect its bytes; it wins back expensive context the model would otherwise waste repeating and re-mangling noise it was never going to use.
Inbound PGP key interception — public keys that arrive by email are imported and linked to the sender's contact before the body reaches the model. The block is replaced by a notice, never shown.
No silent downgrade (⚠) —
email_sendencrypts by default. If no key is on file for the recipient it refuses to send; the model must explicitly chooseencrypt=Falseto send in the clear. A model that guesses wrong silently is worse than one that stops and asks.Full fingerprints only — 16-char key IDs are rejected everywhere (Evil32 collision attack).
Secrets are opaque —
SecretStringwraps passphrase/password; reprs, logs, tracebacks show***. Private keys are never exported.Fail-fast config (⚠) — missing secrets abort startup with a list, never a warning and never a fallback default. A half-configured server is a guesser; fail-fast is fail-safe.
Immutable identity entries (⚠) — the agent (
EMAIL_ADDRESS) and the owner (OWNER_EMAIL) each have exactly one contact entry, fixed at setup. No tool can add, remove, re-key, or clear them, and the server refuses to start until both exist. Identity is a provisioning decision, not a model action.
Related MCP server: phison-mail-mcp
Tool surface
Domain | Tools |
| |
Contacts |
|
GPG |
|
Archive |
|
Utility |
|
Requirements
Python 3.13+
GnuPG — Gpg4win on Windows,
gnupg2on Linux/macOSAn email account with IMAP/SMTP app-password access (Gmail: enable 2FA, create an app password)
(optional) KeePassXC — for the
keepassxcsecret backend (production)
Install
git clone <repo-url> mcp_agent_mail
cd mcp_agent_mail
uv syncSetup
Generate a dedicated agent PGP key pair:
gpg --full-generate-keyCreate
.envfrom the template and fill it (see.env.examplefor passphrase quoting pitfalls):Copy-Item .env.example .envRequired:
EMAIL_ADDRESS,OWNER_EMAIL,EMAIL_PASSWORD(app password),GPG_KEY_ID(the agent key's full 40-char fingerprint),GPG_PASSPHRASE.Two backends are implemented. The default
envbackend stores secrets in plaintext on disk — fine for development on a machine whose disk you trust. For production, useSECRET_BACKEND=keepassxc(see Secret backends below), which keeps the secrets in a dedicated, key-file-only KeePassXC database and leaves.envholding only non-secret paths and identity values.Sanity check:
uv run mcp-agent-mail setup # deps + key presence + secret status uv run mcp-agent-mail doctor # offline config diagnostics uv run mcp-agent-mail doctor --live # opt-in IMAP + keyring checksProvision the identity entries (see below).
doctorshows a[!!]line and the server refuses to start until they exist.
Secret backends
The two secrets (EMAIL_PASSWORD, GPG_PASSPHRASE) are resolved through
the provider named by SECRET_BACKEND. GPG_KEY_ID is a public fingerprint
and always comes from .env, never from a secret store.
env(default) — readsEMAIL_PASSWORD/GPG_PASSPHRASEfrom the process environment /.env. Plaintext on disk; development only.keepassxc— reads them from a dedicated KeePassXC database viakeepassxc-cli, unlocked non-interactively by a key file (no master password is stored anywhere).
Setting up the keepassxc backend
In KeePassXC, create the database with no password and a key file: New Database → uncheck Password → check Key file → Create. Keep the
.kdbxand the.keyxoutside this repo (e.g. a<vault-dir>on any local drive).Add one entry per secret, with the secret in the entry's password field — e.g. entries
gpg-passphraseandmail-app-password.Confirm the CLI reads them with no prompt:
keepassxc-cli show -q -a password -k <vault-dir>/mail-agent.keyx ` --no-password <vault-dir>/mail-agent.kdbx gpg-passphraseIn
.env, set the backend and mappings, and blank outEMAIL_PASSWORD/GPG_PASSPHRASEso no plaintext copy lingers:SECRET_BACKEND=keepassxc KEEPASSXC_DB=<vault-dir>/mail-agent.kdbx KEEPASSXC_KEYFILE=<vault-dir>/mail-agent.keyx KEEPASSXC_ENTRY_GPG_PASSPHRASE=gpg-passphrase KEEPASSXC_ENTRY_EMAIL_PASSWORD=mail-app-password
mcp-agent-mail doctor reports the binary, database, and key-file state and
whether each secret resolves — booleans and paths only, never values.
Back up the key file. With a key-file-only database it is the single point of failure: lose it and neither you nor the agent can open that vault.
Identity & setup
Two contact entries pin the identity boundary of the whole system and are immutable from the tool surface. They are provisioned once, by hand (or by a future owner-facing CLI) — never created or edited by the tools:
Agent entry — the record whose
emailequalsEMAIL_ADDRESS. It is the agent's own identity inside the contact book and carries the agent key fingerprint (GPG_KEY_ID). Conventional surname:agent of <owner given name>.Owner entry — the record whose
emailequalsOWNER_EMAIL. That is the human running the server.
contact_add, contact_remove, contact_link_key,
contact_set_fingerprint and contact_clear_key refuse to touch either
entry (status protected). The server fails fast at startup if an identity
email is absent or held by more than one record.
Minimal data/contacts.json with both identities provisioned:
{
"Hermes": {
"added": "2026-09-13T00:00:00",
"given_name": "Hermes",
"surname": "agent of John",
"email": "agent@example.com",
"gpg_key_fingerprint": "AAAABBBBCCCCDDDDEEEEFFFF0000111122223333",
"key_source": "keyring_uid_match",
"key_linked_at": "2026-09-13T00:00:00",
"key_cleared_at": "",
"notes": "",
"updated": "2026-09-13T00:00:00"
},
"John Smith": {
"added": "2026-09-13T00:00:00",
"given_name": "John",
"surname": "Smith",
"email": "owner@example.com",
"gpg_key_fingerprint": "4444555566667777888899990000AAAABBBBCCCC",
"key_source": "keyring_uid_match",
"key_linked_at": "2026-09-13T00:00:00",
"key_cleared_at": "",
"notes": "",
"updated": "2026-09-13T00:00:00"
}
}The
added/updatedtimestamps are ISO 8601.key_sourcevalues:"keyring_uid_match","manual","cleared".
Usage
Run as an MCP server (stdio — what harnesses expect)
uv run mcp-agent-mailTo serve over HTTP instead:
uv run mcp-agent-mail serve --http --port 8000Register in an MCP client
Point the client at the project — the server reads .env directly and does
not need the environment pre-seeded:
{
"mcpServers": {
"mcp-agent-mail": {
"command": "uv",
"args": ["--project", "C:/path/to/mcp_agent_mail", "run", "mcp-agent-mail"]
}
}
}HTTP transport with curl (manual probing)
The HTTP mode speaks the standard MCP streamable-http transport. A raw
HTTP call is not a single request-response: MCP is session-based. You
initialize, then send JSON-RPC messages that carry an Mcp-Session-Id
header, and results come back as Server-Sent Events
(text/event-stream), not plain JSON.
Start the server:
uv run mcp-agent-mail serve --http --port 80001. Initialize the session — the mcp-session-id header in the
response is the session token for every following call:
curl -s -D - -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}'2. Signal the client is initialized (protocol requirement; produces no response body):
curl -s -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: <session-id>" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'3. Call a tool — list the three most recent inbox messages:
curl -s -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: <session-id>" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"email_check_inbox","arguments":{"limit":3}}}'The result arrives as an SSE frame; the useful payload is the "text"
field inside the JSON (example, sanitized):
event: message
data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"text":"{\n \"folder\": \"INBOX\",\n \"count\": 1,\n \"messages\": [\n {\n \"uid\": \"100\",\n \"folder\": \"INBOX\",\n \"from\": \"John Smith <john.smith@example.com>\",\n \"to\": \"agent@example.com\",\n \"subject\": \"Hello\",\n \"date\": \"Tue, 01 Jan 2026 10:00:00 +0100\"\n }\n ]\n}","type":"text"}],"isError":false}}Reading runs the same PGP pipeline as stdio: inbound ciphertext is decrypted, sender key blocks are intercepted and linked in the contact book, and key material never reaches you as raw bytes:
curl -s -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: <session-id>" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"email_read","arguments":{"uid":"100"}}}'stdio is what MCP harnesses expect and what the server runs by default. HTTP serves manual probing, remote access, and web-based clients — it changes the transport only, never which tools are exposed.
Tighten your harness's tool-use prompt
Small local models occasionally misread an otherwise-unambiguous tool contract
on first use — e.g. batching two local MCP calls into one invocation array and
getting the batch rejected. Don't fight this in the server; fix it in the
harness. Add a one-line tool-use rule to the harness's always-injected context
(SOUL.md, AGENTS.md, CLAUDE.md, .cursorrules — whichever the harness
loads from user-owned data, so updates don't clobber it):
One local tool invocation is exactly one entry per
tool_call; onlyconnectors__-type names may be batched together. Mixed or multi-local batches are rejected.
The rule costs tokens once per session prefix, not per call, and removes the whole class of first-tool-call failures.
CLI commands
Command | Purpose |
| Run the MCP server. |
| Check dependencies, key presence, secret status, identity setup |
| Offline diagnostics; |
| List keyring keys ( |
| Inspect the JSONL archive ( |
Development
uv run ruff check .
uv run pytest -q187 tests cover: secret handling (env + KeePassXC backends), fail-fast config,
identity immutability,
fingerprint/key-block validation, contact provenance, archive dedup/search,
the outbound encryption gate, and the full tool surface. No .env or real
account is needed — tests seed fake secrets and mock the transports.
Configuration reference
Var | Default | Purpose |
| — (required) | Agent account identity / IMAP+SMTP login |
| — (required) | Owner (human) identity entry holder |
| — (env backend) | IMAP/SMTP app password |
| — (required) | Agent key fingerprint (40 hex) |
| — (env backend) | Agent key passphrase |
|
| Secret backend: |
| — (keepassxc) | Absolute path to the |
| — (keepassxc) | Absolute path to the key file |
| — (keepassxc) | Vault entry for the GPG passphrase |
| — (keepassxc) | Vault entry for the app password |
|
| Binary override |
|
| Entry attribute to read |
|
| IMAP (SSL) |
|
| SMTP (STARTTLS) |
|
| Set |
|
| From display name |
|
| Relative to project root |
|
| JSONL mail archive |
|
| Rotating DEBUG logs (5×5 MB) |
|
| Where public keys are exported |
| (blank = system default) | Must be blank or absolute |
Documentation
docs/SECURITY.md— threat model, guarantees, trade-offsdocs/ARCHITECTURE.md— module map and data flow
License
MIT © 2026 Christof Milius
This server cannot be deployed
Maintenance
Related MCP Connectors
Email infrastructure for AI agents — send, receive, search, and reply to email over MCP.
- MailowsOAuthcom.mailows
Give AI agents their own email inboxes: list, read and send mail via scoped, revocable OAuth grants.
Email inboxes for AI agents: send, receive, reply, search, and manage threaded email over MCP.
Email for AI agents: read, search and send from your Gmail, Outlook, Yahoo, iCloud or IMAP inbox.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to interact with email accounts via IMAP and SMTP, supporting mailbox listing, email search, retrieval, sending, and management.MIT
- FlicenseBqualityCmaintenanceEnables AI models to send, receive, search, and manage emails via SMTP/IMAP, including support for attachments, contacts, and advanced search.18-
- AlicenseAqualityBmaintenanceEnables LLM agents to autonomously manage email via IMAP/SMTP, including reading, searching, drafting, sending, triaging, calendar event extraction, and secure attachment handling with zero-trust antivirus scanning.171MIT
- AlicenseAqualityBmaintenanceLets AI agents send email over SMTP and read, search, and manage email over IMAP or POP3 using app passwords or authorization codes, with attachment handling and support for common providers.717 npmMIT