Skip to main content
Glama
Apple Mail MCP

Apple Mail MCP

by cyberprive

apple-mail-mcp

A local MCP server that lets Claude, or any MCP client, read, search, draft and file email in Apple Mail on macOS.

It drives Mail through AppleScript (JXA), so it works with every account Mail already has (iCloud, Gmail, Exchange/Microsoft 365, plain IMAP) and never sees a password or token. It writes drafts but never sends: you review and press Send in Mail.

Requirements

  • macOS with Mail set up

  • Python 3.11+

  • An MCP client: Claude Code, Claude Desktop, or another

Related MCP server: macos-mail-mcp

Install

git clone https://github.com/cyberprive/apple-mail-mcp.git
cd apple-mail-mcp
python3 -m venv .venv
.venv/bin/pip install -e .

Connect it to your client

Claude Code, from the repo folder:

claude mcp add apple-mail -- "$PWD/.venv/bin/python" -m apple_mail_mcp.server

Claude Desktop: add this to ~/Library/Application Support/Claude/claude_desktop_config.json, using the absolute path to your clone, then restart Claude Desktop:

{
  "mcpServers": {
    "apple-mail": {
      "command": "/absolute/path/to/apple-mail-mcp/.venv/bin/python",
      "args": ["-m", "apple_mail_mcp.server"],
      "env": {}
    }
  }
}

Settings from Configuration go in env.

First run: allow automation

The first tool call that touches Mail makes macOS ask whether the app running the server (your terminal, or Claude) may control Mail. Click OK. Mail launches if it isn't running.

If you clicked Don't Allow, or calls fail with Not authorized to send Apple events to Mail (error -1743): open System Settings → Privacy & Security → Automation, find that app, and turn on Mail.

To check it works, ask your client to call ping, then list_accounts.

Safety

Email is untrusted input. search_messages and get_thread hand message text to the model, and anyone who can email you can write instructions into that text (prompt injection). The server is built to limit what such an instruction can do:

  • No sending. create_draft saves to Drafts. Nothing leaves your Mac until you press Send.

  • Attachments only land in one folder. save_attachment and save_all_attachments write only inside APPLE_MAIL_MCP_ATTACHMENTS_DIR (default ~/Downloads/apple-mail-mcp). Other paths are refused, including ones that try to leave the folder with .., ~ or a symlink. Filenames chosen by the sender are cleaned: no /, no leading dots or dashes, no control characters.

  • Moving a message or overwriting a file needs confirm=true. Without it, the tool does nothing and returns a CONFIRM_REQUIRED description of what it would do.

confirm=true is a checkpoint, not a lock: a model can pass it on its first call. The real gate is your client's approval prompt, so keep approval on for move_to_mailbox, save_attachment and save_all_attachments. Every tool carries MCP annotations (readOnlyHint, destructiveHint) that clients can use to decide what to ask about.

Everything stays local. The server talks to your client over stdio and opens no network port. It logs tool calls, account and mailbox names, message ids and errors to ~/Library/Logs/apple-mail-mcp.log. Message bodies are not logged; the one exception is a script that returns malformed output, whose first 200 characters are logged to help debug it.

Tools

Tool

Kind

Arguments

Returns

ping

read

—

"OK"

version

read

—

{version}

list_accounts

read

—

[{id, name, user_name, account_type, email_addresses, enabled}]

list_mailboxes

read

account, with_counts=false

[{name, unread_count?, total_count?}]

search_messages

read

account, mailbox?, since?, before?, limit=50, unread_only=false, query?, field="all"

[{id, message_id, account, mailbox, subject, sender, date_received, read, snippet}]

get_thread

read

account, mailbox, id

{messages: [{id, message_id, account, mailbox, subject, sender, to, cc, date_received, date_sent, read, body}]}

list_attachments

read

account, mailbox, id

[{index, name, size, mime_type, downloaded}]

create_draft

writes

account, body, to[]?, subject?, cc[]?, bcc[]?, reply_to?, reply_all=false, visible?

{created, mailbox, account, sender, to, cc, bcc, subject, visible, replied_to_id}

mark_read / mark_unread

writes

account, mailbox, id

{id, read}

flag_message

writes

account, mailbox, id, color (0–6, -1 clears)

{id, flag_index}

move_to_mailbox

destructive

account, mailbox, id, target_mailbox, confirm=false

{id, from_mailbox, to_mailbox}

save_attachment

destructive

account, mailbox, id, index, target_dir?, overwrite=false, confirm=false

{saved_path, name, original_name, size, downloaded}

save_all_attachments

destructive

account, mailbox, id, target_dir?, overwrite=false, confirm=false

{saved: [...], skipped: [...]}

Typical flow: list_accounts → list_mailboxes(account) → search_messages(account, since=...) → get_thread(account, mailbox, id) → create_draft(...) → mark_read or move_to_mailbox once handled.

Notes:

  • account is the name from list_accounts.

  • search_messages looks in the inbox when mailbox is omitted, whatever language Mail uses for it. since defaults to 7 days ago; pass an older date to look further back. field is one of subject, sender, content (slow: reads every body) or all (subject + sender).

  • Message ids are volatile. Mail can renumber messages after a move, delete or reindex. In multi-step flows, search again rather than reusing old ids.

  • target_dir on the save tools is optional. A relative path is a subfolder of the attachments folder; an absolute one must already be inside it.

Mailbox aliases

move_to_mailbox accepts aliases for standard folders, so callers don't need to know the account's language. search_messages accepts @inbox.

Alias

Matches (examples)

@inbox

INBOX, Inbox, Bandeja de entrada, Boîte de réception, Posteingang, …

@archive

Archive, Archivo, Archives, All Mail, Archiv, Archivio, Arquivo

@drafts

Drafts, Borradores, Brouillons, Entwürfe, Bozze, Rascunhos

@sent

Sent, Sent Messages, Enviados, Elementos enviados, Envoyés, Gesendet

@trash

Trash, Deleted Messages, Papelera, Elementos eliminados, Corbeille, …

@junk

Junk, Correo no deseado, Courrier indésirable, Werbung, Spam

The first name that exists in the account wins. The response reports the literal folder name it used.

Drafts and replies

  • New message: pass account, to, subject, body.

  • Reply: pass account, body and reply_to={"account", "mailbox", "id"} from search_messages. Mail sets the threading headers, the subject becomes Re: …, the recipient becomes the original sender, and the original is quoted below your text. cc/bcc are added on top. reply_all=true replies to everyone.

Responses and errors

Every tool returns the same envelope:

{"ok": true,  "data": "…"}
{"ok": false, "error": {"code": "…", "message": "…", "details": {}}}

Code

Meaning

INVALID_ARGUMENT

A required argument is missing or malformed, or a path is outside the attachments folder.

CONFIRM_REQUIRED

The action needs confirm=true. details says exactly what would happen.

APPLESCRIPT_ERROR

Mail refused or failed: account/mailbox/message not found, permission missing, etc.

TIMEOUT

Mail didn't answer in time. Narrow since, or raise the timeout.

INTERNAL_ERROR

A bug. Please open an issue with the log line.

Configuration

All optional. Set them in your client's env block for the server.

Variable

Default

What it does

APPLE_MAIL_MCP_ATTACHMENTS_DIR

~/Downloads/apple-mail-mcp

The only folder attachments may be saved into.

APPLE_MAIL_MCP_DRAFT_VISIBLE

false

true opens the compose window for each draft; false saves silently.

APPLE_MAIL_MCP_APPLESCRIPT_TIMEOUT_MS

45000

Per-call timeout for Mail.

APPLE_MAIL_MCP_LOG_PATH

~/Library/Logs/apple-mail-mcp.log

JSON log file (rotates at 2 MB).

APPLE_MAIL_MCP_LOG_LEVEL

INFO

DEBUG, INFO, WARNING or ERROR.

.env.example has the same list with notes.

Performance

Mail's scripting interface is slow on big mailboxes, so the server pushes date and unread filters into Mail's own index: query time follows the size of the result, not the mailbox. On a ~30k-message iCloud inbox, a 24-hour search takes about 3 s instead of 60+ s.

Always pass a since on large mailboxes (the default is 7 days). list_mailboxes(with_counts=true) and field="content" are the slow paths.

Limitations

  • Drafts are plain text. Mail's scripting interface doesn't reliably set HTML bodies.

  • create_draft doesn't return the draft's id; Mail saves drafts asynchronously.

  • get_thread returns the requested message only, not the whole conversation.

Development

.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest

The unit tests mock Mail, so they run anywhere; one filename test runs real JXA and is skipped off macOS. See CONTRIBUTING.md for the rules a change has to follow.

License

MIT

Available Tools

14 tools
create_draftA

Create a plain-text draft in Apple Mail's Drafts folder.

Two modes:

  1. New message — pass to and subject. body is the message text.

  2. Reply — pass reply_to={"account":..., "mailbox":..., "id":...} (use the values returned by search_messages). subject and to become optional and are auto-set from the original. body is your reply text; the original message is automatically quoted below it. Threading headers (In-Reply-To/References) are set so the recipient's mail client groups this with the original conversation. cc / bcc are additive.

Args: account: Sender account name (from list_accounts). body: Plain-text reply/message text. May be empty. to: Recipient addresses. Required for new messages, optional for replies. subject: Subject line. Required for new messages, optional for replies (defaults to "Re: "). Pass an explicit value to override. cc, bcc: Optional CC/BCC addresses. reply_to: If set, creates a reply. {account, mailbox, id} of the original message. account here is the account where the original was received, which may differ from the sending account. reply_all: If true and replying, replies to all recipients. Default false. visible: If true, Mail brings the compose window forward; if false the draft is saved silently. Defaults to APPLE_MAIL_MCP_DRAFT_VISIBLE.

Returns metadata about the created draft. Does NOT return a persisted Drafts-mailbox id — Mail commits drafts asynchronously. Drafts are reviewed in Mail.app.

HTML body rendering is v1.5.

Inputs: body (required), account (required), cc, to, +5 more.

flag_messageA
Idempotent

Set or clear the flag color on a message.

Args: account, mailbox, id: As returned by search_messages. color: 0..6 sets a color; -1 clears the flag. 0 red · 1 orange · 2 yellow · 3 green · 4 blue · 5 purple · 6 gray

Inputs: id (required), color (required), account (required), mailbox (required).

get_threadA
Read-only

Fetch the full content for a single message.

v1 returns one message (no thread reconstruction). For triage this is sufficient; full threading lands in v1.5.

Args: account: Account name. mailbox: Mailbox name (from search_messages output). id: Mail.app numeric message id (from search_messages output).

Returns {messages: [{id, message_id, account, mailbox, subject, sender, to, cc, date_received, date_sent, read, body}]}.

Inputs: id (required), account (required), mailbox (required).

list_accountsA
Read-only

List every account configured in Apple Mail.

Returns a list of {id, name, user_name, account_type, email_addresses, enabled}. The name field is the canonical handle to pass to other tools as account.

Inputs: no named parameters.

list_attachmentsA
Read-only

List attachments on a message.

Args: account, mailbox, id: As returned by search_messages.

Returns a list of {index, name, size, mime_type, downloaded}. Pass index to save_attachment. downloaded=false means the attachment body isn't on disk yet — open the message in Mail.app to fetch it before saving.

Inputs: id (required), account (required), mailbox (required).

list_mailboxesA
Read-only

List mailboxes (folders) inside an account.

Args: account: Account name as returned by list_accounts. with_counts: If true, include unread_count and total_count per mailbox. Slow on large accounts — Mail enumerates each folder to count messages. On a 30k-message iCloud account with 80+ folders this can take 30+ seconds. Default false.

Returns a list of {name, unread_count?, total_count?}.

Inputs: account (required), with_counts.

mark_readB
Idempotent

Mark a single message as read.

Args: account, mailbox, id: As returned by search_messages.

Inputs: id (required), account (required), mailbox (required).

mark_unreadB
Idempotent

Mark a single message as unread.

Args: account, mailbox, id: As returned by search_messages.

Inputs: id (required), account (required), mailbox (required).

move_to_mailboxA
Destructive

[DESTRUCTIVE] Move a message from one mailbox to another within the same account.

This tool refuses without confirm=true. Always surface the planned move to the user and get explicit approval before passing confirm=true — moving a message changes user-visible state (especially when the target is Trash, Junk, or Archive). Reversible by moving back, but disruptive.

Both mailbox and target_mailbox accept either an exact mailbox name OR a localization-aware alias starting with "@": @inbox, @archive, @drafts, @sent, @trash, @junk The MCP resolves the alias against the account's actual folder names (e.g. @archive → "Archivo" on a Spanish-localized M365 account, "Archive" on iCloud English). The returned to_mailbox field is the resolved literal name so callers can log accurately.

Args: account, mailbox, id: As returned by search_messages. target_mailbox: Destination — exact name or @alias. confirm: Must be true to actually perform the move. False (default) returns a CONFIRM_REQUIRED envelope describing the action.

Inputs: id (required), account (required), mailbox (required), target_mailbox (required), +1 more.

pingA
Read-only

Health check. Returns {ok: true, data: "OK"} if the MCP is reachable.

Inputs: no named parameters.

save_all_attachmentsA
Destructive

Save every attachment on a message, inside the attachments folder.

Same rules as save_attachment: writes only inside APPLE_MAIL_MCP_ATTACHMENTS_DIR, filenames cleaned, overwrite needs confirm.

Args: account, mailbox, id: As returned by search_messages. target_dir: Optional subfolder (relative, or absolute inside the folder). overwrite: If false (default), taken names get a " (2)", " (3)" suffix. True replaces existing files and requires confirm=true. confirm: Required with overwrite=true. Get the user's approval first.

Returns {saved: [{index, saved_path, name, original_name, size}], skipped: [{index, name, reason}]}. reason is typically "not_downloaded" — open the message in Mail.app to fetch it.

Inputs: id (required), account (required), mailbox (required), confirm, +2 more.

save_attachmentA
Destructive

Save one attachment to disk, inside the attachments folder.

Files can only be written inside the attachments folder (APPLE_MAIL_MCP_ATTACHMENTS_DIR, default ~/Downloads/apple-mail-mcp). Paths outside it are refused. The attachment's filename comes from the sender and is cleaned (no path separators, no leading dots); the result reports both name (as saved) and original_name.

Args: account, mailbox, id: As returned by search_messages. index: 1-based index from list_attachments. target_dir: Optional subfolder. Relative paths are taken relative to the attachments folder; absolute paths must be inside it. Default: the folder itself. Created if missing. overwrite: If false (default), append " (2)", " (3)" etc. when the name is taken. True replaces existing files and requires confirm=true. confirm: Required with overwrite=true. Get the user's approval first.

Returns {saved_path, name, original_name, size, downloaded}.

Inputs: id (required), index (required), account (required), mailbox (required), +3 more.

search_messagesA
Read-only

Search messages in a mailbox. Returns lightweight summaries.

Args: account: Account name (from list_accounts). mailbox: Mailbox name (from list_mailboxes). When omitted (or set to "@inbox"), the server auto-detects the account's inbox by trying the common localizations (INBOX, Bandeja de entrada, Posteingang, Boîte de réception, Caixa de entrada, etc.). since: ISO 8601 timestamp; only messages received at or after are returned. Defaults to 7 days ago if not provided — without a date floor, the script must materialize the entire mailbox, which is ruinous on large iCloud inboxes. Pass an explicit older date if you need to look further back. before: ISO 8601 timestamp; only messages received strictly before are returned. limit: Max results, capped at 500. Default 50. unread_only: If true, only return unread messages. query: Case-insensitive substring filter. Scope set by field. field: Where query is matched. One of: "subject" — subject line only "sender" — sender name + address "content" — full body text (slower; iterates message content) "all" — subject + sender (default; content excluded for perf)

Returns a list of {id, message_id, account, mailbox, subject, sender, date_received, read, snippet}. Use id with get_thread to fetch the full body.

Inputs: account (required), field, limit, query, +4 more.

versionA
Read-only

Return the MCP server version.

Inputs: no named parameters.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 14 tools

    Disambiguation5/5

    Each tool targets a distinct resource or action: search_messages returns summaries while get_thread fetches full bodies, mark_read/mark_unread are complementary opposites, and save_attachment vs save_all_attachments differ clearly in scope. The only mild overlap is version vs ping, but those are conventional and clearly differentiated (build info vs health probe).

    Naming Consistency4/5

    The set is almost entirely consistent snake_case verb_noun (list_accounts, search_messages, create_draft, move_to_mailbox, save_attachment). The outliers are 'version' and 'ping', which are noun/verb-less utility names, but these are conventional and readable, so the deviation is minor.

    Tool Count5/5

    Fourteen tools sit comfortably in the well-scoped 3-15 range, and each earns its place across account discovery, search, read, draft, move, flag, and attachment workflows. Nothing feels redundant or padded.

    Completeness4/5

    Core lifecycle is well covered: account/mailbox discovery, search, full-message fetch, drafting (including replies), moving (with @trash effectively enabling delete), flagging, and attachment save. Minor gaps remain—no explicit send or permanent-delete tool, no forward helper, and no batch mark-read—but these are arguably intentional safety choices and workable around.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      A
      maintenance
      This MCP server allows AI assistants to read, send, search, and manage emails in Apple Mail on macOS. It uses AppleScript to interact with the Mail app locally.
      52
      9,061 npm
      107
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      An MCP server for Apple Mail that enables Claude to read, search, manage, and compose emails via AppleScript.
      20
      284 npm
      1
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables Claude to control the macOS Mail app for reading, searching, drafting, sending, and managing emails directly from Claude Desktop.
      12
      MIT