Self-hosted MCP server that connects Claude to Apple iCloud: Mail, Calendar, Contacts, Reminders, Notes and iCloud Drive. Single-owner OAuth, approval-first, built for prompt-injection safety.
Overview
Ask your AI about your week, your inbox or the file you saved last spring, and it just knows. iCloud MCP connects Claude, Codex, ChatGPT or any other MCP client to the Apple account you already live in. It runs on your own machine, keeps your password there, and asks before anything leaves. "Find the email from my landlord about the heating and draft a polite reply." "When am I free for an hour next week? Book lunch with Anna then." "Move Thursday's dentist appointment to Friday, same time. Only that one." "Remind me to renew my passport on the first of next month." "Find my tax return PDF in iCloud Drive and tell me what I paid last year." "How long does it take me to cycle to the station, if I need to be there at nine?" "Catch me up on my messages from this weekend."
README
Ask your AI about your week, your inbox or the file you saved last spring, and it just knows. iCloud MCP connects Claude, Codex, ChatGPT or any other MCP client to the Apple account you already live in. It runs on your own machine, keeps your password there, and asks before anything leaves.
In 0.9 to 0.11: Maps, Messages, the menu bar app and pause, drafts and folders, calendars and contact groups, and richer Reminders. In 0.6 to 0.8: about three times faster calendars, lighter mail, an instant first call, your own rules file, replies owed, clash checks and a hardened recurrence guard. Since 0.4: bulk clean-up with undo, safe unsubscribe, exact bookings, note editing, search inside Drive files, birthdays and Shortcuts. In 0.3: the one-click Claude Desktop extension, local mode, Keychain storage, free time, RSVP and scam warnings.
Mail, Calendar and Contacts use Apple’s standard protocols.Reminders, Notes, iCloud Drive, Maps, Messages and Health go through an optional helper on your Mac.It connects out, so your Mac never opens a port.
| Any client on this computer | Claude Desktop, one click | Your own server | |
|---|---|---|---|
| Setup | One command or a few lines of config | Double-click an extension | Docker and an HTTPS address |
| Works in | Claude Code, Codex, Claude Desktop, Cursor, VS Code and any other MCP client on this computer | Claude Desktop on this computer | Claude and ChatGPT on the web, desktop and phone, Codex, and any client that supports remote MCP servers |
| Outgoing mail | Saved to Drafts for you to send | Saved to Drafts for you to send | Waits for your approval in a browser |
| Guide | Set up › | Install › | Quick start › |
Tested with Claude (web, desktop, phone, Claude Code and the Desktop extension) and with Codex CLI running the server locally. Any client that speaks MCP works the same way: locally it starts the server itself, and hosted it signs in with OAuth.
Run it locally
The server runs on your own computer. Your client starts it when it needs it and talks to it directly, so there is no public address, tunnel, Docker or OAuth. Apps on the web and on your phone can’t reach it; for that, host it.
Claude Desktop in one click
- Download
icloud-mcp-.mcpbfrom the latest release. - Double-click it, or drag it onto Claude Desktop → Settings → Extensions.
- Fill in your Apple Account email, an app-specific password (Claude Desktop keeps it in your system keychain), your name and your time zone, such as
Europe/Amsterdam.
It starts approval-first: mail is saved to Drafts for you to send, and invitations to other people are off. This covers Mail, Calendar and Contacts. Reminders, Notes and iCloud Drive need the Mac helper and the manual setup below.
Manual setup (any MCP client)
1. Put your settings in a file only you can read.
mkdir -p ~/.icloud-mcp && chmod 700 ~/.icloud-mcp
cat > ~/.icloud-mcp/icloud.env <<'END'
[email protected]
ICLOUD_DISPLAY_NAME=Your Name
DEFAULT_TIMEZONE=Europe/Berlin
END
chmod 600 ~/.icloud-mcp/icloud.env
2. Keep the password in your Keychain (on a Mac). It asks for the app-specific password and never shows it on the command line:
uvx icloud-mcp-server --store-password
Not on a Mac? Add ICLOUD_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx to the file instead. Any setting from Configuration can go in it; MCP_PUBLIC_URL and MCP_OWNER_PASSWORD are not needed.
3. Add it to your client (needs uv).
Claude Code:
claude mcp add icloud -- uvx icloud-mcp-server --local --env-file ~/.icloud-mcp/icloud.env
Codex:
codex mcp add icloud -- uvx icloud-mcp-server --local --env-file ~/.icloud-mcp/icloud.env
Claude Desktop: Settings → Developer → Edit Config, add this to claude_desktop_config.json, and restart Claude. Cursor (~/.cursor/mcp.json), VS Code, Windsurf, Gemini CLI and most other clients take the same command and args in their own MCP settings:
{
"mcpServers": {
"icloud": {
"command": "uvx",
"args": ["icloud-mcp-server", "--local", "--env-file", "/Users/YOU/.icloud-mcp/icloud.env"]
}
}
}
If your client can’t find uvx, use its full path (which uvx). Without uv, pip install icloud-mcp-server and use icloud-mcp as the command.
[!TIP] Fewer tools, better choices. Clients pick the right tool more reliably from a short list. Add
TOOLS=essentialfor a core of 19: search, read and reply to mail; list events, find free time and create or update events; find contacts; the main Reminders, Notes and Drive tools; and the health check. Add exact names to it as needed, for exampleTOOLS=essential,mail_move_messages, or pick whole areas:TOOLS=mail,TOOLS=mail,calendar(alsocontacts,reminders,notes,drive,maps,imessage,health; the health check always stays).
How sending works locally. There is no approval page, so with the default SEND_REQUIRES_APPROVAL=true every message your AI sends is saved to your Drafts folder instead, and the result says so. You review it in Mail and press Send yourself. Set SEND_REQUIRES_APPROVAL=false to let it send directly; your client’s own approval prompt is then the only check.
Reminders, Notes and iCloud Drive locally. Add ENABLE_REMINDERS=true (and/or ENABLE_NOTES, ENABLE_DRIVE) and a BRIDGE_TOKEN to the env file, start your client once, then install the Mac helper with server https://127.0.0.1:8001 and the fingerprint from ~/.icloud-mcp/bridge_fingerprint.txt. In local mode the bridge only listens on 127.0.0.1. If two clients start the server at once, only the first gets the Mac tools; Mail, Calendar and Contacts work in both.
Quick start
Host it once, and your AI reaches your iCloud from the web, desktop apps and your phone: Claude, ChatGPT, Codex, or any client that supports remote MCP servers.
You need
- Docker with the compose plugin (or Python 3.11+).
- An Apple Account with two-factor authentication and an app-specific password (account.apple.com → Sign-In and Security → App-Specific Passwords).
- A public HTTPS address. Web and phone apps connect from their provider’s cloud, so a LAN or VPN address won’t work. A Cloudflare Tunnel is the simplest: outbound only, no port forwarding.
- A client that can add a remote MCP server with OAuth: Claude (custom connectors), ChatGPT (developer mode) or Codex, for example.
[!WARNING] Don’t put Cloudflare Access or any other login wall in front of the server. Your client’s servers can’t pass an interactive login, and the server has its own OAuth.
1. Configure
git clone https://github.com/epinethrone/icloud-mcp.git && cd icloud-mcp
cp .env.example .env # fill in ICLOUD_USERNAME, ICLOUD_APP_PASSWORD, ICLOUD_DISPLAY_NAME,
# MCP_PUBLIC_URL (your https address, no trailing slash), MCP_OWNER_PASSWORD
chmod 600 .env
MCP_OWNER_PASSWORD is a new random password (12+ characters) that you type when approving a client and on the outbox page. It is not your Apple password. ./configure.sh asks for the secrets with silent prompts if you prefer.
2. Check your credentials against the real iCloud before exposing anything
docker build -t icloud-mcp:local .
docker run --rm --env-file .env icloud-mcp:local python -m icloud_mcp.selftest # logs in to IMAP, SMTP, CalDAV, CardDAV; sends nothing
docker run --rm --env-file .env icloud-mcp:local python -m icloud_mcp.selftest --probe-sent [email protected] # sends ONE test mail
If a login fails, the login name is the usual cause: set IMAP_USERNAME, SMTP_USERNAME, CALDAV_USERNAME or CARDDAV_USERNAME separately.
3. Run it
docker compose up -d --build # listens on 127.0.0.1:8000 only
For the bundled Cloudflare Tunnel: create a tunnel, point its public hostname (the host in MCP_PUBLIC_URL) at http://icloud-mcp:8000, put the token in .tunnel.env as TUNNEL_TOKEN=..., and start with docker compose --profile tunnel up -d --build. Any TLS front (Caddy, nginx) works too, as long as it keeps the Host header. MCP_PUBLIC_URL must match the public address exactly.
4. Connect your client
Add https:///mcp as a remote MCP server. Your server then shows an approval page: enter the owner password.
- Claude: Settings → Connectors → Add custom connector.
- ChatGPT: Settings → Apps & Connectors → Advanced → turn on developer mode, then create a connector with OAuth (needs a plan with developer mode).
- Codex:
codex mcp add icloud --url https:///mcp, thencodex mcp login icloud. - Others: any client that supports remote (streamable HTTP) MCP servers with OAuth. A web client whose sign-in returns to another host needs that host in
OAUTH_ALLOWED_REDIRECT_HOSTS.
Reconnect whenever you change tools or settings, because clients cache tool definitions. The menu bar app signs out one connected client or all of them; without it, delete oauth_state.json in the data volume and restart to revoke every client.
5. Check it works
In a new chat, ask “Check my iCloud connection.” Your AI runs icloud_check_health, which signs in to each service and reports how long each took. Then try “What’s on my calendar this week?” and ask it to email you. With the default settings nothing is sent: the message waits at https:///dashboard/approvals until you approve it. If anything fails, see Troubleshooting.
Approving outgoing mail
With the default SEND_REQUIRES_APPROVAL=true, mail_send_message, mail_reply_to_message and mail_forward_message return queued_for_owner_approval and nothing leaves. Open https:///dashboard/approvals (bookmark it, and only sign in there, never on a link an agent gives you), review the exact recipients and text, then approve or discard. Queued messages expire after OUTBOX_TTL_SECONDS (24 hours by default) and are released at most once. The older https:///outbox page still works and asks for the owner password on every visit.
The dashboard
https:///dashboard is where you look after the server: what is waiting for your approval, what agents changed, which apps are connected, and whether mail, calendar and contacts answer. It works on a phone (tab bar) and on a computer (sidebar), in light and dark mode.
- Sign in with a passkey. Sign in once with the owner password, then add a passkey under Security: Face ID, Touch ID or a security key from then on. A passkey only works on your server’s own address, so a look-alike page cannot use it. Once you have one,
OWNER_PASSWORD_LOGIN=falseturns password sign-in off. Passkeys need the optionalwebauthnpackage (pip install "icloud-mcp-server[passkey]"); the Docker image has it. - Activity. Every write an agent makes (moving mail, changing an event, editing a contact), every approval and discard, every sign-in and failed attempt, and every app you connect or sign out is recorded in
DATA_DIR/audit.jsonl, with names, addresses and subjects but never message bodies, passwords or tokens. Each entry carries the hash of the one before it, and the Activity page says whether the file was changed afterwards. Entries are kept forAUDIT_KEEP_DAYS(90). Turn it off withAUDIT_LOG=false. - Calendar invitations can wait for you too. With
ALLOW_CALENDAR_INVITES=approve, a calendar change that makes iCloud email other people (an event with guests created, changed or cancelled, or your answer to an invitation) is held on the Approvals page instead of written. Approving writes exactly what you saw, and only if the event has not changed since; otherwise nothing is written and the page says so. Moving an event to another calendar sends no email and is not held. - Settings. Everything the server does can be changed under Settings: areas and tools, how mail is sent, invitations, contacts, the Mac helper, read-only mode and the dashboard itself. A value that comes from the environment says so at the field, and values set there are listed with a Reset button. Changes are stored in
DATA_DIR/settings.json, win over the environment and apply after a restart (one button). The server checks that it could start with the new values before it saves them, and a change that weakens protection asks you to sign in again. Where the server listens, how apps sign in and what it connects to stay in the environment and are only shown.DASHBOARD_SETTINGS=viewshows settings without changing them;offignoressettings.json. - Live, and installable. An open Overview or Approvals page shows new requests within 15 seconds without a reload, and Send and Discard work in place: you can open a request on the Overview and approve it in its row, and a discard can be undone for five seconds. The page checking for news does not count as use, so an open tab still signs out after
DASHBOARD_SESSION_MINUTES. In Chrome and Edge, or with Add to Home Screen in Safari, the dashboard installs as an app. Its service worker caches nothing, so no mail ends up stored in the browser. - Sessions. A sign-in lasts until
DASHBOARD_SESSION_MINUTES(30) pass without use, and 12 hours at most. Removing a passkey, changing the password and signing out apps ask you to confirm when your sign-in is older than five minutes. - Sign-in noise stays readable, and your passkey sign-in stays. Failed sign-ins that the lock holds back are written as one summary line when the ten-minute window ends, even if nothing follows, so the activity log shows every burst without flooding. Sign-in tickets are signed with a per-process key and never stored, and a ticket counts as used only after a sign-in with the passkey succeeds, so a flood of challenges cannot fill anything or push the owner’s passkey sign-in aside. A server restart voids open sign-in tickets; the owner simply signs in again.
Security
A connector that can read your mail and act for you is a prompt-injection target: a hostile email or invitation can contain text that tries to steer the agent. The server labels all such content as untrusted and tells agents to treat it as data, but that is a request to a language model, not a guarantee. What actually protects you is configuration:
| Risk | Default | Setting |
|---|---|---|
| Agent sends mail on injected instructions | Sending only queues the message for your approval on /dashboard/approvals (/outbox with DASHBOARD=false; locally: saves it to Drafts) |
SEND_REQUIRES_APPROVAL=true |
| Agent emails invitations to strangers | Attendee changes are blocked | ALLOW_CALENDAR_INVITES=false |
| Agent invites the wrong people once invites are on | Any address, at most 10 guests | INVITE_ALLOWLIST, MAX_ATTENDEES |
| Agent puts a stranger’s address on a real contact so a later reply goes there | Allowed, but the address is marked as agent-added in results and on the approval page | CONTACTS_ALLOW_EMAIL_CHANGES=false to block it |
| Agent reads years of archive for a task about today | Whole mailbox | MAIL_MAX_AGE_DAYS |
| Hostile text is not spotted | Built-in patterns (English and Dutch) plus removal of text hidden in HTML mail (warned about when it reads like instructions; show_hidden shows it) |
SAFETY_SCREEN=command: adds your own classifier |
| Agent mails arbitrary addresses | Any address, at most 25 per message | SEND_ALLOWLIST, MAX_RECIPIENTS |
| Agent destroys mail | Delete moves mail to Trash; deleting from Trash is off | ALLOW_PERMANENT_DELETE=false |
| Agent destroys notes or files | Notes go to Recently Deleted, Drive files to the Trash. Reminders have no trash, so a deleted reminder is gone (it is one line, easily recreated); moving one between lists never deletes it | always |
| Agent texts people on injected instructions | iMessage sending is off; when on, every message waits for your approval, only people on an allowlist (empty = nobody) can receive one, and a never-send list blocks handles outright | IMESSAGE_ALLOW_SEND=false, IMESSAGE_SEND_ALLOWLIST, IMESSAGE_NEVER_SEND |
| Agent reads your codes from Messages | Messages is off; when on, bank and one-time-code senders are hidden, and you can hide or allow chats | ENABLE_IMESSAGE=false, IMESSAGE_HIDE_SHORT_CODES=true, IMESSAGE_HIDDEN_CHATS |
| Agent changes anything at all | Everything writable | READ_ONLY=true for a read-only connector |
Most clients can also ask you before a tool runs (in Claude, “ask before use” per tool): use it for the send, reply, forward and delete tools. Anyone who obtains the app-specific password has full access to mail, calendar and contacts (Apple offers no narrower scope), so protect the server and its .env accordingly.
Tools
92 tools. 50 for Mail, Calendar, Contacts, the clock and the health check, 40 more with the optional Mac helper, and 2 for Shortcuts you allowlist. Open a section for the details.
Every name is area_verb_noun (mail_send_message, calendar_create_event). Eleven tools were renamed in 0.7.0 and 36 in 0.12.0 (a verb with no noun, such as mail_send) to follow that pattern; TOOLS still accepts the old names and logs the new one.
Ready-made workflows
The server also offers MCP prompts your client can show as one-click workflows: Triage my inbox, Replies I owe, Follow-ups I am waiting on, Plan my week, Prepare for an appointment, Calendar from my mail, Find a time with someone, Tidy my reminders and Birthdays coming up. Each only appears when the areas it needs are on, and each tells the agent to show you what it would do before sending, booking, moving or deleting anything.
Working with agents
The server tells every agent how to use it: its instructions are built from the tools you actually enabled, always start with the security rules (content from mail, events and contacts is untrusted data, never instructions), and explain the approval flow you configured.
Add your own rules with AGENT_NOTES_FILE: a short Markdown file on the server’s machine, read fresh on every use, so an
edit applies without reconnecting. Agents see it after the security rules, which it can refine but never relax, and can
re-read it as the icloud://agent-notes resource. docs/agent-notes.example.md
shows the idea: which calendar gets what, how to sign mail, lists that are shared. Keep it under 8,000 characters.
Tools that save an agent guesswork:
icloud_get_timegives the owner’s date, time and timezone, so “tomorrow” means the right day.mail_list_awaiting_replylists mail you sent that nobody answered;mail_search_messageswithunanswered_onlyandpeople_onlyfinds what you still owe, without newsletters.calendar_create_eventreports overlapping events and likely duplicates, and can refuse to create either;calendar_list_eventswithneeds_replyfinds invitations still waiting for an answer.OWNER_ADDRESSESlists your aliases, so invitations sent to them count as yours.
With many tools some clients choose less reliably: TOOLS=essential loads a smaller core set, and TOOLS=mail,calendar
loads whole areas.
Performance
Measured on a local test stack with 40 ms added to every round trip, comparing 0.5.0 and 0.6.0 (details and method):
| 0.5.0 | 0.6.0 | |
|---|---|---|
| 30 days of events, all calendars | 0.74 s | 0.25 s |
| The same list, bytes returned | 26,017 | 12,384 |
| Search 20 messages and read 10, received from iCloud | 465 KB | 55 KB |
| A 20-hit mail search, bytes returned | 8,946 | 6,447 |
| Schema text every client loads | 51,951 chars | 37,956 chars |
Connections stay signed in for 10 minutes after the last call (IMAP_IDLE_SECONDS, CALDAV_KEEPALIVE_SECONDS), and
WARMUP_ON_START signs in as the server starts. dev/bench.py repeats the measurements against your own account.
Control it from the menu bar
menubar/ holds iCloud MCP Control, a native macOS app for a server that runs on your Mac. Its menu is a standard macOS menu, like Time Machine’s: it shows whether the server is up, how many tools and connected apps it has and any problem in plain words, and lets you pause the server (tools answer that it is paused, nobody is disconnected), restart or stop it, the Mac helper and a tunnel. Its Settings change the owner passcode, replace the iCloud app-specific password (tested with iCloud before it is saved) and list every connected app with when it was last used, so you can sign out one, a group or all of them. Turn on its admin API with ADMIN_PORT: it listens on 127.0.0.1 only, on its own port, and every request needs the token the server writes to admin-token in its data folder.
Reminders, Notes, iCloud Drive, Maps, Messages and Health through your Mac
Apple only exposes Reminders, Notes, iCloud Drive, Apple Maps, your Messages history and Health data on its own devices, so a small helper (mac-helper/) runs on your Mac and does the work when the server asks.
- Nothing listens on your Mac. The helper connects out to a private HTTPS port of the server (never the public address, never the tunnel) and long-polls for jobs.
- No code is ever sent. The server sends an operation name and validated arguments from a fixed list. Reminders run a small EventKit program the installer builds on your Mac, and Maps a small MapKit program. Notes run static scripts. Messages are read from your own Messages database, read-only. iCloud Drive and Health each run one fixed Python script under Apple’s own Python. In every case the arguments arrive as one JSON value, never as code.
- Pinned and authenticated. TLS with a self-signed certificate the helper pins by fingerprint, plus a bearer token.
- Honest when it’s off. It works while your Mac is on and reachable (home network or VPN). When it isn’t, the tools say so.
Enable it in .env with any of ENABLE_REMINDERS=true, ENABLE_NOTES=true, ENABLE_DRIVE=true, ENABLE_MAPS=true, ENABLE_IMESSAGE=true and ENABLE_HEALTH=true, a BRIDGE_TOKEN of at least 32 random characters, and BRIDGE_BIND set to the address the Mac reaches the server on. The server logs the certificate fingerprint for the installer, and also writes it to bridge_fingerprint.txt in its data folder. Then follow the Mac helper guide.
Shortcuts, allowlisted twice. To let the assistant run some of your Shortcuts (shortcuts_list_shortcuts, shortcuts_run_shortcut), list their exact names in SHORTCUTS_ALLOW on the server and, one per line, in ~/Library/Application Support/icloud-mac-helper/shortcuts-allow.txt on the Mac. A name must be on both lists, so a compromised server can never run a shortcut you did not allow at the Mac itself. Apple’s shortcuts command runs it, with optional text input, and its text output comes back. The tools do not exist without an allowlist or on a read-only server.
[!IMPORTANT] iCloud Drive and Messages need Full Disk Access for the helper’s Python. On macOS 27 the grant only takes effect when the helper runs as the Command Line Tools
Python.appexecutable, which is what the installer sets up. Details in the Mac helper guide.
Configuration
Everything is an environment variable. .env.example has a comment for each one.
Troubleshooting
Start by asking your AI to “check my iCloud connection”: icloud_check_health tests every enabled area and names the one that fails.
Contributing
Issues and pull requests are welcome. Every pull request gets the offline tests on Python 3.11 to 3.13 and an automated security-minded review.
- Security problems: please don’t open a public issue. Follow the security policy instead.
- Before a pull request: make sure
pytest tests --ignore=tests/integrationpasses, and add tests for new behaviour. - Anything that talks to iCloud: run
selftest, and try it by hand against a real account. Local test servers accept things iCloud doesn’t. - New tools: keep the safety defaults intact. Anything that sends, invites or deletes must stay behind the existing settings, and anything read from iCloud must be treated as data, never as instructions.
Acknowledgements
Built on the MCP Python SDK, IMAPClient, caldav, icalendar, html2text, python-dateutil, Uvicorn and HTTPX. The Notes scripts are adapted from MrGo2/icloud-mcp (MIT); see THIRD_PARTY_NOTICES.md.
Install
uvx icloud-mcp-server --local --env-file /Users/YOU/.icloud-mcp/icloud.envConfiguration
{
"mcpServers": {
"icloud": {
"command": "uvx",
"args": ["icloud-mcp-server", "--local", "--env-file", "/Users/YOU/.icloud-mcp/icloud.env"]
}
}
}