scanruler-mcp 0.4.0 → 0.4.1

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.
package/README.md CHANGED
@@ -1,431 +1,434 @@
1
- # scanruler-mcp
2
-
3
- Drive [ScanRuler](https://scanruler.com/), the browser tool for measuring 3D
4
- scans, from an AI agent. This is a [Model Context Protocol](https://modelcontextprotocol.io)
5
- server: the agent starts it and calls its tools; the ScanRuler tab open in
6
- your browser connects to it over `127.0.0.1` and runs each call there, in
7
- front of you. Every step the agent takes is one step of the tab's undo
8
- history, so you can take any of it back.
9
-
10
- Nothing goes anywhere but your own computer: the scan travels from your disk
11
- to your browser through this server, as it does when you open it yourself.
12
- What the agent reads — the figures, the report — goes to the agent's model
13
- provider, like everything else it reads. The server's one request of its own
14
- asks npm, once a day, for its latest version (see *Staying current*).
15
-
16
- ## Setting up
17
-
18
- You need [Node.js](https://nodejs.org/) 22 or newer, and Chrome, Edge or
19
- Firefox.
20
-
21
- ### 1. Add the server to your agent
22
-
23
- **Claude Code**
24
-
25
- ```bash
26
- claude mcp add --scope user scanruler -- npx -y scanruler-mcp@latest
27
- ```
28
-
29
- `--scope user` has it in every project; leave it out for this one only.
30
- Exports the agent makes land in the folder Claude Code was started in,
31
- unless the agent names a path.
32
-
33
- **Codex** — the CLI, the IDE extension and the app share one setting:
34
-
35
- ```bash
36
- codex mcp add scanruler -- npx -y scanruler-mcp@latest
37
- ```
38
-
39
- or, in `~/.codex/config.toml`, with room for the first start (npx fetches the
40
- package) and for long calls — Codex gives a server 10 seconds to start and a
41
- call 60 unless told otherwise:
42
-
43
- ```toml
44
- [mcp_servers.scanruler]
45
- command = "npx"
46
- args = ["-y", "scanruler-mcp@latest"]
47
- startup_timeout_sec = 60
48
- tool_timeout_sec = 300
49
- ```
50
-
51
- **Claude Desktop** — Settings → Developer → Edit Config, then in
52
- `claude_desktop_config.json`:
53
-
54
- ```json
55
- {
56
- "mcpServers": {
57
- "scanruler": {
58
- "command": "npx",
59
- "args": ["-y", "scanruler-mcp@latest", "--out", "/Users/you/Documents/ScanRuler"]
60
- }
61
- }
62
- }
63
- ```
64
-
65
- Claude Desktop starts servers in a folder of its own, so give `--out` a folder
66
- for the exports, and name files to the agent by their full path. Quit Claude
67
- Desktop after saving — File → Exit; closing the window leaves it running —
68
- and start it again. It starts the server twice, for its chats and for its
69
- Cowork and Code sessions; the copy that starts first holds the port and the
70
- tab connects to it, and the other sends its calls through that one — see
71
- *Several agents* below.
72
-
73
- **Cursor** — `.cursor/mcp.json` in a project, or `~/.cursor/mcp.json` for all:
74
-
75
- ```json
76
- {
77
- "mcpServers": {
78
- "scanruler": {
79
- "command": "npx",
80
- "args": ["-y", "scanruler-mcp@latest"]
81
- }
82
- }
83
- }
84
- ```
85
-
86
- **From a clone of the ScanRuler repository**, to try a change: run
87
- `npm --prefix mcp ci` once, then use
88
- `node /path/to/scanruler/mcp/bin/scanruler-mcp.js` where the examples say
89
- `npx -y scanruler-mcp@latest` — in Claude Code,
90
- `claude mcp add scanruler -- node /path/to/scanruler/mcp/bin/scanruler-mcp.js`.
91
-
92
- ### 2. Let the agent open ScanRuler
93
-
94
- Ask the agent for something in ScanRuler — or just *"Connect to ScanRuler"*.
95
- With no tab connected, it calls `scanruler_open`, which opens ScanRuler in
96
- your default browser with the pairing in its address: the tab switches the
97
- connection on with this server's port and token, and connects. The first
98
- time, **Chrome and Edge** ask whether scanruler.com may access "apps and
99
- services on this device" — allow it, that is this server. **Firefox** asks
100
- the same. The tab keeps the pairing, so this is done once per browser.
101
-
102
- When the default browser is not the one you use ScanRuler in, or the agent
103
- runs somewhere it cannot open one, pair by hand:
104
-
105
- ```bash
106
- npx -y scanruler-mcp@latest --pair
107
- ```
108
-
109
- prints the port, the pairing token and a link. Open the link once in the
110
- browser you use ScanRuler in, or in ScanRuler go to ⚙ **Settings → Agents**,
111
- switch on **Local agent connection**, and enter the port and the token. The
112
- token is made on the first run and kept in your config folder
113
- (`%APPDATA%\scanruler-mcp` on Windows,
114
- `~/Library/Application Support/scanruler-mcp` on macOS,
115
- `$XDG_CONFIG_HOME/scanruler-mcp` elsewhere, `~/.config/scanruler-mcp`
116
- without it). `scanruler_status` hands out the same
117
- link.
118
-
119
- A chip in ScanRuler's top bar says where the connection stands: **listening**
120
- while the agent's server is not running, **connected**, or **turned away**
121
- (a wrong token, or another tab connected already — the server takes one tab
122
- at a time). If you denied the browser's question, allow *Local network
123
- access* for the site in the site settings (the icon left of the address) and
124
- reload. **Safari** does not let a page from the web reach the computer it
125
- runs on at all; use `--serve` below.
126
-
127
- ### Staying current
128
-
129
- With `@latest` in the setting, npx asks npm for the newest version at every
130
- start and fetches it when there is one, so quitting the app and opening it
131
- again updates the server. Offline, npx starts the copy it has; on a network
132
- that turns connections away rather than having none, it tries for about a
133
- minute first. (npm 9.7 and newer treat a bare `scanruler-mcp` the same way.)
134
-
135
- The server also keeps itself current — for a setting without `@latest` on an
136
- older npm, or a copy installed with `npm install -g`. Once a day, in the
137
- background, it asks npm's registry (registry.npmjs.org, or the one npm is set
138
- to) for its latest version: that one request, given 2 seconds. When there is
139
- a newer one, npm fetches it into its cache, and the next start runs it in
140
- place of the old copy, without waiting on the network. What it found is kept
141
- in `update.json` in the config folder, and `scanruler_status` says it:
142
- `server.version`, `server.latest`, and `server.handedOffFrom` when an older
143
- copy started this one. A version pinned in the setting
144
- (`scanruler-mcp@0.4.0`) is left as it is, and so is a copy run from a clone
145
- of the repository. To turn it off: `--no-update`, or
146
- `SCANRULER_MCP_NO_UPDATE=1`.
147
-
148
- When ScanRuler shows **Your ScanRuler connector is out of date**, the server
149
- your app starts is older than the page needs: change the setting to the
150
- `@latest` line it shows, then quit the app completely — not just its window —
151
- and open it again.
152
-
153
- ### Several agents
154
-
155
- Any number of copies of the server can share the tab. The first to start
156
- holds the port and the tab's connection; one that finds the port taken
157
- connects to that one, with the same token, and sends its calls through it.
158
- When the copy holding the port stops, another takes the port over, and the
159
- tab — which keeps trying — connects to it within ten seconds.
160
- `scanruler_status` says which this copy is (`server.role`: `holder`,
161
- `relay`, or `none` with `server.error` saying why). Copies with different
162
- tokens cannot share a port: give them the same `SCANRULER_MCP_TOKEN`, or
163
- each its own `--port`. Changes from several agents run one after another in
164
- the tab, each one step of the same undo history.
165
-
166
- ### Serving the app yourself
167
-
168
- ```bash
169
- npx -y scanruler-mcp@latest --serve /path/to/scanruler/dist
170
- ```
171
-
172
- serves a built ScanRuler (`npm run build` in a clone) at
173
- `http://127.0.0.1:7317/` from the same port the tab connects to — for Safari,
174
- for working offline, and for the open-source build. A page from there needs
175
- no browser permission, and `--pair` then gives a link to it.
176
-
177
- ### Without a browser (headless)
178
-
179
- ```bash
180
- npm run build:engine # in a clone of ScanRuler
181
- npx -y scanruler-mcp@latest --engine /path/to/scanruler/dist-engine/engine.mjs
182
- ```
183
-
184
- runs ScanRuler itself inside the server, with no browser and no tab: the
185
- engine is a build of the app for Node, `dist-engine/engine.mjs`, which the
186
- server loads from where it is and names nothing in. `--headless` takes the
187
- engine built in the checkout the server is part of; `SCANRULER_ENGINE` gives
188
- a path as `--engine` does. There is no scan, no viewport and nobody
189
- watching, so only the commands that run without a page are tools —
190
- `session_state` and `history_undo`/`history_redo` in the app itself, and
191
- whatever a build of ScanRuler marks to run so; a tool that needs the page
192
- (`view_render`, `scan_open` …) answers `unavailable`, naming the page mode.
193
- Files still go by `path`. The server's instructions say it runs headless,
194
- `scanruler_status` says `backend: "headless"` and where the engine is, and
195
- `scanruler_open` has nothing to open. Each server is a session of its own —
196
- an agent's subagents can each start one and work side by side, sharing
197
- nothing — and what the app logs goes to stderr with the server's own lines.
198
- `--tools compact` works headless too: the server's own tools are listed, and
199
- the engine's are read through `scanruler_tools` and run through
200
- `scanruler_batch` (see *Small local models*).
201
- ### Small local models
202
-
203
- Listed in full, every command the tab offers is a tool of its own: some sixty
204
- tools and 25k tokens of schema in the open-source build, over a hundred and
205
- 50k tokens at scanruler.com, all read before the conversation starts. A
206
- frontier model takes that in its stride. A local model with a 128k window
207
- runs out of room within a few tool rounds, and picks less reliably among so
208
- many. For such a model, start the server with `--tools compact` (or set
209
- `SCANRULER_MCP_TOOLS=compact`). It then lists about a dozen tools, a few
210
- thousand tokens: the server's six, `scanruler_tools`, and the tab's tools
211
- that return a picture — `view_render`, `section_render`, `flat_render` and
212
- `scan_heightmap`.
213
-
214
- - `scanruler_tools` with no arguments lists the tab's tools, a name and a
215
- line each, about 1.5k tokens for a hundred. With `names` it gives those
216
- tools' full definitions, as the full list has them, up to 20 at a time.
217
- Names it does not know come back in `unknown`.
218
- - The model runs the tools through `scanruler_batch`, one call as a batch of
219
- one. A refused call comes back with the tab's error code and what was
220
- wrong, so the model can try again with corrected arguments. A client that
221
- lets the model call a tool it was not given can call them directly too.
222
- - The tools that return a picture are listed so the model calls them
223
- directly and sees the picture. In a batch a picture is not shown: it is
224
- written to a file, the call's `path` or a new file in the output folder
225
- (`--out`), and the call's result names it. A tab that connects with
226
- another picture tool has the client list the tools again.
227
-
228
- **LM Studio**: add `--tools` and `compact` to the server's arguments as two
229
- separate entries. In `mcp.json`:
230
-
231
- ```json
232
- {
233
- "mcpServers": {
234
- "scanruler": {
235
- "command": "npx",
236
- "args": ["-y", "scanruler-mcp@latest", "--tools", "compact"]
237
- }
238
- }
239
- }
240
- ```
241
-
242
- **Codex** with a local model:
243
-
244
- ```bash
245
- codex mcp add scanruler -- npx -y scanruler-mcp@latest --tools compact
246
- ```
247
-
248
- or `args = ["-y", "scanruler-mcp@latest", "--tools", "compact"]` in
249
- `~/.codex/config.toml`.
250
-
251
- ## Options
252
-
253
- | Option | |
254
- |---|---|
255
- | `--port <n>` | The port the tab connects to; 7317 unless given. Also `SCANRULER_MCP_PORT`. |
256
- | `--out <dir>` | Where exports go when a tool call names no path; the folder the server was started in otherwise. |
257
- | `--serve <dir>` | Also serve a built ScanRuler from `<dir>`. |
258
- | `--allow-origin <url>` | Let a ScanRuler tab from another address connect — a self-hosted copy. Repeatable. scanruler.com and any page on this computer are always allowed. |
259
- | `--app <url>` | The ScanRuler address the pairing link opens. |
260
- | `--engine <file>` | Run ScanRuler headless in the server, from an engine built with `npm run build:engine` — no tab. Also `SCANRULER_ENGINE`. |
261
- | `--headless` | The same, with the engine built in the checkout the server is part of. |
262
- | `--tools <mode>` | `full`, the default: every command the tab offers is a tool. `compact`: the server's own and the tab's tools that return a picture; the rest are read through `scanruler_tools` and run through `scanruler_batch` — see *Small local models*. Also `SCANRULER_MCP_TOOLS`. |
263
- | `--trace <dir>` | Write a line per tool call — tool, ms, bytes each way, error, whether the tab was hidden — into a new file in `<dir>`, for measuring a run; off unless given. `scripts/trace-summary.mjs` in the ScanRuler repository sums a run up. Also `SCANRULER_MCP_TRACE`. |
264
- | `--trace-args` | With `--trace`, each call's arguments too; otherwise only their size. |
265
- | `--answer-within <s>` | Hold a call at most this many seconds, 45 unless given: a change, a batch or a long read still running then answers `{ queued: true, job, after }` and runs on, and `scanruler_wait` waits no longer either. A client, or a bridge between it and this computer, may cut a call at a minute; a cut call says nothing. `0` holds every call to its end. Also `SCANRULER_MCP_ANSWER_WITHIN`. |
266
- | `--no-update` | Do not keep the server current (see *Staying current*). Also `SCANRULER_MCP_NO_UPDATE=1`. |
267
- | `--pair` | Print the port, the token and the pairing link, and exit. |
268
- | `--version` | Print the server's version, and exit. |
269
- | `--help` | The options. |
270
-
271
- `SCANRULER_MCP_TOKEN` overrides the stored token; `SCANRULER_MCP_CONFIG_DIR`
272
- moves the config folder.
273
-
274
- ## Tools
275
-
276
- Every command the ScanRuler tab offers is a tool, its name with dots turned
277
- into underscores — `scan_open`, `element_fit`, `dimension_add`,
278
- `report_get`, `history_undo` and the rest; their descriptions say what each
279
- needs and returns. The list is the tab's while one is connected — a build
280
- with more commands lists more. Before a tab is there it is the list of the
281
- last tab that connected, which the server keeps in its config folder
282
- (`tools.json`), or, the first time, the app's own; a client that lists the
283
- tools once, when it starts, sees them all that way, and one that listens is
284
- told when the list changes. Six more are the server's: `scanruler_status`
285
- (is a tab connected, which version, how to pair one, and what the tab's
286
- session holds — its scan, project, unsaved changes and work counted — and,
287
- when the client's list is out of date, which tools were added, removed or
288
- changed since and which page it came from),
289
- `scanruler_open` (open ScanRuler in the default browser, paired, and wait
290
- for it), `scanruler_wait` (until a tab is connected, or also idle),
291
- `scanruler_batch` (several of the tab's tools in one call, in order — a run
292
- of `scan_nearest` or `element_fit` calls — each answer in its place, stopped
293
- at the first refused unless told to go on), `scanruler_write` (a text
294
- file — `.md`, `.txt`, `.csv` or `.json`, a report say — written in the folder
295
- the server last opened a scan or a project from, and nowhere else) and
296
- `scanruler_files` (that folder's files — name, size, kind — newest first).
297
- The folders are kept beside `tools.json` (`folders.json`), so a server
298
- started afresh still writes and lists there, and `scanruler_status` lists
299
- them. With `--tools compact` only the tab's tools that return a picture are
300
- listed; the rest are read through a seventh of the server's own,
301
- `scanruler_tools` — see *Small local models*.
302
-
303
- - **Places on the scan** are `{ "point": [x, y, z] }` in millimetres — the
304
- scan vertex nearest it is taken — or `{ "vertex": n }`. `scan_query` reads
305
- the scan's own points: the vertices in a box or about a point, with their
306
- normals, facing a way if asked; `section_get` a section's cut as
307
- polylines, on the sheet and in the world.
308
- - **Files are paths.** `scan_open`, `deviation_open_reference`,
309
- `flat_open_image` and `project_load` take `path`, a file on this computer;
310
- the server reads it and streams it to the tab. The exports and
311
- `project_save` take an optional `path` — a file, or a folder for the name
312
- ScanRuler gives it — and the server writes the file there and returns where.
313
- `scan_open` and `project_load` take a folder too, and open the one mesh,
314
- or the one `.scanruler`, in it; with none or several the answer lists what
315
- is there.
316
- A tool that replaces the session (`scan_open`, `project_load`,
317
- `session_reset`) takes `saveTo`: the session is saved as a project file
318
- there first, then replaced — one call to keep what a tab someone left
319
- holds.
320
- - **Arguments from a file.** `{ "$file": path }` anywhere in a tool's
321
- arguments — an object of that one key — is replaced by the JSON the file
322
- holds: a long list of stations a script wrote need not pass through the
323
- conversation. A relative path is taken from the folder the server was
324
- started in.
325
- - **Pictures.** `view_render` comes back as an image the agent sees — the
326
- 3D view as the tab shows it, after `view_set` has turned it if asked — and
327
- is written to a file only when the call gives `path`. `show` puts the
328
- scan, the reference, the sections, the elements or the labels away for
329
- the picture (`{ "scan": false }` shows what was built on the scan on its
330
- own; a workspace adds keys of its own, which `view_set` lists), and
331
- `frame` looks at a box or a place closely. `flat_render` is the 2D sheet's:
332
- drawn as the sheet lies, with a grid ruled in millimetres and its values
333
- at the margins, and a `map` from picture pixels to sheet places, so a
334
- place read off the picture goes straight back as a place to click.
335
- `section_render` pictures a section — or any plane, cut for the picture
336
- alone — on its own sheet in millimetres: the scan's cut as points, what
337
- the workspaces draw over it (a model's cut, a sketch's pieces with their
338
- ids), and every point further than the tolerance from the model in red,
339
- counted in the answer with the box round them.
340
- - **The 2D sheet** takes places as `[x, y]` in its own millimetres (pixels
341
- before any scale), origin at the image's bottom-left corner, y up. An edge
342
- is named by its chain number — `flat_edges` lists them, `flat_render`
343
- writes them beside the edges, `flat_fit` takes them as `chains` — and a
344
- pick answers where it snapped. `flat_objects` finds the separate parts on
345
- an image and measures them; `flat_profile` reads an outline round a centre
346
- or along an axis and counts its peaks — a knurl's teeth, say.
347
- - **Batches stand or fall whole.** `scanruler_batch` stops at the first
348
- refused call — the answer names `failed: { index, tool, error }`. When
349
- that call is a change, `rollback` (the default) undoes what the calls
350
- before it changed, once the tab is idle: `undone` lists the steps taken
351
- back, newest first (`history_checkpoint` and `history_restore` do the same
352
- by hand across several calls). A refused read — a `section_get` of a
353
- section that is not there — undoes nothing, and `kept` says what came
354
- before it stands. A call refused `busy` (the tab working out something of
355
- its own) is run again once, when the tab is idle, and says
356
- `retried: "busy"`. `timings:
357
- true` gives each call its `ms`, the batch its `total_ms`, and a change's
358
- build the steps it played with their milliseconds (`result.steps.played`;
359
- otherwise only how many it played and took up from a state held).
360
- - **Long changes.** Every tool that changes the session takes `wait`, and
361
- so does a read the tab marks long (a comparison over whole bodies, say);
362
- `wait: false` answers at once with `{ queued: true, job }` while the call
363
- runs on, a batch too, and `scanruler_wait { for: "job", job }` gives its
364
- answer when it is done. Still running at the answer limit
365
- (`--answer-within`, 45 s), a call answers the same way with `after`, its
366
- seconds; `scanruler_wait` waits no longer than that either and answers
367
- `state: "running"` — call it again. The server keeps every change as a
368
- job, so a call the client stopped waiting for keeps running and keeps its
369
- answer: `scanruler_status` lists the latest jobs, running, done or failed.
370
- The tab keeps a record of the commands run under each job too
371
- (`session_recent`, the last hundred), so a server started afresh — whose
372
- own jobs are gone — still answers `scanruler_wait` for a job of the copy
373
- before it, from that record (`from` says so).
374
- `scanruler_wait { for: "idle" }` waits for the changes still running too,
375
- and answers `finished`: every change done or failed since it last said —
376
- a failed one with its error, a batch with the call it stopped at — each
377
- once, so a run of `wait: false` changes is read back in one call, and
378
- `state`: `idle`, or `running` when the limit came first. A
379
- change called on its own takes `timings: true` as well: its answer adds
380
- the call's `ms` and the steps its build played. The tab answers what only
381
- reads — `session_state` — while a change runs.
382
- - **A hidden tab.** A tab in the background, a covered or minimised window
383
- or a locked screen is hidden to the browser, which then draws no frames
384
- and slows timers to once a minute. Changes still run, and the waits a
385
- command makes — for the session to settle, for the screen to follow —
386
- look again on every change the session's stores see and draw their own
387
- frames, so pictures still come. `scanruler_status` says `page.hidden`,
388
- with a note: a browser that unloads background tabs (Chrome's Memory
389
- Saver) may close it — keep the site among those always kept active.
390
- - **Results** are the session as the tab holds it afterwards, as JSON, in
391
- millimetres and degrees, every number that is not whole kept to six
392
- significant digits — except in an answer whose numbers are handed back
393
- as inputs (a plane's frame, say), which keeps every digit. A refusal comes back as an error with a code —
394
- `no_scan`, `busy`, `invalid_input`, `invalid_state` (something open in a
395
- panel is in the way), `not_found`, `failed`, `unavailable`.
396
-
397
- A session might go: `scanruler_status`, `scanruler_open` if no tab is
398
- connected, `scan_open`, `session_state` (the scan's bounding box says where
399
- the part lies), `element_fit` twice, `dimension_add`, `report_get`,
400
- `view_render`.
401
-
402
- ## How it works
403
-
404
- The tab connects to `ws://127.0.0.1:<port>/`. Before that, a tab from the web
405
- fetches `http://127.0.0.1:<port>/scanruler-mcp`, which is what has Chrome and
406
- Edge ask for the permission a WebSocket alone never asks for. The server lets
407
- in a tab from scanruler.com, from this computer, or from an `--allow-origin`
408
- address, that presents the pairing token — one tab at a time — and, beside
409
- it, other copies of itself that present the token, whose requests it passes
410
- to the tab (`src/relay.js`). Messages are
411
- JSON; a file travels as a binary frame right after the JSON that announces it,
412
- never as base64 (`src/framing.js`, and `src/commands/framing.ts` in the app).
413
- Everything the server logs goes to stderr; stdout is the MCP channel.
414
-
415
- ## Development
416
-
417
- ```bash
418
- npm ci
419
- npm test # node --test: framing, tool generation, the link, an MCP client end to end
420
- ```
421
-
422
- The app's browser check `node scripts/e2e-agent.mjs` (from the repository
423
- root, against a running dev server) drives the whole path with the SDK's own
424
- MCP client. `src/commands.json` is the app's command list as of this version,
425
- for listing the tools before a tab connects; the app's test
426
- `tests/mcpCommands.test.ts` keeps it current
427
- (`UPDATE_MCP_COMMANDS=1 npx vitest run tests/mcpCommands.test.ts`).
428
-
429
- ## Licence
430
-
431
- AGPL-3.0-only, as ScanRuler — see [LICENSE](LICENSE).
1
+ # scanruler-mcp
2
+
3
+ Drive [ScanRuler](https://scanruler.com/), the browser tool for measuring 3D
4
+ scans, from an AI agent. This is a [Model Context Protocol](https://modelcontextprotocol.io)
5
+ server: the agent starts it and calls its tools; the ScanRuler tab open in
6
+ your browser connects to it over `127.0.0.1` and runs each call there, in
7
+ front of you. Every step the agent takes is one step of the tab's undo
8
+ history, so you can take any of it back.
9
+
10
+ Nothing goes anywhere but your own computer: the scan travels from your disk
11
+ to your browser through this server, as it does when you open it yourself.
12
+ What the agent reads — the figures, the report — goes to the agent's model
13
+ provider, like everything else it reads. The server's one request of its own
14
+ asks npm, once a day, for its latest version (see *Staying current*).
15
+
16
+ ## Setting up
17
+
18
+ You need [Node.js](https://nodejs.org/) 22 or newer, and Chrome, Edge or
19
+ Firefox.
20
+
21
+ ### 1. Add the server to your agent
22
+
23
+ **Claude Code**
24
+
25
+ ```bash
26
+ claude mcp add --scope user scanruler -- npx -y scanruler-mcp@latest
27
+ ```
28
+
29
+ `--scope user` has it in every project; leave it out for this one only.
30
+ Exports the agent makes land in the folder Claude Code was started in,
31
+ unless the agent names a path.
32
+
33
+ **Codex** — the CLI, the IDE extension and the app share one setting:
34
+
35
+ ```bash
36
+ codex mcp add scanruler -- npx -y scanruler-mcp@latest
37
+ ```
38
+
39
+ or, in `~/.codex/config.toml`, with room for the first start (npx fetches the
40
+ package) and for long calls — Codex gives a server 10 seconds to start and a
41
+ call 60 unless told otherwise:
42
+
43
+ ```toml
44
+ [mcp_servers.scanruler]
45
+ command = "npx"
46
+ args = ["-y", "scanruler-mcp@latest"]
47
+ startup_timeout_sec = 60
48
+ tool_timeout_sec = 300
49
+ ```
50
+
51
+ **Claude Desktop** — Settings → Developer → Edit Config, then in
52
+ `claude_desktop_config.json`:
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "scanruler": {
58
+ "command": "npx",
59
+ "args": ["-y", "scanruler-mcp@latest", "--out", "/Users/you/Documents/ScanRuler"]
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ Claude Desktop starts servers in a folder of its own, so give `--out` a folder
66
+ for the exports, and name files to the agent by their full path. Quit Claude
67
+ Desktop after saving — File → Exit; closing the window leaves it running —
68
+ and start it again. It starts the server twice, for its chats and for its
69
+ Cowork and Code sessions; the copy that starts first holds the port and the
70
+ tab connects to it, and the other sends its calls through that one — see
71
+ *Several agents* below.
72
+
73
+ **Cursor** — `.cursor/mcp.json` in a project, or `~/.cursor/mcp.json` for all:
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "scanruler": {
79
+ "command": "npx",
80
+ "args": ["-y", "scanruler-mcp@latest"]
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ **From a clone of the ScanRuler repository**, to try a change: run
87
+ `npm --prefix mcp ci` once, then use
88
+ `node /path/to/scanruler/mcp/bin/scanruler-mcp.js` where the examples say
89
+ `npx -y scanruler-mcp@latest` — in Claude Code,
90
+ `claude mcp add scanruler -- node /path/to/scanruler/mcp/bin/scanruler-mcp.js`.
91
+
92
+ ### 2. Let the agent open ScanRuler
93
+
94
+ Ask the agent for something in ScanRuler — or just *"Connect to ScanRuler"*.
95
+ With no tab connected, it calls `scanruler_open`, which opens ScanRuler in
96
+ your default browser with the pairing in its address: the tab switches the
97
+ connection on with this server's port and token, and connects. The first
98
+ time, **Chrome and Edge** ask whether scanruler.com may access "apps and
99
+ services on this device" — allow it, that is this server. **Firefox** asks
100
+ the same. The tab keeps the pairing, so this is done once per browser.
101
+
102
+ When the default browser is not the one you use ScanRuler in, or the agent
103
+ runs somewhere it cannot open one, pair by hand:
104
+
105
+ ```bash
106
+ npx -y scanruler-mcp@latest --pair
107
+ ```
108
+
109
+ prints the port, the pairing token and a link. Open the link once in the
110
+ browser you use ScanRuler in, or in ScanRuler go to ⚙ **Settings → Agents**,
111
+ switch on **Local agent connection**, and enter the port and the token. The
112
+ token is made on the first run and kept in your config folder
113
+ (`%APPDATA%\scanruler-mcp` on Windows,
114
+ `~/Library/Application Support/scanruler-mcp` on macOS,
115
+ `$XDG_CONFIG_HOME/scanruler-mcp` elsewhere, `~/.config/scanruler-mcp`
116
+ without it). `scanruler_status` hands out the same
117
+ link.
118
+
119
+ A chip in ScanRuler's top bar says where the connection stands: **listening**
120
+ while the agent's server is not running, **connected**, or **turned away**
121
+ (a wrong token, or another tab connected already — the server takes one tab
122
+ at a time). If you denied the browser's question, allow *Local network
123
+ access* for the site in the site settings (the icon left of the address) and
124
+ reload. **Safari** does not let a page from the web reach the computer it
125
+ runs on at all; use `--serve` below.
126
+
127
+ ### Staying current
128
+
129
+ With `@latest` in the setting, npx asks npm for the newest version at every
130
+ start and fetches it when there is one, so quitting the app and opening it
131
+ again updates the server. Offline, npx starts the copy it has; on a network
132
+ that turns connections away rather than having none, it tries for about a
133
+ minute first. (npm 9.7 and newer treat a bare `scanruler-mcp` the same way.)
134
+
135
+ The server also keeps itself current — for a setting without `@latest` on an
136
+ older npm, or a copy installed with `npm install -g`. Once a day, in the
137
+ background, it asks npm's registry (registry.npmjs.org, or the one npm is set
138
+ to) for its latest version: that one request, given 2 seconds. When there is
139
+ a newer one, npm fetches it into its cache, and the next start runs it in
140
+ place of the old copy, without waiting on the network. What it found is kept
141
+ in `update.json` in the config folder, and `scanruler_status` says it:
142
+ `server.version`, `server.latest`, and `server.handedOffFrom` when an older
143
+ copy started this one. A version pinned in the setting
144
+ (`scanruler-mcp@0.4.0`) is left as it is, and so is a copy run from a clone
145
+ of the repository. To turn it off: `--no-update`, or
146
+ `SCANRULER_MCP_NO_UPDATE=1`.
147
+
148
+ When ScanRuler shows **Your ScanRuler connector is out of date**, the server
149
+ your app starts is older than the page needs: change the setting to the
150
+ `@latest` line it shows, then quit the app completely — not just its window —
151
+ and open it again.
152
+
153
+ ### Several agents
154
+
155
+ Any number of copies of the server can share the tab. The first to start
156
+ holds the port and the tab's connection; one that finds the port taken
157
+ connects to that one, with the same token, and sends its calls through it.
158
+ When the copy holding the port stops, another takes the port over, and the
159
+ tab — which keeps trying — connects to it within ten seconds.
160
+ `scanruler_status` says which this copy is (`server.role`: `holder`,
161
+ `relay` — with the holding copy's version as `server.holder.version` —, or
162
+ `none` with `server.error` saying why). The page judges the copy holding
163
+ its connection: when that one is out of date, a relaying copy's status says
164
+ so, and quitting the app that started it lets a current copy take over. Copies with different
165
+ tokens cannot share a port: give them the same `SCANRULER_MCP_TOKEN`, or
166
+ each its own `--port`. Changes from several agents run one after another in
167
+ the tab, each one step of the same undo history.
168
+
169
+ ### Serving the app yourself
170
+
171
+ ```bash
172
+ npx -y scanruler-mcp@latest --serve /path/to/scanruler/dist
173
+ ```
174
+
175
+ serves a built ScanRuler (`npm run build` in a clone) at
176
+ `http://127.0.0.1:7317/` from the same port the tab connects to — for Safari,
177
+ for working offline, and for the open-source build. A page from there needs
178
+ no browser permission, and `--pair` then gives a link to it.
179
+
180
+ ### Without a browser (headless)
181
+
182
+ ```bash
183
+ npm run build:engine # in a clone of ScanRuler
184
+ npx -y scanruler-mcp@latest --engine /path/to/scanruler/dist-engine/engine.mjs
185
+ ```
186
+
187
+ runs ScanRuler itself inside the server, with no browser and no tab: the
188
+ engine is a build of the app for Node, `dist-engine/engine.mjs`, which the
189
+ server loads from where it is and names nothing in. `--headless` takes the
190
+ engine built in the checkout the server is part of; `SCANRULER_ENGINE` gives
191
+ a path as `--engine` does. There is no scan, no viewport and nobody
192
+ watching, so only the commands that run without a page are tools —
193
+ `session_state` and `history_undo`/`history_redo` in the app itself, and
194
+ whatever a build of ScanRuler marks to run so; a tool that needs the page
195
+ (`view_render`, `scan_open` …) answers `unavailable`, naming the page mode.
196
+ Files still go by `path`. The server's instructions say it runs headless,
197
+ `scanruler_status` says `backend: "headless"` and where the engine is, and
198
+ `scanruler_open` has nothing to open. Each server is a session of its own —
199
+ an agent's subagents can each start one and work side by side, sharing
200
+ nothing — and what the app logs goes to stderr with the server's own lines.
201
+ `--tools compact` works headless too: the server's own tools are listed, and
202
+ the engine's are read through `scanruler_tools` and run through
203
+ `scanruler_batch` (see *Small local models*).
204
+ ### Small local models
205
+
206
+ Listed in full, every command the tab offers is a tool of its own: some sixty
207
+ tools and 25k tokens of schema in the open-source build, over a hundred and
208
+ 50k tokens at scanruler.com, all read before the conversation starts. A
209
+ frontier model takes that in its stride. A local model with a 128k window
210
+ runs out of room within a few tool rounds, and picks less reliably among so
211
+ many. For such a model, start the server with `--tools compact` (or set
212
+ `SCANRULER_MCP_TOOLS=compact`). It then lists about a dozen tools, a few
213
+ thousand tokens: the server's six, `scanruler_tools`, and the tab's tools
214
+ that return a picture — `view_render`, `section_render`, `flat_render` and
215
+ `scan_heightmap`.
216
+
217
+ - `scanruler_tools` with no arguments lists the tab's tools, a name and a
218
+ line each, about 1.5k tokens for a hundred. With `names` it gives those
219
+ tools' full definitions, as the full list has them, up to 20 at a time.
220
+ Names it does not know come back in `unknown`.
221
+ - The model runs the tools through `scanruler_batch`, one call as a batch of
222
+ one. A refused call comes back with the tab's error code and what was
223
+ wrong, so the model can try again with corrected arguments. A client that
224
+ lets the model call a tool it was not given can call them directly too.
225
+ - The tools that return a picture are listed so the model calls them
226
+ directly and sees the picture. In a batch a picture is not shown: it is
227
+ written to a file, the call's `path` or a new file in the output folder
228
+ (`--out`), and the call's result names it. A tab that connects with
229
+ another picture tool has the client list the tools again.
230
+
231
+ **LM Studio**: add `--tools` and `compact` to the server's arguments as two
232
+ separate entries. In `mcp.json`:
233
+
234
+ ```json
235
+ {
236
+ "mcpServers": {
237
+ "scanruler": {
238
+ "command": "npx",
239
+ "args": ["-y", "scanruler-mcp@latest", "--tools", "compact"]
240
+ }
241
+ }
242
+ }
243
+ ```
244
+
245
+ **Codex** with a local model:
246
+
247
+ ```bash
248
+ codex mcp add scanruler -- npx -y scanruler-mcp@latest --tools compact
249
+ ```
250
+
251
+ or `args = ["-y", "scanruler-mcp@latest", "--tools", "compact"]` in
252
+ `~/.codex/config.toml`.
253
+
254
+ ## Options
255
+
256
+ | Option | |
257
+ |---|---|
258
+ | `--port <n>` | The port the tab connects to; 7317 unless given. Also `SCANRULER_MCP_PORT`. |
259
+ | `--out <dir>` | Where exports go when a tool call names no path; the folder the server was started in otherwise. |
260
+ | `--serve <dir>` | Also serve a built ScanRuler from `<dir>`. |
261
+ | `--allow-origin <url>` | Let a ScanRuler tab from another address connect — a self-hosted copy. Repeatable. scanruler.com and any page on this computer are always allowed. |
262
+ | `--app <url>` | The ScanRuler address the pairing link opens. |
263
+ | `--engine <file>` | Run ScanRuler headless in the server, from an engine built with `npm run build:engine` — no tab. Also `SCANRULER_ENGINE`. |
264
+ | `--headless` | The same, with the engine built in the checkout the server is part of. |
265
+ | `--tools <mode>` | `full`, the default: every command the tab offers is a tool. `compact`: the server's own and the tab's tools that return a picture; the rest are read through `scanruler_tools` and run through `scanruler_batch` — see *Small local models*. Also `SCANRULER_MCP_TOOLS`. |
266
+ | `--trace <dir>` | Write a line per tool call — tool, ms, bytes each way, error, whether the tab was hidden — into a new file in `<dir>`, for measuring a run; off unless given. `scripts/trace-summary.mjs` in the ScanRuler repository sums a run up. Also `SCANRULER_MCP_TRACE`. |
267
+ | `--trace-args` | With `--trace`, each call's arguments too; otherwise only their size. |
268
+ | `--answer-within <s>` | Hold a call at most this many seconds, 45 unless given: a change, a batch or a long read still running then answers `{ queued: true, job, after }` and runs on, and `scanruler_wait` waits no longer either. A client, or a bridge between it and this computer, may cut a call at a minute; a cut call says nothing. `0` holds every call to its end. Also `SCANRULER_MCP_ANSWER_WITHIN`. |
269
+ | `--no-update` | Do not keep the server current (see *Staying current*). Also `SCANRULER_MCP_NO_UPDATE=1`. |
270
+ | `--pair` | Print the port, the token and the pairing link, and exit. |
271
+ | `--version` | Print the server's version, and exit. |
272
+ | `--help` | The options. |
273
+
274
+ `SCANRULER_MCP_TOKEN` overrides the stored token; `SCANRULER_MCP_CONFIG_DIR`
275
+ moves the config folder.
276
+
277
+ ## Tools
278
+
279
+ Every command the ScanRuler tab offers is a tool, its name with dots turned
280
+ into underscores — `scan_open`, `element_fit`, `dimension_add`,
281
+ `report_get`, `history_undo` and the rest; their descriptions say what each
282
+ needs and returns. The list is the tab's while one is connected — a build
283
+ with more commands lists more. Before a tab is there it is the list of the
284
+ last tab that connected, which the server keeps in its config folder
285
+ (`tools.json`), or, the first time, the app's own; a client that lists the
286
+ tools once, when it starts, sees them all that way, and one that listens is
287
+ told when the list changes. Six more are the server's: `scanruler_status`
288
+ (is a tab connected, which version, how to pair one, and what the tab's
289
+ session holds — its scan, project, unsaved changes and work counted — and,
290
+ when the client's list is out of date, which tools were added, removed or
291
+ changed since and which page it came from),
292
+ `scanruler_open` (open ScanRuler in the default browser, paired, and wait
293
+ for it), `scanruler_wait` (until a tab is connected, or also idle),
294
+ `scanruler_batch` (several of the tab's tools in one call, in order — a run
295
+ of `scan_nearest` or `element_fit` calls — each answer in its place, stopped
296
+ at the first refused unless told to go on), `scanruler_write` (a text
297
+ file — `.md`, `.txt`, `.csv` or `.json`, a report say — written in the folder
298
+ the server last opened a scan or a project from, and nowhere else) and
299
+ `scanruler_files` (that folder's files — name, size, kind — newest first).
300
+ The folders are kept beside `tools.json` (`folders.json`), so a server
301
+ started afresh still writes and lists there, and `scanruler_status` lists
302
+ them. With `--tools compact` only the tab's tools that return a picture are
303
+ listed; the rest are read through a seventh of the server's own,
304
+ `scanruler_tools` — see *Small local models*.
305
+
306
+ - **Places on the scan** are `{ "point": [x, y, z] }` in millimetres — the
307
+ scan vertex nearest it is taken — or `{ "vertex": n }`. `scan_query` reads
308
+ the scan's own points: the vertices in a box or about a point, with their
309
+ normals, facing a way if asked; `section_get` a section's cut as
310
+ polylines, on the sheet and in the world.
311
+ - **Files are paths.** `scan_open`, `deviation_open_reference`,
312
+ `flat_open_image` and `project_load` take `path`, a file on this computer;
313
+ the server reads it and streams it to the tab. The exports and
314
+ `project_save` take an optional `path` — a file, or a folder for the name
315
+ ScanRuler gives it — and the server writes the file there and returns where.
316
+ `scan_open` and `project_load` take a folder too, and open the one mesh,
317
+ or the one `.scanruler`, in it; with none or several the answer lists what
318
+ is there.
319
+ A tool that replaces the session (`scan_open`, `project_load`,
320
+ `session_reset`) takes `saveTo`: the session is saved as a project file
321
+ there first, then replaced — one call to keep what a tab someone left
322
+ holds.
323
+ - **Arguments from a file.** `{ "$file": path }` anywhere in a tool's
324
+ arguments — an object of that one key — is replaced by the JSON the file
325
+ holds: a long list of stations a script wrote need not pass through the
326
+ conversation. A relative path is taken from the folder the server was
327
+ started in.
328
+ - **Pictures.** `view_render` comes back as an image the agent sees — the
329
+ 3D view as the tab shows it, after `view_set` has turned it if asked — and
330
+ is written to a file only when the call gives `path`. `show` puts the
331
+ scan, the reference, the sections, the elements or the labels away for
332
+ the picture (`{ "scan": false }` shows what was built on the scan on its
333
+ own; a workspace adds keys of its own, which `view_set` lists), and
334
+ `frame` looks at a box or a place closely. `flat_render` is the 2D sheet's:
335
+ drawn as the sheet lies, with a grid ruled in millimetres and its values
336
+ at the margins, and a `map` from picture pixels to sheet places, so a
337
+ place read off the picture goes straight back as a place to click.
338
+ `section_render` pictures a section — or any plane, cut for the picture
339
+ alone — on its own sheet in millimetres: the scan's cut as points, what
340
+ the workspaces draw over it (a model's cut, a sketch's pieces with their
341
+ ids), and every point further than the tolerance from the model in red,
342
+ counted in the answer with the box round them.
343
+ - **The 2D sheet** takes places as `[x, y]` in its own millimetres (pixels
344
+ before any scale), origin at the image's bottom-left corner, y up. An edge
345
+ is named by its chain number — `flat_edges` lists them, `flat_render`
346
+ writes them beside the edges, `flat_fit` takes them as `chains` — and a
347
+ pick answers where it snapped. `flat_objects` finds the separate parts on
348
+ an image and measures them; `flat_profile` reads an outline round a centre
349
+ or along an axis and counts its peaks — a knurl's teeth, say.
350
+ - **Batches stand or fall whole.** `scanruler_batch` stops at the first
351
+ refused call — the answer names `failed: { index, tool, error }`. When
352
+ that call is a change, `rollback` (the default) undoes what the calls
353
+ before it changed, once the tab is idle: `undone` lists the steps taken
354
+ back, newest first (`history_checkpoint` and `history_restore` do the same
355
+ by hand across several calls). A refused read — a `section_get` of a
356
+ section that is not there — undoes nothing, and `kept` says what came
357
+ before it stands. A call refused `busy` (the tab working out something of
358
+ its own) is run again once, when the tab is idle, and says
359
+ `retried: "busy"`. `timings:
360
+ true` gives each call its `ms`, the batch its `total_ms`, and a change's
361
+ build the steps it played with their milliseconds (`result.steps.played`;
362
+ otherwise only how many it played and took up from a state held).
363
+ - **Long changes.** Every tool that changes the session takes `wait`, and
364
+ so does a read the tab marks long (a comparison over whole bodies, say);
365
+ `wait: false` answers at once with `{ queued: true, job }` while the call
366
+ runs on, a batch too, and `scanruler_wait { for: "job", job }` gives its
367
+ answer when it is done. Still running at the answer limit
368
+ (`--answer-within`, 45 s), a call answers the same way with `after`, its
369
+ seconds; `scanruler_wait` waits no longer than that either and answers
370
+ `state: "running"` — call it again. The server keeps every change as a
371
+ job, so a call the client stopped waiting for keeps running and keeps its
372
+ answer: `scanruler_status` lists the latest jobs, running, done or failed.
373
+ The tab keeps a record of the commands run under each job too
374
+ (`session_recent`, the last hundred), so a server started afresh — whose
375
+ own jobs are gone — still answers `scanruler_wait` for a job of the copy
376
+ before it, from that record (`from` says so).
377
+ `scanruler_wait { for: "idle" }` waits for the changes still running too,
378
+ and answers `finished`: every change done or failed since it last said —
379
+ a failed one with its error, a batch with the call it stopped at — each
380
+ once, so a run of `wait: false` changes is read back in one call, and
381
+ `state`: `idle`, or `running` when the limit came first. A
382
+ change called on its own takes `timings: true` as well: its answer adds
383
+ the call's `ms` and the steps its build played. The tab answers what only
384
+ reads — `session_state` — while a change runs.
385
+ - **A hidden tab.** A tab in the background, a covered or minimised window
386
+ or a locked screen is hidden to the browser, which then draws no frames
387
+ and slows timers to once a minute. Changes still run, and the waits a
388
+ command makes — for the session to settle, for the screen to follow —
389
+ look again on every change the session's stores see and draw their own
390
+ frames, so pictures still come. `scanruler_status` says `page.hidden`,
391
+ with a note: a browser that unloads background tabs (Chrome's Memory
392
+ Saver) may close it — keep the site among those always kept active.
393
+ - **Results** are the session as the tab holds it afterwards, as JSON, in
394
+ millimetres and degrees, every number that is not whole kept to six
395
+ significant digits — except in an answer whose numbers are handed back
396
+ as inputs (a plane's frame, say), which keeps every digit. A refusal comes back as an error with a code —
397
+ `no_scan`, `busy`, `invalid_input`, `invalid_state` (something open in a
398
+ panel is in the way), `not_found`, `failed`, `unavailable`.
399
+
400
+ A session might go: `scanruler_status`, `scanruler_open` if no tab is
401
+ connected, `scan_open`, `session_state` (the scan's bounding box says where
402
+ the part lies), `element_fit` twice, `dimension_add`, `report_get`,
403
+ `view_render`.
404
+
405
+ ## How it works
406
+
407
+ The tab connects to `ws://127.0.0.1:<port>/`. Before that, a tab from the web
408
+ fetches `http://127.0.0.1:<port>/scanruler-mcp`, which is what has Chrome and
409
+ Edge ask for the permission a WebSocket alone never asks for. The server lets
410
+ in a tab from scanruler.com, from this computer, or from an `--allow-origin`
411
+ address, that presents the pairing token — one tab at a time — and, beside
412
+ it, other copies of itself that present the token, whose requests it passes
413
+ to the tab (`src/relay.js`). Messages are
414
+ JSON; a file travels as a binary frame right after the JSON that announces it,
415
+ never as base64 (`src/framing.js`, and `src/commands/framing.ts` in the app).
416
+ Everything the server logs goes to stderr; stdout is the MCP channel.
417
+
418
+ ## Development
419
+
420
+ ```bash
421
+ npm ci
422
+ npm test # node --test: framing, tool generation, the link, an MCP client end to end
423
+ ```
424
+
425
+ The app's browser check `node scripts/e2e-agent.mjs` (from the repository
426
+ root, against a running dev server) drives the whole path with the SDK's own
427
+ MCP client. `src/commands.json` is the app's command list as of this version,
428
+ for listing the tools before a tab connects; the app's test
429
+ `tests/mcpCommands.test.ts` keeps it current
430
+ (`UPDATE_MCP_COMMANDS=1 npx vitest run tests/mcpCommands.test.ts`).
431
+
432
+ ## Licence
433
+
434
+ AGPL-3.0-only, as ScanRuler — see [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scanruler-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Drive ScanRuler, the 3D-scan measuring tool, from an AI agent: a local MCP server that reaches the ScanRuler tab open in your browser.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -11,11 +11,22 @@
11
11
  "url": "git+https://github.com/CNCKitchen/scanruler.git",
12
12
  "directory": "mcp"
13
13
  },
14
- "keywords": ["mcp", "model-context-protocol", "3d-scan", "metrology", "scanruler"],
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "3d-scan",
18
+ "metrology",
19
+ "scanruler"
20
+ ],
15
21
  "bin": {
16
22
  "scanruler-mcp": "bin/scanruler-mcp.js"
17
23
  },
18
- "files": ["bin", "src", "README.md", "LICENSE"],
24
+ "files": [
25
+ "bin",
26
+ "src",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
19
30
  "engines": {
20
31
  "node": ">=22"
21
32
  },
package/src/commands.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "app": "0.6.3",
3
- "saved": "2026-10-23T08:23:00.150Z",
2
+ "app": "0.7.0",
3
+ "saved": "2026-10-23T09:03:55.406Z",
4
4
  "commands": [
5
5
  {
6
6
  "name": "align.auto",
package/src/link.js CHANGED
@@ -235,7 +235,7 @@ export class PageLink extends Asker {
235
235
  if (message.peer && typeof message.peer === 'object') {
236
236
  if (message.peer.token !== this.token) return refuse(CLOSE.badToken, 'Its pairing token is not this one’s.')
237
237
  this.peers.add(ws)
238
- ws.send(JSON.stringify({ welcome: { server: 'scanruler-mcp', page: this.page } }))
238
+ ws.send(JSON.stringify({ welcome: { server: 'scanruler-mcp', page: this.page, ...(this.version ? { version: this.version } : {}) } }))
239
239
  this.log(`Another scanruler-mcp${message.peer.version ? ` ${message.peer.version}` : ''} sends its calls through this one.`)
240
240
  return true
241
241
  }
package/src/main.js CHANGED
@@ -192,7 +192,7 @@ export async function main(argv = process.argv.slice(2)) {
192
192
  cwd,
193
193
  outDir,
194
194
  connectHelp: () => (link.error ? `${link.error} ` : '') + connectHelp(),
195
- serverInfo: () => ({ name: 'scanruler-mcp', version: VERSION, ...updates.info(), port, role: link.role, tools, outDir, config: config.file, ...(trace ? { trace: trace.file } : {}), ...(link.error ? { error: link.error } : {}) }),
195
+ serverInfo: () => ({ name: 'scanruler-mcp', version: VERSION, ...updates.info(), port, role: link.role, ...(link.role === 'relay' ? { holder: { version: link.holderVersion ?? null } } : {}), tools, outDir, config: config.file, ...(trace ? { trace: trace.file } : {}), ...(link.error ? { error: link.error } : {}) }),
196
196
  remembered: { load: () => loadRemembered(), save: (commands) => saveRemembered(commands) },
197
197
  folderStore: { load: () => loadFolders(), save: (folders) => saveFolders(folders) },
198
198
  openPage: () => openInBrowser(pairing),
package/src/mcp.js CHANGED
@@ -444,11 +444,26 @@ export function createMcpServer({
444
444
  unsaved: project.unsaved ?? null,
445
445
  ...(project.holds ?? {}),
446
446
  },
447
- // The page's word that this server is older than its commands assume.
448
- ...(typeof state.serverTooOld === 'string' ? { serverTooOld: state.serverTooOld } : {}),
447
+ // The page's word that the server it is connected to is older than its
448
+ // commands assume — while relaying, that is the holding copy.
449
+ ...(typeof state.serverTooOld === 'string' ? { serverTooOld: tooOldHere(state.serverTooOld) } : {}),
449
450
  }
450
451
  }
451
452
 
453
+ /** The page judges the copy it is connected to. A relaying copy is not
454
+ * that one: say which is out of date, and that quitting its app is the
455
+ * fix — updating this one changes nothing. */
456
+ const tooOldHere = (said) => {
457
+ const info = serverInfo()
458
+ if (info.role !== 'relay') return said
459
+ const holder = info.holder?.version ? `version ${info.holder.version}` : 'from before 0.4.1'
460
+ return (
461
+ `This server (${info.version}) is current; it sends its calls through another copy of scanruler-mcp, ${holder}, which holds the tab’s connection and is the one out of date. ` +
462
+ 'Another AI app or chat started it — quit that app completely (Claude Desktop, Claude Code, Codex or another), and this copy takes the connection over within about ten seconds; or update that app’s setting as the page says: ' +
463
+ said
464
+ )
465
+ }
466
+
452
467
  // ---- answers that fit -----------------------------------------------------
453
468
  // An answer past answerCap — a read with every point in it, a batch of
454
469
  // many — would crowd out the agent's context: it is written whole to a
@@ -543,7 +558,7 @@ export function createMcpServer({
543
558
  {
544
559
  name: 'scanruler_status',
545
560
  title: 'ScanRuler connection status',
546
- description: 'Whether a ScanRuler page is connected to this agent, which version and plugins it has, whether its tab is hidden (page.hidden), and — when none is — how the user connects one. session, with a page, says what it holds: the scan, the project’s name, whether it has changes not saved (unsaved), and its work counted — elements, features, notes … — so a session someone else left is seen before it is replaced (scan_open with saveTo keeps it). backend says whether ScanRuler runs in a tab (page) or in this server (headless). jobs lists the calls that change the session still running and the latest five others, newest first, each running, done or failed — a call whose answer you did not get is among them; scanruler_wait { for: "job", job } gives its answer, also for a job of an earlier copy of this server, which the page remembers; jobsMore counts the older ones kept. folders: where a scan or a project was opened from or saved into, the latest first — what scanruler_write writes into and scanruler_files lists; kept when the server restarts. server.answerWithin: the seconds a call is held at most before it answers with its job. server.version is this server’s, server.latest the newest on npm when known, server.handedOffFrom the older copy that started this one in its place. serverTooOld, when there, says this server is older than the page needs: tell the person once, in its words, and install nothing yourself. tools, when there, says the tools you were given are not the ones the page offers now (stale): which were added, removed or changed — named the first time, counted after — and which page they were listed from.',
561
+ description: 'Whether a ScanRuler page is connected to this agent, which version and plugins it has, whether its tab is hidden (page.hidden), and — when none is — how the user connects one. session, with a page, says what it holds: the scan, the project’s name, whether it has changes not saved (unsaved), and its work counted — elements, features, notes … — so a session someone else left is seen before it is replaced (scan_open with saveTo keeps it). backend says whether ScanRuler runs in a tab (page) or in this server (headless). jobs lists the calls that change the session still running and the latest five others, newest first, each running, done or failed — a call whose answer you did not get is among them; scanruler_wait { for: "job", job } gives its answer, also for a job of an earlier copy of this server, which the page remembers; jobsMore counts the older ones kept. folders: where a scan or a project was opened from or saved into, the latest first — what scanruler_write writes into and scanruler_files lists; kept when the server restarts. server.answerWithin: the seconds a call is held at most before it answers with its job. server.version is this server’s, server.latest the newest on npm when known, server.handedOffFrom the older copy that started this one in its place. server.role: holder — this copy holds the port and the tab’s connection; relay — another copy (another chat or AI app) holds them and this one sends its calls through it, the work still running in the tab, server.holder.version saying which (null: from before 0.4.1); none — neither yet, server.error saying why. serverTooOld, when there, says the server the page is connected to is older than the page needs — while relaying, the holding copy, not this one: tell the person once, in its words, and install nothing yourself. tools, when there, says the tools you were given are not the ones the page offers now (stale): which were added, removed or changed — named the first time, counted after — and which page they were listed from.',
547
562
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
548
563
  annotations: { readOnlyHint: true, openWorldHint: false },
549
564
  call: async () => text({ ...status(), ...(await sessionBrief()) }),
package/src/relay.js CHANGED
@@ -24,6 +24,9 @@ export class PeerLink extends Asker {
24
24
  this.token = token
25
25
  this.version = version
26
26
  this.log = log
27
+ /** The holding copy's version, as its welcome said; null from a copy
28
+ * before 0.4.1, whose welcome to another copy carried none. */
29
+ this.holderVersion = null
27
30
  }
28
31
 
29
32
  /** Connect to the copy on the port. Resolves `{ ok: true }` once it took
@@ -40,6 +43,7 @@ export class PeerLink extends Asker {
40
43
  if (message.welcome) {
41
44
  accepted = true
42
45
  this.socket = ws
46
+ this.holderVersion = typeof message.welcome.version === 'string' ? message.welcome.version : null
43
47
  this.#setPage(message.welcome.page ?? null)
44
48
  resolve({ ok: true })
45
49
  } else resolve({ refused: String(message.refused ?? 'It said no.') })
@@ -141,6 +145,12 @@ export class SharedLink extends EventEmitter {
141
145
  return this.current?.page ?? null
142
146
  }
143
147
 
148
+ /** While relaying: the version of the copy holding the port, null when
149
+ * it is from before 0.4.1 and did not say. Undefined otherwise. */
150
+ get holderVersion() {
151
+ return this.role === 'relay' ? (this.peer?.holderVersion ?? null) : undefined
152
+ }
153
+
144
154
  run(name, input, binaryKey, onProgress, job, deferred = false) {
145
155
  const link = this.current
146
156
  if (!link) return Promise.reject({ code: 'unavailable', message: 'No ScanRuler page is connected.' })