Command Palette

Search for a command to run...

UnylyUnyly
Весь каталог

ICloud CalDAV Connector

БесплатноНе проверен

An MCP server that enables interaction with iCloud Calendars via the CalDAV protocol using an app-specific password. It allows users to list, create, update, an

GitHubEmbed

Описание

An MCP server that enables interaction with iCloud Calendars via the CalDAV protocol using an app-specific password. It allows users to list, create, update, and delete calendar events through MCP-aware clients like ChatGPT.

README

An HTTP Model Context Protocol (MCP) server exposing iCloud services to MCP-aware clients (e.g., claude custom connectors, IDEs) using an iCloud app-specific password.

Supported: iCloud Calendar (CalDAV) + iCloud Mail (IMAP/SMTP).

Unofficial. Keep this service private; it forwards your iCloud app-specific password to Apple’s servers.


Why did I build this?

I built this to use in Claude 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
  • Calendar 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)
  • Calendar tools (Deep Research read-only profile, DR_PROFILE=1):
    • search(query) → basic text search over SUMMARY/DESCRIPTION in a time window
    • fetch(ids) → fetch raw text/calendar ICS blobs for search results
  • Mail tools (opt-in, MAIL_ENABLED=1):
    • list_mailboxes() — list all folders
    • list_messages(mailbox, limit, unread_only) — list messages with headers
    • get_message(uid, mailbox) — fetch full message with body
    • search_messages(query, mailbox, limit) — IMAP TEXT search
    • send_message(to, subject, body, cc?, bcc?) — send via SMTP
    • delete_message(uid, mailbox) — move to Trash
    • mark_message(uid, mailbox, read) — mark read/unread
  • ISO datetime input (YYYY-MM-DDTHH:MM:SS, with optional Z or 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) — one password works for both calendar and mail
  • Network access to https://caldav.icloud.com, imap.mail.me.com, smtp.mail.me.com

Environment

Create a .env next to server.py (auto-loaded):

[email protected]                 # Use your Apple ID email
ICLOUD_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx  # App-specific password (works for both calendar and mail)
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 calendar 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)

# Mail (IMAP / SMTP) — optional, disabled by default
MAIL_ENABLED=1                           # Set to 1 to enable mail tools
IMAP_HOST=imap.mail.me.com              # optional, default shown
IMAP_PORT=993                            # optional, default shown
SMTP_HOST=smtp.mail.me.com              # optional, default shown
SMTP_PORT=587                            # optional, default shown
ICLOUD_TRASH_FOLDER=Deleted Messages     # optional, iCloud trash folder name

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 | null
  • url: 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 URL
  • start, end: str — ISO datetimes; search is [start, end)
  • expand_recurring: bool — include concrete instances of recurring series

Returns each event with:

  • uid: str
  • summary: str
  • start: 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.

  • tzid defaults to TZID env if omitted; naive datetimes are assumed in that zone and stored as UTC.

  • description is optional; omit or pass null to skip it.

  • location is optional; omit or pass null to 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 + @claude-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.
  • recurrence:
    • If provided, replaces any existing RRULE using the same shape as in create_event.
  • clear_recurrence:
    • If True, removes any RRULE and converts the event back to a single non-recurring instance.
    • If True and recurrence is also provided, clear_recurrence wins (no recurrence).
  • Returns True on success, False if uid not found in ±3-year window.

delete_event(calendar_name_or_url, uid) -> bool

Deletes the first matching uid in a ±3-year window.

  • Returns True if deleted, False if not found.

Date/Time Notes

  • Accepts naive or Z/offset datetimes (YYYY-MM-DDTHH:MM:SS, optionally Z or -04:00 etc.)
  • New/edited events emit DTSTART;TZID=... and DTEND;TZID=... using provided tzid or TZID env
  • Updates attempt to reuse the original TZID when present
  • LOCATION is emitted when location is provided and non-empty; passing an empty string when updating an event removes the existing location.

Mail Tool Reference

Enable with MAIL_ENABLED=1. Uses the same APPLE_ID and ICLOUD_APP_PASSWORD as the calendar. No extra dependencies — pure Python stdlib (imaplib, smtplib).

list_mailboxes() -> List[{name}]

Returns all IMAP folders (INBOX, Sent, Drafts, Junk, Deleted Messages, etc.).

list_messages(mailbox="INBOX", limit=20, unread_only=False) -> List[Message]

Returns newest-first headers for up to limit messages. Each item:

  • uid: str, subject: str, from: str, date: str, read: bool

get_message(uid, mailbox="INBOX") -> Message

Fetches the full message including decoded body (text/plain preferred, HTML stripped as fallback). Returns:

  • uid, subject, from, to, cc, date, body, read

search_messages(query, mailbox="INBOX", limit=20) -> List[Message]

IMAP TEXT search — matches subject and body. Returns same header fields as list_messages.

send_message(to, subject, body, cc=None, bcc=None) -> bool

Sends via SMTP (STARTTLS on port 587). to and cc may be comma-separated. Returns True on success.

delete_message(uid, mailbox="INBOX") -> bool

Copies to Trash (Deleted Messages by default, override with ICLOUD_TRASH_FOLDER) then expunges. Returns True on success.

mark_message(uid, mailbox="INBOX", read=True) -> bool

Sets or clears the \Seen flag. Returns True on success.


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 claude 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!

from github.com/alexey-max-fedorov/icloud-mcp

Установка ICloud CalDAV Connector

У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.

▸ github.com/alexey-max-fedorov/icloud-mcp

FAQ

ICloud CalDAV Connector MCP бесплатный?

Да, ICloud CalDAV Connector MCP бесплатный — установка в пару кликов через Unyly без оплаты.

Нужен ли API-ключ для ICloud CalDAV Connector?

Нет, ICloud CalDAV Connector работает без API-ключей и переменных окружения.

ICloud CalDAV Connector — hosted или self-hosted?

Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.

Как установить ICloud CalDAV Connector в Claude Desktop, Claude Code или Cursor?

Открой ICloud CalDAV Connector на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.

Похожие MCP

Compare ICloud CalDAV Connector with

Не уверен что выбрать?

Найди свой стек за 60 секунд

Автор?

Embed-бейдж для README

Похожее

Все в категории productivity