scanruler-mcp 0.3.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 +434 -364
- package/bin/scanruler-mcp.js +15 -8
- package/package.json +14 -3
- package/src/commands.json +4611 -3584
- package/src/config.js +38 -6
- package/src/link.js +304 -293
- package/src/main.js +44 -11
- package/src/mcp.js +1170 -615
- package/src/relay.js +228 -218
- package/src/tools.js +192 -17
- package/src/update.js +380 -0
package/README.md
CHANGED
|
@@ -1,364 +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.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`
|
|
88
|
-
`
|
|
89
|
-
`
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
the
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
- **
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
the
|
|
333
|
-
`
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
`
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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).
|