@classcad/mcp 0.2.2 → 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 (72) hide show
  1. package/README.md +128 -87
  2. package/dist/app/SOURCE.txt +4 -0
  3. package/dist/app/assets/index-54a14bc0.css +8 -0
  4. package/dist/app/assets/index-c650c783.js +11257 -0
  5. package/dist/app/assets/troika-three-text.esm-188d86b5.js +220 -0
  6. package/dist/app/favicon.svg +98 -0
  7. package/dist/app/index.html +19 -0
  8. package/dist/app/manifest.json +14 -0
  9. package/dist/auth.d.ts +8 -0
  10. package/dist/auth.js +49 -10
  11. package/dist/auth.js.map +1 -1
  12. package/dist/backend.d.ts +8 -0
  13. package/dist/backend.js +39 -0
  14. package/dist/backend.js.map +1 -0
  15. package/dist/client.d.ts +48 -11
  16. package/dist/client.js +152 -167
  17. package/dist/client.js.map +1 -1
  18. package/dist/daemon.d.ts +0 -2
  19. package/dist/daemon.js +77 -62
  20. package/dist/daemon.js.map +1 -1
  21. package/dist/engine/key.d.ts +29 -0
  22. package/dist/engine/key.js +145 -0
  23. package/dist/engine/key.js.map +1 -0
  24. package/dist/engine/wasm-worker.js +7 -7
  25. package/dist/engine/wasm-worker.js.map +1 -1
  26. package/dist/engine/wasm.d.ts +16 -14
  27. package/dist/engine/wasm.js +22 -30
  28. package/dist/engine/wasm.js.map +1 -1
  29. package/dist/mcp-server.d.ts +4 -4
  30. package/dist/mcp-server.js +72 -28
  31. package/dist/mcp-server.js.map +1 -1
  32. package/dist/queue.js +1 -1
  33. package/dist/queue.js.map +1 -1
  34. package/dist/server.js +69 -36
  35. package/dist/server.js.map +1 -1
  36. package/dist/share/hub.d.ts +70 -0
  37. package/dist/share/hub.js +494 -0
  38. package/dist/share/hub.js.map +1 -0
  39. package/dist/share/protocol.d.ts +87 -0
  40. package/dist/share/protocol.js +54 -0
  41. package/dist/share/protocol.js.map +1 -0
  42. package/dist/share/relay.d.ts +45 -0
  43. package/dist/share/relay.js +230 -0
  44. package/dist/share/relay.js.map +1 -0
  45. package/dist/share/server.d.ts +27 -0
  46. package/dist/share/server.js +399 -0
  47. package/dist/share/server.js.map +1 -0
  48. package/dist/share/session.d.ts +120 -0
  49. package/dist/share/session.js +336 -0
  50. package/dist/share/session.js.map +1 -0
  51. package/dist/tools/auth.js +1 -1
  52. package/dist/tools/auth.js.map +1 -1
  53. package/dist/tools/docs.js +3 -2
  54. package/dist/tools/docs.js.map +1 -1
  55. package/dist/tools/lifecycle.js +36 -9
  56. package/dist/tools/lifecycle.js.map +1 -1
  57. package/dist/tools/share.d.ts +18 -0
  58. package/dist/tools/share.js +277 -0
  59. package/dist/tools/share.js.map +1 -0
  60. package/dist/tools/viewer.d.ts +1 -1
  61. package/dist/tools/viewer.js +11 -6
  62. package/dist/tools/viewer.js.map +1 -1
  63. package/package.json +4 -3
  64. package/dist/bridge/protocol.d.ts +0 -87
  65. package/dist/bridge/protocol.js +0 -14
  66. package/dist/bridge/protocol.js.map +0 -1
  67. package/dist/bridge/server.d.ts +0 -35
  68. package/dist/bridge/server.js +0 -236
  69. package/dist/bridge/server.js.map +0 -1
  70. package/dist/tools/bridge.d.ts +0 -4
  71. package/dist/tools/bridge.js +0 -115
  72. package/dist/tools/bridge.js.map +0 -1
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Model Context Protocol server for the [ClassCAD](https://classcad.ch) CAD engine. Self-contained: it runs the ClassCAD engine itself (WASM), so there is no server to set up.
4
4
 
5
- Lets MCP-capable hosts (Claude Code, the Claude desktop app, VS Code Copilot, Cursor, …) drive a live ClassCAD session: build parts and assemblies through scripts, inspect the structure tree, render snapshots, save and load STEP, STL and OFB, and dock into a session an interactive app (buerligons) already has open.
5
+ Lets MCP-capable hosts (Claude Code, the Claude desktop app, VS Code Copilot, Cursor, …) drive a live ClassCAD session: build parts and assemblies through scripts, inspect the structure tree, render snapshots, save and load STEP, STL and OFB.
6
+
7
+ The session is shared. The CAD app, [Buerligons](https://buerligons.io), docks into it: the user turns, selects and edits the same model the agent builds. Its link can be sent to a colleague or opened on another device (through the share relay), and the agent can also join the session of an app the user already has open.
6
8
 
7
9
  ---
8
10
 
@@ -16,17 +18,20 @@ Lets MCP-capable hosts (Claude Code, the Claude desktop app, VS Code Copilot, Cu
16
18
  classcad-mcp daemon (ONE per machine)
17
19
  session 1 ── worker thread ──► ClassCAD WASM (the MCP's own engine)
18
20
  session 2 ── WebSocket ─────► ClassCAD worker (classcad-cli, optional)
19
- session 3 ── bridge ────────► buerligons.io tab (WASM engine in the page)
20
- bridge listener ws://127.0.0.1:9096/bridge ◄── apps announce share tokens
21
+ session 3 ── WebSocket ─────► a session an app shares (by invite)
22
+ 127.0.0.1:9098 ◄── the app, docked into a session · apps that host a session of their own
23
+ wss, dialled out ──► share relay (Cloudflare) ◄── the app on any other machine, by share link
21
24
  ```
22
25
 
23
26
  - **Self-contained.** The MCP brings its own engine: the published ClassCAD WASM build, run in a worker thread of the daemon. No server and no key to configure (a six-month engine key is built in); one sign-in with a free classcad.ch account. On first use it downloads the release assets (about 16 MB) once and caches them.
24
27
  - Every host starts the MCP as a **stdio** child process (`npx -y @classcad/mcp@latest`). That process is a thin **shim**: it looks for the daemon on `127.0.0.1:9097`, starts it if none runs, and forwards its host's JSON-RPC to it. The first tab starts the daemon, every later tab reuses it.
25
- - The **daemon** is the actual MCP. It holds one MCP server instance **per session** (per tab: own engine, emission config, caches, tool queue) and the one **bridge listener** apps connect to. With no session left and no app attached to its bridge it exits by itself after `CLASSCAD_DAEMON_IDLE_MS` (60 s). Nothing to install or manage: it is part of this package (`dist/daemon.js`) and lives only while it is used.
26
- - Why a daemon: the in-app bridge means the MCP *listens* on a port, and a port belongs to exactly one process. With one MCP process per tab, the second tab could not bind (or bound the other address family of `localhost` and got half the apps).
27
- - **Optional engines.** If a `classcad-cli worker` is reachable (`CLASSCAD_WS_URL`, default `ws://localhost:9094/`), the default policy `auto` uses it instead of the local engine — for multi-client sessions and `?invite=` sharing with apps. The MCP can also attach to an engine running inside a buerli app's browser tab. See [Engines](#engines-worker-in-app-bridge-local-wasm).
28
+ - The **daemon** is the actual MCP. It holds one MCP server instance **per session** (per tab: own engine, caches, tool queue, invites) and the one **listener sessions are joined on** (`127.0.0.1:9098`): the app docks into the MCP's sessions there, and apps that run the engine in their own page offer their sessions there. With no session left and no such app waiting it exits by itself after `CLASSCAD_DAEMON_IDLE_MS` (60 s). Nothing to install or manage: it is part of this package (`dist/daemon.js`) and lives only while it is used.
29
+ - Why a daemon: the MCP *listens* on a port, and a port belongs to exactly one process. With one daemon, every session's app link has the same address, and an app that looks for the MCP finds it.
30
+ - **Shared with the user.** Every session has a live CAD app, docked into the same engine: see [The app](#the-app). The session protocol is the one a ClassCAD server speaks, so any buerli app joins the same way: see [Sharing](#sharing-one-session-several-participants).
31
+ - **Shared beyond the machine.** A session is also offered on the share relay ([`packages/relay`](../relay), a Cloudflare Worker), so the app's link works on any machine. The MCP dials out to it; nothing on the machine listens for the outside. See [Somebody elsewhere joins](#somebody-elsewhere-joins).
32
+ - **Optional engines.** If a `classcad-cli worker` is reachable (`CLASSCAD_WS_URL`, default `ws://localhost:9094/`), the default policy `auto` uses it instead of the local engine. See [Engines](#engines-worker-or-local-wasm).
28
33
  - The engine is started lazily on the first tool call, so an idle session costs nothing.
29
- - Fallback: if `127.0.0.1:9097` is held by something that is not a classcad daemon, the shim serves the MCP in-process (everything works except the in-app bridge) and says so on stderr.
34
+ - Fallback: if `127.0.0.1:9097` is held by something that is not a classcad daemon, the shim serves the MCP in-process (with a listener of its own, on a free port if `9098` is taken) and says so on stderr.
30
35
 
31
36
  ---
32
37
 
@@ -142,21 +147,22 @@ Most hosts accept the Claude-style `mcpServers` JSON.
142
147
 
143
148
  ### First test
144
149
 
145
- In a new session, ask the agent to *make a box*. The first time, it shows a sign-in link: open it, sign in, and the agent carries on by itself. It should then call `run_script` and answer with a volume; `session_info` shows `"transport": "wasm"` (or `"ws"` when a worker is running). The very first call takes a little longer while the WASM assets download.
150
+ In a new session, ask the agent to *make a box*. The first time, it shows a sign-in link: open it, sign in, and the agent carries on by itself. It should then call `run_script` and answer with a volume, and the app comes up with the box (in the desktop app's Browser pane, or in your browser); `session_info` shows `"transport": "wasm"` (or `"ws"` when a worker is running). The very first call takes a little longer while the WASM assets download.
146
151
 
147
152
  ---
148
153
 
149
- ## The 3D view
154
+ ## The app
150
155
 
151
- Every session has a live 3D view of its model, in the user's browser:
156
+ Every session has a live CAD app: [Buerligons](https://buerligons.io), the buerli CAD app, docked into the session's own engine. The package carries a build of it and serves it itself, so there is nothing to install and it works offline.
152
157
 
153
- - **It opens by itself** with the first model of a session, and follows every change the agent makes: no reload. Results of `run_script`, `snapshot`, `load` and `restore` end with its link (`3D view: http://127.0.0.1:9098/v/…`), `session_info` names it, and `view` returns it and brings the view up.
154
- - **What it shows.** The part as the family draws it: flat, the colour of its paper (or the colour the model gave it), by its lines — the B-rep edges and the silhouettes of its curved faces — lit a little, orthographic. It turns, pans and zooms; it has the standard views, light and dark, the feature history, the bodies, and the model's size.
155
- - **Exports.** The page's Export menu downloads the model as STEP, STL or glTF (`.glb`).
156
- - **One view per session.** The link carries a secret token; a page shows exactly one session's model, so several sessions (tabs, hosts) work side by side. When a session ends, its page says so and keeps the last model on screen.
157
- - **Local only.** The MCP process (the daemon, or a shim serving in-process) listens on `127.0.0.1:9098` (`CLASSCAD_VIEWER_PORT`; another free port when that one is taken), answers only requests that name it as their host, and serves the page with everything it needs (three.js, the font): no model data leaves the machine, and it works offline.
158
+ - **It comes up with the first model** of a session and shows every change the agent makes as it happens. Where it comes up is the host's: in the **Claude desktop app** (Code tab) it is the app's own Browser pane, beside the conversation — the agent opens it there; everywhere else the MCP opens the user's default browser. The result of the script that makes the first model ends with the link (`App: …`), once per session: agents do not carry it into every answer. `session_info` names it, and `view` returns it and brings the app up again (unless the user already has it open: one tab per session is enough).
159
+ - **The user works in it too.** Turn and zoom, measure, select; edit a feature's values, draw and constrain a sketch, add or delete features, start over or open a file. It is the same model: what the user changes is in the tree the agent reads next, and what the agent builds appears in the app at once. A command from the app waits for a running script to finish, so the two never interleave inside one.
160
+ - **Pointing.** What the user has selected in the app, the agent reads with `get_selection` ("fillet *this* edge"); with `set_selection` the agent highlights something for the user.
161
+ - **Files.** The app's File menu saves the model as OFB (ClassCAD's own format, with the features), STEP or STL, and opens such files into the session.
162
+ - **One link per session, and it travels.** The link carries an invite to exactly one session, so several sessions (tabs, hosts) work side by side, each with its own app. By default it is a **share link**, `https://<relay>/?invite=…`: the same link works for a colleague, or on the user's tablet (see [Somebody elsewhere joins](#somebody-elsewhere-joins)). While the relay does not hold the session, or with `CLASSCAD_SHARE=ask` or `off`, it is the link on this machine, `http://127.0.0.1:9098/?invite=…`. When a session ends, or moves to another engine, the app says so and offers to join again.
163
+ - **Where the model is.** The session and its engine stay on the user's machine. The MCP serves the app on `127.0.0.1:9098` (`CLASSCAD_VIEWER_PORT`; another free port when that one is taken) and answers only requests that name it as their host: opened from there, nothing of the model leaves the machine. Opened under the share link, the app's frames pass through the relay, which keeps none of them. The invite is the key to the session, so the link is a secret: whoever has the share link is in.
158
164
 
159
- `CLASSCAD_VIEWER_OPEN=0` keeps the browser closed (the link is still offered); `CLASSCAD_VIEWER=off` turns the view off.
165
+ `CLASSCAD_VIEWER_OPEN` says where the app comes up: `browser` (the user's default browser), `host` (the agent opens it in its host's own browser pane — for any host that has one) or `0` (nowhere: the agent hands the link over). Unset, it is `host` in the Claude desktop app and `browser` everywhere else. `CLASSCAD_APP_URL` names an app that is hosted elsewhere instead of the one in this package: the link becomes `<url>?invite=…`, and the app has to join that invite on `ws://127.0.0.1:9098/session` (a Buerligons built for the engine in its page does). A page from a public site reaches `127.0.0.1` only if the browser lets it: see [the limit](#what-the-browser-has-to-allow) below. `CLASSCAD_VIEWER=readonly` shows the read-only 3D view instead of the app (a page that only turns and exports the model; a build of this package without the app has it too), `CLASSCAD_VIEWER=off` shows nothing.
160
166
 
161
167
  ---
162
168
 
@@ -185,39 +191,46 @@ From a terminal, the same flow and more (`node dist/server.js <command>` in a ch
185
191
  | ------------------------ | ---------------------------------------------------------------------------------------------------------------- |
186
192
  | `CLASSCAD_WS_URL` | WebSocket URL of an optional ClassCAD worker. Default `ws://localhost:9094/`; unreachable → the local WASM engine. Any reachable worker works (`wss://…`). |
187
193
  | `CLASSCAD_SNAPSHOT_DIR` | Where `snapshot` writes its PNGs when the call passes no `outDir`. Default `<tmpdir>/classcad-snapshots`. |
188
- | `CLASSCAD_SKILL_PATH` | Use a local `classcad-skill` checkout for docs instead of the installed `@classcad/skill` package. |
189
- | `CLASSCAD_BRIDGE_LISTEN` | Listener for the in-app bridge (see below). Default `ws://127.0.0.1:9096/bridge`. The daemon starts even if it cannot bind and retries every 5 s. |
194
+ | `CLASSCAD_SKILL_PATH` | Read the docs live from a skill directory (`packages/skill` of a checkout) instead of the bundle in the installed `@classcad/skill`: edits show without a rebuild. |
190
195
  | `CLASSCAD_MCP_PORT` | Port of the daemon's HTTP endpoint on `127.0.0.1`. Default `9097`. Shim and daemon must agree (the shim passes it on when it starts the daemon). |
191
196
  | `CLASSCAD_MCP_URL` | Full daemon URL instead of `http://127.0.0.1:<port>` (rarely needed). |
192
- | `CLASSCAD_DAEMON_IDLE_MS`| How long the daemon lives without any session and without any app on the bridge before it exits. Default `60000`. |
197
+ | `CLASSCAD_DAEMON_IDLE_MS`| How long the daemon lives without any session, and without any app that offers a session of its own, before it exits. Default `60000`. |
193
198
  | `CLASSCAD_MCP_LOG` | Daemon log file. Default `<tmpdir>/classcad-mcp/daemon.log` (the shim prints the path on stderr at startup). |
194
199
  | `CLASSCAD_ENGINE` | Engine policy when no token/URL decides: `auto` (default: the worker, else the local WASM engine), `drogon` (worker only), `wasm` (local engine only). |
195
- | `CLASSCAD_VIEWER` | `off` turns the live 3D view off. Default: on. |
196
- | `CLASSCAD_VIEWER_PORT` | Port of the 3D view's listener on `127.0.0.1`. Default `9098`; a free port is taken when it is in use (`0`: always a free one). |
197
- | `CLASSCAD_VIEWER_OPEN` | `0`: do not open the view in the browser with the first model; the link is still offered. |
198
- | `CLASSCAD_AUTH_URL` | The sign-in page. Default `https://classcad.ch/connect` (the dev site: `http://localhost:9090/connect`). |
200
+ | `CLASSCAD_VIEWER` | What a session shows the user. Default: the app. `readonly`: the read-only 3D view. `off`: nothing. |
201
+ | `CLASSCAD_VIEWER_PORT` | Port of the listener sessions are joined on, on `127.0.0.1`. Default `9098`; a free port is taken when it is in use (`0`: always a free one). Apps that host their own session look for the MCP on `9098`. |
202
+ | `CLASSCAD_VIEWER_OPEN` | Where the app comes up with the first model: `browser` (the user's default browser), `host` (the agent opens it in its host's own browser pane), `0` (nowhere; the link is handed over). Default: `host` in the Claude desktop app's Code tab, `browser` elsewhere. Per session. |
203
+ | `CLASSCAD_APP_URL` | The app to dock into sessions, when it is hosted elsewhere (e.g. `https://buerligons.io/`); the link becomes `<url>?invite=…`. Default: the app in this package. |
204
+ | `CLASSCAD_RELAY_URL` | The share relay sessions are shared on (`https://…`; [`packages/relay`](../relay)). `off`: sessions are never shared. Default: `https://classcad-share.it-5ca.workers.dev` (`DEFAULT_RELAY_URL` in `src/share/relay.ts`). |
205
+ | `CLASSCAD_SHARE` | When a session gets a link that works beyond its machine: `always` (default — from its first model on, and the app comes up under that link), `ask` (when the user asks for it, the `share` tool; the app's own link stays on `127.0.0.1`), `off` (never). |
206
+ | `CLASSCAD_BACKEND` | `classcad` (default): the plans backend, sign-in on classcad.ch. `develop`: the same backend, signed in on a develop build of classcad.ch (`http://localhost:9090`). `staging`: signed in on `https://staging01.classcad.ch`. |
207
+ | `CLASSCAD_TOKEN` | A secret access token (`ccsk_…`, made on classcad.ch/account), for machines nobody signs in on (CI): the engine's key is fetched with it instead of the sign-in. |
208
+ | `CLASSCAD_AUTH_URL` | The sign-in page. Default `https://classcad.ch/connect` (with `CLASSCAD_BACKEND=develop`: `http://localhost:9090/connect`). |
199
209
  | `CLASSCAD_AUTH_FILE` | Where the sign-in is stored. Default `~/.classcad-mcp/auth.json`. |
200
- | `CLASSCAD_WASM_KEY` | A ClassCAD key (classcad.ch/user) for the local WASM engine. Optional: a key valid until 2027-03-30 is built in. |
201
- | `CLASSCAD_WASM_ORIGIN` | Origin the local engine believes it runs on; must be in the key's allowed origins. Default `http://localhost:3000`. |
210
+ | `CLASSCAD_AUTH_NO_BROWSER` | `1`: the sign-in page is not opened in a browser; the agent shows its link (also so when `CI` is set). |
211
+ | `CLASSCAD_WASM_KEY` | A key of your own for the local engine, instead of one from the backend: a machine without internet, a contract. Not needed otherwise. |
212
+ | `CLASSCAD_WASM_ORIGIN` | Origin the local engine says it runs on. Default `http://localhost`, which keys for local use allow on any port. |
213
+ | `CLASSCAD_WASM` | `off`: no local engine (a worker only). |
214
+ | `CLASSCAD_KEY_URL`, `CLASSCAD_KEY_FILE` | The token exchange (default the plans backend's `/api/v1/key`) and where the engine's key is kept (default `~/.classcad-mcp/engine-key.json`). |
202
215
  | `CLASSCAD_WASM_VERSION` | ClassCAD release to host. Default `21.2.0` (what the buerli apps load). |
203
216
  | `CLASSCAD_WASM_DIR` | Where the release assets are cached. Default `~/.classcad-mcp/wasm/<version>`. |
204
217
  | `CLASSCAD_WASM_URL` | Download base override. Default `https://awvstatic.com/classcad/download/release/<version>/wasm`. |
205
218
 
206
- None is required. Set them in the host's MCP config `env` block; the shim passes them on to the daemon it starts. `CLASSCAD_WS_URL` is per session — the daemon receives it with every new session, so two tabs can point at different workers.
219
+ None is required. Set them in the host's MCP config `env` block; the shim passes them on to the daemon it starts. `CLASSCAD_WS_URL`, `CLASSCAD_ENGINE` and `CLASSCAD_VIEWER_OPEN` are per session — the daemon receives them with every new session, so two tabs can point at different workers, and a tab in the Claude desktop app shows its app in the pane while a terminal's opens the browser.
207
220
 
208
221
  ### The daemon in practice
209
222
 
210
- - **Which process is which:** `classcad-mcp` shim = the process your host started (`dist/server.js`), one per tab, exits with the tab. `dist/daemon.js` = the one long-lived process; `GET http://127.0.0.1:9097/health` shows its pid, version, active session count, attached app count, bridge address and log file.
223
+ - **Which process is which:** `classcad-mcp` shim = the process your host started (`dist/server.js`), one per tab, exits with the tab. `dist/daemon.js` = the one long-lived process; `GET http://127.0.0.1:9097/health` shows its pid, version, active session count, the number of apps in a session (`apps`), the address sessions are joined on (`join`) and the log file.
211
224
  - **Multiple tabs:** each tab is one session in the same daemon. Sessions are independent (a script in tab A does not touch tab B's caches). `session_info` shows what a tab is attached to.
212
- - **Lifetime:** the daemon exits a minute after the last session closed, unless an app still holds a bridge (a shared buerligons.io tab keeps it alive, so the next tab attaches instantly). Apps reconnect by themselves within 5 s after a daemon restart; `use_session` waits 8 s for that. A shim exits when its host closes stdin or sends SIGTERM; the daemon drops that session immediately.
213
- - **Upgrades:** after `npm run build` (or a package update) the next shim notices the version or build-stamp mismatch (`/health` reports both; the stamp is taken when the daemon starts and covers `dist/daemon.js` plus the `@classcad/renderer` and `@classcad/script` builds). An idle old daemon is shut down and replaced. A busy one (other tabs) is told to *drain* — it takes no new sessions and exits when its last tab closes — and the new tab runs the current build **in-process** meanwhile (worker and local WASM work, only the in-app bridge is unavailable in that tab until the old daemon is gone). Old tabs keep their old code until they are restarted.
225
+ - **Lifetime:** the daemon exits a minute after the last session closed, unless an app still offers a session of its own (a buerli app with the engine in its page keeps it alive, so the next tab joins it instantly). Such apps find a restarted daemon by themselves within 5 s; `use_session` waits 8 s for that. A shim exits when its host closes stdin or sends SIGTERM; the daemon drops that session immediately, and the app docked into it says that the session is over.
226
+ - **Upgrades:** after `npm run build` (or a package update) the next shim notices the version or build-stamp mismatch (`/health` reports both; the stamp is taken when the daemon starts and covers `dist/daemon.js` plus the `@classcad/renderer` and `@classcad/script` builds). An idle old daemon is shut down and replaced. A busy one (other tabs) is told to *drain* — it takes no new sessions and exits when its last tab closes — and the new tab runs the current build **in-process** meanwhile (with a listener of its own for its app, on a free port). Old tabs keep their old code until they are restarted.
214
227
  - **Logs:** the shim writes one line per session start/end to stderr (visible in the host's MCP log); the daemon writes to `CLASSCAD_MCP_LOG`. Run it by hand to watch: `CLASSCAD_MCP_DAEMON=1 node dist/daemon.js` (logs to stderr when `CLASSCAD_MCP_LOG` is unset).
215
- - **Troubleshooting:** `curl http://127.0.0.1:9097/health` — no answer means no daemon (the next tool call starts one); an answer without `"bridge"` means port 9096 is held by another program (usually an old MCP process — close that tab or kill it, the daemon retries by itself); a `name` other than `classcad-mcp` means a foreign program owns 9095 (set `CLASSCAD_MCP_PORT`). `classcad-mcp stop` stops an idle daemon (see below). Ports: 9094 worker (ws), 9095 worker (wss), 9096 bridge, 9097 daemon, 9098 the 3D view — all on `127.0.0.1` except the worker.
228
+ - **Troubleshooting:** `curl http://127.0.0.1:9097/health` — no answer means no daemon (the next tool call starts one); a `join` address on another port than 9098 means that port is held by another program (usually an MCP process of an older build — close that tab): app links still work, but apps that host their own session will not find the MCP; a `name` other than `classcad-mcp` means a foreign program owns 9097 (set `CLASSCAD_MCP_PORT`). `classcad-mcp stop` stops an idle daemon (see below). Ports: 9094 worker (ws), 9095 worker (wss), 9097 daemon, 9098 the app and the sessions — all on `127.0.0.1` except the worker. The share relay is reached by an outgoing WebSocket (443); `session_info` shows under `shared` whether it holds the session.
216
229
  - **Stopping / restarting:** the package binary doubles as a small CLI (`node dist/server.js <command>` in a checkout):
217
230
  - `classcad-mcp status` — the running daemon's `/health` (pid, build, sessions, apps, log file).
218
231
  - `classcad-mcp stop` — stops the daemon when no session is active; refuses (exit 1) otherwise.
219
232
  - `classcad-mcp stop --force` — SIGTERM (SIGKILL after 5 s) regardless of sessions, e.g. to load a rebuilt renderer right away. Every connected tab loses its drawing; its shim reconnects on the next tool call — it starts a fresh daemon and replays the host's `initialize`, so the host does not need a restart.
220
- - **Security:** the daemon binds `127.0.0.1` and refuses (403) every request that carries an `Origin` header or a `Host` other than `127.0.0.1` / `localhost` / `[::1]` — web pages cannot drive it, not even via DNS rebinding. The bridge WebSocket (9096) is meant for browser apps and is protected by share tokens instead.
233
+ - **Security:** the daemon binds `127.0.0.1` and refuses (403) every request that carries an `Origin` header or a `Host` other than `127.0.0.1` / `localhost` / `[::1]` — web pages cannot drive it, not even via DNS rebinding. The listener on 9098 is meant for browser apps: it answers only requests that name it as their host, and a session is joined only with its invite (a 122-bit random token) on a signed-in machine. Sharing a session beyond the machine opens no port: the MCP dials out to the relay, which takes the offer only with the machine's sign-in and lets in whoever has that session's share invite — an invite of its own, which the listener does not take and `share` takes back.
221
234
 
222
235
  ---
223
236
 
@@ -229,11 +242,12 @@ None is required. Set them in the host's MCP config `env` block; the shim passes
229
242
  | `tree` / `find` / `inspect` | Structure tree (cached, pulled on demand), search by class/name, full node detail with parent chain |
230
243
  | `snapshot` | Render the drawing to PNG (iso/top/front/…, section cuts, four-view sheet, technical drawing with hidden lines, highlights, markers) |
231
244
  | `list_methods` / `describe_method` / `docs` | Method index, per-method reference with LLM-oriented gotchas, recipes |
232
- | `view` | The session's live 3D view: returns its link and opens it in the user's browser |
233
- | `save` / `load` / `clear` / `checkpoint` / `restore` | `save` writes STEP, STL, GLB or OFB to a `path` on disk (or returns base64); `load` reads OFB / STEP / STL from base64 content. `checkpoint` / `restore` are undo points, kept in the MCP's memory. |
245
+ | `view` | The session's app: returns its link and brings it up (the host's browser pane, or the user's browser), unless the user already has it open. `local: true`: the link on this machine, for when the share link does not load |
246
+ | `share` | The link to the session's app that works from anywhere, for somebody on another machine; `stop: true` takes it back |
247
+ | `get_selection` / `set_selection` | What the user has selected in the app (faces, edges, vertices, tree objects), with the ids API calls take; and selecting something there to point it out |
248
+ | `save` / `load` / `clear` / `checkpoint` / `restore` | `save` writes STEP, STL, GLB or OFB to a `path` on disk (or returns base64); with `analytic` a STEP gets planes, cylinders and cones as such instead of B-spline surfaces. `load` reads OFB / STEP / STL from a `path` on disk (or from base64 content). `checkpoint` / `restore` are undo points, kept in the MCP's memory. |
234
249
  | `login` | Sign the machine in (returns the link, then waits for it to be used) or out |
235
- | `session_info` / `use_session` | Connection status (transport ws/bridge); attach to a named session, an invite link, or an in-app engine's `?bridge=` link |
236
- | `bridge.list_clients` / `bridge.get_selection` / `bridge.set_selection` | Read/write the selection of a connected CC app (see bridge) |
250
+ | `session_info` / `use_session` | Status (engine, the app's link, the share link under `shared`, who else is in the session, sign-in); switch engine, or join the session of an app with its invite link |
237
251
 
238
252
  `snapshot` returns the PNG as an inline image block **for the model** and writes it to `CLASSCAD_SNAPSHOT_DIR`. Claude Code and the desktop app's Code tab do not show tool-result images to the user — the tool result says so and names the saved file, so the model can hand it over (Claude Code: `SendUserFile`). The desktop app's chat renders it directly. Claude Code only previews sent files that live in the session's own folders (scratchpad or project) — anything else arrives as a grey placeholder card — so the tool tells the model to pass `outDir` = its scratchpad.
239
253
 
@@ -241,15 +255,14 @@ None is required. Set them in the host's MCP config `env` block; the shim passes
241
255
 
242
256
  ---
243
257
 
244
- ## Engines: worker, in-app bridge, local WASM
258
+ ## Engines: worker, or local WASM
245
259
 
246
- The MCP can run its commands on three kinds of engine. A token or URL always decides by itself; without one the **engine policy** decides.
260
+ The MCP runs its own sessions on one of two engines; the **engine policy** decides which. (Joining the session of an app is not a choice of engine: the engine is the app's, see [Sharing](#sharing-one-session-several-participants).)
247
261
 
248
262
  | Engine | What it is | How the MCP gets there |
249
263
  | --- | --- | --- |
250
- | **Worker (Drogon)** | A `classcad-cli worker` — a server on your machine, in Docker or hosted. Multi-client sessions, sharing with apps via `?invite=` links. | `CLASSCAD_WS_URL`, `use_session(sessionId)`, `use_session(url)` with a `ws(s)://` or `?invite=` link. |
251
- | **In-app bridge** | The engine inside a buerli app's browser tab (WASM in the page, e.g. buerligons.io). | `use_session(url)` with the app's `?bridge=` share link. |
252
264
  | **Local WASM** | The published ClassCAD WASM build, hosted by the MCP itself in a worker thread of the daemon. No server, no browser, works offline once the assets are cached. | Nothing to configure (a six-month key is built in); the policy (`auto`/`wasm`) or `use_session(engine="wasm")`. |
265
+ | **Worker (Drogon)** | A `classcad-cli worker` — a server on your machine, in Docker or hosted. | `CLASSCAD_WS_URL`, the policy (`auto`/`drogon`), `use_session(sessionId)`. |
253
266
 
254
267
  **Policy.** `CLASSCAD_ENGINE` (default `auto`) and the `engine` argument of `use_session`:
255
268
 
@@ -257,82 +270,96 @@ The MCP can run its commands on three kinds of engine. A token or URL always dec
257
270
  - `drogon` — the worker only. "Connect the MCP to my Drogon/ClassCAD server." No fallback; a dead worker is reported as such.
258
271
  - `wasm` — the local engine only. "Connect the MCP to WASM / work locally / offline."
259
272
 
260
- The choice sticks for the session until `use_session` changes it; a `?bridge=` or `?invite=` link overrides it for as long as that session is used. The fallback only fires when the worker is *unreachable* (refused, timeout) — errors after a successful connect are never papered over — and never for an explicitly named session id.
273
+ The choice sticks for the session until `use_session` changes it; an invite link overrides it for as long as that session is used. The fallback only fires when the worker is *unreachable* (refused, timeout) — errors after a successful connect are never papered over — and never for an explicitly named session id. A session that moves to another engine starts with that engine's (empty) drawing; an app docked into it is told and joins again with the same link.
261
274
 
262
275
  **What the local engine needs.**
263
276
 
264
- - **A key.** ClassCAD keys are bound to *allowed origins* (managed on classcad.ch/user). The engine's license check compares the origin it runs on with those. A Node process has no origin, so the MCP presents itself as `CLASSCAD_WASM_ORIGIN` (default `http://localhost:3000`). A **six-month key** (wasm, allowed origin `http://localhost:3000`, valid until 2027-03-30) is built in, so nothing needs configuring. The engine refuses an expired key, so a release with a renewed key has to ship before that date (`buerli-backend/functions/scripts/appkey.mjs` issues one); `CLASSCAD_WASM_KEY` swaps in your own key (set `CLASSCAD_WASM_ORIGIN` to match if it allows another origin).
277
+ - **A key.** The engine starts only with a ClassCAD key, which it checks itself. The MCP carries none: with the sign-in it asks the backend for one (`src/engine/key.ts`), which issues what the account's plan allows, for `localhost` on any port; the MCP says it runs on `http://localhost`, which is true. The key is kept in `~/.classcad-mcp/engine-key.json` until shortly before it expires and renewed ahead of time; while the backend cannot be reached, a kept key that has not expired still starts the engine. On CI, `CLASSCAD_TOKEN` (a secret access token) stands in for the sign-in. `CLASSCAD_WASM_KEY` takes a key of one's own instead. The plan's export formats apply to `save` and to the app's downloads: Free exports STL (and GLB, also triangles); STEP, OFB and JSON come with Solo and up.
265
278
  - **The release assets** (about 16 MB: glue, main module, three side modules, class file, filter config). Downloaded once from awvstatic.com into `CLASSCAD_WASM_DIR` on first use — the first tool call that needs the engine takes a moment longer and the daemon log shows the progress. Afterwards the engine starts in about a second per session (~130 MB heap each; one engine per session, ended with the session).
266
279
  - **Node ≥ 20** for `worker_threads` and `fetch` — the same requirement as the rest of the package.
267
280
 
268
- **What is different on the local engine.** It is the same engine as in the browser apps: one drawing, no multi-client sessions, no `?invite=` sharing, and no per-connection emission config (`run_script` runs without payload suppression — fine for the sizes an MCP session handles). `snapshot`, `tree`, `find`, `inspect`, `save`/`load`, `checkpoint`/`restore` all work. Nothing the MCP does on its own engine is visible to any app.
281
+ **What is different on the local engine.** It is the same engine as in the browser apps: one drawing, and no per-connection emission config (`run_script` runs without payload suppression — fine for the sizes an MCP session handles). `snapshot`, `tree`, `find`, `inspect`, `save`/`load`, `checkpoint`/`restore` all work. The session layer a server has around its engine (invites, guests, presence) the MCP provides itself: see [Sharing](#sharing-one-session-several-participants).
269
282
 
270
283
  **When the local engine fails.** Engine errors come back like on the worker (`maxLevel` 51 plus messages, so `run_script` throws), including the ones the WASM engine reports only in its nested `result` or as a separate ErrorMessage frame; a Result with neither a value nor a level is an error, never an empty success. An engine that crashes, traps mid-command or does not answer within the request timeout is retired: that command fails with the reason, and the next one starts a new engine with an **empty drawing** (checkpoints stay valid — `restore` loads them into the new engine). `use_session(engine="wasm"|"auto")` health-checks a running engine and replaces it if it does not answer; a healthy one is kept together with its drawing (`clear` empties it).
271
284
 
272
285
  ---
273
286
 
274
- ## Sessions: your own, or one an app already has open
287
+ ## Sharing: one session, several participants
275
288
 
276
- By default the MCP gets a fresh session from the worker. Two ways to work on a session that already exists:
289
+ A session can have more than one participant: the agent, the user at an app, a colleague on another machine, another agent. They share **one engine and one model**; every change of one is sent to the others as it happens. The protocol is the session sharing of a ClassCAD server (invites, guests, presence, every command's frames fanned out; `SessionSharing.md` in the ClassCAD sources specifies it, `WSClient` of `@buerli.io/classcad` is the client). Who owns the session depends on where the engine runs, and the MCP takes part in all three:
277
290
 
278
- **Named session** (`ClassCAD-Session-Id` model):
291
+ | The engine runs … | The session is hosted by | The others join |
292
+ | --- | --- | --- |
293
+ | in the MCP (local WASM) | the MCP: `src/share/hub.ts` is the session layer of a server for its own engine | with the session's invite on `ws://127.0.0.1:9098/session/?invite=…` — the app's link does that |
294
+ | on a ClassCAD worker | the server | with a server invite on the worker's URL; an MCP session's app is passed through to it, so its link is the same |
295
+ | in the page of an app (`WASMClient`) | the app (`session/host.ts` in `@buerli.io/classcad`) | where the MCP introduces them: the page offers its invite on `ws://127.0.0.1:9098/session/?host=…`, guests join with `?invite=…`, and the listener joins the two |
279
296
 
280
- ```
281
- use_session(sessionId="test-session")
282
- ```
297
+ A colleague, or the user on another device, joins a session the MCP hosts through the share relay: see [Somebody elsewhere joins](#somebody-elsewhere-joins).
283
298
 
284
- **Invite link** (multi-client server): in buerligons open *Session Management → Create* and hand the link to the model:
299
+ ### The agent hosts, the user joins
285
300
 
286
- ```
287
- use_session(url="https://app.example/?invite=…") # app share link — the token is applied to CLASSCAD_WS_URL
288
- use_session(url="wss://cad.example/?invite=…") # or the worker URL directly
289
- ```
301
+ The default: every session's app docks into it (see [The app](#the-app)); `view` returns the link.
290
302
 
291
- The MCP then joins that session as a guest; the app sees every change the model makes. `session_info` reports the current session, `use_session()` without arguments returns to a fresh one.
303
+ ### Somebody elsewhere joins
292
304
 
293
- **In-app engines (WASM).** A buerli app that runs ClassCAD in the page (`WASMClient`, e.g. buerligons.io) has no server anyone could connect to. Its share links carry `?bridge=<token>` instead: when the app mints the token it opens an outbound WebSocket to the daemon's bridge listener (`CLASSCAD_BRIDGE_LISTEN`, default `ws://127.0.0.1:9096/bridge`) and announces it; `use_session(url)` with such a link attaches the MCP to that app, and from then on every engine command is relayed to the page (`engine.execute`) — `run_script`, `tree`, `snapshot` all work, the app's view follows, and revoking the share in the app ends the MCP session. The app must stay open, the MCP must run on the same machine as the browser (or the app must be pointed at the listener with `?mcpBridge=ws://…`), and the published WASM engine has no per-connection emission config, so scripts run without suppression there.
305
+ A link on `127.0.0.1` is good on the user's machine and nowhere else. So a session is offered on the **share relay** as well, and where the relay holds it, the app comes up under that link instead — one link (`App: https://<relay>/?invite=…`), which the user opens and can send to a colleague or open on a tablet:
294
306
 
295
- Same API on both sides: `client.createInvite()` / `revokeInvite()` / `peers` / `invites` and `buildShareUrl()` from `@buerli.io/classcad` work for server sessions and in-app engines alike; only the query parameter differs (`?invite=` vs `?bridge=`).
307
+ ```
308
+ view() → the app's link: the share link, or the one on 127.0.0.1 while the relay does not hold the session
309
+ view(local=true) → the link on 127.0.0.1, whatever the relay does
310
+ share() → the share link again
311
+ share(stop=true) → the share link stops working, whoever has the app open under it is disconnected
312
+ ```
296
313
 
297
- ### Emission model
314
+ **It cannot break the session.** The relay is asked while the first script runs. One that has turned the offer down by the time the model is there is not waited for; one that has not answered gets 6 s more. Either way the first model then comes with the link on `127.0.0.1` — no error. Should the relay go away later and take the user's app with it, the next result offers the app again, on this machine; and an agent that sees the share link not loading calls `view` with `local: true`. The offer itself is made again in the background until the relay takes it. `CLASSCAD_SHARE=ask` keeps the app on `127.0.0.1` and shares a session only when the user asks (`share` then returns a second link), `off` never.
298
315
 
299
- The engine keeps an emission config **per connection** (`GetEmissionConfig`/`SetEmissionConfig`). The MCP leaves its connection at the engine defaults, so an app sharing the session keeps receiving structure and graphic for everything the model does. While a `run_script` runs, payloads are switched off for that script only (results only — a 100-command script is 100 small replies) and restored afterwards; the MCP pulls once after every script so the app and its own caches converge. `api.tree()` / `api.graphic()` inside a script pull on demand and are cached until the next mutation.
316
+ Whoever opens the share link gets the same app, docked into the same session: they see the model as the agent builds it, and can turn, select and edit it. The session stays where it is. The MCP offers it on the **share relay** ([`packages/relay`](../relay), a Cloudflare Worker) the way a page offers a session of its own on the listener — a standing connection that says "this invite is mine", and one more connection per guest — only that it dials out to do so (`src/share/relay.ts`). Nothing listens for the outside, and nothing but Node is needed on the machine; it works behind NAT and firewalls.
300
317
 
301
- ---
318
+ - **An invite of its own.** The share link carries another invite than the link on `127.0.0.1`, and the listener there does not take it. Taking it back (`share` with `stop`) disconnects everyone who came in with it — the user too, if their app is open under it; the link on `127.0.0.1` is untouched. Sharing again gives a new one.
319
+ - **The user's app goes through the relay too** when it is open under the share link: every frame makes the round trip to Cloudflare. `CLASSCAD_SHARE=ask` keeps it on the machine.
320
+ - **Whoever has the link is in**, like the user at the app (the `view` role is advisory, see `src/share/protocol.ts`). The agent hands it to the user only.
321
+ - **What the relay sees.** It takes an offer only from a signed-in machine (the Firebase ID token of the sign-in, checked against Google's keys), passes the frames between host and guest, and keeps none. It also serves the app to the guest — the build this package carries, deployed with the relay.
322
+ - **When the connection drops** (a sleeping laptop, another network) the MCP offers the invite again by itself, and the link keeps working. `session_info` shows the link under `shared` (null while the relay does not hold it), and the guests among `peers`.
323
+ - **Limits.** 16 guests per session; one frame is at most 32 MiB (Cloudflare's limit — a very large model's complete graphic may not fit).
324
+
325
+ `CLASSCAD_RELAY_URL` names another relay than the built-in one; `off` leaves sessions on their machine: no `share` tool, and the agent is not told of any.
302
326
 
303
- ## App-state bridge
327
+ ### The user hosts, the agent joins
304
328
 
305
- The MCP can read and write *client-side* state (selection today; view, current product, hover planned) of a CC app — provided the app opens an outbound WebSocket to the MCP and announces itself for the same session. It complements the server-side state the other tools already see.
329
+ In the app open *Sessions → Create* and hand the link to the agent:
306
330
 
307
331
  ```
308
- announce + events ┌────────────────┐
309
- stdio (Claude / VS Code) ─────► cc MCP ──── ws ────────►│ buerligons │
310
- ┌──────┐ requests │ (or any CC app)│
311
- │bridge│ └────────────────┘
312
- │ tools│
313
- └──────┘
314
- │
315
- ▼
316
- ws://localhost:9094 (the ClassCAD worker)
332
+ use_session(url="https://app.example/?invite=…") # the link of the app, wherever its engine runs
333
+ use_session(url="wss://cad.example/?invite=…") # or a server's URL directly
317
334
  ```
318
335
 
319
- - The daemon listens on `CLASSCAD_BRIDGE_LISTEN` (default `ws://127.0.0.1:9096/bridge` — `127.0.0.1` on purpose: `localhost` may resolve to IPv6 in the browser and IPv4 in Node, and a listener binds one of them). One listener per machine, shared by all sessions; the token in the app's share link decides which session an app belongs to.
320
- - The app connects outbound and sends an `announce` with its `sessionId`, `drawingId`, app name and capabilities; the MCP routes `bridge.*` calls to the app whose session matches the one `use_session` attached to.
321
- - No app connected → the bridge tools return a clean "no bridge connected"; everything else works unchanged.
336
+ The MCP joins that session as a guest and works in the app's model; the app shows every change. Whose engine it is, the link does not say: an invite that an app on this machine offers is joined here, any other is applied to the worker (`CLASSCAD_WS_URL`). An app with the engine in its page must stay open, and must run on this machine. `session_info` reports the session and who is in it; `use_session()` without arguments returns to a session of the agent's own. A second agent joins the first one's session the same way, with the link `view` gave the first.
322
337
 
323
- Wire format (see `src/bridge/protocol.ts`):
338
+ **Named session** (`ClassCAD-Session-Id` model, worker only): `use_session(sessionId="test-session")`.
324
339
 
325
- ```json
326
- { "type": "announce", "protocolVersion": 1, "sessionId": "test-session", "drawingId": "<id>", "app": "buerligons",
327
- "capabilities": ["selection.read", "selection.write"], "clientId": "buerligons-abc123" }
340
+ ### What the browser has to allow
328
341
 
329
- // MCP → app // app → MCP
330
- { "type": "request", "id": 7, "method": "selection.get" }
331
- { "type": "response", "id": 7, "result": [{ "kind": "face", "classcadId": 460, "raw": { "containerId": 514, "graphicId": -17, "prodRefId": 319 } }] }
332
- { "type": "event", "channel": "selection.changed", "payload": { "items": [] } }
333
- ```
342
+ The app in this package is served from `127.0.0.1` and joins its session on the same address: nothing to allow. An app that is served from somewhere else and has to reach this machine's listener (`ws://127.0.0.1:9098/session`) is a different case: a page with the engine in it that offers its session, a second page that joins such a session, or an app named by `CLASSCAD_APP_URL`. Browsers treat a public page that connects to `127.0.0.1` as local network access: Chromium-based browsers ask the user for permission (or deny it where nobody can be asked, as in an embedded browser pane), and without it the page never finds the MCP. An app served from `localhost` (a dev server) is not affected, and neither is an app whose session runs on a ClassCAD server.
343
+
344
+ ### What participants see of each other
345
+
346
+ Besides the model, participants publish a little about themselves, as *presence* — small frames a session relays to everyone in it and keeps for those who join later. The buerli packages define the channels (`session/selection.ts`, `session/identity.ts` in `@buerli.io/classcad`; `session/sessionClient.ts` in `@buerli.io/react-cad`), the MCP speaks them (`src/share/protocol.ts`), and they work the same in all three kinds of session:
347
+
348
+ | Channel | Who | What |
349
+ | --- | --- | --- |
350
+ | `client` | everyone | Who the participant is: `{ app, version, name, kind: 'app' \| 'agent' }`. `session_info` lists the others. |
351
+ | `selection` | apps | What is selected, whenever it changes: `{ items: [{ kind, objectId, prodRefId, containerId, graphicId, type, name }], total }`. `get_selection` reads it. |
352
+ | `select` | the agent | A request to select `items` (by `graphicId` or `objectId`); `set_selection` sends it and reports what the app has selected afterwards. |
353
+ | `config` | the host | What a session offers its guests, for a host that offers less than everything: `{ saveFormats: ['STP', 'STL'] }`. Apps hide what is not offered. The MCP's own sessions keep nothing back and publish none. |
354
+ | `view`, `cursor` | apps | The cameras and pointers the apps show of each other. |
355
+
356
+ A selected face, edge or vertex carries its `graphicId`: the id API calls take for it (e.g. `v1.sketch.create({ planeId })`), valid for the model as it is now. An app shares its selection by calling `syncSelection(client)`.
334
357
 
335
- Selection entities carry resolved fields (`kind`, `position`, `normal`) and the raw `{containerId, graphicId, prodRefId}` triplet that goes straight into API calls (e.g. `v1.sketch.create({ planeId: raw.graphicId })`). Reference implementation: `packages/modeler/src/mcpBridge.ts` in the buerli monorepo (~200 lines; `?mcpBridge=off` disables it).
358
+ ### Emission model
359
+
360
+ On a worker the engine keeps an emission config **per connection** (`GetEmissionConfig`/`SetEmissionConfig`). The MCP leaves its connection at the engine defaults, so an app sharing the session keeps receiving structure and graphic for everything the model does. While a `run_script` runs, payloads are switched off for that script only (results only — a 100-command script is 100 small replies) and restored afterwards; the MCP pulls once after every script so the app and its own caches converge. `api.tree()` / `api.graphic()` inside a script pull on demand and are cached until the next mutation.
361
+
362
+ The local engine always generates everything a command changed. A session the MCP hosts therefore sends every participant every change as it happens, each in the form its own connection asked for (streamed frames for an app, results only for an agent's script); nobody's emission config starves the others.
336
363
 
337
364
  ---
338
365
 
@@ -342,19 +369,29 @@ Selection entities carry resolved fields (`kind`, `position`, `normal`) and the
342
369
  npm install # monorepo root
343
370
  npm run build # skill → script → renderer → mcp (+ buerli-ai)
344
371
  cd packages/mcp
345
- npm run build # tsc; postbuild runs the emission + daemon contract tests against a fake worker
346
- npm test # + live tests against a worker on CLASSCAD_WS_URL (skipped if none)
372
+ git submodule update --init # the app's sources: vendor/buerligons, vendor/react-cad, vendor/buerli
373
+ (cd ../../vendor/buerligons && yarn install --pure-lockfile) # the app's dependencies
374
+ npm run build:app # the app: Buerligons with react-cad and buerli from their sources (BUERLIGONS_DIR, BUERLI_REACT_CAD, BUERLI) → app/
375
+ npm run build # tsc, app/ → dist/app/; postbuild runs the contract tests (emission, daemon, sign-in, sharing, the relay)
376
+ npm test # + the live tests: the local WASM engine, and a worker on CLASSCAD_WS_URL if one runs
347
377
  node dist/server.js # the stdio shim by hand (starts/uses the daemon)
348
378
  CLASSCAD_MCP_DAEMON=1 node dist/daemon.js # the daemon in the foreground, logging to stderr
379
+ CLASSCAD_SHARE_DEBUG=1 … # … with every command a docked app sends: how long it waited, how long it ran
380
+ npm run dev -w @classcad/relay # the share relay on http://127.0.0.1:8787, with the app build:app made (no Cloudflare account needed) …
381
+ CLASSCAD_RELAY_URL=http://127.0.0.1:8787 … # … and sessions shared on it
382
+ npm test -w @classcad/relay # the relay's contract, in wrangler's local runtime (Node 22+)
349
383
  ```
350
384
 
351
- Source map: `src/server.ts` (stdio shim: find/start daemon, proxy, in-process fallback), `src/daemon.ts` (HTTP endpoint, sessions, bridge listener, idle exit), `src/mcp-server.ts` (the MCP server: tools wired to one engine client — what a session is), `src/client.ts` (engine client: WebSocket to a worker or bridge to an app), `src/bridge/` (listener + protocol), `src/engine/wasm.ts` + `wasm-worker.ts` (the local WASM engine: asset download, worker thread, origin/XHR shims), `src/tools/`, `src/queue.ts` (per-session tool queue). `test/daemon.mjs` is the daemon contract: two shims → one daemon with two independent sessions, idle exit, foreign-port fallback. `test/wasm-live.mjs` (part of `npm test`; downloads the assets on first run) covers the local engine: policy `wasm`, `auto` fallback with a dead worker, `drogon` without fallback, switching with `use_session`, engine errors surfacing through `run_script`, and replacing a crashed, hung or wedged engine.
385
+ The tests open no browser (`CLASSCAD_VIEWER_NO_BROWSER=1`, `CLASSCAD_AUTH_NO_BROWSER=1`) and sign in against a stand-in (`test/fake-auth.mjs`: `CLASSCAD_AUTH_TOKEN_URL`, `CLASSCAD_AUTH_PROJECT`, `CLASSCAD_AUTH_API_KEY`).
386
+
387
+ Source map: `src/server.ts` (stdio shim: find/start daemon, proxy, in-process fallback), `src/daemon.ts` (HTTP endpoint, sessions, idle exit), `src/mcp-server.ts` (the MCP server: tools wired to one engine client — what a session is), `src/client.ts` (engine client: WebSocket to a worker, or the local engine), `src/share/` (sharing: `hub.ts` the session layer for the local engine, `server.ts` the listener for the app and for sessions, `relay.ts` the session offered on the share relay, `session.ts` who is there, what they point at and the links, `protocol.ts`), `src/engine/wasm.ts` + `wasm-worker.ts` (the local WASM engine: asset download, worker thread, origin/XHR shims), `src/tools/`, `src/queue.ts` (per-session tool queue). `test/daemon.mjs` is the daemon contract: two shims → one daemon with two independent sessions, idle exit, foreign-port fallback. `test/share.mjs` is the sharing contract, without an engine: a guest is served like a server serves it (streamed and bundled), presence, what only a host may do, a page-hosted session joined through the listener, and a session offered on the relay (the listener stands in for it; `packages/relay` tests the relay itself). `test/wasm-live.mjs` (part of `npm test`; downloads the assets on first run; needs a way to an engine key — a secret access token in `CLASSCAD_TOKEN`, a sign-in from `login` in `CLASSCAD_LIVE_AUTH_FILE` with its `CLASSCAD_BACKEND`, or `CLASSCAD_WASM_KEY` — and is skipped without one) covers the local engine: policy `wasm`, `auto` fallback with a dead worker, `drogon` without fallback, switching with `use_session`, engine errors surfacing through `run_script`, replacing a crashed, hung or wedged engine, and sharing: an app docked into the MCP's engine (changes and selection both ways) and a second agent joining the first one's session.
352
388
 
353
389
  ### Publishing
354
390
 
355
391
  Releases run in GitHub Actions ([release.yml](../../.github/workflows/release.yml)), without tokens or 2FA prompts:
356
392
 
357
- 1. Bump the MCP's version everywhere it is named: `package.json`, `VERSION` in `src/mcp-server.ts`, `server.json` (`version` and `packages[0].version`), and the Claude plugin (`plugins/classcad/.claude-plugin/plugin.json` and `PACKAGE` in `plugins/classcad/launch.mjs`). Bump `@classcad/skill`, `script` or `renderer` too if they changed.
393
+ 0. The app has to be in `packages/mcp/app/` when the release is built: `npm run build:app` builds Buerligons into it from the submodules pinned under `vendor/` (`app/SOURCE.txt` names the commits). The workflow does not build it (it checks out no submodules), and `release-check` stops a release that would go out without the app.
394
+ 1. Bump the MCP's version everywhere it is named: `package.json`, `VERSION` in `src/mcp-server.ts`, `server.json` (`version` and `packages[0].version`), and the Claude plugin (`plugins/classcad/.claude-plugin/plugin.json` and `PACKAGE` in `plugins/classcad/launch.mjs`). The plugin is installed from this repository's `master` and runs the package version it pins: its README and its skill describe that version, so what a release changes for the user goes into them with this step, not before. Bump `@classcad/skill`, `script` or `renderer` too if they changed.
358
395
  2. `node scripts/release-check.mjs mcp-vX.Y.Z` (from the repo root) checks that the versions agree and lists what is new on npm.
359
396
  3. Commit, push, then `git tag mcp-vX.Y.Z && git push origin mcp-vX.Y.Z`.
360
397
 
@@ -362,6 +399,10 @@ The workflow builds and tests the tag, publishes every `@classcad` package whose
362
399
 
363
400
  One-time setup, per package on npmjs.com: Settings → Trusted publishing → GitHub Actions, organization `awv-informatik`, repository `classcad-ai`, workflow `release.yml`.
364
401
 
402
+ A patch to a released line is made on its release branch (`release/mcp-0.2` for 0.2.x), tagged there and merged into `master` afterwards; `master` is pushed only once npm has the version, since the plugin installed from it pins that version.
403
+
404
+ The share relay is not part of a release: it is deployed on its own (`npm run deploy -w @classcad/relay`, see [its README](../relay)), and serves guests the app this package carries. Deploy it again after a release that changes the app.
405
+
365
406
  ## Build failure and cancellation behavior
366
407
 
367
408
  `run_script` rejects engine errors and reports progress when the caller supplies
@@ -0,0 +1,4 @@
1
+ Buerligons, built in mcp mode on 2026-10-05
2
+ buerligons 61e58eb
3
+ react-cad 1.2.0 (npm)
4
+ buerli 1.2.0 (npm)