apple-mail-fast-mcp
🤖 MCP server for Apple Mail - Manage emails with AI using Claude Desktop. Search, send, organize mail with natural language.
How to read this: tool names here are observed from a live tools/list handshake. The Risk label is a heuristic inferred from the tool name (write/destructive verbs), not from executing the tool — a conservative guess, not a verified capability. We never escalate risk from a description. Found one that's wrong? Tell us — we fix on report.
| Tool | Risk | Side effects | Approval |
|---|---|---|---|
| list_accounts 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}, ...
]} | read | false | unknown |
| update_rule 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 when the
patch touches ``conditions`` or ``match_logic`` (which alter matching
scope), or replaces ``actions`` with a set that includes a dangerous
action (move / forward / delete / copy). An ``actions`` patch limited to
organizational flags (``mark_read`` / ``mark_flagged`` / ``flag_color``)
skips the prompt, as do patches limited to ``enabled`` and/or ``name``
(trivially reversible). 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. | write | true | unknown |
| list_rules 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} | read | false | unknown |
| list_mailboxes List all mailboxes for an account. | read | false | unknown |
| get_thread 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). | read | false | unknown |
| delete_rule 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). | destructive | true | true |
| search_messages 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. ``account`` is required.
- ``source=[id1, id2, ...]``: scope the search to the specific
messages identified by the given ids. ``account``/``mailbox`` are
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. | read | false | unknown |
| save_attachments Save attachments from a message to a directory. | unknown | unknown | unknown |
| create_rule Create a new Mail.app rule.
Rules with actions that can move, forward, or delete mail
(delete / forward_to / move_to / copy_to) require user confirmation —
a single create can install automation that auto-forwards or deletes
all future mail (#222). Organizational-only rules (mark_read,
mark_flagged, flag_color) are created without a prompt. Mail.app
appends new rules to the end of the rule list, so the returned
``rule_index`` equals the new total rule count. | write | true | unknown |
| get_messages 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. | read | false | unknown |
| get_attachment_content Read one attachment's content inline, without writing it to disk.
For "triage" workflows where you want to inspect an attachment (a text
file, JSON, a small PDF) before deciding what to do with it — instead of
``save_attachments`` → read the file → clean up. | read | false | unknown |
| update_message 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. | write | true | unknown |
| create_mailbox Create a new mailbox/folder. | write | true | unknown |
| update_mailbox Rename and/or re-parent (move) an existing mailbox.
Two delivery paths:
- **Rename only** (``new_name`` set, ``new_parent`` is ``None``):
AppleScript. Fast, no IMAP credentials needed.
- **Move** (``new_parent`` set; optionally combined with rename):
IMAP RENAME. Requires IMAP credentials in Keychain (#73 opt-in
flow) — returns ``error_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. | write | true | unknown |
| delete_mailbox 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. | destructive | true | true |
| save_template Create or overwrite a template. | unknown | unknown | unknown |
| delete_messages Delete messages (always moves to the account's Trash mailbox).
Destructive: gated behind user confirmation via MCP elicitation
(issue #239), matching delete_rule / delete_mailbox / delete_template. | destructive | true | true |
| delete_template Delete a template by name.
Destructive — requires user confirmation via MCP elicitation before
running. | destructive | true | true |
| list_templates List all stored email templates.
Templates live as files at ~/.apple_mail_mcp/templates/<name>.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). | read | false | unknown |
| get_template Read a single template by name. | read | false | unknown |
| create_draft Create a draft (fresh, reply, or forward). Optionally send immediately.
Mail.app's actual primitive is the draft — every outgoing message is
a draft until sent. This tool lets callers create one, optionally
seeded from an existing message (reply or forward), and either save
it for later or send it now. | write | true | unknown |
| render_template 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. | unknown | unknown | unknown |
| update_draft Update an existing draft. Implemented as delete-and-recreate.
**Returns a NEW draft_id** — Mail.app forbids mutating saved drafts,
so update is implemented by reading the draft's current state,
deleting it, and creating a new draft with the merged fields.
Threading headers (for reply seeds) and forward anchor are preserved
via persisted seed metadata.
Field merge semantics: any non-None argument overrides the existing
value. ``None`` keeps the existing value. ``attachment_paths=None``
PRESERVES existing attachments (extracted via Mail's ``save``
command); ``[]`` explicitly clears them; a list replaces.
For drafts created externally (not via ``create_draft``), seed
recovery falls back to scanning Mail.app for the In-Reply-To header
— this can be slow on large mailboxes (~30s+ per call). Forward
seeds without disk state are misclassified as fresh; pass an
explicit body if so. | write | true | unknown |
| delete_draft Delete (move to Trash) an existing draft.
Lifecycle endpoint for cancellation. Mail.app moves the message to
the Deleted Messages mailbox; recovery is technically possible but
Mail.app no longer treats trashed drafts as editable, so this is
effectively a one-way discard. No elicitation (recoverable from
Trash) and no rate limit (local operation). | destructive | true | true |
- repohttps://github.com/s-morgan-jeffries/apple-mail-fast-mcp
- homepagehttps://github.com/s-morgan-jeffries/apple-mail-mcp
- licenseMIT
- adoption98 stars · 32 forks
The access this server can exercise, inferred from its verified tools — not a declared OAuth scope.
Add the “as seen on MCPExplorer” badge to your README.
This is one server. A loadout combines the right servers, governance, and proven plays for a whole job — assembled deliberately, not tool-dumped.
Explore loadouts →