Skip to main content
Glama
mail-calendar-mcp

mail-calendar-mcp

by mathewcsims

mail-calendar-mcp

A local MCP server giving full read/write control over Apple Mail.app and Calendar.app on macOS — every account configured in either app (iCloud, Google, Exchange, IMAP, etc.) is exposed uniformly, with no per-provider setup and no GUI/computer-use automation. All automation is done via JXA (osascript -l JavaScript) talking to the same AppleScript dictionaries Mail.app/Calendar.app themselves expose.

Status

Feature-complete: all of Mail and Calendar's core scriptable surface is implemented, plus computed availability. Every tool below was exercised through the real MCP protocol against a live Mail.app/Calendar.app during development, not just type-checked — several real bugs were found and fixed or explicitly documented as platform limitations in the process (see below).

Calendar runs on a native EventKit backend by default (helper/main.swift, compiled to bin/calendar-helper by npm run build). This exists because Calendar.app's AppleScript bridge silently no-ops on several operations. The native backend fixes the confirmed recurring-event-delete and calendar-delete bugs, and adds capabilities AppleScript cannot express at all: per-occurrence vs whole-series edits/deletes (occurrenceDate + span), stable calendarIds, calendar source selection, richer event detail (attendees/alarms/detached-occurrence status), and cross-calendar queries in one call. If the helper binary is missing or lacks Calendar permission, calendar tools fall back to the JXA path automatically (safe even for writes — the permission check runs before any operation). MCM_CALENDAR_BACKEND=jxa forces the old path. Mail remains JXA-only: there is no public native API for Mail.app automation.

Related MCP server: calendar-mcp

Tool inventory

Mail

Tool

Notes

mail_list_accounts, mail_get_account, mail_get_account_stats

Read-only

mail_list_mailboxes, mail_create_mailbox

Works reliably

mail_rename_mailbox

Confirmed broken — Mail rejects the property assignment. Rename manually in Mail.app.

mail_delete_mailbox

Confirmed broken on cloud-synced accounts (iCloud tested) — untested on a local "On My Mac" mailbox. [IRREVERSIBLE], requires confirm:true.

mail_search_messages, mail_get_message, mail_get_message_source, mail_get_message_headers, mail_list_attachments, mail_get_attachment

Read-only, paginated (default 50/max 500)

mail_set_read_status, mail_set_flagged, mail_set_flag_color, mail_move_message, mail_copy_message

Works reliably

mail_delete_message

Moves to Trash, recoverable

mail_permanently_delete_message

[IRREVERSIBLE], requires confirm:true

mail_create_draft, mail_create_reply_draft, mail_create_forward_draft, mail_update_draft, mail_list_drafts, mail_delete_draft

Requires fromAddress on new-compose calls — see gotcha below

mail_add_attachment_to_draft

Unverified — the JXA attachment-insertion syntax could not be live-tested; treat with suspicion

mail_send_message

[IRREVERSIBLE], requires confirm:true. Two modes: send an existing composeSessionId, or compose+send fresh

mail_list_signatures, mail_create_signature, mail_update_signature, mail_delete_signature

Works reliably

mail_list_rules

Read-only by design — see permanent limitations below

Calendar

Tool

Notes

calendar_list_calendars

Read-only. Returns an index for disambiguating duplicate calendar names (confirmed real on this machine: two calendars named "Calendar", two named "Birthdays")

calendar_create_calendar, calendar_rename_calendar

Works reliably. Create accepts sourceName (EventKit backend) to pick the account

calendar_list_sources

Calendar accounts/sources, for calendar_create_calendar placement. EventKit backend only

calendar_delete_calendar

Reliable via the EventKit backend. (JXA fallback confirmed broken for this — expect PLATFORM_LIMITATION there.) [IRREVERSIBLE], requires confirm:true

calendar_list_events, calendar_search_events

Read-only. startDate/endDate are required, capped at 366 days — confirmed an unbounded query hangs indefinitely

calendar_get_event

By uid; EventKit backend adds attendees, alarms, recurrence detail, and per-occurrence fetch via occurrenceDate

calendar_create_event, calendar_update_event

Reliable. EventKit backend adds alarmMinutesBefore and, on update, occurrenceDate/span for one-occurrence vs whole-series edits — semantics AppleScript can't express

calendar_delete_event

Reliable via the EventKit backend, including recurring events and per-occurrence deletes (occurrenceDate/span). (The JXA fallback silently no-ops on recurring events — confirmed, caught by verify-after-write.) [IRREVERSIBLE], requires confirm:true

calendar_add_alarm, calendar_list_alarms, calendar_remove_all_alarms

Works reliably (display/sound/mail kinds)

calendar_list_attendees, calendar_add_attendee

Worked reliably in testing (better than pessimistic community reports this was designed against). Adding an attendee surfaces an extra email:null self/organizer entry (isLikelyOrganizerSelf:true) — not a bug

calendar_remove_attendee

Unverified — implemented but not live-tested

calendar_get_availability

Computed locally (not a native Calendar.app feature) from busy intervals across the calendars you specify

Cross-cutting

Tool

Notes

mail_app_status, calendar_app_status

Health-check — distinguishes "app not running" from a genuine data error

Setup

npm install
npm run build   # or `npm run dev` for a live tsx run during development
npm test        # runs the vitest suite (Node-side logic only, see Testing below)

Add to your MCP client config (Claude Desktop / Claude Code) pointing at dist/index.js (after npm run build) or src/index.ts via npx tsx for development.

Building requires the Xcode Command Line Tools (swiftc) for the native calendar helper.

One-time macOS permission grants

Two separate macOS permissions are involved (both one-time, both per host app — the app that spawns the server, e.g. Claude Desktop or your terminal):

  1. Automation (Mail + Calendar via JXA): the first JXA call triggers "<host> wants to control Mail/Calendar" — click OK. Re-enable under System Settings → Privacy & Security → Automation if ever denied.

  2. Calendars — Full Access (native EventKit backend): the host app needs Full Access under System Settings → Privacy & Security → Calendars. Note macOS may silently grant only "Add Events Only" without ever prompting — if calendar_app_status reports the helper unauthorized, flip that entry to Full Access manually. Until then, calendar tools transparently fall back to JXA.

If a tool call fails with error code AUTOMATION_NOT_AUTHORIZED, these are the settings to check. The ad-hoc code signature on bin/calendar-helper changes on every rebuild, which can re-trigger the Calendars grant.

Important gotcha: fromAddress

Creating a draft or sending without specifying fromAddress silently uses Mail.app's global default "send new messages from" account — confirmed during development that this is not necessarily the account you intended (on the development machine it defaulted to an unrelated work account rather than the personal account being targeted). fromAddress is a required field on mail_create_draft and on mail_send_message when composing fresh (not required when sending an existing composeSessionId, which already has an account fixed).

Design notes

  • Injection-safe: every JXA invocation uses execFile('osascript', ['-l','JavaScript', <fixed script path>, <JSON string>]) — never a shell string, never AppleScript source built from caller input. See src/jxa/runner.ts.

  • Timeouts: every call has a per-operation timeout with SIGKILL on expiry, since Mail/Calendar can hang against an offline/asleep account or an unbounded query. Verified against both a deliberately-hung script and a real unbounded Calendar query during development.

  • Error taxonomy: see src/errors.ts — tool failures return one of a fixed set of codes (APP_NOT_RUNNING, AUTOMATION_NOT_AUTHORIZED, ACCOUNT_OFFLINE_OR_ASLEEP, NOT_FOUND, TIMEOUT, PLATFORM_LIMITATION, INVALID_INPUT, UNEXPECTED_OSA_ERROR) rather than opaque strings. PLATFORM_LIMITATION specifically covers writes that report success but don't actually take effect — caught by re-reading state after every mutating call.

  • Destructive-tool gating: the highest-risk tools (permanent delete, send, delete calendar/mailbox) require an explicit confirm: true argument in addition to whatever gating your MCP client applies, as defense-in-depth against an accidental or hallucinated call. See requireConfirm() in src/tools/register.ts.

  • Object identity: Mail messages are addressed by {accountName, mailboxPath, messageId} (Mail's internal numeric id) or by RFC822 Message-ID; mailboxes by hierarchical name path; Calendar events by their iCalendar uid (stable via scripting, unlike calendars themselves, which have no queryable uid — hence the calendarIndex disambiguator).

  • Native Swift/EventKit helper: originally deferred (most JXA calendar operations worked in initial testing), later built once the JXA bridge's failures proved painful in real use — see the Status section. helper/main.swift is a single-file CLI compiled by npm run build:helper with an embedded Info.plist (linker -sectcreate) and an ad-hoc code signature; it speaks the same JSON-envelope contract as the JXA scripts, so src/helper/backend.ts can route per-call with JXA fallback. EventKit's public API cannot write attendees, so attendee add/remove stays on JXA regardless of backend.

Known permanent platform limitations

(Not bugs — confirmed not achievable via AppleScript/JXA, and in most cases no alternative API exists either.)

  • Mail: creating/editing Mail rules (rules can only invoke a script as an action, not be created externally — mail_list_rules is read-only by design); mail account creation/credential management.

  • Calendar: sending or RSVP-replying to meeting invitations; to-dos (moved entirely to Reminders.app, a separate app/dictionary, out of scope here).

Known confirmed-broken/flaky operations

(Found via live testing, not guessed — reproduced deterministically before being documented.)

  • mail_rename_mailbox — the property assignment is rejected outright.

  • mail_delete_mailbox — fails on cloud-synced accounts (confirmed on iCloud); untested on local mailboxes.

  • mail_add_attachment_to_draft — syntax unverified, no live test possible; treat with suspicion.

  • calendar_delete_calendar — fixed by the EventKit backend; the JXA fallback remains broken.

  • calendar_delete_event on a recurring event — fixed by the EventKit backend; the JXA fallback reports success without actually deleting (silently wrong unless caught, which is why the JXA path verifies its result before reporting success).

  • calendar_remove_attendee — implemented but not live-tested; treat with suspicion until verified. (EventKit's public API cannot write attendees at all, so this stays on JXA regardless of backend.)

Testing

There's no way to unit-test "did this actually change Mail.app/Calendar.app state" outside a live session, so verification is two-tiered:

  1. Manual checklists (test/manual-checklists/) — the primary verification method, run against dedicated scratch fixtures so destructive-op testing never touches real mail/events:

    • Mail: a Testing/MCP-Scratch mailbox on a non-work account, all send-tests target only your own address.

    • Calendar: a local MCP-Scratch calendar ("On My Mac", to avoid sync churn).

    • One leftover artifact from development still needs manual cleanup: an empty MCP-Delete-Test mailbox in iCloud Mail (mailbox deletion via scripting is broken, see above). Two recurring test events stuck in the MCP-Scratch calendar — the JXA delete bug's own evidence — are removable via the EventKit backend.

  2. npm test (vitest) — covers only the Node-side logic that doesn't need live app state: error-code mapping (test/errors.test.ts), destructive-tool confirm-gating (test/register.test.ts), JSON envelope parsing robustness (test/jxa-runner.test.ts), and free/busy gap computation (test/availability.test.ts).

Available Tools

51 tools
calendar_add_alarmB

Add a display/sound/email alarm to an event, N minutes before it starts.

Inputs: uid (required), minutesBefore (required), kind, calendarName, +1 more.

calendar_add_attendeeA

Add an attendee (by email) to an event. This has historically been an unreliable area of Calendar's AppleScript/JXA dictionary — the tool verifies the attendee actually appears on re-read before reporting success.

Inputs: uid (required), email (required), calendarName, calendarIndex.

calendar_app_statusA
Read-onlyIdempotent

Check whether Calendar.app is running and responding to Automation. Use this before other Calendar tools to distinguish "app not running/asleep" from a genuine data error.

Inputs: no named parameters.

calendar_create_calendarA

Create a new calendar. Verifies the calendar actually exists after creation before reporting success (Calendar's scripting dictionary has known flaky spots).

Inputs: name (required).

calendar_create_eventA

Create a new event. Verifies the event actually exists (by uid) after creation before reporting success — Calendar's scripting dictionary has known flaky spots for event creation.

Inputs: endDate (required), startDate (required), summary, location, +5 more.

calendar_delete_calendarA
Destructive

[IRREVERSIBLE] Permanently delete a calendar and everything in it. Confirmed via testing to be unreliable via scripting on at least some calendars — expect a PLATFORM_LIMITATION error and delete manually in Calendar.app as a fallback. Requires confirm:true.

Inputs: confirm, calendarName, calendarIndex.

calendar_delete_eventA
Destructive

[IRREVERSIBLE] Delete an event by uid. Confirmed via testing: deleting a RECURRING event silently reports success without actually deleting it on this platform — this tool detects that and returns PLATFORM_LIMITATION instead of a false success; delete recurring events manually in Calendar.app (where you can choose 'this event' vs 'all future events'). Requires confirm:true.

Inputs: uid (required), confirm, calendarName, calendarIndex.

calendar_get_availabilityA
Read-onlyIdempotent

Compute free/busy gaps across one or more calendars within a bounded date range (max 366 days). This is computed locally from calendar_list_events-equivalent data, not a native Calendar.app feature — Calendar's scripting dictionary has no free/busy API.

Inputs: endDate (required), calendars (required), startDate (required).

calendar_get_eventA
Read-onlyIdempotent

Get a single event by its uid (stable iCalendar identifier — see calendar_list_events output).

Inputs: uid (required), calendarName, calendarIndex.

calendar_list_alarmsC
Read-onlyIdempotent

List all alarms (display/sound/mail) on an event.

Inputs: uid (required), calendarName, calendarIndex.

calendar_list_attendeesA
Read-onlyIdempotent

List attendees on an event. Note: once any attendee is added to an event you organize, an extra entry with a null email (the organizer/self) commonly appears — flagged as isLikelyOrganizerSelf, not a bug.

Inputs: uid (required), calendarName, calendarIndex.

calendar_list_calendarsA
Read-onlyIdempotent

List every calendar across all accounts (iCloud/Google/Exchange/etc.) with its index (use for calendar_* tools when names are ambiguous), writability, and description.

Inputs: no named parameters.

calendar_list_eventsA
Read-onlyIdempotent

List events in a calendar within a bounded date range (max 366 days — required, not optional, to avoid multi-year recurrence-expansion queries hanging). Paginated.

Inputs: endDate (required), startDate (required), limit, offset, +2 more.

calendar_remove_all_alarmsC

Remove every alarm from an event.

Inputs: uid (required), calendarName, calendarIndex.

calendar_remove_attendeeA

Remove an attendee (by email) from an event. Unverified/experimental — treat failures as a real platform limitation, not a bug in this tool.

Inputs: uid (required), email (required), calendarName, calendarIndex.

calendar_rename_calendarB
Idempotent

Rename an existing calendar.

Inputs: newName (required), calendarName, calendarIndex.

calendar_search_eventsA
Read-onlyIdempotent

Search events in a calendar by summary text within a bounded date range (max 366 days).

Inputs: endDate (required), startDate (required), textContains (required), calendarName, +1 more.

calendar_update_eventA

Update fields on an existing event by uid. Verifies changed fields actually took effect on re-read before reporting success — recurrence edits on already-recurring events are a known-flaky area.

Inputs: uid (required), endDate, summary, location, +6 more.

mail_app_statusA
Read-onlyIdempotent

Check whether Mail.app is running and responding to Automation. Use this before other Mail tools to distinguish "app not running/asleep" from a genuine data error.

Inputs: no named parameters.

mail_copy_messageA

Copy a message to a different mailbox, leaving the original in place.

Inputs: accountName (required), mailboxPath (required), destMailboxPath (required), messageId, +2 more.

mail_create_draftA

Create a new draft email (saved to the account's Drafts mailbox). Returns a composeSessionId usable with mail_send_message, mail_update_draft — valid only until Mail.app next restarts.

Inputs: fromAddress (required), cc, to, bcc, +3 more.

mail_create_forward_draftB

Create a forward draft of an existing message.

Inputs: messageId (required), accountName (required), mailboxPath (required), cc, +2 more.

mail_create_mailboxB

Create a new mailbox/folder under an account, optionally nested under an existing parent mailbox path.

Inputs: name (required), accountName (required), parentPath.

mail_create_reply_draftA

Create a reply draft to an existing message, with quoted original content prefilled.

Inputs: messageId (required), accountName (required), mailboxPath (required), replyToAll, +1 more.

mail_create_signatureC

Create a new Mail signature.

Inputs: name (required), content (required).

mail_delete_draftA
Destructive

Delete a persisted draft from the Drafts mailbox (recoverable via Trash, like mail_delete_message).

Inputs: accountName (required), draftMessageId (required).

mail_delete_mailboxA
Destructive

[IRREVERSIBLE] Permanently delete a mailbox/folder and everything in it. Known to be unreliable for cloud-synced accounts (iCloud/Exchange/IMAP) due to a Mail.app scripting limitation — may fail with PLATFORM_LIMITATION. Requires confirm:true.

Inputs: path (required), accountName (required), confirm.

mail_delete_messageA
Destructive

Move a message to Trash. Recoverable — it stays in the account's Trash mailbox until emptied.

Inputs: accountName (required), mailboxPath (required), messageId, rfc822MessageId.

mail_delete_signatureC
Destructive

Delete a Mail signature.

Inputs: name (required).

mail_get_accountA
Read-onlyIdempotent

Get details for a single Mail account by name.

Inputs: accountName (required).

mail_get_account_statsA
Read-onlyIdempotent

Get unread message counts for an account, broken down by mailbox (recursive).

Inputs: accountName (required).

mail_get_attachmentA

Save one attachment (by its index from mail_list_attachments) to a local directory. Refuses attachments that would need more than a reasonable size — check size via mail_list_attachments first for anything large.

Inputs: index (required), accountName (required), mailboxPath (required), destDirectory (required), +2 more.

mail_get_messageC
Read-onlyIdempotent

Get full metadata and plaintext body for a single message.

Inputs: accountName (required), mailboxPath (required), messageId, rfc822MessageId.

mail_get_message_headersB
Read-onlyIdempotent

Get just the raw header block of a message.

Inputs: accountName (required), mailboxPath (required), messageId, rfc822MessageId.

mail_get_message_sourceA
Read-onlyIdempotent

Get the full raw RFC822 source (headers + MIME body) of a message. Use this to extract HTML bodies or inspect MIME structure.

Inputs: accountName (required), mailboxPath (required), messageId, rfc822MessageId.

mail_list_accountsA
Read-onlyIdempotent

List every Mail account configured in Mail.app (iCloud, Google, Exchange, IMAP, etc.) with type, enabled state, and email addresses.

Inputs: no named parameters.

mail_list_attachmentsA
Read-onlyIdempotent

List attachments on a message (name, size, download state) without downloading them.

Inputs: accountName (required), mailboxPath (required), messageId, rfc822MessageId.

mail_list_draftsA
Read-onlyIdempotent

List persisted drafts in an account's Drafts mailbox.

Inputs: accountName (required).

mail_list_mailboxesA
Read-onlyIdempotent

List the full mailbox/folder tree (including nested subfolders and unread counts) for a Mail account.

Inputs: accountName (required).

mail_list_rulesA
Read-onlyIdempotent

List existing Mail rules (name, enabled state). Read-only: creating/editing rules is not achievable via AppleScript/JXA — Mail's rule automation only supports rules invoking a script as an action, not external rule creation. Manage rules in Mail.app > Settings > Rules.

Inputs: no named parameters.

mail_list_signaturesA
Read-onlyIdempotent

List all Mail signatures (name + content).

Inputs: no named parameters.

mail_move_messageB

Move a message to a different mailbox (recoverable — just relocates it).

Inputs: accountName (required), mailboxPath (required), destMailboxPath (required), messageId, +2 more.

mail_permanently_delete_messageA
Destructive

[IRREVERSIBLE] Permanently delete a message, bypassing/purging Trash. This cannot be undone. Requires confirm:true.

Inputs: accountName (required), mailboxPath (required), confirm, messageId, +1 more.

mail_rename_mailboxA
Idempotent

Rename an existing mailbox/folder. Confirmed via testing to be unreliable via AppleScript/JXA on at least some accounts — expect a PLATFORM_LIMITATION error and rename manually in Mail.app as a fallback.

Inputs: path (required), newName (required), accountName (required).

mail_search_messagesA
Read-onlyIdempotent

Search/list messages in a mailbox with optional filters (read status, flagged status, subject/sender substring). Paginated: default limit 50, max 500.

Inputs: accountName (required), mailboxPath (required), limit, offset, +4 more.

mail_send_messageA
Destructive

[IRREVERSIBLE] Send an email. Either pass composeSessionId to send an existing draft from this Mail.app session, or pass to/subject/content directly to compose and send in one call. Requires confirm:true.

Inputs: cc, to, bcc, confirm, +5 more.

mail_set_flag_colorB
Idempotent

Set a message's colored-flag index (-1 clears the flag; 0-6 select a color per Mail.app's flag palette).

Inputs: colorIndex (required), accountName (required), mailboxPath (required), messageId, +1 more.

mail_set_flaggedB
Idempotent

Flag or unflag a message.

Inputs: value (required), accountName (required), mailboxPath (required), messageId, +1 more.

mail_set_read_statusC
Idempotent

Mark a message as read or unread.

Inputs: value (required), accountName (required), mailboxPath (required), messageId, +1 more.

mail_update_draftA

Update fields (subject/content/recipients) of an open draft compose session by composeSessionId.

Inputs: composeSessionId (required), cc, to, bcc, +4 more.

mail_update_signatureC
Idempotent

Update an existing Mail signature's content.

Inputs: name (required), content (required).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.7/5.0

    Scored across 51 tools

    Disambiguation5/5

    Each tool has a clear domain prefix (calendar_ or mail_) and a distinct action on a specific resource. Overlaps are minimal and descriptions clarify any ambiguity.

    Naming Consistency5/5

    All tools follow a consistent snake_case verb_noun pattern with domain prefixes. The naming is uniform within and across both calendar and mail domains.

    Tool Count4/5

    51 tools is high but warranted for covering two full applications (Mail and Calendar). The granularity supports detailed operations, though some niche tools (e.g., mail_get_message_headers) could be merged.

    Completeness5/5

    CRUD operations, alarms, attendees, signatures, drafts, flags, attachments, and advanced features like availability and raw message source are all covered, with documented platform limitations.

    Maintenance

    ActivityInactive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables integration with Apple Mail on macOS using JavaScript for Automation (JXA). Supports reading, searching, sending, and managing emails across multiple accounts with full mailbox hierarchy support.
      5
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      Exposes Apple Calendar data through an EventKit helper app. Enables reading, creating, updating, and deleting calendar events on macOS.
      7
      20 npm
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables Claude or any MCP client to read Apple Mail, Calendar, Photos, and create Apple Notes locally on macOS, without any data leaving the machine.
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      Enables AI assistants to read, search, send, and manage email on macOS through programmatic access to Apple Mail.
      29
      MIT