iCloud CalDAV MCP Connector
An HTTP Model Context Protocol (MCP) server exposing iCloud Calendar (CalDAV) tools so MCP-aware clients can list calendars, read events, and create/update/delete events using an iCloud app-specific password.
README
iCloud CalDAV MCP Connector
An HTTP Model Context Protocol (MCP) server exposing iCloud Calendar (CalDAV) tools so MCP-aware clients (e.g., ChatGPT custom connectors, IDEs) can list calendars, read events, and create/update/delete events using an iCloud app-specific password.
Unofficial. Calendar only. Keep this service private; it forwards your iCloud app-specific password to Apple’s CalDAV endpoint.
Why did I build this?
I built this to use in ChatGPT Custom Connector, so I can change my iCloud Calendar compared to changing it manually. Came up with this idea on a Friday night before a TOP Pset was due, and this turned out to be a fun 1-day project.
Features
- HTTP MCP server (
/mcp) +GET /health - Tools (default write-capable profile):
list_calendars()list_calendars_with_events(start, end, expand_recurring=True)list_events(calendar_name_or_url, start, end, expand_recurring=True)create_event(calendar_name_or_url, summary, start, end, tzid?, description?, location?, recurrence?)update_event(calendar_name_or_url, uid, summary?, start?, end?, tzid?, description?, location?, recurrence?, clear_recurrence=False)delete_event(calendar_name_or_url, uid)
- Tools (Deep Research read-only profile):
search(query)→ basic text search over SUMMARY/DESCRIPTION in a time windowfetch(ids)→ fetch rawtext/calendarICS blobs for search results
- ISO datetime input (
YYYY-MM-DDTHH:MM:SS, with optionalZor timezone offset) - Minimal ICS generation (summary/description escaping), UID matching across a ±3-year window
Requirements
- Python 3.11+
- Apple ID (email identity, not phone number)
- iCloud app-specific password (revocable)
- Network access to
https://caldav.icloud.com
Environment
Create a .env next to server.py (auto-loaded):
APPLE_ID=you@example.com # Use your Apple ID email
ICLOUD_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx # App-specific password
CALDAV_URL=https://caldav.icloud.com # optional, default shown
HOST=127.0.0.1 # optional
PORT=8000 # optional
TZID=America/New_York # default TZ for new/edited events
# Deep Research: read-only profile (optional)
DR_PROFILE=0 # Set to 1 to enable DR mode (default 0)
SCAN_DAYS=1095 # Time window (days) scanned by DR search/fetch (default ~3 years)
Required: APPLE_ID, ICLOUD_APP_PASSWORD.
Quick Start (local)
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Ensure .env exists (see above), then:
python server.py
# -> Listening on http://127.0.0.1:8000
curl http://127.0.0.1:8000/health # OK
MCP endpoint: http://127.0.0.1:8000/mcp
Tool Reference (functional details)
list_calendars() -> List[Calendar]
Returns:
name: str | nullurl: str(preferred identifier for other calls)id: str | null
list_calendars_with_events(start, end, expand_recurring=True) -> List[Calendar]
Returns only the calendars that contain at least one event in the given time window.
Args
start, end: str— ISO datetimes; search is [start, end)expand_recurring: bool— treat recurring series as concrete instances
Each returned calendar has the same shape as list_calendars().
list_events(calendar_name_or_url, start, end, expand_recurring=True) -> List[Event]
Args
calendar_name_or_url: str— display name or full CalDAV URLstart, end: str— ISO datetimes; search is [start, end)expand_recurring: bool— include concrete instances of recurring series
Returns each event with:
uid: strsummary: strstart: str(ISO)end: str | null(ISO)raw: str(original ICS text)
create_event(calendar_name_or_url, summary, start, end, tzid?, description?, location?, recurrence?) -> str
Creates a minimal VEVENT.
-
tziddefaults toTZIDenv if omitted; naive datetimes are assumed in that zone and stored as UTC. -
descriptionis optional; omit or passnullto skip it. -
locationis optional; omit or passnullto skip it. -
recurrence(optional) describes how the event should repeat, for example:{ "frequency": "weekly", // daily | weekly | monthly | yearly | custom "interval": 1, // optional, default 1 "by_weekday": ["MO", "WE"], // optional; for weekly/custom "by_monthday": [1, 15], // optional; for monthly/custom "end": { // optional end condition "type": "on_date", // or "after_occurrences" "date": "2025-12-31" // when type == "on_date" // or: "count": 10 // when type == "after_occurrences" } // for custom frequency you can pass a raw RRULE: // "frequency": "custom", // "rrule": "FREQ=MONTHLY;BYDAY=MO,TU;BYSETPOS=1" } -
Returns the generated
uid(random hex +@chatgpt-mcp).
update_event(calendar_name_or_url, uid, summary?, start?, end?, tzid?, description?, location?, recurrence?, clear_recurrence=False) -> bool
Updates the whole event identified by uid (for recurring events this updates the series VEVENT, not a single instance).
- Preserves any omitted fields from the original component.
location:- If omitted (
null/ not provided), keeps the existing location. - If provided as a non-empty string, updates the event’s location.
- If provided as an empty string, clears the event’s location.
- If omitted (
recurrence:- If provided, replaces any existing RRULE using the same shape as in
create_event.
- If provided, replaces any existing RRULE using the same shape as in
clear_recurrence:- If
True, removes any RRULE and converts the event back to a single non-recurring instance. - If
Trueandrecurrenceis also provided,clear_recurrencewins (no recurrence).
- If
- Returns
Trueon success,Falseifuidnot found in ±3-year window.
delete_event(calendar_name_or_url, uid) -> bool
Deletes the first matching uid in a ±3-year window.
- Returns
Trueif deleted,Falseif not found.
Date/Time Notes
- Accepts naive or
Z/offset datetimes (YYYY-MM-DDTHH:MM:SS, optionallyZor-04:00etc.) - New/edited events emit
DTSTART;TZID=...andDTEND;TZID=...using providedtzidorTZIDenv - Updates attempt to reuse the original TZID when present
LOCATIONis emitted whenlocationis provided and non-empty; passing an empty string when updating an event removes the existing location.
Deep Research read-only mode
Set DR_PROFILE=1 to run a read-only tool set for Deep Research. This exposes only:
- search(query) -> [{ id, title, snippet }]
- fetch(ids) -> [{ id, mimeType: 'text/calendar', content }]
Example:
DR_PROFILE=1 HOST=127.0.0.1 PORT=8000 python server.py
Notes:
- Write tools (list_events/create_event/update_event/delete_event) are disabled in this mode.
- SCAN_DAYS controls the search window around “now” (default: 1095 days ≈ 3 years).
- Keep this service private or add auth
Example (programmatic client)
import asyncio, json
from fastmcp import Client
MCP_URL = "http://127.0.0.1:8000/mcp"
CAL_URL = "<paste one of your calendar URLs>"
def unwrap(res):
sc = getattr(res, "structured_content", None)
if isinstance(sc, dict) and "result" in sc:
return sc["result"]
return json.loads(res.content[0].text)
async def main():
async with Client(MCP_URL) as c:
cals = unwrap(await c.call_tool("list_calendars", {"confirm": True}))
print("Calendars:", cals[:2])
evs = unwrap(await c.call_tool("list_events", {
"calendar_name_or_url": CAL_URL,
"start": "2025-09-01T00:00:00",
"end": "2025-10-01T00:00:00",
"expand_recurring": True
}))
print("Events:", len(evs))
uid = unwrap(await c.call_tool("create_event", {
"calendar_name_or_url": CAL_URL,
"summary":"Demo",
"start":"2025-09-29T15:00:00",
"end":"2025-09-29T15:30:00",
"tzid":"America/New_York",
"location": "Bobst Library"
}))
print("Created:", uid)
asyncio.run(main())
Deployment / Public HTTPS
To use this with ChatGPT Custom Connectors you need a public HTTPS endpoint that forwards to your local server.
See DEPLOY.md for:
- Cloudflare Tunnel (stable hostname, free)
- ngrok (quick test)
- VPS + Caddy/Nginx (permanent)
Security: add auth (Cloudflare Access, Basic Auth proxy, IP allowlist). Do NOT expose this unauthenticated; it holds live calendar write access.
You need a public HTTPS URL that forwards to your local http://127.0.0.1:8000.
Troubleshooting
| Symptom | Likely Cause / Fix |
|---|---|
401 Unauthorized |
Wrong Apple ID or app-specific password; ensure .env uses email, not phone. |
| Empty event results | Wrong calendar URL or time window; remember end is exclusive. |
| Update/Delete no-ops | UID not in ±3-year scan window or different calendar than you’re querying. |
| Timezone drift | Pass tzid explicitly (e.g., America/New_York) or use UTC ...Z. |
Security
- Use app-specific passwords and rotate as needed
- Keep this server private (tunnel ACLs, IP allowlists, auth proxy)
- This project rewrites minimal VEVENTs; advanced fields (attendees, alarms, recurrence exceptions) are not preserved on update
License
MIT License.
Happy scheduling, I hope this helps!
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.