Skip to main content
Glama
alexey-max-fedorov

iCloud CalDAV MCP Connector

iCloud MCP 连接器

一个 HTTP 模型上下文协议 (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)

  • 日历工具(深度研究只读配置,DR_PROFILE=1):

    • search(query) → 在时间窗口内对 SUMMARY/DESCRIPTION 进行基础文本搜索

    • fetch(ids) → 获取搜索结果的原始 text/calendar ICS 数据块

  • 邮件工具(可选,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 生成(摘要/描述转义),在 ±3 年窗口内进行 UID 匹配


要求

  • Python 3.11+

  • Apple ID(电子邮件标识,非电话号码)

  • iCloud 应用专用密码(可撤销) — 一个密码同时适用于日历和邮件

  • https://caldav.icloud.com, imap.mail.me.com, smtp.mail.me.com 的网络访问权限


环境

server.py 旁边创建 .env 文件(自动加载):

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 — 显示名称或完整的 CalDAV URL

  • 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(随机十六进制 + @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

    • 如果提供,则使用与 create_event 中相同的格式替换任何现有的 RRULE。

  • clear_recurrence

    • 如果为 True,则删除任何 RRULE 并将事件转换回单个非重复实例。

    • 如果为 True 且同时提供了 recurrence,则 clear_recurrence 优先(无重复)。

  • 成功时返回 True,如果未在 ±3 年窗口内找到 uid,则返回 False

delete_event(calendar_name_or_url, uid) -> bool

删除 ±3 年窗口内第一个匹配的 uid

  • 如果已删除,返回 True;如果未找到,返回 False

日期/时间说明

  • 接受普通或 Z/偏移日期时间(YYYY-MM-DDTHH:MM:SS,可选 Z-04:00 等)

  • 新建/编辑的事件使用提供的 tzidTZID 环境变量发出 DTSTART;TZID=...DTEND;TZID=...

  • 更新时会尝试在存在时重用原始 TZID

  • 当提供 location 且非空时会发出 LOCATION;在更新事件时传递空字符串会删除现有位置。


邮件工具参考

通过 MAIL_ENABLED=1 启用。使用与日历相同的 APPLE_IDICLOUD_APP_PASSWORD。无额外依赖 — 纯 Python 标准库 (imaplib, smtplib)。

list_mailboxes() -> List[{name}]

返回所有 IMAP 文件夹(收件箱、已发送、草稿、垃圾邮件、已删除邮件等)。

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 发送(端口 587 上的 STARTTLS)。tocc 可以用逗号分隔。成功时返回 True

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

复制到废纸篓(默认为 Deleted Messages,可通过 ICLOUD_TRASH_FOLDER 覆盖)然后清除。成功时返回 True

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

设置或清除 \Seen 标志。成功时返回 True


深度研究只读模式

设置 DR_PROFILE=1 以运行用于深度研究的只读工具集。这仅公开:

  • 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 白名单)。不要在未经身份验证的情况下公开此服务;它拥有实时日历写入权限。 您需要一个转发到本地 http://127.0.0.1:8000 的公共 HTTPS URL。


故障排除

症状

可能的原因 / 修复

401 Unauthorized

Apple ID 或应用专用密码错误;确保 .env 使用的是电子邮件,而不是电话号码。

空的事件结果

日历 URL 或时间窗口错误;请记住 end 是排他的。

更新/删除无操作

UID 不在 ±3 年扫描窗口内,或者与您查询的日历不同。

时区偏移

显式传递 tzid(例如 America/New_York)或使用 UTC ...Z


安全性

  • 使用应用专用密码并根据需要轮换

  • 保持此服务器私有(隧道 ACL、IP 白名单、身份验证代理)

  • 此项目重写最小化的 VEVENT;高级字段(与会者、提醒、重复异常)在更新时不会被保留


许可证

MIT 许可证。


祝您日程安排愉快,希望这对您有帮助!

-
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