~/satyajit

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

mdjsonmcp

2026-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.

tangency-labs/solidcraft@52ea418 · snapshot 2026-11-10
tracked files
14,664
license
Apache-2.0 / MIT
branch
main
tests
6024 files
source
111.7 MB
commit date
2026-11-10
source by language
Rust109.3 MB(3766)Python1.8 MB(145)WGSL285.3 kB(17)Shell148.4 kB(40)JavaScript146.5 kB(14)PowerShell49.5 kB(6)CSS21.8 kB(3)

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:

LayerCratesJob
L0geomvectors, planes, profiles, meshes and their measures
L1sketch, kernel, render, dynamicsconstraint solver; the B-rep kernel boundary
L2docparameters, expressions, the feature timeline and its incremental evaluation
L3iodesign files, STL/OBJ/STEP/3MF export and import
L4enginethe session and the command registry ("everything is a command")
L5gpu, ui-egui, mcprenderer, 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):

GroupTools
discoverlist_commands, describe_command, skill_reference
changeexecute, batch, set_parameter, undo, redo, new_design, open, save
checkinspect_design, measure, body_topology, check_parameters, compare_picture, screenshot
shipexport
operatepending, 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:

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−π/4)⋅20−25π⋅2024000 - 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.

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:

CaseExpected volume (mm³)OpenCascade, relative differenceFaces, expected / OpenCascade after merging
01-box24,00006 / 6
03-box-through-hole45,738.053307 / 7
07-fillet-all-edges23,340.8494026 / 26
08-chamfer23,730.66671.5e-1610 / 10
09-shell13,632011 / 11
10-rect-pattern-holes29,803.5400018 / 18
11-circular-pattern-flange66,758.84392.2e-1610 / 10
14-combine-cut42,931.41651.7e-167 / 7
15-combine-intersect54,454.27278.4e-97 / 7
38-coincident-cylinders15,707.963303 / 3
62-heat-sink43,1253.4e-1666 / 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

SolidCraftSteve's desktop CAD3D Model Anything
Geometryexact B-rep, solidcore (SMLib port, Rust)exact B-rep, OpenCascade via a CadQuery forktriangle meshes in three.js
Agent interfaceMCP server, 21 tools over one command registrya document is Python against a feature APIa coding agent writes TypeScript
Parametersuser parameters and expressions on a feature timelinePython variables, re-run the documentconstants in code
How it checks itself90 reference parts, analytic and FreeCAD values, 1e-3 tolerancePython unittest suites (not run there)renders compared with photos by eye; no metric
LicenceMIT or Apache-2.0Apache-2.0, Blender kept separatemixed
Run hererefusedrefusedbuilt 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

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.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "SolidCraft, read: a Rust CAD app that agents drive through 21 MCP tools, and how exact its rebuilds are", ai.satyajit.si, November 2026.

bibtex
@misc{ghana2026solidcraft,
  author = {Satyajit Ghana},
  title  = {SolidCraft, read: a Rust CAD app that agents drive through 21 MCP tools, and how exact its rebuilds are},
  url    = {https://ai.satyajit.si/articles/solidcraft},
  year   = {2026}
}
share