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 /healthHerramientas 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 tiempofetch(ids)→ obtiene blobs ICStext/calendarsin procesar para los resultados de búsqueda
Herramientas de correo (opcional,
MAIL_ENABLED=1):list_mailboxes()— lista todas las carpetaslist_messages(mailbox, limit, unread_only)— lista mensajes con encabezadosget_message(uid, mailbox)— obtiene el mensaje completo con el cuerposearch_messages(query, mailbox, limit)— búsqueda IMAP TEXTsend_message(to, subject, body, cc?, bcc?)— envía vía SMTPdelete_message(uid, mailbox)— mueve a la Papeleramark_message(uid, mailbox, read)— marca como leído/no leído
Entrada de fecha y hora ISO (
YYYY-MM-DDTHH:MM:SS, conZopcional 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 nameRequerido: 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 # OKPunto final MCP: http://127.0.0.1:8000/mcp
Referencia de herramientas (detalles funcionales)
list_calendars() -> List[Calendar]
Devuelve:
name: str | nullurl: 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 CalDAVstart, 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: strsummary: strstart: 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.
tzidtoma como valor predeterminado el entornoTZIDsi se omite; las fechas y horas ingenuas se asumen en esa zona y se almacenan como UTC.descriptiones opcional; omítelo o pasanullpara omitirlo.locationes opcional; omítelo o pasanullpara 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
uidgenerado (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
Truey también se proporcionarecurrence,clear_recurrencegana (sin recurrencia).
Devuelve
Truesi tiene éxito,Falsesi no se encuentra eluiden 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
Truesi se eliminó,Falsesi no se encontró.
Notas sobre fecha/hora
Acepta fechas y horas ingenuas o con
Z/desplazamiento (YYYY-MM-DDTHH:MM:SS, opcionalmenteZo-04:00etc.)Los eventos nuevos/editados emiten
DTSTART;TZID=...yDTEND;TZID=...usando eltzidproporcionado o el entornoTZIDLas actualizaciones intentan reutilizar el TZID original cuando está presente
LOCATIONse emite cuando se proporcionalocationy 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.pyNotas:
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 |
| ID de Apple o contraseña específica de la aplicación incorrecta; asegúrate de que |
Resultados de eventos vacíos | URL de calendario o ventana de tiempo incorrecta; recuerda que |
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 |
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!
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