Cupertino
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@CupertinoWhat's on my calendar tomorrow?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Cupertino
Put your agent to work in your everyday Apple apps.
MCP servers for the Apple apps already on your Mac, and the signed app that grants them their permissions once instead of once each — for any agent that speaks MCP, not for one host.
Unofficial. Not affiliated with Apple. These drive the apps that are already on your Mac.
In use
Look at how I write in my work inbox, then draft this reply in the same voice.
Pull together everything about the Atlas launch from my mail, my notes and my calendar. What do I still owe people?
Turn the action items from yesterday's client thread into reminders, due Friday.
Every one of those carries a constraint — an account, a date bound, a filter. That is the part the
naive osascript path answers in 74 seconds or answers wrongly, and the reason a server earns its
place: it holds what the model would otherwise re-derive every session. The measurements are in
docs/verify.md; what the alternatives cost is in
docs/alternatives.md.
The last one needs the write gate open on Reminders. Writes are off per surface until you turn them on, and the toggle decides whether the mutating tools are registered at all — an agent with writes off cannot see that they exist.
Related MCP server: apple-mcp
Surfaces
Surface | Package | Status |
implemented — 18 tools, search/read/attachments + gated writes | ||
Notes | implemented — 12 tools, search/read/attachments + gated writes | |
Reminders | implemented — 11 tools, lists/search/dates + gated writes | |
Calendar | implemented — 9 tools, ranges/search/recurrence + gated writes | |
Contacts | implemented — 7 tools, resolves handles to names + gated writes | |
Messages | implemented — 6 tools, chats/search/decoded text + gated send | |
Safari | implemented — 6 tools, history/tabs/reading list, read-only | |
— | shared: the osascript boundary, TCC-aware errors, ro SQLite |
Each surface is its own server and its own npm package, so a host loads only the tools it wants. They share one bundle and one Full Disk Access grant, which is the whole reason they live together — see docs/distribution.md.
Quick start
Each server is on npm and runs straight from npx — for Claude Code, a .mcp.json beside your
project:
{
"mcpServers": {
"apple-mail": {
"command": "npx",
"args": ["-y", "@mgcrea/mcp-apple-mail"]
},
"apple-notes": {
"command": "npx",
"args": ["-y", "@mgcrea/mcp-apple-notes"]
}
}
}The packages are MIT and need no licence key. What they do need is a permission, and on npm you
grant it to whatever launches them — your editor, your terminal — which is the trade the signed
Cupertino.app exists to avoid: one Full Disk Access grant held by a
notarized binary, instead of one per host. See docs/licensing.md.
Or run them from source:
git clone https://github.com/mgcrea/mcp-cupertino.git
cd mcp-cupertino
pnpm install
pnpm buildthen point your host at packages/<surface>/dist/cli.js by absolute path.
Writes are off unless you ask for them — see Configuration.
Running through the menu bar app instead routes every server through the bridge, so Full Disk Access is granted to Cupertino rather than to whichever editor spawned the server:
make run # build Cupertino.app, point it at packages/*/dist, launch it
make smoke # handshake every server through the bridgeThe repo's checked-in .mcp.json is wired for that path. make on its own lists
every target.
Note the different server names. Wired by hand as above, a server is apple-mail and runs under
whatever grant its host process has. Wired by Cupertino it is cupertino-mail, because that entry
points at the app's bridge and runs under the app's grant. Two names for two deployments, and you
can have both. The app only ever touches its own cupertino-* keys — an apple-mail entry
belonging to some other server is left alone.
Cupertino is machine configuration, not a project dependency, so it belongs in a per-user config:
one file each for Claude Desktop, Cursor, LM Studio and Windsurf, and --scope user for the CLIs,
which is what the app's copyable commands use. Deliberately not --scope project, which writes
an .mcp.json meant to be committed — that entry is an absolute path into a bundle on one Mac,
backed by one person's Full Disk Access grant, and it would be useless to a teammate and unwise to
offer them.
Which clients get written and which get a command is not about popularity. The app merges into a
config only when it is strict JSON with servers under mcpServers; Visual Studio Code's is JSONC
and Codex's is TOML, and re-serialising either would delete the comments in a file maintained by
hand. Claude Code's ~/.claude.json is strict JSON, but it holds credentials and running sessions
write to it concurrently, so a read-modify-write from a menu bar could drop somebody else's change.
Those three get a line to paste. ChatGPT is absent entirely: it takes remote HTTP connectors and
cannot spawn a local stdio server at all.
This repo's own .mcp.json is the exception, and it names its servers
cupertino-*-dev on purpose: it points at apps/apple/.build, so working on the app means having
the development build and the installed one side by side. Claude Code reports servers of the same
name in two scopes as a conflict rather than picking one, so the suffix is what keeps both usable.
Cupertino.app needs macOS 26 or later — its icon is an Icon Composer bundle, which nothing
older can render. The servers themselves are plain Node and carry no such floor; only the menu bar
app does.
Permissions
Two separate macOS grants, and they land on whatever process launched the server — your editor, your terminal, or Cupertino — never on Mail, Notes or Reminders themselves.
Grant | Needed for |
Full Disk Access | the index lane: Mail search, attachment bytes |
Automation (per target app, prompted) | the Apple Events lane: accounts, mailboxes, all writes |
Contacts (prompted) | the Contacts surface — its store is not behind Full Disk Access |
System Settings → Privacy & Security → Full Disk Access → add the launching app, then restart it. Granting it to Mail.app does nothing; the reader needs the permission, not Mail.
What works without Full Disk Access:
Surface | Without the grant |
accounts, mailboxes and writes only — search falls back to Apple Events at ~74 s | |
Notes | fully usable below roughly 5k notes; only attachment bytes need the grant |
Reminders | usable, but all-day dates and subtasks need the store — the container cannot even be listed without it |
Calendar | nothing. The only surface with no Apple Events read path fast enough to be a fallback — one 90-day range query costs 3.4 s — so every read needs the grant. Writes still work. |
Contacts | nothing — but it does not want Full Disk Access. Its store sits behind the separate Contacts permission, which macOS prompts for rather than making you find a settings pane. Writes need Automation on top. |
Messages | nothing at all. No Apple Events read path exists — Messages answers "Application isn't running" even while running — so there is no second lane and no degraded mode. A send can still be attempted, but with no store to pick the target from or reconcile against, it usually cannot be addressed at all. |
Safari | live tabs, and only those. The one surface whose lanes are not fallbacks for each other: Apple Events sees what is open now, the file lane sees everything else. History, bookmarks and the Reading List all need the grant. |
Tools that need the index don't disappear when it's missing — the tool list is a pure function of
allowWrites and nothing else, because MCP clients cache it. They return a structured degraded
result naming what's absent. apple_mail_diagnostics / apple_notes_diagnostics report which lane
is live and how to grant what's missing.
Tools
Read tools are always registered. Write tools are invisible unless writes are enabled — not merely refused.
Always available | Write-gated |
|
|
|
|
|
|
Notes
Always available | Write-gated |
|
|
|
Reminders
Always available | Write-gated |
|
|
|
Calendar
Always available | Write-gated |
|
|
|
list_events expands repeating events, so a weekly standup is returned once per week. Every result
carries the window the expansion is known to cover, and sets truncated when a range runs past it
rather than coming back short — a short list of events is indistinguishable from a free afternoon.
Contacts
Always available | Write-gated |
|
|
|
resolve_handles turns phone numbers and email addresses into names, which is what makes the
Messages surface readable at all. Read status on each result rather than assuming a name came
back: unknown is common and not an error, and ambiguous means two contacts share the number, so
no name is returned rather than a guess.
There is no delete tool. Contacts' scripting dictionary has no delete command of any kind, and writes go through Apple Events on every surface here because the store is read-only by policy. See docs/contacts.md.
Messages
Always available | Write-gated |
|
|
|
One write tool, because the dictionary has one usable command. sdef lists send, login and
logout; the other two would sign the user out of iMessage on every device they own. There is no
edit, delete, mark-as-read or reaction verb to expose.
This is the only surface where Apple Events is a write lane and nothing else — every read
through it fails, so with APPLE_MESSAGES_ALLOW_WRITES off no Apple Event is ever sent and no
Automation grant is ever requested.
send_message prefers a chatRef from list_chats over a raw handle, because Messages refuses to
enumerate participants for a script: an existing conversation is the only target that can be
addressed reliably. Apple Events hands back no identifier for what it sent, so the result is
reconciled against chat.db — reconciliation: "matched" carries a real message ref, and
"pending" means Messages accepted the send but has not written the row yet. Pending is not a
failure, and retrying it sends the message twice.
About 3% of messages across all history keep their text only in an archived NSArchiver blob that
SQL cannot reach — and Apple stopped writing the plain column in early 2026, so for anything recent
it is ~100%. This server decodes them; textSource on every result says which lane answered. See
docs/messages.md.
Safari
Always available | Write-gated |
| — none |
|
Read-only, and the write column is empty on purpose. Opening a URL or adding to the Reading List is an Apple Event that navigates a real, visible browser, and no write on this surface was ever probed.
list_tabs is the only tool in the whole bundle that works without Full Disk Access — it needs an
Automation grant instead, and Safari has to be running. A tab's history field being null means
not found in history, never "never visited": only about 55% of open tabs match a history row,
because redirects, session parameters and pages never committed to history all produce a URL that
is simply not there.
There is no do JavaScript tool. That verb needs "Allow JavaScript from Apple Events", a
Safari developer-menu toggle which is not a TCC grant and whose own state cannot be read reliably.
Shipping it would mean reporting a healthy surface whose most powerful capability silently fails.
See docs/safari.md.
All names are prefixed apple_mail_ / apple_notes_ / apple_reminders_ / apple_calendar_ /
apple_contacts_ / apple_messages_ / apple_safari_.
Configuration
Environment only — these servers hold no secret of their own, so there is no config file. Prefix
is APPLE_MAIL_, APPLE_NOTES_, APPLE_REMINDERS_, APPLE_CALENDAR_, APPLE_CONTACTS_,
APPLE_MESSAGES_ or APPLE_SAFARI_.
Variable | Default | What |
|
| register the mutating tools at all |
| all | account allowlist (names or UUIDs) — the read-side control |
|
| the only directory attachments may be written into |
|
| cap on any listing |
|
|
|
|
| per-Apple-Events-call timeout |
|
| verbose logging to stderr |
allowWrites gates mutation, but 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. Mail also takes *_ROOT, *_ENVELOPE_INDEX, *_DEGRADED_MAX_MESSAGES, *_BODY_MAX_BYTES,
*_BODY_SCAN_MAX, *_BODY_SCAN_BYTES and *_MAILBOX_CACHE_TTL_MS; see packages/mail/src/config.ts.
The app
The menu bar is Cupertino's whole surface — there is no Dock icon and no main window.
Section | What it answers |
Full Disk Access | granted or not, with the button that opens the right Settings pane |
One pane per surface | Automation status per app, the consent prompt, and the writes toggle |
Connections | which client is talking to which server right now, and how many tools it has called |
MCP clients | one-click wiring for Claude Desktop, Cursor, LM Studio and Windsurf; a copyable command for Claude Code, VS Code and Codex |
Activity… | opens a window listing every tool call, live |
The Activity window records tool names only — never arguments, message contents or results. 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 spawned them.
Writes are off per surface until you turn them on, and the toggle decides whether the mutating tools are registered at all — an assistant with writes off cannot see that they exist.
Why a single app
Full Disk Access is one indivisible whole-disk grant. Granting it per surface buys no containment and costs a System Settings trip each time, so every surface ships inside one signed, notarized app called Cupertino. docs/distribution.md also records why the Mac App Store cannot host any of this, so the question does not get re-opened.
But the grant is not the only thing the app holds, and it is not the only reason to run the servers under it rather than under an editor:
The grant lands on Cupertino | not on whichever editor spawned the server, and with it every extension and task that editor runs |
A visible audit trail | the Activity window lists every tool call, live; a server inside an editor is unobservable |
Writes are off, per surface | and the toggle decides whether the mutating tools are registered at all |
| the blast radius on Mail is the archive, not the mutations |
Results say how much to trust them |
|
Four surfaces, one grant | which is the actual payoff of the indivisibility above |
docs/alternatives.md is the honest version of that list: what else reads Apple Mail for an assistant, and where those tools are ahead.
Documentation
how this ships, and why not the App Store | |
which surfaces, and what each one costs | |
what is open, what is sold, what buys trust | |
what else reads Apple Mail, and where we lose | |
the body-search lane, and how it is decided | |
Apple Notes phase-0 measurements | |
Apple Reminders phase-0 measurements | |
Apple Messages: measurements, decoder, send | |
Apple Calendar phase-0 measurements | |
Safari phase-0 measurements | |
Mail's observed | |
checking the Mail server against a real index |
Working on it
pnpm build # every package
pnpm test # every package
pnpm typecheck
pnpm lint
pnpm formatThe marketing site is its own workspace, and deploys by hand:
pnpm --filter @mgcrea/cupertino-website dev # astro dev
pnpm --filter @mgcrea/cupertino-website build # static buildIt is built from the design canvas in .idea/design/, and reads its tool counts from
packages/*/src/tools/ rather than from this file — see apps/website.
The Swift half is xcodebuild, named by the Makefile rather than wrapped by it:
make app # build Cupertino.app (Debug)
make run # build, point at packages/*/dist, launch
make smoke # handshake every server through the bridge
make stop # quit and remove the socketThe app's screenshots are captured rather than taken by hand — apps/apple/Screenshots/ holds the
config and the committed goldens, and the website renders the output:
make screenshots # capture, gate against the goldens, compose both sets
make screenshots-check # gate only — fails if the UI drifted
make screenshots-update # accept the captures as the new goldens (review the diffs first)
make screenshots-selftest # prove the gate fails when it shouldA run takes over the pointer and the active app at the moment of each shot, so do not use the
machine while it runs. It needs Screen Recording permission for the terminal, never for
Cupertino itself. make screenshots-doctor checks that and the two other things that otherwise
fail silently: whether the caption font resolves, and whether the output size is one a store would
accept.
What the screenshots show is fixture data from apps/apple/Cupertino/DemoSeed.swift, not this Mac:
in -ScreenshotMode the app starts no host, seeds its own log and sessions, and answers the
permission and store questions from a table. Without that the images would report one laptop's TCC
state and print its home directory into the marketing site.
Phase-0 probes are repo-wide and read-only. They need the permission of the surface they measure, and redact their output to counts, timings and DDL:
pnpm probe:mail # Envelope Index — needs Full Disk Access
pnpm probe:mail-body # which lane can search message bodies — needs Full Disk Access
pnpm probe:notes # Notes — the Apple Events half runs without it
pnpm probe:reminders # Reminders — the store path is a glob, so finding it is itself privileged
pnpm probe:messages # chat.db — no Apple Events read lane exists, so this one needs the grant
# --send-target=<handle> also checks the send lane's targeting, without sending
pnpm probe:calendar # settles whether Calendar has a file lane at all
pnpm probe:safari # History.db, and the Reading List hiding inside Bookmarks.plist
pnpm probe:contacts # the resolver Messages needs — its own TCC grant, not Full Disk AccessEvery probed surface now has a package. Safari is read-only — it registers no mutating tool, and that is a recorded decision rather than an omission: opening a URL or adding to the Reading List navigates a real, visible browser and was never probed. See docs/safari.md. Messages registers exactly one write tool, send_message, which is the whole of what its scripting dictionary can do.
Every probe degrades rather than exits — an app that is not running, or a permission that is not
granted, is reported as a finding — and none of them launches an app unless you pass --launch.
Their shared mechanism lives in scripts/lib/probe-kit.mjs.
Releases are tagged per package, so a tag names what it publishes: mail-v1.1.0,
reminders-v1.1.0, calendar-v1.1.0, core-v1.1.0. The app is tagged app-v1.1.0 and releases on its own lane —
a signed, notarized Cupertino.zip attached to the GitHub release, plus its SHA-256. See
docs/distribution.md.
The repo is
mcp-cupertino; the npm packages stay@mgcrea/mcp-apple-*, because that is what people search npm for. Neither name is load-bearing. The bundle identifierio.mgcrea.cupertinois the string that actually matters, because changing it would invalidate every user's Full Disk Access grant.
Licence
Two, because the halves are not the same thing.
Part | Licence |
| MIT — libraries, vendor them freely |
| source-available — read, audit, compile; binary redistribution reserved |
the signed, notarized build | sold, under the EULA shipped with it |
Cupertino asks for Full Disk Access, so the source stays readable — that is what such a grant is owed, and reading it is the point. Running it is a separate question: the licence check lives in the source, so any build asks for a key, yours or ours. What is sold is the notarized binary and the maintenance behind it. The servers are MIT and run on their own with no key at all. The reasoning is in docs/licensing.md.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceA local MCP server that provides a secure bridge for automating macOS applications like Notes, Calendar, and Mail through AppleScript. It uses template-based execution and policy-based allowlists to enable safe, structured interaction with system tools.MIT
- AlicenseBqualityDmaintenanceMCP server for macOS Apple apps. Enables read/write access to Notes, Reminders, Calendar, Contacts, and Safari using SQLite and JXA, all running locally.428122MIT
- AlicenseAqualityCmaintenanceMCP server for privacy-gated local Apple data access including Mail, Messages, Notes, Calendar, Contacts, Photos, Reminders, Voice Memos, and iCloud Drive.301MIT
- AlicenseNot gradedqualityAmaintenanceA collection of MCP servers for Apple macOS apps (Mail, Contacts, Notes, Memory, Messages, Calendar, Reminders) enabling AI assistants to read, search, create, and update data via JXA, SQLite, and EventKit.12MIT
Related MCP Connectors
Search, read, and write your Apple Notes from ChatGPT/Claude via a local Mac agent + MCP relay.
Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.
MCP connector for iMessage & Contacts via a local Mac agent + Vercel relay
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/mgcrea/mcp-cupertino'
If you have feedback or need assistance with the MCP directory API, please join our Discord server