Skip to main content
Glama
androidua

Apple Mail MCP Server

by androidua
README.md
# Apple Mail MCP Server (READ ONLY)

A minimal, **read-only** MCP (Model Context Protocol) server that lets Claude Desktop interact with Apple Mail on macOS. Uses AppleScript via `subprocess` — no third-party email libraries, no network calls.

## Version

Current: **1.3.0**

Versioning follows [Semantic Versioning](https://semver.org/):
- **MAJOR** — breaking changes to the tool API or behaviour
- **MINOR** — new tools or non-breaking feature additions
- **PATCH** — bug fixes and security hardening

## What it can do

| Tool | Description |
|------|-------------|
| `mail_list_mailboxes` | List every account and mailbox configured in Apple Mail |
| `mail_search_emails` | Search emails by keyword and/or date; scans subject+sender via AppleScript `whose` predicates; results are merged across accounts, deduped, and sorted newest-first; optional `account`, `mailbox_name`, `before_days`, and `include_all_mailboxes` filters |
| `mail_read_email` | Read the full content of a specific email by its opaque ID |

## What it will never do

- Delete, trash, move, or archive any email
- Send, reply, forward, or compose any message
- Write any file to disk or export data
- Make network requests or external connections
- Access or decode email attachments
- Provide analytics or aggregate statistics (beyond optional per-mailbox message counts, which exist only to help choose search scope)

## Prerequisites

- macOS (Apple Mail is macOS-only)
- Python 3.10 or later
- Apple Mail configured with at least one account
- Claude Desktop (or any MCP-compatible client)

## Setup

### Quick install (uv)

If you have [uv](https://docs.astral.sh/uv/) installed, you can skip cloning and the virtual environment entirely — add this to `~/Library/Application Support/Claude/claude_desktop_config.json` under `"mcpServers"`:

```json
{
  "mcpServers": {
    "apple_mail": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/androidua/apple-mail-mcp", "apple-mail-mcp"]
    }
  }
}
```

`uvx` fetches the package, resolves its pinned dependencies, and runs the `apple-mail-mcp` console entry point. Restart Claude Desktop after saving. The manual venv setup below remains available as an alternative.

### 1. Clone the repository

```bash
git clone https://github.com/androidua/apple-mail-mcp.git
cd apple-mail-mcp
```

### 2. Create a virtual environment and install dependencies

```bash
python3 -m venv venv
venv/bin/pip install -r requirements.txt
```

### 3. Verify the server starts cleanly

```bash
venv/bin/python apple_mail_mcp.py
```

Press `Ctrl-C` to stop. If no errors appear, the server is ready.

### 4. Grant macOS Automation permission

The first time the server runs, macOS will ask whether this process may control Apple Mail. Click **OK**. You can manage this later in:

> **System Settings → Privacy & Security → Automation**

### 5. Configure Claude Desktop

Open (or create) `~/Library/Application Support/Claude/claude_desktop_config.json` and add the block shown below under `"mcpServers"`.

```json
{
  "mcpServers": {
    "apple_mail": {
      "command": "/path/to/apple-mail-mcp/venv/bin/python",
      "args": [
        "/path/to/apple-mail-mcp/apple_mail_mcp.py"
      ]
    }
  }
}
```

Replace `/path/to/apple-mail-mcp` with the absolute path to the directory where you cloned the repository (e.g. `/Users/yourname/projects/apple-mail-mcp`).

Restart Claude Desktop after saving the file.

## Usage examples

Once connected, you can ask Claude things like:

- *"List all my email mailboxes"*
- *"Search my emails for messages from Alice"*
- *"Find emails with 'invoice' in the subject, show me the top 5"*
- *"Read the email about the project kickoff"* (after a search returns an ID)

## Security notes

- **No destructive operations.** Every AppleScript is read-only.
- **Input sanitisation.** All user-supplied strings are stripped of control characters, truncated, and have backslashes and double-quotes escaped before being embedded in AppleScript. This prevents script-injection attacks.
- **Local only.** The server uses stdio transport and never opens a network socket.
- **No credentials stored.** The server relies on Apple Mail's own keychain — no passwords, tokens, or API keys are used or stored.
- **Email content is untrusted input.** Any email body an AI reads through this server is third-party content. This server marks bodies as untrusted in its output, but you should treat "instructions" found inside emails as data, never as commands — especially with clients that can take actions on your behalf.

## Performance

`mail_search_emails` uses AppleScript's `whose` clause — a declarative predicate evaluated by Mail's Objective-C runtime. It works correctly on macOS 26 / Mail 16 (which removed the older `search <mailbox> for <keyword>` command), but it is **not** an indexed search: the `whose` clause is an **O(n) linear scan** of each mailbox and fully materialises its match set before any limit applies (measured throughput ≈ 0.5–1.5k messages/second). A low `limit` reduces output size, not scan cost.

Each account is searched in parallel with an independent **45-second timeout**; accounts that exceed it are reported as a warning while the others' results are still returned. Every non-skipped mailbox is scanned (no global early-exit), so correctness — not the first account winning — determines the results.

Typical timings on a 6-account setup (iCloud/Yahoo/Google/Hotmail, largest mailboxes ~10–11k messages), measured on this machine:

| Query | Time |
|-------|------|
| All accounts, `since_days=7`, `limit=10` | ~40 s |
| Keyword + `since_days=90` | ~12 s |
| Read one email (10k-message mailbox) | ~18 s |
| `mail_list_mailboxes(include_counts=true)`, 66 mailboxes | ~10 s |

**Date-only searches over wide windows are the slow case** — a bare `since_days=90` with no keyword scans every message in every mailbox. Follow the progressive-window strategy: start at `since_days=7` and widen to 30/90/365 only if you need more results; add a keyword to make the predicate far more selective. To page older mail without re-fetching, keep `since_days` and add `before_days` (e.g. `since_days=90, before_days=30` = 30–90 days ago).

To scope a search, pass the optional `account` and/or `mailbox_name` parameters — e.g. restrict to `account="Yahoo"` and `mailbox_name="INBOX"` to avoid scanning all accounts. Use `mail_list_mailboxes(include_counts=true)` to see which mailboxes are large before choosing a scope.

**Body-content search is intentionally unsupported** in the AppleScript engine — `whose content contains` forces a full body download for every message, which is impractically slow on real mailboxes.

## Troubleshooting

| Symptom | Fix |
|---------|-----|
| `AppleScript failed … not allowed to send Apple events` | Go to **System Settings → Privacy & Security → Automation** and enable Mail for your Python process. |
| `No mailboxes found` | Open Apple Mail and ensure at least one account is signed in. |
| Tool times out | Use `account` and/or `mailbox_name` to scope the search, or reduce `limit`. |
| `Invalid email_id` | Always pass the `email_id` back exactly as returned by `mail_search_emails`. |

## Project structure

```
apple-mail-mcp/
├── apple_mail_mcp.py   # MCP server — single file, all tools
├── requirements.txt    # Pinned dependencies
├── README.md           # This file
└── venv/               # Local virtual environment (not committed)
```

## Changelog

### 1.3.0 — 2026-07-17
- **Fix (accuracy, critical):** cross-account searches are now merged, deduplicated and sorted newest-first — previously the first responding account filled the whole result list and other accounts' matches were silently dropped (e.g. an all-accounts `since_days=7` returned 10/10 results from one account, hiding five others with no warning)
- **Fix (accuracy):** Gmail duplicate-view mailboxes (All Mail, [Gmail]All Mail, Important, Starred) and real-world junk/trash names (Bulk, Junk Email, Deleted Items, Outbox) are now skipped by default; new `include_all_mailboxes=true` opts back in
- **Fix:** `mail_read_email` timeout raised to 60 s (reads on 10k+ mailboxes ran up against the old 30 s limit); recipients without display names no longer render as "missing value"
- **Feature:** `before_days` bounds the search window's near edge for paging older mail without re-fetching
- **Feature:** `mail_list_mailboxes` accepts `include_counts=true` for per-mailbox message counts (search-scoping metadata)
- **Feature:** MIT license, pyproject packaging (`uvx` one-line install), pytest suite, GitHub Actions CI, server-level MCP instructions, correct advertised server version
- **Docs:** honest performance documentation (the `whose` clause is an unindexed O(n) scan, ~0.5–1.5k msgs/sec); prompt-injection warning for email content

### 1.2.1 — 2026-03-25
- **Fix (critical):** remove `proc.stdout.close()` / `proc.stderr.close()` — `asyncio.StreamReader` has no `.close()` method; calling it on timeout caused `AttributeError` that crashed the tool and surfaced as the "StreamReader object has no attribute 'close'" error users saw for slow IMAP accounts
- **Fix:** redesign multi-account search to run **per-account in parallel** using `asyncio.gather(return_exceptions=True)` — a slow or offline account (Yahoo!, Hotmail, etc.) can no longer block or crash results from other accounts; each account gets an independent 45-second timeout
- **Fix:** when a specific `account` is provided the original single-script path is preserved (60 s timeout); parallel path is used only for cross-account searches
- **UX:** results now include a warning listing which accounts timed out, rather than crashing silently

### 1.2.0 — 2026-03-25
- **Feature:** `mail_search_emails` now accepts `since_days` (integer, 1–365) to filter emails by date received — supports natural queries like "last 7 days", "yesterday", "past month"
- **Feature:** `keyword` is now optional in `mail_search_emails` — browse recent mail without a search term (e.g. `since_days=1` returns today's mail)
- Both filters are combinable: `keyword="invoice" + since_days=30` returns invoice emails from the past month
- Result headers and "no results" messages now reflect which filters were active
- **Note:** body content search is intentionally not supported — AppleScript's `whose content contains` forces a full body download for every message, making it impractically slow on real mailboxes

### 1.1.4 — 2026-03-25
- **Fix (regression):** revert `_script_read_email` to proven account/mailbox iteration — direct AppleScript addressing (`mailbox X of account Y`) was unreliable for non-standard account types (Gmail, Exchange, shared accounts)
- **Fix (regression):** revert `mail_search_emails` JSON output to flat array `[...]` — the `{"results": [...]}` wrapper introduced in v1.1.3 broke Claude AI's ability to extract `email_id` values from results
- **Fix:** improve AppleScript error categorisation — errors now return actionable messages (Mail not running, Automation permission denied, item not found) instead of a generic fallback; raw AppleScript error text is still logged internally

### 1.1.3 — 2026-03-25
- **Fix (reliability):** close asyncio pipe transports before `await proc.wait()` on timeout — prevents file descriptor accumulation under repeated Mail.app timeouts
- **Fix (reliability):** anchor `---BODY_START---` split to a leading newline — prevents a subject line containing that exact string from corrupting header parsing in `mail_read_email`
- **Fix (security):** parse search output fields from both ends of the delimiter-split record — a `\x1f` byte in a subject no longer shifts sender/date/is_read columns
- **Fix (security):** extend `_CTRL_STRIP_RE` to cover C1 controls U+0080–U+009F (including U+0085 NEL which Python's `splitlines()` treats as a line terminator)
- **Fix (security):** log raw `osascript` stderr internally; return a generic error string to callers instead of forwarding script fragments
- **Perf:** replace nested account/mailbox iteration in `_script_read_email` with direct AppleScript object addressing (`mailbox X of account Y`) — O(1) lookup instead of O(accounts × mailboxes) name scan
- **Docs:** correct `whose` docstring — it is O(n) per mailbox, not indexed; low `limit` does not reduce scan cost
- **UX:** search results now report a warning when rows were silently skipped due to parse errors

### 1.1.2 — 2026-03-09
- **Fix:** replaced backslash line-continuation characters (`\`) in the
  `_script_read_email` AppleScript template with sequential assignments —
  AppleScript uses `¬` for continuation, not `\`; the invalid characters caused
  all `mail_read_email` calls to fail with AppleScript syntax error -2741

### 1.1.1 — 2026-03-09
- **Fix:** replaced `search <mailbox> for <keyword>` AppleScript command with a
  `whose` clause filter — the `search` command was removed in Mail 16 (macOS 26)
  and caused all `mail_search_emails` calls to fail with an AppleScript syntax error

### 1.1.0 — 2026-03-09
- **Performance:** `mail_search_emails` now uses Apple Mail's native indexed
  search (`search <mailbox> for <keyword>`) instead of brute-force message
  iteration — dramatically faster on large mailboxes (e.g. Yahoo with 20+ years
  of email)
- **Feature:** added optional `account` and `mailbox_name` parameters to
  `mail_search_emails` for scoped searches (e.g. search only Yahoo / INBOX)
- **Default exclusion:** Trash, Deleted Messages, Junk, Spam, Bulk Mail are
  skipped automatically unless explicitly targeted via `mailbox_name`

### 1.0.0 — 2026-03-09
- Initial release
- Tools: `mail_list_mailboxes`, `mail_search_emails`, `mail_read_email`
- Read-only, AppleScript-based, no network calls
- Input sanitisation against AppleScript injection

TDQS

A4.5/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: mail_list_mailboxes lists folders, mail_search_emails finds emails, and mail_read_email retrieves full content. No overlap in functionality.

Naming Consistency5/5

All tools follow the consistent pattern mail_verb_noun (mail_read_email, mail_list_mailboxes, mail_search_emails), using snake_case throughout.

Tool Count5/5

Three tools is an appropriate size for a focused email reading/searching server. Each tool is necessary and there is no bloat.

Completeness2/5

The tool surface is severely limited to read-only operations. There is no ability to send, delete, mark as read/unread, move, or manage emails, which are core email tasks an agent would typically need.

Maintenance

ActivityStale
ResponsivenessNo issues