Apple Reminders MCP
README.md
# Apple Reminders MCP
A local stdio MCP server that gives a model controlled read/write access to macOS Reminders: lists, reminders, dates, repeats, early reminders, priority, flags, notes, and URLs.
macOS only. Built against macOS 27 with Swift 6.4.
## Install
### As an extension bundle (recommended)
```bash
npm install && npm run package:mcpb
```
That produces `apple-reminders-mcp.mcpb`. Open it, or drag it into Claude Desktop's Extensions pane, the same way as `apple-calendar-mcp.mcpb` and `apple-mail-mcp.mcpb`.
### From source
```bash
npm install && npm run build
```
Then register it with your MCP client:
```json
{
"mcpServers": {
"apple-reminders": {
"command": "node",
"args": ["/absolute/path/to/apple-reminders-mcp/dist/index.js"]
}
}
}
```
The Swift helper is deliberately **not** code-signed with its own identity. An ad-hoc identity makes it a separate TCC subject, which breaks EventKit access inheritance (verified: signing makes `requestFullAccessToReminders` return `false`). Unsigned, it inherits the MCP client's permissions.
On first use your MCP client will need:
- **Reminders** access (System Settings → Privacy & Security → Reminders) — for everything.
- **Automation → Reminders** access — only if you use flags.
## Tools
Each tool carries a display name in Reminders' own vocabulary, which is what clients show instead of the raw function name.
| Shown as | Tool | What it does |
|---|---|---|
| Show Lists | `list_lists` | Every list with id, account, color, writability, and which is default |
| New List | `create_list` | New list, optional hex color |
| Edit List | `update_list` | Rename a list and/or change its color |
| Delete List | `delete_list` | Delete a list **and every reminder in it** — requires `confirm: true` |
| Find Reminders | `list_reminders` | Filter by list, completion, due window, text; `limit` with a `truncated` flag |
| Open Reminder | `get_reminder` | Full detail for one reminder |
| New Reminder | `create_reminder` | Create with any supported field |
| Edit Reminder | `update_reminder` | Patch any field, including checking off via `completed` |
| Move to List | `move_reminder` | Move a reminder to another list |
| Delete Reminder | `delete_reminder` | Delete one reminder |
Also exposed: `reminders://lists` and `reminders://capabilities` resources, and a `triage_reminders` prompt.
## Field mapping
How the Reminders app's detail panel maps onto this server:
| Reminders app | Field | Notes |
|---|---|---|
| Title | `title` | |
| Notes | `notes` | `""` clears |
| URL | `url` | `""` clears |
| Date / Time | `due`, `allDay` | `YYYY-MM-DD` → all-day; ISO datetime → timed; `""` clears |
| Repeat | `recurrence` | iCal RRULE without the `RRULE:` prefix |
| End Repeat | `recurrence` | the RRULE's `UNTIL=` or `COUNT=` |
| Early Reminder | `earlyReminderMinutes` | minutes before due; `null` clears |
| List | `list` / `move_reminder` | |
| Flag | `flagged` | AppleScript-backed, slow — see below |
| Priority | `priority` | `none` \| `low` \| `medium` \| `high` |
| checkbox | `completed` | |
| **Tags** | — | **not supported, see below** |
| **Urgent** | — | **not supported, see below** |
Every reminder comes back with both `due` (UTC, or `YYYY-MM-DD` when all-day) and `dueLocal`. Use `dueLocal` when telling a person when something is due.
### Checking off a repeating reminder
Checking off a repeating reminder **advances it to its next occurrence** instead of completing it — the same thing the Reminders app does. The response returns `completed: false` with a later due date. To end a series, clear `recurrence` first or delete the reminder.
## What is not supported, and why
Two fields in the Reminders UI have no API behind them on macOS 27. This was established by direct testing, not assumption:
**Tags.** `EKReminder` and `EKCalendarItem` expose no tag property (dumped via the Objective-C runtime: `class_copyPropertyList` returns no tag-shaped member, and `respondsToSelector:` is false for `tags` and `hashtags`). The Reminders AppleScript dictionary has no `tags` term either — its `reminder` class exposes only name, id, container, creation/modification date, body, completed, completion date, due date, allday due date, remind me date, priority, and flagged. Writing `#tag` into a title does **not** create a tag: a reminder created via EventKit titled `zzz mcp probe #zzmcpprobe` rendered in Reminders as literal text with no tag attached. Tags live in the Reminders group container, which is TCC-protected and unsafe to write directly (it is a synced CoreData store).
**Urgent.** The "Urgent" toggle (`Mark this reminder as urgent to set an alarm`) appears in neither EventKit nor the AppleScript dictionary. Toggling it in the UI produced no property this server can read or write.
If you need either field, it has to be set by hand in the Reminders app.
## Flags are slow
`flagged` is the one field EventKit cannot reach, so it goes through Reminders.app's AppleScript bridge. That bridge is slow and degrades with list size — measured on this machine: ~1s on a small list, ~15s for a global lookup by id, ~30s for a list holding hundreds of reminders.
So flag state is **opt-in**:
- Reads omit it unless you pass `includeFlagged: true`; otherwise `flagged` comes back `null` (meaning "not read", not "not flagged").
- Writes only pay the cost when you actually send `flagged`.
Raise `APPLE_REMINDERS_MCP_TIMEOUT_MS` (default 120000) if you have very large lists.
## Safety
- Writes to read-only lists are refused.
- `APPLE_REMINDERS_MCP_ALLOW_LISTS` — comma-separated list names or ids. When set, writes are confined to those lists.
- `delete_list` requires `confirm: true` because it destroys every reminder in the list.
- Destructive tools carry `destructiveHint` annotations so clients can gate them.
## Verifying
```bash
npm run verify
```
That builds, runs the helper's self-tests (RRULE round trip, priority mapping, date components, id bridging, AppleScript escaping, input rejection), then runs an end-to-end smoke test that drives the real MCP server over stdio against throwaway lists and deletes them afterwards.
The smoke test covers all 10 tools in 27 assertions, including a guard that every tool advertises a usable JSON input schema. Set `SMOKE_SKIP_FLAGS=1` to skip the slow flag checks.
## Layout
```
helper/EventKitReminders.swift EventKit + AppleScript, JSON in on stdin, JSON out on stdout
src/helper.ts spawns the helper, one process per call
src/index.ts MCP tools, zod schemas, model-facing instructions
icon.png 256x256 bundle icon
scripts/make-icon.swift redraws icon.png; geometry and colours are
measured from the Reminders app icon, drawn in
CoreGraphics rather than copied as an asset
scripts/build-helper.sh swiftc build (intentionally unsigned)
scripts/helper-self-test.mjs pure-logic self-tests, touches no real data
scripts/smoke.mjs end-to-end test over stdio JSON-RPC
scripts/package.mjs builds apple-reminders-mcp.mcpb
scripts/package-check.mjs asserts the bundle has the runtime files, an
executable helper, and no dev artifacts
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues