# SolidCraft, read: a Rust CAD app that agents drive through 21 MCP tools, and how exact its rebuilds are

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.satyajit.si/articles/solidcraft
> date: 2026-11-09
> tags: cad, open-source, agents, mcp, rust

On 4 November, [@awe0_io](https://x.com/awe0_io/status/2118004875875529150) 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](https://github.com/tangency-labs/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.

<RepoCard repo="tangency-labs/solidcraft" />

## 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](https://github.com/NVIDIA-Omniverse/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](https://github.com/ricosjp/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](/articles/steve-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:

- `batch` runs 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_parameters` tries 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_topology` returns 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_picture` overlays 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,
$24000 - 9(1 - \pi/4) \cdot 20 - 25\pi \cdot 20$, 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`.

<ParamBlock />

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:

<ErrorLadder />

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](/articles/steve-desktop-cad), 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:

<NamesNotIndices />

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](/articles/steve-desktop-cad) | [3D Model Anything](/articles/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.
