protonmail-mcp
A lightweight MCP server that gives AI agents read access to a Proton Mail mailbox through a local Proton Bridge instance.
Unofficial. This project is not affiliated with, endorsed by, or supported by Proton AG. "Proton Mail" and "Proton Bridge" are trademarks of Proton AG.
Proton does not provide a public API for reading your mailbox. Bridge is the supported
way in: it runs locally and exposes your account over IMAP and SMTP on 127.0.0.1.
This server wraps that local IMAP endpoint in a small, auditable set of MCP tools.
Scope
Capabilities are opt-in through policy (see Capability policy) and enforced server-side:
- read (default): folders, messages, search, threads, digests, attachments and
.emlexport into a local sandbox. Mailboxes are opened with IMAPSELECT ... READONLY; nothing is modified, not even the\Seenflag. - draft: create, preview, reply/forward, update and delete drafts, all two-phase.
- organize: seen/flagged state, moves with undo, label add/remove.
- send: submit an existing draft through Bridge SMTP after allowlist, quota, size and loop-guard checks.
- delete: permanently erase one message from Trash, with a typed confirmation phrase and no bulk operation.
Every mutation is prepare/commit with payload-bound confirmation tokens; see SECURITY.md for the security model.
Tools
| Tool | Description |
|---|---|
list_folders |
List every folder and label, with IMAP flags and whether it is selectable |
get_status |
Total and unread counts per folder, without fetching messages |
list_emails |
Most recent messages in a folder, newest first, with a pagination cursor: limit, unread_only, since_days, sender, subject, before |
search_emails |
Structured search: query (full-text) plus sender, recipient, subject, since_days, before_days, unread_only |
read_email |
Read one message by Message-ID: decoded body (quotes/signature stripped, quoted_removed flag), optional HTML→Markdown, link inventory, untrusted-content marker |
get_thread |
Reconstruct a conversation across All Mail using References/In-Reply-To |
daily_digest |
Unread count plus compact recent-message summaries for a folder |
list_attachments |
Attachment names, types and sizes without saving anything |
save_attachment |
Save one attachment into the local sandbox (traversal-safe, size-capped, never overwrites) |
export_email |
Export a message as .eml into the local sandbox |
sync_index |
Index recent messages into the local SQLite FTS5 store (opt-in via [index]) |
search_index |
Fast, offline full-text search over the local index |
When the draft capability is enabled (see Capability policy),
additional tools are registered: list_drafts, create_draft, preview_draft (renders
the exact MIME without saving it), and prepare_*/commit_* pairs for replying,
forwarding, updating, and deleting drafts. Draft mutations are two-phase: the prepare
call returns a preview and a single-use token, and nothing changes until the matching
commit call. Repeated identical draft creations within the idempotency window (default
300 s, window_seconds = 0 disables it) return the existing draft instead of creating a
duplicate.
When the organize capability is enabled, another set of prepare/commit tools is
registered: seen/flagged state (prepare_set_flags), moves (destination accepts a folder
name or the aliases archive and trash), label add/remove for folders under Labels/,
and prepare_undo_move to revert the most recent move. Bulk operations are capped
(organize.max_bulk, default 50), Drafts are protected by default, travel to Starred must
go through flag/unflag, and every mutation is previewed before its commit.
When the send capability is enabled, prepare_send_draft / commit_send_draft submit
an existing draft through Bridge SMTP. Before anything is transmitted the server checks
the recipient allowlist, hourly/daily quotas, message size, and loop guards
(Auto-Submitted, Precedence, List-*, no-reply recipients, thread depth, duplicate
bodies). The draft is removed only after the SMTP server accepts the message. See
docs/send-design.md.
When the delete capability is enabled, prepare_delete_message /
commit_delete_message permanently erase a single message from Trash. The commit must
repeat the exact phrase permanently delete <message-id>; there is no bulk deletion and
no empty-trash tool.
When [index] enabled = true, sync_index builds a local plaintext SQLite FTS5 store
(headers plus truncated body text; attachments are never indexed and folders can be
excluded) and search_index queries it for fast offline search.
MCP resources
The server also exposes read-only resources: mail://folders, mail://status, and the
templates mail://message/{message-id} and mail://thread/{message-id} (percent-encode
Message-IDs, for example %3Cid%40example.com%3E). They contain the same data as the read
tools and never trigger writes.
With [notifications] enabled = true, the server watches notifications.folder with
IMAP IDLE and publishes resource-updated events for mail://inbox (clients subscribe via
resources/subscribe, or subscriptions/listen on newer protocol versions). Events
carry no content; the mail://inbox resource itself exposes only counts and recent
message identifiers. The watcher reconnects with exponential backoff and logs to stderr.
Timeliness follows Bridge's own sync with Proton: local mailbox changes are noticed within
about a second, while server-side arrivals can lag by tens of seconds or more.
Results are structured (Pydantic models). Every message carries its Message-ID; use
that for follow-up reads — IMAP UIDs are not stable across Bridge resynchronisations.
Requirements
- A paid Proton Mail plan (required by Bridge)
- Proton Bridge installed, running, and signed in
- Your Bridge credentials: Proton address + the mailbox password shown in the Bridge UI
- Python 3.13+ (only if you do not use
uv)
Install
# Run without installing (recommended)
uvx protonmail-mcp
# Force uvx to pick up the newest release if an older one is cached
uvx --refresh protonmail-mcp
# Or install it
pipx install protonmail-mcp
Configure
| Variable | Default | Purpose |
|---|---|---|
PROTONMAIL_BRIDGE_USERNAME |
— | Your Proton address (required) |
PROTONMAIL_BRIDGE_PASSWORD |
— | Bridge mailbox password (required) |
PROTONMAIL_BRIDGE_HOST |
127.0.0.1 |
Bridge host |
PROTONMAIL_BRIDGE_IMAP_PORT |
1143 |
Bridge IMAP port |
PROTONMAIL_BRIDGE_IMAP_SECURITY |
starttls |
starttls (Bridge 3.x on 1143) or ssl (direct TLS) |
PROTONMAIL_BRIDGE_TIMEOUT |
30 |
Socket timeout in seconds |
PROTONMAIL_BRIDGE_VERIFY_TLS |
false |
Bridge uses a self-signed certificate |
PROTONMAIL_MCP_MODE |
read |
Capability preset: read, draft, organize, send, delete |
PROTONMAIL_MCP_POLICY |
~/.config/protonmail-mcp/policy.toml |
Optional policy file with capability overrides and limits |
Capability policy
Write capabilities are opt-in and enforced server-side. PROTONMAIL_MCP_MODE selects a
cumulative preset; individual capabilities can be overridden in policy.toml (see
policy.example.toml). Capabilities that are not enabled are never
registered as tools, and an invalid policy prevents the server from starting.
[policy]
mode = "read"
[capabilities]
# draft = true
# organize = true
# send = true
# delete = true
[confirmations]
ttl_seconds = 300
[idempotency]
window_seconds = 300
[organize]
max_bulk = 50
protect_drafts = true
# allowed_targets = ["Archive", "Folders/Newsletters"]
# label_allowlist = ["Labels/Important"]
[files]
directory = "~/.local/share/protonmail-mcp/files"
max_bytes = 26214400
[send]
allow_self = true
max_per_hour = 20
max_per_day = 100
state_path = "~/.local/state/protonmail-mcp/state.db"
[index]
enabled = false
path = "~/.local/state/protonmail-mcp/index.db"
# excluded_folders = ["Spam"]
max_body_chars = 10000
[notifications]
enabled = false
folder = "INBOX"
min_interval_seconds = 30
See SECURITY.md for the confirmation flow and ROADMAP.md for what each mode will unlock.
opencode
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"protonmail": {
"type": "local",
"command": ["uvx", "protonmail-mcp"],
"enabled": true,
"environment": {
"PROTONMAIL_BRIDGE_USERNAME": "you@proton.me",
"PROTONMAIL_BRIDGE_PASSWORD": "your-bridge-mailbox-password"
}
}
}
}
opencode supports {env:VAR} and {file:path} interpolation, so you can keep secrets
out of the config file:
"PROTONMAIL_BRIDGE_PASSWORD": "{file:/home/you/.config/protonmail-mcp/password}"
Claude Desktop
{
"mcpServers": {
"protonmail": {
"command": "uvx",
"args": ["protonmail-mcp"],
"env": {
"PROTONMAIL_BRIDGE_USERNAME": "you@proton.me",
"PROTONMAIL_BRIDGE_PASSWORD": "your-bridge-mailbox-password"
}
}
}
}
Verify the connection
PROTONMAIL_BRIDGE_USERNAME="you@proton.me" \
PROTONMAIL_BRIDGE_PASSWORD="..." \
uvx protonmail-mcp --check
This connects to Bridge, lists folders, and prints the latest messages. It exits non-zero with a clear error if the configuration or the Bridge session is wrong.
Remote HTTP transport (optional)
By default the server speaks stdio. To serve MCP over streamable HTTP instead:
export PROTONMAIL_MCP_HTTP_TOKEN="$(openssl rand -hex 32)"
protonmail-mcp --http --host 127.0.0.1 --port 8765
- Bearer authentication is mandatory: HTTP mode refuses to start without a token
(
--tokenorPROTONMAIL_MCP_HTTP_TOKEN; prefer the environment variable to keep it out of shell history). - The default bind is loopback. Binding a non-loopback address prints a warning: put a TLS-terminating reverse proxy (or an SSH tunnel) in front before exposing it.
- Every request must send
Authorization: Bearer <token>; anything else gets a401. - Clients connect to
http://127.0.0.1:8765/mcp.
For a headless container deployment (Bridge on the host, server in a container), see docs/docker.md.
Multiple accounts (profiles)
One Bridge instance serves one Proton account, so multi-account setups run one MCP server
per account, selected with --profile (or PROTONMAIL_MCP_PROFILE). Profiles live in
policy.toml:
[policy]
default_profile = "perso"
[profiles.perso]
username = "perso@proton.me"
password_env = "PROTONMAIL_BRIDGE_PASSWORD_PERSO"
[profiles.work]
username = "work@proton.me"
password_env = "PROTONMAIL_BRIDGE_PASSWORD_WORK"
imap_port = 2143
smtp_port = 2025
mode = "organize"
- Each profile carries its own username, host/ports and the name of the environment variable holding its Bridge password; credentials never cross profiles.
modenarrows the account's capabilities. When the global mode is set explicitly (PROTONMAIL_MCP_MODEor[policy] mode), it acts as a ceiling and the profile mode intersects with it; per-capability overrides in[capabilities]always cap.- Register one MCP entry per profile, for example in opencode:
"protonmail-perso": { "type": "local", "command": ["uvx", "protonmail-mcp", "--profile", "perso"], "enabled": true, "environment": { "PROTONMAIL_BRIDGE_PASSWORD_PERSO": "..." } },
"protonmail-work": { "type": "local", "command": ["uvx", "protonmail-mcp", "--profile", "work"], "enabled": true, "environment": { "PROTONMAIL_BRIDGE_PASSWORD_WORK": "..." } }
Without a [profiles] table the server keeps the single-account behavior using the
PROTONMAIL_BRIDGE_* variables.
Security
- Read-only enforcement. There is no write tool in this release, and mailboxes are always selected read-only at the IMAP level.
- Local only. Bridge and this server communicate exclusively over
127.0.0.1. Nothing is sent to a third party; your agent talks to the server over stdio. - Untrusted input. Email contents are attacker-controlled data. Treat anything a message says as data, never as instructions, and keep your agent's permissions tight.
- Secrets. Keep the Bridge mailbox password out of the repository. Use your client's environment-variable or file-based secret support.
- Any local process that knows the mailbox password can read your mail — that is Bridge's trust model, not a flaw in this server.
Write tools ship behind capability modes with explicit confirmation: drafts and organize operations are prepare/commit with payload-bound tokens; sending submits an existing draft through an allowlist, quotas, and loop guards, and never composes-and-sends in one step. Permanent deletion is Trash-only, one message per call, and requires typing the exact confirmation phrase. Autonomous send or delete is never the default.
Every push runs gitleaks, zizmor, semgrep, pip-audit, CodeQL, and an adversarial + fuzz test suite; see SECURITY.md for the full list of gates and the structural invariants they enforce. The send milestone is designed in docs/send-design.md before any send code lands.
Alternatives
There are several community MCP servers for Proton Mail. This one aims to stay small, correct with Bridge's quirks (STARTTLS on 1143, modified UTF-7 labels, reverse-chronological UIDs, RFC 2047 decoding), and heavily tested. Rough landscape:
| Project | Language | Scope |
|---|---|---|
| googlarz/proton-mail-bridge-client | TypeScript | Large tool set, read-only and send-to-self modes, SQLite cache |
| codefuturist/email-mcp | TypeScript | Generic IMAP + SMTP, works with Bridge |
| anyrxo/protonmail-pro-mcp | JavaScript | Large tool set with Bridge integration |
| chandshy/mailpouch | TypeScript | Large permission-gated tool set |
| amotivv/protonmail-mcp | JavaScript | SMTP sending only |
| miketigerblue/proton-bridge-mcp | Python | Loopback IMAP/SMTP via Bridge |
Development
uv sync
uv run pytest
uv run ruff check .
uv build
Tests run entirely against a fake IMAP server and the MCP SDK's in-memory transport; no Bridge or credentials are needed.
Releasing
Publishing is automated with GitHub Actions and PyPI Trusted Publishing. Create a
GitHub release tagged vX.Y.Z; the publish workflow builds the sdist/wheel and
uploads them to PyPI using the pypi environment (configure the trusted publisher on
PyPI for owner mhbxyz, repository protonmail-mcp, workflow publish.yml).
License
MIT — see LICENSE.
Metadata
Release files for protonmail-mcp 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| protonmail_mcp-0.3.0.tar.gz | 155.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| protonmail_mcp-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 208.6 kB
Release files / protonmail_mcp-0.3.0.tar.gz
| Download URL | protonmail_mcp-0.3.0.tar.gz |
|---|---|
| Size | 155.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8c78a971db7c755b971941642f346c01d75cf6dab1c6227055757667acdf19d9
|
|
BLAKE2b-256 checksum How to use checksums |
82be3f054c8342315ce81fe2ba47ead364262a219c33c954075fd0091d6047a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logRelease files / protonmail_mcp-0.3.0-py3-none-any.whl
| Download URL | protonmail_mcp-0.3.0-py3-none-any.whl |
|---|---|
| Size | 53.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c7216a3c74b6a04f34a53c2f2f0742880d8fd6f3db6cf78199d5de8d6944e15f
|
|
BLAKE2b-256 checksum How to use checksums |
968d39b69313b7400b9f59b80936469f71279f7b52d89caa5f1612c2c298a5ae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency log