apple-mail-mcp
Programmatic Apple Mail access for AI assistants: read, search, organize, compose, and send email with safety and confirmation gates.
List accounts, mailboxes, rules, and stored templates.
Search messages by mailbox, date range, read/flagged state, sender, subject, body/text, attachment presence, or explicit message IDs (including current Mail UI selection).
Fetch full messages with bodies, attachment metadata, and thread members.
Bulk-update messages: mark read/unread, flag/color, move between mailboxes, or delete to Trash.
Save attachments from messages to a directory.
Create, rename/move, and delete mailboxes (move/delete use IMAP; delete requires confirmation and guards non-empty mailboxes).
List, create, update, and delete Mail.app rules, with confirmation for destructive or irrecoverable changes.
Create, read, overwrite, delete, and render email templates with variables and reply context.
Create, update, delete, and send drafts (fresh, reply, reply-all, forward), with outbound allowlist enforcement on send.
Send HTML email directly, also allowlist-gated.
Use optional IMAP for faster search with AppleScript fallback; operations are local-only with audit logging and confirmation for destructive actions.
Supports AOL Mail accounts through Apple Mail, with optional IMAP-backed search after an app-specific password is configured.
Provides programmatic access to Apple Mail on macOS, enabling tools to read, send, search, and manage emails, mailboxes, drafts, threads, attachments, rules, and templates.
Supports Gmail accounts through Apple Mail, with optional IMAP-backed search for faster querying when an app-specific password is configured.
Supports iCloud Mail accounts through Apple Mail, with optional IMAP-backed search for faster querying when an app-specific password is configured.
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., "@apple-mail-mcp Search my inbox for unread emails from my manager about the Q3 report."
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.
Apple Mail MCP Server
An MCP server that provides programmatic access to Apple Mail, enabling AI assistants like Claude to read, send, search, and manage emails on macOS.
⚠️ Pre-1.0 — expect breaking changes. The MCP tool surface (tool names, parameters, return shapes) is still evolving as the project matures. Pin to a specific version (for example,
apple-mail-mcp==0.8.1) and review the CHANGELOG before upgrading.
Tools (25)
Core: list_mailboxes, search_messages, get_messages, update_message Drafts lifecycle (v2 — verb-split): draft_create, draft_update, draft_delete, draft_send Sending: email_send_html Mailbox CRUD: create_mailbox, update_mailbox, delete_mailbox Attachments & Management: save_attachments, delete_messages Discovery & Rules: list_accounts, list_rules, get_thread, create_rule, update_rule, delete_rule Templates: list_templates, get_template, save_template, delete_template, render_template
See docs/reference/TOOLS.md for full parameter and return-shape documentation.
Related MCP server: Mac Local Mail MCP
Prerequisites
macOS 10.15 (Catalina) or later
Python 3.10 or later
Apple Mail configured with at least one account
uv (recommended) or pip
Installation
# From source (recommended for development)
git clone https://github.com/s-morgan-jeffries/apple-mail-mcp.git
cd apple-mail-mcp
uv sync --devConfiguration
Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"apple-mail": {
"command": "uv",
"args": ["--directory", "/path/to/apple-mail-mcp", "run", "python", "-m", "apple_mail_mcp.server"]
}
}
}Running as a fleet daemon
Where many agent sessions share one Mail, the server can instead run as one resident daemon, with a thin stdio proxy per session. The stdio entry above is unchanged, and a client that runs its own server needs neither.
mail-serveis the daemon. It serves the tools over streamable HTTP on127.0.0.1:41108, loopback only (--portto change it; 41108 is provisional). It runs under a process supervisor (pm2 in the fleet that runs it), launched from this repo asuv run --project <this repo> mail-serve.The daemon tends Mail's compose windows: at start, every 15 minutes, and soon after a composition fails and leaves its window open, it closes the windows this server opened and left (salvaged to Drafts, or discarded when empty), and only those; every other window is left and counted in the audit log.
--tend-interval SECONDSchanges the interval, and 0 turns it off (docs/research/compose-window-tending.md).The daemon quits and relaunches Mail on its own. Every 5 minutes it asks Mail one cheap question under the Mail lock; when Mail leaves two in a row unanswered for 20 seconds, it restarts Mail, and it also restarts Mail once a day at 04:00 local time (a day whose hour passed while the daemon was down is skipped, and the daily one waits while a send or draft is being composed). It also restarts Mail before it wedges, when it has kept most of a CPU core busy for 15 minutes or its memory footprint has stayed above 3 GiB on two checks; these too wait for a composition. A restart holds the Mail lock throughout, asks Mail to quit, then sends SIGTERM and SIGKILL if Mail's process has not exited, relaunches it in the background with
open -g, and waits for it to answer; once Mail answers, it runs a tending pass rather than waiting out tending's back-off. It never launches a Mail that was not running, never restarts Mail more than once an hour, and logs every restart to the audit log asrestart_mail.--mail-probe-interval SECONDSchanges the probe interval, and 0 turns probing and both kinds of restart off;--mail-restart-hour HOURmoves the daily one. The stdio server never restarts Mail.Restarting the daemon is a deploy. A session already connected through its proxy reaches the new daemon on its next call; changed instructions reach a session when it next starts.
mail-proxyis what a session launches:uv run --project <this repo> mail-proxy. It takes no arguments and declares nothing of its own. Every listing and call, confirmation prompts included, is forwarded to the daemon, and the daemon's instructions are fetched when the proxy starts.APPLE_MAIL_SERVER_URLpoints it at a daemon elsewhere.Registering
mail-proxyin a session's MCP configuration is the operator's edit.
Permissions
On first run, macOS will prompt for Automation access. Grant permission in: System Settings > Privacy & Security > Automation > Terminal (or your IDE)
Optional: faster search via IMAP
search_messages works out of the box via AppleScript. For large mailboxes (thousands of messages), AppleScript's whose clause can take 1–5 seconds per query. If you want faster server-side search, you can enable IMAP delegation per account by adding a Keychain entry.
How it works. If a Keychain entry exists for an account, the server uses IMAP (fast, server-side SEARCH). Otherwise — or on any IMAP failure (offline, wrong password, timeout) — it silently falls back to AppleScript. You never lose functionality; you only gain speed when IMAP is configured and reachable. No config flags, no environment variables; the Keychain entry's presence is the opt-in.
One-time setup per account.
Generate an app-specific password at your provider. The procedure varies:
iCloud: appleid.apple.com/account/manage → App-Specific Passwords. Requires 2FA on your Apple ID (default).
Gmail: myaccount.google.com/apppasswords. Requires 2-Step Verification on your Google account.
Yahoo / Fastmail / AOL: generate an app password in the provider's account-security settings.
Run the
setup-imapsubcommand. It prompts for the password (no echo), writes the Keychain entry, and verifies by connecting:apple-mail-mcp setup-imap --account iCloudSubstitute the Mail.app account name exactly — whatever it's labeled in Mail.app (e.g.
iCloud,Gmail,"Yahoo!"). The CLI:looks up the account's primary email from Mail.app (override with
--email),prompts via
getpassso the password never lands in shell history,writes to Keychain at
apple-mail-mcp.imap.<account>(idempotent — re-running with a new password updates the existing entry),opens an IMAP connection and runs a real LOGIN to confirm the password works. On rejection it rolls back the Keychain entry so you can retry without leaving a broken item behind.
If you see a one-time "security wants to use the 'login' keychain" prompt on the next IMAP-backed call, click Always Allow.
To remove the entry later: apple-mail-mcp setup-imap --account iCloud --uninstall.
Verifying the setup. The setup-imap command does this for you. If you want to spot-check post-hoc:
uv run python -c "from apple_mail_mcp.mail_connector import AppleMailConnector; \
print(AppleMailConnector().search_messages(account='<ACCOUNT_NAME>', limit=1))"If IMAP is working, the call returns in ~1 second. If it logs a WARNING about falling back (visible with --log-level=DEBUG), check that the account name matches Mail.app's account name exactly and that the email in your Keychain entry matches what email addresses of account returns.
Known provider quirks.
iCloud: the IMAP server accepts
@icloud.com/@me.comaliases as LOGIN username, not the Apple ID email. The server (andsetup-imap) readsemail addresses of accountfrom Mail.app for that reason.Yahoo: app passwords have been progressively deprecated; the option may not be available for all accounts. If Yahoo's account-security page doesn't show the option, IMAP setup isn't possible for that account and AppleScript is the only path.
Gmail: requires 2-Step Verification enabled. If your Google Workspace admin has disabled app passwords at the tenant level, IMAP setup isn't possible for that account.
Gmail thread retrieval — All Mail visibility tradeoff.
find_thread_members(used internally by thread-aware queries) is fastest when[Gmail]/All Mailis exposed over IMAP — that path is ~5 round-trips, mailbox-count-independent. Many users hide All Mail (Gmail Settings → Forwarding and POP/IMAP → Folder size limits → "Do not show in IMAP") because it duplicates every message. When hidden, the connector falls back to a per-mailbox X-GM-THRID iteration (still ~6× faster than the universal BFS, but proportional to your label count — ~25s on a 92-label account). Expose All Mail if you want the headline speed; keep it hidden if you prefer the cleaner IMAP folder list.
Write operations (draft_create, draft_update, draft_send) always use AppleScript regardless of IMAP configuration — these need Mail.app's compose UI.
Development
# Setup
uv sync --dev
# Common commands
make test # Run unit tests
make lint # Lint with ruff
make typecheck # Type check with mypy
make check-all # All checks (lint, typecheck, test, complexity, version-sync, parity)
make coverage # Coverage report
make test-integration # Integration tests (requires Mail.app)
# Validation scripts
./scripts/check_version_sync.sh # Version consistency
./scripts/check_client_server_parity.sh # Connector-server alignment
./scripts/check_complexity.sh # Cyclomatic complexity
./scripts/check_applescript_safety.sh # AppleScript safety auditBranch Convention
{type}/issue-{num}-{description} — e.g., feature/issue-42-thread-support
Architecture
server.py + tools/ (FastMCP tools — thin orchestration)
-> mail_connector.py (AppleScript bridge — domain logic)
-> subprocess.run(["osascript", ...])
-> Apple Mail.appserver.py — the FastMCP instance, the connector, confirmation and the error envelope
tools/ — the MCP tools, one module per domain: input validation, gates, response formatting
mail_connector.py and the modules beside it —
AppleMailConnector, which runs the AppleScript, and the modules that build it (script_blocks.py,search_script.py,compose_script.py,rule_script.py)security.py — Input sanitization, audit logging, confirmation flows
utils.py — Pure functions: escaping, parsing, validation
exceptions.py — Typed exception hierarchy
Security
Local execution only (no cloud processing)
Uses existing Mail.app authentication (no credential storage)
All inputs sanitized and AppleScript-escaped
Destructive operations require confirmation
Operation audit logging
See SECURITY.md for policy and docs/SECURITY.md for detailed analysis
Contributing
See CONTRIBUTING.md for development workflow, coding standards, and PR process.
License
Available Tools
25 toolscreate_mailboxC
Create a new mailbox/folder.
Inputs: name (required), account (required), parent_mailbox. Returns: object
create_ruleA
Create a new Mail.app rule.
Additive — no confirmation prompt. Mail.app appends new rules to the
end of the rule list, so the returned rule_index equals the new
total rule count.
Inputs: name (required), actions (required), conditions (required), enabled, +1 more. Returns: object
delete_mailboxA
Delete a mailbox via IMAP.
Mail.app's AppleScript dictionary doesn't expose a working delete
primitive for mailboxes, so this operation goes through IMAP. Requires
IMAP credentials in Keychain (#73 opt-in flow) — returns
error_type: "imap_required" when missing.
Always elicits user confirmation (destructive). By default refuses
non-empty mailboxes to prevent accidental data loss; pass
delete_messages=True to cascade.
Refused (#164): targeting the bare [Gmail] parent or any
[Gmail]/... child path returns error_type: "unsupported_gmail_system_label". Gmail's IMAP server doesn't
support DELETE for these paths.
Inputs: name (required), account (required), delete_messages. Returns: object
delete_messagesA
Delete messages (always moves to the account's Trash mailbox).
Inputs: message_ids (required), account, permanent, source_mailbox. Returns: object
delete_ruleA
Delete a Mail.app rule by 1-based positional index.
Destructive — requires user confirmation via MCP elicitation before running. Cannot be undone (Mail.app does not version rule history).
Inputs: rule_index (required). Returns: object
delete_templateA
Delete a template by name.
Destructive — requires user confirmation via MCP elicitation before running.
Inputs: name (required). Returns: object
draft_createA
Create a draft (fresh, reply, or forward). DOES NOT SEND.
To actually send, call draft_send(draft_id) as a separate step
after reviewing/editing the draft. The split is intentional —
sending requires its own explicit action and is the only path where
the outbound allowlist policy is enforced.
Modes (driven by reply_to / forward_of):
Neither: fresh draft (
subjectrequired).reply_to=<message_id>: reply draft. Mail.app auto-derives recipients and subject prefix unless overridden.forward_of=<message_id>: forward draft. Recipients default to empty (user must specify); subject prefixed withFwd:.
Inputs: cc, to, bcc, body, +8 more. Returns: object
draft_deleteA
Delete (move to Trash) an existing draft. No send, no recovery expected — Mail.app moves the draft to Deleted Messages.
Inputs: draft_id (required). Returns: object
draft_sendA
Send an existing draft. THIS IS THE ONLY SEND TOOL.
Hard policy gate: every recipient (to/cc/bcc) on the draft must
match the outbound allowlist (see outbound_allowlist.py). If any
recipient is off-list, the send is blocked and the draft is left
INTACT for human review — you can edit it via draft_update or
open Mail.app and send/discard manually.
Off-list recipients are detected BEFORE any destructive operation,
so a blocked draft_send is a pure no-op on Mail.app state.
Inputs: draft_id (required). Returns: object
draft_updateA
Update an existing draft. DOES NOT SEND.
Patch semantics: only fields you pass change. None means "keep
existing"; empty list/string means "clear". To actually send, call
draft_send(draft_id) afterwards.
IMPORTANT: Mail.app forbids mutating saved drafts, so this is
implemented as delete-and-recreate. The returned draft_id is a
NEW id — use it for any subsequent draft_update or draft_send
call. The id you passed in is stale after this call returns.
Inputs: draft_id (required), cc, to, bcc, +6 more. Returns: object
email_send_htmlA
Send an HTML email directly. Does not save a draft first.
Body must be an HTML string. The email is composed via clipboard injection into Mail.app's rich-text compose window and sent immediately.
Outbound allowlist policy applies — all recipients must be on the allowlist.
Inputs: to (required), body (required), subject (required), cc, +2 more. Returns: object
get_messagesA
Get full details of one or more messages, with bodies.
Returns a list of message dicts (possibly of length 0 or 1). Pair with
search_messages (metadata-only) and get_thread (thread member
ids) to fetch bodies for specific messages.
Inputs: message_ids (required), account, mailbox, headers_only, +2 more. Returns: object
get_templateA
Read a single template by name.
Inputs: name (required). Returns: object
get_threadA
Return all messages in the thread containing the given message.
Looks up the anchor message by its id, then reconstructs the
conversation via the connector's tiered IMAP threading dispatch
(Tier 1 X-GM-THRID for Gmail, Tier 3 header-search BFS fallback)
or the AppleScript path. Result rows are sorted by date_received
ascending.
The returned ids can be piped into search_messages(source=[ids])
for filtered metadata or get_messages([ids]) for full bodies.
Known limitation: thread members whose subject was rewritten mid-conversation are missed on the AppleScript fallback path (subject prefilter tradeoff).
Inputs: message_id (required). Returns: object
list_accountsA
List all configured email accounts in Apple Mail.
Returns each account's id (UUID), display name, email addresses, account type, and enabled state. Account ids are stable across name changes; prefer them over names for identifying accounts.
Returns: Dictionary containing the accounts list.
Example: >>> list_accounts() {"success": True, "accounts": [ {"id": "B21B254B-...", "name": "Gmail", "email_addresses": ["me@gmail.com"], "account_type": "imap", "enabled": True}, ... ]}
Inputs: no named parameters. Returns: object
list_mailboxesA
List all mailboxes for an account.
Inputs: account (required). Returns: object
list_rulesA
List all Mail.app rules (read-only).
Returns each rule's display name and enabled state. Rule names are NOT guaranteed unique — Mail allows duplicates — and rules have no stable id via AppleScript. This tool is read-only; mutation (enable/disable, create, delete) is tracked as a separate enhancement.
Returns: Dictionary containing the rules list.
Example: >>> list_rules() {"success": True, "rules": [ {"name": "Junk filter", "enabled": True}, {"name": "News From Apple", "enabled": False}, ... ], "count": 2}
Inputs: no named parameters. Returns: object
list_templatesA
List all stored email templates.
Templates live as files at ~/.apple_mail_mcp/templates/.md. Override the location with the APPLE_MAIL_MCP_HOME environment variable.
Returns: Dictionary with each template's name and subject (or null if no subject header is set).
Inputs: no named parameters. Returns: object
render_templateA
Render a template into ready-to-send subject and body text.
No side effects — caller is responsible for passing the rendered
text to create_draft or update_draft (with send_now=True
when ready to send).
With message_id, the original sender's display name and email,
the original subject, and today's date are auto-populated as
recipient_name, recipient_email, original_subject, and
today. Without message_id, only today is auto-filled.
User-supplied vars always override auto-fills on conflict.
Inputs: name (required), vars, message_id. Returns: object
save_attachmentsB
Save attachments from a message to a directory.
Inputs: message_id (required), save_directory (required), attachment_indices. Returns: object
save_templateC
Create or overwrite a template.
Inputs: body (required), name (required), subject. Returns: object
search_messagesA
Search for messages matching criteria. Returns metadata-only rows.
Two corpus modes:
source=None(default): search the given account/mailbox using the IMAP/AppleScript SEARCH path.accountis required.source=[id1, id2, ...]: scope the search to the specific messages identified by the given ids.account/mailboxare ignored; the connector resolves each id self-sufficiently. The resulting message dicts are post-filtered by the other criteria (sender_contains,read_status, etc.) — full filter composition. The literal token"SELECTED"may appear in the list and is server-resolved at call time to Mail.app's current UI selection (zero-or-more messages). Mixed lists like["SELECTED", "12345"]are valid. Missing ids drop out silently (partial-results).
For thread retrieval, call get_thread(message_id) to expand an
anchor into thread member ids, then optionally pipe those ids into
source=[ids] for filtered metadata browsing or into
get_messages([ids]) for full bodies.
Inputs: limit, source, account, date_to, +10 more. Returns: object
update_mailboxA
Rename and/or re-parent (move) an existing mailbox.
Two delivery paths:
Rename only (
new_nameset,new_parentisNone): AppleScript. Fast, no IMAP credentials needed.Move (
new_parentset; optionally combined with rename): IMAP RENAME. Requires IMAP credentials in Keychain (#73 opt-in flow) — returnserror_type: "imap_required"when missing.
At least one of new_name / new_parent must be provided.
Refused (#164): operations targeting the bare [Gmail] parent or
any [Gmail]/... child path return error_type: "unsupported_gmail_system_label". Applies to both the source
name and the resulting destination (new_parent join). Gmail's
IMAP server doesn't support normal RENAME semantics for these paths;
user-created Gmail labels (Newsletters, etc.) behave normally.
Inputs: name (required), account (required), new_name, new_parent. Returns: object
update_messageA
Update one or more messages: change read state, flag, and/or move, in one atomic call (#135).
Patch semantics — caller specifies only the fields to change. All
specified mutations apply in a single AppleScript pass via the
bulk-update helper. Replaces the previous mark_as_read,
move_messages, and flag_message tools.
Order of operations (matters for IMAP): read-state and flag changes apply first (in source mailbox), then the move. IMAP requires the message to exist in the source folder for STORE before MOVE.
Inputs: message_ids (required), account, flagged, flag_color, +4 more. Returns: object
update_ruleA
Update an existing Mail.app rule (patch semantics).
Patch semantics: only fields you provide are changed. conditions and
actions, when provided, REPLACE their respective structures wholesale
(not merged).
Conditional confirmation: prompts the user via MCP elicitation only when
the patch touches conditions, actions, or match_logic —
those replacements are irrecoverable. Patches limited to enabled
and/or name (trivially reversible) skip the prompt. The
enable/disable path replaces the removed set_rule_enabled tool: call
update_rule(rule_index, enabled=True|False).
Refuses to update any rule whose existing actions include something outside the supported schema (run-AppleScript, redirect, reply text, play sound, custom highlight color); raises MailUnsupportedRuleActionError. Edit such rules in Mail.app's UI.
Inputs: rule_index (required), name, actions, enabled, +2 more. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 25 tools
Each tool targets a distinct resource and action: templates, drafts, messages, mailboxes, rules, and accounts are clearly separated. The message-related tools (search, get, update, delete, thread, attachments) each have unique purposes, and the draft tools (create/update/delete/send) are unambiguous. No two tools appear to do the same thing.
Most tools follow a verb_noun pattern (list_templates, create_rule, get_messages, etc.), but the draft tools use a noun_verb pattern (draft_create, draft_send) and email_send_html also deviates. This inconsistency between subgroups makes the naming less predictable, though each subgroup is internally consistent.
25 tools is above the typical ideal range (3-15) but still appropriate for a comprehensive mail-client server covering messages, drafts, templates, rules, mailboxes, and accounts. Each tool serves a specific purpose, and no tools are redundant, though the large count requires careful organization.
The tool surface covers the full lifecycle for all major domains: messages (search, retrieve, update, delete, thread), drafts (create, update, delete, send), templates (list, get, save, delete, render), rules (list, create, update, delete), and mailboxes (list, create, update, delete). There are no obvious gaps that would hinder common workflows.
Maintenance
Related MCP Connectors
Manage Gmail end-to-end: search, read, send, draft, label, and organize threads. Automate workflow…
Bring Gmail, Outlook, iCloud Mail, Yahoo Mail, Fastmail and other IMAP mailboxes together. Search connected email, including semantic search; cached or indexed results may be incomplete or stale. Read messages and attachments, create drafts, send or reply when requested, and manage rules for new mail. Separate permissions control reading, attachments, drafts, sending and mailbox changes.
Hosted email MCP for your own Gmail, Outlook.com, Microsoft 365, iCloud or IMAP inbox: read, search, draft, reply in thread, forward and file mail. It moves or flags up to 500 messages in one call, and a send leaves exactly one copy in Sent. A calendar is a separate connection, and connecting one adds diary and scheduling tools.
Send and read iMessages from your own Mac and Apple ID — drafts, history and contacts.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables integration with Apple Mail on macOS using JavaScript for Automation (JXA). Supports reading, searching, sending, and managing emails across multiple accounts with full mailbox hierarchy support.5MIT
- AlicenseNot gradedqualityDmaintenanceEnables MCP clients to search, read, and inspect Apple Mail on macOS, including conversations and attachments. It can create new, reply, reply-all, or forward drafts, but cannot send or modify existing messages.MIT
- AlicenseAqualityBmaintenanceEnables AI assistants to read, search, send, and manage email on macOS through programmatic access to Apple Mail.29MIT
- FlicenseNot gradedqualityFmaintenanceEnables reading, searching, composing, replying, forwarding, sending, flagging, and moving macOS Apple Mail messages through native scripting, with policy-controlled outgoing drafts and attachments.-