circal-mcp 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +86 -28
  2. package/dist/circal-mcp.mjs +2122 -823
  3. package/package.json +9 -7
package/README.md CHANGED
@@ -1,21 +1,33 @@
1
1
  # circal-mcp
2
2
 
3
3
  A stdio [MCP](https://modelcontextprotocol.io) server that reads and writes
4
- circal's own mirror file the `.json` a browser tab or the macOS app keeps
5
- in sync on disk, through the File System Access API on the web and the
6
- shell's own bridge in the app (see `src/lib/mirror.ts`, `src/lib/mirrorLink.ts`
7
- and `mac/main.swift`'s `MirrorFile`). It imports circal's own domain layer
4
+ the user's own circal calendar, through circal's own domain layer
8
5
  (`src/lib/events.ts`, `recur.ts`, `quickadd.ts`, `mutate.ts`, `backup.ts`, …),
9
6
  so an agent and the app can never disagree about what a legal event, an
10
7
  occurrence, or a recurrence rule is.
11
8
 
9
+ Two transports, one tool table:
10
+
11
+ - **`--file <path>`** — the mirror file, the `.json` a browser tab or the
12
+ macOS app keeps in sync on disk (see `src/lib/mirror.ts`,
13
+ `src/lib/mirrorLink.ts` and `mac/main.swift`'s `MirrorFile`).
14
+ - **`--cloud`** — the cloud copy, the one encrypted Supabase row the app's
15
+ cloud sync writes (see `src/lib/cloudLink.ts`, `SYNC.md`). Sign in once
16
+ with `circal-mcp login`; every tool call then reads the row, decrypts it
17
+ **on this machine**, mutates, re-seals and swaps it back on `rev` — the
18
+ same compare-and-swap two browsers already use against each other, so the
19
+ server is simply the row's third client. Supabase still holds only
20
+ ciphertext: the derived key never leaves this machine, which is why this
21
+ server must run on the user's own hardware, never hosted.
22
+
12
23
  Built on `@modelcontextprotocol/server` v2, implementing MCP protocol
13
24
  revision 2026-07-28.
14
25
 
15
- **The browser tab does not need to be open.** The mirror file is the only
16
- thing this server reads or writes; if circal's tab is open and connected, it
17
- picks up the change and its own next write moves the file's `rev` forward
18
- again, the same as any second writer would.
26
+ **The browser tab does not need to be open.** The mirror file (or the row) is
27
+ the only thing this server reads or writes; if circal's tab is open and
28
+ connected, it picks up the change instantly over Realtime for the cloud
29
+ copy — and its own next write moves the `rev` forward again, the same as any
30
+ second writer would.
19
31
 
20
32
  ## Quick start
21
33
 
@@ -34,21 +46,16 @@ zone, revision) plus a ready-to-paste config block. Paste that block into
34
46
  your client below, swap the path if it picked up the wrong file, then
35
47
  restart the client.
36
48
 
37
- > **Use a locally built bundle, not `npx circal-mcp`.** The published
38
- > `circal-mcp@0.1.0` predates several fields in `src/lib` (`free`,
39
- > `remindLead`, `organizer` and the guest pair on an event; `backupNudge`,
40
- > `backupInterval`, `deviceZone`, `stampedZones` in settings). Every write
41
- > round-trips the whole document through its reader, so it silently drops
42
- > those nine keys — measured, same fixture, two bundles. A current server
43
- > refuses to write a file whose format is newer than its own, so this cannot
44
- > happen unnoticed any more, but the published bundle is still stale.
45
- >
46
- > ```sh
47
- > pnpm build:mcp # -> mcp/dist/circal-mcp.mjs
48
- > ```
49
- >
50
- > Then use `node /absolute/path/to/circal/mcp/dist/circal-mcp.mjs` wherever
51
- > the examples below say `npx -y circal-mcp`.
49
+ > **Use `circal-mcp@0.3.0` or newer** (`npx -y circal-mcp` resolves it).
50
+ > The cautionary tale behind that floor: `0.1.0` predated nine fields in
51
+ > `src/lib` (`free`, `remindLead`, `organizer` and the guest pair on an
52
+ > event; `backupNudge`, `backupInterval`, `deviceZone`, `stampedZones` in
53
+ > settings), and every write round-trips the whole document through its
54
+ > reader, so it silently dropped all nine — measured, same fixture, two
55
+ > bundles. A current server refuses to write a document whose format is
56
+ > newer than its own, so a stale bundle can no longer damage one unnoticed;
57
+ > it will simply refuse. Contributors run against their own build with
58
+ > `pnpm build:mcp` → `node mcp/dist/circal-mcp.mjs`.
52
59
 
53
60
  ### Claude Desktop
54
61
 
@@ -103,6 +110,16 @@ No mirror file yet: on the Mac app there is almost always one already (`--find`
103
110
  it); in a browser, turn the mirror on from circal's Settings panel, then run `--find` again. Run
104
111
  `npx -y circal-mcp --help` for the full flag and environment variable list.
105
112
 
113
+ ### Pinning a calendar
114
+
115
+ `--calendar <c>` (or `CIRCAL_CALENDAR`), beside either transport, pins the
116
+ connection's default calendar — an id or a unique name prefix. Every write
117
+ that names no `calendarId` lands there instead of on the user's default,
118
+ which is how an agent working one calendar stays on it. A create that was
119
+ neither named nor pinned says so in its own summary
120
+ (`calendar defaulted, not named`), so a wrong-calendar write is loud rather
121
+ than discovered by whoever gets the reminder.
122
+
106
123
  ## From source (contributors)
107
124
 
108
125
  Building from a checkout instead of the published package:
@@ -117,6 +134,41 @@ Produces the same single-file ESM bundle at `mcp/dist/circal-mcp.mjs`.
117
134
  `command`/`args` at `node` and the absolute path to that bundle instead of
118
135
  `npx`, with the same `env`/`--file` as above.
119
136
 
137
+ ## The cloud copy
138
+
139
+ ```sh
140
+ circal-mcp login # email → sign-in code → passphrase; interactive
141
+ circal-mcp --cloud # serve against the row
142
+ circal-mcp logout # withdraw this machine's key, now
143
+ ```
144
+
145
+ `login` asks the site which Supabase project it uses (`https://circal.day/mcp.json`,
146
+ see `src/pages/mcp.json.ts`; `--site <url>` points a fork at its own
147
+ deployment, `--url`/`--key` name a project outright). Then three prompts:
148
+ email, the emailed sign-in code, the passphrase. The passphrase is proven
149
+ against the row's own box before anything is written, and what is left behind
150
+ is a `0600` record at the platform config dir
151
+ (`~/Library/Application Support/circal/cloud.json` on macOS;
152
+ `CIRCAL_CLOUD_CREDENTIALS` overrides) holding the session and the
153
+ **derived key — never the passphrase**. This is the browser's "stay
154
+ unlocked" record in Node clothes: same authority, same threat model, one
155
+ honest step weaker in that a file's bytes are readable where a
156
+ non-extractable `CryptoKey`'s are not — the price of a runtime with no
157
+ IndexedDB. Whoever can read that file can read the calendar; `logout`
158
+ deletes it immediately.
159
+
160
+ Sign-in is once per machine: the session renews itself, and each rotated
161
+ refresh token is persisted before it is spent. A browser's own session is
162
+ deliberately never borrowed — Supabase rotates refresh tokens with reuse
163
+ detection, so two clients sharing one session sign each other out.
164
+
165
+ The row must already exist: this server never mints the box. Enrolment — the
166
+ account, the passphrase, the first seal — belongs to the app's own Settings
167
+ panel, where the passphrase's meaning is explained before it is chosen.
168
+
169
+ Client config is the same as above with `--cloud` in place of
170
+ `--file <path>`.
171
+
120
172
  ## What it refuses, and why
121
173
 
122
174
  - **No `CIRCAL_FILE` (or `--file <path>`).** The path is never guessed — a
@@ -134,8 +186,14 @@ Produces the same single-file ESM bundle at `mcp/dist/circal-mcp.mjs`.
134
186
  - **A repeating event with no `scope`.** `update_event`, `delete_event` and
135
187
  similar ask for `"one"`, `"future"`, or `"all"` before touching a series,
136
188
  and name the event in the refusal rather than guessing.
137
- - **A moved file.** Every mutation reads the file, applies the change,
138
- re-reads to confirm nothing else wrote in between, then writes `rev + 1`
139
- through a temp file plus rename (so a crash mid-write cannot truncate the
140
- file). One retry on a conflicting write, then a refusal naming both
141
- revisions.
189
+ - **A moved document.** Every mutation reads, applies the change, and writes
190
+ `rev + 1` only if nothing else wrote in between a confirm-read under an
191
+ advisory lock plus temp-file-and-rename for the file (so a crash mid-write
192
+ cannot truncate it), a conditional `update where rev = N` for the row.
193
+ One retry against a fresh read, then a refusal naming both revisions.
194
+ - **Cloud only: an account with no row yet** (enrolment is the app's job, see
195
+ above), **a row re-sealed under a new passphrase** (the kept key cannot
196
+ open it; the refusal sends you back to `circal-mcp login`, and nothing is
197
+ written blind), and **a document one format version ahead** — a write here
198
+ round-trips the whole document, so an older server writing a newer file
199
+ would silently drop every field it has no name for. Reads still answer.