SolidCraft, read: a Rust CAD app that agents drive through 21 MCP tools, and how exact its rebuilds are
mdjsonmcp2026-11-09 · 16 min · cad · agents · mcp · rust
Why read this
Notabletop 60%Reads an agent-driven Rust CAD app's kernel, MCP surface and naming, and cross-checks its reference parts in an independent kernel.
- Original analysis
- Runs on a laptop CPU
- Widely used
Developer tools & infraMITPractitioner teardown
How this was scored
- Is it new?
- 1 of 3: An incremental tweak
- Can I trust it?
- 2 of 3: Measures key facts from files, code or configs
- Can I run it?
- 2 of 3: Open code or weights with real limits
- Will I understand it?
- 2 of 3: Mechanism from first principles with figures
- Can I act on it?
- 2 of 3: A concrete recipe, numbers or comparison
- Will it last?
- 1 of 3: Relevant for months
- Does it affect many?
- 2 of 3: A widely used model, tool or lab release
- Only here?
- 2 of 3: A teardown or measurement few others did
Score 62 of 100, ranked 370 of 701 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored
On 4 November, @awe0_io posted, in Chinese, that 3D modelling software was being taken over by AI: a GitHub project with 13,000 stars that hands parametric CAD to an agent. The list in the post: create sketches and set dimensions; extrude, chamfer, cut holes and combine solids; change one parameter and the whole model rebuilds; measure geometry and inspect the model; export STEP and STL; connect to Claude Code and other agents over MCP. It also mentioned a Raspberry Pi case built from nothing, with standoffs, port cut-outs and vent slots. All of that is reported: I read the post through the fxtwitter mirror, which showed 370 likes, 53 reposts and 13 replies. I could not check the star count; the GitHub API is closed to this sandbox.
The project is SolidCraft, from Tangency Labs. The
interesting question is not whether an agent can call extrude. It is what "the whole model
rebuilds" means in numbers. When a parameter changes, how close is the rebuilt solid to the
part you meant, how would you know, and what stops a fillet from landing on the wrong edge
afterwards?
I cloned it shallow at commit 7e902aa (dated 9 November 2026). Labels below: measured is
something I counted or computed here, reported is what a file or page says, reasoned is my
inference from the two.
- license
- Apache-2.0 / MIT
- branch
- main
- tests
- 6024 files
- source
- 111.7 MB
- commit date
- 2026-11-10
by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded
local clone, 2026-11-10 at 52ea418 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount
shallow clone: counts describe the pinned tree, not the history
What I ran, and what I could not
I downloaded the v0.30.1 Linux .deb from the release page (105 MB; the CLI binary alone is
96 MB unpacked) and tried solidcraft-cli --version. The sandbox's permission layer refused to
execute it. Building from source with cargo would run the same code by another route, so I did
not try that either. SolidCraft did not run here. Every statement about what it computes is
from reading its source and docs.
What I could run is an independent kernel. I installed cadquery-ocp 8.0.1.1 (the Python
bindings of OpenCascade 8.0) into a throwaway virtual environment and rebuilt SolidCraft's own
reference parts from their recipes, on a shared 4-core CPU. That checks the yardstick SolidCraft
grades itself against, the closed-form numbers it says its parts must hit, and it lets me measure
the things the post claims in a kernel I can execute: a rebuild after a parameter change, a STEP
round trip, an STL export, and what happens to edge references. Where a number below comes from
that run, it says OpenCascade.
What it is
A parametric CAD application in pure Rust, dual-licensed MIT or Apache-2.0. The README's own
status line is modest: "Early but usable for simple parts". Desktop (egui), browser (WebAssembly),
a headless CLI and an MCP server all run one command registry (reported, README.md). The
repository is large: 14,630 tracked files, about 1.07 million lines of Rust under crates/ and
1.43 million under solidcore/, test files included (measured, git ls-files and wc -l).
The layering, from docs/architecture.md:
| Layer | Crates | Job |
|---|---|---|
| L0 | geom | vectors, planes, profiles, meshes and their measures |
| L1 | sketch, kernel, render, dynamics | constraint solver; the B-rep kernel boundary |
| L2 | doc | parameters, expressions, the feature timeline and its incremental evaluation |
| L3 | io | design files, STL/OBJ/STEP/3MF export and import |
| L4 | engine | the session and the command registry ("everything is a command") |
| L5 | gpu, ui-egui, mcp | renderer, desktop front end, the MCP tools |
The kernel is new, and so is its port
The geometry is not OpenCascade. crates/kernel is a boundary around solidcore, which its
README describes as "a boundary-representation (B-rep) solid modeling kernel in safe Rust ...
based on NVIDIA's SMLib" from the Apache-2.0 USD BRep
release. Every call into it goes through a guard that turns a kernel panic into an error on the
timeline instead of a crash (reported, docs/book/src/dev/kernel.md).
This is recent. docs/kernel-smlib-history.md records the move from the earlier
truck kernel to SMLib starting on 21 October 2026, with a
naming corpus of 154 designs as the guard that names do not change across the switch. The kernel
the post's star count was earned on is, in part, three weeks old (reasoned, from the dates in
that file and the commit date).
One rule in AGENTS.md explains why it is not OpenCascade: "No GPL/LGPL/AGPL code, not even
reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD)". FreeCAD still appears,
but only as an external program that computes reference values for the tests (more on that
below). Compare Steve's desktop CAD, which went the other way and
builds on OpenCascade through a CadQuery fork. Same job, opposite licence bet.
What an agent sees
solidcraft-cli mcp is an MCP server over stdio. Headless, it runs its own design session;
with --connect, it drives the running desktop app so you can watch. docs/mcp.md lists 21
tools (measured, from its table and crates/mcp/src/tools.rs):
| Group | Tools |
|---|---|
| discover | list_commands, describe_command, skill_reference |
| change | execute, batch, set_parameter, undo, redo, new_design, open, save |
| check | inspect_design, measure, body_topology, check_parameters, compare_picture, screenshot |
| ship | export |
| operate | pending, running, crash_reports |
The design choice that matters is the small number. An agent does not get one tool per CAD
feature. It gets execute, which runs any command in the registry with JSON parameters, and
list_commands to find them; the whole catalogue is "about 350 KB" (reported), so the listing
pages and filters by prefix. The same commands are what the toolbar runs and what a JSON script
for solidcraft-cli run contains, so a session an agent recorded can be replayed headless.
Four tools are shaped around how agents fail, and they are worth stealing for any MCP server:
batchruns steps in order, stops at the first error and rolls back the steps it already ran, so a half-built part never stays in the design. Its reply names the failing step and which steps were applied.check_parameterstries parameter values without keeping them: checkpoint, set every value in one rebuild, read the volume, bounding box, failing features and every cylindrical face, then restore. The doc says it replaces a set, measure, inspect, set-back loop that "cost every agent session three to five calls and up to 16 s of rebuilds" (reported).body_topologyreturns edges with their midpoints, because an edge's midpoint is how you pick it for a fillet. Agents name geometry by world coordinates, not by index.compare_pictureoverlays the model's silhouette on a reference photo and reports the edge offsets in millimetres at nine stations. It compares outlines only; the doc says holes are not compared.
File access is fenced: tool calls reach only the open design's folder (inside the home folder and
not hidden), folders the agent opened or saved a design in during the run, a recovery folder, a
private scratch folder, and anything named with --allow-path. Dotfiles such as ~/.ssh are never
reachable by default (reported, docs/mcp.md). For a server that writes files on an agent's
behalf, that is the right default.
The doc's "typical session" builds a 40 by 30 by 20 box, rounds one vertical edge with radius 3,
cuts a radius-5 hole through it, and shows measure returning "volume_mm3": 22390.6. I built
the same part in OpenCascade and got 22,390.575 mm³, and the closed form,
, gives the same to every printed digit
(measured). The worked example in the docs is right.
What "change one parameter and it rebuilds" means
The reference parts in tests/oracle/ are written in SolidCraft's recipe format, and case
24-user-parameters is the post's claim in its smallest form: four user parameters (width,
depth, height, hole_d), one sketch whose dimensions are expressions of them (width / 2
places the hole), and one extrude whose distance is height.
Changing height touches only the extrude; changing hole_d re-solves the sketch and then
rebuilds the extrude. set_parameter reports which features were recomputed and which were
restored, meaning an earlier result for the same inputs was reused, for instance after setting a
value back (reported, docs/mcp.md). The sketch solver is SolidCraft's own, a damped
Gauss-Newton with degree-of-freedom analysis (reported, docs/book/src/guide/sketching.md).
A B-rep kernel rebuilding this part is not approximating anything. The box faces are planes and the
hole wall is a cylinder, each stored as an equation and trimmed by exact edges. In OpenCascade I
rebuilt case 24 at twelve settings, width in 40, 60, 80, 100 by hole_d in 4, 10, 16, and the
worst relative volume error against the closed form was 1.6e-16, one unit in the last place of
a double (measured). Each rebuild of the box minus the cylinder took a few milliseconds.
Exact until you export a mesh
How SolidCraft measures a body changed with the kernel. A note in the migration history says that
during the move, "Decoded" volumes were "SolidCraft's mesh-based measure of the decoded body,
within 2e-4 relative". The current crates/kernel/src/measure.rs measures a B-rep body with SMLib's
mass properties "integrated on its exact surfaces", at a relative accuracy of 1e-8, and a comment
there records why it does not use the kernel's public API for it: SmApiBrepComputeMassProperties
"clamps to [1e-4, 1e-1], which leaves curved faces off by 1e-5 to 1e-3". Each result says what it
was measured by, smlib or mesh (reported, read in the source). So measure on a B-rep body
is meant to be good to about 1e-8, not to the last bit, and a mesh body is measured from its own
triangles.
The export is where exactness ends by design. STEP carries the B-rep; STL carries triangles. I wrote case 24 both ways from OpenCascade and read each file back:
The STEP file (19,030 bytes) reads back to the same volume within 1.25e-15. The STL at a 0.1 mm chord and 0.5 rad angle, a common default, has 120 triangles and over-measures the volume by 3.3e-4, because the hole becomes a polygon inscribed in its circle and the mesh holds material the part does not. Tightening to 0.001 mm and 0.01 rad takes 5,044 triangles and 252,284 bytes, 13 times the STEP file, to get to 1.4e-7 (measured). The same effect on a sphere is in the Steve article, where a 32-longitude mesh under-measures by 1.6%. For a 3D printer the STL is fine. For checking what an agent built, measure the B-rep, before export.
How SolidCraft grades itself
cargo xtask oracle replays every case in tests/oracle/ through the command engine and compares
body count, volume, area, centre of mass, bounding box and, where the geometry fixes it, face
count, against expected.json. Where a case has a reference.stl, it also samples the surface
distance between the two shapes. The expected values come from two places: analytic formulas
worked by hand (69 cases), and FreeCAD 1.1.4 building the same recipe through its Python API as
an external program (21 cases). docs/oracle.md reports 90 of 90 passing (reported). The
README, written earlier, still says "75 of 76".
Two things about that yardstick deserve a look. First, the default tolerance on volume and area is
1e-3 relative (reported, tests/oracle/README.md), and no case in the folder tightens it. A
volume 0.1% off passes. That fits a measure that once went through meshes; it is loose for one that
now integrates exact surfaces at 1e-8. Second, a test is only as good as its expected numbers,
and hand-derived formulas for fillets, chamfers and spherical caps are easy to get wrong.
So I checked the numbers. I rebuilt eleven of the analytic cases in OpenCascade from their recipes,
including the all-edges fillet, the chamfered top edges, the shelled box and the cube intersected
with a sphere, and compared with each expected.json:
| Case | Expected volume (mm³) | OpenCascade, relative difference | Faces, expected / OpenCascade after merging |
|---|---|---|---|
| 01-box | 24,000 | 0 | 6 / 6 |
| 03-box-through-hole | 45,738.0533 | 0 | 7 / 7 |
| 07-fillet-all-edges | 23,340.8494 | 0 | 26 / 26 |
| 08-chamfer | 23,730.6667 | 1.5e-16 | 10 / 10 |
| 09-shell | 13,632 | 0 | 11 / 11 |
| 10-rect-pattern-holes | 29,803.5400 | 0 | 18 / 18 |
| 11-circular-pattern-flange | 66,758.8439 | 2.2e-16 | 10 / 10 |
| 14-combine-cut | 42,931.4165 | 1.7e-16 | 7 / 7 |
| 15-combine-intersect | 54,454.2727 | 8.4e-9 | 7 / 7 |
| 38-coincident-cylinders | 15,707.9633 | 0 | 3 / 3 |
| 62-heat-sink | 43,125 | 3.4e-16 | 66 / 66 |
All eleven agree, ten to the last bit or one past it (measured). The exception, the cube
intersected with a radius-25 sphere, differs by 8.4e-9, the size of a numerical integration
tolerance on a trimmed sphere, not of a wrong formula. Areas agree as closely. Face counts agree
once coplanar faces are merged: OpenCascade's fuse leaves the two coaxial cylinders as five faces
and the heat sink as 96 until ShapeUpgrade_UnifySameDomain merges them, while the oracle counts
"a maximal smooth patch of one surface" as one face. I also swept the fillet radius of case 07 from
1 to 9 mm against the Steiner formula the case uses; the worst difference was 4.6e-16.
What this establishes: SolidCraft's reference numbers are right, independently of FreeCAD and of SolidCraft (measured). What it does not establish is how close SolidCraft's own results are to them, beyond the "within 1e-3" that a pass guarantees. The oracle table publishes the surface distance for FreeCAD-sourced cases (for example 0.013 mm worst on the loft), but not SolidCraft's volumes. Printing the measured relative error next to each pass would cost one column and would answer the question this article could not.
Names, not indices
The hard part of "change a parameter and it rebuilds" is not the extrude. It is the fillet downstream that was applied to "that edge". If the fillet remembers its edge as a position in the kernel's edge list, any upstream edit that adds geometry can move it to a different edge, and nothing complains. This is CAD's topological naming problem, and it is the problem an agent hits hardest, because an agent edits parameters far more freely than a person would.
I measured how easily an index goes stale. In OpenCascade, take an L bracket (a 40 by 60 by 6 plate and a 6 by 60 by 30 wall), merge coplanar faces, and list its 18 edges in explorer order. Then cut one 8 mm hole through the plate, upstream of any fillet. The hole touches none of the 18 edges. Five of them still change index, because the hole's three new edges are listed at 13 to 15 and shift the rest. Without the face merge, 2 of 28 move (measured). Edge 15, the bottom edge along the plate's free side, becomes the hole's bottom circle:
SolidCraft's answer, from docs/book/src/dev/naming.md (reported): features store names,
and names follow the kernel's history. Every kernel operation reports what each face and edge of its
result came from (crates/kernel/src/history.rs). A face built by a feature is named by role, such
as F<feature>:side:<sketch curve> for an extrude's side wall or F<feature>:blend:<edge> for a
fillet face. An edge is named by the two faces it separates, <face A>|<face B>. Pieces of a split
face are told apart by the faces that bound them, which "survives the slot moving, growing or
shrinking". The doc is plain about why the older scheme went: names used to be guessed from sample
points, which was "wrong exactly when it matters", and "a fillet or a sketch quietly re-attaches
to a different face. Nothing tells the user, which is the worst thing a CAD tool can do."
Two limits are stated with the same candour. Operations whose kernel path does not report history
yet, listed as including sheet metal, plastic and surface features, emboss, rib and the faceted
thread cut, fall back to geometric names marked unverified, and a reference resolved that way warns
face ... was found by geometry, not exact history; check it. And in recipes and in MCP calls,
the input is still a point: "edges": [[40, -30, -6]] picks the edge nearest that coordinate when
the fillet is created. The name is what is stored after that (reasoned, from the recipe format
and the naming doc together). An agent that sends coordinates computed for the old dimensions to a
new fillet can still pick the wrong edge; body_topology exists so it can look first.
Against the two CAD articles already here
| SolidCraft | Steve's desktop CAD | 3D Model Anything | |
|---|---|---|---|
| Geometry | exact B-rep, solidcore (SMLib port, Rust) | exact B-rep, OpenCascade via a CadQuery fork | triangle meshes in three.js |
| Agent interface | MCP server, 21 tools over one command registry | a document is Python against a feature API | a coding agent writes TypeScript |
| Parameters | user parameters and expressions on a feature timeline | Python variables, re-run the document | constants in code |
| How it checks itself | 90 reference parts, analytic and FreeCAD values, 1e-3 tolerance | Python unittest suites (not run there) | renders compared with photos by eye; no metric |
| Licence | MIT or Apache-2.0 | Apache-2.0, Blender kept separate | mixed |
| Run here | refused | refused | built and rendered on a CPU |
The three sit on a line from exact to evidential. 3D Model Anything has no ground truth except a
photograph, and the agent decides when it looks right. Steve and SolidCraft both have a kernel that
can say how big something is to many digits, so an agent can check its own work with arithmetic.
SolidCraft goes one step further in putting the checks into the tool surface itself:
check_parameters, body_topology, measure with a summary mode sized for "a stage check against
computed numbers". That is the difference between an agent that models and an agent that can
verify what it modelled (reasoned).
Where it is thin
- The kernel moved weeks ago. The SMLib port landed in late October. The naming corpus and the 90 oracle cases were built to hold across that move, but 90 parts is a small sample of what people model, and the README's own line is "early but usable for simple parts".
- The pass bar is wide. 1e-3 relative on volume is a reasonable gate for a fillet patch that two kernels build differently. It is too wide to show that an exact rebuild is exact.
- The numbers I could not get. I do not know SolidCraft's actual volumes on these parts, how long a rebuild takes, or whether the Raspberry Pi case in the demo rebuilds cleanly at other dimensions. All three need the binary to run.
- Star count and adoption are from the post, not from GitHub.
What to take away
If you are giving an agent a CAD tool, give it an exact kernel, a measure that reports what it
measured by, a rollback on every multi-step call, and references by name rather than by index.
SolidCraft has all four, and its docs say plainly where each one stops. Then check the agent's
part against a formula before you export the STL, because the STL is where the exactness ends.
To reproduce the cross-check: pip install cadquery-ocp, then build each tests/oracle/<case>/recipe.json
with BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder, BRepAlgoAPI_Cut and
BRepFilletAPI_MakeFillet, and compare BRepGProp.VolumeProperties_s with the case's
expected.json. The cases are small; all eleven build in well under a second on one core.