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: Mail MCP Server
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)
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.The
envbackend stores secrets in plaintext on disk. For production use, followdocs/SECURITY.mdand plan to move to a credential-store backend (M2 roadmap: Windows Credential Manager / KeePassXC / gpg-agent pinentry).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.
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 Chris",
"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"
},
"Chris": {
"added": "2026-09-13T00:00:00",
"given_name": "Chris",
"surname": "Example",
"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 -q127 tests cover: secret handling, 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 |
| — (required) | IMAP/SMTP app password |
| — (required) | Agent key fingerprint (40 hex) |
| — (required) | Agent key passphrase |
|
| Secret resolution backend (M2: more) |
|
| 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.
Email inboxes for AI agents: send, receive, reply, search, and manage threaded email over MCP.
Email inboxes and calendars for AI agents: send, receive, search, draft and schedule.
Email inboxes and calendars for AI agents: send, receive, search, draft and schedule.
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
- AlicenseBqualityDmaintenanceEnables AI assistants to send, read, and manage emails via SMTP and IMAP, with support for attachments, threads, and mailbox organization.1615 npm1MIT
- FlicenseBqualityDmaintenanceEnables AI models to send, receive, search, and manage emails via SMTP/IMAP, including support for attachments, contacts, and advanced search.18-
- AlicenseBqualityBmaintenanceEnables AI agents to securely interact with Gmail and QQ Mail, including IMAP search/read/organization, attachments, and preview-confirmed sending.412MIT