Proton Mail MCP
A local-only macOS Model Context Protocol server that exposes your Proton Mail account to Claude Desktop and Claude Code via Touch-ID-gated tool calls.
proto-mcp
A signed, notarized, Touch-ID-gated bridge between Proton Mail and Claude — running entirely on your Mac.
proto-mcp exposes 29 Model Context Protocol
tools that let Claude Desktop and Claude Code read, search, compose,
label, and send Proton Mail on your behalf. Every write is gated by
a per-tool YAML policy and a macOS Touch ID prompt that shows the
literal recipients and subject before the message goes out. Every
call writes a redacted row to a local audit log. Nothing leaves
your laptop except the mail itself.
Status:
v1.0.0-alpha. Phase 1–6 merged; Phase 7/A (UX), 7/B (log rotation + polish), 7/C (signing + notarization + binary integrity) all merged. 7/D (OS-level Keychain ACL) is blocked on a provisioning profile and deferred to 7/E (.app bundle). 7/E (Homebrew + AppVersion + release CI) in progress. Personal-use, technical-audience early access. Read the caveats below before installing.
What you get
| Class | Tools |
|---|---|
| Read | mail_list, mail_search, mail_read, mail_read_thread, mail_list_attachments, labels_list, folders_list, account_whoami, mail_sync |
| State | mail_mark_read, mail_mark_unread, mail_move, mail_label, mail_trash |
| Labels & folders | labels_create, labels_update, labels_delete, folders_create, folders_update, folders_delete |
| Drafts | mail_draft_create, mail_draft_update, mail_draft_delete, mail_draft_list |
| Send | mail_send, mail_reply, mail_reply_all, mail_forward, mail_send_draft |
| Reserved | mail_delete_permanent (denied by default; opt-in via policy) |
29 callable tools; one explicit deny-by-default.
Architecture (Phase 6 daemon model)
Claude Desktop Claude Code
│ │
(stdio, JSON-RPC over NDJSON per MCP spec)
│ │
▼ ▼
protonmcp-shim protonmcp-shim <- one per client
│ │ (tiny stdio↔socket forwarder)
└────── Unix socket ────┘
(0600, in ~/Library/Application Support/protonmcp/)
▼
protonmcpd <- one long-running daemon
│ (LaunchAgent, KeepAlive)
│
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ │ │ │ │
internal/proton internal/store internal/policy internal/ internal/audit
(go-proton-api (SQLite mirror, (default.yaml + (Swift Touch (SQLite + JSONL,
+ GPG) FTS5, body cache, user override) ID helper) rotated at 50MB)
SQLCipher TBD)
protonmcp serve-stdio is the old single-process mode — still
runnable for power users, but the default install registers the
shim with Claude clients so multiple clients share one daemon and
one Touch-ID-unlocked session.
Security model
The Keychain item that holds your Proton session is sealed behind three layers:
- macOS Keychain encryption — the standard at-rest protection for any keychain item. Anything below assumes the user is logged in and the keychain is unlocked.
- Touch ID at session-acquire time — the daemon prompts for
biometric (or password fallback per Apple's
.deviceOwnerAuthentication) on every startup AND everyprotonmcp unlockafter a manual or auto-lock. The prompt is application-issued via the Swift helper. An OS-level SecAccessControl on the keychain item was prototyped in 7/D but reverted (needs an Apple-provisioned profile / .app bundle — tracked as D37, deferred to 7/E). - Per-call approval — every
prompt-gated tool (everything that writes) fires a custom NSAlert + Touch ID prompt showing the literal recipients and subject. Cached approvals expire per policy TTL;mail_sendhas TTL 0, so every send re-prompts.
Plus:
- Hardened-runtime + Developer-ID-signed + Apple-notarized binaries. Gatekeeper accepts them without the "developer unknown" dialog.
- SHA-256 binary integrity check at daemon startup. If
protonmcpdwas replaced between install and launch, the daemon refuses to start. - SO_PEERCRED / LOCAL_PEERPID on every shim connection — the daemon records the real connecting client's PID + UID in audit rows.
- Default-deny policy for unknown tools. Adding a new tool without a policy stub fails registration; you can't accidentally ship an unguarded write.
- Auto-lock triggers: screen lock, sleep, and
idle_lock_minutes. Walking away from your laptop locks the daemon; unlocking requires Touch ID. - Redacted audit log. Passwords / tokens / cookies become
[REDACTED]. Bodies become{sha256, bytes}. Recipient addresses stay literal (so the prompt verification chain is honest).
SECURITY.md has the audit trail and per-defect
fix log. DEFECTS.html is the open issue list
(currently 5 open / 33 resolved; the open set is all medium / low).
Install
Two paths. Homebrew (signed + notarized binaries, recommended) once the first tagged release is up; build from source for contributors and pre-release testing.
Homebrew (Phase 7/E — pending first tagged release)
brew tap just-an-oldsalt/proto-mcp
brew install --cask proto-mcp
protonmcp login # interactive: SRP + TOTP + key unlock
protonmcp backfill # one-time: drains every message envelope
protonmcp daemon install # registers + starts the LaunchAgent
protonmcp install # registers shim with Claude Desktop + Claude Code
(The cask is proto-mcp with a hyphen; the binaries it installs
keep their existing names protonmcp, protonmcpd, etc.)
The cask installs all five binaries into the Homebrew prefix's
bin/ (signed + notarized; no Gatekeeper warning). brew uninstall --cask proto-mcp reverses everything; --zap also removes
~/Library/Application Support/protonmcp, ~/Library/Logs/protonmcp,
and the LaunchAgent plist.
Build from source
Requires macOS 13+, Go 1.26+, and Xcode
Command Line Tools (for swiftc).
git clone https://github.com/just-an-oldsalt/proto-mcp.git
cd proto-mcp
make all # builds bin/* + Swift helpers
./bin/protonmcp login # interactive: SRP + TOTP + key unlock
./bin/protonmcp backfill # one-time: drains every message envelope
./bin/protonmcp daemon install # registers + starts the LaunchAgent
./bin/protonmcp install # registers shim with Claude Desktop + Claude Code
Source builds are ad-hoc signed by default. For a signed-locally
build, see scripts/signing-setup.md.
Restart Claude Desktop / Claude Code after either install path.
The 29 tools show up under protonmcp in /mcp.
A Touch ID prompt looks like this
When Claude says "move 'Re: gear list' from inbox to archive," the NSAlert that fires says exactly that — not a redacted argument dump. Specifically:
┌──────────────────────────────────────────────┐
│ protonmcp-touchid is trying to │
│ move message 'Re: gear list' from inbox │
│ to Archive │
│ │
│ Touch ID or enter your password to allow. │
│ [ Cancel ] [ Touch ID ] │
└──────────────────────────────────────────────┘
The verb phrase comes from a per-tool PromptBody closure
(internal/mcptools/prompt_helpers.go) that looks up message_id → Subject and label_id → Name from the local SQLite mirror. You
read what you're approving.
For sends, the format is stricter:
┌──────────────────────────────────────────────┐
│ Send mail_send? │
│ │
│ To: [email protected] │
│ CC: [email protected] │
│ Subject: Re: gear list │
│ │
│ [ Cancel ] [ Send & Touch ID ] │
└──────────────────────────────────────────────┘
Body content is replaced with a SHA-256 reference in the audit log but the recipient list is always verbatim in the prompt — that's the verification surface you tap against.
Configuring policy
Defaults are in internal/policy/default.yaml
(embedded into the binary). Override per-tool by creating
~/Library/Application Support/protonmcp/policy.yaml:
tools:
mail_send:
decision: prompt
confirm: true
rate_limit: 5/hour # cap LLM-driven sends
allowed_recipients: ["@mydomain.com"] # restrict to one domain
mail_delete_permanent:
decision: deny # default; remove this to enable with prompt
# Phase 7/A — auto-lock idle timer
idle_lock_minutes: 30 # lock if no tool call for 30 minutes (0 = disabled)
Reload without restarting:
./bin/protonmcp policy reload # SIGHUP to every running daemon / serve-stdio
./bin/protonmcp policy show # print the merged effective policy
./bin/protonmcp policy validate ./my-policy.yaml
Rate-limit buckets persist to SQLite (Phase 6/E), so a daemon restart doesn't reset the per-hour cap.
Locking
./bin/protonmcp lock # SIGUSR1 — daemon zeros its in-memory session
./bin/protonmcp unlock # SIGUSR2 — Touch ID prompt re-acquires from Keychain
The daemon also auto-locks on:
- macOS screen lock (
com.apple.screenIsLockeddistributed notification) - system sleep (
NSWorkspaceWillSleepNotification) - idle timeout (
idle_lock_minutespolicy field; default 0 = disabled)
While locked, every tool call returns daemon is locked (<reason>); run \protonmcp unlock` to resume`. No audit row is written for the
attempt (logged at WARN instead).
Observability
Two log destinations, both auto-rotated at 50MB × 10 generations (Phase 7/B):
# Tail the audit log (one JSON object per completed tool call)
tail -f ~/Library/Application\ Support/protonmcp/audit.log
# Tail the daemon's slog output
tail -f ~/Library/Logs/protonmcp/daemon.log
Or query the SQLite source of truth for richer analytics:
sqlite3 ~/Library/Application\ Support/protonmcp/store.db \
'SELECT tool, outcome, policy_decision, duration_ms
FROM audit_log
ORDER BY id DESC LIMIT 20;'
Every audit row has: tool name, caller PID + UID + binary, policy decision, outcome (ok / denied / error), approval source (touchid / cached / policy), error message (if any), duration in ms, and redacted args.
Caveats — read before installing
Plaintext bodies on disk (until Phase 8)
When Claude reads a message via mail_read, the decrypted body
caches in SQLite for 30 days (protonmcp purge --older-than 7d to
shrink the window). On laptop theft + iCloud-restored disk imaging
that's recoverable cleartext. The secure_delete=on pragma zeros
deleted cells on the next page write; protonmcp purge --vacuum
forces it immediately. SQLCipher / envelope encryption is Phase 8.
Proton AppVersion (resolved when Phase 7/E lands)
Today, proto-mcp sends AppVersion: [email protected] — Proton
Bridge's identifier, not ours. Phase 7/E swaps in a legitimate
protonmcp@<version> once Proton grants it (request email is in
docs/proton-appversion-request.md).
Until then: don't rate-abuse, scrape, or run multi-account
automation through proto-mcp. Anything that violates Proton's
Terms is no less violating because
we're using Bridge's header.
macOS only
internal/keystore uses keybase/go-keychain + cgo against
Security.framework. (A SecAccessControl cgo wrapper sits
dormant in internal/keystore/access_control_darwin.{h,c,go},
ready to re-enable once Phase 7/E lands the .app bundle + a
provisioning profile that authorizes the required
keychain-access-groups entitlement.) The Swift helpers need
LAContext + AppKit + workspace notifications. Linux builds
compile (testing only) but the auth flow won't work.
License
GPLv3. We transitively depend on proton-bridge (also GPLv3) via
go-proton-api; that constrains us. See LICENSE.
Testing
For end-to-end validation see TESTING.md — a
sectioned playbook another agent (or you) can run to validate
build, signing, daemon lifecycle, Touch ID, lock/unlock, per-tool
correctness, audit log, and defect regressions. Reports go directly
into DEFECTS.html using the existing D-numbering.
For day-to-day development:
make test # go test ./...
make race # go test -race ./...
make verify-sign # codesign --verify each binary (after make sign)
Project status
| Phase | Scope | Status |
|---|---|---|
| 0–2 | Build, store, sanitize, sync | Merged |
| 3 | MCP server + 9 read tools | Merged |
| 4 | Policy + audit + Touch ID + middleware | Merged |
| 5 | 20 write tools + rate limit + allowed_recipients | Merged |
| 5.5 | Security audit follow-up (21 findings closed) | Merged |
| 6 | Daemon + shim + launchd + lock/unlock + persistent rate-limit | All sub-PRs in flight |
| 7 | Signing, notarization, Keychain ACL, Homebrew, AppVersion | 7/A + 7/B + 7/C merged; 7/D reverted (provisioning-profile gap); 7/E in progress |
| 8 | SQLCipher / envelope encryption at rest | Planning |
TODO.html has the full per-phase plan and the
backlog. DEFECTS.html is the truth about what's
broken.
Contributing
This is alpha software. PRs welcome but please open an issue
first — most architectural direction is locked by the design spec
in TODO.html and unsolicited big-scope PRs probably won't land.
.github/CODEOWNERS defines required reviewers for security-load-
bearing paths (internal/redact/, internal/keystore/,
internal/policy/, internal/approval/, helpers/touchid/,
helpers/lockwatch/).
Acknowledgements
- Proton AG for
proton-bridgeandgo-proton-api, on which the entire crypto + transport layer rests. Working on a legitimate AppVersion grant; appreciate the publish of a real Go client. - Anthropic for the MCP specification and the Claude clients this server targets.
- Every defect in
DEFECTS.htmlthat took the shape it did because someone —cmd-r,claude-review,claude-security-review, or a live testing session — looked at the same code more carefully than I would have on my own.
関連サーバー
LINE Bot MCP Server
Connects an AI agent to a LINE Official Account using the LINE Messaging API.
CData Google Contacts
A read-only MCP server for Google Contacts, powered by the CData JDBC Driver.
Hacker News
Fetch and interact with Hacker News content, including top stories, comments, and search functionality.
Twitter MCP
Interact with Twitter to post and search for tweets.
Postproxy
Publish to multiple social networks with only one MCP
Fastmail MCP Server
Integrate your Fastmail account with AI assistants using the Model Context Protocol.
net.4bots/4bots
A service for AI agents to add quality content to their daily briefing for their humans
AivisSpeech
A server for text-to-speech generation using the AivisSpeech engine.
Alice MCP Server
Secure audio transcription meets AI. Connect Alice recordings to Claude, ChatGPT, Gemini, and more.
Gmail MCP Server
An MCP server that integrates with the Gmail API for intelligent email management, including search, categorization, and archiving.