cobos-apple-mail-mcp
by ErnestoCobos
README.md
<p align="center">
<img src="docs/assets/hero.svg" alt="cobos-apple-mail-mcp — a unified Apple Mail MCP server: fast on-disk reads and search, plus complete AppleScript writes, behind one safety layer" width="100%">
</p>
<p align="center">
<a href="https://pypi.org/project/cobos-apple-mail-mcp/"><img src="https://img.shields.io/pypi/v/cobos-apple-mail-mcp?color=2d6cdf&label=PyPI" alt="PyPI"></a>
<img src="https://img.shields.io/badge/python-3.10%2B-2d6cdf" alt="Python 3.10+">
<img src="https://img.shields.io/badge/platform-macOS-8a8f98" alt="Platform: macOS">
<img src="https://img.shields.io/badge/MCP%20tools-31-34a853" alt="31 MCP tools">
<a href="https://github.com/ErnestoCobos/cobos-apple-mail-mcp/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0--or--later-f5b94a" alt="License: GPL-3.0-or-later"></a>
</p>
# cobos-apple-mail-mcp
**A unified Apple Mail MCP server: fast on-disk reads and search, plus complete AppleScript
writes, behind one safety layer.** GPL-3.0-or-later. By Ernesto Cobos
([@ErnestoCobos](https://github.com/ErnestoCobos)).
```
search "budget review" --highlight → ~ms, full-mailbox, BM25-ranked
get_inbox_overview → ~ms, computed from a local index
move_email + undo_last → AppleScript, resolved by Message-ID, reversible
```
**Install it in one command** (macOS, for Claude Desktop / Cowork):
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/ErnestoCobos/cobos-apple-mail-mcp/main/scripts/install-claude-desktop.sh)
```
Installs the CLI, builds the local index, and registers the server — backing up your config,
merging in only the one entry. Other paths (`uvx` / `pipx` / `.pyz`, other clients, read-only) are
in [Quick start](#quick-start-one-command) and [Install](#install).
Full depth lives in the **[GitHub Wiki](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki)**
(architecture, on-disk format, every tool's parameters, configuration reference, troubleshooting).
This README is the quickstart and the pitch.
**Jump to:** [Why it's better](#why-its-better-than-the-alternatives) ·
[Performance](#performance) · [Quick start](#quick-start-one-command) · [Install](#install) ·
[Register a client](#register-with-your-mcp-client) ·
[Docs](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki)
## Why it's better than the alternatives
Two families of Apple Mail MCP server exist today — fast but read-only, or write-capable but slow.
This one is both, and fixes what neither got right:
| | Read speed | Full-mailbox body search | Writes | Message targeting | Safety layer | Knowledge/triage |
|---|---|---|---|---|---|---|
| **Read-only servers** (e.g. imdinu/apple-mail-mcp) | Fast (disk-direct) | Yes | **None** | n/a | n/a | No |
| **Write-only servers** (e.g. patrickfreyer/apple-mail-mcp) | Slow (AppleScript) | Limited/none | Yes | **Subject substring — can hit the wrong message** | Partial (batch caps, some dry-run) | Heuristic, AppleScript-computed |
| **cobos-apple-mail-mcp** | Fast (disk-direct) | Yes (FTS5, optional semantic) | Yes (AppleScript) | **Canonical Message-ID, read-back verified** | Mandatory: read-only mode, batch caps, dry-run, confirm, honest undo | Heuristic, index-computed (fast) |
Concretely, this project is the union of both upstreams' strengths plus three things neither
had:
1. **An identity bridge that can't silently act on the wrong message.** Every write resolves
the target by RFC822 Message-ID, scoped by hints/cache/the read layer's own context, then
**verifies the match by reading its Message-ID back** before mutating anything. If more than
one message matches, the server returns `MultipleMatches` and asks for disambiguation — it
never guesses. This directly replaces the fragile `subject_keyword` substring matching used
by the write-capable upstream, which can hit the wrong message in a thread with repeated
subjects.
2. **A mandatory safety layer**, not an optional convention: `--read-only` disables every
send/modify tool (draft creation stays allowed); batch limits *reject* oversized requests
instead of silently truncating them; `dry_run` previews with zero mutation; permanent
delete/empty-trash require `confirm=true`; reversible writes (move, trash, flag/status) are
journaled so `undo_last()` can reverse them — and the server is honest that sends and
permanent deletes are **not** undoable, rather than pretending otherwise.
3. **Never hangs.** Every `osascript`/JXA call runs in its own process group with a hard timeout
and is killed outright on expiry — bypassing Apple Events' own ~2-minute default wait. Reads
never depend on Mail.app being responsive at all. `--read-only` blocks fail in milliseconds,
before any AppleScript call is even attempted.
## Performance
Measured against a real 7-account, 210,152-message mailbox (not a synthetic benchmark):
- Single `get_email` fetch: low single-digit milliseconds (reads one `.emlx` file).
- Full-mailbox `search` for a realistic, selective term (`"invoice"`, or `"meeting"` scoped to
subject): **9-20ms**, BM25-ranked. This is the common case.
- A deliberately non-selective single-word query matching a large fraction of the whole corpus
(e.g. `"the"`, matching ~39% of all 210k messages here) is **not** sub-100ms — BM25 has to rank
tens of thousands of candidates before returning the top page, observed at 0.3-1.6s depending on
system load. Real queries are essentially never this unselective; this is a documented edge
case, not a hidden one.
- First `index build --full`: one-time, ~3ms/message (679.6s / 210,152 messages here) — includes
HTML→text conversion and full JWZ re-threading, not just parsing. Every build after that is
incremental (`index build` without `--full`, or `--watch`) and only touches what changed.
- Index build (incremental) is far faster than scripting Mail.app for the same scan, because it
never launches AppleScript — it walks changed `.emlx` files and an immutable SQLite read
directly.
- `--watch` incremental updates: new mail typically reflected in the index within a couple of
seconds of arrival, debounced and batched.
## What you get — 31 tools in one server
```mermaid
flowchart TB
HUB(["cobos-apple-mail-mcp<br/>31 MCP tools + email:// resources + recipes"])
HUB --> R["Read & search<br/>get_email · list/get_emails<br/>search (FTS5 · semantic · attachment text)<br/>threads · links · export"]
HUB --> K["Knowledge<br/>inbox overview · needs-response<br/>awaiting-reply · top senders<br/>statistics · contacts"]
HUB --> W["Write — safety-gated<br/>compose / reply / forward · drafts<br/>move · flag-color · mark read<br/>mailboxes · trash · undo_last"]
HUB --> A["Automate<br/>Mail rules (list/enable/disable/delete)<br/>unsubscribe (RFC 8058 one-click)"]
classDef hub fill:#2d6cdf,stroke:#1b3a75,color:#fff
class HUB hub
```
## What it is, and how it works
Apple Mail MCP servers split into two families, and neither half alone is enough for an agent
that needs to actually *do* email work:
- **Read-only servers** read `~/Library/Mail` directly (the `Envelope Index` SQLite database +
`.emlx` files) for millisecond-fast search, but can't send, move, or flag anything.
- **Write-capable servers** drive Mail.app via AppleScript/JXA — correct for writes, but every
*read* also goes through Mail.app scripting, which is 100-1000x slower than reading the files
directly, and full-text body search across the whole mailbox usually isn't offered at all.
`cobos-apple-mail-mcp` runs both paths in one server:
```mermaid
flowchart TB
MCP["MCP client — tools · email:// resources · recipes"]
MCP -->|"every write"| GUARD{"guard()<br/>read-only · batch caps<br/>dry-run · confirm · undo"}
GUARD --> READ["ReadBackend — fast, never touches Mail.app<br/>Envelope Index + .emlx + FTS5 index"]
GUARD --> WRITE["WriteBackend<br/>AppleScript / JXA via osascript"]
DISK[("~/Library/Mail<br/>on disk")] -->|"read immutable"| READ
WRITE -->|"send / move / flag"| MAIL["Mail.app"]
READ -. "canonical RFC822 Message-ID<br/>(read-back verified — never a fuzzy subject match)" .-> WRITE
classDef safe fill:#e8a838,stroke:#8a5a00,color:#222
class GUARD safe
```
Reads never touch Mail.app at all — they query a local FTS5 index built from the same on-disk
data Mail itself uses, kept fresh by an incremental `--watch` updater. Writes go through
AppleScript/JXA (the only correct way to make Mail.app send/move/flag anything), with every
target message resolved by its canonical Message-ID and **read-back verified** before any
mutation — never picked by a fuzzy subject match. See
[Architecture](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki/Architecture) for the
full design.
## Why each major choice (one line each — full rationale in the Wiki and RESEARCH.md)
- **Immutable Envelope Index reads** — `file:...?immutable=1` sidesteps SQLite locking instead
of racing Mail.app for it; the `.emlx` file is the authoritative source for everything anyway.
- **FTS5, not a heavier engine** — verified against Tantivy/DuckDB/Spotlight/vector-native
stores for this exact architecture (embedded, single-user, hybrid-ready); nothing else won
outright. Wrapped behind a `SearchBackend` seam in case that ever changes.
- **Apple `NaturalLanguage` as the default semantic backend** — built into macOS, zero model
download, the most resource-frugal option available; MiniLM is an opt-in fallback.
- **Never-hang as a first-class invariant** — every external call (subprocess, broad scan) is
bounded; nothing in this server can leave an MCP client waiting indefinitely.
- **GPL-3.0-or-later** — the read engine's architecture derives from a GPL-3.0 upstream; see
[NOTICE](NOTICE).
## Quick start (one command)
Already sold? On macOS, one command installs the CLI, builds the local index, and registers the
server with **Claude Desktop / Cowork** — backing up your `claude_desktop_config.json` and merging
in only the one entry, so your other MCP servers are untouched:
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/ErnestoCobos/cobos-apple-mail-mcp/main/scripts/install-claude-desktop.sh)
```
That runs steps 1–5 of the [walkthrough](#getting-started-5-minute-walkthrough) below, plus client
registration. Prefer control? Pick a manual path in [Install](#install). To try search and triage
without any write access, add `--read-only` to the script (or register with
`apple-mail-mcp --read-only serve`). Review-before-run and the general `scripts/install.sh`
(which also registers the Claude Code CLI) are in
[Install per client](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki/Install-per-client).
## Install
### Requirements
- macOS, with Apple Mail configured with at least one account.
- Python 3.10+.
- **Full Disk Access** for your terminal/MCP client host (to read `~/Library/Mail` for indexing)
and **Automation** permission for Mail.app (to script writes) — System Settings → Privacy &
Security. Full instructions:
[Permissions & troubleshooting](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki/Permissions-and-troubleshooting).
### uvx / pipx / pip
`uvx` is the zero-install path most MCP clients use — it fetches and runs on demand:
```bash
uvx cobos-apple-mail-mcp serve # run the server directly (no install)
# or install a persistent CLI:
pipx install cobos-apple-mail-mcp # or: pip install cobos-apple-mail-mcp
# with optional PDF/DOCX attachment-text search:
pipx install "cobos-apple-mail-mcp[attachments]"
apple-mail-mcp init # writes ~/.cobos-apple-mail-mcp/config.toml
apple-mail-mcp index build # first full index build
apple-mail-mcp index status
```
Then walk through [Getting started](#getting-started-5-minute-walkthrough) and register with your
[MCP client](#register-with-your-mcp-client).
### Single file (`.pyz`)
No `pip install` needed — just a matching Python 3.10+ on `$PATH` (run it with the **same Python
minor version** the release was built with — e.g. `python3.12`; the release notes say which one.
This is a verified, not theoretical, requirement: the `.pyz` bundles a compiled dependency tied
to that exact ABI):
```bash
curl -LO https://github.com/ErnestoCobos/cobos-apple-mail-mcp/releases/latest/download/apple-mail-mcp.pyz
python3.12 apple-mail-mcp.pyz init
python3.12 apple-mail-mcp.pyz index build
```
See [Single-file packaging](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki/Single-file-packaging)
for how to build this yourself (`make pyz`) and the `apple-mail-mcp-full.pyz` variant that bundles
`[watch]`+`[semantic]`.
### Read-only mode
```bash
apple-mail-mcp --read-only serve
```
Disables every send/modify tool (draft creation stays allowed). Useful for a first install, or
any time you want search/triage without write access.
## Getting started (5-minute walkthrough)
From a fresh install to asking your MCP client a real question about your inbox. (Already ran the
[one-command installer](#quick-start-one-command)? It automates steps 1–5 — skip ahead to
[Register with your MCP client](#register-with-your-mcp-client).)
**1. Grant permissions.** Give **Full Disk Access** to your terminal/MCP client host and
**Automation** access to Mail.app — System Settings → Privacy & Security. Full walkthrough with
screenshots-equivalent steps:
[Permissions & troubleshooting](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki/Permissions-and-troubleshooting).
You can skip this to try read-only search first — indexing only needs Full Disk Access, not
Automation.
**2. Initialize config.**
```bash
$ apple-mail-mcp init
wrote /Users/you/.cobos-apple-mail-mcp/config.toml
```
Open that file if you want to exclude a mailbox, cap batch sizes further, or turn on semantic
search — every option is commented inline. Defaults work for most setups.
**3. Build the index.** First run reads every `.emlx` file on disk once; after that,
`apple-mail-mcp watch` keeps it current incrementally.
```bash
$ apple-mail-mcp index build --full
{
"added": 210152,
"changed": 0,
"deleted": 0,
"moved": 0,
"failed": 0,
"duration_sec": 679.6,
"full": true
}
```
(Real numbers from a first full build against a 7-account, 210k-message mailbox on this project's
own dev machine — about 11 minutes, or ~3ms/message, one-time. A mailbox in the low thousands
finishes in a few seconds. `--watch` (or a plain `index build` without `--full` afterwards) is
incremental from here — only new/changed/deleted mail gets reparsed, typically reflected within a
couple of seconds. `failed` counts messages that couldn't be parsed at all; a handful is normal
across years of varied real-world mail — inspect them anytime with `apple-mail-mcp index status`,
they're dead-lettered and never block the rest of the build.)
**4. Confirm it's live.**
```bash
$ apple-mail-mcp index status
{
"mail_dir": "/Users/you/Library/Mail/V10",
"envelope_index_available": true,
"total_indexed": 210152,
"pending_added": 0,
"pending_changed": 0,
"stale": false,
...
}
```
**5. Search it.**
```bash
$ apple-mail-mcp search "invoice" --scope subject --highlight --limit 3
{
"query": "invoice",
"mode": "keyword",
"scope": "subject",
"total_estimated": 14,
"returned": 3,
"timing_ms": 3.2,
"hits": [
{
"message_ref": {"message_id": "abc123@vendor.example.com", "account": "Work", "mailbox": "INBOX"},
"score": 9.4,
"subject": "Your <mark>invoice</mark> #4471 is ready",
"sender_name": "Billing",
"sender_addr": "billing@vendor.example.com",
"date_received": 1782950400,
"is_read": true,
"snippet_html": "Attached is your <mark>invoice</mark> for this billing period..."
}
]
}
```
Sub-100ms, full-mailbox, no Mail.app scripting involved. Try `apple-mail-mcp overview` or
`apple-mail-mcp awaiting-reply` next — both are computed instantly from the same local index.
**6. Register with your MCP client and ask it something.** See
[Register with your MCP client](#register-with-your-mcp-client) below (Claude Desktop / Claude
Code / Codex / Kimi). After restarting the client, try asking:
> "What's in my inbox that still needs a reply?"
The client calls `get_needs_response` (or `get_awaiting_reply`) under the hood, gets back
ranked, structured results in milliseconds, and answers from those — no email content needed to
round-trip through a slow AppleScript read first. Ask it to draft a reply to one of them and it
calls `reply_to_email` in draft mode; nothing sends without you reviewing it in Mail.app first
(and nothing sends *at all* if you registered with `--read-only`). Then try a
[bundled recipe](#try-a-bundled-recipe).
## Register with your MCP client
If you used the [one-command installer](#quick-start-one-command), your server is already
registered — verify it below, then jump to [Try a bundled recipe](#try-a-bundled-recipe).
Test first with the official inspector:
```bash
npx @modelcontextprotocol/inspector apple-mail-mcp serve
```
**Claude Desktop & Cowork** — the desktop app (and its Cowork workspace) load local servers from
one file. Edit it (Settings → Developer → Edit Config, or directly)
`~/Library/Application Support/Claude/claude_desktop_config.json`, adding `apple-mail` next to any
servers you already have:
```json
{
"mcpServers": {
"apple-mail": {
"command": "apple-mail-mcp",
"args": ["serve"]
}
}
}
```
Restart Claude Desktop completely (Cmd-Q, then reopen) after editing — the same app serves Cowork,
so it's registered there too. If the app can't find `apple-mail-mcp`, use the absolute path from
`which apple-mail-mcp`, and grant **Claude Desktop itself** Full Disk Access. (Single-file variant:
`"command": "python3.12", "args": ["/absolute/path/apple-mail-mcp.pyz", "serve"]` — use the Python
minor version that built the `.pyz`, not just any `python3`; see [Single-file packaging](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki/Single-file-packaging).)
**Claude Code (CLI)** — also drives Cowork automations:
```bash
claude mcp add apple-mail -- apple-mail-mcp serve
claude mcp list # verify
```
For a team-shared registration, use `--scope project` (writes to `.mcp.json` in the repo).
**OpenAI Codex CLI** — add to `~/.codex/config.toml`:
```toml
[mcp_servers.apple-mail]
command = "apple-mail-mcp"
args = ["serve"]
startup_timeout_sec = 10
```
or: `codex mcp add apple-mail -- apple-mail-mcp serve`
**Kimi CLI (Moonshot)** — add to `~/.kimi/mcp.json`:
```json
{
"mcpServers": {
"apple-mail": { "type": "stdio", "command": "apple-mail-mcp", "args": ["serve"] }
}
}
```
or: `kimi mcp add apple-mail -- apple-mail-mcp serve`, then `kimi mcp test apple-mail`.
Full per-client details, troubleshooting, and absolute-path notes:
[Install per client](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki/Install-per-client).
## Try a bundled recipe
The fastest way to see the knowledge layer in action:
```bash
$ apple-mail-mcp recipe run daily-triage
```
or, from your MCP client, just ask for your "daily triage" — recipes are registered as MCP
prompts, so the client can invoke them by name. See
[Resources and prompts/recipes](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki/Resources-and-prompts-recipes)
for the other four (`inbox-zero`, `awaiting-reply`, `weekly-review`, `thread-catchup`) and how to
write your own.
## CLI usage
Every tool is also a standalone CLI subcommand with JSON output (the same tools your MCP client
calls — handy from a shell or a script):
```bash
apple-mail-mcp search "invoice" --scope subject --highlight
apple-mail-mcp emails --filter unread --limit 20
apple-mail-mcp thread --message-id "abc123@example.com"
apple-mail-mcp overview
apple-mail-mcp awaiting-reply --days-back 14
apple-mail-mcp move m1@example.com --to-mailbox Archive --dry-run
apple-mail-mcp move m1@example.com --to-mailbox Archive
apple-mail-mcp undo-last
apple-mail-mcp recipe run daily-triage --arg account=Work
```
Full reference: [Tools reference](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki/Tools-reference).
## Documentation
- [GitHub Wiki](https://github.com/ErnestoCobos/cobos-apple-mail-mcp/wiki) — architecture,
on-disk format, identity/resolution, safety/undo, indexing & watch, search, threading &
knowledge, every tool's parameters, resources & recipes, configuration reference, permissions
& troubleshooting, single-file packaging, install per client, performance, development.
- [RESEARCH.md](RESEARCH.md) — the Phase 0 research this design is based on.
- [CLAUDE.md](CLAUDE.md) — project memory: invariants, knowledge map, conventions.
## License & attribution
GPL-3.0-or-later. This project merges architectural ideas and, in places, adapted code from
[imdinu/apple-mail-mcp](https://github.com/imdinu/apple-mail-mcp) (GPL-3.0) and
[patrickfreyer/apple-mail-mcp](https://github.com/patrickfreyer/apple-mail-mcp) (MIT) — see
[NOTICE](NOTICE) for full attribution. All original work (the identity bridge, safety/undo
layer, knowledge/triage layer, and packaging) is by Ernesto Cobos.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessUnresponsive