apple-calendar-mcp
A Model Context Protocol server that lets Claude read and manage your Apple Calendar: list your schedule, find free time, and create, move or delete events, including recurring ones.
It talks to macOS through Apple's EventKit framework. That is the same database Calendar.app uses, so every account synced on your Mac (iCloud, Google, Exchange, your university's Outlook) shows up through one interface. Nothing leaves your machine except what Claude reads in the conversation.
It can also run as a remote connector for claude.ai on the web and on your phone: your own small server that talks to iCloud over CalDAV and signs Claude in with OAuth. See Remote connector.
You: Find me two free hours this week for the CS350 project, then block them.
Claude: ▸ get_time_context ▸ find_free_slots ▸ create_event
Booked "CS350 project" Thu 13:00–15:00 in Work. Wednesday 09:00–11:00 was also free.
Features
| Tool | What it does | Kind |
|---|---|---|
get_time_context |
Current local date, time and time zone, so "next Friday" resolves correctly | read |
list_calendars |
Calendars with account, colour and whether they're writable | read |
list_events |
Events in a range, with recurring events expanded into occurrences | read |
search_events |
Case-insensitive search across title, location and notes; all terms must match | read |
get_event |
Full details, including attendees and repeat rule | read |
find_free_slots |
Gaps of at least N minutes within working hours, skipping weekends if you want | read |
create_event |
Timed or all-day events, with alarms, URL and repeat rules | write |
update_event |
Partial edits. Recurring events take span: this, future or all |
write |
delete_event |
Delete one occurrence, this and later ones, or the whole series | destructive |
Other parts of the design:
- Tool annotations. Each tool is marked read-only or destructive, so clients can ask you before they change anything.
--read-onlymode. Runs the server without the write tools at all.- Prompt-injection hygiene. The server tells the model that event text is untrusted data. Anyone who sends you an invite controls that text.
- Typed output. Every tool returns JSON with a published output schema, generated from pydantic models.
How it works
flowchart LR
C[Claude Desktop / Claude Code] -- "JSON-RPC over stdio" --> S
subgraph S [apple-calendar-mcp]
direction TB
T["server.py<br/>MCP tools + schemas"] --> V["service.py<br/>validation, defaults,<br/>search, free slots"]
V --> P{{"CalendarStore<br/>(Protocol)"}}
P --> EK["eventkit_store.py<br/>PyObjC → EventKit"]
P --> CD["caldav_store.py<br/>CalDAV → iCloud"]
P --> M["memory_store.py<br/>in-memory (tests, demo)"]
end
EK --> DB[(macOS Calendar database<br/>iCloud · Google · Exchange)]
W[claude.ai web / mobile] -- "HTTPS + OAuth<br/>remote.py" --> S
CD --> IC[(iCloud CalDAV)]
server.pydeclares the tools. The function signatures and pydanticFielddescriptions become the JSON Schema Claude sees. The file contains no logic.service.pyturns loose model input into precise requests. It parses ISO 8601, fills defaults (a 1-hour duration, all-day ranges, search windows), keeps the duration when only the start time moves, and builds search and free-slot finding on top of plain CRUD.CalendarStoreis a six-method protocol with three backends. EventKit is the local one. CalDAV powers the remote connector. The in-memory backend reproduces the recurrence semantics both of them follow, so the service and MCP layers are tested on Linux CI.remote.pyserves the same tools over streamable HTTP, behind a single-owner OAuth authorization server (dynamic client registration, PKCE, rotating refresh tokens, hashed storage).scheduling.pyfinds free slots with interval arithmetic: sort, merge the busy intervals, then sweep each day with two pointers. That is O(n log n + d) for n events over d days.
Design decisions worth knowing
- Occurrence ids. EventKit gives every occurrence of a recurring event the same
eventIdentifier. Ids here are"<eventIdentifier>|<original start>", built from EventKit'soccurrenceDate. Each id therefore points at exactly one occurrence, even after that occurrence has been moved. span = all. EventKit only has this and future spans. All is implemented as future applied to the first occurrence. The target occurrence's time change is converted into the same shift for the whole series.- Exclusive ends. For an all-day event on 3 Nov,
endis2026-11-04T00:00, the same convention as iCalendarDTEND. EventKit internally reports23:59:59on the last day, so the backend normalises in both directions. On input, an all-dayendis the inclusive last date, because that is how people say it ("5th to 7th"). - Concurrency. The MCP SDK runs sync tools on worker threads. All EventKit calls go through one lock, and each call runs inside an Objective-C autorelease pool.
Requirements
- macOS 13 or later (macOS 14+ uses the newer full-access permission API)
- Python 3.11+ and uv
- Claude Desktop or Claude Code
Claude plugin
The quickest way to use it in Claude Code (and in Cowork sessions that run on your Mac) is the plugin. It adds the server and a short skill that teaches Claude the workflow and the one-time permission setup. Install it in Claude Code:
/plugin install apple-calendar-mcp --marketplace oguz-kagan-gocmez-edu/apple-calendar-mcp
On Claude Code older than 2.1.275, add the marketplace first:
/plugin marketplace add oguz-kagan-gocmez-edu/apple-calendar-mcp
/plugin install apple-calendar-mcp@apple-calendar-mcp
You need uv on your PATH. The first start takes a little longer while uv installs the dependencies.
Then do the permission setup once (it needs the Xcode Command Line Tools, xcode-select --install). Ask Claude to "set up calendar access" and it runs install-app and doctor for you, or run them yourself from a clone as in Setup. If you set up the helper app before, the plugin reuses it. See macOS permissions for what the helper app is.
Plain chats on claude.ai, the desktop app and phones don't run local servers. For those, use the remote connector.
What the plugin runs and sends
- What it starts. Claude Code runs
uv run --locked --no-dev --project <plugin folder> apple-calendar-mcp. On the first start uv installs the exact dependency versions fromuv.lock(mcp,pydantic,pyobjc-framework-EventKitand what they need) from PyPI into a.venvinside the plugin folder, and downloads Python 3.11+ if none is installed. After a plugin update this happens once more. - What it reads and writes. The server only talks to macOS EventKit, the calendar database on your Mac. It makes no network requests of its own, keeps no copy of your events and writes no logs to disk. Events leave your Mac only as tool results inside your Claude conversation.
- Changes to your calendar. Only through
create_event,update_eventanddelete_event. They are marked as write and destructive tools, so Claude asks before running them. Start the server with--read-onlyto remove them. - The helper app.
install-appruns only when you (or Claude, with your go-ahead) run it. It compiles a ~70-line C launcher withclangand writes the ad-hoc-signed~/Applications/Apple Calendar MCP.app. Delete that app to remove it. - No accounts or credentials. The plugin asks for no passwords or tokens. Your calendar accounts stay configured in macOS.
This is an unofficial project and isn't affiliated with Apple.
Setup
git clone https://github.com/oguz-kagan-gocmez-edu/apple-calendar-mcp.git
cd apple-calendar-mcp
uv sync
uv run apple-calendar-mcp install-app # builds the helper app that holds calendar permission
uv run apple-calendar-mcp doctor # asks for permission, lists your calendars
doctor triggers a macOS prompt for "Apple Calendar MCP". Choose Allow Full Access. You only do this once: Claude Desktop, Claude Code and Terminal all reuse that permission. install-app needs the Xcode Command Line Tools (xcode-select --install).
Without cloning (PyPI)
The server is also on PyPI as apple-calendar-eventkit-mcp, so uvx can run it without a clone:
uvx apple-calendar-eventkit-mcp install-app
uvx apple-calendar-eventkit-mcp doctor
claude mcp add apple-calendar -s user -- uvx apple-calendar-eventkit-mcp
In Claude Desktop, use "command": "/opt/homebrew/bin/uvx" and "args": ["apple-calendar-eventkit-mcp"] (run which uvx for your path). The PyPI name differs from the repository's because apple-calendar-mcp was already taken there; the command inside stays apple-calendar-mcp.
Claude Desktop
Open Settings → Developer → Edit Config and add the server (template: examples/claude_desktop_config.json):
{
"mcpServers": {
"apple-calendar": {
"command": "/opt/homebrew/bin/uv",
"args": ["--directory", "/Users/YOU/Projects/apple-calendar-mcp", "run", "apple-calendar-mcp"]
}
}
}
Use absolute paths. Run which uv to find yours. Quit Claude Desktop completely (⌘Q) and reopen it.
Claude Code
claude mcp add apple-calendar -s user -- uv --directory /Users/YOU/Projects/apple-calendar-mcp run apple-calendar-mcp
Options
apple-calendar-mcp [--read-only] [--timezone Europe/Istanbul] [--log-level INFO]
apple-calendar-mcp --backend memory --demo # fake data, no Mac needed
apple-calendar-mcp doctor
apple-calendar-mcp install-app [--dest PATH]
apple-calendar-mcp --backend caldav doctor # iCloud over CalDAV (CALDAV_* env vars)
apple-calendar-mcp serve-http [--host H] [--port P] # remote connector, see below
macOS permissions (read this if nothing works)
macOS protects calendars with TCC (Transparency, Consent and Control). It asks on behalf of the responsible app: normally the GUI app that started the process. Since macOS 14 that app must declare NSCalendarsFullAccessUsageDescription in its Info.plist, or the request is refused without any prompt. Terminal and Claude don't declare it, so running the server directly fails with "macOS refused calendar access without showing a prompt".
install-app fixes this. It builds ~/Applications/Apple Calendar MCP.app, a tiny ad-hoc-signed bundle with no Dock icon. The bundle declares calendar access and contains a ~70-line C launcher (launcher.c). When the bundle is installed, apple-calendar-mcp re-runs itself through the launcher. The launcher makes the bundle responsible for its own permissions, so macOS asks for "Apple Calendar MCP" whichever app started the server. Your Claude config doesn't change.
- Choose Allow Full Access. "Add Events Only" is not enough to read your events.
- To change your answer later: System Settings → Privacy & Security → Calendars → Apple Calendar MCP.
doctorshows whether the helper app is in use. SetAPPLE_CALENDAR_MCP_NO_APP=1to bypass it.- Rebuilding the app (for example after an upgrade) may ask for permission once more.
- Without a GUI session (SSH, launchd), the prompt can never appear. The server times out after 30 s with a clear error instead of hanging.
Remote connector (claude.ai web and mobile)
The local server only works where it runs: your Mac. To use your calendar from claude.ai in a browser or the Claude phone app, run your own copy of the remote connector. It talks to iCloud over CalDAV and is added to Claude as a custom connector. The easiest home for it is Cloudflare's free Workers plan: no server, no card, always on.
flowchart LR
A[Claude on web / phone] -- "HTTPS + OAuth" --> B[your connector<br/>Cloudflare Worker or your own server]
B -- "CalDAV + app-specific password" --> C[(iCloud Calendar)]
Each person runs their own copy with their own credentials. Nobody else holds your Apple ID details.
1. Create an app-specific password
At appleid.apple.com go to Sign-In and Security → App-Specific Passwords and create one called "Claude calendar". You can revoke it there at any time.
2. Deploy it
Free, on Cloudflare Workers (recommended)
The worker/ folder is the same connector written for Cloudflare Workers: same tools, same sign-in page. The free plan is plenty for one person (100,000 requests a day) and needs no card. You don't need a Mac for this, only Node.js 22 or newer and git.
- Get the code:
git clone https://github.com/oguz-kagan-gocmez-edu/apple-calendar-mcp.git cd apple-calendar-mcp/worker && npm ci
- Create a free account at dash.cloudflare.com. Then sign in from the terminal, giving Wrangler only the four permissions this needs (plain
wrangler loginasks for access to your whole account):npx wrangler login --use-keyring --scopes account:read user:read workers_scripts:write workers_kv:write
--use-keyringkeeps the token in your OS keychain instead of a plain file. - Set your time zone:
TZinworker/wrangler.jsonc(an IANA name such asEurope/IstanbulorAmerica/New_York). - Deploy:
npx wrangler deploy
The first deploy creates the storage for sign-ins (OAUTH_KV) and, if your account has none yet, aworkers.devsubdomain. It prints your address,https://apple-calendar-mcp.<your-subdomain>.workers.dev. Your connector URL is that address plus/mcp. - Add the secrets. Each command asks for the value; it is stored encrypted at Cloudflare, can't be read back, and takes effect at once:
npx wrangler secret put CALDAV_USERNAME # your Apple ID email npx wrangler secret put CALDAV_PASSWORD # the app-specific password from step 1 above npx wrangler secret put CONNECTOR_PASSWORD # a new random password, 12+ characters; you'll type it once when connecting
To make the connector password without it ever showing on screen (macOS; it lands in your clipboard for your password manager):openssl rand -hex 24 | tr -d '\n' | tee >(pbcopy) | npx wrangler secret put CONNECTOR_PASSWORD
- Check it:
https://<your address>/healthzshould show{"ok":true}. Then sign out of Wrangler:npx wrangler logout
Updating. Only changes to the code (worker/src) or to wrangler.jsonc need a deploy: git pull, log in as in step 2, npx wrangler deploy, log out. Changing a secret needs only npx wrangler secret put ….
Troubleshooting. npx wrangler tail shows the live log (passwords and tokens are never logged).
- "This connector isn't set up yet" (HTTP 503): the log says which setting is missing or invalid. A
CONNECTOR_PASSWORDthat is short or still the example from.env.exampleis refused on purpose. - "The CalDAV server rejected the login":
CALDAV_USERNAMEorCALDAV_PASSWORDis wrong. Apple shows an app-specific password only once, and revokes all of them when you change your Apple ID password. Create a new one and runnpx wrangler secret put CALDAV_PASSWORDagain. No redeploy or reconnect is needed. - No "App-Specific Passwords" on the Apple page: turn on two-factor authentication for your Apple ID first. School or work (managed) Apple IDs can't create them.
- Claude asks permission before each change: that's claude.ai confirming tools that change your calendar. You can set the read-only tools to "Always allow" under Settings → Connectors.
Or run the Python server yourself
Configure it:
cp .env.example .env
Fill in CALDAV_USERNAME (your Apple ID), CALDAV_PASSWORD (the app-specific password), PUBLIC_URL (the https address of your server), CONNECTOR_PASSWORD (a long random string you'll type once when connecting) and TZ.
Check the iCloud side before deploying:
uv sync --extra remote
uv run --env-file .env apple-calendar-mcp --backend caldav doctor
Then run it with Docker, on any host:
docker compose up -d
Or without Docker: uv run --env-file .env apple-calendar-mcp serve-http.
claude.ai only connects to https addresses. Use your host's HTTPS (Fly.io, Render, Railway and similar give you one), a reverse proxy like Caddy, or, to try it from your own computer, a tunnel:
cloudflared tunnel --url http://localhost:8000
Put the https://….trycloudflare.com address it prints into PUBLIC_URL and restart the server. (Quick tunnels get a new address each time; use a named tunnel or a host for something permanent.)
On a server with Caddy (Google Cloud VM).
docker-compose.https.yml adds Caddy, which gets the HTTPS certificate by itself. Any small Linux server with a public IP works; Google Cloud's always-free e2-micro (us-west1, us-central1 or us-east1) is enough:
gcloud compute instances create calendar-mcp --zone=us-central1-a --machine-type=e2-micro \
--image-family=debian-12 --image-project=debian-cloud --boot-disk-size=30GB \
--boot-disk-type=pd-standard --tags=https-server
gcloud compute firewall-rules create allow-web --allow=tcp:80,tcp:443,udp:443 --target-tags=https-server
gcloud compute scp deploy/gcp/setup-vm.sh calendar-mcp:~ --zone=us-central1-a
gcloud compute ssh calendar-mcp --zone=us-central1-a --command='bash setup-vm.sh'
The script installs Docker, clones the repo and tells you what to put in .env. No domain needed: <ip-with-dashes>.sslip.io points at your IP. Then, on the server:
docker compose -f docker-compose.yml -f docker-compose.https.yml up -d --build
Set a billing budget alert: Google's free tier rules can change, and the public IP may be billed.
3. Add it to Claude
In claude.ai: Settings → Connectors → Add custom connector. Enter your connector URL (https://…workers.dev/mcp, or https://<your server>/mcp), click Connect, and type your CONNECTOR_PASSWORD on the page that opens. The connector now appears in your connectors list on web, desktop and mobile.
Security notes
- Claude gets in only through OAuth. Registration accepts only Claude's own redirect URIs.
http://localhost(for the MCP Inspector) is allowed only whenPUBLIC_URLis itself localhost, or withOAUTH_ALLOW_LOCALHOST=1. - The login page names the site the sign-in will return to, compares the password in constant time, and locks out after repeated failures (5 per IP, 20 overall, per 15 minutes). The server refuses to start with the example password from
.env.example. - Access tokens last 1 hour, refresh tokens 30 days and rotate on every use. If an old refresh token is ever replayed, the whole grant is revoked and you sign in again. Codes and tokens are stored only as SHA-256 hashes in
DATA_DIR/auth.db(owner-only permissions). - On Cloudflare Workers,
/registerand/authorizeare rate limited per client IP (10 a minute), so scripted abuse can't burn through the free plan's daily KV write quota and block sign-ins. - Public endpoints are bounded: a flood of client registrations can't push out a signed-in Claude, and unfinished sign-ins are capped.
- Host and Origin headers are checked against
PUBLIC_URL. Logs never contain credentials, tokens or event contents. - The Docker setup runs as a non-root user with a read-only filesystem, no Linux capabilities, and the port bound to
127.0.0.1so only your HTTPS proxy reaches it. --read-onlyworks here too, if you want Claude on the web to only read your calendar (on Workers:"READ_ONLY": "1"inwrangler.jsonc).- On Cloudflare Workers, OAuth is handled by Cloudflare's
workers-oauth-provider: tokens are stored hashed in Workers KV and props are encrypted with the token. The same rules apply (Claude-only redirect URIs, 1 hour access tokens, 30 day rotating refresh tokens, lockout), with two differences: an old refresh token keeps working until its replacement is first used, and the lockout counter lives in KV, which is eventually consistent, so the limit is approximate.
What the remote connector can't see
It reads iCloud calendars only. Google, Exchange or university calendars that your Mac syncs through Calendar.app are not on iCloud, so only the local (EventKit) server sees them. Subscribed calendars and Birthdays are not available over CalDAV either.
Try it without a Mac
The in-memory backend serves a realistic fake week, which is handy with the MCP Inspector:
npx @modelcontextprotocol/inspector uv run apple-calendar-mcp --backend memory --demo
Development
uv sync --group dev --extra remote
uv run pytest --cov=apple_calendar_mcp # 190 tests (CalDAV ones run against a local Radicale server)
uv run ruff check . && uv run ruff format --check .
uv run mypy src # strict
The test suite has six layers:
| Layer | File | What it covers |
|---|---|---|
| Unit | test_timeutil_recurrence.py, test_scheduling.py |
ISO parsing, DST-safe recurrence expansion, interval merging and free slots |
| Service | test_service.py |
Create, update and delete validation, plus this / future / all semantics on recurring series |
| Protocol | test_server.py |
Full lifecycle over a real MCP client session, schemas, annotations, error mapping |
| Backend | test_eventkit_mapping.py, test_caldav_store.py |
EventKit on a fake PyObjC layer. CalDAV against a real Radicale server, re-running every service scenario plus iCalendar mapping (overrides, EXDATE, all-day, alarms, RRULE) |
| Remote | test_remote_auth.py |
The whole OAuth flow (registration, login, PKCE, refresh rotation, revocation, expiry, lockout) and tool calls over streamable HTTP |
| CLI | test_cli.py, test_macos_app.py |
Backend selection, --read-only and --demo, doctor output, and building the helper app and relaunching through it |
The Cloudflare Worker has its own TypeScript suite (cd worker && npm test): the same service scenarios against Radicale, recurrence and DST checks, a CPU budget check (the free plan allows 10 ms of CPU per request), and the OAuth flow and MCP tools inside workerd, the real Workers runtime.
CI runs on Ubuntu (Python 3.11–3.13) and macOS. The macOS job also imports the real PyObjC bindings and builds and signs the helper app. A Docker job builds the remote-connector image and checks it starts and requires a token. A Worker job type-checks, lints, tests and builds the Cloudflare Worker.
Manual check on a Mac
CI cannot click "Allow" in a permission dialog. Before a release, run through this list:
uv run apple-calendar-mcp doctorshows all your accounts' calendars.- In Claude: "What's on my calendar this week?" The results match Calendar.app.
- Create a test event. It appears in Calendar.app within a few seconds and syncs to your phone.
- Create a weekly test event. Move one occurrence (
this), then rename the rest (future), and check Calendar.app. - Delete the series (
all).
Limitations and roadmap
- No invitations. EventKit cannot add attendees. Attendees are read-only.
- Recurrence subset. Repeat rules support daily, weekly (with weekdays), monthly and yearly frequency, with an interval and a count or until date. More complex rules created in Calendar.app (for example "last Friday of the month") are reported in simplified form.
- Reminders are not supported yet. EventKit exposes them through the same store, so this would be a natural next step.
- Remote connector is self-hosted. Each person runs their own copy (a free Cloudflare Worker or a server). A shared, multi-user service listed in Claude's connector directory would need encrypted per-user credential storage and Anthropic's review.
License
MIT © Oğuz Kağan Göçmez
Metadata
Release files for apple-calendar-eventkit-mcp 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| apple_calendar_eventkit_mcp-0.2.0.tar.gz | 351.1 kB | Without provenance | Details for apple_calendar_eventkit_mcp-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| apple_calendar_eventkit_mcp-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for apple_calendar_eventkit_mcp-0.2.0-py3-none-any.whl |
Total release size: 413.9 kB
Release files / apple_calendar_eventkit_mcp-0.2.0.tar.gz
| Download URL | apple_calendar_eventkit_mcp-0.2.0.tar.gz |
|---|---|
| Size | 351.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9293fab9585607c031a4c21bcffed5c22af22bee72030e217e2b2a97426bb6a4
|
|
BLAKE2b-256 checksum How to use checksums |
a2d02c288efe3aaa92b8ddc9ee83920d19657665588003e4879424d8be3ff578
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / apple_calendar_eventkit_mcp-0.2.0-py3-none-any.whl
| Download URL | apple_calendar_eventkit_mcp-0.2.0-py3-none-any.whl |
|---|---|
| Size | 62.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b7b27717a72f5544da782b0c8b47889562bf5bba71e8adce0e45db7901b8913e
|
|
BLAKE2b-256 checksum How to use checksums |
fec66b0e8e5b407b8811699e0b2f7e7fe245bf367d384344424c899e822707e1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for apple-calendar-eventkit-mcp 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.