fantastical
by sidbandy
README.md
# fantastical-mcp
An [MCP](https://modelcontextprotocol.io) server for creating calendar events in
[Fantastical](https://flexibits.com/fantastical) on macOS — and, crucially,
**verifying that each event actually landed on the intended day.**
Fantastical has no public API. It does expose an AppleScript `parse sentence`
command (the same natural-language parser as its quick-entry box), but that
command is effectively write-only: it can create events and list calendars, but
it cannot query events back. This server pairs that write path with a read-back
from the macOS calendar store (via EventKit), so a created event is confirmed
rather than assumed.
Built for use from **Claude Desktop** (or any MCP client that runs local stdio
servers). It cannot run in a browser-based client — it drives a native macOS app.
## Why the verification matters
Fantastical's parser reads the whole sentence, so it can silently reinterpret
input — consume part of a title as a time, or route an event to the wrong
calendar — and it reports no error when it does. On top of that, Fantastical
fuses three backends (real calendars, Todoist projects, Reminders lists) into one
list with colliding names, and `calendarName` does fuzzy matching, so a partial
name can land an event somewhere unexpected.
This server treats the model's parse as untrusted and checks it mechanically:
- **All date math happens in code** (Luxon), never in the model. `resolve_date`
and `validate_item` block a write on a weekday that disagrees with the date
(source says "Thursday" but the date is a Wednesday), an impossible date
(Feb 30), a past date, a two-digit-year misread, or an end before the start.
- **Write, then read back.** After every write, the event is read out of the
macOS calendar store and compared to what was intended. Anything that can't be
confirmed is reported as `UNVERIFIED` — never as success.
- **No AppleScript injection.** Every value (titles may be arbitrary pasted text)
is passed as an argument, never interpolated into a script.
- **Verifiable calendars only.** The server cross-references Fantastical's list
against the macOS store and flags which titles are real, writable event
calendars versus Todoist/Reminders look-alikes.
## Tools
| Tool | Purpose |
|---|---|
| `resolve_date` | Resolve a date interpretation to an absolute date **and its weekday**, in code. |
| `validate_item` | Mechanically validate a parsed event; blocks the write on any inconsistency. |
| `preview_event` | Render the exact sentence Fantastical will parse, without writing. |
| `create_event` | Create the event, then read it back and confirm the date. |
| `list_calendars` | List calendars, marking which are safe to write to and verify. |
| `verify_events` | Read events back from the macOS calendar store over a window. |
## Requirements
- macOS with [Fantastical](https://flexibits.com/fantastical) installed.
- Node.js ≥ 18 and the Swift toolchain (ships with Xcode / Command Line Tools).
- Calendars used for events must exist in the macOS calendar store (System
Settings → Internet Accounts), so the read-back can see them.
## Install & build
```sh
git clone https://github.com/sidbandy/fantastical-mcp.git
cd fantastical-mcp
npm install
npm run build # compiles the Swift EventKit reader and the TypeScript server
npm test # 20 date-correctness tests
```
`npm run build` produces `bin/ekread` (a small, read-only EventKit binary, ad-hoc
signed with an embedded usage description) and `dist/` (the compiled server).
## Register with Claude Desktop
Add the server to `~/Library/Application Support/Claude/claude_desktop_config.json`
(use absolute paths; find your node with `which node`):
```json
{
"mcpServers": {
"fantastical": {
"command": "/usr/local/bin/node",
"args": ["/absolute/path/to/fantastical-mcp/dist/index.js"]
}
}
}
```
Restart Claude Desktop. On first use, macOS will prompt once to let the client
control Fantastical (to write) and once to access Calendars (to verify) — allow
both. Then ask it, e.g., *"add a dentist appointment on July 20, 2026 from 3 to
4 PM."*
## How verification works (and its fallback)
The primary read-back is `bin/ekread`, an EventKit reader that answers in
milliseconds and returns JSON. EventKit requires Calendars permission attributed
to the host app; when granted, it is the fast, reliable path.
If EventKit is unavailable (e.g. the host process has no stable TCC identity), the
server falls back to reading via Calendar.app AppleScript — slower, but it keeps
verification working rather than silently degrading to "couldn't check."
## Design notes
- `src/dates.ts` — the correctness core (resolution, validation, sentence
building). Covered by `src/dates.test.ts`.
- `src/calendars.ts` — calendar discovery, the write/verify allowlist, and
read-back with matching.
- `src/osa.ts` — safe AppleScript/EventKit invocation (arguments only, no shell).
- `scripts/*.applescript` — the Fantastical and Calendar.app bridges.
- `swift/ekread.swift` — the read-only EventKit reader.
## Limitations
- macOS only, and requires the Fantastical desktop app.
- Cannot be used from browser-based MCP clients.
- Fantastical's natural-language parser is the write path; the server mitigates
its ambiguity with absolute sentences and post-write verification, but the
verification step is what provides the guarantee, not the sentence.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues