Apple Mail MCP
Provides tools for reading, searching, drafting, and filing email in Apple Mail on macOS, including listing accounts and mailboxes, searching messages, retrieving message details, creating drafts and replies, managing read/flag state, moving messages, and saving attachments.
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 the last week"
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
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.serverClaude 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_draftsaves to Drafts. Nothing leaves your Mac until you press Send.Attachments only land in one folder.
save_attachmentandsave_all_attachmentswrite only insideAPPLE_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 aCONFIRM_REQUIREDdescription 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 |
| read | — |
|
| read | — |
|
| read | — |
|
| read |
|
|
| read |
|
|
| read |
|
|
| read |
|
|
| writes |
|
|
| writes |
|
|
| writes |
|
|
| destructive |
|
|
| destructive |
|
|
| destructive |
|
|
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:
accountis thenamefromlist_accounts.search_messageslooks in the inbox whenmailboxis omitted, whatever language Mail uses for it.sincedefaults to 7 days ago; pass an older date to look further back.fieldis one ofsubject,sender,content(slow: reads every body) orall(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_diron 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, Bandeja de entrada, Boîte de réception, Posteingang, … |
| Archive, Archivo, Archives, All Mail, Archiv, Archivio, Arquivo |
| Drafts, Borradores, Brouillons, Entwürfe, Bozze, Rascunhos |
| Sent, Sent Messages, Enviados, Elementos enviados, Envoyés, Gesendet |
| Trash, Deleted Messages, Papelera, Elementos eliminados, Corbeille, … |
| 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,bodyandreply_to={"account", "mailbox", "id"}fromsearch_messages. Mail sets the threading headers, the subject becomesRe: …, the recipient becomes the original sender, and the original is quoted below your text.cc/bccare added on top.reply_all=truereplies to everyone.
Responses and errors
Every tool returns the same envelope:
{"ok": true, "data": "…"}
{"ok": false, "error": {"code": "…", "message": "…", "details": {}}}Code | Meaning |
| A required argument is missing or malformed, or a path is outside the attachments folder. |
| The action needs |
| Mail refused or failed: account/mailbox/message not found, permission missing, etc. |
| Mail didn't answer in time. Narrow |
| 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 |
|
| The only folder attachments may be saved into. |
|
|
|
|
| Per-call timeout for Mail. |
|
| JSON log file (rotates at 2 MB). |
|
|
|
.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_draftdoesn't return the draft's id; Mail saves drafts asynchronously.get_threadreturns the requested message only, not the whole conversation.
Development
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytestThe 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
Available Tools
14 toolscreate_draftA
Create a plain-text draft in Apple Mail's Drafts folder.
Two modes:
New message — pass
toandsubject.bodyis the message text.Reply — pass
reply_to={"account":..., "mailbox":..., "id":...}(use the values returned bysearch_messages).subjectandtobecome optional and are auto-set from the original.bodyis 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/bccare 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_messageAIdempotent
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_threadARead-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_accountsARead-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_attachmentsARead-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_mailboxesARead-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_readBIdempotent
Mark a single message as read.
Args: account, mailbox, id: As returned by search_messages.
Inputs: id (required), account (required), mailbox (required).
mark_unreadBIdempotent
Mark a single message as unread.
Args: account, mailbox, id: As returned by search_messages.
Inputs: id (required), account (required), mailbox (required).
move_to_mailboxADestructive
[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.
pingARead-only
Health check. Returns {ok: true, data: "OK"} if the MCP is reachable.
Inputs: no named parameters.
save_all_attachmentsADestructive
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_attachmentADestructive
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_messagesARead-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.
versionARead-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
Scored across 14 tools
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).
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.
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.
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
Related MCP Connectors
Connect any IMAP/SMTP mailbox to Claude & ChatGPT: search, read, sort, draft. Sends only with OK.
Email infrastructure for AI agents — send, receive, search, and reply to email over MCP.
Connect any mailbox to Claude, ChatGPT & AI: read, send, reply, schedule & search emails.
Search, read, and write your Apple Notes from ChatGPT/Claude via a local Mac agent + MCP relay.
Related MCP Servers
- AlicenseAqualityAmaintenanceThis 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.529,061 npm107MIT
- AlicenseAqualityBmaintenanceAn MCP server for Apple Mail that enables Claude to read, search, manage, and compose emails via AppleScript.20284 npm1MIT
- AlicenseAqualityCmaintenanceEnables Claude to control the macOS Mail app for reading, searching, drafting, sending, and managing emails directly from Claude Desktop.12MIT
- AlicenseNot gradedqualityBmaintenanceEnables using Apple Mail accounts to search, read, manage, draft, and send messages from Codex or Claude Code locally.MIT