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 /healthKalender-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 Zeitfensterfetch(ids)→ Abrufen von rohentext/calendarICS-Blobs für Suchergebnisse
Mail-Tools (Opt-in,
MAIL_ENABLED=1):list_mailboxes()— alle Ordner auflistenlist_messages(mailbox, limit, unread_only)— Nachrichten mit Headern auflistenget_message(uid, mailbox)— vollständige Nachricht mit Inhalt abrufensearch_messages(query, mailbox, limit)— IMAP TEXT-Suchesend_message(to, subject, body, cc?, bcc?)— Senden via SMTPdelete_message(uid, mailbox)— in den Papierkorb verschiebenmark_message(uid, mailbox, read)— als gelesen/ungelesen markieren
ISO-Datum/Uhrzeit-Eingabe (
YYYY-MM-DDTHH:MM:SS, mit optionalemZoder 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 nameErforderlich: 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 # OKMCP-Endpunkt: http://127.0.0.1:8000/mcp
Tool-Referenz (funktionale Details)
list_calendars() -> List[Calendar]
Gibt zurück:
name: str | nullurl: 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-URLstart, end: str— ISO-Datumsangaben; Suche ist [start, end)expand_recurring: bool— konkrete Instanzen wiederkehrender Serien einbeziehen
Gibt jedes Ereignis zurück mit:
uid: strsummary: strstart: 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.
tzidist standardmäßig dieTZID-Umgebungsvariable, falls weggelassen; naive Datumsangaben werden in dieser Zone angenommen und als UTC gespeichert.descriptionist optional; weglassen odernullübergeben, um sie zu überspringen.locationist optional; weglassen odernullü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
uidzurü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
Trueundrecurrenceebenfalls bereitgestellt wird, gewinntclear_recurrence(keine Wiederholung).
Gibt
Truebei Erfolg zurück,Falsewennuidnicht 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
Truezurück, wenn gelöscht,Falsewenn nicht gefunden.
Datum/Zeit-Hinweise
Akzeptiert naive oder
Z/Offset-Datumsangaben (YYYY-MM-DDTHH:MM:SS, optionalZoder-04:00etc.)Neue/bearbeitete Ereignisse geben
DTSTART;TZID=...undDTEND;TZID=...unter Verwendung der bereitgestelltentzidoderTZID-Umgebungsvariable ausAktualisierungen versuchen, die ursprüngliche TZID wiederzuverwenden, wenn vorhanden
LOCATIONwird ausgegeben, wennlocationbereitgestellt 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.pyHinweise:
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 |
| Falsche Apple ID oder App-spezifisches Passwort; stellen Sie sicher, dass |
Leere Ereignisergebnisse | Falsche Kalender-URL oder Zeitfenster; denken Sie daran, dass |
Update/Delete ohne Wirkung | UID nicht im ±3-Jahres-Scanfenster oder anderer Kalender als der, den Sie abfragen. |
Zeitzonen-Drift | Übergeben Sie |
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!
This server cannot be installed
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