Skip to main content
Glama
jakobjhartmann

apple-calendar-jxa-mcp

apple-calendar-jxa-mcp

An MCP (Model Context Protocol) server for Apple Calendar on macOS — built to solve one specific, maddening problem: most Apple-Calendar MCP servers are silently denied calendar access under hosts like Claude Desktop. No permission prompt ever appears, no entry shows up in System Settings, and every call fails with a generic "permission denied".

This server fixes that by never touching EventKit from Node at all. Node only speaks MCP; every calendar operation is delegated to /usr/bin/osascript (JXA) — an Apple platform binary that is fully allowed to use EventKit and to trigger the macOS permission prompt.

Zero dependencies. One file. Node ≥ 18. macOS only. MIT.

Quick start

cd ~/Documents   # any stable location — just not Downloads
git clone https://github.com/jakobjhartmann/apple-calendar-jxa-mcp.git
cd apple-calendar-jxa-mcp
bash install.sh

install.sh registers the server in Claude Desktop's config (keeping your other MCP servers and backing up the file). Restart Claude, ask "What's on my calendar today?", and click Allow Full Access when macOS shows the permission dialog. That's it — no Terminal launches, no app modification, launch Claude normally from the Dock.

Using an AI assistant? If you're setting this up through Claude (Cowork) or another agent, just point it at this repository — see AGENTS.md, which tells the assistant exactly what to do, how to verify it, and how to troubleshoot.

The rest of this README explains why the usual approaches fail, what install.sh does under the hood, the manual alternative, and the full tool reference.

Related MCP server: iCloud Calendar MCP

Why your calendar MCP server never shows a permission prompt

Two independent macOS mechanisms have to say yes before any process reads your calendar:

  1. TCC (privacy consent) attributes every calendar request to the responsible process — the app at the root of the process tree. For an MCP server spawned by Claude Desktop, that is Claude Desktop itself. If that app does not declare calendar usage in its Info.plist, macOS rejects the request instantly: no dialog, and no entry in System Settings → Privacy & Security → Calendars (entries only appear after a valid request). This is why the pane stays empty no matter how often you reset with tccutil.

  2. Hardened runtime. Official Node.js builds (including everything nvm installs) are signed with the hardened runtime and without the com.apple.security.personal-information.calendars entitlement. Any EventKit call made inside the Node process is therefore blocked at the signature level — even if TCC would allow it. You can verify this yourself:

    codesign -dv --entitlements - "$(which node)" 2>&1 | grep flags
    # flags=0x10000(runtime)  ← hardened, and no calendar entitlement anywhere

Most Apple-Calendar MCP servers run EventKit through Node bindings or a bundled helper and hit one or both walls. This project routes around both:

Claude Desktop ──spawns──▶ node index.js        (MCP protocol only, no EventKit)
                              │
                              └──execFile──▶ /usr/bin/osascript -l JavaScript
                                                (JXA + EventKit: Apple platform
                                                 binary, prompts & access allowed)

The remaining requirement is TCC attribution (mechanism 1) — and here modern Claude Desktop versions do something helpful: they spawn MCP servers as self-responsible processes. The permission request this server makes through osascript is therefore attributed to the node process itself, which macOS happily shows a dialog for. You approve once ("node — Full Access" appears in System Settings), and it keeps working no matter how Claude is launched — and survives every Claude update, because the grant belongs to your Node binary, not to the app.

What install.sh does (and manual config)

install.sh adds this to ~/Library/Application Support/Claude/claude_desktop_config.json (existing servers preserved, file backed up first):

{
  "mcpServers": {
    "apple-calendar": {
      "command": "/ABSOLUTE/PATH/TO/node",
      "args": ["/ABSOLUTE/PATH/TO/apple-calendar-jxa-mcp/index.js"]
    }
  }
}

It uses an absolute Node path because the PATH Claude Desktop sees usually does not include nvm. That's all it does — it never touches the Claude app itself. To wire the config by hand instead, add the block above yourself (it also ships as examples/claude_desktop_config.example.json).

Permission setup

Default: no setup at all. On first calendar use, this server actively requests EventKit access and waits up to 45 seconds. Because Claude Desktop spawns MCP servers as self-responsible processes, macOS shows the dialog for the Node process: "node" would like full access to "Calendar" → click Allow Full Access. Done — verify under System Settings → Privacy & Security → Calendars: node — Full Access. The grant sticks across app launches and app updates. (If you later switch Node versions, e.g. via nvm, the dialog simply appears once more.)

⚠️ Do not patch or re-sign the Claude app to "add" calendar permission. An earlier version of this project did exactly that — it works, but ad-hoc re-signing changes the app's code identity, which breaks its keychain-bound state: forced re-logins and, worse, a broken device pairing for Claude's cloud/Cowork features. Learned the hard way so you don't have to.

Fallback — launch from Terminal (older hosts)

If no dialog ever appears (your host does not spawn MCP servers as self-responsible processes), make Terminal the responsible process and grant it calendar access once.

Step 1 — trigger the permission prompt and verify in one go. Run the built-in self-test from the repo directory in Terminal:

node index.js --selftest

A dialog appears: "Terminal" would like full access to "Calendar" → click Allow Full Access. The self-test then prints your calendar list — that's your proof the whole chain works. Verify under System Settings → Privacy & Security → Calendars: Terminal — Full Access.

Step 2 — launch your MCP host from Terminal. For Claude Desktop:

/Applications/Claude.app/Contents/MacOS/Claude

That's it. Launched this way, macOS attributes calendar requests to Terminal, which now has access.

Optional — make launching comfortable. Add an alias to ~/.zshrc:

alias claude-cal='nohup /Applications/Claude.app/Contents/MacOS/Claude >/dev/null 2>&1 & disown'

Then start Claude with claude-cal; the Terminal window can be closed afterwards.

Why Terminal? On hosts that don't disclaim responsibility for child processes, calendar requests are attributed to the host app itself — and unless its bundle declares calendar usage, macOS silently denies without a prompt. Launching from Terminal makes Terminal the responsible process, which can hold the permission.

Tools

calendar_calendars

Lists all calendars: id, name, account, writable. Call this first — several calendars can share a name across accounts (three calendars called "Calendar" is normal); disambiguate with id.

calendar_events

CRUD for events via action: read | create | update | delete.

  • readstartDate/endDate (defaults: today → +14 days), filterCalendar (name or id), search (matches title/notes/location/calendar), or id for a single event. Returns title, times, calendar, location, notes, URL, status, availability, recurrence flag and alarms — plus, for invitations, the organizer and all attendees with name, email address and response status (accepted/declined/pending), including which attendee is you (isMe).

  • createtitle + startDate required; endDate (default +1 h), targetCalendar (default: system default calendar), isAllDay, location, note, url, availability (busy/free/tentative/unavailable), alarms ([{"relativeOffset": -1800}] = 30 min before), recurrenceRules (frequency daily/weekly/monthly/yearly, interval, daysOfWeek 1=Sun…7=Sat, endDate or occurrenceCount).

  • updateid required; any create-field to change, plus clearAlarms, clearRecurrence, span (this-event | future-events), occurrenceStart to target a specific occurrence of a recurring event.

  • deleteid required; span/occurrenceStart as above.

Dates use local time, format YYYY-MM-DD HH:mm:ss (or just YYYY-MM-DD).

calendar_free_slots

Finds free time windows across calendars — for planning a week or finding time for a call against a shared calendar. Parameters: startDate (required), endDate (default +7 days), durationMinutes (5–1440, default 30), dayStart/dayEnd (default 09:00–21:00), calendars (busy sources; default all), includeAllDay (default false — holidays and birthdays don't block), treatFreeAsBusy (default false). Returns per-day slots with date, from, to and length.

Example prompts

  • "What's on my calendar today?"

  • "Plan my week: list everything Monday–Friday grouped by day."

  • "Create 'Dentist' on Thursday 14:00–15:00 in my Work calendar, reminder 30 minutes before."

  • "Move my 10 o'clock meeting tomorrow to 11."

  • "When could I make a 45-minute call this week, considering my calendar and the shared one?"

  • "Who hasn't responded to Friday's meeting invite yet, and what are their email addresses?"

  • "Block tomorrow 12–13 as 'Focus time', marked as free so it doesn't block scheduling."

Scheduled tasks (daily briefings etc.)

One non-obvious constraint if you automate with this server: Claude's cloud-based scheduled tasks cannot use it. They fire as headless cloud sessions, and the bridge that exposes local MCP servers only attaches to interactive sessions — so scheduled cloud runs have no calendar access even with your Mac awake and Claude Desktop open, and they fail silently (the run continues without calendar data). For a recurring calendar briefing, create a local scheduled task in the Claude Desktop app instead; local runs have this server attached. Per-run requirements: Mac awake, Claude Desktop running. (Observed July 2026; host behavior may change.)

Limitations

  • Sending invitations (adding attendees) is not possible. Apple exposes no public API for adding attendees to an event — EKEvent.attendees is read-only, and Calendar.app's scripting dictionary does not support it either. Practical routes: create the event here, then add invitees in the Calendar app; or use a shared calendar — events created in a shared iCloud calendar automatically notify the other participants, which covers many "invite" use cases without any invitation at all.

  • Responding to invitations (accept/decline) is not possible. Same reason: no public API for setting your participation status. Attendee names, email addresses and their responses are fully readable; changing yours requires the Calendar app. Both limitations apply to every third-party calendar tool on macOS, not just this one.

  • Each tool call spawns a short-lived osascript process (~100–300 ms overhead). Fine for interactive use; not built for bulk sync.

  • Very large date ranges are capped by EventKit's internal query limits (~4 years per read).

  • macOS only, by nature.

Troubleshooting

  • The permission dialog appeared but the tool call timed out — normal on the very first use: the dialog can outlive the host's tool timeout. Click Allow, then simply retry the call once.

  • No dialog ever appears and calls fail with "Kein Kalender-Vollzugriff: …" — your host attributes the request to a process that cannot prompt. Use the Terminal fallback above. Also check Screen Time / MDM restrictions; tccutil reset Calendar gives you a clean slate to retry.

  • You clicked "Don't Allow" or granted "Add Only" — run tccutil reset Calendar, restart the host, trigger a calendar action and choose Full Access this time.

  • Broke after switching Node versions (nvm) — the grant is bound to the Node binary; the dialog reappears once, just allow it again. (Also update the command path in your config.)

Roadmap

  • Apple Reminders support (same JXA approach; needs the separate Reminders permission)

  • npm publish for npx installation

  • Optional read-only mode

License

MIT — see LICENSE.


Born from a long debugging session that started with an empty Calendars pane in System Settings and ended at flags=0x10000(runtime). If this README saved you that evening: you're welcome.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

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/jakobjhartmann/apple-calendar-jxa-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server