Skip to main content
Glama
alexey-max-fedorov

iCloud CalDAV MCP Connector

iCloud MCP-Connector

Ein HTTP Model Context Protocol (MCP)-Server, der iCloud-Dienste für MCP-fähige Clients (z. B. Claude Custom Connectors, IDEs) unter Verwendung eines iCloud App-spezifischen Passworts bereitstellt.

Unterstützt: iCloud-Kalender (CalDAV) + iCloud-Mail (IMAP/SMTP).

Inoffiziell. Halten Sie diesen Dienst privat; er leitet Ihr iCloud App-spezifisches Passwort an die Server von Apple weiter.


Warum habe ich das gebaut?

Ich habe dies für den Claude Custom Connector gebaut, damit ich meinen iCloud-Kalender ändern kann, anstatt dies manuell zu tun. Die Idee kam mir an einem Freitagabend, bevor eine TOP-Aufgabe fällig war, und dies entwickelte sich zu einem unterhaltsamen 1-Tages-Projekt.


Funktionen

  • HTTP MCP-Server (/mcp) + GET /health

  • Kalender-Tools (Standard-Profil mit Schreibzugriff):

    • 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)

  • Kalender-Tools (Deep Research Read-Only-Profil, DR_PROFILE=1):

    • search(query) → einfache Textsuche über SUMMARY/DESCRIPTION in einem Zeitfenster

    • fetch(ids) → Abrufen von rohen text/calendar ICS-Blobs für Suchergebnisse

  • Mail-Tools (Opt-in, MAIL_ENABLED=1):

    • list_mailboxes() — alle Ordner auflisten

    • list_messages(mailbox, limit, unread_only) — Nachrichten mit Headern auflisten

    • get_message(uid, mailbox) — vollständige Nachricht mit Inhalt abrufen

    • search_messages(query, mailbox, limit) — IMAP TEXT-Suche

    • send_message(to, subject, body, cc?, bcc?) — Senden via SMTP

    • delete_message(uid, mailbox) — in den Papierkorb verschieben

    • mark_message(uid, mailbox, read) — als gelesen/ungelesen markieren

  • ISO-Datum/Uhrzeit-Eingabe (YYYY-MM-DDTHH:MM:SS, mit optionalem Z oder Zeitzonen-Offset)

  • Minimale ICS-Generierung (Maskierung von Zusammenfassung/Beschreibung), UID-Abgleich über ein ±3-Jahres-Fenster


Anforderungen

  • Python 3.11+

  • Apple ID (E-Mail-Identität, keine Telefonnummer)

  • iCloud App-spezifisches Passwort (widerrufbar) — ein Passwort funktioniert sowohl für Kalender als auch für Mail

  • Netzwerkzugriff auf https://caldav.icloud.com, imap.mail.me.com, smtp.mail.me.com


Umgebung

Erstellen Sie eine .env neben server.py (wird automatisch geladen):

APPLE_ID=you@example.com                 # 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

Erforderlich: APPLE_ID, ICLOUD_APP_PASSWORD.


Schnellstart (lokal)

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-Endpunkt: http://127.0.0.1:8000/mcp


Tool-Referenz (funktionale Details)

list_calendars() -> List[Calendar]

Gibt zurück:

  • name: str | null

  • url: str (bevorzugter Bezeichner für andere Aufrufe)

  • id: str | null

list_calendars_with_events(start, end, expand_recurring=True) -> List[Calendar]

Gibt nur die Kalender zurück, die mindestens ein Ereignis im angegebenen Zeitfenster enthalten.

Argumente

  • start, end: str — ISO-Datumsangaben; Suche ist [start, end)

  • expand_recurring: bool — wiederkehrende Serien als konkrete Instanzen behandeln

Jeder zurückgegebene Kalender hat die gleiche Form wie list_calendars().

list_events(calendar_name_or_url, start, end, expand_recurring=True) -> List[Event]

Argumente

  • calendar_name_or_url: str — Anzeigename oder vollständige CalDAV-URL

  • start, end: str — ISO-Datumsangaben; Suche ist [start, end)

  • expand_recurring: bool — konkrete Instanzen wiederkehrender Serien einbeziehen

Gibt jedes Ereignis zurück mit:

  • uid: str

  • summary: str

  • start: str (ISO)

  • end: str | null (ISO)

  • raw: str (ursprünglicher ICS-Text)

create_event(calendar_name_or_url, summary, start, end, tzid?, description?, location?, recurrence?) -> str

Erstellt ein minimales VEVENT.

  • tzid ist standardmäßig die TZID-Umgebungsvariable, falls weggelassen; naive Datumsangaben werden in dieser Zone angenommen und als UTC gespeichert.

  • description ist optional; weglassen oder null übergeben, um sie zu überspringen.

  • location ist optional; weglassen oder null übergeben, um sie zu überspringen.

  • recurrence (optional) beschreibt, wie sich das Ereignis wiederholen soll, zum Beispiel:

    {
        "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"
    }
  • Gibt die generierte uid zurück (zufälliger Hex-Wert + @claude-mcp).

update_event(calendar_name_or_url, uid, summary?, start?, end?, tzid?, description?, location?, recurrence?, clear_recurrence=False) -> bool

Aktualisiert das gesamte Ereignis, das durch uid identifiziert wird (bei wiederkehrenden Ereignissen aktualisiert dies die Serien-VEVENT, nicht eine einzelne Instanz).

  • Behält alle weggelassenen Felder der ursprünglichen Komponente bei.

  • location:

    • Wenn weggelassen (null / nicht bereitgestellt), bleibt der bestehende Ort erhalten.

    • Wenn als nicht leerer String bereitgestellt, wird der Ort des Ereignisses aktualisiert.

    • Wenn als leerer String bereitgestellt, wird der Ort des Ereignisses gelöscht.

  • recurrence:

    • Wenn bereitgestellt, ersetzt es jede bestehende RRULE unter Verwendung der gleichen Form wie in create_event.

  • clear_recurrence:

    • Wenn True, wird jede RRULE entfernt und das Ereignis zurück in eine einzelne, nicht wiederkehrende Instanz umgewandelt.

    • Wenn True und recurrence ebenfalls bereitgestellt wird, gewinnt clear_recurrence (keine Wiederholung).

  • Gibt True bei Erfolg zurück, False wenn uid nicht im ±3-Jahres-Fenster gefunden wurde.

delete_event(calendar_name_or_url, uid) -> bool

Löscht die erste übereinstimmende uid in einem ±3-Jahres-Fenster.

  • Gibt True zurück, wenn gelöscht, False wenn nicht gefunden.

Datum/Zeit-Hinweise

  • Akzeptiert naive oder Z/Offset-Datumsangaben (YYYY-MM-DDTHH:MM:SS, optional Z oder -04:00 etc.)

  • Neue/bearbeitete Ereignisse geben DTSTART;TZID=... und DTEND;TZID=... unter Verwendung der bereitgestellten tzid oder TZID-Umgebungsvariable aus

  • Aktualisierungen versuchen, die ursprüngliche TZID wiederzuverwenden, wenn vorhanden

  • LOCATION wird ausgegeben, wenn location bereitgestellt und nicht leer ist; das Übergeben eines leeren Strings beim Aktualisieren eines Ereignisses entfernt den bestehenden Ort.


Mail-Tool-Referenz

Aktivieren mit MAIL_ENABLED=1. Verwendet dieselbe APPLE_ID und dasselbe ICLOUD_APP_PASSWORD wie der Kalender. Keine zusätzlichen Abhängigkeiten — reines Python-Standard-Library (imaplib, smtplib).

list_mailboxes() -> List[{name}]

Gibt alle IMAP-Ordner zurück (INBOX, Sent, Drafts, Junk, Deleted Messages, etc.).

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

Gibt die neuesten Header für bis zu limit Nachrichten zurück. Jedes Element:

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

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

Ruft die vollständige Nachricht einschließlich des dekodierten Inhalts ab (text/plain bevorzugt, HTML als Fallback entfernt). Gibt zurück:

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

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

IMAP TEXT-Suche — entspricht Betreff und Inhalt. Gibt dieselben Header-Felder wie list_messages zurück.

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

Senden via SMTP (STARTTLS auf Port 587). to und cc können durch Kommas getrennt sein. Gibt True bei Erfolg zurück.

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

Kopiert in den Papierkorb (Deleted Messages standardmäßig, überschreibbar mit ICLOUD_TRASH_FOLDER) und löscht dann endgültig. Gibt True bei Erfolg zurück.

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

Setzt oder löscht das \Seen-Flag. Gibt True bei Erfolg zurück.


Deep Research Read-Only-Modus

Setzen Sie DR_PROFILE=1, um ein Read-Only-Tool-Set für Deep Research auszuführen. Dies macht nur Folgendes verfügbar:

  • search(query) -> [{ id, title, snippet }]

  • fetch(ids) -> [{ id, mimeType: 'text/calendar', content }]

Beispiel:

DR_PROFILE=1 HOST=127.0.0.1 PORT=8000 python server.py

Hinweise:

  • Schreib-Tools (list_events/create_event/update_event/delete_event) sind in diesem Modus deaktiviert.

  • SCAN_DAYS steuert das Suchfenster um "jetzt" (Standard: 1095 Tage ≈ 3 Jahre).

  • Halten Sie diesen Dienst privat oder fügen Sie eine Authentifizierung hinzu.


Beispiel (programmatischer 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())

Bereitstellung / Öffentliches HTTPS

Um dies mit Claude Custom Connectors zu verwenden, benötigen Sie einen öffentlichen HTTPS-Endpunkt, der an Ihren lokalen Server weiterleitet.

Siehe DEPLOY.md für:

  • Cloudflare Tunnel (stabiler Hostname, kostenlos)

  • ngrok (schneller Test)

  • VPS + Caddy/Nginx (permanent)

Sicherheit: Authentifizierung hinzufügen (Cloudflare Access, Basic Auth Proxy, IP-Allowlist). Setzen Sie dies NICHT unauthentifiziert aus; es bietet Live-Schreibzugriff auf den Kalender. Sie benötigen eine öffentliche HTTPS-URL, die an Ihr lokales http://127.0.0.1:8000 weiterleitet.


Fehlerbehebung

Symptom

Wahrscheinliche Ursache / Lösung

401 Unauthorized

Falsche Apple ID oder App-spezifisches Passwort; stellen Sie sicher, dass .env die E-Mail verwendet, nicht die Telefonnummer.

Leere Ereignisergebnisse

Falsche Kalender-URL oder Zeitfenster; denken Sie daran, dass end exklusiv ist.

Update/Delete ohne Wirkung

UID nicht im ±3-Jahres-Scanfenster oder anderer Kalender als der, den Sie abfragen.

Zeitzonen-Drift

Übergeben Sie tzid explizit (z. B. America/New_York) oder verwenden Sie UTC ...Z.


Sicherheit

  • Verwenden Sie App-spezifische Passwörter und rotieren Sie diese bei Bedarf

  • Halten Sie diesen Server privat (Tunnel-ACLs, IP-Allowlists, Auth-Proxy)

  • Dieses Projekt schreibt minimale VEVENTs neu; erweiterte Felder (Teilnehmer, Alarme, Wiederholungsausnahmen) bleiben bei einer Aktualisierung nicht erhalten


Lizenz

MIT-Lizenz.


Frohes Planen, ich hoffe, das hilft!

-
security - not tested
A
license - permissive license
-
quality - not tested

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/alexey-max-fedorov/icloud-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server