Apple Mail MCP Server

A Model Context Protocol (MCP) server that enables AI assistants like Claude to read, send, search, and manage emails in Apple Mail on macOS.

npm version npm downloads node CI OpenSSF Scorecard platform: macOS License: MIT MCP

Note: This is the npm/Node.js package — install with npx or npm. There is an unrelated Python project of the same name on PyPI (imdinu/apple-mail-mcp) installed via pipx/uvx. If you're using uvx and seeing a cyclopts dependency error, you're looking for that project, not this one.

What is This?

This server acts as a bridge between AI assistants and Apple Mail. Once configured, you can ask Claude (or any MCP-compatible AI) to:

  • "Check my inbox for unread messages"
  • "Find emails from [email protected]"
  • "Send an email to the team about the meeting"
  • "Create a draft email for me to review"
  • "Reply to that message"
  • "Forward this to my colleague"
  • "Move old newsletters to the Archive folder"

The AI assistant communicates with this server, which then uses AppleScript to interact with the Mail app on your Mac. All data stays local on your machine.

Quick Start

Using Claude Code (Easiest)

If you're using Claude Code (in Terminal or VS Code), just ask Claude to install it:

Install the sweetrb/apple-mail-mcp MCP server so you can help me manage my Apple Mail

Claude will handle the installation and configuration automatically.

Or register it deterministically in one command:

claude mcp add apple-mail -s user -- npx -y apple-mail-mcp

Using the Plugin Marketplace

Install as a Claude Code plugin for automatic configuration and enhanced AI behavior:

/plugin marketplace add sweetrb/apple-mail-mcp
/plugin install apple-mail

This method also installs a skill that teaches Claude when and how to use Apple Mail effectively.

Configuring IMAP/SMTP for a plugin install: a plugin install has no editable env block, so supply settings via the config file at ~/Library/Application Support/apple-mail-mcp/config.json — Method B in the IMAP / SMTP Setup Guide. Passwords stay in the macOS Keychain; run the doctor tool to verify.

Using the Codex Marketplace

Install the same public marketplace in Codex:

codex plugin marketplace add sweetrb/apple-mail-mcp
codex plugin add apple-mail@apple-mail-mcp

The Codex package registers the same apple-mail MCP server through an exactly pinned runtime — npx -y apple-mail-mcp@<plugin version> — and includes the Apple Mail skill guidance. The pin in codex/.mcp.json is rewritten to match package.json by scripts/sync-plugin-version.mjs on every version bump, so the plugin manifest and the server it launches are always the same release; CI fails the PR if they drift.

Other Hosts (Hermes, Antigravity)

Two more hosts can run the same apple-mail MCP server (npx -y apple-mail-mcp):

  • Hermes Agent (NousResearch) — Hermes has no plugin/marketplace drop-in, so there is nothing in this repo to install from. Register the server with the CLI:

    hermes mcp add apple-mail --command npx --args -y apple-mail-mcp
    

    Or add it to ~/.hermes/config.yaml by hand:

    mcp_servers:
      apple-mail:
        command: npx
        args: ["-y", "apple-mail-mcp"]
    

    Restart your Hermes session afterward so the tools load.

  • Antigravity (Google) — add the server entry from .antigravity-plugin/mcp_config.json to ~/.gemini/config/mcp_config.json (or via Antigravity's MCP settings).

Manual Installation

1. Install the server:

npm install -g apple-mail-mcp

2. Add to Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "apple-mail": {
      "command": "npx",
      "args": ["apple-mail-mcp"]
    }
  }
}

3. Restart Claude Desktop and start using natural language:

"Show me my unread emails"

On first use, macOS will ask for permission to automate Mail.app. Click "OK" to allow.

Configuring email (IMAP & SMTP)

The server works out of the box over AppleScript with no configuration. Two opt-in power features take a one-time setup:

  • Fast IMAP reads — server-side search, counts, and large-mailbox handling that AppleScript is too slow for (it times out on big Gmail mailboxes).
  • Clean SMTP sendingsend-email submits clean MIME directly, avoiding the macOS 15+ Mail.app <blockquote> wrapping that otherwise makes sent mail look quoted/indented like a reply.

Both are driven by non-secret APPLE_MAIL_MCP_* settings — supplied via an env block or a config.json file (for hosts like Claude Desktop that strip env) — with passwords kept in the macOS Keychain, never in config.

👉 IMAP / SMTP Setup Guide — step-by-step: app passwords, Keychain, both config methods, multi-account, SMTP, verification with the doctor tool, and troubleshooting. Verify any time by running the doctor tool.

Requirements

  • macOS - Apple Mail and AppleScript are macOS-only
  • Node.js 20+ - Required for the MCP server
  • Node.js 22.5+ and Full Disk Access - Required by search-contacts only. It reads the Contacts database directly through Node's built-in node:sqlite, which does not exist before 22.5. On an older runtime, or without Full Disk Access for the Node binary, it logs one line to stderr and returns an empty list rather than an error — so "no contacts found" can mean "cannot read Contacts". Every other tool works on Node 20+. See Node runtime & TCC permissions.
  • Apple Mail - Must have at least one account configured (iCloud, Gmail, Exchange, etc.)

Features

Messages

Feature Description
List Messages List messages with pagination, sender filter, date display
Search Messages Search by sender, subject, content, date range, read/flagged status — across all accounts
Read Messages Get full email content (plain text or HTML)
Send Email Compose and send new emails (attach by file path or inline base64 content)
Send Serial Email Mail merge — send personalized emails to a list of recipients with {{placeholder}} support
Create Draft Save emails to Drafts folder (attach by file path or inline base64 content)
Reply Reply to messages (with reply-all support)
Forward Forward messages to new recipients
Get Thread Group a conversation by normalized subject (across AppleScript or IMAP)
Mark Read/Unread Change read status (single or batch)
Flag/Unflag Flag or unflag messages (single or batch)
Delete Messages Move messages to trash (single or batch)
Move Messages Organize into mailboxes (single or batch)
List Attachments View attachment metadata (name, type, size)
Save Attachment Save attachments to disk
Fetch Attachment Get an attachment's bytes as base64 (no disk write)

Read/list/get tools also return structured JSON (structuredContent) alongside the text, so agents can consume results without parsing prose.

Mailbox & Account Management

Feature Description
List Mailboxes Show all folders with message/unread counts
Create/Delete/Rename Mailbox Full mailbox lifecycle management
List Accounts Show configured accounts
Unread Count Get unread counts per mailbox

Rules, Contacts & Templates

Feature Description
List Rules View all mail rules and their enabled status
Enable/Disable Rules Toggle mail rules on or off
Create/Delete Rules Create rules with conditions + actions, or delete by name
Search Contacts Look up contacts from Contacts.app by name
Email Templates Save, list, use, and delete reusable email templates (persisted to disk across restarts)

Diagnostics

Feature Description
Health Check Verify Mail.app connectivity
Doctor Diagnose Mail permission, account state, and each IMAP/SMTP backend with actionable messages
Statistics Message and unread counts per account, recently received stats
Sync Status Check if Mail.app is actively syncing
Effect reconciliation Every delete/move reports what it actually did to the mailbox (countDelta), and warns when more messages left than were operated on — see Auditing destructive operations

MCP resources & prompts

Resources expose read-only context the client can attach without a tool call: mail://accounts, mail://templates, and mail://mailboxes/{account}. Prompts package common workflows: triage-inbox, compose-reply, weekly-summary.


Tool Reference

This section documents all available tools. AI agents should use these tool names and parameters exactly as specified.

Message Operations

search-messages

Search for messages matching criteria. Searches all accounts by default.

Parameter Type Required Description
query string No Text to search in subject/sender
from string No Filter by sender email address
subject string No Filter by subject line
mailbox string No Mailbox to search in (omit to search all mailboxes)
account string No Account to search in (omit to search all accounts)
isRead boolean No Filter by read status
isFlagged boolean No Filter by flagged status
dateFrom string No Start date filter (e.g., "January 1, 2026")
dateTo string No End date filter (e.g., "March 1, 2026")
limit number No Max results, 1–500 (default: 50)

Large mailboxes & partial results. Apple Mail's AppleScript bridge cannot search very large IMAP/Gmail mailboxes (tens of thousands of messages) before the Apple Event times out — empirically even reading the newest 20 messages of a 44k-message mailbox takes ~45s. To avoid burning minutes only to return a misleading empty result, an unscoped (all-mailboxes) search skips mailboxes whose message count exceeds a threshold (default 5000), enforces a per-account time budget, and reports anything it skipped or that timed out rather than silently returning nothing. When coverage is incomplete the result includes an explicit warning, e.g.:

⚠️  Partial results — this is NOT a confirmed "no such mail":
  - skipped mailbox(es) too large to search via AppleScript: Gmail / All Mail (44287) — scope the search with `mailbox` + a `dateFrom`/`dateTo` window to target them

To search inside a large mailbox, scope the call with mailbox (and ideally a dateFrom/dateTo window). Tune or disable the skip threshold with the APPLE_MAIL_MAX_SEARCH_MAILBOX environment variable (default 5000; set to 0 to disable the guard and attempt every mailbox regardless of size). (#24)


get-message

Get the full content of a message.

Parameter Type Required Description
id string Yes Message ID
preferHtml boolean No Return HTML source instead of plain text
mailbox string No Mailbox holding the message (e.g. "Sent Items"). With account, opens that mailbox directly instead of scanning every mailbox — this is the fix for timeouts on large folders
account string No Account holding the message. Pair with mailbox to skip the cross-mailbox scan

Returns: Subject line and message body (plain text by default, HTML if preferHtml is true and HTML content is available).

Large messages / attachments: reading a full message routes through osascript, whose captured output buffer defaults to 64 MB. Override it with the APPLE_MAIL_MCP_MAX_BUFFER environment variable (in bytes) if you work with messages whose raw MIME (e.g. a large embedded attachment) exceeds that — a value below the message size makes the read fail with a buffer-overflow error rather than truncating (#27).


list-messages

List messages in a mailbox.

Parameter Type Required Description
mailbox string No Mailbox name (omit to list from all mailboxes)
account string No Account name
limit number No Max messages, 1–500 (default: 50)
offset number No Number of messages to skip, ≥ 0 (for pagination)
from string No Filter by sender email address or name
unreadOnly boolean No Only show unread messages

Returns: List of messages with ID, date, subject, and sender.


send-email

Send a new email immediately.

⚠️ Safety: Sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling.

Parameter Type Required Description
to string[] Yes Recipient addresses
subject string Yes Email subject
body string Yes Email body (plain text)
cc string[] No CC recipients
bcc string[] No BCC recipients
account string No Mail.app account label, or an email-form SMTP From override. An SMTP override must match APPLE_MAIL_MCP_SMTP_USER, APPLE_MAIL_MCP_SMTP_FROM, or an address in APPLE_MAIL_MCP_SMTP_ALLOWED_FROM
attachments (string | {filename, contentBase64})[] No Up to 20 attachments: absolute file paths inside the configured read roots (e.g., "/Users/me/Documents/report.pdf") and/or inline {filename, contentBase64} objects up to 25 MiB decoded each
transport "applescript" | "smtp" No Send transport. If omitted, SMTP is used automatically when configured (otherwise AppleScript). Pass "smtp" to require clean MIME, or "applescript" to force the Mail.app path — see SMTP transport

Example:

{
  "to": ["[email protected]"],
  "subject": "Meeting Tomorrow",
  "body": "Hi, just confirming our meeting at 2pm tomorrow.",
  "account": "Work",
  "attachments": ["/Users/me/Documents/agenda.pdf"]
}
SMTP transport

On macOS 15+ (Sequoia/Tahoe), Mail.app wraps any AppleScript-injected body in <blockquote type="cite"> under the Apple-Mail-URLShareWrapperClass template, so emails sent through the default applescript transport render to recipients as if they were quoted/forwarded (Apple radar FB11734014, open since Ventura). The SMTP transport bypasses Mail.app entirely and submits clean MIME directly. Once SMTP is configured, send-email uses it automatically (no need to pass transport per call); pass transport: "applescript" to force the Mail.app path.

Two differences to know when SMTP is auto-preferred:

  • No Sent-folder copy. SMTP submission does not file the message in Mail.app's Sent mailbox (the server's own "save to Sent" may, depending on provider). Use transport: "applescript" if you need the local Sent copy.
  • account is a From override, not account selection. Over SMTP, account is used as the From address only when it is an email address; a Mail.app account label (e.g. "Work") can't select an account over SMTP, so a call that passes one is left on the AppleScript path automatically. To force account selection, pass transport: "applescript" explicitly. For sender safety, an email-form override must match the SMTP login user, the configured APPLE_MAIL_MCP_SMTP_FROM, or an address listed in the comma-separated APPLE_MAIL_MCP_SMTP_ALLOWED_FROM; any other From address is rejected before connecting.

Both plain-text and HTML bodies are supported — over SMTP an HTML body (CLI --html-body-file) is sent as multipart/alternative with the plain-text fallback.

Configure SMTP via environment variables on the MCP server. The password is read from the macOS Keychain by default, so no secret goes in config:

Non-implicit-TLS SMTP connections fail closed if STARTTLS is unavailable. APPLE_MAIL_MCP_SMTP_ALLOW_PLAINTEXT=1 is a deliberate escape hatch for a trusted isolated server or test fixture; it disables the upgrade requirement and can expose credentials and message content. The server emits a warning when it is used. Keep the default unset.

Variable Required Default Description
APPLE_MAIL_MCP_SMTP_HOST Yes SMTP server hostname (e.g. smtp.fastmail.com)
APPLE_MAIL_MCP_SMTP_USER Yes SMTP username
APPLE_MAIL_MCP_SMTP_PORT No 465 if secure, else 587 SMTP port
APPLE_MAIL_MCP_SMTP_SECURE No false true for implicit TLS (port 465); otherwise STARTTLS
APPLE_MAIL_MCP_SMTP_ALLOW_PLAINTEXT No 0 Set 1 only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required
APPLE_MAIL_MCP_SMTP_FROM No = user From address
APPLE_MAIL_MCP_SMTP_ALLOWED_FROM No Comma-separated sender aliases permitted as per-message From overrides
APPLE_MAIL_MCP_SMTP_PASSWORD No Password (if set, used instead of the Keychain)
APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE No = host Keychain item service/server name
APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT No = user Keychain item account

Store the password in the Keychain once (an app-specific password for Gmail/ iCloud). A generic-password item with an explicit service name keeps it from colliding with the system mail account password, and matches APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE:

# Fastmail (Keychain service defaults to the host)
security add-internet-password -s smtp.fastmail.com -a [email protected] -w

# Gmail / Google Workspace, using a dedicated Keychain service name:
#   APPLE_MAIL_MCP_SMTP_HOST=smtp.gmail.com
#   [email protected]
#   APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE=apple-mail-mcp-smtp
security add-generic-password -s apple-mail-mcp-smtp -a [email protected] -w

Once the env vars are set, a plain send-email (no transport) already goes out clean:

{
  "to": ["[email protected]"],
  "subject": "Standings",
  "body": "Plain body — no blockquote wrapping."
}
apple-mail-send CLI (no MCP server required)

The package also installs an apple-mail-send binary — a standalone CLI over the same SMTP path, for cron jobs, scheduled tasks, and scripts that can't run an MCP session. It reads the identical APPLE_MAIL_MCP_SMTP_* env + Keychain config:

apple-mail-send \
  --from [email protected] --to [email protected] \
  --subject "Standings" --body-file /tmp/body.txt \
  [--html-body-file /tmp/body.html] [--attach /tmp/report.pdf]

Repeatable --to/--cc/--bcc/--attach; an --html-body-file is sent as a multipart/alternative alongside the plain --body-file. Exit codes follow sysexits.h: 0 success, 64 usage error, 66 unreadable body file, 78 SMTP not configured.

IMAP backend — opt-in

📘 For step-by-step setup (app passwords, Keychain, config methods, multi-account, upgrading, troubleshooting), see the IMAP / SMTP Setup Guide. The summary below is the reference; the guide is the walkthrough.

AppleScript runs search/list predicates client-side over the Apple Event bridge, which is slow and can time out (false-empty) on large Gmail/IMAP mailboxes (see #24), and its delete/rename mailbox and draft handlers don't work on server-side accounts at all (#42). When an account is configured for IMAP, the MCP routes to a server-side IMAP backend (#43) that is fast and correct on exactly those mailboxes. This is opt-in and additive: any account without IMAP configured behaves exactly as before (AppleScript).

What routes to IMAP when an account is IMAP-configured:

  • Read: search-messages, list-messages (server-side SEARCH, typically sub-second), and get-message.
  • Folder ops: create-mailbox, rename-mailbox, delete-mailbox — IMAP's CREATE/RENAME/DELETE succeed on the iCloud/Gmail/Workspace/Exchange mailboxes Mail.app's AppleScript bridge can't touch (#42).
  • Message mutations: mark-as-read/unread, flag-message/unflag-message, move-message, delete-message.
  • Batch mutations (2.1): batch-mark-as-read/unread, batch-flag/unflag-messages, batch-move-messages, batch-delete-messagesimap: ids are grouped by mailbox and applied as a single UID STORE/UID MOVE; numeric ids in the same batch still use AppleScript.
  • Counts & stats (2.1): get-unread-count and list-mailboxes use STATUS; get-mail-stats uses STATUS + SEARCH SINCE — authoritative and fast even on huge mailboxes. As of v2.6.0 these prefer IMAP whenever it's configured (see Read routing below), merging across accounts when no account is given.
  • Attachments (2.1): list-attachments, save-attachment, fetch-attachment use BODYSTRUCTURE + FETCH BODY[part] for imap: ids — faster and able to see MIME-embedded attachments AppleScript misses.
  • Threading (2.1): get-thread links a conversation via References/Message-ID (HEADER SEARCH) for an imap: seed, falling back to subject grouping otherwise.

Message ids are backend-tagged. The IMAP read path emits self-describing ids of the form imap:<token> (the token encodes the account, mailbox path, and UID). Pass that id back to get-message, a message mutation, a batch op, or the attachment/thread tools and it routes to IMAP automatically; bare numeric ids continue to use AppleScript. So an agent never has to know which backend a message came from — the id carries it.

Read routing (v2.6.0): reads PREFER direct IMAP whenever IMAP is configured. The read tools — search-messages, get-thread, list-messages, list-mailboxes, get-unread-count, get-mail-stats — now go to IMAP whenever any APPLE_MAIL_MCP_IMAP_* account is configured, not just when an explicit matching account is passed. There are three cases:

  • Explicit IMAP account — single-account IMAP (fast server-side path).
  • Explicit non-IMAP account — AppleScript (that account isn't on IMAP).
  • No account givenmerge across all accounts: the query fans out over every configured IMAP account, and AppleScript runs only for the accounts no IMAP config covers (the account list is partitioned — accounts already served by IMAP are not re-scanned via AppleScript). If every Mail account is IMAP-configured, AppleScript is skipped entirely. The results are merged so no account is dropped. Message lists still de-duplicate as a safety net (preferring the IMAP copy, which carries the round-trippable imap: id) and sort newest-first; count tools (get-unread-count, get-mail-stats) count each account via exactly one backend so a coverage mismatch can never double- (or under-) count.
    • Default mailbox is resolved per account. When you don't pin a mailbox, a fan-out search scopes each account to its own default — Gmail/Workspace to [Gmail]/All Mail, every other IMAP host (iCloud, etc.) to INBOX (since [Gmail]/All Mail is Gmail-only and selecting it elsewhere would silently drop that account). Pin a mailbox to search a wider scope on non-Gmail accounts.

If IMAP is not configured at all, every read behaves exactly as before (pure AppleScript). The three mailbox-write ops (create-mailbox, delete-mailbox, rename-mailbox) remain conservative — they route to IMAP only for an explicitly-named IMAP account, never on an omitted account.

Variable Required Default Description
APPLE_MAIL_MCP_IMAP_USER Yes Login address; setting it enables IMAP
APPLE_MAIL_MCP_IMAP_ACCOUNT No = user Mail account name to match for routing
APPLE_MAIL_MCP_IMAP_HOST No imap.gmail.com IMAP server hostname
APPLE_MAIL_MCP_IMAP_PORT No 993 IMAP port (993 = implicit TLS)
APPLE_MAIL_MCP_IMAP_ALLOW_PLAINTEXT No 0 Set 1 only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required
APPLE_MAIL_MCP_IMAP_PASSWORD No Password (if set, used instead of the Keychain)
APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE No Keychain item service/server name
APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT No = user Keychain item account
APPLE_MAIL_MCP_IMAP_ACCOUNTS No JSON array of additional IMAP accounts for multi-account setups (see below)
APPLE_MAIL_MCP_IMAP_IDLE No 0 Set 1 to enable IMAP IDLE push notifications (new-mail alerts) for every configured account
APPLE_MAIL_MCP_IMAP_IDLE_MS No 30000 Idle timeout (ms) before a pooled IMAP connection is closed (0 = never close)
APPLE_MAIL_MCP_STATS_BUDGET_MS No 25000 Per-account wall-clock budget for get-mail-stats (minimum 1000). Raise it for very large accounts
APPLE_MAIL_MCP_STATS_DEADLINE_MS No 50000 Overall wall-clock deadline for one get-mail-stats call (minimum 2000), measured from when the request arrived and covering time queued behind other tool calls, account enumeration and every per-account read. Keep it below your client's request timeout

Multiple IMAP accounts (C2): set APPLE_MAIL_MCP_IMAP_ACCOUNTS to a JSON array, e.g. [{"account":"Work","user":"[email protected]","host":"imap.co.com","keychainService":"imap.co.com"}]. Each entry accepts account, user, host, port, password, keychainService, keychainAccount. Calls route to the account matching their account argument (or the decoded imap: id), and each account keeps its own pooled connection.

Non-implicit-TLS IMAP connections require STARTTLS and fail closed when the server does not offer a usable upgrade. APPLE_MAIL_MCP_IMAP_ALLOW_PLAINTEXT=1 is a deliberate escape hatch for a trusted isolated server or test fixture; it disables the upgrade requirement and can expose credentials and message content. Keep the default unset.

As with SMTP, the password is read from the macOS Keychain by default (use an app-specific password for Gmail/Workspace/iCloud), so no secret goes in config. Gmail label semantics: common names (All Mail, Sent, Trash, Spam, Important, …) map to their [Gmail]/… IMAP paths automatically.

Note: IMAP connections are pooled — one kept-alive connection per account is reused across calls (verified with a NOOP, closed after APPLE_MAIL_MCP_IMAP_IDLE_MS of inactivity), so there's no per-call connection overhead (#50).

iCloud: set APPLE_MAIL_MCP_IMAP_HOST=imap.mail.me.com, APPLE_MAIL_MCP_IMAP_USER to your iCloud address, APPLE_MAIL_MCP_IMAP_ACCOUNT to the Mail account name (e.g. iCloud), and use an app-specific password (from appleid.apple.com) stored in the Keychain.

Connection footprint (playing nice with Gmail)

IMAP connections are a shared, capped resource: Gmail allows at most 15 simultaneous IMAP connections per account, and Apple Mail itself needs some of those slots. This server keeps its footprint small:

  • One pooled connection per account, reused across calls and closed after ~30s idle (tune with APPLE_MAIL_MCP_IMAP_IDLE_MS; 0 = never close). So a server that isn't actively serving IMAP calls holds zero connections.
  • IMAP IDLE is opt-in (APPLE_MAIL_MCP_IMAP_IDLE=1). When on, it adds one persistent connection per account (a long-lived watcher), on top of the pooled request connection — leave it off if you don't need push notifications.
  • Connections are dropped on shutdown — SIGINT/SIGTERM and stdin-EOF (the MCP client/parent going away). As of v2.6.1 the server also self-exits if it becomes orphaned (parent force-quit/crashed → reparented to launchd), polling every 30s, so it can't linger holding sockets after its session is gone.

The catch is multiple concurrent instances. A host like the Claude desktop app spawns a separate set of MCP servers per open conversation (and respawns them after a crash), so the footprint is per instance × accounts. With IDLE off, an idle instance trends to 0 connections; with many active conversations or IDLE on, the per-account total climbs toward Gmail's 15-connection cap and can starve Apple Mail of slots (→ intermittent "cannot connect"). If you hit that, close idle Claude conversations, keep APPLE_MAIL_MCP_IMAP_IDLE off unless you need push, and/or lower APPLE_MAIL_MCP_IMAP_IDLE_MS.

Configuration file (when the host strips env)

Some host apps (e.g. Claude Desktop) launch the MCP server with a scrubbed environment and ignore the env block in their server config, so there's no way to pass APPLE_MAIL_MCP_* settings through it. In that case, put them in a JSON file the host doesn't manage — APPLE_MAIL_MCP_CONFIG_FILE, or by default ~/Library/Application Support/apple-mail-mcp/config.json:

{
  "APPLE_MAIL_MCP_IMAP_USER": "[email protected]",
  "APPLE_MAIL_MCP_IMAP_HOST": "imap.gmail.com",
  "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE": "imap.gmail.com",
  "APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT": "[email protected]",
  "APPLE_MAIL_MCP_IMAP_IDLE": "1"
}

The server reads it at startup and merges values into the environment without overriding anything already set there (so an explicit env still wins). Store only non-secret config here — passwords belong in the Keychain, never in this file.

Push notifications (IMAP IDLE) — opt-in

When APPLE_MAIL_MCP_IMAP_IDLE=1, the server opens a dedicated, long-lived connection to each configured IMAP account and watches its INBOX for new mail. On arrival it pushes two MCP notifications to the client (no polling by the client required):

  1. notifications/message (logging) — a human-readable line, e.g. New mail in "Work": 2 new message(s) (INBOX now 1843).
  2. notifications/resources/updated — for the affected account's resource mail://mailboxes/{account}, so a client subscribed to that resource knows to re-read it.

This requires an IMAP account to be configured (single-account env or APPLE_MAIL_MCP_IMAP_ACCOUNTS); accounts that only use AppleScript aren't watched. Detection is real-time via the IMAP IDLE EXISTS event where the server pushes it, with an automatic polling fallback for servers that don't. Dropped connections reconnect with backoff, and the watchers shut down cleanly on SIGINT/SIGTERM.

Enable it in your MCP client config alongside the IMAP settings:

{
  "mcpServers": {
    "apple-mail": {
      "command": "node",
      "args": ["/path/to/apple-mail-mcp/build/index.js"],
      "env": {
        "APPLE_MAIL_MCP_IMAP_USER": "[email protected]",
        "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE": "imap.gmail.com",
        "APPLE_MAIL_MCP_IMAP_IDLE": "1"
      }
    }
  }
}

Note: this is most useful with clients that surface MCP logging messages or subscribe to resource-update notifications. Clients that ignore notifications are unaffected — the feature is opt-in and adds no behavior unless enabled.


send-serial-email

Send individual personalized emails to a list of recipients (mail merge). Each recipient receives their own email — recipients don't see each other. Supports {{placeholder}} tokens in both subject and body.

Parameter Type Required Description
recipients object[] Yes List of recipients, max 100 (see below)
subject string Yes Email subject — use {{Key}} for placeholders
body string Yes Email body — use {{Key}} for placeholders
account string No Send from specific account
delayMs number No Delay between sends in ms (default: 500, max 10000)

Each recipient object:

Field Type Required Description
email string Yes Recipient email address
variables object Yes Key-value pairs for placeholder replacement

Example:

{
  "recipients": [
    { "email": "[email protected]", "variables": { "Name": "Alice", "Company": "Acme" } },
    { "email": "[email protected]", "variables": { "Name": "Bob", "Company": "Globex" } }
  ],
  "subject": "Hello {{Name}}!",
  "body": "Dear {{Name}},\n\nGreat to connect about {{Company}}.\n\nBest regards"
}

Returns: Per-recipient success/failure results with a summary count.

⚠️ Safety: Sends real mail immediately to every recipient and cannot be unsent. Confirm the recipient list, subject, and body with the user before calling.


create-draft

Save an email to Drafts without sending.

Parameter Type Required Description
to string[] Yes Recipient addresses
subject string Yes Email subject
body string Yes Email body (plain text)
cc string[] No CC recipients
bcc string[] No BCC recipients
account string No Account for draft
attachments (string | {filename, contentBase64})[] No Up to 20 attachments: absolute file paths inside the configured read roots and/or inline {filename, contentBase64} objects up to 25 MiB decoded each

Returns: Confirmation that draft was created.

get-thread

Group a conversation by normalized subject (across the AppleScript or IMAP backend).

Parameter Type Required Description
id string Yes A message ID in the conversation (numeric or imap:…)
account string No Account to search (omit to search all)
mailbox string No Mailbox to search (omit to search all)
limit number No Max messages in the thread (default 50)

Returns: The conversation's messages, oldest-first.

fetch-attachment

Return an attachment's bytes as base64 (the read counterpart to inline-base64 send).

Parameter Type Required Description
id string Yes Message ID (numeric or imap:…)
attachmentName string Yes Attachment filename (from list-attachments)

Returns: The attachment bytes, base64-encoded (also in structuredContent.contentBase64).


resolve-message-id

Map imap: message IDs to their numeric Mail.app IDs, via each message's RFC 5322 Message-ID (the join key both backends share). Needed only for the two tools that are numeric-ID-only — reply-to-message and forward-message. Numeric IDs pass through unchanged.

Parameter Type Required Description
ids string[] Yes 1–100 message IDs, each numeric or imap:…

Returns: For each input ID, its numericId (or null when it can't be resolved) and the messageId used, plus count and resolvedCount. The lookup scopes to the message's account and checks its INBOX first, to avoid scanning a large All Mail/Archive mailbox.

You do not need this for flag colors (v2.10.0+). Colors used to require the numeric-ID path, and older docs and tool descriptions said so. flag-message and batch-flag-messages now write the color over IMAP directly, as Mail.app's $MailFlagBit0/1/2 keywords, so a smart mailbox keyed on flag color matches an IMAP-flagged message. Resolving IDs just to apply a color reintroduces the AppleScript/TCC dependency 2.10.0 removed. Flag, move, mark, and delete all accept imap: IDs as-is.


reply-to-message

Reply to an existing message.

Parameter Type Required Description
id string Yes Message ID to reply to
body string Yes Reply body
replyAll boolean No Reply to all recipients (default: false)
send boolean No Send immediately (default: true, false = save as draft)

Example - Reply to sender only:

{
  "id": "12345",
  "body": "Thanks for the update!"
}

Example - Reply all, save as draft:

{
  "id": "12345",
  "body": "I'll review this and get back to everyone.",
  "replyAll": true,
  "send": false
}

Transport (v2.5.0): when SMTP is configured, reply-to-message sends via clean SMTP, threading the reply with proper RFC 5322 In-Reply-To/References headers (built from the original message) so it lands in the same conversation. When SMTP is not configured (or the original lacks the headers needed to thread), it falls back to Mail.app's AppleScript reply … without opening window — same reliable-from-background-process path as before. See SMTP transport.

⚠️ Safety: With the default send: true, sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling (or pass send: false to save a draft for review).


forward-message

Forward a message to new recipients.

Parameter Type Required Description
id string Yes Message ID to forward
to string[] Yes Recipients to forward to
body string No Message to prepend
send boolean No Send immediately (default: true, false = save as draft)

Transport (v2.5.0): when SMTP is configured, forward-message sends via clean SMTP (a fresh message with the original quoted, no threading headers — a forward starts a new conversation). When SMTP is not configured it falls back to Mail.app's AppleScript forward … without opening window. See SMTP transport.

⚠️ Safety: With the defa