Skip to main content
Glama
Lingnik

icloud-reminders-mcp

by Lingnik
README.md
# icloud-reminders-mcp

A local [MCP](https://modelcontextprotocol.io) server that gives an AI agent
(e.g. Claude Code) **read + write access to Apple Reminders** over **iCloud
CalDAV** — no Mac required. It talks only to Apple's servers and the local MCP
client, and manages the same reminder lists that sync to your iPhone/iPad,
including lists shared via Family Sharing.

Reminders are `VTODO` items in CalDAV; each reminder *list* is a task-capable
calendar collection. Calendar events (`VEVENT`), Contacts, Mail, and Notes are
out of scope.

> [!IMPORTANT]
> **Viability depends on your account.** Since iOS 13 / macOS 10.15, Apple
> moved Reminders to a newer (CloudKit) store. **Only reminder lists that were
> never "upgraded" to the new format remain reachable over CalDAV.** On some
> accounts that is *zero* lists. Run `scripts/doctor.py` (below) once before
> relying on this — it is a go/no-go probe. Apple may remove CalDAV access at
> any time; this is an undocumented, partially-compliant surface.

## Why CalDAV (and not `pyicloud`)

Apple exposes no REST/JSON API for Reminders. CalDAV (RFC 4791) is the only
official-protocol, cross-platform route. Auth is a **static app-specific
password** — no interactive 2FA, no token refresh. We deliberately avoid
`pyicloud` (a private web API that broke on Apple's SRP-6a auth change).

## Requirements

- Python **3.11+**
- An Apple ID with an **app-specific password** (see below)
- [`uv`](https://docs.astral.sh/uv/) (recommended) or `pip` + `venv`

## Install

```bash
git clone https://github.com/Lingnik/icloud-reminders-mcp
cd icloud-reminders-mcp
uv venv && uv pip install -e ".[dev]"
```

## Generate an app-specific password

1. Sign in at [appleid.apple.com](https://appleid.apple.com).
2. **Sign-In & Security → App-Specific Passwords → Generate**.
3. Label it (e.g. `icloud-reminders-mcp`). You get `xxxx-xxxx-xxxx-xxxx`.
4. This is the **kill switch**: revoke it there to instantly cut access.
   Changing your Apple ID password also invalidates it.

> An app-specific password is **all-or-nothing** — it grants access to your
> whole iCloud account at Apple's end, not just Reminders. Treat it as a
> secret, and see [SECURITY.md](SECURITY.md).

## Configure

Configuration is via environment variables only (never committed). Copy
[`.env.example`](.env.example) to `.env` and fill it in, **or** — preferred on a
host with [1Password CLI](https://developer.1password.com/docs/cli/) — keep the
real values in 1Password and inject them at launch:

```bash
# ~/.secrets holds op:// references, not secrets:
#   ICLOUD_USERNAME="op://<vault>/icloud-reminders-mcp/username"
#   ICLOUD_APP_PASSWORD="op://<vault>/icloud-reminders-mcp/password"
op run --env-file ~/.secrets -- icloud-reminders-mcp
```

| Variable | Required | Default | Purpose |
|---|---|---|---|
| `ICLOUD_USERNAME` | yes | — | Apple ID email |
| `ICLOUD_APP_PASSWORD` | yes | — | App-specific password (`xxxx-xxxx-xxxx-xxxx`) |
| `ICLOUD_CALDAV_URL` | no | `https://caldav.icloud.com/` | Root URL; the library discovers your account's `pNN` shard from it — do not hardcode a shard |
| `REMINDERS_ALLOW_DELETE` | no | `false` | Enable the destructive `delete_reminder` tool |
| `REMINDERS_LIST_ALLOWLIST` | no | *(all)* | Comma-separated list names the server may touch |
| `ICLOUD_REQUEST_TIMEOUT` | no | `30` | Per-request timeout (seconds) |

## Go/no-go probe

Run once against your real account to confirm CalDAV reachability and a full
create → read → complete → delete round-trip:

```bash
op run --env-file ~/.secrets -- python scripts/doctor.py --list "Reminders"
```

It fails loudly if no VTODO-capable list is found, and reports which fields
survived the round-trip. It is never run in CI. After it succeeds, confirm the
throwaway reminder appeared (and cleared) on your iPhone.

## Use with Claude Code

Add to your MCP config (e.g. `~/.claude/mcp.json` or via `claude mcp add`).
Using 1Password so no secret is written into the config file:

```json
{
  "mcpServers": {
    "icloud-reminders": {
      "command": "op",
      "args": [
        "run", "--env-file", "/home/you/.secrets", "--",
        "uv", "--directory", "/home/you/git/icloud-reminders-mcp", "run",
        "icloud-reminders-mcp"
      ]
    }
  }
}
```

If you would rather put values inline (less safe), set them under `"env"` and
call `uv` directly:

```json
{
  "mcpServers": {
    "icloud-reminders": {
      "command": "uv",
      "args": ["--directory", "/home/you/git/icloud-reminders-mcp", "run", "icloud-reminders-mcp"],
      "env": {
        "ICLOUD_USERNAME": "you@icloud.com",
        "ICLOUD_APP_PASSWORD": "xxxx-xxxx-xxxx-xxxx",
        "REMINDERS_ALLOW_DELETE": "false"
      }
    }
  }
}
```

## Tools

| Tool | Type | Purpose |
|---|---|---|
| `list_lists` | read | Reminder lists (`list_id`, `list_name`, url, count) |
| `list_reminders` | read | Filter by list / completed / due range; paginated (`limit`/`offset`) |
| `get_reminder` | read | One reminder by uid |
| `create_reminder` | write | New VTODO (title, list, due, notes, priority, url) |
| `complete_reminder` | write | Mark complete |
| `update_reminder` | write | Patch fields; preserves everything it doesn't manage |
| `delete_reminder` | **destructive** | Gated by `REMINDERS_ALLOW_DELETE` **and** `confirm: true` |

**Reminder JSON shape** (normalized from VTODO):

```json
{
  "uid": "…", "list_id": "…", "list_name": "Family TODOs", "list_url": "…",
  "title": "…", "notes": "…|null",
  "due": "2026-07-10T09:00:00-07:00|2026-07-10|null", "due_all_day": false,
  "completed": false, "completed_at": "…|null",
  "priority": 0, "priority_label": "none|low|medium|high",
  "percent_complete": 0, "url": "…|null",
  "created": "…", "modified": "…", "etag": "…"
}
```

Notes on the model:

- **Lists are keyed by `list_id`** (stable, URL-derived), with `list_name` as a
  mutable label. Tools accept either an id or a name.
- **Priority** is the raw iCalendar 0–9 integer plus a friendly label. Apple's
  UI only has none/low/medium/high and will collapse arbitrary integers to
  1/5/9 when you edit in the app — fidelity beyond the four buckets is not
  guaranteed.
- **Updates preserve unknown properties.** `update_reminder`/`complete_reminder`
  parse the existing VTODO and mutate only known fields, so `RRULE`, `VALARM`,
  `RELATED-TO` (subtasks), and `X-APPLE-*` extensions survive. Editing
  recurrence/subtasks is **not** supported in v1 (they're preserved, not
  managed).
- **Update field semantics:** an omitted/null field is left unchanged; pass an
  empty string (`""`) to clear `notes`, `due`, or `url`.

## Development

```bash
uv run ruff check .
uv run pytest
```

Tests are network-free: pure mapping/config unit tests plus a fake-CalDAV layer
exercising discovery filtering, pagination, ETag-conflict retry, and delete
gating. Fixtures are synthetic — never captured from a real account.

## Troubleshooting

- **No lists found / empty.** Most likely the CloudKit-upgrade issue above. Try
  a list you know is old, or one freshly created that you have not migrated.
- **401 / auth rejected.** You're using your Apple ID password, not an
  app-specific password, or it was revoked/expired. Generate a new one.
- **PROPFIND hangs / flaky connects.** A known iCloud quirk on dual-stack
  hosts; try forcing **IPv4 or IPv6** (e.g. disable one stack, or set a
  hosts/route preference) and retry. Raise `ICLOUD_REQUEST_TIMEOUT` if needed.
- **A field didn't stick.** iCloud may normalize or drop properties on write.
  The doctor script's round-trip report tells you what your account actually
  preserves.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A4.2/5.0

Scored across 7 tools

Disambiguation5/5

Each tool has a unique and clear purpose: complete, create, delete, get, list lists, list reminders, and update. No overlap or ambiguity.

Naming Consistency5/5

All tools follow a consistent verb_noun snake_case pattern (e.g., complete_reminder, list_reminders), making them predictable and easy to understand.

Tool Count5/5

7 tools is well-scoped for a reminders server, covering all essential CRUD operations plus list management without unnecessary bloat.

Completeness5/5

The set provides full lifecycle management for reminders: create, read, update (including complete/reopen), delete, and list. It also includes list listing, which is necessary for context.

Maintenance

ActivityStale
ResponsivenessUnresponsive