Skip to main content
Glama
alexey-fedorov-com

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.


Related MCP server: mcp-labrat

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!

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    An MCP server that enables interaction with CalDAV calendars to manage events and check availability through natural language or voice commands. It provides specific tools for listing, searching, and creating calendar entries using an OpenAI-compatible interface.
    -
  • A
    license
    Not graded
    quality
    F
    maintenance
    Provider-agnostic CalDAV calendar MCP server that connects any CalDAV calendar to AI assistants, enabling calendar operations like listing, creating, updating, and deleting events.
    AGPL 3.0