cobos-apple-mail-mcp
Unifies fast on-disk read and search with complete AppleScript writes, behind a mandatory safety layer with read-only mode, batch caps, dry-run, and undo for reversible operations.
README
<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).
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 <(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 and Install.
Full depth lives in the GitHub 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 · Performance · Quick start · Install · Register a client · Docs
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:
- 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
MultipleMatchesand asks for disambiguation — it never guesses. This directly replaces the fragilesubject_keywordsubstring matching used by the write-capable upstream, which can hit the wrong message in a thread with repeated subjects. - A mandatory safety layer, not an optional convention:
--read-onlydisables every send/modify tool (draft creation stays allowed); batch limits reject oversized requests instead of silently truncating them;dry_runpreviews with zero mutation; permanent delete/empty-trash requireconfirm=true; reversible writes (move, trash, flag/status) are journaled soundo_last()can reverse them — and the server is honest that sends and permanent deletes are not undoable, rather than pretending otherwise. - 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-onlyblocks 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_emailfetch: low single-digit milliseconds (reads one.emlxfile). - Full-mailbox
searchfor 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 buildwithout--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
.emlxfiles and an immutable SQLite read directly. --watchincremental 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
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/Maildirectly (theEnvelope IndexSQLite database +.emlxfiles) 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:
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 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=1sidesteps SQLite locking instead of racing Mail.app for it; the.emlxfile 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
SearchBackendseam in case that ever changes. - Apple
NaturalLanguageas 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.
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 <(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 below, plus client
registration. Prefer control? Pick a manual path in 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.
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/Mailfor indexing) and Automation permission for Mail.app (to script writes) — System Settings → Privacy & Security. Full instructions: Permissions & troubleshooting.
uvx / pipx / pip
uvx is the zero-install path most MCP clients use — it fetches and runs on demand:
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 and 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):
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
for how to build this yourself (make pyz) and the apple-mail-mcp-full.pyz variant that bundles
[watch]+[semantic].
Read-only mode
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? It automates steps 1–5 — skip ahead to 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. You can skip this to try read-only search first — indexing only needs Full Disk Access, not Automation.
2. Initialize config.
$ 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.
$ 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.
$ 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.
$ 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 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.
Register with your MCP client
If you used the one-command installer, your server is already registered — verify it below, then jump to Try a bundled recipe.
Test first with the official inspector:
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:
{
"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.)
Claude Code (CLI) — also drives Cowork automations:
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:
[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:
{
"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.
Try a bundled recipe
The fastest way to see the knowledge layer in action:
$ 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
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):
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.
Documentation
- GitHub 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 — the Phase 0 research this design is based on.
- 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 (GPL-3.0) and patrickfreyer/apple-mail-mcp (MIT) — see NOTICE for full attribution. All original work (the identity bridge, safety/undo layer, knowledge/triage layer, and packaging) is by Ernesto Cobos.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.