Skip to content

fix: scanruler-mcp tool and CLI items from #34 - #52

Open
Jozkah wants to merge 9 commits into
CNCKitchen:mainfrom
Jozkah:fix/mcp-tools-review
Open

Jozkah wants to merge 9 commits into
CNCKitchen:mainfrom
Jozkah:fix/mcp-tools-review

Conversation

@Jozkah

@Jozkah Jozkah commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

The scanruler-mcp tool and CLI items from #34, rebased on 0.9.3. The scanruler_write guard is left out because you shipped it. Each fix is its own commit, with a regression test that fails without it:

  • Colons in returned file names. On Windows, a name containing : wrote to an NTFS alternate data stream, reported success, and left the visible file empty. Characters Windows refuses are now replaced, and reserved names like CON get a prefix.
  • Picture tools overwrote any file. They're annotated read-only for the client, yet a path replaced whatever was there. An existing file is now refused and left untouched. Folders still number past existing files.
  • --allow-origin was a raw string match. A trailing slash, a path or an uppercase host never matched. It now compares URL origins, and a value that isn't a URL is refused at start.
  • Precision of large coordinates. Six significant digits lost 0.1 mm or more on large absolute values. Values of 100 or more now keep four decimals; smaller ones are as before. Two pinned values in tools.test.js moved accordingly.
  • The server's own tools didn't check their arguments. timeoutSeconds could be NaN, negative or a string. Numbers, booleans, enums and ranges are now checked against each tool's schema.
  • A usage error blacklisted an update. If the handed-to copy exited at once because of the user's own arguments, that version was marked failed. Usage errors now exit 2 and pass through.
  • First-run token race. Copies started together could each write their own token, and a corrupt config.json was silently replaced. The first token is linked into place and the others read it. A Windows read during that write (EBUSY/EPERM) is retried rather than taken as corrupt. A file that stays unparsable is set aside as config.json.bad.
  • --out was concatenated, not resolved.
  • Symlinks out of the scan folder. scanruler_read and scanruler_files now also check where the path really leads, as your 0.9.3 change does for scanruler_write, which I left as it is. The .. check also no longer refuses names like ..notes.md.

Build: npm run build passes. npm --prefix mcp test (139), npm test (3094), the plugin boundary and units checks, and npm run e2e:ci all pass. The token race test passed 20/20 alone and the full MCP suite 8 times in a row.

A file the page handed back was written under the name the page gave it,
unchanged. On Windows a name with a colon, such as one taken from a scan
titled "Bracket v2: final", wrote the bytes to a hidden NTFS stream and
left an empty file, while the answer reported success.

Reserved characters and control characters in the name now become an
underscore, trailing dots and spaces are dropped, and a device name such
as CON is prefixed so it is written as a plain file.
The picture tools (view_render, section_render, flat_render,
scan_heightmap) are annotated read-only, so a client may run them without
asking, yet given the path of an existing file they replaced it with PNG
bytes.

A picture written to a file path is now created with the exclusive flag.
When a file is already there the call is refused with invalid_input and
the file is left untouched. Folders still number the new picture past
existing ones, and other returned files keep their current behaviour.
The allowed origins were compared to the browser's Origin header as raw
strings. A value given with a trailing slash, a path or an uppercase host,
such as https://x.example/, never matched, so the page was refused with
403 and only one log line to explain it.

Each allowed value is now reduced to its URL origin before comparing, and
a value that is not a URL is refused at start-up instead of silently never
matching.
Answers rounded every number to six significant digits. For a scan far
from the origin, as a scanner's frame often places it, a coordinate such
as 12345.678 came back as 12345.7 and 123456.789 as 123457, so a point the
agent fed back was off by a tenth of a millimetre or more.

Numbers of a hundred or more are now rounded to a fixed step of 1e-4
instead, so millimetre values keep four decimals at any distance. Smaller
numbers keep six significant digits as before. The existing rounding test
is updated to the new values.
The MCP SDK's low-level server does not validate tool arguments, so the
schemas of the server's own tools were advisory only. A scanruler_wait
with timeoutSeconds "abc" or -5 computed a NaN or negative delay, the
timer fired at once, and the agent polled in a tight loop believing it
had waited.

Before one of the server's own tools runs, each top-level number, integer
and boolean argument is now checked for its type, its enum and its
minimum, exclusive minimum and maximum, and a mismatch is answered with
invalid_input. Lists and objects are left to the tools, which already
accept some of them given as a string of JSON.
The launcher treated any non-zero exit within three seconds of handing off
to a newer copy as a copy that failed to start, and stored that version in
update.json as failed. A bad --port or --tools made the new copy exit with
code 1 at once, so a good version was blacklisted and automatic updates
stopped. The launcher then ran itself in its place, possibly after the
child had already read the client's initialize message, which could hang
the client.

The server now exits with a dedicated usage code, 2, when it refuses its
arguments or settings. The launcher passes that code on without marking
the version failed and without starting itself as a fallback. The CLI
tests that check a refused --tools value expect the new code.
loadConfig checked for config.json, read it and wrote a new token as
separate steps. Two servers started together on a first run could each
make a token; the last write won, and the other copy held a token that
was not on disk, so pages paired with it were refused. A config.json that
could not be read or parsed was silently replaced, dropping its other
keys and invalidating the token already pasted into ScanRuler.

The first token is now written whole to a temporary file and linked into
place. Whoever's link succeeds wins; a copy whose link fails because the
file is already there reads the winner's token and never makes its own.
Read errors and parse errors are handled separately: only a missing file
leads to a new token, while a read refused for a moment (EBUSY or EPERM
on Windows, as another copy links the file in) is retried. A file that
still does not parse after about a second of retries is kept aside as
config.json.bad rather than overwritten.
A relative --out was joined to the working folder by string concatenation
after a hand-written absolute-path check. "--out ../reports" was reported
as "cwd/../reports", unresolved and with mixed separators on Windows, in
the status and in answers.

The folder is now resolved with path.resolve against the working folder.
scanruler_read, scanruler_write and scanruler_files kept to the folders a
scan or project came from by comparing paths as written. A symbolic link
or junction in the scan folder that pointed elsewhere carried them out of
it. The three tools also used slightly different checks, and the
"starts with .." test refused a plain name such as "..notes.md".

The three tools now share one check: the path must lie in a known folder
both as written and with links followed (the real path of the deepest
existing part, for a file not yet written). Only a ".." segment counts as
climbing out, so a name that merely starts with two dots is accepted.
Tools that take an absolute path by design, such as scan_open and $file,
are unchanged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant