Skip to main content
Glama
stefanikostic

iCloud Reminders and Calendar MCP Server

README.md
# iCloud Reminders and Calendar MCP Server

Lets Claude read and manage your iCloud Reminders and Calendar through a custom MCP connector — ask Claude "what's on my groceries list" or "add milk and eggs" or "What do I need to do today"
from any Claude client, including the iPhone app.

## Architecture

```
Claude (iPhone/desktop/web)
        |  HTTPS + API key
        v
Anthropic's cloud infrastructure
        |  HTTPS
        v
EC2 instance (Caddy: auto HTTPS via sslip.io + Let's Encrypt)
        |  localhost
        v
Python MCP server (this repo)
        |  CalDAV
        v
iCloud (your Reminders)
```

Custom MCP connectors are reached by Anthropic's cloud, not by your device
directly — so the server has to be genuinely public, not just reachable on
your home network. This deployment gets that for **$0/month** (within AWS
free tier) using:
- **EC2 free tier** (750 hrs/month for 12 months) instead of a paid VPS
- **sslip.io** for a free "domain" (no purchase, no signup) instead of a
  real registered domain
- **Caddy** for free automatic HTTPS via Let's Encrypt
- **CalDAV** to talk to iCloud directly — no Mac, no AppleScript required

## Why CalDAV instead of AppleScript

Apple Reminders has no public REST API. AppleScript works but only runs
locally on a Mac. CalDAV is an open standard Apple also supports for
Calendar/Reminders sync, reachable from any OS or cloud server with just an
Apple ID and an app-specific password — which is what makes a Mac-free,
cloud-hosted version of this possible at all.

---

## Setup

### 1. Generate an app-specific Apple ID password

1. Go to https://appleid.apple.com and sign in
2. **Sign-In and Security** → **App-Specific Passwords** → generate one
3. Save it — you'll need it in step 4. This is not your normal Apple ID
   password, and can be revoked independently at any time.

### 2. Launch an EC2 instance

1. AWS Console → EC2 → **Launch instance**
2. Choose **Ubuntu Server** (latest LTS), instance type **t3.micro** or
   **t2.micro** (free tier eligible)
3. Create or select a key pair (for SSH access) — download the `.pem` file
4. Security group: allow inbound **SSH (22)** from your IP, and
   **HTTP (80)** + **HTTPS (443)** from anywhere (`0.0.0.0/0`) — port 80 is
   needed briefly for Let's Encrypt's verification, Caddy handles the rest
5. Launch it

### 3. Attach an Elastic IP (so the address doesn't change on reboot)

1. EC2 → **Elastic IPs** → **Allocate Elastic IP address**
2. **Actions** → **Associate** → select your instance
3. Note this IP — you'll need it for the sslip.io hostname

### 4. SSH in and set up the server

```bash
ssh -i your-key.pem ubuntu@YOUR-ELASTIC-IP

sudo apt update
sudo apt install -y python3-venv python3-pip git

git clone https://github.com/YOUR_USERNAME/reminders-mcp.git
cd reminders-mcp

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

cp .env.example .env
nano .env   # fill in APPLE_ID, APPLE_APP_PASSWORD, and a generated MCP_API_KEY
```

Generate a strong API key with:
```bash
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
```

### 5. Install and configure Caddy

```bash
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
```

Edit the Caddyfile to use your actual Elastic IP (dots replaced with
dashes), e.g. if your IP is `54.123.45.67`:

```bash
sudo nano /etc/caddy/Caddyfile
```

```
54-123-45-67.sslip.io {
    handle_path /REPLACE_WITH_YOUR_MCP_API_KEY/* {
      reverse_proxy localhost:8787 {
        header_up x-api-key "YOUR_MCP_API_KEY"
      }
    }
    respond 404
}
```

```bash
sudo systemctl restart caddy
```

Caddy will automatically get a free HTTPS certificate for that hostname the
first time it starts — no manual steps needed.

### 6. Set up the systemd service (so it survives reboots)

```bash
sudo cp reminders-mcp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable reminders-mcp
sudo systemctl start reminders-mcp
sudo systemctl status reminders-mcp   # confirm it's running
```

### 7. Test it's reachable

```bash
curl -i https://54-123-45-67.sslip.io/{YOUR_MCP_API_KEY}/mcp
```

{YOUR_MCP_API_KEY} will be extracted from the path and Caddy will inject it as a request header.
A response other than a connection error means it's working (a `406` here
is expected and fine — it just means the request wasn't a full valid MCP
handshake, which curl alone won't do).

### 8. Add the connector on claude.ai

Custom connectors can only be **added** via a browser or Claude Desktop —
not from the iPhone app directly. Once added, it's usable everywhere,
including mobile.

1. Go to claude.ai → **Customize → Connectors** → **"+"** → **Add custom
   connector**
2. Name: `iCloud Reminders`
3. URL: `https://54-123-45-67.sslip.io/{YOUR_MCP_API_KEY}/mcp`
5. Click **Add**, then enable it in a conversation

### 9. Use it from your iPhone

Open the Claude app, start a conversation, enable the connector from the
"+" menu if it isn't already, and try:

- "What's on my Groceries list?"
- "Add milk and eggs to Groceries"
- "Mark milk as done on Groceries"
- "What do I need to do today?"

---

## Tools this server exposes

| Tool | What it does |
|---|---|
| `list_reminder_lists()` | Names of all your iCloud Reminders lists |
| `get_pending_reminders(list_name)` | Incomplete items in a given list |
| `add_reminder(list_name, title)` | Adds a new reminder to an **existing** list |
| `complete_reminder(list_name, title)` | Marks an item done |

Note: `add_reminder` requires the list to already exist — create it once in
the Reminders app on any Apple device and it syncs to iCloud automatically.
Reliably creating brand-new top-level lists via CalDAV varies across iCloud
accounts, so it's left as a one-time manual step.

## Security

- The server requires a valid `YOUR_MCP_API_KEY` header in every request path — without it, requests are rejected with 401. Treat this key like a password.
- Your Apple app-specific password only grants limited access and can be
  revoked anytime from appleid.apple.com without touching your main
  password.
- The EC2 security group only allows inbound 22/80/443 — the MCP server's
  actual port (8787) is only reachable via `localhost`, through Caddy.
- Never commit your real `.env` file — `.gitignore` is already set up to
  exclude it. Only `.env.example` (placeholder values) should be in git.

## Cost

Within the AWS Free Tier (first 12 months): **$0/month**.
- EC2 t3.micro/t2.micro: free (750 hrs/month covers 24/7 use)
- Elastic IP: free while attached to a running instance
- sslip.io hostname: free, no signup
- Let's Encrypt certificate via Caddy: free, auto-renewing

After free tier expires, EC2 alone runs roughly $7–9/month if left running
continuously — stop the instance when not in use to avoid ongoing charges.

## Troubleshooting

- **Caddy fails to get a certificate** → confirm port 80 is open in your
  security group (Let's Encrypt needs it briefly for verification), and
  that the sslip.io hostname matches your Elastic IP exactly.
- **401 Unauthorized from your own curl test** → double-check the
  `YOUR_MCP_API_KEY` in URL `https://54-123-45-67.sslip.io/{YOUR_MCP_API_KEY}/mcp` matches `MCP_API_KEY` in `.env` exactly, no
  trailing whitespace.
- **Service won't start** → `sudo journalctl -u reminders-mcp -e` to see
  the actual error; usually a missing/incorrect `.env` value.
- **Claude says the connector won't connect** → confirm you can reach the
  HTTPS URL from a browser or curl first, before troubleshooting on the
  Claude side.

## Next steps

- Add a Calendar tool (`get_todays_events`) using the same CalDAV
  connection, so Claude can start combining "what's on my calendar" with
  "what's on my todo list" into a single daily briefing.