circal-mcp
v0.2.1
Published
MCP server for circal — read and write your calendar through its mirror file
Maintainers
Readme
circal-mcp
A stdio MCP server that reads and writes
circal's own mirror file — the .json a browser tab or the macOS app keeps
in sync on disk, through the File System Access API on the web and the
shell's own bridge in the app (see src/lib/mirror.ts, src/lib/mirrorLink.ts
and mac/main.swift's MirrorFile). It imports circal's own domain layer
(src/lib/events.ts, recur.ts, quickadd.ts, mutate.ts, backup.ts, …),
so an agent and the app can never disagree about what a legal event, an
occurrence, or a recurrence rule is.
Built on @modelcontextprotocol/server v2, implementing MCP protocol
revision 2026-07-28.
The browser tab does not need to be open. The mirror file is the only
thing this server reads or writes; if circal's tab is open and connected, it
picks up the change and its own next write moves the file's rev forward
again, the same as any second writer would.
Quick start
A FileSystemFileHandle (what a browser hands circal when the mirror is turned on there) never
exposes the full path it came from: that is deliberate browser privacy design, so circal cannot print
it for you. The macOS app does not have this problem — it mints and adopts a mirror at a fixed path
on first launch, no gesture required — so --find checks that path first, by name, then falls back
to the browser case and searches the places a mirror file usually lands.
npx -y circal-mcp --findIt prints every mirror file it located (path, event and calendar counts, zone, revision) plus a ready-to-paste config block. Paste that block into your client below, swap the path if it picked up the wrong file, then restart the client.
Use a locally built bundle, not
npx circal-mcp. The published[email protected]predates several fields insrc/lib(free,remindLead,organizerand the guest pair on an event;backupNudge,backupInterval,deviceZone,stampedZonesin settings). Every write round-trips the whole document through its reader, so it silently drops those nine keys — measured, same fixture, two bundles. A current server refuses to write a file whose format is newer than its own, so this cannot happen unnoticed any more, but the published bundle is still stale.pnpm build:mcp # -> mcp/dist/circal-mcp.mjsThen use
node /absolute/path/to/circal/mcp/dist/circal-mcp.mjswherever the examples below saynpx -y circal-mcp.
Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"circal": {
"command": "npx",
"args": ["-y", "circal-mcp", "--file", "/absolute/path/to/your/calendar.json"]
}
}
}Claude Code
claude mcp add circal -- npx -y circal-mcp --file /absolute/path/to/your/calendar.jsonOr in .mcp.json:
{
"mcpServers": {
"circal": {
"command": "npx",
"args": ["-y", "circal-mcp", "--file", "/absolute/path/to/your/calendar.json"]
}
}
}Cursor
Add to .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"circal": {
"command": "npx",
"args": ["-y", "circal-mcp", "--file", "/absolute/path/to/your/calendar.json"]
}
}
}No mirror file yet: on the Mac app there is almost always one already (--find should have caught
it); in a browser, turn the mirror on from circal's Settings panel, then run --find again. Run
npx -y circal-mcp --help for the full flag and environment variable list.
From source (contributors)
Building from a checkout instead of the published package:
pnpm install
pnpm build:mcpProduces the same single-file ESM bundle at mcp/dist/circal-mcp.mjs.
pnpm mcp builds and runs it directly, for local testing. Point a client's
command/args at node and the absolute path to that bundle instead of
npx, with the same env/--file as above.
What it refuses, and why
- No
CIRCAL_FILE(or--file <path>). The path is never guessed — a wrong guess here means writing over the wrong year's calendar. The server exits with one line on stderr before it ever connects. - A zone mismatch. Every day boundary in circal is local time. If the
file's own recorded zone does not match the zone this process is running
in, reads go through with a warning line; writes are refused, because a
write meant for "today" could land on the file's yesterday or tomorrow.
Override with
CIRCAL_ALLOW_ZONE_MISMATCH=1if you are certain. - A feed calendar. A calendar with a subscription attached is a mirror of somebody else's ICS feed. Its events are pulled, not written — the same read-only rule the event editor already enforces — so no tool here can create, edit, delete, or tick an event on one.
- A repeating event with no
scope.update_event,delete_eventand similar ask for"one","future", or"all"before touching a series, and name the event in the refusal rather than guessing. - A moved file. Every mutation reads the file, applies the change,
re-reads to confirm nothing else wrote in between, then writes
rev + 1through a temp file plus rename (so a crash mid-write cannot truncate the file). One retry on a conflicting write, then a refusal naming both revisions.
