This file provides guidance for AI agents (Claude, etc.) when using this MCP server.
This MCP server enables AI assistants to interact with Apple Mail on macOS via AppleScript. All operations are local - no data leaves the user's machine.
When sending content with backslashes to any tool, you MUST escape them.
The MCP protocol uses JSON for parameters. In JSON, \ is an escape character. To include a literal backslash:
| You want | Send in JSON parameter |
|---|---|
\ |
\\ |
\\ |
\\\\ |
C:\Users\ |
C:\\Users\\ |
If you send a single backslash without escaping:
\ as an escape sequence\ (backslash-space) cause silent failuresCorrect - Windows path in email:
body: "The file is at C:\\Users\\Documents\\report.pdf"
Incorrect - Will fail:
body: "The file is at C:\Users\Documents\report.pdf"
All message operations require an id parameter. Always get IDs first using list-messages or search-messages:
# List messages returns IDs
list-messages mailbox="INBOX"
→ Messages with IDs like "12345", "12346", etc.
# Use ID for all subsequent operations
get-message id="12345"
mark-as-read id="12345"
delete-message id="12345"
reply-to-message id="12345" body="Thanks!"
The to, cc, and bcc parameters must always be arrays:
Correct:
{
"to": ["bob@example.com"],
"subject": "Hello"
}
Incorrect:
{
"to": "bob@example.com",
"subject": "Hello"
}
send-email for immediate sendingcreate-draft when the user should review firstattachments parameter (array of absolute file paths)create-draft and tell the user to review in Mail.app{{placeholder}} tokens in subject and body, replaced per-recipient{ "Name": "Alice", "Company": "Acme" }replyAll: true to reply to all recipientssend: false to save as draft instead of sending immediatelywithout opening window internally — no Mail.app compose window is opened, which ensures reliable body delivery from background processes (see Known Issues below)id and to arraybody to prepend a messagesend: false to save as draftwithout opening window internally — same background-process fix as reply-to-messagesearch-messages searches all accounts when no account is specifiedlist-accounts to see available accountsaccount parameter to target specific account| Error | Likely Cause |
|---|---|
| "Mail.app not responding" | Mail.app frozen or not running |
| "Message not found" | Message ID is invalid or message was deleted/moved |
| "Permission denied" | macOS automation permission needed |
| "Account not found" | Account name doesn't match exactly (case-sensitive) |
| "Failed to send email" | Network issue or Mail.app configuration problem |
| Silent failure | Backslash not escaped in content |
create-draft for review.1. list-accounts → get available accounts
2. search-messages query="boss@company.com" → find emails from boss
3. get-message id="..." → read the full content
1. get-message id="..." → read original message
2. reply-to-message id="..." body="..." send=false → save as draft
3. Tell user to review in Mail.app before sending
1. create-draft to=["recipient@example.com"] subject="..." body="..."
2. Tell user: "I've created a draft. Review it in Mail.app and send when ready."
OR if user confirms they want to send immediately:
3. send-email to=["recipient@example.com"] subject="..." body="..."
1. get-message id="..." → read the message to forward
2. forward-message id="..." to=["colleague@company.com"] body="FYI - see below"
1. search-messages query="newsletter" → find newsletters
2. For each: move-message id="..." mailbox="Archive"
1. search-messages query="old" → find messages to clean up
2. batch-delete-messages ids=["123", "456", "789"] → delete multiple
OR
batch-move-messages ids=["123", "456"] mailbox="Archive" → archive multiple
OR
batch-mark-as-read ids=["123", "456"] → mark multiple as read
OR
batch-mark-as-unread ids=["123", "456"] → mark multiple as unread
OR
batch-flag-messages ids=["123", "456"] → flag multiple
OR
batch-unflag-messages ids=["123", "456"] → unflag multiple
Note: all batch operations are limited to 100 messages per request
1. list-messages mailbox="INBOX" → get message IDs
2. list-attachments id="..." → see attachments (name, MIME type, size)
3. save-attachment id="..." attachmentName="report.pdf" savePath="/tmp" → save to disk
1. list-mailboxes → see all folders
2. create-mailbox name="Projects" → create new folder
3. rename-mailbox oldName="Projects" newName="Active Projects" → rename
4. delete-mailbox name="Old Folder" → delete
1. list-rules → see all rules and their status
2. disable-rule name="Newsletter Filter" → turn off a rule
3. enable-rule name="Newsletter Filter" → turn it back on
1. search-contacts query="John" → find contacts by name
Returns names, email addresses, and phone numbers from Contacts.app
1. save-template name="Weekly Report" subject="Weekly Report" body="..." to=["team@..."]
2. list-templates → see saved templates
3. use-template id="tmpl_1" → create draft from template
4. use-template id="tmpl_1" to=["other@..."] → override recipients
Note: templates are stored in memory and reset when the server restarts
1. send-email to=["colleague@company.com"] subject="Report" body="See attached." attachments=["/Users/me/report.pdf"]
OR to let the user review first:
2. create-draft to=["colleague@company.com"] subject="Report" body="See attached." attachments=["/Users/me/report.pdf"]
Note: attachment paths must be absolute and the files must exist; max 20 files per message
1. send-serial-email recipients=[
{"email": "alice@acme.com", "variables": {"Name": "Alice", "Company": "Acme"}},
{"email": "bob@globex.com", "variables": {"Name": "Bob", "Company": "Globex"}}
] subject="Hello {{Name}}" body="Great to connect about {{Company}}."
Each recipient gets their own individual email with placeholders replaced.
1. get-sync-status → see if Mail.app is running and syncing
2. get-mail-stats → see total/unread counts and recently received counts
Prior to v1.4.0, reply-to-message and forward-message would send replies/forwards with empty body text when the MCP server was running as a background process (e.g., spawned via execSync from a Node.js MCP server, which is how Claude Code invokes it).
The root cause was the AppleScript reply msg with opening window command. This creates a GUI compose window asynchronously. When set content runs immediately after, the window may not be ready yet, and the content assignment is silently ignored. Even adding delays (delay 1, delay 2) was unreliable — the compose window's readiness depends on system load, Mail.app state, and whether the process has GUI access.
A secondary issue: the old code appended & content of theReply (the original quoted message) to the body. This was always a no-op — the quoted content lives in the HTML layer of the compose window, not the plaintext content property.
Replaced with opening window with without opening window for both reply and forward commands. With this approach:
set content works immediately — no delay neededexecSync, MCP stdio transport)In-Reply-To and References headers are still set correctly by Mail.app (the reply command knows which message it's replying to)reply to all and send both work as expected| Approach | Result |
|---|---|
delay 1 / delay 2 before set content |
Body still empty from background process (works interactively) |
reply msg without opening window (old attempt) |
Previously dismissed, but actually works — set content is reliable without the window |
set html content on reply object |
AppleScript error — not a valid property |
| System Events UI scripting (keystroke) | Blocked: "osascript is not allowed to send keystrokes" from background process |
make new outgoing message with same subject |
Body arrives, but no In-Reply-To/References headers (can't set reply id on outgoing messages) |
Manual headers on outgoing message |
Not possible — Mail.app's outgoing message class doesn't expose a headers property |
Before sending emails with paths or special characters, verify escaping:
~/path/to/file - No escaping needed (no backslashes)C:\Users\ - Needs escaping: C:\\Users\\file\ name.txt - Needs escaping: file\\ name.txt