CalDAV MCP Server

io.github.lukegskwPublished by GitHub account lukegskwv0.2.0Updated Oct 30, 2026

A TypeScript MCP server for iCloud Calendar with native multi-VALARM support.

Overview

AI-generated overview

MCP server that connects one iCloud Calendar account over CalDAV, letting an assistant list calendars and read, create, update and delete events.

What you can do
  • List the calendars in the configured iCloud account and get their opaque calendar IDs
  • List events in a time range, expanding recurring series into occurrences, with paging
  • Read a single event by resource ID or by calendar ID and UID
  • Create timed, all-day and recurring events with multiple display alarms and optional Apple travel time
  • Patch or delete an event or a whole recurring series, optionally guarded by an ETag
When to use it
Useful when you want an assistant to answer questions about your iCloud calendar or to add, change and remove appointments and reminders from a chat client, including recurring events and multi-alarm setups.
Requirements
A local runtime: Node.js 22 or newer for the npm package, or Docker for the published container image; Claude Desktop can use the one-click bundle. An iCloud account with Calendar and two-factor authentication enabled, plus an Apple app-specific password supplied through CALDAV_USERNAME and CALDAV_PASSWORD. Network access to iCloud. One account per process.
Before you install
The server needs an Apple app-specific password in CALDAV_PASSWORD; keep the client configuration file private and do not pass credentials as tool arguments. Create, update and delete change the remote calendar, and deletion is permanent. The optional Streamable HTTP endpoint has no authentication, so restrict it to a trusted network. Raw iCalendar can be requested and may contain sensitive calendar data.

Installation

In SourceWeft

  1. Open CalDAV MCP Server in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.

Other MCP clients

Follow the launch instructions in the repository.

README

CalDAV MCP Server

[TypeScript] [CI] [npm] [npm downloads] [Container] [MCP Registry] [License]

CalDAV MCP Server is a Model Context Protocol server for managing iCloud Calendar events, including native support for multiple VALARM reminders on one event.

iCloud Calendar is the only provider officially supported and manually validated in the first release. The server works with any MCP client that supports stdio or Streamable HTTP.

This independent project is not affiliated with, authorized, sponsored, or approved by Apple Inc. Apple and iCloud are trademarks of their respective owner.

Quick start

Create an Apple app-specific password, then pick the option for your client.

Claude Desktop (one click, no Node.js install): download caldav-mcp.mcpb, open it with Claude Desktop, and enter your Apple Account email and app-specific password. See Claude Desktop extension.

Any other stdio client: install Node.js 22 or newer and add this server to a JSON-configured MCP client such as Gemini CLI or Claude Code:

json
{  "mcpServers": {    "icloud-calendar": {      "command": "npx",      "args": ["--yes", "@lukegskw/caldav-mcp@latest"],      "env": {        "CALDAV_USERNAME": "[email protected]",        "CALDAV_PASSWORD": "xxxx-xxxx-xxxx-xxxx"      }    }  }}

Restart the client and confirm that it lists six calendar tools. See client-specific setup and Docker deployment below. Keep the configuration file private because it contains the app-specific password.

Navigation

About

The server connects one configured account to iCloud through CalDAV. It discovers the account's calendars and exposes normalized read and write operations through MCP.

Updates preserve the complete iCalendar resource, including unknown properties, Apple extensions, VTIMEZONE, recurrence exceptions, and alarms omitted from a patch. Writes use opaque resource identifiers and ETags instead of assuming that a CalDAV filename matches an event UID.

Calendar resources are processed in memory. The server has no telemetry and no application database, and raw iCalendar is returned only when explicitly requested.

Features

  • Discovers calendars available to the configured iCloud account.
  • Lists events in semi-open time ranges and expands recurring occurrences.
  • Creates timed, all-day, and recurring events.
  • Supports zero, one, or multiple display alarms per event.
  • Reads and writes Apple Calendar travel time (X-APPLE-TRAVEL-DURATION).
  • Emits the Apple alarm extensions expected by iCloud Calendar.
  • Reads events by opaque resource ID or by calendar ID and UID.
  • Applies partial updates while preserving omitted and unknown iCalendar data.
  • Uses ETags for optimistic concurrency on updates and deletions.
  • Rejects isolated recurrence mutations instead of changing the complete series.
  • Redacts credentials, raw calendar content, and CalDAV paths from logs and errors.
  • Runs as a non-root container with a read-only root filesystem configuration.
  • Supports stdio and Streamable HTTP MCP transports.

MCP tools

list_calendars

Lists the calendars discovered for the configured account. Each result includes an opaque calendar_id, display name, description, timezone, and best-effort write status.

list_events

Lists events in a semi-open interval and expands recurring occurrences. The maximum range is 366 days, the default page size is 100, and the maximum page size is 500. Results use a deterministic chronological order. Pagination cursors are opaque and do not represent a snapshot when events are modified during traversal.

Example input:

json
{  "calendar_id": "opaque-calendar-id",  "start": "2026-09-01T00:00:00Z",  "end": "2026-10-01T00:00:00Z",  "timezone": "Europe/Berlin",  "limit": 100}

get_event

Reads an event by resource_id, or by a calendar_id and UID pair. Raw iCalendar is excluded by default and can be requested with include_raw_ical: true for controlled diagnostics.

create_event

Creates an event and reads back the representation stored by the server.

Timed event with two alarms:

json
{  "calendar_id": "opaque-calendar-id",  "summary": "Buy Shinkansen tickets",  "start": {    "date_time": "2026-09-06T03:00:00+02:00",    "timezone": "Europe/Berlin"  },  "end": {    "date_time": "2026-09-06T03:30:00+02:00",    "timezone": "Europe/Berlin"  },  "description": "Smart-EX",  "location": null,  "alarms": [    { "minutes_before": 1440, "action": "DISPLAY" },    { "minutes_before": 0, "action": "DISPLAY" }  ],  "rrule": null}

All-day event with an exclusive end date:

json
{  "calendar_id": "opaque-calendar-id",  "summary": "Trip",  "start": { "date": "2026-09-06" },  "end": { "date": "2026-09-08" },  "alarms": []}

Recurring events accept an RFC 5545 rule without the RRULE: prefix:

json
{  "calendar_id": "opaque-calendar-id",  "summary": "Weekly planning",  "start": {    "date_time": "2026-09-07T09:00:00+02:00",    "timezone": "Europe/Berlin"  },  "end": {    "date_time": "2026-09-07T09:30:00+02:00",    "timezone": "Europe/Berlin"  },  "rrule": "FREQ=WEEKLY;BYDAY=MO;COUNT=10"}

Timed event with 30 minutes of Apple Calendar travel time:

json
{  "calendar_id": "opaque-calendar-id",  "summary": "Dentist",  "start": {    "date_time": "2026-10-05T10:00:00+02:00",    "timezone": "Europe/Amsterdam"  },  "end": {    "date_time": "2026-10-05T10:30:00+02:00",    "timezone": "Europe/Amsterdam"  },  "travel_minutes": 30}

travel_minutes sets a fixed travel duration before the event. In this example, travel starts at 09:30 and the appointment stays at 10:00–10:30. The server accepts whole minutes from 1 to 1,440 for timed events; omit the field or use null when creating an event without travel time. It does not calculate routes or change the event's start, end, or alarms.

The duration is stored in Apple's X-APPLE-TRAVEL-DURATION extension. Display in other calendar clients depends on their support for that extension. Returned events include travel_minutes, rounded to the nearest whole minute, or null when the duration is missing, invalid, or rounds to zero.

update_event

Patches an event or the master event of a recurring series. Omitted fields are preserved, null removes a nullable field, and alarms: [] removes all alarms. An optional expected_etag prevents overwriting a newer server version.

For travel time, omit patch.travel_minutes to keep the current value, set it to a number to replace the duration, or set it to null to remove the duration and related Apple routing metadata. For example, "patch": { "travel_minutes": null } clears travel time. When converting an event with travel time to all-day, include travel_minutes: null alongside the new start and end dates. Existing recurrence exceptions retain their own travel settings.

delete_event

Deletes an event or complete recurring series, optionally requiring an observed ETag. Deleting a single expanded occurrence is not supported in the current release.

Tech stack

Installation

Prerequisites

  • An iCloud account with Calendar enabled.
  • Two-factor authentication enabled for the Apple Account.
  • An app-specific password.
  • Docker and Docker Compose for container deployment, Node.js 22+ for npx, or Claude Desktop for the one-click extension.
  • pnpm is required only when building from source. Corepack and CI use the version pinned in package.json.

Node.js requirements

UseNode.js
Claude Desktop extension (.mcpb)None to install; Claude Desktop uses its built-in Node.js
npx or a global npm install22 LTS or newer; CI tests 22 and 24
Docker imageNone to install; the image ships Node.js 24
Building from source and contributing22.13 or newer, required by ESLint 10 and Vitest 5; 24 recommended

engines.node in package.json is >=22. Node.js 20 reached end of life in April 2026 and is not supported; npm prints an EBADENGINE warning on older versions. Check the version your MCP client will use with node --version. Desktop clients may use a different node than your terminal, so if a client cannot start the server, point it at the absolute path of a Node.js 22+ npx (command -v npx on macOS/Linux, where npx on Windows).

npm / npx

No global install or repository clone is required. MCP clients can launch the latest published package directly:

sh
CALDAV_USERNAME='[email protected]' \CALDAV_PASSWORD='xxxx-xxxx-xxxx-xxxx' \npx --yes @lukegskw/caldav-mcp@latest

The command waits for MCP messages on stdin and normally prints nothing to stdout. In practice, add it to the client configuration as shown in MCP client setup. For reproducible environments, replace latest with an exact published version such as X.Y.Z.

Docker Compose

The recommended installation uses the published multi-architecture image:

text
ghcr.io/lukegskw/caldav-mcp:latest

Download the Compose example:

sh
curl -O https://raw.githubusercontent.com/lukegskw/caldav-mcp/main/compose.example.yaml

Provide the Apple Account email and app-specific password, then start the service:

sh
export CALDAV_USERNAME='[email protected]'export CALDAV_PASSWORD='xxxx-xxxx-xxxx-xxxx'docker compose -f compose.example.yaml up -d

To publish a different host port, set:

sh
export CALDAV_MCP_PUBLISHED_PORT=18100docker compose -f compose.example.yaml up -d

The latest tag follows the newest stable release. Stable releases also publish an exact tag such as 0.1.6 and a minor-series tag such as 0.1. The Compose example pins latest by digest so deployments are reproducible. To upgrade, download the updated Compose example or replace the full image reference with the desired published version and digest.

The Streamable HTTP endpoint will be available at:

text
http://<host>:8100/mcp

The host port can change without changing port 8100 inside the container. No persistent volume is required; calendar data remains in iCloud.

Docker run

The same hardened container configuration can be started directly:

sh
docker run -d \  --name caldav-mcp \  --restart unless-stopped \  --read-only \  --user 10001:10001 \  --cap-drop ALL \  --security-opt no-new-privileges:true \  --tmpfs /tmp:size=16m,mode=1777 \  -e CALDAV_PROVIDER=icloud \  -e CALDAV_USERNAME \  -e CALDAV_PASSWORD \  -e CALDAV_MCP_TRANSPORT=streamable-http \  -e CALDAV_MCP_HOST=0.0.0.0 \  -p 8100:8100 \  ghcr.io/lukegskw/caldav-mcp:latest

Build the container from source

Building locally is optional. Prefer the published image unless you need to modify or audit the container build.

sh
git clone https://github.com/lukegskw/caldav-mcp.gitcd caldav-mcpdocker buildx build --load -t caldav-mcp:local .

Local Node.js installation

Building from source requires Node.js 22.13 or newer; Node.js 24 is recommended. Builds and typechecks use TypeScript 7. The typescript dependency aliases @typescript/typescript6 for ESLint, which still requires the TypeScript 6 API; @typescript/native supplies TypeScript 7’s tsc executable. See the TypeScript migration guidance.

sh
git clone https://github.com/lukegskw/caldav-mcp.gitcd caldav-mcppnpm install --frozen-lockfilecp .env.example .envpnpm buildpnpm start -- --transport stdio

In stdio mode, stdout is reserved exclusively for MCP messages. To run Streamable HTTP locally:

sh
CALDAV_MCP_TRANSPORT=streamable-http pnpm start

Configuration

All settings use the CALDAV_ or CALDAV_MCP_ prefix.

VariableRequiredDefaultDescription
CALDAV_PROVIDERNoicloudProvider policy. iCloud is the supported profile.
CALDAV_URLNohttps://caldav.icloud.comCalDAV discovery URL.
CALDAV_USERNAMEYesNoneApple Account email.
CALDAV_PASSWORDYesNoneApp-specific password, not the account password.
CALDAV_MCP_TRANSPORTNostdiostdio or streamable-http.
CALDAV_MCP_HOSTNo0.0.0.0HTTP bind address.
CALDAV_MCP_PORTNo8100HTTP listening port.
CALDAV_MCP_LOG_LEVELNoINFOApplication log level.
CALDAV_MCP_REQUEST_TIMEOUT_MSNo30000CalDAV request timeout.

The core retains an experimental generic provider policy and configurable URL to keep Apple extensions isolated from the shared iCalendar implementation. No compatibility with other providers is currently claimed.

Secrets must be supplied through the deployment platform or environment. Never commit .env, pass credentials as MCP tool arguments, or include them in diagnostic reports.

MCP client setup

Claude Desktop extension (recommended)

Claude Desktop for macOS and Windows installs the server as a one-click MCP Bundle and runs it with its built-in Node.js:

  1. Download caldav-mcp.mcpb from the latest release.
  2. Open it with Claude Desktop (double-click it, or drag it onto Settings -> Extensions) and choose Install.
  3. Enter the Apple Account email and app-specific password. The password is stored as a sensitive setting.

To upgrade, install the .mcpb from a newer release. See Claude Desktop extension for bundle contents and how it is built and released.

Gemini CLI extension

Install directly from GitHub and enable automatic extension updates:

sh
gemini extensions install https://github.com/lukegskw/caldav-mcp --auto-update

Gemini prompts for the username and stores the app-specific password as a sensitive setting. The public extension gallery discovers tagged releases from this repository.

Manual configuration

For any MCP client that accepts Streamable HTTP server definitions, configure the URL:

yaml
mcp_servers:  caldav:    url: http://<host>:8100/mcp

If the client shares the Compose network, use the service name and internal port:

yaml
mcp_servers:  caldav:    url: http://caldav-mcp:8100/mcp

For clients that launch local stdio servers, prefer the npm command from Quick start. If a desktop client cannot find npx, use the absolute path reported by command -v npx on macOS/Linux or where npx on Windows.

Claude Desktop

The extension is the simplest option. To configure the server manually instead, add the Quick start JSON under mcpServers in claude_desktop_config.json, then completely restart Claude Desktop. Open the file through Settings -> Developer -> Edit Config instead of assuming its location.

Claude Code

Claude Code can add the same stdio server at user scope:

sh
claude mcp add --transport stdio --scope user \  --env [email protected] \  --env CALDAV_PASSWORD=xxxx-xxxx-xxxx-xxxx \  icloud-calendar -- npx --yes @lukegskw/caldav-mcp@latest

This command places the values in Claude's MCP configuration. Avoid running it where shell history is shared or retained insecurely.

Codex

Codex can add the server to its shared CLI and IDE configuration:

sh
codex mcp add icloud-calendar \  --env [email protected] \  --env CALDAV_PASSWORD=xxxx-xxxx-xxxx-xxxx \  -- npx --yes @lukegskw/caldav-mcp@latest

Run codex mcp list to verify it. For finer control, use the official Codex MCP configuration in ~/.codex/config.toml or a project-scoped .codex/config.toml.

Gemini CLI

Following the Gemini CLI MCP configuration, add the server under mcpServers in ~/.gemini/settings.json (user scope) or the project's .gemini/settings.json:

json
{  "mcpServers": {    "icloud-calendar": {      "command": "npx",      "args": ["--yes", "@lukegskw/caldav-mcp@latest"],      "env": {        "CALDAV_USERNAME": "[email protected]",        "CALDAV_PASSWORD": "xxxx-xxxx-xxxx-xxxx"      }    }  }}
Claude Desktop (Docker, stdio alternative)

Claude Desktop launches local stdio servers as subprocesses. Running the published container this way keeps the app-specific password on the client machine and opens no network port, which matches the transport guidance in Limitations.

Add the server to claude_desktop_config.json:

json
{  "mcpServers": {    "icloud-calendar": {      "command": "/absolute/path/to/docker",      "args": [        "run",        "-i",        "--rm",        "--env-file",        "/absolute/path/to/caldav-mcp.env",        "-e",        "CALDAV_PROVIDER=icloud",        "-e",        "CALDAV_MCP_TRANSPORT=stdio",        "ghcr.io/lukegskw/caldav-mcp@sha256:<digest>"      ]    }  }}

-i is required. Without an attached stdin the client cannot speak MCP to the container. --rm removes the container once the client stops it.

Supply credentials through --env-file rather than -e. Arguments passed to docker run are visible in the host process list; the contents of an env file are not. The file holds the variables described in Configuration:

[email protected]CALDAV_PASSWORD=xxxx-xxxx-xxxx-xxxx

Pin the image by digest instead of latest, so that restarting the client cannot silently start a different version:

docker pull ghcr.io/lukegskw/caldav-mcp:latestdocker images --digests ghcr.io/lukegskw/caldav-mcp

Restart Claude Desktop completely after editing the configuration file.

Windows

When Claude Desktop is installed from the Microsoft Store, Windows redirects %APPDATA%\Claude into the package container and the file lives at:

%LOCALAPPDATA%\Packages\Claude_<package-id>\LocalCache\Roaming\Claude\claude_desktop_config.json

In that case dir %APPDATA%\Claude reports nothing. Server logs are written next to the configuration file, in logs\mcp-server-<server-name>.log.

Use the absolute path to docker.exe, because PATH inside the package container is not reliable. where docker prints it, typically C:\Program Files\Docker\Docker\resources\bin\docker.exe. Backslashes must be escaped in JSON.

Configuration formats differ between MCP clients. Consult the client's documentation for its exact schema and reload or restart it after changing the server definition.

Verification

Check the container state and logs:

sh
docker compose -f compose.example.yaml psdocker compose -f compose.example.yaml logs caldav-mcp

The container should report healthy. The TCP healthcheck validates the server process, not iCloud credentials.

Run the repository verification suite:

sh
pnpm install --frozen-lockfilepnpm format:checkpnpm lintpnpm typecheckpnpm test:unitpnpm test:integrationpnpm buildpnpm test:packagepnpm test:distributionpnpm build:mcpbpnpm test:mcpb

Finally, connect with an MCP client and confirm that all six tools are listed.

Limitations

  • One iCloud account is configured per server process or container.
  • The Streamable HTTP endpoint has no authentication in the current release. Restrict it to a trusted LAN, VPN, or private container network; do not expose it directly to the internet.
  • Individual recurrence occurrences are read-only. Updating or deleting the complete series is supported.
  • Only ACTION:DISPLAY alarms are created.
  • Individual iCalendar resources are limited to 5 MiB.
  • Event list ranges are limited to 366 days and pages to 500 results.
  • Events may contain at most 20 alarms.
  • Attendee scheduling is outside the current scope.
  • Providers other than iCloud are not officially supported.

Review SECURITY.md before reporting a security issue or attaching diagnostics.

Contributing

Contributions are welcome. Before opening a pull request:

sh
pnpm install --frozen-lockfilepnpm format:checkpnpm lintpnpm typecheckpnpm testpnpm builddocker buildx build --load -t caldav-mcp:test .

Changes to CalDAV writes or iCalendar serialization must preserve ETag checks, opaque resource boundaries, unknown properties, recurrence exceptions, and alarms omitted from patches. TypeScript changes must continue to satisfy the rules in .codex/rules/typescript.md.

Releasing

Releases are version-driven and automated from main so that a partial registry outage can be retried without publishing a second npm version.

  1. Prepare the new version with pnpm release:prepare X.Y.Z. This synchronizes the npm, MCP Registry, Gemini, and MCP Bundle (manifest.json) metadata.
  2. Run the verification suite, including pnpm test:distribution and pnpm test:mcpb.
  3. Commit and push the release changes to main.
  4. The release workflow validates the commit and creates the matching vX.Y.Z tag automatically before publishing.

The release workflow validates the versions, tests the packed npm artifact and the Claude Desktop bundle, and publishes the exact, minor-series, and latest container tags together with the npm package, MCP Registry entry, and GitHub release with caldav-mcp.mcpb attached. Prereleases receive only their exact container tag. Gemini can discover the tagged extension without another per-release edit. A rerun skips matching artifacts that already exist and resumes the missing steps. See Claude Desktop extension for building, testing, and shipping the .mcpb bundle.

License

MIT. See LICENSE.

Source: README.md at commit 76dbf1f

Tools

6

list_calendars

List calendars

List calendars available in the configured iCloud account. Use this first to obtain the opaque calendar_id required by list_events and create_event; writable is a best-effort capability indicator.

Takes no parameters.

list_events

List calendar events

List events that overlap the start-inclusive, end-exclusive interval and expand recurring series into occurrences, across at most 366 days. Use list_calendars first to obtain calendar_id and use get_event instead for one known event. Continue with next_cursor and unchanged query filters when more results are available.

Parametersendrequiredlimitstartrequiredcursortimezonecalendar_idrequired

get_event

Get calendar event

Read one event by resource_id, preferably from a previous result, or by the calendar_id and uid pair. Use list_events for range searches. Request raw iCalendar only for controlled diagnostics because it may contain sensitive calendar data.

Parametersuidcalendar_idresource_idinclude_raw_ical

create_event

Create calendar event

Create a new event in a writable iCloud calendar and return the stored representation; existing events are not changed. Use list_calendars first to obtain calendar_id, and use update_event when the event already exists. Timed values require matching offsets and timezones, all-day end dates are exclusive, recurrence rules omit the RRULE: prefix, and travel_minutes sets Apple Calendar travel time for timed events.

Parametersendrequiredrrulestartrequiredalarmssummaryrequiredlocationcalendar_idrequireddescriptiontravel_minutes

update_event

Update calendar event

Modify an existing event or entire recurring series; individual expanded occurrences are not supported. Target it by resource_id or by calendar_id with uid. Omitted patch fields are preserved, null clears nullable fields, an empty alarms array removes alarms, and expected_etag can prevent a stale write.

Parametersuidpatchrequiredcalendar_idresource_idexpected_etag

delete_event

Delete calendar event

Permanently delete an event or entire recurring series; individual expanded occurrences are not supported. Target it by resource_id or by calendar_id with uid, and supply expected_etag when available to prevent deleting a concurrently changed event.

Parametersuidcalendar_idresource_idexpected_etag

Version history

3
  1. v0.2.0LatestOct 21, 2026
  2. v0.1.9Sep 23, 2026
  3. v0.1.8Sep 16, 2026

Score

90% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 days25 pts
  • Auth declared10 pts

    Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.

  • Tools listedRequired15 pts
  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts