circal-mcp 0.2.1 → 0.3.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 +76 -28
  2. package/dist/circal-mcp.mjs +1372 -694
  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
 
@@ -117,6 +124,41 @@ Produces the same single-file ESM bundle at `mcp/dist/circal-mcp.mjs`.
117
124
  `command`/`args` at `node` and the absolute path to that bundle instead of
118
125
  `npx`, with the same `env`/`--file` as above.
119
126
 
127
+ ## The cloud copy
128
+
129
+ ```sh
130
+ circal-mcp login # email → sign-in code → passphrase; interactive
131
+ circal-mcp --cloud # serve against the row
132
+ circal-mcp logout # withdraw this machine's key, now
133
+ ```
134
+
135
+ `login` asks the site which Supabase project it uses (`https://circal.day/mcp.json`,
136
+ see `src/pages/mcp.json.ts`; `--site <url>` points a fork at its own
137
+ deployment, `--url`/`--key` name a project outright). Then three prompts:
138
+ email, the emailed sign-in code, the passphrase. The passphrase is proven
139
+ against the row's own box before anything is written, and what is left behind
140
+ is a `0600` record at the platform config dir
141
+ (`~/Library/Application Support/circal/cloud.json` on macOS;
142
+ `CIRCAL_CLOUD_CREDENTIALS` overrides) holding the session and the
143
+ **derived key — never the passphrase**. This is the browser's "stay
144
+ unlocked" record in Node clothes: same authority, same threat model, one
145
+ honest step weaker in that a file's bytes are readable where a
146
+ non-extractable `CryptoKey`'s are not — the price of a runtime with no
147
+ IndexedDB. Whoever can read that file can read the calendar; `logout`
148
+ deletes it immediately.
149
+
150
+ Sign-in is once per machine: the session renews itself, and each rotated
151
+ refresh token is persisted before it is spent. A browser's own session is
152
+ deliberately never borrowed — Supabase rotates refresh tokens with reuse
153
+ detection, so two clients sharing one session sign each other out.
154
+
155
+ The row must already exist: this server never mints the box. Enrolment — the
156
+ account, the passphrase, the first seal — belongs to the app's own Settings
157
+ panel, where the passphrase's meaning is explained before it is chosen.
158
+
159
+ Client config is the same as above with `--cloud` in place of
160
+ `--file <path>`.
161
+
120
162
  ## What it refuses, and why
121
163
 
122
164
  - **No `CIRCAL_FILE` (or `--file <path>`).** The path is never guessed — a
@@ -134,8 +176,14 @@ Produces the same single-file ESM bundle at `mcp/dist/circal-mcp.mjs`.
134
176
  - **A repeating event with no `scope`.** `update_event`, `delete_event` and
135
177
  similar ask for `"one"`, `"future"`, or `"all"` before touching a series,
136
178
  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.
179
+ - **A moved document.** Every mutation reads, applies the change, and writes
180
+ `rev + 1` only if nothing else wrote in between a confirm-read under an
181
+ advisory lock plus temp-file-and-rename for the file (so a crash mid-write
182
+ cannot truncate it), a conditional `update where rev = N` for the row.
183
+ One retry against a fresh read, then a refusal naming both revisions.
184
+ - **Cloud only: an account with no row yet** (enrolment is the app's job, see
185
+ above), **a row re-sealed under a new passphrase** (the kept key cannot
186
+ open it; the refusal sends you back to `circal-mcp login`, and nothing is
187
+ written blind), and **a document one format version ahead** — a write here
188
+ round-trips the whole document, so an older server writing a newer file
189
+ would silently drop every field it has no name for. Reads still answer.