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?)— отправка через SMTPdelete_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 # OKMCP-эндпоинт: http://127.0.0.1:8000/mcp
Справочник инструментов (функциональные детали)
list_calendars() -> List[Calendar]
Возвращает:
name: str | nullurl: 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 CalDAVstart, end: str— ISO-даты; поиск выполняется в диапазоне [start, end)expand_recurring: bool— включать конкретные экземпляры повторяющихся серий
Возвращает каждое событие с полями:
uid: strsummary: strstart: 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.
Устранение неполадок
Симптом | Вероятная причина / Решение |
| Неверный Apple ID или пароль приложения; убедитесь, что в |
Пустые результаты событий | Неверный URL календаря или временное окно; помните, что |
Обновление/удаление не работает | UID не в окне сканирования ±3 года или календарь отличается от того, который вы запрашиваете. |
Смещение часового пояса | Передайте |
Безопасность
Используйте пароли приложений и меняйте их по мере необходимости
Держите этот сервер приватным (ACL туннеля, белые списки IP, прокси с аутентификацией)
Этот проект перезаписывает минимальные VEVENT; расширенные поля (участники, напоминания, исключения повторений) не сохраняются при обновлении
Лицензия
MIT License.
Удачного планирования, надеюсь, это поможет!
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