Look at how I write in my work inbox, then draft this reply in the same voice.
Scoped to one account. APPLE_MAIL_ACCOUNTS bounds what can be read, not just what can be written.
Cupertino puts the Apple apps on your Mac behind MCP, for any agent that speaks it. One Full Disk Access grant, held by a signed menu-bar app you can watch — instead of one for every editor that happens to spawn a server.
macOS 26 or later · 30-minute trial, no account · 30-day refund
MIT servers · unofficial, not affiliated with Apple
What did I promise Marie last week, and is any of it on my calendar?
What your agent called
reads only · one grant · nothing left the Mac
Every one of these carries a constraint — an account, a date bound, a filter. That is the part a shell script gets slowly or gets wrong, and the reason a server earns its place: it holds what the model would otherwise have to re-derive every session.
Look at how I write in my work inbox, then draft this reply in the same voice.
Scoped to one account. APPLE_MAIL_ACCOUNTS bounds what can be read, not just what can be written.
Pull together everything about the Atlas launch from my mail, my notes and my calendar. What do I still owe people?
Three surfaces, one grant — the payoff of shipping them in one bundle.
Find every unread message from my accountant this quarter.
A filter. This query shape costs 74 seconds over Apple Events; the index lane answers it in milliseconds.
What is actually on my calendar next week once the repeating meetings are expanded? Block two hours before the release.
Repeats expanded, and the window the answer covers reported with it — a short list is otherwise indistinguishable from a free afternoon.
Turn the action items from yesterday's client thread into reminders, due Friday.
Reads one surface, writes another. Both write gates off by default.
Did anyone ever reply to the invoice I sent on the 3rd?
The whole pitch, for someone who has never edited a config file.
Writes are off until you turn them on, per surface — and the toggle decides whether the mutating tools are registered at all, so an agent with writes off is never told they exist.
“What did Anna send me about the lease, and is it in my notes?”
Two different lanes, and it does not matter which. Mail came off the Envelope Index, because asking Mail.app the same thing takes 74 seconds. Notes came off Apple Events, because below roughly 5k notes the grant buys nothing there — so it is not asked for.
Full Disk Access is indivisible, and it lands on whatever process launched the server — your editor, your terminal, never Mail itself. A grant for mail is already a grant for Messages, Safari history and SSH keys. Granting it to Mail.app does nothing: the reader needs the permission, not Mail.
Automation is prompted separately, one row per target app. The bundle identifier io.mgcrea.cupertino is the TCC identity, so the grant survives every update.
The tool list is a pure function of allowWrites and nothing else, because MCP clients cache it. With writes off the mutating tools are never registered — the host cannot call what it was never told about, so there is nothing to refuse.
APPLE_MAIL_ALLOW_WRITES=Mutation is one control. On Mail the larger blast radius is reading an entire archive — that is what *_ACCOUNTS is for, and it is enforced in exactly one place so no query path escapes it.
Every tool call, live, grouped by the client that made it. It is the answer to “what did the assistant just do with my mail?” — and the reason the servers run under an app you can see rather than inside whichever editor happened to spawn them.
The menu bar shows the same thing one level up: which client is talking to which server right now, and how many tools it has called. None of it is sent anywhere. The one connection Cupertino can make is the update check, it is off until you turn it on, and it carries no identifier — which CI checks against the built binary rather than the source.
Cupertino runs as a menu-bar app you can open. That is the point of holding the servers in one signed process rather than spawning them inside whichever editor asked first: there is something to look at, and it tells you what happened.

Whether the app can be driven, whether its store can be read, and who is talking to it — one surface at a time.

The same pane as below, seeded with a single turn: “What did I promise Marie last week, and is any of it on my calendar?” Contacts leads, because the question names a person and every other surface is addressed by handle.

The same pane again, this time across everything a working day sends at it. Tool names only — never arguments, message contents or results — and a refused write shows up as a refusal.

One row per connection, because each client opens one per server it is wired to — and a client you have run several times over opens several. Three clients is eighteen rows here, and this is the pane that shows them.

Automation is asked for one app at a time, and writes are a separate switch again. With one off, that surface's mutating tools are never registered — so an agent is not refused, it is never told they exist.
Same query, same mailbox. Apple Events makes Mail.app do the work one message at a time; the index lane reads the Envelope Index directly. Reading the index is the easy half — Mail owns that file and is still writing to it, so every result carries how old the index was and whether the read could see the write-ahead log at all.
drawn to scale — 0.13% of the bar above
A search result carries indexAgeSeconds, and a warning when the index could only be opened immutable — which skips the write-ahead log, so the newest mail is exactly the mail most likely to be missing. A fast wrong answer is the failure mode every indexed reader shares; saying so is the part that is optional.
Without the grant, Notes still works fully below roughly 5k notes; Mail search falls back to the 74-second path. Tools that need the index never disappear — the tool list is a pure function of allowWrites and nothing else, so they return a structured degraded result naming what is absent.
macOS has no body index to borrow — Mail keeps none, and Spotlight excludes ~/Library outright. So the index narrows the query and only the survivors are read, which makes cost linear in how well you filtered. Past the bound it refuses and says so.
log scale · measured against 181,734 messages
degraded: true capability: "body-scan" reason: "6566 messages match the other filters, above the 2000-message body scan bound. Nothing was scanned, so this is not "no results"." hint: "Narrow with mailbox, account, sender or dateFrom and try again — a body search over a few hundred candidates is near-instant."
| Approach | Speed | On disk | Upkeep |
|---|---|---|---|
Ask Mail directly What an AppleScript-only reader is left with. Measured, not estimated. | 74 s for one filtered query | nothing | nothing |
Build a full-text index A second copy of the archive, 365 s to build and stale the moment mail arrives. | fast, including unbounded | 2.2 GB | forever |
Narrow, then read What Cupertino does. The index narrows; only the survivors are read. | 48 ms – 3 s when filtered | nothing | nothing |
Each surface is its own npm package, so a host loads only the tools it wants — and write tools against Mail and against Reminders are not the same risk. They share one bundle and one grant, which is the whole reason they live together.
The deep one. Search, read, threads, attachments and message source across accounts and mailboxes; eight mutating tools behind the write gate.
@mgcrea/mcp-apple-mailSearch, read and attachments across every folder and account, with five mutating tools behind the write gate.
@mgcrea/mcp-apple-notesLists, due dates and search across every account, with five mutating tools behind the write gate.
@mgcrea/mcp-apple-remindersRanges, search and repeating events expanded properly, with three mutating tools behind the write gate.
@mgcrea/mcp-apple-calendarTurns a phone number into a name, and creates or edits a card. Reads need no Automation prompt at all; only the two write tools do.
@mgcrea/mcp-apple-contactsMail without the grant — Accounts, mailboxes and writes only — search falls back to the 74-second path.
Notes without the grant — Fully usable below roughly 5k notes with no grant at all — only attachment bytes need it.
Reminders without the grant — Workable over Apple Events — the file lane is what makes search scale.
Calendar without the grant — Nothing — the only surface with no Apple Events read path fast enough to be a fallback.
Contacts without the grant — Nothing — but it asks for the Contacts permission rather than the whole disk, and unlike Full Disk Access that one prompts.
Messages is not started — no read API exists without the file lane, so it is the one surface that cannot ship without Full Disk Access at all.
One Full Disk Access grant, held by a notarized binary from an identifiable developer, instead of four grants handed out to whichever editors happened to spawn a server. The servers themselves stay MIT and free forever.
The price rises when a surface lands, never because time passed — and once risen it does not come back down. Buying early is the whole of the discount.
Run the servers on their own and pay nothing. Theservers are MIT and always were — they need no key, and they do the work. What a licence buys is the app: the signed and notarized binary that holds one Full Disk Access grant on their behalf, and the work of keeping it working. Its source is public for a reason that has nothing to do with price: nobody should grant Full Disk Access to software they cannot read. Read it, build it, run your own — thelicence is explicit that any build asks for a key, rather than leaving you to find out at the first run.
The trial is held in memory and never written to disk, so quitting Cupertino and opening it again starts another one. Nothing stops you and nothing pretends to. When the window closes it closes properly: the servers it started are stopped, and your assistant reports a dropped connection. A deadline named on the button that started it is not a window if it cannot shut.
Signed with a Developer ID and notarized by Apple. One payment covers every 1.x release, on every Mac you own — or run every surface for 30 minutes first and decide afterwards.
brew install --cask mgcrea/tap/cupertinomacOS 26 or later · 30-day refund · SHA-256 · release notes