Labsco
kiki830621 logo

che-ical-mcp

β˜… 30

from kiki830621

Native macOS Calendar & Reminders MCP server with 24 tools using Swift EventKit - supports recurring events, location triggers, search, batch operations

πŸ”₯πŸ”₯πŸ”₯βœ“ VerifiedFreeAdvanced setup

che-ical-mcp

License: MIT macOS Swift MCP

Give Claude native control of macOS Calendar and Reminders. A Swift MCP server built directly on EventKit β€” 29 tools for events, reminders, tags, batch operations, conflict detection, and undo/redo. Not just calendar events: it drives Reminders and tasks too.

English | 繁體中文


Why che-ical-mcp?

FeatureOther Calendar MCPsche-ical-mcp
Calendar EventsYesYes
Reminders/TasksNoYes
Reminder #TagsNoYes (MCP-level)
Multi-keyword SearchNoYes
Duplicate DetectionNoYes
Conflict DetectionNoYes
Batch OperationsNoYes
Local TimezoneNoYes
Source DisambiguationNoYes
Create CalendarSomeYes
Delete CalendarSomeYes
Event RemindersSomeYes
Location & URLSomeYes
LanguagePythonSwift (Native)

All 29 Tools

<details> <summary><b>Calendars (4)</b></summary>
ToolDescription
list_calendarsList all calendars and reminder lists (includes source_type)
create_calendarCreate a new calendar
delete_calendarDelete a calendar
update_calendarRename a calendar or change its color (v0.9.0)
</details> <details> <summary><b>Events (4)</b></summary>
ToolDescription
list_eventsList events with filter/sort/limit (v1.0.0)
create_eventCreate an event (with reminders, location, URL, per-event timezone)
update_eventUpdate an event (including timezone, recurrence, span for recurring)
delete_eventDelete an event (with occurrence support for recurring)
</details> <details> <summary><b>Reminders (8)</b></summary>
ToolDescription
list_remindersList reminders with filter/sort/limit, tags extraction (v1.0.0)
create_reminderCreate a reminder with due date, tags (v1.3.0)
update_reminderUpdate a reminder (including tags, clear_due_date) (v1.3.0)
complete_reminderMark as completed/incomplete
delete_reminderDelete a reminder
search_remindersSearch reminders by keyword(s) or tag (v1.3.0)
list_reminder_tagsList all unique tags with usage counts (v1.3.0)
cleanup_completed_remindersDelete all completed reminders in one call, dry_run preview by default (v1.7.2)
</details> <details> <summary><b>Advanced Features (10)</b> ✨ New in v0.3.0+</summary>
ToolDescription
search_eventsSearch events by keyword(s) with AND/OR matching
list_events_quickQuick shortcuts: today, tomorrow, this_week, next_7_days, etc.
create_events_batchCreate multiple events at once (with per-event timezone)
check_conflictsCheck for overlapping events in a time range
copy_eventCopy an event to another calendar (with optional move)
move_events_batchMove multiple events to another calendar
delete_events_batchDelete events by IDs or date range, with dry-run preview (v1.0.0)
find_duplicate_eventsFind duplicate events across calendars (v0.5.0)
create_reminders_batchCreate multiple reminders at once (v0.9.0)
delete_reminders_batchDelete multiple reminders at once (v0.9.0)
</details> <details> <summary><b>Undo/Redo (3)</b> ✨ New in v1.4.0</summary>
ToolDescription
undoUndo the most recent calendar/reminder operation
redoRedo the last undone operation
undo_historyList undoable operations with timestamps
</details>

v1.0.0 Features

Flexible Date Parsing

All date parameters now accept 4 formats:

FormatExampleInterpretation
Full ISO8601"2026-02-06T14:00:00+08:00"Exact date and time (offset preserved)
Without timezone"2026-02-06T14:00:00"Uses event timezone if provided, otherwise system timezone
Date only"2026-02-06"Midnight in event timezone or system timezone
Time only"14:00"Today at that time

Per-Event Timezone (v1.5.0)

Set the display timezone for individual events β€” essential for multi-timezone travel itineraries.

Copy & paste β€” that's it
"Create a flight departure at 09:14 Berlin time"
β†’ create_event(title: "Flight LH123", start_time: "2026-04-08T09:14:00", timezone: "Europe/Berlin", ...)

"Update the hotel check-in to Dubai time"
β†’ update_event(event_id: "...", timezone: "Asia/Dubai")

"Remove the custom timezone from an event"
β†’ update_event(event_id: "...", clear_timezone: true)
  • timezone parameter accepts IANA identifiers (e.g., Europe/Berlin, America/New_York, Asia/Taipei)
  • When timezone is provided, naive datetimes (without offset) are interpreted in that timezone
  • Event output includes the event's own timezone in timezone field and formats start_date_local/end_date_local accordingly
  • Available on create_event, update_event, and create_events_batch
  • Undo/redo preserves per-event timezone

Attendees & Organizer (Read-Only)

Event responses include participant information when available. These fields are read-only due to EventKit limitations β€” they cannot be set or modified through the MCP.

Available in: list_events, search_events, list_events_quick, check_conflicts

attendees (array, optional) β€” Present when the event has participants. Each attendee object contains:

FieldTypeDescription
namestring or nullDisplay name, null if not in Address Book
emailstringEmail address extracted from participant URL
rolestringOne of: unknown, required, optional, chair, non_participant
statusstringOne of: unknown, pending, accepted, declined, tentative, delegated, completed, in_process
typestringOne of: unknown, person, room, resource, group
is_current_userbooleanWhether this participant is the current user

organizer (object, optional) β€” Present when the event has an organizer. Contains:

FieldTypeDescription
namestring or nullDisplay name
emailstringEmail address
is_current_userbooleanWhether the organizer is the current user

Note: Both fields are omitted when the event has no participants or organizer (e.g., local calendar events created without invitees).

Fuzzy Calendar Matching

Calendar names are now matched case-insensitively. If not found, the error message lists all available calendars.

Enhanced list/delete Tools

  • list_events: filter (all/past/future/all_day), sort (asc/desc), limit
  • list_reminders: filter (all/incomplete/completed/overdue), sort (due_date/creation_date/priority/title), limit
  • delete_events_batch: date range mode (before_date/after_date) + dry_run preview

Breaking Change: list_events and list_reminders now return {events/reminders: [...], metadata: {...}} instead of a plain array.


Supported Calendar Sources

Works with any calendar synced to macOS Calendar app:

  • iCloud Calendar
  • Google Calendar
  • Microsoft Outlook/Exchange
  • CalDAV calendars
  • Local calendars

Same-Name Calendar Disambiguation (v0.6.0+)

If you have calendars with the same name from different sources (e.g., "Work" in both iCloud and Google), use the calendar_source parameter:

Copy & paste β€” that's it
"Create an event in my iCloud Work calendar"
β†’ create_event(calendar_name: "Work", calendar_source: "iCloud", ...)

"Show events from my Google Work calendar"
β†’ list_events(calendar_name: "Work", calendar_source: "Google", ...)

If ambiguity is detected, the error message will list all available sources.


Technical Details

  • Current Version: v1.14.1
  • Framework: MCP Swift SDK v0.12.0
  • Calendar API: EventKit (native macOS framework)
  • Transport: stdio
  • Platform: macOS 14.0+ (Sonoma and later β€” bumped from 13.0 in the post-1.10 cluster per #119)
  • Tools: 29 tools for calendars, events, reminders, tags, undo/redo, cleanup, and advanced operations

Version History

VersionChanges
v1.14.1Metadata correction β€” tool-count consistency. server.json description said "24 tools" and PROMOTION.md said "20 tools"; the server actually exposes 29 tools (matching mcpb/manifest.json long_description and the ManifestParityTests tool-parity guard). Corrected the registry-facing server.json, docs/COMPETITIVE_ANALYSIS.md, and PROMOTION.md to 29. No code or tool-surface changes β€” functionally identical to v1.14.0; this release exists solely to publish corrected registry metadata (registry versions are immutable).
v1.14.0Claude Desktop tool-injection drop fixed (#166): a literal & in mcpb/manifest.json display_name made Desktop 1.18286.0 silently drop the whole 29-tool server from every conversation (Claude Code unaffected); changed & β†’ and, confirmed by single-variable intervention on the failing install + a ManifestParityTests regression guard. Also aligned serverInfo.name to the kebab manifest id (hygiene; empirically refuted as the cause). #154 sister batch: csreq-mismatch TCC drift signal (#155, SecCodeCheckValidity self-check for the silent-denial class), .mcpb denial message no longer dead-ends on --setup for the already-.denied signature (#158), macOS badge 13.0 β†’ 14.0 (#157), swift-nio 2.96 β†’ 2.101 (#159). 454 tests.
v1.13.0SwiftUI SetupWindow (#164): interactive --setup presents a live-status window (per-entity Grant buttons + resolved binary path) inside the #163 foreground NSApplication. Desktop Calendar-denied fix (#165): isNonInteractive misfired on TERM == nil for GUI-app-spawned servers β†’ fast-failed before requestFullAccess, so the first-grant dialog never appeared through Claude Desktop; now uses a CGSession GUI-session signal. 429 tests.
v1.12.0Foreground --setup (#163): interactive --setup now runs inside a foreground NSApplication so EventKit's Calendar TCC modal actually presents (previously silently denied from a bare CLI async context). Denial messages + startup banner surface the resolved binary path + a copy-pasteable "<path>" --setup command for the buried .mcpb binary.
v1.11.1create_event time-range validation (#160): symmetric with update_event β€” rejects inverted / zero-duration timed events via a shared validateTimeRange guard. 405 tests.
v1.11.0TCC healing re-prompt unblocked (#154): Entitlements.plist ships personal-information.calendars + .reminders β€” long-lived pre-v1.7.1 installs could hit silent permanent Calendar denial on macOS 26.5 (TCC row pinned to old cdhashes, healing re-prompt policy-blocked because the binary shipped no entitlements, every diagnostic reporting green); signed-binary release gate verifies both keys. Non-interactive EventKit hardening (#131 / #143 / #144 + #146–#150). BREAKING: deploy floor raised to macOS 14.0 (#119). 401 tests.
v1.10.0TCC drift detector + startup banner (#122): single-shot stderr banner at MCP-server startup with version/path/PID + drift signals (TCC.db path mismatch per-service, stale processes); opt-out via CHE_ICAL_MCP_NO_BANNER=1. Pipe-deadlock fix in subprocess helpers; CWE-117 stderr-injection defense on all interpolated banner values.
v1.9.0TCC access gate refactor (#108 Phase 2, closes #109): removed the process-lifetime has*Access cache anti-pattern; per-call EKEventStore.authorizationStatus(for:) via new AuthorizationGate + AuthorizationStatusSource seam (Apple TN3153 pattern) β€” state changes surface immediately instead of silent stale-grant fail. Adds --print-tcc-path diagnostic flag.
v1.8.1Docs: mcpb/README.md post-install / upgrade TCC permission setup guide (#108 Phase 1).
v1.8.0Wire-format consistency wave + response-shape parameters (#101 cluster β€” 5 issues closed in 3 days, all Refs #N IDD + 6-AI ensemble verify). Event listing response-shape params (#47 / #101): detail_level (summary/standard), fields allow-list, display_timezone (strict IANA), limit (cap 10000) β€” LLM verbosity tuning. Envelope unification (#102 / #107, breaking wire-format): list_events.metadata.returned + list_reminders.metadata.returned removed; all 5 list/search envelopes use top-level <entity>_count with pre-limit semantic; search_reminders.result_count β†’ reminder_count; search_reminders gains limit parameter (mirror search_events). MCP clients reading metadata.returned or result_count must update. Validator hardening (#101 F1–F3): requireOptionalInt uses Int(exactly:) closing the Int.max DoS trap; detail_level / display_timezone validators distinguish absent vs. non-string (no silent coerce). Runtime-anchored drift detection (#103, strengthening #101 M3): formatEventDict ↔ validEventFields divergence test now via EventFormattingSource seam + FakeFormattableEvent. CHANGELOG reclass (#106): wire-format renames moved from Fixed to Changed (Keep a Changelog 1.1.0). Release pipeline fix: pre-pack defense check now derives Team ID from DEVELOPER_ID cert (was comparing SHA hash against human-readable Authority= string).
v1.7.2Hardening + features wave (30+ commits over v1.7.1, all Refs #N IDD with 6-AI verify). --self-update (#49) + SHA-256 binary verify (#98): existing-install upgrade path with cryptographic guarantee against corrupted releases. make install-signed (#50): maintainer dev TCC flow on macOS 26 β€” fail-fast on missing Developer ID + force codesign verification. CI test workflow (#51): PR-time swift build + swift test on macos-latest. Sanitizer hardening cluster: escapeForStderr full C0+DEL coverage (#73), sanitizeForInterpolation for executeUndo/executeRedo title interpolation (#74), CLIRunner stderr delegated to writeFailureLog for trusted-branch carve-out (#80), writeFailureLog 1024-char DoS cap (#86), CLIError.invalidJSON author-controlled-only contract doc (#85), FileHandle.standardError.write thread-safety + macOS PIPE_BUF=512 documented (#70 / #94). Distribution polish: stale-codesign-cache install snippets get rm -f preamble (#90 zh-TW parity for #62). Post-v1.7.1 polish (#46 #57 #58 #60): redo error interpolation parity, build-mcpb.sh step renumbering, Entitlements.plist documentation, Makefile release-signed: cwd note. cleanup_completed_reminders tool (#21): single-call cleanup of all completed reminders, dry_run=true default.
v1.7.1Security hardening (#20 #26): input validation (length limits + URL scheme allowlist) at all event/reminder entry points, prompt-injection wrapper on MCP read responses, parse-boundary validation for days_of_week / days_of_month / alarms_minutes_offsets (throws instead of silent-dropping invalid values), Info.plist catch-up, 42 new regression tests.
v1.7.0Attendee & organizer info (#17): read-only attendees array and organizer object in event responses. Refactored shared formatEventDict method.
v1.6.0--setup flag (#13): pre-authorize TCC permissions for launchd/automation. Non-interactive session detection (TERM + ppid). Combined SSH+launchd error messages. --cli mode (#14): invoke all 28 tools directly from command line without MCP server. Flag-based (--key value) and JSON stdin modes. Smart type inference for bool/int/double/array params. MCP Swift SDK 0.12.0 (Swift 6.3 compat).
v1.5.0Per-event timezone (#12): timezone parameter on create_event/update_event/create_events_batch, event output uses event's own timezone, naive datetimes parsed in event timezone. Clear due date (#9): clear_due_date on update_reminder. Weekday validation (#5): create_event/update_event validate start_time weekday against days_of_week. Undo/redo (#8): 3 new tools (undo, redo, undo_history). Recurring event fixes (#7): occurrence-level delete/update with occurrence_date. Swift 6 build (#11): README updated for make release workflow
v1.4.0LLM reliability: Fix default search range (Β±2yr instead of distantPast/Future), searched_range metadata in search_events response, similar_events hints in create_events_batch, LLM tips in tool descriptions
v1.3.1Docs fix: Clarified that tags are MCP-level (not native Reminders.app tags); Apple provides no public API for native tags
v1.3.0Reminder tags (MCP-level): #hashtag text stored in notes for create_reminder/update_reminder/create_reminders_batch, tag-based filtering in search_reminders, new list_reminder_tags tool; MCP SDK 0.11.0. Note: tags are searchable via MCP but do not appear as native Reminders.app tags (Apple provides no public API for this)
v1.2.0Idempotent writes: create_event, create_events_batch, create_reminder, create_reminders_batch, create_calendar now check-before-write to prevent duplicates on retry; responses include skipped count
v1.1.0Recurrence + Location: recurring events/reminders (daily/weekly/monthly/yearly), structured locations with coordinates, location-based reminder triggers (geofence enter/leave), rich recurrence output
v1.0.0DX improvements: flexible date parsing (4 formats), fuzzy calendar matching, list_events/list_reminders filter/sort/limit, delete_events_batch dry-run + date range mode
v0.9.04 new tools (20β†’24): update_calendar, search_reminders, create_reminders_batch, delete_reminders_batch
v0.8.2i18n week support: week_starts_on parameter for list_events_quick (monday/sunday/saturday/system)
v0.8.1Fix: update_event time validation bug, duration preservation when moving events
v0.8.0BREAKING: calendar_name now required for create operations (no more implicit defaults)
v0.7.0Tool annotations for Anthropic Connectors Directory, auto-refresh mechanism, improved batch tool descriptions
v0.6.0Source disambiguation: calendar_source parameter for same-name calendars
v0.5.0Batch delete, duplicate detection, multi-keyword search, improved permission errors, PRIVACY.md
v0.4.0Copy/move events: copy_event, move_events_batch
v0.3.0Advanced features: search, quick range, batch create, conflict check, timezone display
v0.2.0Swift rewrite with full Reminders support
v0.1.xPython version (deprecated)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Release Process (for maintainers)

Version numbers live in three places with different semantics:

FileRoleWhen to bump
Sources/CheICalMCP/Version.swift β€” AppVersion.currentSource of truth; appears in --version, help, and MCP serverInfo.versionEvery release
Sources/CheICalMCP/Info.plist β€” CFBundleVersionmacOS bundle versionEvery release; must match AppVersion.current
mcpb/manifest.json β€” versionClaude Desktop bundle manifest shipped inside .mcpbEvery release; must match AppVersion.current
server.json β€” version + packages[].identifier + fileSha256MCP Registry submission snapshotOnly when re-submitting a new .mcpb to the MCP Registry (independent cadence)

scripts/build-mcpb.sh enforces the first three match; it will fail the build if any drifts. server.json is intentionally decoupled because bumping it requires a rebuilt .mcpb, a fresh SHA256, and a re-submission β€” steps that don't happen every source release.

Signing & Notarization (required for macOS 26+)

Starting v1.7.1, release binaries are signed with a Developer ID Application certificate and notarized via Apple's notarytool. This is required on macOS 26 β€” ad-hoc signed binaries cannot trigger Calendar / Reminders TCC permission dialogs there.

Prerequisites (one-time setup):

  1. Apple Developer Program enrollment.
  2. Developer ID Application certificate installed in login keychain.
    • Verify with: security find-identity -p codesigning -v (must show Developer ID Application: <Your Name> (<TeamID>)).
    • Your Team ID is your own β€” find it at https://developer.apple.com/account β†’ Membership Details. (The maintainer's 6W377FS7BS shown anywhere in this repo is for reference only.)
  3. notarytool keychain profile (any name; che-ical-mcp is the default the build script looks for).
    • Create interactively (recommended β€” keeps password out of shell history):
      Copy & paste β€” that's it
      xcrun notarytool store-credentials che-ical-mcp --apple-id <your-apple-id> --team-id <your-team-id>
      # notarytool will prompt for the app-specific password
    • App-specific password: generate at https://account.apple.com β†’ Sign-In and Security β†’ App-Specific Passwords. Use a single-purpose password (e.g. named che-ical-mcp); revoke + regenerate if leaked. Never pass it via --password on the command line β€” it lands in ~/.zsh_history.
  4. Export your identity for the build script:
    Copy & paste β€” that's it
    export DEVELOPER_ID='Developer ID Application: <Your Name> (<TeamID>)'
    export NOTARY_PROFILE='che-ical-mcp'   # match what you set up in step 3
    Persist these in ~/.zshrc or a project-local .envrc (gitignored). The script intentionally has no defaults for these, so a fresh fork doesn't fail with errors referring to the maintainer's identity.

Per-release flow:

Copy & paste β€” that's it
make release-signed     # builds universal binary β†’ signs + notarizes β†’ packages .mcpb
gh release create vX.Y.Z mcpb/server/CheICalMCP mcpb/server/CheICalMCP.sha256 mcpb/che-ical-mcp-X.Y.Z.mcpb mcpb/che-ical-mcp-X.Y.Z.mcpb.sha256 --notes "..."

make release-signed runs scripts/build-mcpb.sh, which after creating the universal binary calls scripts/sign-and-notarize.sh. The signing script does pre-flight checks (cert + notarytool profile) and fails fast with friendly messages if anything's missing. Notarization typically takes 1–15 minutes (notarytool submit --wait blocks until Apple finishes).

Verification after build (run all three to confirm end-to-end):

Copy & paste β€” that's it
# 1. Signature properties (cert + hardened runtime + team ID)
codesign -dv --verbose=2 mcpb/server/CheICalMCP
# Expected:
#   Authority=Developer ID Application: <Your Name> (<TeamID>)
#   TeamIdentifier=<TeamID>
#   flags=0x10000(runtime)
#   Signature size in the few thousand bytes range (varies by cert chain)

# 2. Signature integrity
codesign --verify --deep --strict --verbose=2 mcpb/server/CheICalMCP
# Expected: exit 0, no warnings

# 3. Notarization end-to-end (this is the real "Gatekeeper would accept" gate)
spctl -a -vvv -t install mcpb/server/CheICalMCP
# Expected: <binary>: accepted; source=Notarized Developer ID
#
# Note on flag choice (verified empirically on macOS 26.4.1, 2026-05-04):
#   -t execute β†’ rejected "code is valid but does not seem to be an app"
#                (Apple's "execute" type expects a .app bundle structure,
#                 not raw Mach-O CLI binaries)
#   -t install β†’ accepted; source=Notarized Developer ID  ← use this
#   -t open    β†’ rejected "Insufficient Context"
#
# Apple's Code Signing Guide describes -t execute as the assessment type for
# "applications and tools", but on macOS 26 raw Mach-O binaries fall through
# the .app bundle check. -t install is the documented assessment type for
# software being installed (which describes how a CLI binary lands in ~/bin),
# and is the type that returns the actual notarization verdict in practice.
# Re-test if Apple changes this behavior in a future macOS update.

Local dev iteration without signing latency:

Copy & paste β€” that's it
SKIP_CODESIGN=1 ./scripts/build-mcpb.sh   # ad-hoc signed; do NOT ship the result
make install                              # installs ad-hoc to ~/bin (dev only)

The build-mcpb.sh script also auto-skips signing when DEVELOPER_ID is unset OR the cert isn't in your keychain β€” so contributors / CI / forks can build a working unsigned .mcpb for testing without manually setting SKIP_CODESIGN. (You'll see a clear "Skipping codesign" warning when this happens.)

Signing identity environment:

Env varDefaultRequired for
DEVELOPER_ID(unset β€” auto-skip signing)Signed release
NOTARY_PROFILE(unset β€” fail-fast in sign-and-notarize.sh)Signed release
ENTITLEMENTSSources/CheICalMCP/Entitlements.plistCustom entitlements file
SKIP_CODESIGN(unset)Force-skip signing even with cert present (set to 1 or true)
REQUIRE_CODESIGN(unset)Fail-fast if signing prerequisites missing (set to 1 by make release-signed β€” canonical release path must not silently produce unsigned artifacts; do not set when running ./scripts/build-mcpb.sh directly for fork-friendly dev builds)

Known limitation β€” no stapling: stapler staple does not support raw Mach-O binaries (only .app / .pkg / .dmg bundles). After notarization, Gatekeeper will online-check the binary on first launch instead of reading a stapled ticket. End users behind air-gapped networks may see "cannot verify developer" warnings; one launch with network resolves it (Apple caches the verdict). Mitigation: xcrun stapler staple on a future .pkg wrapper if needed.

Troubleshooting:

  • Notarization rejected? xcrun notarytool log <submission-id> --keychain-profile $NOTARY_PROFILE shows Apple's reason. The signing script prints the submission ID on every run.
  • codesign complains about missing identity? security find-identity -p codesigning -v to confirm cert is present + valid; xcrun notarytool history --keychain-profile $NOTARY_PROFILE to confirm the profile works.
  • Cert expired? Re-issue at https://developer.apple.com/account/resources/certificates, install, re-export DEVELOPER_ID.
  • Security warning: don't unlock signing keychain on shared / untrusted machines. The cert + private key signing artifact is supply-chain critical.

License

MIT License - see LICENSE for details.


Author

Created by Che Cheng (@kiki830621)

If you find this useful, please consider giving it a star!