Skip to main content
Glama

mesh2cad

ci emirmrkaljevic/mesh2cad MCP server

Turn 3D meshes (STL/OBJ/PLY) into real, editable CAD (STEP): fitted planes, cylinders, spheres, cones, and constant-radius fillets instead of triangle soup. CLI + MCP server for AI agents. MIT, pure Python on top of OpenCascade.

demo

Try it in the browser: the example gallery shows four real printer parts in 3D before and after, with the STEP files and the generated CadQuery/OpenSCAD scripts to download.

before/after

Left: 21k-triangle mesh with 0.1 mm noise. Right: the STEP mesh2cad writes: 10 planes + 2 cylinders, max deviation 0.05 mm from the true part.

30-second quickstart

pip install mesh2cad
mesh2cad convert part.stl -o part.step --report report.md

Open part.step in FreeCAD, Fusion, Onshape or any CAD tool: you get real planes, cylinders and fillets you can select and dimension. report.md says how close the result is to the mesh, and mesh2cad falls back to faceted output instead of guessing when a fit is bad.

Using an AI agent (Claude Desktop, Cursor, any MCP client)? pip install "mesh2cad[mcp]" and add:

{ "mcpServers": { "mesh2cad": { "command": "mesh2cad", "args": ["mcp"] } } }

Proof on real parts: all 6 tested Original Prusa i3 MK3S parts convert to exact STEP, and all 6 also to an editable CadQuery, OpenSCAD, or FreeCAD script (see Real parts).

Related MCP server: ai-cad-workbench

Install

pip install mesh2cad              # CLI + STEP
pip install "mesh2cad[mcp]"       # + MCP server
pip install "mesh2cad[cadquery]"  # + run/verify generated CadQuery scripts

OpenSCAD scripts are plain text. Verifying one needs the openscad program (openscad.org); it is not a Python dependency. Without it the .scad file is still written.

FreeCAD macros are plain text too. Verifying one needs freecadcmd (the FreeCAD console, from freecad.org). It is not a Python dependency. Without it the .FCMacro file is still written. Ubuntu 24.04 does not ship a freecad apt package, so install the console build and put freecadcmd on PATH.

Use

mesh2cad convert part.stl -o part.step --tol 0.1 --report report.md
mesh2cad report part.stl --json

--tol ≈ the noise / accuracy of your mesh in model units (default 0.1 % of the bounding-box diagonal).

Output: STEP + fidelity report: method (exact / faceted-hybrid / faceted), face types, solid validity, two-sided Hausdorff / RMS deviation, volume error, % area per surface type.

Editable parametric CadQuery script

For simple prismatic parts mesh2cad can also write a CadQuery Python script where every dimension is a named parameter, so you change a number and re-run instead of editing B-rep faces:

mesh2cad convert bracket.stl --cadquery bracket.py   # add -o bracket.step to also write STEP
python bracket.py                                    # -> bracket.step
# --- parameters ---
base_x           = 40.0  # base length (X)
base_y           = 30.0  # base width (Y)
base_t           = 6.0  # base thickness (Z)
...
hole_1_d         = 6.0  # diameter
hole_1_x         = 0.0
hole_1_y         = 0.0
hole_1_csk_d     = 12.0  # countersink diameter
hole_1_csk_angle = 90.0  # countersink angle

Make the hole 2 mm bigger: mesh2cad edit bracket.py --set hole_1_d=8 --in-place -o bracket.step, or set hole_1_d = 8.0 and run the script yourself. A plate or block whose four vertical corners share one radius also gets fillet_r and .edges("|Z").fillet(fillet_r), so the same edit grows the round. A quarter-torus fillet on a vertical boss or hole is a parameter of that feature: boss_1_fillet where a straight boss meets the top face, hole_1_rim_r on the top rim of a hole, hole_1_bot_rim_r on the bottom rim, and hole_1_floor_r where a blind hole's floor meets the wall. mesh2cad edit part.py --set boss_1_fillet=3 rebuilds that round.

A hole or boss on a side face uses the same kind of parameter. This is the block below (60×40×36, an Ø8 hole through X, an Ø6 hole 12 deep opening on +Y, an Ø10 boss 8 tall on +X):

boss_1_d     = 10.0  # diameter
boss_1_h     = 8.0  # height from the +X face
boss_1_y     = 10.0
boss_1_z     = 8.0
hole_1_d     = 8.0  # diameter (axis X)
hole_1_y     = 0.0
hole_1_z     = -6.0
hole_2_d     = 6.0  # diameter (axis Y)
hole_2_x     = -12.0
hole_2_z     = 8.0
hole_2_depth = 12.0  # blind depth from the +Y face

Set hole_1_d = 10.0 and run the script again. The volume drops by π(5²−4²)·60 mm³, the analytic change for that through hole.

A part that is one straight extrusion of a 2D outline is the same kind of script. An L, a notched plate, or a block with a slanted face, extruded along X, Y, or Z, becomes extrude_len plus an outline list. plug-aligner (the printed part in the table below, 7.3×17.7×5.5 mm) is a 5.5 mm extrusion along Z of an eight-point outline with a rectangular slot:

extrude_len = 5.5  # length along Z
extrude_0   = 0.0  # start along Z
# outline in the XY plane, (x, y)
outline = [
    (0.0, 0.0),
    (6.1, 0.0),
    (6.1, 6.95),
    (7.3, 6.95),
    (7.3, 10.75),
    (6.1, 10.75),
    (6.1, 17.7),
    (0.0, 17.7),
]
# through opening in the outline
cutout_1 = [
    (0.45, 1.85),
    (5.65, 1.85),
    (5.65, 15.85),
    (0.45, 15.85),
]

Set extrude_len = 7.5 and the part is 2 mm thicker. A corner moves when one pair in outline changes. A round through-hole on an extrusion is still hole_1_d, with hole_1_y and hole_1_z when the hole runs along X. cutout_1 is a polygonal slot that goes all the way through. An extrusion along Y stores the outline as (x, z); along X, as (y, z).

Supported: a Z-up box/plate base, rectangular pads on the top face, holes along X, Y, or Z (through, blind, countersunk when the axis is vertical, and counterbored on any of those axes), bosses along X, Y, or Z (straight, and drafted or chamfered when the axis is vertical), that one vertical corner radius, a quarter-torus fillet on a vertical boss or hole (boss_N_fillet, hole_N_rim_r, hole_N_bot_rim_r, hole_N_floor_r), and one straight extrusion of a 2D outline along X, Y, or Z, including a polygonal through-slot and round through-holes. A counterbored hole is hole_N_d for the through diameter, hole_N_cbore_d for the large diameter, and hole_N_cbore_depth for how far that large diameter goes in from the face it opens on. A part that is a few of those extrusions is their intersection, or one of them minus straight cuts along the other axes. Two or three outlines use extrude_z_len / outline_z (and the same for X and Y); a single outline keeps extrude_len and outline. A cut is cut_1_len, cut_1_0, and cut_1. Up to four faces tilted to X, Y, and Z are planar clips (clip_1_x, clip_1_y, clip_1_z, clip_1_d). The box script is the one that is written when it already reproduces the mesh, so a plain plate stays base_x / base_y / base_t and does not become an outline list. A rectangular plate with a counterbored hole stays on those base parameters. Values are rounded to clean numbers (5.997 → 6.0) when that stays within --tol. Before writing, the script is executed and compared with the mesh (needs cadquery installed); if the part is outside this vocabulary or the script deviates, nothing is written and the command exits with code 2 and the reason. Use STEP output for everything else.

Editable parametric OpenSCAD script

The same parts can be written as OpenSCAD, which is what the 3D-printing Customizer on Thingiverse and Printables edits. Names and values match the CadQuery script. --openscad and --cadquery can be combined, and -o still writes STEP.

mesh2cad convert plate.stl --openscad plate.scad
openscad -o plate.stl plate.scad

A 40×30×6 plate with a Ø10 hole comes out as below, plus a short header. Before:

// --- parameters ---
/* [Base] */
// base length (X)
base_x   = 40.0;
// base width (Y)
base_y   = 30.0;
// base thickness (Z)
base_t   = 6.0;
base_cx  = 0.0;
base_cy  = 0.0;
// bottom face height
base_z0  = -3.0;

/* [Holes] */
// diameter
hole_1_d = 10.0; // [0.2:0.1:30]
hole_1_x = 0.0;
hole_1_y = 0.0;

/* [Hidden] */
$fn = 96; // circle segments

// --- model ---
module mesh2cad_part() {
  difference() {
    translate([base_cx, base_cy, base_z0 + base_t / 2])
      cube([base_x, base_y, base_t], center=true);
    translate([hole_1_x, hole_1_y, base_z0 - 1])
      cylinder(h=base_t + 2, d=hole_1_d);
  }
}

mesh2cad_part();

After, the hole is 2 mm bigger: one number changes, then re-render:

hole_1_d = 12.0; // [0.2:0.1:30]

Re-render (F6) and export STL. A drafted boss is a cylinder with r1/r2, a chamfer or countersink is the same, and the four vertical corners of a rectangular base are offset(r=fillet_r) on a smaller square, then extruded. A boss-base fillet, a hole-rim round, and a blind-floor fillet are a rotate_extrude() of a square with a quarter circle taken out of it, using boss_1_fillet, hole_1_rim_r, or hole_1_floor_r. The profile overlaps the sharp solid by 0.02 mm so the union stays closed. A hole or boss whose axis is X or Y is the same cylinder, turned with rotate so it points along that axis; its parameters are the diameter and the two coordinates of the axis (hole_1_y / hole_1_z for an X hole). A straight extrusion is linear_extrude(extrude_len) of polygon(outline), with polygon(cutout_1) subtracted in 2D when the outline has a slot. An extrusion along Y or X is that solid turned so the outline sits in the XZ or YZ plane. A counterbored hole is a second cylinder of diameter hole_1_cbore_d and height hole_1_cbore_depth, from the face it opens on. Several extrusions are an intersection(), and a cut extrusion or a planar clip is subtracted. Names match the CadQuery script (extrude_len, outline, hole_1_d, hole_1_cbore_d, cut_1, clip_1_d). A slider (// [min:max]) is added only where the part gives a bound: hole diameter up to the shorter side of the face it is cut through (or of the outline, on an extrusion), counterbore diameter and depth on that same axis, fillet up to half the short side of the plate, draft until the tip would close, countersink angle in 60–120°, chamfer up to the boss, pad size up to the base, blind depth up to the length of the hole's axis. Positions (hole_1_x, base_z0, extrude_0, …) stay plain numbers. The outline is a point list, not a slider.

Same honesty as CadQuery. Before writing, mesh2cad renders the script with the openscad CLI and compares the STL to the mesh (sampled Hausdorff and volume, same limits as the CadQuery check). If openscad is not installed the file is still written and the command says it was unverified. If the part is outside this vocabulary, or the render does not match, nothing is written and the command exits with code 2 and the reason. Circles are $fn = 96 polygons; on these parts that is a few thousandths of a millimetre, not an analytic solid. STEP remains the path for everything else.

Editable parametric FreeCAD macro

The same parts can be written as a FreeCAD macro. Names and values match the CadQuery script. --freecad can be combined with --cadquery, --openscad, and -o.

mesh2cad convert plate.stl --freecad plate.FCMacro
freecadcmd plate.FCMacro

A 40×30×6 plate with a Ø10 hole comes out as below (the file also has a short header and, when you run it, creates a Part::Feature and writes plate.step next to the macro):

# --- parameters ---
base_x   = 40.0  # base length (X)
base_y   = 30.0  # base width (Y)
base_t   = 6.0  # base thickness (Z)
base_cx  = 0.0
base_cy  = 0.0
base_z0  = -3.0  # bottom face height
hole_1_d = 10.0  # diameter
hole_1_x = 0.0
hole_1_y = 0.0

# --- model ---
result = Part.makeBox(base_x, base_y, base_t, App.Vector(base_cx - base_x / 2, base_cy - base_y / 2, base_z0))
top = base_z0 + base_t
result = result.cut(Part.makeCylinder(hole_1_d / 2, base_t + 2, App.Vector(hole_1_x, hole_1_y, base_z0 - 1), App.Vector(0, 0, 1)))

Make the hole 2 mm bigger: set hole_1_d = 12.0 and run the macro again. A drafted or chamfered boss is Part.makeCone. The four vertical corners of a rectangular base are result.makeFillet(fillet_r, ...) on the edges parallel to Z. The same makeFillet rounds the circular edge of a vertical boss base, a hole rim, or a blind-hole floor, with boss_1_fillet, hole_1_rim_r, or hole_1_floor_r. A hole or boss whose axis is X or Y is the same cylinder, with App.Vector(1, 0, 0) or App.Vector(0, 1, 0). A straight extrusion is _extrude(outline, "Z", extrude_0, extrude_len) (or "X" / "Y"). A polygonal slot is a second extrusion subtracted from the first. Several extrusions are .common() (intersection) or .fuse() (a partial-depth union). A cut extrusion, a hole, a counterbore, a 45 degree chamfer, and a planar clip are .cut(). Names match the other scripts (extrude_len, outline, hole_1_d, hole_1_cbore_d, cut_1, clip_1_d).

The solid in FreeCAD is analytic: cylinders and cones, not a $fn polygon. The macro is a Part script with named numbers at the top. It does not build a PartDesign body or a constrained sketch, so the edit is the number, then run the macro again.

Same honesty as the other two. Before writing, mesh2cad runs the macro with freecadcmd and compares a fine tessellation of the solid to the mesh (sampled Hausdorff and volume, same limits). If freecadcmd is not installed the file is still written and the command says it was unverified. If the part is outside this vocabulary, or the solid does not match, nothing is written and the command exits with code 2 and the reason. STEP remains the path for everything else.

Change a parameter and re-render

params and edit close the loop on a script mesh2cad already wrote. They do not fit a mesh.

mesh2cad params bracket.py
mesh2cad edit bracket.py --set hole_1_d=8 --set base_x=+2 --in-place -o bracket.step

params lists every named value in a CadQuery .py, an OpenSCAD .scad, or a FreeCAD .FCMacro: name, current value, kind, and unit when one is known. Kinds are the ones those scripts use (diameter, length, depth, radius, angle, position, and points for an outline or a cutout). Units are millimetres, or degrees for an angle (hole_1_csk_angle, boss_1_draft_deg). Names are the ones in the scripts above: base_x, base_y, base_t, hole_1_d, hole_1_depth, hole_1_cbore_d, fillet_r, extrude_len, outline, and the same names with an axis or an index (extrude_z_len, outline_z, cut_1).

edit replaces the numeric token and leaves every other byte of the file as it was. hole_1_d=8 sets that value. base_x=+2 adds 2 mm, and base_x=-1 subtracts 1 mm. A leading + or - is always a delta, so it is not how you type an absolute negative: move base_z0 by a delta from its current value. --in-place rewrites the script. --output-script new.py writes a copy. With neither flag the script on disk stays as it is. -o writes the solid. CadQuery and FreeCAD write .step or .stl. OpenSCAD writes .stl.

The command prints volume and the bounding box before and after, and whether the result is one valid solid.

Limits, plainly:

  • Only scripts mesh2cad generated, the ones with the parameter block it writes. A hand-written CadQuery file is refused.

  • Outline and cutout point lists are listed and are not edited as a single number. Changing a corner of outline is still a text edit.

  • An unknown name is refused, and the message lists the names that exist.

  • A hole wider than the wall it is cut through, a counterbore or countersink that no longer clears the through-hole, a fillet that eats the short side of the base, or a draft that closes a boss, is refused. So is a render that is empty or is not one valid solid. Nothing is written in those cases.

  • The wall check uses the rectangular base, or the outline the script extruded. It is the obvious case (the hole does not fit in that face), not a full constraint solver.

  • CadQuery runs in-process (pip install 'mesh2cad[cadquery]'). OpenSCAD and FreeCAD use the openscad and freecadcmd programs on PATH. If the program is missing, the script can still be rewritten and the command says the solid was not rendered. -o in that case writes nothing.

  • Values from --set are numbers substituted into the existing literal. The script is executed only the same way a render already executes it.

  • This does not build a PartDesign body or a constrained sketch. The number changes, then the same script runs again.

MCP server (Claude Desktop, Cursor, …)

{ "mcpServers": { "mesh2cad": { "command": "mesh2cad", "args": ["mcp"] } } }

Tools: convert(input_path, output_path?, tol?, cylinders?, spheres?, cones?), analyze(input_path, tol?, cylinders?, spheres?, cones?), report(input_path, tol?, cylinders?, spheres?, cones?), to_cadquery(input_path, output_path?, tol?), to_openscad(input_path, output_path?, tol?), to_freecad(input_path, output_path?, tol?), list_params(script_path), edit_params(script_path, changes, output_path?). list_params and edit_params take a script mesh2cad already wrote. edit_params updates that script in place. A change is a number, or a string that starts with + or - for a delta. output_path is the STEP or STL. When it is omitted, a .step (.stl for OpenSCAD) is written next to the script if that backend can run. The result includes the old and new values, the rendered path, validity, the volume and bounding-box delta, and warnings. An unknown name or an edit that is not one valid solid comes back under error, and nothing is written. The script tools return the parameters and, when the script was run or rendered, the measured deviation. to_openscad returns verified: false when the openscad program is absent. to_freecad returns verified: false when freecadcmd is absent. spheres and cones default on.

Listed on Glama and the TensorBlock MCP catalog.

How it works

  1. Load + clean mesh (trimesh). Noisy meshes get a Taubin-smoothed copy for normals. A clean CAD tessellation (dihedral angles piled onto a few exact values, including an export whose faces are single quads) is segmented on the raw normals, and a face of one or two triangles is kept.

  2. Region growing into planes; strip chains and leftovers are fitted as cylinders, spheres, or cones (normal-ray / algebraic / constant-normal-angle fits, robust nonlinear refinement). A narrow round between two planes is gathered back into one partial cylinder and snapped so its generators lie in those planes. A coaxial cylinder + cone component (countersink, chamfered pin) is split by normal-to-axis angle first. Small noisy patches that tile a sphere are merged into one spherical region. Split/redundant regions are merged and absorbed.

  3. Exact B-rep (OCP / OpenCascade): plane polygons from triple-plane intersections, circles from plane ∩ cylinder and plane ∩ sphere, plane ∩ cone, full cylindrical walls, partial fillet cylinders, quarter tori tangent to a plane and a coaxial cylinder, spherical caps and zones, conical frustums, sewn into a solid and validated.

  4. Fallbacks: hybrid faceted solid (vertices snapped to fitted surfaces, coplanar triangles merged), then raw faceted solid.

  5. Fidelity report against the input mesh.

Self-test benchmark

Ground-truth cadquery parts → tessellated + subdivided (≤ 2 mm edges) → Gaussian vertex noise → mesh2cad → compared with the truth. 3 seeds, worst case shown (python benchmarks/run.py); the same cases run in CI. Numbers are STEP / B-rep fidelity (Hausdorff, RMS, volume, face counts). Parametric OpenSCAD, CadQuery, and FreeCAD export are verified separately when you pass --openscad / --cadquery / --freecad (or the MCP to_openscad / to_cadquery / to_freecad tools); they are not rows in this table.

part

noise σ (mm)

exact

faces (plane/cyl[/sph[/cone[/torus]]])

radius err (mm)

Hausdorff (mm)

RMS (mm)

vol err %

time s

cube

0.0

3/3

6/0

-

0.000

0.000

0.00

0.6

cube

0.05

3/3

6/0

-

0.019

0.006

0.07

0.6

cube

0.2

3/3

6/0

-

0.075

0.026

0.34

0.7

l_bracket

0.0

3/3

8/0

-

0.000

0.000

0.00

0.9

l_bracket

0.05

3/3

8/0

-

0.040

0.006

0.03

1.1

l_bracket

0.2

3/3

8/0

-

0.216

0.026

0.15

1.2

stepped_block

0.0

3/3

11/0

-

0.000

0.000

0.00

1.0

stepped_block

0.05

3/3

11/0

-

0.032

0.006

0.02

2.2

stepped_block

0.2

3/3

11/0

-

0.136

0.022

0.07

2.4

plate_with_hole

0.0

3/3

6/1

-

0.016

0.002

0.00

1.8

plate_with_hole

0.05

3/3

6/1

-

0.028

0.006

0.05

2.3

plate_with_hole

0.2

3/3

6/1

-

0.078

0.023

0.18

1.9

boss

0.0

3/3

7/1

-

0.027

0.006

0.01

1.8

boss

0.05

3/3

7/1

-

0.033

0.007

0.03

2.2

boss

0.2

3/3

7/1

-

0.091

0.017

0.09

4.6

ribbed_plate

0.0

3/3

10/2

-

0.012

0.001

0.00

2.5

ribbed_plate

0.05

3/3

10/2

-

0.028

0.005

0.03

2.5

ribbed_plate

0.2

3/3

10/2

-

0.108

0.020

0.12

2.6

spherical_dome

0.0

3/3

6/0/1

-

0.065

0.007

0.04

1.8

spherical_dome

0.05

3/3

6/0/1

-

0.067

0.008

0.06

1.8

spherical_dome

0.2

3/3

6/0/1

-

0.241

0.028

0.25

1.9

spherical_pocket

0.0

3/3

6/0/1

-

0.046

0.004

0.02

2.2

spherical_pocket

0.05

3/3

6/0/1

-

0.057

0.005

0.03

2.1

spherical_pocket

0.2

3/3

6/0/1

-

0.075

0.015

0.13

2.1

countersunk_plate

0.0

3/3

6/1/0/1

-

0.039

0.003

0.01

2.2

countersunk_plate

0.05

3/3

6/1/0/1

-

0.055

0.009

0.07

2.2

countersunk_plate

0.2

1/3

16342/0, 6/1/0/1

-

0.590

0.049

0.54

21.2

tapered_boss

0.0

3/3

7/0/0/1

-

0.086

0.011

0.05

1.9

tapered_boss

0.05

3/3

7/0/0/1

-

0.095

0.012

0.05

1.8

tapered_boss

0.2

3/3

7/0/0/1

-

0.123

0.025

0.22

2.1

chamfered_boss

0.0

3/3

7/1/0/1

-

0.053

0.005

0.02

2.7

chamfered_boss

0.05

3/3

7/1/0/1

-

0.096

0.012

0.08

2.5

chamfered_boss

0.2

3/3

7/1/0/1

-

0.511

0.059

0.49

2.3

filleted_plate

0.0

3/3

6/4

0.001

0.013

0.002

0.01

1.3

filleted_plate

0.05

3/3

6/4

0.100

0.038

0.008

0.07

1.3

filleted_plate

0.2

2/3

5796/0, 6/4, 7/3

0.447

0.857

0.086

0.33

6.8

filleted_box

0.0

3/3

6/4

0.003

0.006

0.001

0.00

1.8

filleted_box

0.05

3/3

6/4

0.041

0.028

0.006

0.03

1.9

filleted_box

0.2

3/3

6/3, 6/4

0.206

0.845

0.048

0.18

1.8

filleted_bracket

0.0

3/3

8/6

0.005

0.010

0.002

0.00

1.8

filleted_bracket

0.05

3/3

8/6

0.131

0.041

0.008

0.02

1.9

filleted_bracket

0.2

0/3

10452/0

-

0.958

0.130

0.50

13.7

filleted_boss

0.0

3/3

7/1/0/0/1

0.008

0.026

0.002

0.05

3.2

filleted_boss

0.05

3/3

7/1/0/0/1

0.279

0.150

0.021

0.10

3.2

filleted_boss

0.2

3/3

7/1

-

0.864

0.068

0.25

3.3

rounded_hole

0.0

3/3

6/1/0/0/1

0.068

0.026

0.004

0.05

3.9

rounded_hole

0.05

2/3

20908/0, 6/1/0/0/1

0.247

0.633

0.036

0.73

29.7

rounded_hole

0.2

3/3

6/1

-

0.681

0.041

0.09

2.8

"exact" = valid analytic B-rep. A face count equal to the triangle count is the faceted fallback, which is still a valid solid. When a torus is present the face column is plane/cylinder/sphere/cone/torus. radius err is the worst |fitted − true| cylinder radius among seeds that came back exact, or the torus minor radius for filleted_boss and rounded_hole, and - when the part has no fillet radius or no seed was exact. All rows are the worst of 3 seeds from python benchmarks/run.py. The 90° countersink (Ø6 hole, Ø12 head, 3 mm deep) at 0.2 mm noise is exact on 1 of 3 seeds. At 0.2 mm the filleted bracket falls back on every seed; the plate is exact on 2 of 3, and one of those solids is missing a corner (7 planes + 3 cylinders). The block stays a valid solid, but one seed keeps only three of the four rounds (Hausdorff 0.85 mm). At 0.2 mm the boss fillet and the hole-rim round are not recovered: the STEP is the sharp cylinder and planes, which is why the Hausdorff is the size of the missing round. CI checks the cases that are exact, plus fallbacks that must stay valid solids.

Real parts

The table above subdivides every edge to ≤ 2 mm, so a planar face is many triangles. An STL written by a CAD tool is the other way around: long triangles, very uneven size, no noise. python benchmarks/real_parts.py downloads six printed parts from the Original Prusa i3 MK3S, converts them at the default tolerance, and tries --cadquery, --openscad, and --freecad on each. The files are Prusa Research's, GPL-2.0, from prusa3d/Original-Prusa-i3 at commit 529e5ad (Printed-Parts/STL/<name>.stl). They are cached under benchmarks/.cache and are not part of this repository. CI does not run the script (it needs the network). A coarse, unsubdivided cadquery mesh of the same kind is what tests/test_coarse.py checks.

part

triangles

method

output faces

regions

valid

Hausdorff

RMS

vol err %

time s

plug-aligner

48

exact

14 plane

14 plane

yes

0.0000

0.0000

0.000

0.5

endstop-block

308

exact

15 plane, 2 cylinder

15 plane, 2 cylinder

yes

0.0168

0.0036

0.204

1.7

y-belt-idler

2464

exact

197 plane, 6 cylinder, 3 cone

197 plane, 6 cylinder, 3 cone

yes

0.0082

0.0005

0.003

5.6

y-belt-holder

2472

exact

210 plane, 4 cylinder

210 plane, 4 cylinder

yes

0.1602

0.0188

0.092

6.4

y-motor-holder

2802

exact

396 plane, 6 cylinder, 2 cone

396 plane, 6 cylinder, 2 cone

yes

0.1001

0.0048

0.016

12.7

y-rod-holder

5608

exact

158 plane, 8 cylinder, 11 cone

158 plane, 8 cylinder, 11 cone

yes

0.2178

0.0305

0.391

12.4

plug-aligner is a 14-plane prism, 48 triangles, watertight. It comes back exact: 14 planes, Hausdorff 0, 0.5 s. endstop-block (308 triangles, 13.6×9×14 mm) is exact: 15 planes and two cylinders, radii 1.65 mm and 3.10 mm, in 1.7 s. Hausdorff 0.017 mm and volume error 0.20% are the chordal error of those holes against the triangles. On 0.7.0 both were faceted-hybrid. The aligner was one unfitted region (the output happened to be 14 planes only after the faceted merge). The endstop was 75 planar faces, holes included, in about 4 s.

All six parts are exact. On 0.10.0 the idler, the holder and the motor holder fell back, and the unfitted area was described as small edge rounds. That was wrong. The STEP files at the same commit have no tori and no spheres: planes, cylinders, 45 degree cones, and (on the idler, holder and motor) a few small belt-profile B-splines, about 0.10 to 0.20% of the area. The gaps were faces glued across a 45 or 90 degree edge, short chamfers the cone test rejected, and cylinders left behind after a chamfer was carved out.

y-belt-idler comes back as 197 planes, 6 cylinders and 3 cones, Hausdorff 0.0082 mm, volume error 0.003%, 5.6 s. The cylinders are the real holes (R 2.80, 1.65, 1.60 and 1.50 mm) and the cones are the 45 degree chamfers. The eight R=11.1 mm bands from 0.10.0 are flat faces whose corners lie on a circle; they are planes. The extra planes, against 127 in the STEP file, are the belt profile tessellated as facets. On 0.10.0 this part was faceted-hybrid, Hausdorff 0.28 mm, 12.3 s.

y-belt-holder is 210 planes and 4 cylinders, Hausdorff 0.1602 mm, volume error 0.092%, 6.4 s. The cylinders are the holes that close (R 2.80 mm and R 1.60 mm). One of those circles is split across four coplanar pads that do not touch, and is sewn as four arcs. Some rounds and the belt profile stay planes, which is the 0.16 mm Hausdorff. On 0.10.0 the output was 890 planar faces, 21.0 s.

y-motor-holder is 396 planes, 6 cylinders and 2 cones, Hausdorff 0.1001 mm, volume error 0.016%, 12.7 s. The cylinders are the real holes (R 2.80, 1.60 and 1.65 mm) and the cones are 45 degree chamfers. The radii near 8.6, 8.9 and 9.1 mm were the same flat-face pattern, not one bore. The large round in the STEP file is R=11.25 mm, only part of a turn, and it meets a chamfer, so it stays a set of planes; that is the 0.10 mm Hausdorff. On 0.10.0 this was faceted-hybrid, Hausdorff 0.27 mm, 13.4 s.

y-rod-holder is exact: 158 planes, 8 cylinders and 11 cones, Hausdorff 0.2178 mm, volume error 0.391%, 12.4 s. The cylinders include the R=4.00 mm bore, and the cones are 45 degree chamfers, including the two on that bore. On 0.11.0 the bore met several chamfers in one loop and did not sew, so the output was the raw mesh: 3043 faces, Hausdorff 0, 34.2 s. The 0.22 mm that remains is three shallow patches kept as single planes. Their vertices sit 0.16 to 0.24 mm off the fitted plane, and no internal edge is sharper than about 10 degrees, so they are not split. Cylinders and cones fit to under 0.005 mm.

--cadquery and --openscad write plug-aligner and endstop-block. plug-aligner is the extrusion above, and both scripts match the mesh to 0.0000 mm. endstop-block is not one extrusion: the cross-section changes along the part. The script is the intersection of three silhouettes (extrude_z_len 14 mm from z=−15 (outline_z, eight points), extrude_x_len 13.56 mm from x=−13 (outline_x, five points), extrude_y_len 9 mm from y=−16 (outline_y, five points)), plus an Ø3.3 hole along X at y=−11, z=−11 with an Ø6.2 counterbore 8 mm deep from the −X end (hole_1_d, hole_1_cbore_d, hole_1_cbore_depth), plus one planar clip (clip_1_x 0.5, clip_1_y −0.7071, clip_1_z 0.5, clip_1_d 7.0561). Both self-checks are 0.0421 mm. The limit at the default tolerance is max(4·tol, 0.1) = 0.1 mm. The CadQuery volume error against the mesh is 0.109%.

y-belt-idler exports both scripts as well. The solid is the intersection of an X extrusion (extrude_x_len 23 mm from x=-11.5) and a Y extrusion (extrude_y_len 34 mm from y=-14.3). The Y outline is a raster of the mesh, because fusing the projected faces came out smaller than the part. The X outline stays analytic, which keeps the outer 45 degree corner. Fifteen cuts remove the belt grooves, three hex pockets, and two logo floors (a logo keeps its letter islands as cut_N_cutout_J). hole_1 is an Ø3.2 hole along X at y=0, z=0 with an Ø5.6 counterbore 4 mm deep from the +X end. hole_2 is an Ø3 blind hole, 0.5 mm deep from the +Y end, at x=-7.5, z=-15, with a 45 degree taper of 0.5 mm at the inner end (hole_2_chamfer). hole_3 and hole_4 are Ø3.3 holes along Z with a 0.4 mm 45 degree chamfer from the -Z end (hole_3_chamfer, hole_4_chamfer). The CadQuery self-check is 0.1534 mm (volume error 0.144%) and the OpenSCAD self-check is 0.1639 mm (volume error 0.145%). The limit is max(4·tol, 0.1) = 0.192 mm, the same gate as every other script. python benchmarks/real_parts.py prints:

part

cadquery

dev mm

openscad

dev mm

freecad

dev mm

plug-aligner

yes

0.0000

yes

0.0000

yes

0.0000

endstop-block

yes

0.0421

yes

0.0421

yes

0.0421

y-belt-idler

yes

0.1534

yes

0.1639

yes

0.1693

y-belt-holder

yes

0.0943

yes

0.0965

yes

0.0896

y-motor-holder

yes

0.1843

yes

0.1843

yes

0.1843

y-rod-holder

yes

0.1300

yes

0.1300

yes

0.1300

FreeCAD passes the same gate on all six. plug-aligner is 0.0000 mm. endstop-block is 0.0421 mm (limit 0.1 mm). y-belt-idler is 0.1693 mm (limit 0.192 mm). y-belt-holder is 0.0896 mm (limit 0.154 mm). y-motor-holder is 0.1843 mm (limit 0.266 mm). y-rod-holder is 0.1300 mm (limit 0.138 mm). Each of those also stayed under the 1% volume gate, or the macro would not have been written. The idler is 0.0054 mm farther than the OpenSCAD mesh and 0.0159 mm farther than CadQuery; the holder is slightly closer than both. The other four match CadQuery's deviation.

y-motor-holder exports both scripts as well. The intersection of the full-length silhouettes is 7.924 mm off (30.07% volume), over the limit of 0.266 mm, so the script is a union of partial-depth extrusions instead. The base runs from z=0 to z=8 (extrude_z1_len). A block and a sloped roof stand on part of it from z=8 to z=19, each clipped by its own plane (extrude_z2_clip1_d, extrude_z3_clip1_d). Three 45 degree edge bevels are short prisms with their own clips. Cuts remove the 0.5 mm slots, two hexagonal pockets, and two shallow pockets in the bottom face (an island in a pocket is cut_N_cutout_1). The base has a 0.4 mm 45 degree chamfer on the edges that are chamfered (extrude_z1_chamfer, extrude_z1_chamfer_at). hole_3 and hole_4 are Ø3.2 holes along Z from z=0 to z=8 (hole_3_z0, hole_3_z1), with an Ø5.6 counterbore 3 mm deep from z=8 and a 0.4 mm chamfer at z=0. The CadQuery self-check is 0.1843 mm (volume error 0.306%) and the OpenSCAD self-check is 0.1843 mm (volume error 0.298%).

y-rod-holder exports both scripts. The intersection of the full-length silhouettes was 0.745 mm off (limit 0.138 mm, volume error 0.61%): the Z outline followed the Ø8 bore where the slot breaks into it, and it chorded the slanted -Y wall. The +Y side reaches y=5.5 only for part of the height, which the X silhouette already has. After that miss, the outline is rebuilt from a raster snapped to the vertical walls. A run that rides a hole is closed at the two mouths, and a flare outside the cylinder stays on the loop. A planar bevel that does not run the full length of the part is a triangular cut (cut_N). A 45 degree cone that is not already a hole chamfer is cone_N_r0, cone_N_r1, cone_N_h. Shallow floors are scored again on that solid. The Ø3.3 hole along Y at x=-11.65, z=-5 keeps the Ø6.3 counterbore, about 3.5 mm deep from the -Y end. That counterbore is recovered from a partial ring of planes on the parametric path only. STEP segmentation is unchanged: the same facets stay planes, which is why the exact solid above is still 158 planes, 8 cylinders and 11 cones. The CadQuery self-check is 0.1300 mm (volume error 0.096%) and the OpenSCAD self-check is 0.1300 mm (volume error 0.089%). The limit is 0.138 mm. On 0.13.0 the refusal was 0.745 mm.

y-belt-holder exports both scripts. The cross-section changes along X: the base is full height over only part of the length and then steps down to a thin guide, a hexagonal pocket and a bore occupy only part of that length, and five slits are 0.2 mm thick. A full-length silhouette fills those openings. On 0.14.0 the refusal was 2.517 mm and 1.97% volume. The script is one bounding extrusion along X (extrude_len 21.5 mm from x=3, outline the YZ rectangle) minus a cut for each span where the real section stays still. The section is kept as cut_N_cutout_1. The hexagon, the bore next to it, and the five slits are polygonal cuts of their own. The belt teeth, including the small B-spline patches, are whatever section the mesh has at that station. CadQuery deviates by 0.0943 mm (volume error 0.700%) and OpenSCAD by 0.0965 mm (volume error 0.700%). The limit is 0.154 mm. The exact STEP solid is unchanged (210 planes, 4 cylinders, Hausdorff 0.1602 mm): those B-spline patches are still planes there.

On 0.7.0 the idler was faceted, 915 faces, about 15 s. The holder, motor holder and rod holder did not finish within 300 s. Segmentation of those meshes was a few seconds. The time was UnifySameDomain on the faceted fallback, after the fit had left the solid as one unfitted component, and that call hit its timeout. Fitting the planes and cylinders keeps the aligner and the endstop off that path. All four of those parts are exact and finish in 6 to 13 s.

Limits (v0.19.0)

Planes, cylinders, spheres, cones, constant-radius fillets between two planes, and quarter tori between a plane and a coaxial cylinder. Cylinders must end on planes perpendicular to their axis (through-holes, bosses, pins). Spheres are exact when each boundary loop is a single plane ∩ sphere circle (domes, spherical bosses, spherical pockets, and zones between two parallel planes). A sphere that runs into an edge or a cylinder falls back to the hybrid solid; vertices are still snapped onto the fitted sphere, but the face stays faceted. Cones are exact when they run between two planes perpendicular to their axis: countersinks, chamfered holes and bosses, drafted/tapered bosses. Cones may also meet a coaxial cylinder (countersunk hole, chamfered pin). A cone that ends in its apex falls back to the hybrid solid. Countersinks and narrow chamfers are exact through about 0.05 mm of noise; a 2 mm chamfer and a wide drafted wall stay exact at 0.2 mm. The countersunk plate's 90° countersink (Ø6 hole, Ø12 head, 3 mm deep) at 0.2 mm noise is exact on one of three seeds. On the other two the conical band and the hole wall meet at a single vertex, so those seeds fall back to a valid faceted solid.

A fillet between two planes is a partial cylinder. When the round runs from one cap to another (rounded vertical corners of a plate or block, a single edge, the inner corner of an L as well as the outer ones), it comes back as one cylinder, tangent to both walls, with the radius in the report's cylinder faces. The radius is the one whose axis is tangent to both walls and which fits the arc; wall triangles that remain in the patch do not pull it off. On the synthetic plate (r=5 mm), block (r=2 mm) and L-bracket (r=3 mm) that rebuild is exact at 0 and 0.05 mm of noise, all three seeds. With no noise the radius error is 0.001 mm on the plate, 0.003 mm on the block and 0.005 mm on the bracket. At 0.05 mm the worst radius error is 0.041 mm on the r=2 corners, 0.100 mm on the r=5 plate and 0.131 mm on the r=3 bracket. At 0.2 mm the bracket falls back on every seed. The plate is exact on 2 of 3 seeds, and one of those solids is missing a corner. The block is a valid solid on every seed, but one seed keeps only three of the four rounds and the Hausdorff rises to 0.85 mm. Whatever does not sew stays a valid faceted solid.

On a clean tessellation (dihedral angles sitting on a few exact values, which is what a CAD export looks like), a region that crosses a real edge of about 35 degrees or more is split and each piece is refit. A flat polygon whose corners lie on a circle stays planes: the face centres are millimetres off that circle. A short 45 degree chamfer is kept when the residual is roundoff, which is how a countersink a few tenths of a millimetre tall survives. A ring of single-facet planes whose centres lie on one full cylinder is merged back into that cylinder, so a chamfer sees one wall instead of dozens of triangles. A hole end split across coplanar pads that do not share an edge is sewn as arcs of one circle. A plane corner whose three normals are linearly dependent, or whose intersection lands far from the mesh, uses the mesh vertex when it lies on the three planes. None of this runs on a noisy mesh. A fillet between two planes is not split.

A fillet that runs into a spherical corner (every edge of a box filleted) is not rebuilt: the corner is a sphere octant bounded by three cylinders. A fillet between a plane and a coaxial cylinder is a quarter torus, tangent to both, and is rebuilt when the band closes around the axis. The report lists a torus face and its major and minor radius. Two shapes use the inner equator (major = cylinder radius + minor): the concave fillet at the base of a boss, and the convex round on the rim of a hole. The floor of a blind hole uses the outer equator (major = cylinder radius minus minor). On the synthetic boss (cylinder radius 8 mm, fillet 2 mm) this is exact on 3 of 3 seeds at 0 and at 0.05 mm. With no noise the minor radius error is 0.008 mm and the major is 10.012 mm (Hausdorff 0.026 mm). At 0.05 mm the worst minor error is 0.279 mm and the Hausdorff is 0.150 mm. The Ø12 hole with a 1.5 mm top round is exact with no noise (minor error 0.068 mm, major 7.451 mm, Hausdorff 0.026 mm) and on 2 of 3 seeds at 0.05 mm (worst minor error 0.247 mm). The third seed stays a valid faceted solid, Hausdorff 0.633 mm. Rounding both rims (1.2 mm) is exact with no noise (two tori, minor error 0.061 mm, Hausdorff 0.027 mm). The Ø10 blind hole with a 1.5 mm floor fillet is exact with no noise (minor 1.463 mm, major 3.529 mm, Hausdorff 0.021 mm). At 0.05 mm the blind floor falls back (Hausdorff up to 0.992 mm) and a hole with both rims rounded falls back (Hausdorff up to 1.835 mm). At 0.2 mm the boss fillet and the single hole rim are not recovered: the STEP is the sharp cylinder and its planes (Hausdorff 0.864 mm on the boss, 0.681 mm on the hole), still one valid solid. A torus that does not sew does the same. The belt-profile patches on the Prusa parts are non-rational B-splines; they come back as planes, and the Hausdorff above is that difference. A cylinder that is only part of a turn and meets more than two caps and two tangent walls (the motor holder's R=11.25 mm round) is not rebuilt as one face. A bore that meets chamfers and planes in one loop is rebuilt: that is the rod holder's R=4 mm cylinder and the cones on its ends. Free-form surfaces are still unsupported. fillet_r is still only the four vertical corners of a rectangular base (OpenSCAD as offset(r=fillet_r) on the base profile). A quarter torus on a vertical boss or hole is also a named parameter. boss_N_fillet is the concave round at the base of a straight boss. hole_N_rim_r is the convex round on the top rim, hole_N_bot_rim_r the bottom rim, and hole_N_floor_r the concave fillet where a blind floor meets the wall. CadQuery and FreeCAD fillet that circular edge. OpenSCAD revolves a square with a quarter circle removed, overlapped 0.02 mm into the sharp solid. An L-bracket fillet and a spherical corner stay on the STEP path. A torus whose axis is not Z is refused. On a filleted boss standing on the +X face the band did not come back as a torus, so there is no script for that case either.

With no noise the script gate is max(4·tol, 0.1) = 0.1 mm, and all three exporters pass it. The boss recovers boss_1_fillet = 2.0. CadQuery deviates by 0.0099 mm (volume error 0.020%), OpenSCAD by 0.0099 mm (0.015%), FreeCAD by 0.0099 mm (0.025%). The Ø12 hole recovers hole_1_rim_r = 1.43 (true 1.5; the cylinder diameter comes out 12.04). CadQuery 0.0274 mm (0.045%), OpenSCAD 0.0274 mm (0.043%), FreeCAD 0.0274 mm (0.047%). Both rims recover 1.14 (true 1.2; diameter 12.03). CadQuery 0.0224 mm (0.027%), OpenSCAD 0.0224 mm (0.025%), FreeCAD 0.0224 mm (0.029%). The blind floor recovers hole_1_floor_r = 1.46 (true 1.5) and depth 12.0 (diameter 9.98). CadQuery 0.0143 mm (0.006%), OpenSCAD 0.0138 mm (0.007%), FreeCAD 0.0141 mm (0.005%).

At 0.05 mm the limit is 0.200 mm. The boss fillet is found on 3 of 3 seeds. The script is written only for seed 0, with boss_1_fillet = 1.7: CadQuery 0.178 mm (volume error 0.113%), OpenSCAD 0.178 mm (0.108%), FreeCAD 0.179 mm (0.118%). Seeds 1 and 2 are refused at 0.226 mm and 0.238 mm (CadQuery, volume error 0.14%). The single hole rim is written on 2 of 3 seeds with hole_1_rim_r = 1.3. Seed 0 is 0.157 mm (CadQuery), 0.158 mm (OpenSCAD), 0.158 mm (FreeCAD). Seed 1 is 0.144 mm (CadQuery, volume error 0.068%). Seed 2 is refused at 0.241 mm and 1.35% volume. Both rims on seed 0 are found as two tori and refused at 0.201 mm, 0.001 mm over the limit, on all three backends (volume error 0.16%). The blind floor on seed 0 is not a torus. The script is refused with "sphere faces are not supported yet", which matches the STEP fallback on that part.

At 0.2 mm the limit is 0.800 mm and no seed keeps a torus, so the fillet parameter is absent. The boss becomes the sharp cylinder. CadQuery is refused on seed 0 at 0.807 mm (volume error 0.45%) and written on seeds 1 and 2 at 0.758 mm and 0.719 mm. OpenSCAD seed 0 is refused at 0.867 mm. FreeCAD seed 0 is written at 0.745 mm. Against the true boss, that sharp script (seed 1) is 0.793 mm off: the sagitta of the missing 2 mm round, which is why the noisy check sits on either side of 0.800 mm. The Ø12 hole is written as a sharp hole on 3 of 3 CadQuery seeds (worst 0.625 mm, volume error 0.073%). The hole with both rims is the same (worst 0.705 mm, volume error 0.182%). On seed 0, OpenSCAD and FreeCAD agree: 0.596 mm for the single rim and 0.584 mm for both rims. The blind hole is refused on seed 0 with "hole profile is not a plain or countersunk hole". The same gate as every other script decides this. A miss is not rewritten into a fillet.

The parametric scripts are one Z-up box, pads on its top face, and holes and straight cylindrical bosses whose axis is X, Y, or Z. A through hole runs from one face of the base to the opposite face. A blind hole opens on one face; hole_N_depth is the distance from that face to the flat end. A side boss sits on one side face and grows straight out. Countersinks, drafts, and chamfers are still vertical only. A quarter-torus fillet is a parameter only when its axis is Z: boss_N_fillet on a straight boss, hole_N_rim_r and hole_N_bot_rim_r on a through hole, and hole_N_floor_r on a blind floor. The same fillet on X or Y is refused. An axis that is not aligned with X, Y, or Z is refused, and so is a side feature that does not open on a face of the base.

A straight extrusion of one outline is written as well, along X, Y, or Z. The outline may be an L, a notch, or a slanted edge. extrude_len is the length, extrude_0 is where it starts, and outline is the point list (YZ when the extrusion is along X, XZ along Y, XY along Z). A polygonal opening through that extrusion is cutout_N. A round through-hole keeps hole_N_d and its position. A counterbored through-hole adds hole_N_cbore_d and hole_N_cbore_depth. When the box script already matches the mesh it is the one that is written, so a plate, a pad, and a side hole stay on base_x, hole_1_d, and fillet_r.

A part made of a few of those extrusions is written too, when every region is a plane, an axis-aligned cylinder, or a 45 degree cone on a principal axis. The solid is the intersection of the silhouettes along X, Y, and Z, or one of those extrusions minus straight cut extrusions along the other axes. A single outline keeps the names above. Two or three use extrude_z_len / outline_z and the same for X and Y. A cut is cut_1, cut_1_len, and cut_1_0. A cut may be blind, with one open end. A shallow floor parallel to an exterior cap, such as a hex pocket or a logo, is the same kind of cut; a non-circular island inside it is cut_N_cutout_J. A face tilted to all three axes, at most four of them, is a planar clip: clip_1_x, clip_1_y, clip_1_z, clip_1_d with the outward normal and n·x = d. Intersected extrusions overlap by 0.02 mm in the script so the shared end faces stay a manifold; the other outline stops at the true end, so the nominal solid does not grow. Cuts are grown by the same 0.02 mm, so a pocket that opens on an outer face is not left with that skin. If the fused silhouette is more than 3% smaller than a filled raster of the mesh, the raster outline replaces it. This replacement runs only after the analytic solid has already missed the gate, so a part whose fused outline is already right does not change. endstop-block is this case (three silhouettes, one counterbored hole, one clip), and both scripts deviate from the mesh by 0.0421 mm. y-belt-idler is the same vocabulary with 45 degree hole chamfers, a blind hole, and the floor cuts, and both scripts deviate by at most 0.1639 mm.

A round hole on a combined extrusion may be a through hole, a counterbored through hole, or a blind hole (hole_N_depth, measured from the face it opens on). A counterbore whose wall is only part of a turn, because the bore breaks out of a side face, is recovered from that ring of planes when the facets sit on one principal cylinder, the arc is one span, and the bore inside the arc is empty. A tangent fillet, and a flat polygon whose corners merely lie on a circle, are not read as a hole wall. When the mouth stops short of the bounding end and the gap is already empty, the counterbore depth is measured from that end. This fit is not applied during segmentation, so a STEP file does not gain the cylinder. A 45 degree cone on that hole is hole_N_chamfer, the axial length of the cone, either a mouth chamfer or a taper at the inner end of a blind hole. A draft and a countersink that is not 45 degrees are still refused. A single extruded outline still has no chamfer: those stay vertical, and only on the box vocabulary.

When that intersection misses the mesh, the same gate is applied to a union of partial-depth extrusions. Two outlines on one axis are extrude_z1_len / outline_z1 and extrude_z2_len / outline_z2 (and the same for X and Y). Each keeps its own start. A joint overlaps by 0.02 mm; the named length stays the true length. A clip that belongs to one extrusion is extrude_z2_clip1_x and the same for y, z, and d, and it cuts only that extrusion. A 45 degree chamfer on the start face of a Z extrusion is extrude_z1_chamfer (the leg) and extrude_z1_chamfer_at (the outline edges that carry it). A hole that opens on an intermediate face carries hole_N_z0 and hole_N_z1 (or x or y), and the counterbore is measured from that span, not from the far end of a taller neighbour. y-motor-holder is this case. Both scripts deviate by 0.1843 mm (CadQuery volume error 0.306%, OpenSCAD 0.298%), under the limit of 0.266 mm. On 0.12.0 the same part was refused at 7.924 mm and 30.07% volume, which was the visual hull of the full-length silhouettes.

When the partial-depth union also misses the gate, the outlines are rebuilt from a raster of the mesh with each edge snapped to a vertical wall. A gap that follows a hole cylinder is closed by the chord of that hole, unless the mesh sits outside the cylinder, in which case those points stay. A planar bevel whose two joining walls are axis-aligned is a triangular cut (cut_N) along the edge, over the span where the face is a strip rather than a single tip. A chord of a known hole is not treated as that bevel. A 45 degree cone that was not used as hole_N_chamfer is its own cutter: cone_N_r0, cone_N_r1, cone_N_h, at cone_N_x, cone_N_y, cone_N_z. Shallow floors are tried again on this solid and kept only when the solid moves closer to the mesh. None of this runs when an earlier solid already passed the gate. y-rod-holder is this case. Both scripts deviate by 0.1300 mm (CadQuery volume error 0.096%, OpenSCAD 0.089%), under the limit of 0.138 mm.

When the wall-snapped rebuild also misses the gate, the part is cut back from one bounding extrusion along the axis whose section area moves the most. Stations are taken along that axis. A run of stations whose outer profile stays within 0.05 mm is one cut: the bounding rectangle, with the section kept as cut_N_cutout_J. A gap between two parallel faces thinner than those stations (down to 0.12 mm) is a polygonal cut of the section between the faces. An inner loop that keeps its centre and its area from one station to the next, such as a hex pocket or a bore, is a polygonal cut over that span. This does not run when an earlier solid already passed the gate. The gate is unchanged.

Still refused, and the command says which: a boss on an extruded outline; a countersink, chamfer, or draft on a single extruded outline; a bevel whose walls are not axis-aligned, or whose section is not the right triangle of the face; a rounded edge that is not the four vertical corners of a rectangular base and not a vertical boss-base fillet, hole-rim round, or blind-floor fillet; a torus fillet whose axis is not Z; a stepped hole that is not a counterbored through-hole; more than four faces tilted to X, Y, and Z (that check runs before the partial-depth union); a recess that is not a straight cut, a shallow floor, or a section cut of the kind above; a sphere; an unfitted region. A flat patch that the fitter returned as a sphere is written as the plane its vertices lie on, so a noisy counterbore floor is not refused as a sphere. Free-form surfaces stay on the STEP path.

On the 60×40×36 block (Ø8 through hole along X at y=0, z=−6; Ø6 blind hole, 12 deep, opening on +Y at x=−12, z=8; Ø10 boss, 8 tall, on +X at y=10, z=8) both scripts recover those dimensions with no noise and at 0.05 mm noise. With no noise the CadQuery script matches the mesh to 0.006 mm (volume error 0.004%) and the OpenSCAD script to 0.006 mm (volume error 0.003%). At 0.05 mm noise the check against that noisy mesh is 0.139 mm / 0.015% for CadQuery and 0.149 mm / 0.016% for OpenSCAD, under the same limit as every other script, max(4·tol, 0.1) = 0.20 mm. The blind end is not its own plane on this part (the disk stays in the cylinder region), so the depth is the median of the vertices clustered at that end. The exact STEP builder does not sew that cylinder; STEP export of this block falls back to a valid faceted solid, Hausdorff 0.004 mm from the mesh. Anything else outside this vocabulary stays on the STEP path.

The FreeCAD macro is that same vocabulary and the same refusal list. fillet_r is still the four vertical corners. makeFillet also rounds the circular edge named by boss_N_fillet, hole_N_rim_r, hole_N_bot_rim_r, or hole_N_floor_r. A boss on an extruded outline, a countersink that is not 45 degrees on a combined extrusion, a sphere, and an unfitted region are still refused, and the message says which. The macro's cylinders and cones are analytic. The check meshes the solid at 0.01 mm linear deflection, which on these parts is a few thousandths of a millimetre, the same kind of gap as OpenSCAD's $fn = 96. The file you edit is a Part script, not a PartDesign tree, so a dimension change is one number at the top and another run of the macro.

Development

pip install -e .[dev]
pytest
python benchmarks/real_parts.py   # Prusa STLs, needs the network; not in CI
python docs/make_demo.py   # regenerate images (needs matplotlib, pillow)

License: MIT

Available Tools

8 tools
analyzeC

Segment a mesh and report detected surfaces without writing STEP.

NameRequiredDescriptionDefault
tolNo
conesNo
spheresNo
cylindersNo
input_pathYes

TDQS

C2.8/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden. It does disclose that segmentation happens and no STEP file is written, but it omits what the 'report' contains, whether the mesh is modified, how tolerance affects detection, and any return or side-effect details.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is a single front-loaded sentence with no filler. The clipped 'without writing STEP' phrasing is slightly awkward but still useful.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a five-parameter analysis tool with no annotations and no output schema, the description is too thin. It states the high-level purpose but leaves parameter behavior, output shape, and operational constraints undocumented.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, and the description does not mention any of the five parameters. Terms like 'tol', 'cones', 'spheres', and 'cylinders' are left entirely unexplained beyond their schema titles and defaults.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb and resource: 'Segment a mesh' and 'report detected surfaces'. It also distinguishes the operation from a conversion/write path by saying 'without writing STEP', though it does not name the sibling tools it differs from.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The phrase 'without writing STEP' implies an analysis-only usage context and suggests it is not the conversion tool. However, it does not explicitly state when to use this tool versus 'convert', 'report', or the export siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

convertA

Convert an STL/OBJ/PLY mesh into a STEP B-rep with fitted planes, cylinders, spheres, cones, and constant-radius fillets.

Args: input_path: path to the mesh file. output_path: STEP path to write (default: input with .step suffix). tol: fit tolerance in model units (~ mesh noise); default 0.1% of bbox diagonal. cylinders: also fit cylinders (holes, bosses, plane-plane fillets). spheres: also fit spheres (domes, spherical bosses). cones: also fit cones (chamfers, countersinks, tapered bosses). Returns the fidelity report (method, faces, deviation, volume error) and output path.

NameRequiredDescriptionDefault
tolNo
conesNo
spheresNo
cylindersNo
input_pathYes
output_pathNo

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the disclosure burden and does well: it states that a file is written to output_path, that output_path defaults to input with .step suffix, that tol defaults to 0.1% of bbox diagonal, and it enumerates the returned fidelity report fields (method, faces, deviation, volume error). It does not cover overwrite behavior, failure modes, or units consistency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded summary sentence followed by a tight Args block and a returns line; no filler, and each parameter note adds distinct information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 6-parameter tool with no output schema, the description covers inputs, defaults, and return contents well. Missing are overwrite semantics for an existing output file and what happens on failed fits, which an agent would want before invoking a write operation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, and it does: every parameter gets a semantic gloss — tol as mesh noise in model units, the three boolean flags with concrete geometry examples (holes/bosses, domes, chamfers/countersinks), and default behaviors for both optional paths.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb (convert) plus concrete source formats (STL/OBJ/PLY mesh) and target representation (STEP B-rep) with the fitted primitive types listed. This distinguishes it clearly from siblings like to_cadquery or to_openscad, which produce code rather than a STEP file.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The input/output formats imply when to reach for it, but there is no explicit statement of when to prefer this over to_openscad/to_cadquery or when conversion is inappropriate (e.g. already-B-rep inputs). Usage is inferable rather than stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

edit_paramsA

Change named parameters in a script mesh2cad generated and re-render the solid.

Args: script_path: path to a .py, .scad, or .FCMacro file written by mesh2cad. changes: mapping of parameter name to a new number, or to a string that starts with + or - to add that delta (for example {"hole_1_d": 8, "base_x": "+2"}). output_path: STEP or STL to write. CadQuery and FreeCAD accept .step or .stl. OpenSCAD accepts .stl. When omitted, a .step (or .stl for OpenSCAD) is written next to the script if that backend is available. The script is updated in place. Every byte outside the replaced numbers stays the same. Unknown names are refused with the valid list. An edit that does not produce one valid solid, or a hole larger than the wall it sits in, is refused and nothing is written. Returns old and new values, the rendered path, validity, volume and bounding-box delta, and warnings. Returns an "error" key when the edit is refused or the backend needed for output_path is not available.

NameRequiredDescriptionDefault
changesYes
output_pathNo
script_pathYes

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations supplied, the description carries the full behavioral burden and does so thoroughly: in-place mutation, byte-level preservation outside replaced numbers, refusal on invalid solid or oversized hole with nothing written, and an 'error' key when the backend is missing. These are exactly the side-effect and failure semantics an agent needs before invoking a mutating tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core action is front-loaded in one sentence, then a compact Args block, then behavior. The Args section partly restates the schema, but each line adds format or constraint detail, so little is wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite no output schema and a nested free-form 'changes' object, the description states the return payload (old/new values, rendered path, validity, volume, bbox delta, warnings) and the error case. Nothing material is missing for correct invocation of a destructive-adjacent edit tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, and it does: script_path file-type restrictions (.py/.scad/.FCMacro), the changes mapping with number vs. '+2'/'-' delta string syntax and a concrete example, and output_path format rules per backend plus the default when omitted. This adds substantial meaning beyond the bare schema types.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb+resource pair ('Change named parameters in a script mesh2cad generated') and adds the consequential second action ('re-render the solid'). It is clearly distinguishable from read-only siblings like list_params and analyze, though it never names a sibling explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied rather than stated: the refusal of unknown names 'with the valid list' hints that list_params should be consulted first, and the output_path rules signal when each backend applies. There is no explicit when-to-use / when-not-to-use statement or named alternative.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_paramsA

List the editable parameters of a CadQuery, OpenSCAD, or FreeCAD script mesh2cad generated.

Args: script_path: path to a .py, .scad, or .FCMacro file written by mesh2cad. Returns name, value, kind, and unit for each parameter. Outline and cutout point lists are included and are not single numbers. Returns an "error" key when the file is not a script mesh2cad generated.

NameRequiredDescriptionDefault
script_pathYes

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does well: it discloses the return fields (name, value, kind, unit), warns that outline and cutout point lists are not single numbers, and documents an "error" key for unsupported files. It doesn't explicitly declare the operation read-only or describe ordering/limits, but the behavior profile is largely covered.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the purpose, then a compact Args line and return-value notes; nothing is padded. The embedded Args block is slightly redundant in style but earns its place given zero schema descriptions.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

A single-parameter read tool with no output schema is adequately covered: purpose, accepted file types, return fields, edge cases, and error signaling are all present. Only an explicit relationship to edit_params is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, and it does: script_path is explained as a path to a .py, .scad, or .FCMacro file written by mesh2cad. That adds real meaning beyond the bare string type in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ("List") and resource ("editable parameters") and constrains scope to scripts generated by mesh2cad in three named formats. It doesn't explicitly contrast itself with the sibling edit_params, so sibling differentiation is only implied by "editable".

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The precondition "written by mesh2cad" and the error behavior for foreign files are stated, which implicitly tells the agent when the tool will fail. However, there is no explicit guidance on when to choose this over edit_params or analyze, and no stated workflow such as "call this before editing".

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

reportC

Full Markdown fidelity report for converting this mesh.

NameRequiredDescriptionDefault
tolNo
conesNo
spheresNo
cylindersNo
input_pathYes

Output Schema

NameRequiredDescription
resultYes

TDQS

C2.4/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden. It discloses only that output is a full Markdown report, but says nothing about whether the tool reads or mutates the mesh, what 'fidelity' means, whether it performs primitive fitting, or what permissions/costs are involved.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The single sentence is front-loaded and free of filler, so it is concise. But the terseness borders on under-specification: one short sentence does not structure enough information for a five-parameter report tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be explained. Still, for a tool with five undocumented parameters and no annotations, the description omits critical context: report contents, tolerance behavior, primitive inclusion flags, and relationship to siblings.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% across five parameters. The description only implicitly suggests 'input_path' is a mesh via 'this mesh'; it gives no meaning for 'tol', 'cones', 'spheres', or 'cylinders', so an agent cannot infer how to use the geometric filtering options.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a 'Markdown fidelity report' tied to mesh conversion, so the output format and general topic are clear. However, it does not say what the report actually contains or how it differs from the sibling 'analyze' tool, leaving the purpose only loosely defined.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to use this versus 'analyze', 'convert', 'to_openscad', or 'to_cadquery'. The phrase 'for converting this mesh' hints it follows a conversion, but no explicit prerequisites, exclusions, or alternative-selection criteria are provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

to_cadqueryB

Write an editable parametric CadQuery script (.py) for a simple part.

Works for a box/plate base with rectangular pads, holes along X, Y, or Z (through, blind, counterbored, and countersunk when the axis is vertical), bosses along X, Y, or Z (straight, and drafted or chamfered when the axis is vertical), a fillet at the base of a vertical boss (boss_N_fillet), a round on a vertical hole rim (hole_N_rim_r, hole_N_bot_rim_r) and a blind-hole floor fillet (hole_N_floor_r), a straight extrusion of a 2D outline along X, Y, or Z, and a small boolean of those extrusions (intersection, or one minus a cut along another axis) with round through-holes, counterbores, and a few planar clips. Each dimension is a named parameter (e.g. hole_1_d = 6.0, extrude_len): edit it and run the script with Python to regenerate the part. Returns the parameters and the measured deviation of the script's output from the mesh. Returns an "error" key when the part is outside that vocabulary.

NameRequiredDescriptionDefault
tolNo
input_pathYes
output_pathNo

TDQS

B3.1/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does disclose meaningful behavior: the output is an editable, re-runnable script, the return includes parameters plus 'measured deviation of the script's output from the mesh', and an 'error' key signals out-of-vocabulary parts. Still missing: what the input model must be, whether the script is written to output_path, and what tolerance/validation is applied.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose is front-loaded in sentence one and the remaining text is dense capability scoping rather than filler. The repeated axis clauses ('along X, Y, or Z' three times) and deep parenthetical nesting make it a heavy read, but each clause constrains what geometry is supported, which is genuinely load-bearing.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

This is a conversion/generation tool with 3 undocumented parameters and no output schema. The description explains the shape of the return value but leaves the entire input contract (file type of input_path, meaning of tol and output_path, error handling for bad paths) unspecified — a significant gap for a tool the agent must invoke correctly on the first try.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% for all three parameters, so the description must compensate and does not: input_path (required), output_path, and tol are never mentioned. The one concrete example ('hole_1_d = 6.0, extrude_len') describes the *output* parameters of the generated script, not the tool's inputs, so the agent gets no help on what file format input_path expects or what tol means.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+artifact: 'Write an editable parametric CadQuery script (.py) for a simple part.' The target format (CadQuery .py) inherently distinguishes it from siblings to_openscad and to_freecad, though it never names them. The capability vocabulary enumeration further pins down exactly what 'simple part' means.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The long vocabulary list ('box/plate base with rectangular pads, holes... bosses... fillets...') plus 'Returns an "error" key when the part is outside that vocabulary' implicitly tells the agent when this tool will succeed versus fail. However, it never explicitly says when to pick this over to_openscad or to_freecad, or what to do on the error path.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

to_freecadA

Write an editable parametric FreeCAD macro (.FCMacro) for a simple part.

Same parts as to_cadquery: a box/plate base with rectangular pads, holes and bosses along X, Y, or Z (countersink, draft, and chamfer only when the axis is vertical), one radius on the four vertical corners, a quarter-torus fillet on a vertical boss or hole (boss_N_fillet, hole_N_rim_r, hole_N_floor_r), a straight extrusion of a 2D outline, and a small boolean of those extrusions. Parameter names match the CadQuery script (hole_1_d, boss_1_h, fillet_r, boss_1_fillet, extrude_len, ...). Edit one and run the macro in FreeCAD, or with freecadcmd. Returns the parameters, whether freecadcmd verified the macro against the mesh, and the measured deviation when it did. Without freecadcmd the macro is still written and verified is false. Returns an "error" key when the part is outside that vocabulary.

NameRequiredDescriptionDefault
tolNo
input_pathYes
output_pathNo

TDQS

A3.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does well: it discloses the optional freecadcmd verification step, that verified is false when freecadcmd is absent, that a measured deviation is returned, and that an 'error' key appears for out-of-vocabulary parts. It omits behavioral details such as whether output_path is overwritten and what happens on invalid input_path.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action, then scoped vocabulary, then return behavior — a sensible order. The long enumeration of supported features is dense but earns its place because the description later says out-of-vocabulary parts return an error, so the list defines the tool's limits.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The output shape is described well, which matters since there is no output schema, and error behavior is covered. However, for a tool whose only required argument is a path, leaving input_path, output_path, and tol entirely unexplained (with the 'parameters' wording ambiguous) leaves a real gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% for all three parameters, and the description never mentions input_path, output_path, or tol. Worse, its use of the word 'parameters' refers to the generated macro's variables (hole_1_d, boss_1_h, fillet_r), not the tool's arguments, which could mislead an agent looking for input semantics.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence gives a specific verb, artifact, and format ('Write an editable parametric FreeCAD macro (.FCMacro) for a simple part'). It also anchors itself against the sibling to_cadquery by stating the part vocabulary and macro parameter names are the same, so an agent can tell which of the two converters produces which artifact.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage context is implied rather than stated: 'Edit one and run the macro in FreeCAD, or with freecadcmd' describes what to do with the output, not when to pick this tool over to_cadquery or to_openscad. The 'Same parts as to_cadquery' framing hints at equivalence of scope, but no explicit when-to-use or when-not-to-use rule is given.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

to_openscadA

Write an editable parametric OpenSCAD script (.scad) for a simple part.

Same parts as to_cadquery: a box/plate base with rectangular pads, holes and bosses along X, Y, or Z (countersink, draft, and chamfer only when the axis is vertical), one radius on the four vertical corners, a quarter-torus fillet on a vertical boss or hole (boss_N_fillet, hole_N_rim_r, hole_N_floor_r), a straight extrusion of a 2D outline, and a small boolean of those extrusions. Parameter names match the CadQuery script (hole_1_d, boss_1_h, fillet_r, boss_1_fillet, extrude_len, ...). Edit one and render in OpenSCAD. Returns the parameters, whether the openscad CLI verified the script against the mesh, and the measured deviation when it did. Without the openscad program the script is still written and verified is false. Returns an "error" key when the part is outside that vocabulary.

NameRequiredDescriptionDefault
tolNo
input_pathYes
output_pathNo

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does well: it discloses the return payload (parameters, verification flag, measured deviation), the degradation path when the openscad CLI is absent ('the script is still written and verified is false'), and an 'error' key for out-of-vocabulary parts. It does not say whether an existing file at output_path is overwritten or how long CLI verification may take, leaving some mutation semantics unstated.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the one-line purpose, then scope, then return behavior. The long parenthetical enumerations of features and parameter names are dense but largely earn their place by defining the accepted vocabulary; a bulleted split of scope vs. returns would read better than the single run-on block.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Returns are well covered despite there being no output schema, and the failure/verification paths are described. The gap is on the input side: for a file-generating tool with three undocumented parameters, an agent still lacks any statement of what input_path must point to (mesh? part file?) and what tol and output_path control.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% for three parameters (input_path, output_path, tol), and the description explains none of them; tol in particular is left undefined despite likely governing the deviation check. Worse, the phrase 'Parameter names match the CadQuery script (hole_1_d, boss_1_h, fillet_r...)' refers to generated model parameters, not tool inputs, which risks confusing it with the tol/input_path/output_path arguments.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and artifact: 'Write an editable parametric OpenSCAD script (.scad) for a simple part.' It also scopes the accepted geometry vocabulary and explicitly names the sibling to_cadquery as producing the same parts, so an agent can tell the two apart by output format.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied rather than stated: because it produces .scad and 'Same parts as to_cadquery', an agent can infer it is the OpenSCAD-target counterpart to to_cadquery/to_freecad. There is no explicit when-to-use/when-not guidance, no mention of convert, and no stated prerequisite such as the input mesh being a supported simple part beyond the vocabulary hint.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv0.19.0
    • Addededit_params
    • Addedlist_params
  2. 1 tool updatev0.16.0
    • Addedto_freecad
  3. 5 tool updatesv0.12.0
    • First observedanalyze
    • First observedconvert
    • First observedreport
    • First observedto_cadquery
    • First observedto_openscad

TDQS

B3.4/5.0

Scored across 8 tools

Disambiguation4/5

The two families are clearly split: convert produces a STEP B-rep by surface fitting, while to_cadquery/to_openscad/to_freecad emit editable parametric scripts for three distinct backends, and list_params/edit_params are obvious companions. The only mild overlap is analyze vs report, since both are read-only mesh analyses, though 'segments and lists surfaces' versus 'full Markdown fidelity report' distinguishes them.

Naming Consistency3/5

The set mixes three conventions: bare verbs (convert, analyze, report), a to_<backend> prefix group (to_cadquery, to_openscad, to_freecad), and verb_noun pairs (list_params, edit_params). Each name is readable and the groupings are intuitive, but there is no single predictable pattern an agent can extrapolate from.

Tool Count5/5

Eight tools is well within the sweet spot, and each one earns its place: one mesh-to-B-rep converter, three backend script generators, two script parameter operations, and two inspection tools. Nothing looks like filler or a redundant variant of another tool.

Completeness4/5

The core lifecycle is covered: analyze the mesh, convert to STEP, generate an editable script in three CAD ecosystems, then list and edit its parameters to re-render. Gaps are minor, e.g. no explicit verification/inspection tool for a written STEP file and no output-format choice for convert beyond STEP, but agents can work around these via edit_params or external tooling.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    B
    maintenance
    CAD-engineering MCP tool server for parametric modeling, DFM validation, mechanical calculations, and more. Enables code-CAD builds (build123d/CadQuery), model inspection, meshing, and mechanical calculators via MCP stdio.
    11
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents and software to create, edit, inspect, validate, and export CAD geometry through MCP using backend-neutral typed operations, with FreeCAD/OpenCascade as the authoritative B-rep backend and JSCAD preview.
    1
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to convert raw STL mesh files into production-grade STEP AP242 solid B-Rep models and compact semantic CAD Features JSON, with sub-second deterministic processing and high mathematical fidelity.
    28
    PolyForm Noncommercial 1.0.0