icloud-mcp
icloud-mcp
Unified Apple/iCloud MCP server for Calendar, Contacts, and Mail: one static Go binary, Model Context Protocol JSON-RPC on stdio.
Remote protocols only (CalDAV, CardDAV, IMAP, SMTP with app-specific
passwords). Not macOS EventKit, AppleScript, browser automation, or a private
Apple API. Runs headless on Linux and macOS; pure Go also builds for Windows
(CI smoke-builds windows/amd64; GitHub Release archives ship linux/amd64,
linux/arm64, and darwin/arm64). Suitable for agents and orchestration, not only
a desktop chat app.
Host-agnostic. Any MCP client that can spawn a child with an environment and
wire stdin/stdout works: personal agents, Hermes, OpenClaw, IDE bridges,
runners, or other stdio hosts. No preferred model vendor, chat product, or
reseller. Configuration is the process environment only; the binary does not
parse host-specific config files or .env.
Domain | Protocol | Default |
Calendar | CalDAV HTTPS | Always on (read + write unless global read-only) |
Contacts | CardDAV HTTPS | Off until |
Mail read | IMAP TLS | Off until |
Mail mutation | IMAP | Off until Mail + |
Mail send | SMTP STARTTLS | Off until Mail + |
Reminders, Notes, Photos, Drive, Messages, and similar apps are out of scope until Apple documents a suitable remote third-party connector. Details: Supported scope.
Quick start
go install github.com/ThomasCrouzet/icloud-mcp/cmd/icloud-mcp@latest
# or: make build
# or: make release VERSION=v0.4.0
# or: make release-all VERSION=v0.4.0Create an app-specific password (never the main Apple Account password).
Export a minimal environment (recommended first deploy: Calendar read-only):
export ICLOUD_EMAIL='you@icloud.com'
export ICLOUD_PASSWORD='your-app-specific-password'
export ICLOUD_MCP_READ_ONLY=true
export ICLOUD_MCP_DEFAULT_TZ=Europe/Paris # owner IANA zone; default UTCRegister command = absolute path to
icloud-mcpand this env in your MCP host (YAML, JSON, TOML, UI, or orchestrator; format is host-specific).Stdio = JSON-RPC; stderr = logs and mutation audit. Reload the host after env changes.
With that config the process exposes 7 tools (Calendar reads + local
helpers + icloud_capabilities). No Contacts or Mail client is constructed.
Optional domains (still host-agnostic export form):
# Contacts reads (writes also need ICLOUD_MCP_READ_ONLY=false)
export ICLOUD_MCP_ENABLE_CONTACTS=true
# Mail reads: IMAP identity may differ from ICLOUD_EMAIL (e.g. name@icloud.com)
export ICLOUD_MCP_ENABLE_MAIL=true
export ICLOUD_MAIL_ADDRESS='mailbox@icloud.com'
# export ICLOUD_MAIL_PASSWORD='...' # optional; else copy of ICLOUD_PASSWORD
# Mail mutation (flags / move / trash); independent of send
export ICLOUD_MCP_ENABLE_MAIL_WRITE=true
export ICLOUD_MCP_READ_ONLY=false
# Mail send: requires exact recipient allowlist even under global read-only
export ICLOUD_MCP_ENABLE_MAIL_SEND=true
export ICLOUD_MCP_SMTP_ALLOWED_RECIPIENTS='alice@example.com,bob@example.net'Boot-only file:// secrets (regular files only, at most 4 KiB, mode 0600 or
stricter; never re-read after start):
export ICLOUD_EMAIL='file:///run/secrets/icloud-email'
export ICLOUD_PASSWORD='file:///run/secrets/icloud-password'
export ICLOUD_MAIL_ADDRESS='file:///run/secrets/icloud-mail-address'
export ICLOUD_MAIL_PASSWORD='file:///run/secrets/icloud-mail-password'See .env.example for the full 12-variable contract.
MCP tools
Maximum 23 tools. Disabled tools are absent from tools/list; disabled
domain clients are not constructed.
Count | When |
10 | Default: Calendar read+write + |
7 | Global read-only, optional domains off (recommended first run) |
23 | Contacts + Mail read + mutation + send, read-only off |
ICLOUD_MCP_READ_ONLY=true removes every Calendar/Contacts write, every Mail
mutation, and Mail send. It does not enable a disabled read domain.
Group | Tools |
Global |
|
Calendar read |
|
Calendar write |
|
Contacts read |
|
Contacts write |
|
Mail read |
|
Mail mutation |
|
Mail send |
|
Highlights: occurrence-aware Calendar update/delete with strong If-Match;
Contacts opaque book IDs and vCard 3.0 writes; Mail identity
(mailbox, UIDVALIDITY, UID), PEEK reads, SMTP exact-recipient policy.
set_message_flags fails closed with protocol_error when CONDSTORE is
advertised and tagged MODIFIED cannot be observed (go-imap beta.8). Full
behavior notes: docs/caldav-compatibility.md,
docs/carddav-compatibility.md,
docs/mail-compatibility.md.
Idempotency: create_event / create_contact use server-side UID keys
(client_uid or alias idempotency_key): a repeat create conflicts if the UID
already exists (never silent overwrite). update_event / update_contact
optional idempotency_key is process-local only (in-memory cache, 15
minute TTL, cleared on process restart). Same key + same params returns the
cached success; same key + different params is conflict. Prefer combining
update keys with a strong etag. See docs/error-codes.md.
Configuration
Exactly 12 product environment variables:
Variable | Default | Contract |
| none | Required Calendar/Contacts identity. |
| none | Required app-specific password; Mail fallback. |
|
| Global mutation kill switch. |
|
| Stderr level; accepted forms are documented below. |
|
| IANA zone for offset-less Calendar inputs and recurring-write fallback. |
|
| Contacts tools; writes only if not read-only. |
|
| Mail reads; requires Mail address/password. |
| none | Full IMAP/SMTP address when Mail is on. |
|
| Optional dedicated Mail app password. |
|
| Three IMAP mutation tools. |
|
|
|
| none | Required if send requested: exact addresses, or literal |
Booleans accept only unset, 0, false, 1, or true. Invalid values fail at
boot. Config is validated before any network access: Mail write/send without
Mail, Mail without address/password, or send without recipient policy are boot
errors (including under read-only for the send policy). Global read-only can
coexist with write/send flags but suppresses their registration.
Log levels are trimmed and case-insensitive: debug/-4, info,
warn/warning/2, and error/4. Unset or unrecognized values use info.
Flags: -version; optional -health 127.0.0.1:port (loopback-only /healthz
and /status JSON with domains and rate limits); optional
-audit-format=json|text (default json mutation audit on stderr).
Dates
Calendar input
start/end: RFC3339 with offset, or wall clock without offset inICLOUD_MCP_DEFAULT_TZ. Prefer no-offset for the user's local time. Recurring or explicit-timezone creates write TZID + VTIMEZONE; non-recurring timed defaults to UTCZon the wire. All-day usesVALUE=DATE.Calendar output: timed events always use RFC3339 with an explicit numeric offset in
ICLOUD_MCP_DEFAULT_TZ(never bareZ). All-day dates areYYYY-MM-DD. Seecalendar_capabilities.outputFormat.Contacts birthdays: write
YYYY-MM-DDonly.Mail search:
sinceinclusive,beforeexclusive (YYYY-MM-DD).
Agent error codes and retry policy: docs/error-codes.md. Host wiring examples: docs/agent-hosts.md. Product roadmap: ROADMAP.md.
Security (summary)
Untrusted remote text can influence an LLM on the host; labels are not a boundary. A compromised model can call every registered tool. Same model for every host and vendor.
Egress fixed: Calendar
caldav.icloud.com/p[0-9]{1,3}-caldav.icloud.com:443; Contacts matching contacts hosts; IMAPimap.mail.me.com:993; SMTPsmtp.mail.me.com:587with mandatory STARTTLS. No configurable destinations, no proxy env for DAV, TLS 1.2+ verified.Isolation: separate credentials, transports/dialers, limiters, semaphores, and protocol stacks per domain; no union authenticated HTTP client.
Secrets: redacted (including Basic and SASL PLAIN forms); boot-only
file://reads require mode 0600 or stricter; noos/exec, telemetry, or disk write after boot.Audit: mutations log
domain,resourceType, process-local HMACresourceTokenonly (never raw paths, UIDs, mailboxes, recipients).Residual risk: one process holds every enabled domain's credentials; feature flags do not remove compiled code. Prefer read-only, least domains, dedicated Mail password, or separate processes when stronger isolation is required.
Full policy: SECURITY.md, docs/security.md. Architecture: docs/architecture.md.
Limits (summary)
Tool deadline | 25s (DAV HTTP 30s) |
Stdio / MCP result | 1 MiB frame; 256 KiB result; reflected protocol errors capped |
Calendar | 366-day search; 400 returned / 2,500 per calendar / 10,000 multi-calendar materialization; 2,000 expansions / 100k steps per series / 250k steps per search; 60 read / 20 write per minute; concurrency 4 / 2 |
Contacts | 100 books; 100 summaries; 2000 cards scanned; 60/20 per minute; concurrency 4 |
60 read / 20 mutation / 20 send per minute; semaphores 2 / 1 / 1; no mutation/send retry | |
Writes | No automatic replay of Calendar PUT/DELETE, Contacts writes, IMAP mutations, or SMTP; ambiguous outcomes use |
Lifecycle: eager Calendar discovery at boot; lazy Contacts discovery; fresh IMAP/SMTP session per call. Rates, XML/iCal/vCard/MIME budgets, and retry rules: docs/testing.md, docs/architecture.md.
Supported scope
Data | Connector | Support |
Calendar | CalDAV | Always |
Contacts | CardDAV | Optional |
Mail read / mutation / send | IMAP + SMTP | Optional, independently gated |
Modern Reminders, Notes, Photos, Drive, Find My, Keychain, Messages, Home | No suitable official remote connector for this model | Excluded |
Modern Reminders are not treated as generic CalDAV VTODO. Apple's third-party documentation for this class of access covers Mail, Calendar, and Contacts.
Multi-account: one process holds one iCloud identity. Spawn separate
icloud-mcp processes (distinct env, optional distinct -health ports) and let
the MCP host multiplex them. See docs/agent-hosts.md.
Dependencies
Go 1.25.12 or newer, one module, exactly 10 direct dependencies. Adding another requires a written justification here.
Dependency | Exact version | Justification |
|
| CalDAV primitives; discovery and conditional ops stay hand-rolled |
|
| iCalendar parse/encode |
|
| MCP stdio, schemas, JSON-RPC |
|
| Bounded recurrence with timezone preservation |
|
| Per-domain rate limiters |
|
| vCard 3.0/4.0 read, 3.0 write |
|
| IMAP behind |
|
| MIME / plain-text bounds |
|
| SMTP + STARTTLS |
|
| SASL PLAIN after STARTTLS |
Build and test
make build # local host binary, VERSION defaults to dev
make test # go test ./... -race -cover
make lint # go vet + pinned golangci-lint
make release VERSION=v0.4.0 # packaged linux/arm64, digest-pinned Go 1.25.12 image
make release-all VERSION=v0.4.0 # packaged linux/amd64, linux/arm64, darwin/arm64 (host Go)
make install # host-compatible build to INSTALL_DIR (default ~/.local/bin)Release targets reject an unset or dev version. Archives contain the binary,
LICENSE, and THIRD_PARTY_NOTICES.md; dist/ also receives a SHA-256
checksum file. GitHub tag releases run make release-all only after CI and
gitleaks succeed on that tag, with Go pinned to 1.25.12 (check-latest
disabled). Local make release remains the digest-pinned container path for
linux/arm64. Cosign keyless signatures are attached to release blobs. -version
prefers the release ldflags value and falls back to Go module build information,
so go install ...@version reports that module version.
CI: race tests, coverage floors (78% aggregate + package floors including
cmd/icloud-mcp and internal/health), fuzz smoke, govulncheck, multi-arch
build (plus windows/amd64 smoke), egress/security AST guards, gitleaks, 20 MiB
binary budget, public-text policy on tree and new commits. Live iCloud tests use
the integration build tag, are opt-in, and never run in CI. See
docs/testing.md.
Attribution
Calendar tool shape and several patterns were inspired by
github.com/roygabriel/mcp-icloud-calendar
(MIT, copyright 2026 Gabe). Code was rewritten, not copied. This server adds
hard per-domain egress, redaction, bounded parsers, conditional mutation,
Contacts, Mail, and no telemetry.
Contributing
See CONTRIBUTING.md. Run gofmt, make test, and make lint
before opening a pull request.
License
MIT. See LICENSE and third-party notices.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/ThomasCrouzet/icloud-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server