Skip to main content
Glama
alexey-max-fedorov

iCloud CalDAV MCP Connector

iCloud MCPコネクタ

iCloudのアプリ固有パスワードを使用して、iCloudサービスをMCP対応クライアント(Claudeカスタムコネクタ、IDEなど)に公開するHTTP Model Context Protocol (MCP) サーバーです。

サポート対象: iCloudカレンダー (CalDAV) + iCloudメール (IMAP/SMTP)。

非公式です。このサービスはAppleのサーバーにiCloudのアプリ固有パスワードを転送するため、非公開にしてください。


なぜこれを作ったのか?

Claudeカスタムコネクタで使用するために作成しました。手動で変更する代わりにiCloudカレンダーを変更できるようにするためです。TOP Psetの締め切り前の金曜の夜にこのアイデアを思いつき、楽しい1日のプロジェクトになりました。


機能

  • 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) → 検索結果の生の text/calendar ICSブロブを取得

  • メールツール (オプトイン, MAIL_ENABLED=1):

    • list_mailboxes() — すべてのフォルダをリスト表示

    • list_messages(mailbox, limit, unread_only) — ヘッダー付きでメッセージをリスト表示

    • get_message(uid, mailbox) — 本文を含むメッセージ全体を取得

    • search_messages(query, mailbox, limit) — IMAP TEXT検索

    • 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のエスケープ)、±3年間のウィンドウ内でのUIDマッチング


要件

  • Python 3.11+

  • Apple ID (メールアドレス形式、電話番号は不可)

  • iCloud アプリ固有パスワード (取り消し可能) — カレンダーとメールの両方で1つのパスワードを使用

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

指定された期間内に少なくとも1つのイベントを含むカレンダーのみを返します。

引数

  • 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 (ランダムな16進数 + @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 など) を受け入れます

  • 新規/編集されたイベントは、提供された tzid または TZID 環境変数を使用して DTSTART;TZID=... および DTEND;TZID=... を出力します

  • 更新時は、元のTZIDが存在する場合、それを再利用しようとします

  • LOCATIONlocation が提供され、空でない場合に出力されます。イベント更新時に空の文字列を渡すと、既存の場所が削除されます。


メールツールリファレンス

MAIL_ENABLED=1 で有効にします。カレンダーと同じ APPLE_IDICLOUD_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経由で送信します (ポート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 を返します。


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許可リスト)。認証なしでこれを公開しないでください。カレンダーへのライブ書き込みアクセス権を保持しています。 ローカルの 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