Skip to main content
Glama
alexey-max-fedorov

iCloud CalDAV MCP Connector

iCloud MCP Connector

HTTP-сервер Model Context Protocol (MCP), предоставляющий доступ к сервисам iCloud для MCP-совместимых клиентов (например, пользовательских коннекторов Claude, IDE) с использованием пароля приложения iCloud.

Поддерживается: Календарь iCloud (CalDAV) + Почта iCloud (IMAP/SMTP).

Неофициальный проект. Держите этот сервис приватным; он пересылает ваш пароль приложения iCloud на серверы Apple.


Зачем я это создал?

Я создал это для использования в пользовательском коннекторе Claude, чтобы иметь возможность изменять свой календарь iCloud вместо того, чтобы делать это вручную. Идея пришла в пятницу вечером перед сдачей задания TOP Pset, и это превратилось в забавный проект на один день.


Функции

  • HTTP MCP-сервер (/mcp) + GET /health

  • Инструменты календаря (профиль с правами записи по умолчанию):

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

  • Инструменты календаря (профиль Deep Research только для чтения, DR_PROFILE=1):

    • search(query) → базовый текстовый поиск по SUMMARY/DESCRIPTION во временном окне

    • fetch(ids) → получение необработанных ICS-блоков text/calendar для результатов поиска

  • Инструменты почты (опционально, MAIL_ENABLED=1):

    • list_mailboxes() — список всех папок

    • list_messages(mailbox, limit, unread_only) — список сообщений с заголовками

    • get_message(uid, mailbox) — получение полного сообщения с телом

    • search_messages(query, mailbox, limit) — IMAP-поиск по тексту

    • send_message(to, subject, body, cc?, bcc?) — отправка через SMTP

    • delete_message(uid, mailbox) — перемещение в корзину

    • mark_message(uid, mailbox, read) — пометить как прочитанное/непрочитанное

  • Ввод даты и времени в формате ISO (YYYY-MM-DDTHH:MM:SS, с опциональным Z или смещением часового пояса)

  • Минимальная генерация ICS (экранирование summary/description), сопоставление UID в окне ±3 года


Требования

  • Python 3.11+

  • Apple ID (email, не номер телефона)

  • Пароль приложения iCloud (можно отозвать) — один пароль работает и для календаря, и для почты

  • Сетевой доступ к https://caldav.icloud.com, imap.mail.me.com, smtp.mail.me.com


Окружение

Создайте файл .env рядом с server.py (загружается автоматически):

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

Обязательно: APPLE_ID, ICLOUD_APP_PASSWORD.


Быстрый старт (локально)

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-эндпоинт: http://127.0.0.1:8000/mcp


Справочник инструментов (функциональные детали)

list_calendars() -> List[Calendar]

Возвращает:

  • name: str | null

  • url: str (предпочтительный идентификатор для других вызовов)

  • id: str | null

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

Возвращает только те календари, которые содержат хотя бы одно событие в заданном временном окне.

Аргументы

  • start, end: str — ISO-даты; поиск выполняется в диапазоне [start, end)

  • expand_recurring: bool — рассматривать повторяющиеся серии как конкретные экземпляры

Каждый возвращенный календарь имеет ту же структуру, что и list_calendars().

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

Аргументы

  • calendar_name_or_url: str — отображаемое имя или полный URL CalDAV

  • start, end: str — ISO-даты; поиск выполняется в диапазоне [start, end)

  • expand_recurring: bool — включать конкретные экземпляры повторяющихся серий

Возвращает каждое событие с полями:

  • uid: str

  • summary: str

  • start: str (ISO)

  • end: str | null (ISO)

  • raw: str (оригинальный текст ICS)

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

Создает минимальный VEVENT.

  • tzid по умолчанию берется из переменной окружения TZID, если не указан; наивные даты считаются находящимися в этой зоне и сохраняются как UTC.

  • description опционально; пропустите или передайте null.

  • location опционально; пропустите или передайте null.

  • recurrence (опционально) описывает, как событие должно повторяться, например:

    {
        "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"
    }
  • Возвращает сгенерированный uid (случайный hex + @claude-mcp).

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

Обновляет все событие, идентифицируемое по uid (для повторяющихся событий это обновляет серию VEVENT, а не отдельный экземпляр).

  • Сохраняет любые пропущенные поля из исходного компонента.

  • location:

    • Если пропущено (null / не предоставлено), сохраняет существующее местоположение.

    • Если предоставлено как непустая строка, обновляет местоположение события.

    • Если предоставлено как пустая строка, очищает местоположение события.

  • recurrence:

    • Если предоставлено, заменяет любое существующее RRULE, используя ту же структуру, что и в create_event.

  • clear_recurrence:

    • Если True, удаляет любое RRULE и преобразует событие обратно в одиночный неповторяющийся экземпляр.

    • Если True и recurrence также предоставлено, clear_recurrence имеет приоритет (повторения не будет).

  • Возвращает True при успехе, False, если uid не найден в окне ±3 года.

delete_event(calendar_name_or_url, uid) -> bool

Удаляет первое найденное событие с указанным uid в окне ±3 года.

  • Возвращает True, если удалено, False, если не найдено.

Примечания по дате/времени

  • Принимает наивные даты или даты с Z/смещением (YYYY-MM-DDTHH:MM:SS, опционально Z или -04:00 и т.д.)

  • Новые/отредактированные события используют DTSTART;TZID=... и DTEND;TZID=... с использованием предоставленного tzid или переменной окружения TZID

  • Обновления пытаются повторно использовать исходный TZID, если он присутствует

  • LOCATION добавляется, когда location предоставлено и не пусто; передача пустой строки при обновлении события удаляет существующее местоположение.


Справочник инструментов почты

Включите с помощью MAIL_ENABLED=1. Использует те же APPLE_ID и ICLOUD_APP_PASSWORD, что и календарь. Дополнительных зависимостей нет — чистая стандартная библиотека Python (imaplib, smtplib).

list_mailboxes() -> List[{name}]

Возвращает все папки IMAP (INBOX, Sent, Drafts, Junk, Deleted Messages и т.д.).

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

Возвращает заголовки последних сообщений (до limit). Каждый элемент:

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

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

Получает полное сообщение, включая декодированное тело (предпочтительно text/plain, HTML удаляется как запасной вариант). Возвращает:

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

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

IMAP-поиск по TEXT — ищет по теме и телу письма. Возвращает те же поля заголовков, что и list_messages.

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

Отправляет через SMTP (STARTTLS на порту 587). to и cc могут быть разделены запятыми. Возвращает True при успехе.

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

Копирует в корзину (по умолчанию Deleted Messages, можно переопределить через ICLOUD_TRASH_FOLDER), затем удаляет. Возвращает True при успехе.

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

Устанавливает или снимает флаг \Seen. Возвращает True при успехе.


Режим Deep Research только для чтения

Установите DR_PROFILE=1 для запуска набора инструментов только для чтения для Deep Research. Это открывает только:

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

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

Пример:

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

Примечания:

  • Инструменты записи (list_events/create_event/update_event/delete_event) в этом режиме отключены.

  • SCAN_DAYS управляет окном поиска вокруг «сейчас» (по умолчанию: 1095 дней ≈ 3 года).

  • Держите этот сервис приватным или добавьте аутентификацию.


Пример (программный клиент)

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

Развертывание / Публичный HTTPS

Чтобы использовать это с пользовательскими коннекторами Claude, вам нужен публичный HTTPS-эндпоинт, который перенаправляет запросы на ваш локальный сервер.

См. DEPLOY.md для:

  • Cloudflare Tunnel (стабильное имя хоста, бесплатно)

  • ngrok (быстрый тест)

  • VPS + Caddy/Nginx (постоянно)

Безопасность: добавьте аутентификацию (Cloudflare Access, прокси с Basic Auth, белый список IP). НЕ выставляйте это в сеть без аутентификации; сервис имеет доступ на запись в ваш календарь. Вам нужен публичный HTTPS URL, который перенаправляет на ваш локальный http://127.0.0.1:8000.


Устранение неполадок

Симптом

Вероятная причина / Решение

401 Unauthorized

Неверный Apple ID или пароль приложения; убедитесь, что в .env используется email, а не телефон.

Пустые результаты событий

Неверный URL календаря или временное окно; помните, что end не включительно.

Обновление/удаление не работает

UID не в окне сканирования ±3 года или календарь отличается от того, который вы запрашиваете.

Смещение часового пояса

Передайте tzid явно (например, America/New_York) или используйте UTC ...Z.


Безопасность

  • Используйте пароли приложений и меняйте их по мере необходимости

  • Держите этот сервер приватным (ACL туннеля, белые списки IP, прокси с аутентификацией)

  • Этот проект перезаписывает минимальные VEVENT; расширенные поля (участники, напоминания, исключения повторений) не сохраняются при обновлении


Лицензия

MIT License.


Удачного планирования, надеюсь, это поможет!

-
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