[BUG] Claude Desktop macOS: MCP server child process cannot obtain Calendar (EventKit) TCC permission — Reminders works, Calendar doesn't

Resolved 💬 1 comment Opened Jul 12, 2026 by hnchavezar Closed Jul 15, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

What's Wrong?

On macOS, a local stdio MCP server spawned by Claude Desktop cannot obtain user consent for Calendar access (EKEventStore.requestFullAccessToEvents), even though the identical binary succeeds instantly when launched from Terminal. Reminders access (requestFullAccessToReminders) works fine through the same MCP server. This makes any MCP server that needs Calendar/EventKit access unusable from Claude Desktop on macOS, with no error dialog and no actionable feedback to the user — the request simply resolves as notDetermined/denied with no system prompt ever shown.

Environment

  • Product: Claude Desktop (macOS)
  • OS: macOS Sequoia (Apple Silicon, MacBook Air)
  • MCP transport: local stdio server (command/args in claude_desktop_config.json, launched via a Node.js binary from nvm)
  • Underlying tool: a Swift binary using EventKit (EKEventStore), invoked by the Node MCP server via child_process.spawn

Steps to Reproduce

  1. Configure a local stdio MCP server in claude_desktop_config.json that spawns a child process (Node → compiled Swift binary in this case, but the pattern should generalize to any native helper) which calls EKEventStore.requestFullAccessToEvents(completion:).
  2. From a Claude Desktop conversation, invoke an MCP tool that triggers that permission request for the first time.
  3. Observe: no system permission dialog ever appears. The completion handler returns success: false, error: nil. Calling EKEventStore.authorizationStatus(for: .event) afterward still reports notDetermined (not denied — the request never resolved at all).
  4. From a separate Terminal window, run the exact same binary (identical code signature, identical bundle) with the identical arguments: the system permission dialog appears normally, the user can grant "Full Calendar Access," and every subsequent call succeeds in well under 200ms.
  5. Reminders access (requestFullAccessToReminders) for the same MCP server, spawned the same way, works correctly and shows/resolves its permission dialog without issue — so this is not a blanket "background processes can't get TCC prompts" limitation, it is specific to how Calendar access resolves for processes in Claude Desktop's spawn chain.

What We Tried (all failed to resolve it)

  • Packaging the helper as a proper .app bundle (LSUIElement=true, LSBackgroundOnly=true, with NSCalendarsFullAccessUsageDescription/NSCalendarsUsageDescription in Info.plist) instead of a bare Mach-O executable — no change; still notDetermined when invoked from Claude Desktop's process chain, while the identical bundle works instantly from Terminal.
  • tccutil reset Calendar followed by a full quit (Cmd+Q) and relaunch of Claude Desktop.
  • Manually setting "Full Calendar Access" (not "Add Only") for the "Claude" entry in System Settings → Privacy & Security → Calendars, followed by a full app restart.
  • Force-killing the underlying Node MCP server process (pkill) to rule out a stale/cached authorization state in a long-lived process before relaunching Claude Desktop.
  • Added a diagnostic MCP tool that calls EKEventStore.authorizationStatus(for:) directly (no request, pure read) to confirm state without side effects — consistently shows remindersStatus: fullAccess / calendarStatus: notDetermined when queried from Claude Desktop's spawn chain, on every attempt, across binary rebuilds, resets, and permission toggles.

Expected vs Actual

  • Expected: An MCP server child process spawned by Claude Desktop should be able to trigger standard macOS TCC consent dialogs (Calendar, and presumably other TCC-gated frameworks) just as it can when launched from Terminal, or at minimum surface a clear, actionable error/state so the developer/user knows a manual step is required.
  • Actual: The request silently resolves to an unresolved (notDetermined) state with no dialog and no distinguishing error from a real denial, and there appears to be no way to reach a resolved state from within that process chain — Reminders resolves fine, Calendar does not, using equivalent EventKit API calls on the same OS, same binary, same MCP server, same session.

Impact

Any MCP server author trying to build macOS calendar integrations (scheduling assistants, calendar sync tools, etc.) for Claude Desktop hits this wall. The workaround (running the helper manually once from Terminal) does not carry over to the Claude Desktop process chain, so there's currently no way to make Calendar-based MCP tools work end-to-end inside a Claude Desktop conversation on macOS.

What Should Happen?

A Claude Desktop MCP server child process should be able to trigger standard macOS TCC consent dialogs (Calendar, and other TCC-gated frameworks) just as it can when the same binary is launched from Terminal — or at minimum surface a clear, actionable error/state so the developer knows a manual step is required. Reminders access (requestFullAccessToReminders) works correctly through the same MCP server; Calendar access (requestFullAccessToEvents) using the equivalent EventKit API does not, with no dialog ever shown and no way to reach a resolved permission state from within that process chain.

Error Messages/Logs

Direct request failure (via calendar_list_events / calendar_list_calendars MCP tools):
reminders-bridge exited with code 1. stdout: {"error":"access_denied_or_timeout","statusAtFailure":"notDetermined"} | stderr:

Diagnostic status check (custom debug tool calling EKEventStore.authorizationStatus(for:) directly, no request triggered):
{"calendarStatus":"notDetermined","remindersStatus":"fullAccess","processID":"42803","processPath":"/Users/.../RemindersBridge.app/Contents/MacOS/reminders-bridge"}

Same status check run from Terminal (not spawned by Claude Desktop), same binary, same session:
{"calendarStatus":"notDetermined","remindersStatus":"fullAccess","processID":"...","processPath":"/Users/.../RemindersBridge.app/Contents/MacOS/reminders-bridge"}
(resolves to fullAccess after one successful interactive request from Terminal; never resolves when requested from Claude Desktop's process chain, across every attempt)

No stderr output, no exceptions, no crash — the completion handler for EKEventStore.requestFullAccessToEvents(completion:) simply returns success=false, error=nil when invoked from the Claude Desktop spawn chain, with no system dialog ever presented. authorizationStatus(for: .event) remains notDetermined afterward, meaning the request never resolved to any decision (not even an explicit denial).

Steps to Reproduce

  1. Configure a local stdio MCP server in claude_desktop_config.json that spawns a child process (Node.js → compiled Swift binary in this case, but the pattern should generalize to any native helper) which calls EKEventStore.requestFullAccessToEvents(completion:).
  2. From a Claude Desktop conversation, invoke an MCP tool that triggers that permission request for the first time.
  3. Observe: no system permission dialog ever appears. The completion handler returns success: false, error: nil. Calling EKEventStore.authorizationStatus(for: .event) afterward still reports notDetermined (not denied — the request never resolved at all).
  4. From a separate Terminal window, run the exact same binary (identical code signature, identical bundle) with identical arguments: the system permission dialog appears normally, the user can grant "Full Calendar Access," and every subsequent call succeeds in well under 200ms.
  5. Reminders access (requestFullAccessToReminders) for the same MCP server, spawned the same way, works correctly and resolves its permission dialog without issue — so this is not a blanket "background processes can't get TCC prompts" limitation, it is specific to how Calendar access resolves for processes in Claude Desktop's spawn chain.

Also tried, none resolved it:

  • Packaging the helper as a proper .app bundle (LSUIElement=true, LSBackgroundOnly=true, with NSCalendarsFullAccessUsageDescription/NSCalendarsUsageDescription in Info.plist) instead of a bare Mach-O executable.
  • tccutil reset Calendar followed by a full quit (Cmd+Q) and relaunch of Claude Desktop.
  • Manually setting "Full Calendar Access" (not "Add Only") for the "Claude" entry in System Settings → Privacy & Security → Calendars, followed by a full app restart.
  • Force-killing the underlying Node MCP server process (pkill) to rule out stale/cached authorization state before relaunching Claude Desktop.

Claude Model

Sonnet (default)

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

Claude 1.20186.1 (df1d8a) 2026-07-10T21:55:12.000Z

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

This is a general MCP-server-development pattern issue, not specific to any single project, so no public repro repo is linked. Happy to share a minimal reproduction MCP server on request.

Relevant excerpt from claude_desktop_config.json (mcpServers section only):
{
"mcpServers": {
"mac-life": {
"command": "/Users/<user>/.nvm/versions/node/v22.23.1/bin/node",
"args": ["/Users/<user>/Projects/mac-life-mcp-server/dist/index.js"]
}
}
}

The MCP server is a Node.js/TypeScript process that spawns a compiled Swift binary (packaged as a proper .app bundle with LSUIElement=true) via child_process.spawn to call EventKit APIs. The Reminders half of this same binary/server works flawlessly through Claude Desktop; only the Calendar half is affected — same code pattern, same EventKit framework, different underlying TCC service.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗