iCloud Calendar MCP Server

An MCP (Model Context Protocol) server that gives AI assistants access to iCloud Calendar via CalDAV, with security controls aligned with the OWASP MCP Top 10.
[!CAUTION]
Never use your main Apple ID password. This server requires an app-specific password which can be revoked independently without affecting your Apple ID.
Why this server
- Portable CalDAV. Runs anywhere a JVM runs. It does not depend on macOS, AppleScript, or Calendar.app.
- Durable event handles.
get_events and create_event return an opaque handle that references an event across sessions and process restarts, so editing or deleting one needs no re-listing.
- Correct recurrence. A series expands into one result per occurrence in the range, and you can edit or delete a single occurrence, this-and-future, or the whole series.
- Bounded responses. Date-span and event-count caps return a clear, structured error instead of a silently truncated response.
- Security aligned with the OWASP MCP Top 10, backed by a dedicated test suite.
- Published on the MCP Registry, npm, and PyPI.
Features
MCP Tools
| Tool | Description | Read-Only | Destructive |
|---|
list_calendars | List all calendars from iCloud account | Yes | No |
get_events | Get events within a date range from a calendar | Yes | No |
create_event | Create a new calendar event | No | No |
update_event | Update an event (whole series or a single occurrence) | No | No |
delete_event | Delete an event (whole series or a single occurrence) | No | Yes |
MCP Resources
| Resource | Description |
|---|
calendar://calendars | Browse available calendars |
MCP Prompts
User-initiated templates that guide Claude through a multi-step task:
| Prompt | Description |
|---|
schedule_meeting | Draft an event from a title, attendees, and duration, then create it |
reschedule | Move an existing event by looking it up, then editing it |
find_conflicts | List a day's events and report overlapping time slots |
Security Features
- Credential Protection - Environment variables only, never in code or config
- Input Validation - All parameters validated with SSRF protection
- Rate Limiting - 60 reads/min, 20 writes/min per MCP specification
- Secure Error Handling - No sensitive data leakage in error messages
- OWASP MCP Top 10 Compliance - 282 security tests covering all major risks
- ReDoS Protection - All regex patterns tested against catastrophic backtracking
- Unicode Security - Protection against homoglyph and encoding attacks
Quick Start
Prerequisites
Installation
Choose your preferred installation method:
[!IMPORTANT]
Every option runs the same Java build. Java 21 or newer must be on your PATH, including for npx and uvx. Those wrappers download and launch the JAR; they do not replace the JVM.
Option 1: npm (Recommended)
npx @icloud-calendar-mcp/server
Option 2: Python (uvx)
Option 3: Direct JAR
# Download the latest release (version-agnostic name, always resolves)
curl -LO https://github.com/icloud-calendar-mcp/icloud-calendar-mcp/releases/latest/download/icloud-calendar-mcp-all.jar
# Run
java -jar icloud-calendar-mcp-all.jar
Option 4: Build from Source
git clone https://github.com/icloud-calendar-mcp/icloud-calendar-mcp.git
cd icloud-calendar-mcp
./gradlew fatJar
java -jar build/libs/icloud-calendar-mcp-*-all.jar
Configuration
Set your iCloud credentials as environment variables:
export ICLOUD_USERNAME="your-apple-id@icloud.com"
export ICLOUD_PASSWORD="your-app-specific-password"
Security Note: Use an app-specific password, not your main Apple ID password.
Claude Desktop Integration
Add to your Claude Desktop configuration:
| Platform | Config Path |
|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Linux | ~/.config/claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
Using npm (Recommended)
{
"mcpServers": {
"icloud-calendar": {
"command": "npx",
"args": ["@icloud-calendar-mcp/server"],
"env": {
"ICLOUD_USERNAME": "your-apple-id@icloud.com",
"ICLOUD_PASSWORD": "your-app-specific-password"
}
}
}
}
Using uvx (Python)
{
"mcpServers": {
"icloud-calendar": {
"command": "uvx",
"args": ["icloud-calendar-mcp"],
"env": {
"ICLOUD_USERNAME": "your-apple-id@icloud.com",
"ICLOUD_PASSWORD": "your-app-specific-password"
}
}
}
}
Using JAR directly
{
"mcpServers": {
"icloud-calendar": {
"command": "java",
"args": ["-jar", "/path/to/icloud-calendar-mcp-all.jar"],
"env": {
"ICLOUD_USERNAME": "your-apple-id@icloud.com",
"ICLOUD_PASSWORD": "your-app-specific-password"
}
}
}
}
Usage Examples
Once configured, you can ask Claude:
- "What's on my calendar this week?"
- "Create a meeting with John tomorrow at 2pm"
- "Show me all my calendars"
- "Delete the dentist appointment on Friday"
- "Move my 3pm meeting to 4pm"
Tool Parameters
list_calendars
No parameters required.
get_events
| Parameter | Type | Required | Description |
|---|
calendar_id | string | Yes | Calendar identifier (from list_calendars) |
start_date | string | Yes | Start date (YYYY-MM-DD) |
end_date | string | Yes | End date (YYYY-MM-DD) |
Behavior to know about:
- UTC day boundaries.
start_date and end_date select whole UTC calendar days: start_date at 00:00 UTC through the end of end_date in UTC. Timed events return UTC startTime/endTime instants; all-day events return a plain YYYY-MM-DD. To resolve a user's day in another timezone, request one extra day on each side and keep the events whose start, converted to that zone, falls on the wanted day; all-day events are floating dates and need no conversion.
- Limits. The range is capped at 366 days, and
end_date must not precede start_date. The response is capped at 1000 events, and a single recurring series that expands to too many occurrences is rejected. If you hit a cap, query a week or a month at a time.
- Recurring series. Each occurrence in the range is returned as its own result, carrying its own
handle and a recurrenceId that identifies the instance.
- Read-after-write. iCloud does not guarantee immediate visibility, so an event created moments ago can be missing from the next
get_events for a short window. This is CDN indexing lag, not a deletion, so do not recreate it.
Each result includes uid, handle, summary, isAllDay, and startTime/endTime (timed) or startDate/endDate (all-day), plus any of description, location, rrule, recurrenceId, status, url, categories, priority, organizer, attendeeCount that are set.
create_event
| Parameter | Type | Required | Description |
|---|
calendar_id | string | Yes | Target calendar |
title | string | Yes | Event title |
start_time | string | Cond. | ISO 8601 datetime for a timed event. A naive value (2026-01-15T09:00:00) is read as UTC unless timezone is set; a Z or offset value is an absolute instant that overrides timezone |
end_time | string | Cond. | ISO 8601 datetime for a timed event (same rules as start_time) |
start_date | string | Cond. | Start date YYYY-MM-DD for an all-day event |
end_date | string | Cond. | End date YYYY-MM-DD, inclusive, for an all-day event |
is_all_day | boolean | No | All-day event flag |
description | string | No | Event description |
location | string | No | Event location |
timezone | string | No | IANA timezone for a timed event (e.g., America/New_York) |
end_timezone | string | No | IANA timezone for the end when it differs from the start (e.g., a flight). Falls back to timezone |
rrule | string | No | Recurrence rule (e.g., FREQ=WEEKLY;BYDAY=MO) |
rdates | string[] | No | Extra occurrence dates (RFC 5545 RDATE) |
exdates | string[] | No | Excluded occurrence dates (RFC 5545 EXDATE) |
alarms | object[] | No | Reminders on the event (see Alarms) |
update_event
Only the fields you pass are changed.