Skip to main content
Glama
alexey-max-fedorov

iCloud CalDAV MCP Connector

Conector MCP de iCloud

Un servidor HTTP Model Context Protocol (MCP) que expone los servicios de iCloud a clientes compatibles con MCP (por ejemplo, conectores personalizados de Claude, IDEs) utilizando una contraseña específica de la aplicación de iCloud.

Soportado: Calendario de iCloud (CalDAV) + Correo de iCloud (IMAP/SMTP).

No oficial. Mantén este servicio privado; reenvía tu contraseña específica de la aplicación de iCloud a los servidores de Apple.


¿Por qué construí esto?

Lo construí para usarlo en el Conector Personalizado de Claude, para poder cambiar mi Calendario de iCloud en lugar de cambiarlo manualmente. Se me ocurrió esta idea un viernes por la noche antes de que venciera un TOP Pset, y resultó ser un proyecto divertido de 1 día.


Características

  • Servidor HTTP MCP (/mcp) + GET /health

  • Herramientas de calendario (perfil predeterminado con capacidad de escritura):

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

  • Herramientas de calendario (perfil de solo lectura para Deep Research, DR_PROFILE=1):

    • search(query) → búsqueda de texto básica sobre SUMMARY/DESCRIPTION en una ventana de tiempo

    • fetch(ids) → obtiene blobs ICS text/calendar sin procesar para los resultados de búsqueda

  • Herramientas de correo (opcional, MAIL_ENABLED=1):

    • list_mailboxes() — lista todas las carpetas

    • list_messages(mailbox, limit, unread_only) — lista mensajes con encabezados

    • get_message(uid, mailbox) — obtiene el mensaje completo con el cuerpo

    • search_messages(query, mailbox, limit) — búsqueda IMAP TEXT

    • send_message(to, subject, body, cc?, bcc?) — envía vía SMTP

    • delete_message(uid, mailbox) — mueve a la Papelera

    • mark_message(uid, mailbox, read) — marca como leído/no leído

  • Entrada de fecha y hora ISO (YYYY-MM-DDTHH:MM:SS, con Z opcional o desplazamiento de zona horaria)

  • Generación mínima de ICS (escape de resumen/descripción), coincidencia de UID en una ventana de ±3 años


Requisitos

  • Python 3.11+

  • Apple ID (identidad de correo electrónico, no número de teléfono)

  • Contraseña específica de la aplicación de iCloud (revocable) — una contraseña funciona tanto para el calendario como para el correo

  • Acceso de red a https://caldav.icloud.com, imap.mail.me.com, smtp.mail.me.com


Entorno

Crea un .env junto a server.py (cargado automáticamente):

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

Requerido: APPLE_ID, ICLOUD_APP_PASSWORD.


Inicio rápido (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

Punto final MCP: http://127.0.0.1:8000/mcp


Referencia de herramientas (detalles funcionales)

list_calendars() -> List[Calendar]

Devuelve:

  • name: str | null

  • url: str (identificador preferido para otras llamadas)

  • id: str | null

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

Devuelve solo los calendarios que contienen al menos un evento en la ventana de tiempo dada.

Argumentos

  • start, end: str — fechas y horas ISO; la búsqueda es [start, end)

  • expand_recurring: bool — trata las series recurrentes como instancias concretas

Cada calendario devuelto tiene la misma forma que list_calendars().

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

Argumentos

  • calendar_name_or_url: str — nombre para mostrar o URL completa de CalDAV

  • start, end: str — fechas y horas ISO; la búsqueda es [start, end)

  • expand_recurring: bool — incluye instancias concretas de series recurrentes

Devuelve cada evento con:

  • uid: str

  • summary: str

  • start: str (ISO)

  • end: str | null (ISO)

  • raw: str (texto ICS original)

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

Crea un VEVENT mínimo.

  • tzid toma como valor predeterminado el entorno TZID si se omite; las fechas y horas ingenuas se asumen en esa zona y se almacenan como UTC.

  • description es opcional; omítelo o pasa null para omitirlo.

  • location es opcional; omítelo o pasa null para omitirlo.

  • recurrence (opcional) describe cómo debe repetirse el evento, por ejemplo:

    {
        "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"
    }
  • Devuelve el uid generado (hexadecimal aleatorio + @claude-mcp).

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

Actualiza el evento completo identificado por uid (para eventos recurrentes, esto actualiza el VEVENT de la serie, no una sola instancia).

  • Conserva cualquier campo omitido del componente original.

  • location:

    • Si se omite (null / no proporcionado), mantiene la ubicación existente.

    • Si se proporciona como una cadena no vacía, actualiza la ubicación del evento.

    • Si se proporciona como una cadena vacía, borra la ubicación del evento.

  • recurrence:

    • Si se proporciona, reemplaza cualquier RRULE existente usando la misma forma que en create_event.

  • clear_recurrence:

    • Si es True, elimina cualquier RRULE y convierte el evento de nuevo en una sola instancia no recurrente.

    • Si es True y también se proporciona recurrence, clear_recurrence gana (sin recurrencia).

  • Devuelve True si tiene éxito, False si no se encuentra el uid en la ventana de ±3 años.

delete_event(calendar_name_or_url, uid) -> bool

Elimina el primer uid coincidente en una ventana de ±3 años.

  • Devuelve True si se eliminó, False si no se encontró.

Notas sobre fecha/hora

  • Acepta fechas y horas ingenuas o con Z/desplazamiento (YYYY-MM-DDTHH:MM:SS, opcionalmente Z o -04:00 etc.)

  • Los eventos nuevos/editados emiten DTSTART;TZID=... y DTEND;TZID=... usando el tzid proporcionado o el entorno TZID

  • Las actualizaciones intentan reutilizar el TZID original cuando está presente

  • LOCATION se emite cuando se proporciona location y no está vacío; pasar una cadena vacía al actualizar un evento elimina la ubicación existente.


Referencia de herramientas de correo

Habilítalo con MAIL_ENABLED=1. Utiliza el mismo APPLE_ID y ICLOUD_APP_PASSWORD que el calendario. Sin dependencias adicionales: biblioteca estándar de Python pura (imaplib, smtplib).

list_mailboxes() -> List[{name}]

Devuelve todas las carpetas IMAP (INBOX, Sent, Drafts, Junk, Deleted Messages, etc.).

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

Devuelve los encabezados más recientes para hasta limit mensajes. Cada elemento:

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

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

Obtiene el mensaje completo, incluido el cuerpo decodificado (se prefiere text/plain, HTML eliminado como alternativa). Devuelve:

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

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

Búsqueda IMAP TEXT: coincide con el asunto y el cuerpo. Devuelve los mismos campos de encabezado que list_messages.

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

Envía vía SMTP (STARTTLS en el puerto 587). to y cc pueden estar separados por comas. Devuelve True si tiene éxito.

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

Copia a la Papelera (Deleted Messages por defecto, sobrescribir con ICLOUD_TRASH_FOLDER) y luego expurga. Devuelve True si tiene éxito.

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

Establece o borra la bandera \Seen. Devuelve True si tiene éxito.


Modo de solo lectura para Deep Research

Establece DR_PROFILE=1 para ejecutar un conjunto de herramientas de solo lectura para Deep Research. Esto expone solo:

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

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

Ejemplo:

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

Notas:

  • Las herramientas de escritura (list_events/create_event/update_event/delete_event) están deshabilitadas en este modo.

  • SCAN_DAYS controla la ventana de búsqueda alrededor de "ahora" (predeterminado: 1095 días ≈ 3 años).

  • Mantén este servicio privado o añade autenticación


Ejemplo (cliente programático)

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

Despliegue / HTTPS público

Para usar esto con los Conectores Personalizados de Claude, necesitas un punto final HTTPS público que reenvíe a tu servidor local.

Consulta DEPLOY.md para:

  • Cloudflare Tunnel (nombre de host estable, gratuito)

  • ngrok (prueba rápida)

  • VPS + Caddy/Nginx (permanente)

Seguridad: añade autenticación (Cloudflare Access, proxy de autenticación básica, lista de permitidos de IP). NO expongas esto sin autenticación; tiene acceso de escritura al calendario en vivo. Necesitas una URL HTTPS pública que reenvíe a tu http://127.0.0.1:8000 local.


Solución de problemas

Síntoma

Causa probable / Solución

401 Unauthorized

ID de Apple o contraseña específica de la aplicación incorrecta; asegúrate de que .env use correo electrónico, no teléfono.

Resultados de eventos vacíos

URL de calendario o ventana de tiempo incorrecta; recuerda que end es exclusivo.

Actualizar/Eliminar no hace nada

UID no está en la ventana de escaneo de ±3 años o es un calendario diferente al que estás consultando.

Desviación de zona horaria

Pasa tzid explícitamente (ej. America/New_York) o usa UTC ...Z.


Seguridad

  • Usa contraseñas específicas de la aplicación y rótalas según sea necesario

  • Mantén este servidor privado (ACL de túnel, listas de permitidos de IP, proxy de autenticación)

  • Este proyecto reescribe VEVENTs mínimos; los campos avanzados (asistentes, alarmas, excepciones de recurrencia) no se conservan al actualizar


Licencia

Licencia MIT.


¡Feliz programación, espero que esto ayude!

-
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