Icloud Calendar
FreeMaintainedMCP server for iCloud (Apple) Calendar access via CalDAV
About
MCP server for iCloud (Apple) Calendar access via CalDAV
README
iCloud Calendar MCP Server
Tests npm PyPI License MCP Registry Security
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_eventsandcreate_eventreturn an opaquehandlethat 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
- Java 21+ (for all installation methods)
- iCloud account with app-specific password
Installation
Choose your preferred installation method:
[!IMPORTANT] Every option runs the same Java build. Java 21 or newer must be on your
PATH, including fornpxanduvx. 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)
uvx icloud-calendar-mcp
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="[email protected]"
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": "[email protected]",
"ICLOUD_PASSWORD": "your-app-specific-password"
}
}
}
}
Using uvx (Python)
{
"mcpServers": {
"icloud-calendar": {
"command": "uvx",
"args": ["icloud-calendar-mcp"],
"env": {
"ICLOUD_USERNAME": "[email protected]",
"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": "[email protected]",
"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_dateandend_dateselect whole UTC calendar days:start_dateat 00:00 UTC through the end ofend_datein UTC. Timed events return UTCstartTime/endTimeinstants; all-day events return a plainYYYY-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_datemust not precedestart_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
handleand arecurrenceIdthat 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_eventsfor 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
event_id |
string | Yes | Reference to the event. Prefer the opaque handle from get_events/create_event; a bare UID also works for an event fetched earlier in the session (see Referencing an event) |
title |
string | No | New title |
start_time |
string | No | New start time (ISO 8601, same rules as create) |
end_time |
string | No | New end time (ISO 8601) |
start_date |
string | No | New start date for an all-day event (YYYY-MM-DD) |
end_date |
string | No | New end date for an all-day event (YYYY-MM-DD) |
is_all_day |
boolean | No | Change to all-day event |
description |
string | No | New description |
location |
string | No | New location |
timezone |
string | No | IANA timezone (e.g., America/New_York) |
end_timezone |
string | No | IANA timezone for the end when it differs from the start |
rrule |
string | No | Recurrence rule |
rdates |
string[] | No | Replace RDATEs (omit to keep, empty array to clear) |
exdates |
string[] | No | Replace EXDATEs (omit to keep, empty array to clear) |
alarms |
object[] | No | Replace reminders (omit to keep, empty array to clear, a list to replace; see Alarms) |
scope |
string | Cond. | Which occurrences a recurring edit affects (see Editing recurring events). Required when the handle points at one occurrence of a series |
delete_event
| Parameter | Type | Required | Description |
|---|---|---|---|
event_id |
string | Yes | Reference to the event. Prefer the opaque handle; a bare UID also works for an event fetched earlier in the session |
scope |
string | Cond. | Which occurrences a recurring delete removes (see Editing recurring events). Required when the handle points at one occurrence of a series |
Referencing an event
get_events and create_event return two identifiers for each event: a uid and an opaque handle. Pass the handle to update_event and delete_event. It is self-contained and works from a fresh process, so the normal flow is get_events (or create_event) to obtain the handle, then update_event/delete_event with it. No extra lookup step is needed across sessions.
A bare uid is also accepted, but only for an event fetched earlier in the same session, where it resolves through a short-lived in-memory cache. There is no stateless server-side lookup by UID: iCloud rejects a CalDAV calendar-query UID prop-filter (HTTP 412), and an unfiltered query would return the whole calendar. Prefer the handle.
A handle carries the event's ETag. If the event changed elsewhere since the handle was issued, the edit reports a conflict rather than overwriting the newer version; re-run get_events for a fresh handle and retry. On success, update_event returns a refreshed handle carrying the new ETag, so use that one for the next edit in a chain.
Editing recurring events
get_events returns one result per occurrence of a recurring series, each with its own handle. When you edit or delete an occurrence handle, set scope:
this_occurrence: change or cancel only that instance.this_and_future: that instance and every later one.all_events: the whole series.
scope is required when the handle points at one occurrence of a series. The operation is rejected without it, so a single-occurrence edit never changes the whole series by accident. Omit scope for standalone events. rrule, rdates, and exdates cannot be combined with this_occurrence or this_and_future.
Alarms
create_event and update_event take an alarms array. Each entry is an object:
| Field | Required | Description |
|---|---|---|
trigger |
Yes | Relative duration (-PT15M, -P1D) or an absolute UTC instant (20260115T093000Z) |
action |
No | DISPLAY (default), AUDIO, or EMAIL |
description |
No | Alarm text (defaults to "Reminder" for DISPLAY) |
summary |
No | Subject line, EMAIL only |
repeat_count |
No | Number of times to repeat |
repeat_duration |
No | Gap between repeats (RFC 5545 duration) |
On update_event, omit alarms to keep the existing ones, pass an empty array to clear them, or pass a list to replace them.
Troubleshooting
A new event does not appear right away. iCloud does not guarantee immediate visibility, so a just-created event can be missing from the next get_events for a short window (CDN indexing lag). The create_event success response is authoritative. Do not recreate the event.
get_events returns a size or count error. The range is too wide. Query a week or a month at a time; the response is capped at 1000 events.
Authentication fails. Use an app-specific password, not your Apple ID password, and set ICLOUD_USERNAME to your full iCloud email address.
Not sure which calendar_id to use. Call list_calendars and use the id of the calendar you want.
A recurring edit was rejected or changed every instance. Set scope (see Editing recurring events): an occurrence handle needs this_occurrence, this_and_future, or all_events.
Times look shifted by your timezone. get_events uses UTC day boundaries (see get_events). Pass timezone when creating timed events, and use the extra-day approach to resolve a local day.
Security
This server is designed with security as a primary concern, following the OWASP MCP Top 10 guidelines.
Privacy
The server talks only to your machine (over STDIO) and to iCloud (caldav.icloud.com). It has no telemetry and sends your calendar data nowhere else. Credentials come from environment variables and are never logged.
Security Controls
| Control | Implementation |
|---|---|
| Credential Storage | Environment variables only, never logged or exposed |
| Input Validation | All inputs validated (calendar IDs, dates, times, text fields) |
| SSRF Protection | Blocks internal IPs, localhost, and dangerous URI schemes |
| Rate Limiting | Sliding window: 60 reads/min, 20 writes/min |
| Error Handling | Passwords, tokens, paths, emails sanitized from errors |
| Injection Prevention | ICS content properly escaped, command injection tested |
| ETag Normalization | RFC 7232 compliant, strips quotes/W/ prefix/XML entities |
| Content-Length Guard | Early rejection of oversized responses before buffering |
| Circuit Breaker | Prevents cascading failures with automatic recovery |
| Audit Logging | CUD operations logged via MCP logging protocol (MCP08) |
| ReDoS Protection | All regex patterns tested for catastrophic backtracking |
| Unicode Security | Homoglyph, normalization, and encoding bypass protection |
OWASP MCP Top 10 Coverage
| Risk | Mitigation | Tests |
|---|---|---|
| MCP01: Token Mismanagement | Credentials masked in logs/errors, secure storage | 14 |
| MCP02: Privilege Escalation | Fixed tool set, no dynamic registration | 5 |
| MCP03: Tool Argument Injection | Input validation, parameterized operations | 8 |
| MCP04: Sensitive Data Exposure | Error sanitization, credential masking | 10 |
| MCP05: Command Injection | Input treated as data, not executed | 3 |
| MCP06: Prompt Injection | Malicious text stored as data, not interpreted | 3 |
| MCP08: Insecure Logging | Rate limiting, sensitive data sanitization | 31 |
| MCP09: Resource Exhaustion | Rate limiting, input size limits, DoS protection | 25 |
| MCP10: Context Over-sharing | Isolated state, no cross-request data leakage | 3 |
See SECURITY.md for full security documentation and vulnerability disclosure process.
Testing
Tests live in two places: the MCP server module and the vendored icaldav-core
iCalendar library. Both run with:
./gradlew test
Test Coverage (MCP server)
| Category | Tests | Description |
|---|---|---|
| Security | 282 | Adversarial inputs, OWASP MCP Top 10, ReDoS, Unicode |
| CalDAV Protocol | 181 | XML parsing, HTTP client, models, ETag normalization |
| ICS Format | 150 | RFC 5545 parsing, building, patching |
| Error Handling | 56 | Secure error responses, credential sanitization |
| Integration | 45 | End-to-end tools, MCP spec compliance, annotations |
| Input Validation | 44 | All parameter validation rules |
| Service Layer | 26 | Calendar operations, caching |
| Rate Limiting | 18 | Concurrent access, window reset |
| Cancellation | 12 | Operation cancellation, cleanup |
| Logging | 9 | MCP logging compliance |
| Progress | 9 | Progress reporting |
| E2E | 11 | Live CalDAV + end-to-end integration |
Security Test Categories
| Category | Tests | Coverage |
|---|---|---|
| Adversarial Inputs | 53 | SQL/NoSQL injection, XSS, path traversal |
| ICS Patcher Security | 43 | CRLF injection, property injection, encoding attacks |
| Unicode Security | 38 | Homoglyphs, normalization, RTL override |
| Logger Security | 31 | Log injection, credential sanitization |
| OWASP MCP Risks | 29 | MCP01-10 specific attack vectors |
| Progress Security | 27 | Token enumeration, injection |
| ReDoS Protection | 25 | Catastrophic backtracking, resource exhaustion |
| Cancellation Security | 22 | Replay attacks, race conditions |
| Credential Security | 14 | Token masking, secure storage |
Running Specific Tests
# All tests
./gradlew test
# Security tests only
./gradlew test --tests "*SecurityTest*"
./gradlew test --tests "AdversarialTest"
# OWASP MCP specific tests
./gradlew test --tests "OwaspMcpSecurityTest"
# Unicode security tests
./gradlew test --tests "UnicodeSecurityTest"
# ReDoS protection tests
./gradlew test --tests "ReDoSSecurityTest"
# CalDAV tests
./gradlew test --tests "*CalDav*"
# ICS tests
./gradlew test --tests "*Ics*"
Architecture
+------------------------------------------------------------------+
| MCP Server (STDIO Transport) |
| |
| +----------------+ +----------------+ +----------------------+ |
| | Rate Limiter | | Input | | Secure Error | |
| | 60r/20w/min | | Validator | | Handler | |
| +----------------+ +----------------+ +----------------------+ |
| |
| +----------------+ +----------------+ +----------------------+ |
| | MCP Logger | | Cancellation | | Progress | |
| | (RFC 5424) | | Manager | | Reporter | |
| +----------------+ +----------------+ +----------------------+ |
| |
| Tools: list_calendars | get_events | create_event | |
| update_event | delete_event |
| |
| Resources: calendar://calendars |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| CalendarService |
| Orchestrates CalDAV operations, caches calendar metadata |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| CalDAV Client Layer |
| |
| +-------------------+ +-------------------+ +----------------+ |
| | OkHttpCalDav | | IcsParser | | IcsBuilder | |
| | Client | | (icaldav-core) | | (icaldav-core)| |
| +-------------------+ +-------------------+ +----------------+ |
| |
| +-------------------+ +-------------------+ +----------------+ |
| | ICloudXml | | IcsPatcher | | EtagUtils | |
| | Parser | | (event edits) | | (RFC 7232) | |
| +-------------------+ +-------------------+ +----------------+ |
| |
| +-------------------+ |
| | Credential | |
| | Manager | |
| +-------------------+ |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| iCloud CalDAV API |
| caldav.icloud.com |
+------------------------------------------------------------------+
Development
Build
# Build
./gradlew build
# Build fat JAR
./gradlew fatJar
# Run tests
./gradlew test
# Clean build
./gradlew clean build
Project Structure
src/main/kotlin/org/onekash/mcp/calendar/
├── Main.kt # MCP server entry point
├── caldav/ # CalDAV protocol implementation
│ ├── CalDavClient.kt # Client interface
│ ├── CalDavModels.kt # Domain models
│ ├── OkHttpCalDavClient.kt
│ ├── ICloudXmlParser.kt
│ └── EtagUtils.kt # RFC 7232 ETag normalization
├── ics/ # ICS format handling (via icaldav-core)
│ ├── IcsParser.kt # Parse iCalendar data
│ ├── IcsBuilder.kt # Generate iCalendar data
│ └── IcsPatcher.kt # Patch existing events (CRLF-safe)
├── service/ # Business logic
│ └── CalendarService.kt # CalDAV orchestration + event cache
├── security/ # Security controls
│ └── CredentialManager.kt
├── validation/ # Input validation
│ └── InputValidator.kt
├── error/ # Error handling
│ └── SecureErrorHandler.kt
├── ratelimit/ # Rate limiting
│ └── RateLimiter.kt
├── logging/ # MCP logging
│ └── McpLogger.kt
├── progress/ # Progress reporting
│ └── ProgressReporter.kt
└── cancellation/ # Operation cancellation
└── CancellationManager.kt
Testing with MCP Inspector
ICLOUD_USERNAME="[email protected]" \
ICLOUD_PASSWORD="test-app-password" \
npx @mcp-use/inspector java -jar build/libs/icloud-calendar-mcp-*-all.jar
Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Security Issues
For security vulnerabilities, please see SECURITY.md for our responsible disclosure process. Do not open public issues for security vulnerabilities.
License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Acknowledgments
- Model Context Protocol by Anthropic
- MCP Kotlin SDK by Anthropic & JetBrains
- ical4j for low-level ICS parsing (via the bundled icaldav-core library)
- OkHttp for HTTP client
- OWASP MCP Top 10 for security guidance
Install Icloud Calendar in Claude Desktop, Claude Code & Cursor
unyly install icloud-calendar-mcpInstalls into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.
First time? Get the CLI: curl -fsSL https://unyly.org/install | sh
Or configure manually
Run in your terminal:
claude mcp add icloud-calendar-mcp --env ICLOUD_PASSWORD="" --env ICLOUD_USERNAME="" -- npx -y @icloud-calendar-mcp/serverStep-by-step: how to install Icloud Calendar
FAQ
Is Icloud Calendar MCP free?
Yes, Icloud Calendar MCP is free — one-click install via Unyly at no cost.
Does Icloud Calendar need an API key?
Yes, it requires environment variables: ICLOUD_PASSWORD, ICLOUD_USERNAME. Unyly injects them into the config during install.
Is Icloud Calendar hosted or self-hosted?
A hosted option is available: Unyly runs the server in the cloud, no local setup required.
How do I install Icloud Calendar in Claude Desktop, Claude Code or Cursor?
Open Icloud Calendar on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Changes
Versions and requested access over time.
- New version published
- New version published
Related MCPs
Notion
Read and write pages in your workspace
by NotionLinear
Issues, cycles, triage — from Claude
by LinearGoogle Drive
Search and read your Drive files
by Googlemindsdb/mindsdb
Connect and unify data across various platforms and databases with [MindsDB as a single MCP server](https://docs.mindsdb.com/mcp/overview).
by mindsdbfulcradynamics/fulcra-context-mcp
MCP server for accessing personal health and biometric data including sleep stages, heart rate, HRV, glucose, workouts, calendar, and location via the Fulcra Li
by fulcradynamicsaymericzip/intlayer
A MCP Server that enhance your IDE with AI-powered assistance for Intlayer i18n / CMS tool: smart CLI access, access to the docs.
by aymericziprinadelph/Agent-MCP
A framework for creating multi-agent systems using MCP for coordinated AI collaboration, featuring task management, shared context, and RAG capabilities.
by rinadelphWhenLabs-org/when
Developer toolkit: auto-detect stack for AI context files, catch port conflicts, validate .env schemas, spot docs drift, audit dependency licenses, and time cod
by WhenLabs-orgBeltran12138/wecom-docs-mcp-server
WeCom (Enterprise WeChat) document operations via MCP: create, read, and edit Docs and Smartsheets (9 tools). Fills the doc-CRUD gap — existing WeCom MCP server
by Beltran12138madbonez/caldav-mcp
Universal MCP server for CalDAV protocol integration. Works with any CalDAV-compatible calendar server including Yandex Calendar, Google Calendar (via CalDAV),
by madbonezCompare Icloud Calendar with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All productivity MCPs
