Public: anyone can read this; the crew can change it.

MCP `read_file` describes an STL model in words: triangles, size in model units, ASCII or binary #7362

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

MCP `read_file` now describes a `.stl` file in words, so an agent reading a 3D model doesn't have to decode the base64 itself. The answer gains `about: { kind: "stl", format, triangles, size: [x, y, z], says: "STL (binary) · 12 triangles · 40.0 × 20.0 × 10.0" }`. If the file is broken, `about` holds only `kind` and a `says` naming the reason, e.g. `"STL, unreadable: truncated: says 12 triangles, h…

shipped production is serving it deploy a4add02

opened this

MCP read_file now describes a .stl file in words, so an agent reading a 3D model doesn't have to decode the base64 itself. The answer gains about: { kind: "stl", format, triangles, size: [x, y, z], says: "STL (binary) · 12 triangles · 40.0 × 20.0 × 10.0" }. If the file is broken, about holds only kind and a says naming the reason, e.g. "STL, unreadable: truncated: says 12 triangles, holds 100 of 684 bytes". Other files get the same answer as before, and the tool's description and doc are unchanged.

  • src/text/stl.ts: readStl(bytes) returns Result<{ format, triangles, min, max }, string> and keeps only the bounds, never the mesh. A binary file must be exactly 84 + 50·n bytes. If it is shorter it is reported as truncated; if the length doesn't match it is reported as lying. Some exporters start a binary header with solid, so a file whose length fits the binary layout exactly is read as binary even then. The ASCII reader checks that each facet has exactly three vertices of plain numbers and that endsolid closes the file. Non-finite coordinates and empty solids are errors, and nothing throws. stlSize and sayStl are exported so #7335 can reuse the bounds for its camera.
  • src/http/mcp/tools/read_file.ts: an aboutFile(path, body) function with one branch per file type. The MP4 and config issues can each add their own branch next to the STL one.
  • Tests: test/text-stl.test.ts checks a binary cube, an ASCII cube and a binary header beginning with solid for counts and bounds. It also feeds truncated, lying, NaN, non-numeric and empty files and expects an error naming why. test/mcp-read-file.test.ts commits a binary cube.stl, an ASCII cube-ascii.STL and a truncated broken.stl, checks about on each, and checks that a text file has no about. The cube builders are in test/helpers/stl.ts.

Not done: STL has no unit field, so sizes are in the model's own units with no guessed unit. Extra trailing bytes after a binary file's records count as a lying file, not as padding.

The type check passes, and so do bun test test/mcp-size.test.ts and the 17 test files scripts/fast.ts names (340 tests). To try it, call read_file on any .stl in a repository and read about.says.

🤖 Generated with Claude Code

Closes #7346

commented

MCP read_file now describes a .stl file in words, so an agent reading a 3D model doesn't have to decode the base64 itself. The answer gains about: { kind: "stl", format, triangles, size: [x, y, z], says: "STL (binary) · 12 triangles · 40.0 × 20.0 × 10.0" }. If the file is broken, about holds only kind and a says naming the reason, e.g. "STL, unreadable: truncated: says 12 triangles, holds 100 of 684 bytes". Other files get the same answer as before, and the tool's description and doc are unchanged.

  • src/text/stl.ts: readStl(bytes) returns Result<{ format, triangles, min, max }, string> and keeps only the bounds, never the mesh. A binary file must be exactly 84 + 50·n bytes. If it is shorter it is reported as truncated; if the length doesn't match it is reported as lying. Some exporters start a binary header with solid, so a file whose length fits the binary layout exactly is read as binary even then. The ASCII reader checks that each facet has exactly three vertices of plain numbers and that endsolid closes the file. Non-finite coordinates and empty solids are errors, and nothing throws. stlSize and sayStl are exported so #7335 can reuse the bounds for its camera.
  • src/http/mcp/tools/read_file.ts: an aboutFile(path, body) function with one branch per file type. The MP4 and config issues can each add their own branch next to the STL one.
  • Tests: test/text-stl.test.ts checks a binary cube, an ASCII cube and a binary header beginning with solid for counts and bounds. It also feeds truncated, lying, NaN, non-numeric and empty files and expects an error naming why. test/mcp-read-file.test.ts commits a binary cube.stl, an ASCII cube-ascii.STL and a truncated broken.stl, checks about on each, and checks that a text file has no about. The cube builders are in test/helpers/stl.ts.

Not done: STL has no unit field, so sizes are in the model's own units with no guessed unit. Extra trailing bytes after a binary file's records count as a lying file, not as padding.

The type check passes, and so do bun test test/mcp-size.test.ts and the 17 test files scripts/fast.ts names (340 tests). To try it, call read_file on any .stl in a repository and read about.says.

🤖 Generated with Claude Code

Changes requested — before the latest push

The parser is correct and well tested. The format detection, the bounds, the error messages for truncated or lying files, and the tests are all good. One finding is high, so I am requesting changes.

High: a single read_file call blocks the whole forge for about 300 ms. readStl runs synchronously on the one server thread for any .stl file up to read_file's 5 MB cap. The ASCII path builds a latin1 copy of the file, splits it into about a million token strings and runs a regex on each number. I benchmarked it on a 5 MB ASCII STL: 285–330 ms per call. A 5 MB file of short junk tokens after solid a took 378 ms. Any read-only token on any public repository can trigger this, including a repository the caller made and pushed a file to. MCP has no rate limit. A few concurrent calls therefore freeze every page, gate and terminal for seconds. The direction treats a blocked loop as pipeline damage too.

Fix first: bound the work before parsing. Pick one:

  • Only describe bodies up to a small cap (for example 1 MB). Above the cap, answer about: { kind: "stl", says: "STL, too large to describe here" }. Binary is cheap, so it can keep the 5 MB limit if you prefer. Or:
  • Replace the split and regex with a single byte-level scan that allocates nothing per token.

Either way, add a test: a 5 MB ASCII STL (or the cap plus one byte) either answers the too-large says or parses within a stated budget such as 50 ms.

Low, fix in the same round: a binary file whose header starts with solid but whose length does not match its count goes to the ASCII reader. It then reports truncated: no endsolid instead of the real problem (says N triangles but holds M bytes). When startsWithSolid is true and the ASCII read fails, prefer the binary length error, since binary is already computed. Add a test using binaryBox(1,1,1,"solid x") with a few bytes cut off.

Nothing else blocks this pull. Identity and visibility still come from repoArg. There are no writes, the output is JSON and nothing throws. Tests cover each new behaviour. The preview answers. The tool description is unchanged, as the issue asked.

commented

Both findings from @sentinel's review are fixed: the high-severity one is bounded and the low one is fixed.

High: one call could block the server thread. readStl now refuses an ASCII body larger than ASCII_LIMIT (1 MB) before it does any parsing, so a large file costs almost nothing. read_file answers that case with about: { kind: "stl", says: "STL, too large to describe here" }. Binary files keep the 5 MB limit of read_file, because reading them is a fixed-stride loop.

Below the limit, readAscii no longer splits the text into tokens. It walks the text once by index and compares keywords in place. The only strings it creates are the three numbers of each vertex. Measured here:

  • a 1 MB ASCII file parses in 17–27 ms (the 285–330 ms in the review was for 5 MB with the old code);
  • the 5 MB junk-token body is refused in under 1 ms.

Low: a binary file whose header starts with solid got the wrong error. When the ASCII read fails, the bytes at offset 80–84, where a binary file keeps its triangle count, now decide which error to report:

  • If those four bytes are not all text, the file is binary and it gets the real length error (truncated: says 12 triangles, holds … or says 12 triangles but holds …).
  • If they are all text, it is really ASCII. Read as a count, text there means at least 151 million triangles, far more than any file here can hold.

This differs from the review's suggestion to always prefer the binary error. That would also replace the error for every truncated ASCII file larger than 84 bytes, because those bytes always give a count that does not match the length. The existing truncated test for half an ASCII box checks exactly that case.

Tests

  • test/text-stl.test.ts: a file of exactly 1 MB parses correctly in under 50 ms. Files of the limit plus one byte, 5 MB, and 5 MB of junk tokens all answer TOO_LARGE. binaryBox(1,1,1,"solid x") with 7 bytes cut off, or 3 bytes added, gets the exact binary length error.
  • test/mcp-read-file.test.ts: an ASCII huge.stl over 1 MB answers the too-large says.

These two test files and bun x tsc --noEmit pass.

🤖 Generated with Claude Code

commented

read_file now describes an ASCII STL only up to ASCII_LIMIT (1 MB). Above that it answers about: { kind: "stl", says: "STL, too large to describe here" } before reading anything. This caps how long one call can hold the server thread. Below the limit, readAscii makes one pass over the text by index: it compares keywords in place and only turns each vertex's three numbers into strings. A 1 MB file now takes about 20 ms here, and a 5 MB file is refused in under 1 ms. Binary files keep the 5 MB limit of read_file, because reading them is a fixed-stride loop.

A binary file whose header starts with solid but whose length does not fit its triangle count now gets the binary length error, not truncated: no endsolid. To tell the two apart, readStl looks at the four bytes where a binary count would sit. If all four are text, the count would be at least 151 million triangles, so the file is really ASCII and keeps its ASCII error. I chose this over "always prefer the binary error" because almost every truncated ASCII file has a count that does not match its length. Those files would all have lost their truncated message.

Tests:

  • test/text-stl.test.ts:

- Parsing a 1 MB ASCII file stays within a 50 ms budget. The test uses expectBudget, judged against reading a binary file of the same size in the same run, as #4429 requires. The first push used a raw toBeLessThan(50), which the check's rule refuses. That was a mistake in my test, not in the code, and this push fixes it. - Bodies of the limit plus one byte, 5 MB, and 5 MB of junk tokens are all refused as too large. - binaryBox(1,1,1,"solid x") with 7 bytes cut off, or 3 bytes added, gets the exact binary length error.

  • test/mcp-read-file.test.ts: an ASCII huge.stl over 1 MB answers the too-large says.

These two test files, the repository's test-rule suites (flake, rule-tests-always) and bun x tsc --noEmit pass locally.

To try it, call MCP read_file on a .stl in any repository you can read: a binary or ASCII model gets an about line with format, triangles and size, and an ASCII file over 1 MB gets the too-large line.

🤖 Generated with Claude Code

Approved — before the latest push

Both asks from my last review (on c6dde53) are met.

High, blocked server thread: met. readAscii now refuses any body over ASCII_LIMIT (1 MB) before it reads anything, so a 5 MB file or 5 MB of junk tokens costs no parsing at all. Below the limit it makes one pass by index, compares keywords in place and only allocates the three numbers of each vertex. The test checks a body of exactly 1 MB with expectBudget against a binary file of the same size, and checks the limit plus one byte, 5 MB, and 5 MB of junk tokens. read_file answers STL, too large to describe here, and mcp-read-file.test.ts covers that through the tool. Binary files keep the 5 MB cap. Reading them is a fixed-stride loop, and only once the length matches the count exactly, so a lying count of 4 billion never loops.

Low, wrong error for a binary file whose header starts with solid: met. When the ASCII read fails, looksAscii checks the four bytes where a binary count would sit. Non-text there sends the file to the binary length error. That is better than my suggestion, because a truncated ASCII file keeps its truncated message. The test covers binaryBox(1,1,1,"solid x") both cut short and with bytes added, with the exact messages.

Nothing new to block. Visibility still comes from repoArg, the tool makes no writes, nothing throws, and the preview answers. Non-blocking: aboutFile matches .stl with any capitalisation, so .STL is described too, which is what a person would expect.

commented

MCP read_file now describes an .stl file in an about field: ASCII or binary, triangle count and size in model units, with one line such as STL (binary) · 12 triangles · 40.0 × 20.0 × 10.0. A truncated file, or one whose length doesn't match its header, gets STL, unreadable: <why> and never throws. This branch is merged with main, so the same field also carries #7359's report of whether a JSON, YAML or TOML file parses. The merged tests pass: test/mcp-read-file.test.ts, test/text-stl.test.ts, test/mcp-tools.test.ts, test/config-check.test.ts and test/text-config-check.test.ts, 179 in all, plus bun x tsc --noEmit.

What to look at

  • Merge in src/http/mcp/tools/read_file.ts: one aboutFile gives the about field, one branch per kind. An .stl path is read as STL; any other text file falls through to the config check. Binary files that aren't STL get no about, as before.
  • Bounded work: an ASCII STL over 1 MB (ASCII_LIMIT) gets STL, too large to describe here before anything is read, so one call cannot hold the server thread. Below the limit, readAscii reads the text in a single pass and only turns vertex numbers into strings: about 20 ms for 1 MB here. Binary STL keeps read_file's 5 MB limit because it reads fast.
  • A binary header that begins with solid: when such a file's length doesn't match its count, it now gets the length error, not truncated: no endsolid. Which error wins depends on the four bytes where a binary count sits. If they are all text, the "count" would be at least 151 million triangles, so the file is really ASCII and keeps its ASCII error. Always preferring the binary error would have replaced the error for nearly every cut-off ASCII file.
  • Speed test: it uses expectBudget, measured against reading a binary file of the same size in the same run, as #4429 requires.

How to try it: call read_file on an .stl in any repository you can read.

🤖 Generated with Claude Code

Approved

Since my approval on 87cf4ba, the only change is the merge with main, which brings in #7359's config check. Everything I asked for in earlier rounds is still met. The 1 MB ASCII_LIMIT is checked before any parsing, the parser makes one indexed pass with an expectBudget test, and a binary header that begins with solid gets its length error through looksAscii. Those lines did not change in the merge.

The merge in read_file.ts is right. aboutFile checks the .stl extension first and reads the raw bytes, so an ASCII or binary STL is never sent to the config check. A text file that is not an STL still goes to configKind/aboutConfig exactly as on main. A binary file that is not an STL is skipped because text === undefined, so it carries no about, as before. Visibility still comes from repoArg. The tool writes nothing, the output is JSON and nothing throws. The pull is green and the preview answers on /; /mcp asks for a token as it should.

Non-blocking: aboutFile's return type is a loose Record<string, unknown>. A small union of the STL and config shapes would let the type checker catch a misspelled key when the MP4 branch arrives.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs