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

ClassTools
Readmail_list, mail_search, mail_read, mail_read_thread, mail_list_attachments, labels_list, folders_list, account_whoami, mail_sync
Statemail_mark_read, mail_mark_unread, mail_move, mail_label, mail_trash
Labels & folderslabels_create, labels_update, labels_delete, folders_create, folders_update, folders_delete
Draftsmail_draft_create, mail_draft_update, mail_draft_delete, mail_draft_list
Sendmail_send, mail_reply, mail_reply_all, mail_forward, mail_send_draft
Reservedmail_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:

  1. 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.
  2. Touch ID at session-acquire time — the daemon prompts for biometric (or password fallback per Apple's .deviceOwnerAuthentication) on every startup AND every protonmcp unlock after 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).
  3. 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_send has 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 protonmcpd was 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.screenIsLocked distributed notification)
  • system sleep (NSWorkspaceWillSleepNotification)
  • idle timeout (idle_lock_minutes policy 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

PhaseScopeStatus
0–2Build, store, sanitize, syncMerged
3MCP server + 9 read toolsMerged
4Policy + audit + Touch ID + middlewareMerged
520 write tools + rate limit + allowed_recipientsMerged
5.5Security audit follow-up (21 findings closed)Merged
6Daemon + shim + launchd + lock/unlock + persistent rate-limitAll sub-PRs in flight
7Signing, notarization, Keychain ACL, Homebrew, AppVersion7/A + 7/B + 7/C merged; 7/D reverted (provisioning-profile gap); 7/E in progress
8SQLCipher / envelope encryption at restPlanning

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-bridge and go-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.html that 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.

関連サーバー