NativeCAD MCP server

Release notes

What changed in every nativecad-mcp release, straight from the project's own CHANGELOG — newest first, each version with its date, its downloads and their checksums. Link to a version with its anchor (#v0-27-0).

Current release

v0.27.0 “Hannigan” · October 28, 2026

Install from the command line: npx -y https://featureboard.ai/nativecad/releases/nativecad-mcp-0.27.0.tgz — drop the .mcpb into Claude Desktop or Cowork, or open the .plugin in a Claude chat. Every way in is on the For AI page; latest.json carries the same files with bytes and sha256 for a script to verify.

46versions
40with downloads
July 2026first entry

NativeCAD 0.27.x

v0.27.0 “Hannigan”

released October 28, 2026

sha256 nativecad-mcp-0.27.0.mcpb ad4458e6b4566ef42f1bb9d782ace60bd9d6973a4c8e6f043413e3e8a35f3bf9sha256 nativecad-mcp-0.27.0.plugin beff70c5bbe29303676a15d84b9b70af6a8a4639f8bae83a538879a9eb262aafsha256 nativecad-mcp-0.27.0.tgz d2a0adf4fbdc747e4723126deb47199f9ec15c4936147f3227314602edd9cb13

Scan to simulation: a room scan (.ply) in, an articulated model a robot can use out (cad_scan_to_sim). cad_import reads PLY meshes. cad_crease_repair mends B-spline faces that Parasolid-based CAD would split, and STEP export warns about them. cad_export_body writes QIF 3.0 inspection plans. Rotating zones and transient runs in cad_wind_tunnel. 187 tools (185 in 0.26.0): cad_scan_to_sim and cad_crease_repair are new. The command schema moves 1.68.0 → 1.71.0 (wind_tunnel rotatingZones and mode:'transient'; fit continuity and minSpan; the crease_repair op). Every 1.68.0 document validates unchanged.

New: cad_scan_to_sim

  • A LiDAR or photogrammetry scan of a room in, one assembly out. Input is a .ply point cloud or mesh, ascii or binary, in metres. A cloud over 100 units across is read as millimetres, and the result says so. The scan is levelled (gravity +Z) and squared to its walls. Floor, walls and ceiling are fitted, and thin panels are split into partitions, floor-anchored pilasters and hung doors, with the hinge side read from the pull. The remaining objects are clustered, recognised and fitted to parametric families. The first family set is a commercial restroom with toilet stalls: toilets, grab bars, tissue rolls, vanity and basins, soap and towel dispensers, waste bins and ceiling lights. An object no rule recognises stays in the model as a static box, never dropped.
  • Levels of detail L0–L4, each written as MJCF (MuJoCo), URDF and assembly.json:
    • L0: the room shell.
    • L1: the layout.
    • L2: articulated fixtures. Door hinges have a 90° stop, damping and friction. Seat hinges, spring-return flush levers and spinning rolls work, the paper tears at its perforations, and the waste bin is a free body.
    • L3: hardware, including a latch bolt that holds its door against the keeper.
    • L4: fasteners, which are visual-only in the simulator but count toward mass.
  • STEP: the highest level asked is also written as STEP, built by this kernel. Fixtures are exact B-reps: a lofted elongated bowl, a flushometer, undermount basins, a gooseneck faucet, swept grab bars, holders, a stainless bin shell. MJCF and URDF draw them as meshes. The toilet bowl collides as 100 convex pieces, so its cavity is open. Every part carries its material, mass, whether it collides, and its provenance: observed (fitted to points) or inferred (placed by an installation rule).
  • The room is closed with stated assumptions (complete:false skips this). An entrance door is placed and sized by the 2010 ADA Standards: a 36 in leaf on the front wall, clear of every fixture and of the 404.2.4 maneuvering space, with a lever at 38 in and a closer at ≤ 5 lbf that takes ≥ 5 s from 90° to 12°. Ceiling lights are assumed if none were seen. Each assumption comes back as a sentence in assumptions.
  • ada reports the room's first-look ADA dimensions, pass or miss with the measured value: door width, hardware and force, turning circle, compartments, grab bars, seat, paper dispenser, lavatory rim and knee clearance, mirror, reach ranges.
  • sim:true exercises every model in MuJoCo and PyBullet. It checks rest, door swing to its stop, the latch holding, the seat, the flush lever, the roll, a paper tear, a reach into the bowl, a bin push, and the entrance closer measured against the ADA limits. It needs a python with mujoco and pybullet (SCAN2SIM_PYTHON; on Windows, WSL Ubuntu-24.04:/root/simenv). A missing simulator is reported, not failed. The checker ships inside the bundle and is written to a temp file to run. importLevel loads that level's STEP into the session.
  • Fixture shapes are cached in $SCAN2SIM_SHAPE_CACHE (default <tmp>/nativecad-scan2sim-shapes); the first build of a fixture takes up to ~30 s.
  • New refusal codes:
    • E_INVALID_ARGS for a missing or non-.ply file, with the PLY reader's reason (PLY_NOT_PLY, PLY_NO_VERTICES, PLY_TRUNCATED, PLY_UNSUPPORTED).
    • E_GEOMETRY with details.reason SCAN_ROOM_INCOMPLETE: the scan gives no usable room. It must show the floor, the ceiling and at least two walls, at about 1 point per cm² or more. A scan thinned to one point in six has no ceiling plane.
    • SCAN_RECONSTRUCT_FAILED for any other reconstruction failure.
  • The sample scan on the showcase page (scan_tripod_sample.ply) is one point in two, 343,524 points, and reconstructs completely. The chest-mounted sample is thinned for viewing only.
  • Sparse scans and stall doors. On a sparse scan the door edge fit can latch onto a gap in the panel. A door whose edges cover less than half its opening now fills the opening between its pilasters, and a flag says so. Before this, the fit could produce a 12 mm door with no height, and its zero-height box made the executor refuse the whole STEP. A refused STEP document now reports the executor's reason in models.L<k>.step.firstError.

Importers

  • cad_import reads .ply meshes like STL and OBJ; toBrep works. A PLY with no faces (a point cloud) is refused PLY_POINT_CLOUD, and the message points at cad_scan_to_sim.
  • Faster Parasolid X_T tessellation. Long thin helical thread flanks triangulate along their long axis (CADSOLVER_NO_CDT_LONG_AXIS=1 turns it off). A ruled direction no longer gets a sag row at every trim vertex (CADSOLVER_NO_101050_RULED_SIDE_SKIP=1). Cylinder and cone covers pace their rows by the circle, not by a dense rim (CADSOLVER_NO_101051_COVER_ROWS=1). The McMaster thumb screw takes 8 s, down from 72 s. The 151-file X_T corpus takes 129 s, down from 250 s, with 4.39M triangles instead of 5.22M and no open-edge changes.

Freeform surfaces: G1 creases

  • New tool cad_crease_repair (op crease_repair). Each interior knot line of a B-spline face that breaks G1 is either smoothed or split into a real edge:
    • Smoothed: one control row moves to the C1 position, then one knot copy is removed exactly, when the move is within smoothTol.
    • Split: the face is cut into G1 sub-faces on the same surface, which loses nothing.
    • mode is auto, smooth or split. A faces filter is optional. The shell is audited before publish, and the whole repair rolls back on any failure.
  • Why it matters: a Parasolid-based reader (Onshape, SolidWorks, NX, Solid Edge) splits such a face, and a creased pillar became ~26,000 faces.
  • cad_export_body format:"step" checks interior G1 on the exported bodies. By default it warns, naming cad_crease_repair. Under strict:true or a handoff mark it refuses E_GEOMETRY export_interior_g1 and writes nothing. CADSOLVER_NO_KNOT_G1_CHECK=1 turns the check off.
  • checkFull gains two warnings:
    • interior_g1_discontinuity, once per creased face.
    • edge_off_face, for an edge that leaves a face it bounds by more than that face's own tolerance (CADSOLVER_NO_100990_EDGE_ON_FACE=1).
  • cad_help has a continuity topic. The freeform verbs report the continuity they built, and fits take a continuity target and minSpan (schema 1.70.0).

Export

  • cad_export_body format:"qif" writes a QIF 3.0 inspection plan from the document's authored PMI. The reply carries counts, byMethod, skipped, unmappedFaces, the unverified elements and the validation verdict. A document with no PMI is refused E_PRECONDITION and nothing is written.
    • For the shipped bundle, the QIF writer and the inspection plan load their modules by static path, print their CLI output only when run as a script, and read the measurement-method table as a staged asset. As first landed, they kept the server from being bundled at all.

CFD (cad_wind_tunnel)

  • Rotating zones and transient runs. rotatingZones: [{ bodies, axis, centre, omega | rpm, radius?, halfHeight? }] takes up to 16 zones, and mode:'transient' takes exactly one of duration or revolutions, with optional sampleEvery and dryRun.
    • Solver: zones are solved by overset grids on the GPU, with the CPU reference behind the LBM budget.
    • Output: each rotor gets cycle-averaged thrust, torque, power, CT/CQ/CP and FM (hover), plus phase-resolved series and a per-revolution convergence record.
    • Refusals: each is named: rotating-axis-degenerate, rotating-zone-overlap, rotating-zone-clips-static-body, rotating-transient-needs-gpu, rotating-zone-too-thin-for-overset, rotating-zone-near-boundary, and the rest of the set.
  • The LBM core runs past 8 GB. One population set is streamed in place, and 16-bit population storage halves the bytes per node (CADSOLVER_LBM_STORAGE = fp32 default, fp16, fp16c).

Booleans and meshing

  • A spline face's geometry no longer depends on cavities the cut never touched.
  • Plane-on-cone sections re-curve as exact ellipses and circles.
  • Chord-fin piece guards refuse only drops that cut a piece off.
  • Cut-body gates read each mesh's own chord deficit.
  • A half-kept face is never reported untouched.
  • Paired raw subtracts keep their cut walls exact.
  • Loft step handling follows the trend on swept lofts.
  • The SSI separation certificate tries the plane normal, so a grazing B-spline wall is proved empty in ~0.1 s.

Tool listing

  • The tools/list line moves from 396 KiB to 401 KiB (ceiling 403 → 408 KiB) for the two new tools. The listing is 408,955 B across 187 tools.

↑ all releases

NativeCAD 0.26.x

v0.26.0 “Graf”

released October 27, 2026

sha256 nativecad-mcp-0.26.0.mcpb ec3690e5edde19d43114c5cc8cc8d586c25e36d2dd9731b325d150cd98eab31bsha256 nativecad-mcp-0.26.0.plugin 64b45389d29896560a55dbc82b08d44d34de2dee29a6907a7f01bdfca7e51a74sha256 nativecad-mcp-0.26.0.tgz 362ef54887603cf966ca37cde6cb4290b27bc782e4a09a0661145b12a88be3d8

Fixes an extra body in exports after a curved boolean, present in 0.24.0 and 0.25.0. CATIA V5 parts now open as exact solids. Creo assemblies, binary ACIS .sab and Rhino 2 files open. STEP export writes pcurves on B-spline faces, and metre-unit files tessellate like their millimetre twins. 185 tools, the same as 0.25.0. The command schema stays at 1.68.0; two optional fields were added without a version move (import exactBrep, dfm_check machiningStockMm), so every 1.68.0 document validates unchanged. The import census now covers 23,347 user-facing files; 92 % of them opened at the census, before the later CATIA, ACIS and Rhino waves.

Fixed: an extra body in exports after a curved boolean (0.24.0, 0.25.0)

  • A general-engine union, subtract or intersect could leave a second live solid in the model (ops/genBool.js). The identity check builds the complement of the answer (A ∩ B for a subtract) to check the volumes. When that complement came from the answer's own classify and its trimmed faces were carried, the faceted complement body stayed live. Every exporter that writes all live solids then wrote it too: STEP, 3MF, GLB and mesh payloads. A sphere r16 minus a through-cylinder r5 wrote two MANIFOLD_SOLID_BREP: the exact ring (14,706.407 mm³) and a 2,447.09 mm³ faceted A ∩ B. The leak came in with 2026-10-20, so it is in 0.24.0 and 0.25.0.
    • The builder now retires every solid it made and does not return, including when it returns nothing or throws. The published solid and the identity record (complementSource, volumes, deltaPct) are byte-identical to 0.25.0. Only the stray body is gone.
    • A session saved by 0.24.0 or 0.25.0 can still hold the stray body. Rebuild the model, or find the extra body with cad_list_model_state and remove it with cad_delete_body, before exporting again.
    • CADSOLVER_KEEP_BOOL_INTERMEDIATES=1 (a debugging switch) still keeps the intermediates.
  • A boolean's retired intermediates are purged when the op ends (ops/retiredPurge.js). A rim union's document went from 134,532 entities to 2,376. Fillet-registry faces are kept, so a later re-fillet still finds them. CADSOLVER_NO_OP_PURGE=1 turns it off.

Importers (cad_import)

  • CATIA V5 .CATPart opens as an exact solid (importers/CatiaCgm.js, CatiaBrep.js). The B-rep is decoded from the part's own CGM stream (clean-room, from file bytes only). A part opens as source:'catia-cgm', brep:true only when its solid closes and agrees with its own display mesh. Otherwise it opens as the display mesh, as in 0.25.0, with the reason on brepSubCode.
    • Results: 251 of the 306 CATParts in the census ledger are exact (0 in 0.25.0), and 19 of 20 NIST CATParts. Across all 2,659 CATIA rows in the census, 1,571 are exact.
    • New import field exactBrep (default true). exactBrep:false gives the display mesh as before.
    • New brepSubCode values: CATPART_BREP_DISPLAY_MISMATCH, CATPART_BREP_DISPLAY_UNAVAILABLE, CATPART_BREP_EDGE_GAP, CATPART_BREP_NO_SOLID, CATPART_BREP_PCURVE_UNSUPPORTED, CATPART_BREP_SHELL_INCONSISTENT, CATPART_BREP_SHELL_OPEN, CATPART_BREP_STREAM_UNDECODED, CATPART_BREP_SURFACE_UNSUPPORTED, CATPART_BREP_TOPOLOGY_UNDECODED and CATPART_BREP_VERTEX_UNDECODED. CATPART_BREP_CGM is unchanged.
    • The decoder's alternative readings are budgeted by counted work, not wall-clock time, so a part opens the same way on a loaded machine. V5R14 to V5R18 display meshes whose strip lengths are in the index width now decode (90 of 107 that refused CATPART_DISPLAY_UNDECODED). .CATProduct component placement is unchanged.
  • Creo assemblies (.asm) open with their components placed (importers/CreoAssembly.js). Components resolve by name and highest version from the assembly's folder, and sub-assemblies compose. searchPaths is now accepted for .asm. 216 of 369 corpus assemblies open (none in 0.25.0).
    • A refusal keeps CREO_ASSEMBLY_NO_GEOMETRY, with the new subCodes CREO_ASSEMBLY_COMPONENTS_MISSING, CREO_ASSEMBLY_COMPONENTS_UNBUILT, CREO_ASSEMBLY_EMPTY and CREO_SECTION_SCRAMBLED. A placed component that does not close is left out and named.
  • Creo parts: cyl_splsrf (threads and springs), b_splsrf, and splsrf with tan_cond [3,3] are read. Faces stored under ActDatums are read, which accounts for most of CREO_SHELL_OPEN. Construction faces (cosmetic threads, sketch regions) are left out of the solid instead of refusing CREO_TOPOLOGY_BROKEN. Stored mass properties older than the geometry are no longer used. New refusals: CREO_SECTION_SCRAMBLED and CREO_FACETED_ONLY; an empty VisibGeom now refuses CREO_NO_SOLID_GEOMETRY. 1,400 of 1,873 corpus .prt open (1,213 at 0.25.0).
  • NX .prt from before NX 11: OLE2 parts that keep an uncompressed Parasolid transmit in /UG_PART/UG_PART open. An OLE2 part whose /part/arrangements names an assembly says so. CADSOLVER_NO_NX_OLE2_RAW=1 turns it off.
  • ACIS (importers/SatProcedural.js):
    • Binary .sab is now an import extension (read through SatReader). It was refused as foreign in 0.25.0.
    • Procedural sumsur faces are rebuilt as exact NURBS, and parametric summary curves are refitted. ACIS 27 records are read. Wire-only files open as wire bodies. Loopless sphere faces build as two hemispheres.
    • New refusals: SAT_EMPTY_MODEL (a header-only healer output), and SAT_UNSUPPORTED_SURFACE for a lemon torus (negative major radius).
    • Results: .sat opens 955 of 981 (825 before), and .sab 2 of 2. Kill switches: CADSOLVER_NO_SAT_PROCEDURAL, CADSOLVER_NO_SAT_WIRES.
  • Rhino .3dm: Rhino 2 B-rep, curve and point records, the 1.2 extrusion layout, and older edge, plane and mesh layouts are read. A mesh-only file opens as a display mesh, and a curve-only file opens as wire bodies instead of throwing. New refusals: RHINO_EMPTY_MODEL, RHINO_POINTS_ONLY, RHINO_ANNOTATION_ONLY and RHINO_INSTANCE_REFS_ONLY. 900 of 937 corpus files open (730 before), 501 of them exact (483 before).
  • The import cache keys on the environment. Every CADSOLVER_* variable is part of the key, so a flipped switch no longer replays an earlier result.

Booleans

  • Butted coaxial pipe runs union exactly (ops/capButtUnion.js). Two solids that meet on one opposed, coincident cap pair are glued on that cap. Before, the union re-tiled the tangent walls as 612 facets: 890 faces, geometryDegraded. Now it gives 16 exact faces with volume = vol(A) + vol(B). New refusals: capbutt_extra_contact and capbutt_not_separated. CADSOLVER_NO_100893_CAP_BUTT_GLUE=1 turns it off.
  • A subtract carries the cut cylinder or cone exact beside an untouched cylinder. A blind r9 bore in an R323 wall removed 1,240.379 mm³ against a closed form of 1,009.898. It now removes 1,018.215 at the default mesh and 1,008.471 at chord 0.002.
  • Near-tangent results: the near-tangent sphere intersect is proven EMPTY again, and the matching subtract refuses no_material_removed instead of publishing a 0.0017 mm³ cut.
  • Pierce:
    • ELLIPSE edges are sampled on the curve, so a cap that crosses its face imprints instead of leaving two coincident faces.
    • Pierce no longer publishes a multi-lump union as one solid.
    • The 32-plane pierce bound now refuses pierce_plane_count. Before, it refused non_parallel_pierce, which still names a real chain failure.
  • Curved trims are re-fitted in more cases:
    • PLANE | B-spline chord chains. Louvred spline endplates now have an exact volume in 45 of 45 cases, 18 before. That volume agrees with OCCT to 3.7e-5.
    • Shallow cone/cylinder crossings.
    • Sparse and shallow chains are densified, and fitted-edge mesh samples are snapped onto both carriers.
    • A crease guard keeps tangent loft seams on their chords. A wing-panel bore that 0.25.0 refused boolean_under_removal now builds.
  • Also fixed:
    • A second lug on a revolved shell's seam plane unions exact and watertight.
    • A plane cutting a cylinder mid-height is no longer taken for a cap rail.
    • The profile hole/outer touch tolerance in extrude is applied.
  • Fillets:
    • The mixed tangent-chain rung is kept only when it rounds at least as many edges as the ordinary path. nist_ctc_01's all-edges r0.5 fillet rounds 88 edges again, after 69 in 0.25.0.
    • Fillet and blend results with B-spline faces report the quadrature volume, like their operands.

Tessellation

  • Metre and other declared-unit files tessellate like their millimetre twins. Five millimetre constants were applied in model units, so on a metre file they were 1000 times too large or too small:
    • the sliver-edge rule. The P424 pillar's outer skin is back on OCCT: 2,862,476 against 2,862,587 mm³.
    • the CDT input snap. P424 wing tips mesh watertight without repair fill.
    • the 0.005 mm spline sag floor.
    • the weld radius.
    • the arc closure and coincidence constants.

Millimetre and unit-less files are unchanged.

  • C0 knots are grid lines and edge samples. A polyline-ruled B-spline face no longer chords across its creases: 0.776 mm off the surface before, 5.7e-13 mm now. Meshes of such faces are larger.
  • Measurements get their own knot-grid budget. Shell gaps and checkFull no longer depend on how often the document was tessellated before.
  • Other fixes:
    • nist_ctc_04 winding conflicts 43 -> 0.
    • Spline grids no longer mint sites the CDT drops, and hole walls no longer mint sites the periodic cover drops.
    • A cone's fan apex no longer drives 485 false refinement points; the wall had read 233.05 mm³ against an exact 261.80.
    • Sphere bands between two latitude rings take the 0.01 mm sagitta rule. A napkin ring reads -0.75 % instead of -1.43 %.
    • Floored cone walls refine the same on both mirror-image halves.
    • Sliver repair in the solid FEA mesher matches the whole-mesh result in about 60 % of the time.
  • checkFull and the STEP void split report shell_gap_below_tolerance when two shells of one solid are closer than the tolerance, measured on the B-rep. A closed spline-loft cavity now splits into BREP_WITH_VOIDS.

STEP export

  • PCURVEs on B-spline faces (packages/interop/src/exporters/stepPcurves.js). OCCT-based readers (gmsh, FreeCAD, build123d) no longer have to project every edge onto its NURBS face; on one flap that projection missed by up to 4.4 mm.
    • Boundary iso-line edges and interior trims each get a (u,v) pcurve, written only when it maps onto the 3-D edge within 1e-7 mm. On a chord-corner flap, OCCT's InvalidCurveOnSurface count went from 136 to 18.
    • Edges spliced verbatim from an imported file are left alone, so an unchanged round trip is byte-identical.
    • CADSOLVER_NO_STEP_PCURVE=1 writes the old bytes. CADSOLVER_NO_100910_INTERIOR_PCURVES=1 turns off only the interior trims.
  • Metre exports declare the tolerance in metres: UNCERTAINTY_MEASURE, the .tol.json sidecar and the in-file tolerance block. A shelled B-spline tube no longer re-reads with coincident end caps.

Modelling

  • Spline lofts (ops/loftSpline.js):
    • Station data that steps (a ply drop) becomes a linear ramp on the smooth cubic, instead of a cubic that rings through thin walls. This is reported in analytic.vFit (loft_spline_v_steps).
    • A G1-forced knee break whose fit bulges off its data is released to a corner. This is reported in sectionFit.g1Released (loft_spline_g1_bulge_released).
    • An auto corner that fades along the loft is carried to every station, by vertex index or by segment position, instead of refusing loft_spline_segment_count_mismatch. This is reported in sectionFit.cornerUnion (loft_spline_corner_union).
    • Smooth sections cut into faces by caller breaks keep G1 at the breaks.
    • Lofts that none of these apply to are byte-identical.
  • Glazing hostMode:'cut' on a planar host cuts with an exact prism. On a box test it was refused at every fitTol but 4 and 5; it now builds at every fitTol with the exact pane and host volumes. A fitTol above 0.1 is now only a rescue. New refusals: glazing_cutter_failed and glazing_cutter_overlap. The cutters are listed in report.cuts[].

Tools and analysis

  • cad_dfm_check declares machiningStockMm for cold spray, metal jetting, MoldJet and metal DLP. Before, the closed schema rejected it.
  • Metal printers: 18 LPBF/EBM printer records gain vendor-sourced build volumes. Four records with no findable source stay null.
  • Refusal remedies:
    • 36 rows for the cad_split_* refusal vocabularies.
    • Rows for fastener-proud-head-on-oml and mesh_to_brep_timeout.
    • A row for freeform_sheet_not_closed.
  • Self-update: update:"now" now bypasses staging, channel and the skipped list as documented. Before, the install gate refused on the rollout before the bypass was reached. A halt still blocks.

Fixed reported issues

  • update:"now" could not bypass staging, channel or the skipped list, because the install gate checked the rollout first.
  • A second planar lug on a revolved shell was fixed before 0.25.0, but a tessellator change in 0.25.0 left the first union not watertight. That union is now exact and watertight again.
  • The second general-engine union on a duct shell no longer fails. The fix shipped before 0.25.0; a regression test landed in this range.

↑ all releases

NativeCAD 0.25.x

v0.25.0 “Glover”

released October 25, 2026

sha256 nativecad-mcp-0.25.0.mcpb dbd80f6c32ec665799f9d4fbe90b2550201464415999e138c1428b83ee4b2ebbsha256 nativecad-mcp-0.25.0.plugin f2fcc1cf3a340b471d9dc4d785e503ae50e56966b88c87c78acf34fe9984749esha256 nativecad-mcp-0.25.0.tgz fd0d23db3cf94ff67bea5b7edfe9588b3b9f676e76bfd208fece5782fc26e6ba

Creo parts open as exact solids, and CATIA V5 parts and products, Inventor assemblies, JT and Rhino .3dm files import. SolidWorks parts are checked against their own display lists. Extrude and pad gain end conditions and taper, sketches keep their constraints, cad_wind_tunnel gains a compressible regime, and a strict STEP export is gated on B-rep validity. 185 tools, one more than 0.24.0 (cad_turbomachine). The command schema moves from 1.65.0 to 1.68.0. 1.66.0 adds wind_tunnel regime + compressible, 1.67.0 adds extrude extent / operation / of and pad extent, and 1.68.0 adds sketch_end constraints. Every document valid at 1.65.0 validates unchanged and builds the same geometry. The whole-corpus census opened 15,287 of 17,120 user-facing files (89.3 %). A later full pass at 27bbf90c counts 16,962 open and 462 failing.

Importers (cad_import)

  • Creo / Pro/ENGINEER parts open as exact solids (packages/interop/src/importers/CreoBrep.js, CreoRecords.js). The B-rep is rebuilt from PTC's own VisibGeom records, in both the text and the binary encodings. The test holds a fixed list of NIST parts to their STEP twins within 0.5 % volume and bounding box. Curve-defined tab_cyl / ruled_srf / srf_rev surfaces and the rolling-ball fillet_srf are decoded. PRT_CREO_UNSUPPORTED changes meaning: it now refuses only a part whose B-rep does not build, and it carries a CREO_* subCode such as CREO_SHELL_INCONSISTENT, CREO_SHELL_OPEN, CREO_TOPOLOGY_BROKEN, CREO_TRUNCATED, CREO_RECORDS_UNDECODED, CREO_NO_SOLID_GEOMETRY, CREO_NO_VISIBLE_GEOMETRY, CREO_NO_DISPLAY_DATA, CREO_UNKNOWN_CONTAINER, CREO_ASSEMBLY_NO_GEOMETRY or CREO_SURFACE_UNSUPPORTED_FILLET_SRF.
  • CATIA V5 .CATPart opens as a display mesh (importers/CatiaV5Import.js, importers/CatiaCgrDisplayMesh.js). The embedded CGR scene graph is decoded, and the result is source:'display-mesh', brep:false. 247 of 262 CATParts with display data open (204 closed). The 19 NIST CATParts with STEP twins match the bounding box exactly or within 0.023 % of the diagonal, with a median volume difference of 0.024 %. CGR vertex layout 2 now decodes as well. Multi-segment streams are read with the correct 20-byte segment entries. New codes: CATPART_BREP_CGM, CATPART_DISPLAY_NO_TESSELLATION, CATPART_DISPLAY_UNDECODED, CATPART_NO_DISPLAY_DATA, CATIA_TRUNCATED and CATIA_UNKNOWN_CONTAINER.
  • Assemblies open with their components placed:
    • Inventor .iam (importers/IamImport.js) and CATIA .CATProduct (importers/CatProductImport.js) are new. searchPaths is now also accepted for .iam and .catproduct. New codes: IAM_COMPONENTS_MISSING, IAM_COMPONENTS_UNREADABLE, IAM_COMPONENT_NO_BODY, IAM_CYCLE, IAM_NOT_COMPOUND_FILE, IAM_NO_COMPONENT_REFS, IAM_NO_RSE_STORAGE, IAM_NO_SOLID, IAM_OCCURRENCE_MAP_UNDECODED, IAM_TOO_DEEP, CATPRODUCT_COMPONENTS_MISSING, CATPRODUCT_COMPONENTS_UNREADABLE, CATPRODUCT_NO_GEOMETRY and CATPRODUCT_STRUCTURE_UNDECODED.
    • A .sldasm with some components missing now opens as a partial assembly. Before, it was refused. The missing parts are listed on imported.assembly.missingComponents and in an XA_COMPONENTS_MISSING warning. XA_COMPONENTS_MISSING now refuses only when no component resolves.
    • Components missing on disk are taken from the assembly's embedded ImportedComp / VirtualComp documents. Across 452 corpus SLDASM files: ok 71 -> 85, XA_COMPONENTS_MISSING 367 -> 351, solids 325 -> 1939. storedFallback.reason: 'XA_STORED_UNPLACED' names a stored fallback.
    • A component that resolves but does not build makes the result partial with XA_COMPONENTS_UNBUILT. An assembly box larger than its placed components gives XA_ENVELOPE_SHORT. Later fixes take partial results from 47 to 45.
  • Siemens JT and Rhino .3dm are new formats:
    • JT (importers/JtImport.js, XzDecoder.js): XT B-rep segments become exact solids through the Parasolid reader. zlib and LZMA/.xz blocks both decode. JT 9.x TopoMesh display LODs decode as well. New codes: JT_BAD_TOC, JT_NOT_JT, JT_NO_BREP, JT_NO_GEOMETRY, JT_SEGMENT_CORRUPT, JT_TESSELLATION_UNDECODED, JT_TRUNCATED, JT_VERSION_UNSUPPORTED, JT_XT_NOT_FOUND, JT_XT_UNREADABLE, XZ_BAD_HEADER, XZ_BAD_MAGIC, XZ_FILTER_UNSUPPORTED, LZMA_BAD_DATA, LZMA_BAD_PROPS and LZMA_TRUNCATED. A stand-in .jt is now refused at build time as E_GEOMETRY / JT_TRUNCATED, no longer E_INVALID_ARGS at validate time.
    • Rhino .3dm (importers/Rhino3dmImport.js): ON_Brep and extrusions import as exact B-rep, closed ones as solids. Meshes import as meshes, and the units come from the settings table. New codes: RHINO_NOT_3DM, RHINO_LEGACY_V1, RHINO_LEGACY_V2_GEOMETRY, RHINO_NO_GEOMETRY, RHINO_SUBD_UNSUPPORTED, RHINO_TRUNCATED, RHINO_BAD_BREP, RHINO_BAD_CHUNK, RHINO_BAD_CURVE, RHINO_BAD_MESH, RHINO_BAD_SURFACE, RHINO_FACES_DROPPED, RHINO_UNSUPPORTED_CURVE, RHINO_UNSUPPORTED_SURFACE and RHINO_ERROR.
  • SolidWorks .sldprt:
    • A part with no Parasolid body now opens as its display mesh (source:'display-mesh', brep:false) instead of refusing (importers/SldprtDisplayMesh.js). import {displayMesh:false} keeps the refusal. On 370 parts with both a B-rep and a display mesh, the mesh bounding box is within 0.5 % on 94 % of parts and the volume within 1 % on 83 %. "STL Graphics" feature meshes open the same way. New codes: SLDPRT_DISPLAY_NO_TESSELLATION, SLDPRT_DISPLAY_NO_GRAPHICS and SLDPRT_DISPLAY_TOO_LARGE.
    • SolidWorks 2001-2004 PKWARE-imploded streams decode (DclExplode.js). Body-less parts refuse with the new sub-codes SLDPRT_EMPTY_PART, SLDPRT_EMPTY_PART_GRAPHICS and SLDPRT_SKETCH_ONLY. All 4,874 corpus sldprt were re-imported with zero regressions.
    • The import is held to the part's own display list. A stale display list warns stale-display-list, and a disagreeing one warns display-mismatch; neither refuses. Partition bodies the current display list does not draw are dropped (: DISPLAY_LIST_ABSENT_BODY, DISPLAY_LIST_HIDDEN_BODY). The partition outranks a larger body-store snapshot. Without a configuration argument, an import that disagrees with the display list now tries the other configurations and picks the one whose extent fills the drawn box. A fallback stream that disagrees with a current display list is flagged fallbackDisplayMismatch (SLDPRT_FALLBACK_DISPLAY_MISMATCH). Bodies drawn but missing from the partition are recovered from the body store (5 parts, 49 bodies).
  • Parasolid .x_t / .x_b (XtReader.js, importers/ParasolidImport.js):
    • Old schemas (SCH_1012 to SCH_11xxx and pre-v6) read: 124 of 131 refused files now import, 109 of them watertight. The pre-v6 B-surface and B-curve families are added, taking x_t ok from 143 to 145.
    • Periodic NURBS stored with the full knot vector are seamed (xtPeriodicSeam.js). Full-knot sldprt rows that open go from 34 to 44, and XT_PERIODIC_FACE_ACROSS_SEAM from 32 to 22. New sub-codes: XT_PERIODIC_FACE_ACROSS_SEAM and XT_PERIODIC_SEAM_NOT_CLOSED (under XT_SEAMED_FACE), and XT_PERIODIC_CURVE_NOT_CLOSED (under XT_BAD_PARAMETERS). Analytic cylinder, cone and torus faces that use their seam edge twice are kept whole or split.
    • Blend surfaces on cones, swept, spun and offset supports build. Blend extensions now have an evaluation budget and a 60 s wall-clock budget: Xbox goes from over 150 s to 22 s (a named refusal), and Black_Decker from over 150 s to 32 s.
  • STEP:
    • Sheet-only files sew into solids (ops/sheetSolidify.js): 30 of 100 sheet-only STEP files open. New codes: STEP_SHEET_CLOSED_NOT_WATERTIGHT and STEP_SHEET_NEAR_CLOSED. A file too large for the heap now refuses STEP_TOO_LARGE_FOR_HEAP instead of aborting. With this release, 17 of 21 STEP sheet rows publish solids (0 before).
    • Curve-only STEP, IGES and .3dm files open as wire bodies, and AP242 tessellated-only files open as a mesh body (importers/wireBodies.js). Before, these were refused STEP_WIREFRAME_ONLY, STEP_NO_BODY_ENTITY, IGES_NO_GEOMETRY or RHINO_NO_GEOMETRY.
    • PMI, validation properties, layers, supplemental geometry and saved views are stored on the imported model. DRAUGHTING_PRE_DEFINED_COLOUR resolves to RGB. A second buildBRep() no longer resurrects a withdrawn solid.
  • IGES (packages/interop/src/IgesReader.js):
    • The import command publishes the closed lumps of a mixed-lump file as solids and the open rest as one sheet, IGES_STITCH_PARTIAL. A pinned stitchTol or CADSOLVER_NO_IGES_LUMP_SPLIT=1 keeps the single open sheet.
    • Entities 192/194/196/198, 104 forms 2/3 and 118 are read. A 144 that asks for the natural boundary of one of those surfaces refuses IGES_UNBOUNDED_SURFACE. In the lane's run, 152 more files open (642 -> 794 of 980).
    • Curve-only IGES opens as wire bodies by default; wireframe:false restores IGES_NO_GEOMETRY. Entities 112 and 130 are exact.
  • ACIS SAT/SAB: 79 of 81 harvested files build, from 19. Uniform-scale transforms, subtype refs and pcurves are read.
  • Byte coverage: every reader's result and refusal carries a whole-file byte ledger (byteCoverage, with inModelBytes / droppedBytes). CADSOLVER_NO_BYTE_COVERAGE=1 turns it off.
  • Parasolid (.x_t inside.sldprt) charts: coarse INTERSECTION polylines are re-read against their own edges, so a speaker housing that read +3.4 % over its display list no longer fills its B-spline faces with repair patches; apple/lemon torus faces a few 1e-6 off the outer lobe are no longer refused as trimming into the inner lobe.

Tessellation

  • cad_export_body with maxTriangles on a single-level export now honours the budget. Before, a blended prism could not go below 13k to 24k triangles. lod.bandAspectRelaxed reports when the second budget stage was needed. An export without maxTriangles is unchanged.
  • Imported-solid volumes checked against their display meshes (follow-ups). Examples:
    • Keycap pockets: +3.2 % to +6.5 % were fixed.
    • Sphere zones meshed as their caps and torus bands were fixed.
    • A full-knot helical spring that came out about 27 % thin now reaches at least 950 mm³, against about 984 exact.
    • Thread run-outs: open edges 33 -> ≤3.
  • Other measured fixes:
    • nist_ctc_02 winding conflicts 236 -> 0, now -0.008 % against the file's own VOLUME_MEASURE.
    • A P424 R362 skin read 1.86M against 2.33M mm³ at default tolerance; chord 0.1, 0.01 and 0.003 now agree.
    • A kinked cubic loft is within 0.01-0.02 % of OCCT.
    • Worst sag on a self-crossing ring dropped from 0.694 to 0.0087 mm.
    • The weld radius is capped for thin bodies and anchored to the body, not the world origin.
  • Faster: unchanged results with less time, from repair, CDT and gate fast paths and from knot-grid refinement budgets (Xbox refinement 27 -> 15 s).
  • Rotated tori at coarse chord are watertight and within 8 % of exact; at chord ≥ 7 they were refused tess_repair_volume_drift at about 12 % of true volume.
  • Coarse display meshes follow the tolerance (opt-in coarseDisplay): a 2-face sphere 552 -> 120 triangles, a 3 m x 4 mm pendant 1,556 -> 184. Default meshes are unchanged.
  • A dense surface-surface intersection trace is no longer collapsed by chain decimation: a strut leg folded into shaft and barrel went from 2,044 facets and a refusal to 18 faces. A stretched-wall union now converges with chord.

Booleans

  • Local general booleans (ops/genBoolLocality.js) chart only the faces that meet the other operand. The P424 main-plane Ø6 hole was refused with 190 gate errors and now builds. The P424 flap builds checkFull-clean; it had 260 errors and took 149 s. The identity check reads B-spline faces against their own surface and imprints into a window of span-wide skins (ops/genBoolWindow.js). A carry backstop that declined every carry now reads like for like.
  • Curved trims re-authored as B-spline edges (ops/curvedTrimRecurve.js): 261 LINE chords between a bore and a curved skin become fitted edges (< 3e-5 mm). All 14 P424 main-plane holes cut certified. A second bore no longer inherits chord tolerances.
  • Impeller blade into hub: the union was refused and now gives 130 faces. The blade-into-hub carry gave 4,557 faces (4,522 PLANE) and now gives 41. An 8-blade chain grows by 13 faces a blade, watertight, with retained heap 422-598 MB. Before, the chain ran out of memory at blade 5.
  • Hollow (BREP_WITH_VOIDS) operands no longer refuse non_positive_lump / non_connected_operand for a tool inside the cavity.
  • Tube joints and tube nets (ops/tubeJoint.js): tube-on-tube unions in the union ladder, tubeJoint: 'weldOn' | 'drilledThrough', non-parallel posts, through-crosses and bar-to-bar joints. A 27-member jungle gym builds in about 1.1 s. New refusals: tube_joint_bar_cross and tube_joint_no_seam_room.
  • union order:"overlap" (opt-in): a 50-body frame goes from 31-40 s to about 9-12 s.
  • Now builds:
    • A flush-top union of two bored blocks, which was refused unsupported_overlap.
    • A tube_sweep ∪ hub, which was rolled back non_manifold_result.
    • Radial cylinder-into-cylinder drills stay on the analytic rungs instead of refusing cylcyl_base_not_coaxial.
    • A larger coaxial bore cut as a second subtract.
    • Dense teardrop helix subtracts.
    • A T-junction bore through a cross-pin bore.
  • Pierce no longer depends on position: verdicts are identical at x = 0, 1e3 and 1e5.
  • A genBool refusal reaches the caller: fallbackReason is never empty and names ops/genBool.js and its code.
  • Kernel API: a multi-lump union's id is a lump set (ops/lumpSet.js). Kernel subtract, union and intersect take the set, or an array of ids, as every lump. A stale set refuses multi_lump_operand_stale. The document executor still addresses lumps itself (lumpSet:false).
  • Small drills: an imprint that is not on the crossed face now refuses IMPRINT_UNSUPPORTED; before, it passed on NaN. Ø0.3 and Ø0.1 drills in inch-numbered blocks build.
  • More late fixes:
    • A near-flush union rung (ops/flushSnap.js) snaps the tool onto the base plane when every other rung refuses.
    • One-break closed spline-loft sections take through-cylinder subtract, union and intersect at every break index; 11 of 40 failed.
    • A real intersect result smaller than the volume identity's noise floor publishes behind an independent certificate instead of refusing bool_identity_violation.
    • Carried plane rims go back on their circles: exact elbow volume. The pierce-swapped rung declines early on an edge in a curved face's split plane.
    • A genuinely under-removed cut that the base-spread allowance forgave is now refused boolean_under_removal.

Modelling

  • Extrude and pad end conditions (ops/extrudeExtent.js, schema 1.67.0):
    • extent {type: blind | symmetric | two_sides | through_all | up_to_next | up_to_face | up_to_body, depth2, face, body, offset, taper, taper2, flip}.
    • operation: new | join | cut | intersect with of, whose results equal the separate booleans.
    • pad gains up_to_* extents. A taper builds exact PLANE and CONICAL_SURFACE walls.
    • New refusals: EXTRUDE_TAPER_COLLAPSE, EXTRUDE_TAPER_SHARP_ARC_CORNER, EXTRUDE_TAPER_CURVE_UNSUPPORTED, EXTRUDE_UPTO_FACE_NOT_PLANAR, EXTRUDE_UPTO_FACE_NOT_PARALLEL, EXTRUDE_UPTO_FACE_NOT_REACHED, EXTRUDE_UPTO_FACE_NEEDS_BODY, EXTRUDE_UPTO_NEXT_NOT_FOUND, EXTRUDE_UPTO_NEXT_CURVED_FACE, EXTRUDE_UPTO_BODY_NOT_REACHED, EXTRUDE_UPTO_OFFSET_FLIPS and EXTRUDE_THROUGH_ALL_NO_MATERIAL.
    • New validator codes: EXTRUDE_EXTENT_FIELD_CONFLICT, EXTRUDE_EXTENT_FIELD_MISSING, EXTRUDE_OPERATION_NEEDS_TARGET and PAD_EXTENT_NEEDS_EXTRUDE.
    • cad_extrude_profile and cad_pad are now listed top-level-only: extent's nested shape comes from cad_help({tool}), and the server still validates the full schema. A document without the new fields writes byte-identical STEP.
  • Sketch constraints persist (schema 1.68.0): sketch_end takes constraints, the geometric constraints and driving dimensions, solved at sketch_end so that an edited value re-solves on rebuild. New refusals: SKETCH_CONSTRAINT_CONFLICT (naming the constraint), SKETCH_CONSTRAINT_UNSATISFIED, SKETCH_CONSTRAINT_INVALID and SKETCH_CONSTRAINT_UNSUPPORTED. Unconstrained sketches build byte-identical STEP.
  • Fields the executor silently dropped are now forwarded or refused by name: loft vSpans (loft_vspans_invalid, loft_vspans_needs_spline), glazing fitTol (glazing_fit_tol_invalid) and revolve inline splines (revolve_inline_splines_invalid, revolve_inline_splines_conflict).
  • mesh_to_brep route "quad" (ops/meshQuadPatch.js, meshQuadLayout.js, meshQuadAnalytic.js): an organic mesh becomes a few-face bicubic B-spline solid. The quad options keep exact planes and cylinders on the call's interfaces / cuts. On the lattice-bracket test all 5 bores come back as CYLINDRICAL_SURFACE; before, all 714 faces were B-spline patches. mesh_to_brep refuses mesh_to_brep_timeout within its wall budget.
  • Blends:
    • ease_edges fallback:"partial".
    • Blanket blends round mixed line/arc chains and shallow saddle rims.
    • Skew-cylinder and mixed-chain bands.
    • Face blends on cylindrical side faces stay exact cylinders.
    • Swapped ellipse-setback corners close.
  • Fasteners:
    • New kinds: csk_iso7046, csk_iso10642, csk_raised_iso7047, pan_iso7045 and connector_bolt.
    • New drive field: pozidriv | phillips | hex | torx | none.
    • import {wetted:true} tags a body as OML, and a fastener on it may omit kind (the head defaults to csk_100).
  • Turbomachinery: blade gains rotor (ops/turbo/rotor.js). Closed (shrouded) impellers build without the shroud boolean, lifting rotor_shroud_union except for closed impellers with splitters (ISO 2858 80-50-200 in about 3.7 s).
  • Heal and check: heal keeps BREP_WITH_VOIDS bodies solid and adds an untangle stage. checkFull reports planar_outline_self_intersecting from the true curves and dismisses crossings between provably disjoint surfaces.
  • Glazing reports per-pane folding (panes[].folded, flippedTriangles, report.foldedPanes) and rebuilds a folding 4-corner pane from its other corner readings.
  • Lattices:
    • lattice_fill exchanger ports may be local discs, several to a face. New refusals: tpms_exchanger_port_overlap and tpms_exchanger_port_spacing.
    • Rib and corner relief no longer leave consumed solids live.
  • Spline lofts align section start points: sections starting at different vertices folded the skin (-15 to -68 % volume); start points are re-chosen by least twist and reported in sectionFit.startAlignment. Descending stacks build outward.

Simulation

  • cad_wind_tunnel regime:"compressible" (packages/cfd/src/compressible/, schema 1.66.0): a mesh-free compressible solver around a body or an imported mesh. Inputs go in compressible (mach 0.3-25, altitude or pressure + temperature, model euler / laminar / SA). The report gives the coefficients, the shock standoff against Billig 1967, and validity.knownShortfalls (the Mach 3 cylinder standoff is +7.6 %). New reason codes: compressible-mach-range, compressible-altitude-range, compressible-freestream-conflict, compressible-gamma-range, compressible-model-unsupported, compressible-gpu-unavailable, compressible-cell-budget (E_LIMIT, 2,000,000 cells), compressible-under-resolved, compressible-bad-mesh, compressible-field-not-read, compressible-grid-invalid and compressible-diverged.
  • The incompressible tunnel's default numbers move:
    • The default upstream padding goes from 1.5 to 3.0 body spans.
    • k / ε / ω are transported.
    • The far field defaults to 'auto' (slip walls when the box allows).
    • A voxel is solid when its centre is inside.
    • The sphere at Re 5e4 now reads Cd 0.4684 / 0.4910 / 0.4974 / 0.4950 on 5 / 6 / 8 / 12 cells/D, in the 0.40-0.50 band; before, it converged near 0.518. The old behaviour is reachable through bc.solver (turbulenceTransport:false, farField:'open', voxelFill:'legacy').
  • cad_fea_case on a field body (cad_lattice_fill output "field") is solved in-process by the homogenised lattice path, never meshed whole. New codes: field_body_no_skin (E_PRECONDITION), field_body_source_missing (E_NOT_FOUND) and field_body_case_unsupported.

Tools

  • New tool cad_turbomachine (ops/turbo/turbomachine.js): kind is pump, inducer, compressor or turbopump; you give a duty, and it returns the meanline, curves or map, checks and a STEP assembly imported to the session. turbine and turbocharger are routed and refused E_UNSUPPORTED with E_NOT_IMPLEMENTED. options.build:false returns in milliseconds; a build takes 20-25 s. See cad_help({topic:"turbomachine"}).
  • cad_design_knowledge action:"laminate" (ops/compositeLaminateAnalysis.js): a classical-laminate calculator (thickness, areal mass, A/B/D, first-ply check, variants table), with refusals E_UNKNOWN_MATERIAL, E_NONPHYSICAL_MATERIAL and E_EMPTY_STACK in details.reason. The tool's readOnlyHint is now false. See cad_help({topic:"composites"}).
  • cad_print mode:"ded" (packages/mcp/src/tools/print/ded.js): writes a robot DED/WAAM program (ABB RAPID, KUKA KRL, FANUC TP, Yaskawa INFORM) and sends nothing. Refusals in details.code: ded_post_refused, robot_unknown, ded_toolpath_bad_input, ded_toolpath_bad_surface, and the ded_toolpath_* / ded_cell_* / ded_post_* families.
  • cad_view_body shares five display modes with the other viewers (shaded, edges, wire, hidden-line, x-ray) and ships the B-rep edges.
  • cad_set_part {flow:{flowPath}} turns on the FLOW-AREA rule in cad_dfm_check and cad_design_review. New findings: sudden_expansion, hub_step, strut_forms_throat and throat_not_at_min.
  • DFM: the LPBF wall and gap probe no longer reads a small radial bore in a curved rim as a 0.1-0.2 mm wall. The moldability draft remedy sends cad_draft only a draftable set. All 48 METAL and foundry rule codes that had no remedy now carry one: 11 mechanical (cad_move_face, cad_support, cad_draft) and 37 honest advisories.
  • cad_print_prep no longer forwards document to cad_dfm_check, whose closed schema rejected it.
  • A JSON-string face on cad_pad is parsed again ({"normal":[0,0,1]} sent as a string was refused E_GEOMETRY): string-to-object coercion now resolves the deduped schema's shared definitions, for every tool whose schema has them.
  • Budgets: cad_mesh_compare / cad_mesh_section move to a new measure family (16 KiB, 120 s), and the compact report names what it cut in dropped. cad_server_info gets a 96 KiB override; it reached 76,086 B and spilled to E_LIMIT.

Verification and export gates

  • cad_verify {gate} is the pre-handoff gate (ops/handoffGate.js). It runs interference, coincident faces, fasteners, OML, mechanism poses and claims checks, and returns a signed report; a verdict other than pass is a hard fail. Inside the same gate object:
    • gate.steps adds the outgoing STEP brep class.
    • rules.mesher adds a gmsh + CalculiX mesher class.
    • A cost-gated visual rule is included.
  • A strict cad_export_body STEP is gated, where strict means strict:true, a handoff-marked part or a handoff document:
    • Before writing, a mesh-derived face refuses E_GEOMETRY / export_mesh_derived_faces with every face listed.
    • After writing, a no-heal re-import that fails moves the file to <path>.brep-rejected and refuses E_GEOMETRY / export_brep_invalid. CADSOLVER_NO_EXPORT_BREP_GATE=1 turns it off.
    • A non-strict export only warns.
    • The independent OCCT oracle runs on cadquery-ocp 8.x as well as 7.x; on 8.x it crashed on an import, so the OCCT half never ran and every strict export warned that it had not.
  • Face provenance: cad_body_topology gives each face's origin (exact, healed, genbool_facet, mesh_clip, mesh_to_brep or imported) and a faceOrigins count with meshDerived.
  • Assembly checks:
    • The fastener audit reads prismatic (flat / D) pins along their extrusion axis.
    • Findings name the second body in nut, blockedBy, host / hosts and nearestFastener.
    • The OML check measures against the enclosed volume, not the skin bodies.
    • Every clashing pair carries an overlapRegion.

Retired codes

  • sketch-solve-degenerate-line is retired. A two-whole-line coincident forces the not-parallel distance instead.

Packaging

  • Cursor edition and a stable npm alias: nativecad-cursor.zip and nativecad-mcp-latest.tgz are listed under the release signature in latest.json. Both are notify-only for self-update.
  • Kernel bundles: the three @nativecad/kernel bundles share one copy of every module, and the index loads op modules and data tables lazily. The staged kernel now carries ops/turbo flange data and the lattice kt_skin table. Before, they were missed silently through dynamic or fs reads.
  • tools/list: the new extent / operation fields took the listing to 405,487 B against the 405,504 B line. Publishing cad_extrude_profile and cad_pad top-level-only brought it to 403,455 B.
  • Improvements to the bug feedback loop and gathering. IP protections and checks enhanced.

Fixed reported issues

  • cad_fastener through pre-drilled holes published the kept plates as deleted bodies. The executor now retires only inputs that were replaced.
  • Regression tests landed in this range (tests/test_cadsb-100429.js, tests/test_cadsb-100300.js). The code fix shipped before 0.24.0.
  • TRIMMED_CURVE PARAMETER_VALUE trims. The issue does not reproduce on master, because an earlier change already fixed it. A 160-case regression matrix landed; no reader change.

↑ all releases

NativeCAD 0.24.x

v0.24.0 “Gelmetti”

released October 21, 2026

sha256 nativecad-mcp-0.24.0.mcpb 3b14a96989d42848ef6831c4d324b688e92046e50dc5ccf003a65a9a2371b4d7sha256 nativecad-mcp-0.24.0.plugin 9b909c0904d42e67ffdff2b80fc566db33bf3f4ba5eb611efb1b0d691e50c2c5sha256 nativecad-mcp-0.24.0.tgz 0a673653598651a756e8c29cfb7b9745ed23be841cdc2678cd8d4a4777ae07ae

NX .prt, Inventor .ipt and SolidWorks .sldasm files import, the tangent boolean cases build exact or are refused by name, and a declared mechanism can be checked across its whole motion. 184 tools, one more than 0.23.3 (cad_assembly_sweep). The command schema moves from 1.64.0 to 1.65.0: import gains an optional searchPaths for .sldasm components, and every document valid at 1.64.0 validates unchanged. The import census over the 654-file corpus stands at 437/654 (66.8 %) on master af70d847, from 306/654 earlier.

Importers (cad_import)

  • NX .prt parts open through their embedded Parasolid partition (packages/interop/src/importers/NxPrtImport.js). 21 of the 25 NX corpus parts import; 13 STEP pairs agree within 0.03 % volume (ftc_11 0.103 %). Creo parts are refused by name (PRT_CREO_UNSUPPORTED), as are PRT_TRUNCATED and PRT_NX_ASSEMBLY_NO_BODY. The validator accepts .prt and .prt.N; XbDecoder reads the FEATURE (90), MEMBER_OF_FEATURE (91) and TRANSFORM (100) layouts.
  • Inventor .ipt parts open through their embedded ACIS SAB stream (importers/IptImport.js, importers/SabDecoder.js), transcoded to SAT and read by SatReader.js. Container faults refuse IPT_NOT_COMPOUND_FILE, IPT_NO_RSE_STORAGE, IPT_NO_BREP_SEGMENT, IPT_SEGMENT_CORRUPT or IPT_SHAPEMANAGER_UNSUPPORTED. With the SatReader extensions below, the ipt census is 16/18; the 2 left are hand-made placeholder fixtures (a 46-byte .ipt, a 50-byte .iam).
  • SatReader reads more of ACIS: {ref N} subtype back-references, exact / rb_blend / sweep spl_sur through their saved B-spline approximation, intersection curves from their B-spline approximation, reversed surfaces and intcurves, reversed narrowing cones, inward-normal sense flips, apex vertex loops, periodic closed curves and elliptical cylinders. nullbs, elliptical cones and dangling refs keep named refusals (SAT_UNSUPPORTED_SURFACE / SAT_UNSUPPORTED_CURVE). SAT_CONE_HALF_ANGLE is retired.
  • SolidWorks assemblies open from their referenced parts (importers/SldasmComponents.js, importers/SldasmImport.js). The importer reads swXmlContents / COMPINSTANCETREE, decodes each instance's swTransform (checked against swBoundingBox on 115/117 assemblies), flattens sub-assemblies, and imports each component .sldprt through ParasolidImport. Components are looked for in the assembly's folder and in the new searchPaths (1 to 64 folders, searched recursively, .sldasm only). A reference that cannot be resolved refuses XA_COMPONENTS_MISSING with the missing files and the folders searched. An assembly that stores only open sheets falls back to its resolved components when every one resolves (BerringForSunGear 0 -> 13 solids, TransWSyncro 0 -> 221), else reports componentFallback with the missing list. sldasm census 3/100 -> 82/100.
  • .sldprt configurations: import takes configuration ("Config-<n>", a name, or a zero-based index); the result's imported.configuration says which one was imported and lists the others. An unknown selector refuses SLDPRT_CONFIGURATION_NOT_FOUND. x_b deltas streams resolve to their base partition (66 files, 0 -> 37 solids), and a rollback-tail fingerprint resolves 19 of the 36 ambiguous ones, 0 wrong.
  • IGES (packages/interop/src/IgesReader.js):
    • 308/408 subfigure instancing: tachyon 167 -> 798 closed bodies, boron 93 -> 259 solids. Closed pre-sewn bodies whose report mesh is refused are published as solids with a named defect in imported.meshRefusals[]; Teensy imports as 176 solids + 1 named refusal.
    • The whole-file sew measures exact curve deviation for edge pairs that share welded vertices but fail the control-polygon check (ops/stitch.js). 8 of 19 IGES_STITCH_OPEN census files, plus OLED, now close with STEP-matching volumes. Genuine open sheets refuse IGES_SHEET_OPEN, and 4-way edge coincidences IGES_STITCH_AMBIGUOUS_EDGES.
    • MSBO B-rep support (IGES_NO_GEOMETRY 38 -> 19 of 647), each MSBO body sewn on its own with micro-edge collapse below file resolution, and MSBO edges bridged from a 502 vertex to a 504 curve end within file resolution. The import command reuses the per-body sew (boron 93, xenon 45, argon 49 solids).
    • Entities 143/141 bounded surfaces, 104 ellipse arcs, 114 surfaces and the 126 curve range; STITCH_FAILED 18 -> 0, closed files 482 -> 491 of 647; B523 orientation 73 s -> 7.7 s.
  • Parasolid .x_t / .x_b (XtReader.js, importers/ParasolidImport.js):
    • Git LFS pointers refuse XT_GIT_LFS_POINTER (with size, oid and the git lfs pull remedy), and header-only transmit stubs refuse XT_NO_BODY_DATA instead of XT_HEADER_MALFORMED / XT_ENCODING_UNKNOWN / XB_BAD_RECORD. The sniff flagged only 5 corpus stubs, 1 fixture and 1 empty file across 19,389 small x_t/x_b files.
    • Charted blend spines map u = base_parameter + base_scale * s (end caps were up to 8.7 mm off). TRIMMED_CURVE windows are sampled param_1 -> param_2, so sense - is no longer reversed. Apple-torus inner-lobe faces build, whole-sphere/torus SPUN_SURF faces build and leading wire/point bodies are skipped.
    • Closed XT sheets sew to solids; a genuine sheet source gets the SHEET_BODY_SOURCE outcome.
  • PDF drawings on one annotation-named layer import (ops/pdfDrawingImport.js). When every curve sits on a layer named like annotation ("Dims"), the layer name is ignored and the stroke style decides: the wide continuous pen is the view outline, the thin pen is dimension and extension lines. These sheets were refused PDF_NO_VIEWS.

Booleans: tangent cases

The 19-case tangent table (three operations each against closed forms) pinned six kernel gaps; all six are closed in this release.

  • Equal-radius tee, exact subtract and intersect (ops/cylCylEqualBool.js). The subtract was refused (self_intersection) and the intersect chorded into 87 LINE edges. Now every edge is an exact ELLIPSE, CIRCLE or LINE: subtract 8 faces / 12 edges, intersect 4 faces / 4 edges (V = 8R³/3).
  • Steinmetz cross, exact subtract and intersect. The general engine had the right volume but 78 / 152 LINE chords where the ellipses belong. Intersect is the bicylinder (4 faces, 4 ELLIPSE edges); subtract is the base rod's two halves as two lumps.
  • Nested tangent balls (ops/nestedTangentBool.js): the union is an exact copy of the outer ball (was 2 faces / 6 edges with straight secant edges), the intersect is the inner ball, and the subtract, which would leave a cavity open through one point, is refused tangent_contact_pinch with the contact point. A rod inside a rod touching along a line now leads with the same tangent_contact_pinch verdict.
  • Near-tangent unions are exact or named, not faceted (ops/arcPrismUnion.js, ops/booleanClearance.js). Two parallel rods 1e-3 mm into each other unioned to 638 PLANE facets; they now build exactly (4 faces). A ball 1e-6 mm into a block (2189 facets) is now refused as a touch at a POINT, with the point.
  • No-volume union refusals name the contact (ops/booleanClearance.js): "touch at a POINT [x,y,z]", "touch along a LINE …" or "are DISJOINT - a GAP of g mm separates them". The reason code is unchanged.
  • An empty intersect says when it dropped a sub-tolerance sliver: a warning with code sub_tolerance_overlap (dropped volume, area, mean thickness vs the resolution) on the op entry. A true contact gets no warning. cad_assembly_contacts / clash volumes build the real sliver body instead.

Booleans: other fixes and speed

  • Welded frames:
    • A tube_sweep {points, bendRadius} round tube now builds as exact cylinders + torus bends by default (surfaces: "analytic"), and a union that meets one of its straight legs stays on the analytic rungs (ops/tubeLegSplice.js). bendRadii is the per-corner spelling. The same path without bendRadius is unchanged.
    • The ball-jointed elbow (ops/ballElbow.js: two equal tubes ending at one node plus a ball of the tube radius) builds exactly in every operand order (≤ 9 faces). It was refused by genBool's validity gate or as non_connected_result.
    • cad_boolean_union {mitre:true} (ops/frameMitre.js) is the welded mitre joint of two straight round or box members: one planar seam, V = section area × the two axis lengths. Unequal sections refuse mitre_sections_differ.
  • Faster large booleans (-100495, -100496, -100497, -100499, -100505, -100513, -100525, -100537, -100545). The braid ∪ ring union runs end to end in about 135 s, was over 180 s. This release has pierce() query the base through a solid probe instead of tessellating it whole. This release gives genBool's planar imprint scan a BVH broad phase that leaves results bit-for-bit identical.
  • pierce() refuses past 32 distinct pierced base planes (non_parallel_pierce) and hands the pair to the general engine. A plane-faceted braid ∪ annular prism used to run 130 s+ with no answer. A planar tool end flush with the entry face, on a tool that cleanly pierces the opposite face, is cut as a through hole (box Ø8 3590 -> 317 ms).
  • Coplanar merge accepts a near-miss only when exact B-rep volumes agree (the intersect, 200 -> 4 faces). Tangent-point cusps collapse exactly (ops/tangentCuspCollapse.js). The maximal-face pass no longer leaves faces bounded by chords that are off their own surface (ops/faceMerge.js). A drill-tip cross bore followed by a second one builds (ops/faceSplit.js).
  • Feature-pattern folds and sequential holes reuse their meshes (ops/featurePattern.js, ops/incrementalCertificate.js): a hole pattern passes one SolidProbe face cache through every fold instead of re-stamping every triangle per fold, and the boolean result gate no longer re-meshes the hole-carrying faces on every operation.
  • The Bambu .3mf writer instances shared meshes: a plate of repeated parts is written once per mesh plus a transform per copy (x1c-plate.3mf 27.7 MB -> 1.9 MB).

Features

  • cad_hole_pattern kind "radial" (ops/radialHolePattern.js): rows of radial through-holes in the coaxial walls of a tube or tube-in-a-tube in one pass, {kind:"radial", rows:[{station, count, diameter?, startAngle?, stagger?, walls?}], axis?, origin?}. This is a mode of the existing tool, not a new one; radial_hole_pattern is also a cad_build_plan verb. 240 holes in one op take 12.0 s with no genBool call, volume within 2e-7 of the closed form. Circular feature_pattern radial seeds and polar hole rows route onto it (CADSOLVER_RADIAL_ROUTE=0 turns that off).
  • cad_chamfer_edges gains flip: it swaps which support face takes distance and which the second setback (distance2 or angle). The result reports flipped. Flip on an equal-distance chamfer is refused chamfer-asymmetric-unsupported.
  • A blanket chamfer refuses an input that already self-intersects (chamfer-input-invalid), the twin of the fillet's fillet-input-invalid.
  • Shell (ops/shellCurved.js, ops/nurbsOffset.js): exact cylinder/plane tip chords and cone/plane counterbore corners shell watertight; a box with one 1 mm fillet shells at t = 2; a rational patch whose arc weight drifts meets its tolerance through a homogeneous refit. Shell refusals carry their cause as data (collapseCause, minRadiusRatio, deviationRatio).
  • Blend refusals carry reach: keep-edge / keep-surface refusals report the overrun and the largest radius that fits. Elliptic and B-spline rim pairs are classified.
  • Glazing: a closed-loop member (A, B, C, D, A) gives pane edge D-A the closing span; it was refused glazing_pane_too_small.
  • cad_engrave_text with face:{normal} honours origin: among same-normal faces it picks the one that contains origin, not the one furthest along the normal.

Assemblies and fasteners

  • New tool cad_assembly_sweep (packages/assembly/src/Mechanism.js, MechanismSweep.js). Declare a mechanism once: revolute/prismatic joints, loop closures, one driver with its range, end stops and named poses. Per body pair in relative motion it returns the minimum clearance over the range (negative = penetration), where it occurs, clash intervals and the value at each named pose. It also gives the transmission angle range and toggle margin per closure pin. Conservative advancement steps each pair by its clearance over its maximum relative speed, so a 2° latch hit that an 11-pose sweep skips is found. checks runs the z-fight, contacts, access and OML checks at every pose; export writes both end states and a scrub GLB. It moves nothing in the session.
  • cad_joint kind "pin" (ops/revoluteJoint.js): a real revolute stack on any axis. It has an ISO 3547 PTFE-lined bush in the moving part, an ISO 286 journal fit (default H8/f7), PTFE thrust washers, ISO 7089 washers, an ISO 4017 bolt and an ISO 4032 nut. joint.sweep drops straight into cad_assembly_sweep. A partB that does not straddle the pin is refused joint-pin-needs-fork.
  • cad_fastener seats hardware in host bodies by envelope (ops/fastenerHost.js). The clearance hole, countersink or counterbore and a potted insert pocket are cut in every host or in none. Failures are named: fastener-host-cut-failed, fastener-host-cut-incomplete, and fastener-flange-wider-than-wall with the largest Ø that fits. wetted:true (an OML face) makes the hardware a flush csk_100 head, and a proud head is refused fastener-proud-head-on-oml.
  • cad_fastener_audit measures along the hardware against the modelled solids (ops/fastenerSeat.js), not only with radial rays. New findings: shank-in-solid, head-coplanar-uncut, insert-inside-host, flange-breaks-out and nut-unreachable. Imported hardware with no placement record (hardware, inserts) is read off its own solid. A body it cannot read is listed in stats.notAudited and makes the audit incomplete.
  • cad_assembly_contacts: facing coaxial cones (a countersunk head in its seat) report their gap in contacts.conical. A bore/shaft clearance ≤ 0.05 mm radial, or a declared fits.runningFits pair, is a running fit and counts as joined; fits.pressFits marks designed interference. coincident:true adds the z-fight check (same-facing coincident area per body pair), and file takes a .glb. cad_design_review kind:"assembly" reports the same check as advisory assembly_coincident_faces findings.
  • The assembly check bounds its exact overlap-volume pass (4 s per pair, 12 s per check, then ray sampling named boolean-budget-exceeded). A glazing cad_design_review went from 158 s to 42 s with identical volumes.

Output, printing and casting

  • A cold whole-document cad_preview_body, or a tessellating cad_export_body, estimated past the job cost line returns a jobId instead of blocking past the 60 s bridge wait (packages/mcp/src/tools/output/cold-cost-route.js). The estimate is 0.04 s per face, against the shared ~40 s line (CADSOLVER_MCP_JOB_COST_S). A document already tessellated in this process stays inline, and formats that write the B-rep as is (STEP, x_t, SAT) are not routed.
  • Tessellation: the watertight repair guard judges a closed but non-manifold or inside-out mesh against the B-rep volume. A repair that lands within 1 % of it is accepted; before, it was refused tess_repair_volume_drift. An explicit chordTolerance no longer meshes a small circle coarser than the default. Sliver faces no longer fold.
  • cad_nest gains the sls_eos_p396 target and wallMarginMm.
  • cad_cast_mould gains draftAllowance ("auto" default, "off", or degrees) on 0° moulded walls (ops/draftAllowance.js). Pour-along-pull orientation, the spigot boss as a cavity-trimmed pillar, and the gate bore and boss cut before the sheet split.
  • The published kernel ships every printer record, so a printer such as x1c resolves from the bundle.
  • A resumed cad_cast_mould answers inline again. The report carried the placed nest's per-copy layout (nest.placed.objects, items, placedBoxes: 111 KB for a 40 x 30 x 20 mm box), which took the response to 253 KB, past the 128 KiB report budget, so it was spilled to a file. report.nest.placed now keeps the summary with objectCount and itemCount; the build 3MF it names holds the full layout (0.24.0 release, ops/castMould.js).

Licensing and packaging

  • The MCP server refuses an SDK development key. cad_license answers E_INVALID_ARGS with details.reason: "dev_key_not_mcp" before applying it. A dev key found at boot is dropped with a warning and the server runs unregistered. Development keys license the NativeCAD SDK only (Development Licence Agreement §2.3A).
  • The Cowork .plugin no longer ships bin/: claude.ai rejects plugins with a top-level bin/, and .mcp.json launches dist/server.min.js directly.
  • The tools/list line moves from 393 KiB to 396 KiB (402432 -> 405504 B): cad_assembly_sweep and the new import, fastener and joint arguments took the public listing to 402901 B across 184 tools. The private fea-intact line follows at +5 KiB (401 KiB) and the absolute ceiling at 403 KiB (packages/mcp/scripts/measure-tool-list.js).
  • cad_assembly_sweep publishes a wire type on its three union fields (checks.poses, mechanism.stops[].at, sampling), so a schema-driven client sends them structured (audit).
  • A CycloneDX SBOM for this version is committed at deploy/sbom/nativecad-mcp-0.24.0.cdx.json (327 components).

Showcase pages (tangent booleans, transaxle fastener checks, 3D nesting) and the import census are repository content, not part of the bundle.

Fixed reported issues

  • A PDF drawing with everything on one annotation-named layer was refused PDF_NO_VIEWS.
  • cad_engrave_text face:{normal} ignored origin among same-normal faces.
  • Cold cad_preview_body {of:"all"} (116 s) and GLB cad_export_body (71 s) on a 23-body STEP assembly timed out past the 60 s bridge wait.

↑ all releases

NativeCAD 0.23.x

v0.23.3 “Gatti”

released October 18, 2026

sha256 nativecad-mcp-0.23.3.mcpb 797ea642694cae7de43eb897d8753725246ba40050c1bd23e69d56f364338f0fsha256 nativecad-mcp-0.23.3.plugin 51b01db63f343bc1aba61864b311f2b8a2b18cd4207172c310e1194373e51ed7sha256 nativecad-mcp-0.23.3.tgz 41c156b5e9a94b7fc2dc78f63311c6070bdd09bdeb0e470ffc663dbf2b7e9dee

Spur gears are exact B-spline solids, angled and face-placed features export to .FCStd as PartDesign features, and the hollow bent pipe + second holed flange union no longer cracks. 183 tools, one more than 0.23.2 (cad_cast_mould). The command schema version is unchanged (1.64.0); cad_dfm_check gains the foundrylab_dmc process and its rules. The three headline fixes come from the cadbench.ai rerun of 0.23.2.

Gears (cad_gear)

  • Spur gear flanks are exact cubic B-splines in the kernel (ops/gearExact.js, ops/gear.js). A 50-tooth gear is 202 B-rep faces (was 2802) and its STEP file carries B-spline surfaces instead of faceted flanks.
  • The .FCStd export of a spur gear is a blank Pad, then a tooth-gap sketch with 2 B-splines, a Pocket and a PolarPattern, so FreeCAD rebuilds the teeth as features: recompute gives 205 faces and the same volume. Editing the tooth count or module on the exported file is follow-up work, not in this release.

FreeCAD export (cad_export_body {format:"fcstd"})

  • A face-placed cad_hole_pattern exports as a Hole plus a PolarPattern or LinearPattern. The row carries the depth every instance was cut to, so a through row on an angled face stops at the far side of the wall.
  • A rotated box or cylinder exports as a sketch plus a Pad on a placed sketch plane. Nothing in the cadbench elbow falls back to a Part::Feature snapshot.

Booleans

  • Arcs that are pieces of one full circle share that circle's 64-point spacing (ops/faceSplit.js, ops/genBool.js, ops/faceMerge.js). A ring split as four quarter arcs now matches one split as two halves, which is what cracked the hollow bent pipe + second holed flange union (181 open edges). The union is one watertight solid, and faceMerge restores true circles at the leg and bend joins.
  • Separate cad_boolean_union calls work one after another (follow-up, commands/rebuild.js, ops/revolveTrim.js, ops/splitSurface.js). A parser-bound entity left on the parser by the revolve trim and surface split was copied onto the next call's fresh parser, so the second union (pipe ∪ flange, then ∪ the second flange) wrote into the old document and failed with an unknown solid. The rebuild now drops it, and each call starts clean.

Casting moulds

  • New tool cad_cast_mould (ops/castMould.js): one call from a part to a printable two-half casting mould with a JSON + HTML report. The linear shrink allowance is required. It also degrades to a partial result after import instead of failing, adds the spigot boss and funnel (the boss foot is now checked for contact with each half's top and extended as a pillar when it would float, ops/spigotContact.js), nests mould pairs in the build volume and builds the shell from scan-compensated geometry.
  • Mould halves carry more of the casting: the conformal flange and its foot are fused onto the shell before the split, so the cavity is cut once (ops/mouldFeatures.js, ops/mouldShell.js); planned core prints get seats and the extra risers are cut into both halves (ops/mouldSeats.js).
  • Mould export is interoperable (ops/mouldInterop.js): per-half STEP files with half_A/half_B products, a <part>_mould_set STEP in AP214 and AP242 with the halves, cores and parting surface, and a <part>_mould_set.3mf with named objects. ops/mouldInterop.js now loads its helpers with literal requires, so the bundled server carries them (the release build refused its dynamic require).
  • The automation rate separates hand-finishing work from review advisories: each remaining item carries kind: "work" or "review", and full automation means no work items left (review advisories allowed).
  • cad_dfm_check pack:"foundrylab_dmc" (ops/foundryLabRules.js): the Foundry Lab digital-casting rules. A call without shrinkPct is refused with FOUNDRYLAB_SHRINK_REQUIRED.

Other fixes

  • cad_picogk refuses to import a decimated body with non-manifold edges. Decimation first splits pinched edges; if any survive, the call answers E_GEOMETRY with code PICOGK_DECIMATE_NONMANIFOLD and names import:false or a larger maxTriangles. Open edges alone are still a warning.
  • cad_assembly_contacts no longer reports a bore/shaft interference when the radial material does not overlap.

Fixed reported issues

  • False bore/shaft interference in cad_assembly_contacts.

↑ all releases

v0.23.2 “Frühbeck de Burgos”

released October 18, 2026

sha256 nativecad-mcp-0.23.2.mcpb 7faeda1dd401243d11fa23431eb54d5937109af5da612124e13450208b099603sha256 nativecad-mcp-0.23.2.plugin a7ae8063a7dbf8cc65a52322600b78599eccac9e8cae5dfbe04aa50dd3506923sha256 nativecad-mcp-0.23.2.tgz a479f3a32ec03d5b7835afff0e42e599cd42242ebe6b60f2e9db6d1d724a6253

FreeCAD round trips close the cadbench.ai pilot gaps: cad_edit_guarded edits .FCStd files, the .FCStd export maps gears, patterns and sweeps to real PartDesign features, and repeated features steer to one pattern call. 182 tools, unchanged from 0.23.1. The command schema is unchanged (1.64.0). The fixes come from the cadbench.ai pilot, where NativeCAD failed the spur gear (frozen geometry), the two edit items, the lego brick (ran out of turns) and the flange (3 bodies).

Editing .FCStd files

  • cad_edit_guarded works on .FCStd files (ops/fcstdReader.js, packages/mcp/src/tools/state/edit-guarded.js). inspect:true lists the document's sketches, constraints, features and patterns without changing anything. An edit can set a sketch constraint by index, by name or by {type, value}; set sketch geometry; and change a pattern's occurrences, angle or length. Only the changed element is rewritten and its object is marked Touched, so FreeCAD recomputes exactly what changed.

FreeCAD export (cad_export_body {format:"fcstd"})

  • The reply no longer fails with E_INTERNAL. cad_export_body's outputSchema now declares the fcstd block, so the file and its fcstd summary come back as a normal result. This was the known issue in 0.23.0 and 0.23.1.
  • A spur gear exports as a full-profile sketch plus a Pad, not a frozen B-rep snapshot, so a grader can edit and recompute it.
  • Patterns export as PartDesign features: linear patterns as LinearPattern, circular patterns as PolarPattern, mirrors as Mirrored (pattern, feature_pattern, through-drill hole_pattern rows and mirror), and a round cad_tube_sweep along a planar bent path as AdditivePipe. Patterns that do not fit stay B-rep snapshots and are listed in fcstd.fallbacks.
  • One PartDesign Body per NativeCAD solid. A boolean never leaves its tool as a second Body: the tool lands in the base Body as features (a primitive, a replayed recipe, a pattern group or Mirrored), and a tool with no feature form (a filleted chain, an intersect tool) is frozen as a hidden B-rep combined by PartDesign::Boolean inside the base Body. The cadbench elbow flange, 3 Bodies in 0.23.1, is one Body.

Agent guidance

  • Repeated features are one pattern call. The MCP instructions gain a "Repeated features" section and the plugin's SKILL.md a matching step: studs, tubes, ribs, vents, a bolt circle or a hole grid go to cad_hole_pattern, cad_feature_pattern or cad_pattern_copies, never one call per feature.
  • Unattended runs (repo integration, not in the bundle): the Harbor benchmark agent's system prompt says nobody can answer questions, so it takes the best fallback, still writes the output file and says what it compromised.

↑ all releases

v0.23.1 “Fedoseyev”

released October 18, 2026

sha256 nativecad-mcp-0.23.1.mcpb 0c790d3d73e2130227e6f56fff3ffd7b2147f312848e1bb9012b94b13e06c8bfsha256 nativecad-mcp-0.23.1.plugin 5eb3d0a6598d042dd6144a66bae8d3976b7ec35effb3116bd95780c0e8329890sha256 nativecad-mcp-0.23.1.tgz be478657c1299b61f8e30375f17483b28349ada15135ecc92e6ed4f5b2d3c97b

.FCStd files exported by NativeCAD now rebuild when FreeCAD recomputes them. 182 tools, unchanged from 0.23.0. The command schema is unchanged (1.64.0).

FreeCAD export

  • Every object in an exported .FCStd is marked Touched. cad_export_body {format:"fcstd"} writes no stored shapes, and without the mark FreeCAD opened the document as "Up-to-date": recompute() did nothing and every PartDesign Body read as a null shape. Any grader that opens the file and recomputes it (the cadbench FreeCAD validator does) scored 0 geometry on every NativeCAD output. The writer now puts Touched="1" on each <Object> declaration (ops/fcstdWriter.js), so the first recompute() rebuilds every sketch and feature. Checked with FreeCAD 1.1.0 on a .FCStd exported by the built bundle: a 40 x 20 x 10 mm pad with a 6 mm hole recomputes to a valid Body of 7717.257 mm³, the NativeCAD volume. Files exported by 0.23.0 still open in FreeCAD; they need Mark to recompute (Ctrl+Shift+R) or obj.touch() on each object before recompute().

Other changes since 0.23.0

  • cadbench rig (repo tooling, not in the bundle): pick_representative_shape returns (shape, owner).
  • Metal printing DFM: cad_dfm_check gains EBM, binder jet, metal FFF, DED/WAAM/EBAM, cold spray, metal jetting, MoldJet and metal DLP rule packs and a metal post-processing chain; its rule codes are listed once as an enum, which shortens its listing.
  • cad_help may return up to 128 KiB for one schema answer, so the cad_dfm_check schema no longer spills.
  • Mesh to B-rep pre-splits full-turn cylinder walls and imprints cylinder and cone patches.

↑ all releases

v0.23.0 “Eschenbach”

released October 17, 2026

sha256 nativecad-mcp-0.23.0.mcpb f4cf339331995807570e9464faeb47da819078a9e4d824aec7c96f587f1da6a1sha256 nativecad-mcp-0.23.0.plugin ec452f8e8aed825fb09ab67116a34b0deecc4b3631f6cd5dd8b91ce340b01459sha256 nativecad-mcp-0.23.0.tgz 0fb1151e10f0828ce800230aa16a89c08b8f1693d5ca0c3eb81bb0e39f4ed928

NativeCAD writes and reads native FreeCAD 1.1 documents, checks its own work with cad_verify, and edits behind a firewall with cad_edit_guarded. 182 tools: the 180 from 0.22.1 plus cad_verify and cad_edit_guarded. The command schema is unchanged (1.64.0); cad_export_body gains the fcstd format.

FreeCAD on the MCP host. Fillet, chamfer, hole and pattern export to .FCStd, ops that fall back to an embedded B-rep, cad_verify's FreeCAD recompute check, a .FCStd reference in cad_verify, and cad_edit_guarded on an .FCStd need FreeCAD 1.1 on the MCP host, with FREECADCMD set to its FreeCADCmd executable. Without it they return a clear error that names FreeCAD 1.1 and FREECADCMD (code E_FREECAD_MISSING when the executable cannot be started), never a raw spawn ENOENT; cad_verify reports the recompute as not run, with that reason. Pads, pockets, revolves and params export without FreeCAD.

FreeCAD 1.1 writer: cad_export_body format "fcstd"

  • cad_export_body {format:"fcstd"} writes a native, parametric FreeCAD 1.1 PartDesign document from the whole op history (parent). Each live body becomes a PartDesign Body: fully constrained sketches (0 DoF) with Pad, Pocket, Revolution, Groove, Loft and Pipe (101941, 101942); Fillet, Chamfer, Draft and Thickness with geometric edge and face references, and Holes with ISO thread tables (101958); Linear, Polar, Mirrored and MultiTransform patterns with parameter-bound counts and spacing; and a Params spreadsheet driving every parameterised field. Changing a spreadsheet value in FreeCAD rebuilds the part.
  • Nothing is dropped. An op PartDesign cannot express is embedded as a Part::Feature B-rep snapshot (joined by a PartDesign::Boolean) and listed in fcstd.fallbacks[] with "parametric editability lost here". The response also carries fcstd.mapped, fcstd.parametric (true when there are no fallbacks), fcstd.params and fcstd.ignored.
  • Whole document only: of must be omitted or "all". Every piece was verified by a real FreeCAD 1.1.0 recompute, including parameter-flip rebuilds (volume within 1e-3 relative). See docs/FCSTD_MAPPER.md.

FreeCAD 1.1 reader

  • A lossless .FCStd reader and editor (ops/fcstdReader.js). A no-op round trip is byte-identical; an edit rewrites only the changed element and marks the object Touched, so FreeCAD recomputes exactly what changed. It backs cad_edit_guarded on .FCStd files. See docs/FCSTD_READER.md.

Verify before you save: cad_verify

  • New tool cad_verify. One read-only call after a build and before cad_save or export returns a ranked list of problems, each with a concrete fix (for example "set param hole_d = 8"):
    • validity: watertight, closed manifold, orientation, B-rep check, and a heal probe run on a rebuilt copy, never on the live body;
    • topology: Betti numbers, holes and through holes;
    • spec: named dimensions with tolerances, measured on the body (a list, or text lines like hole_d = 8 mm ±0.05), each pass or fail with its delta;
    • drawing: an engineering drawing PNG. The views are found, cropped, registered and named; first or third angle is detected; each view reports silhouette IoU, edge agreement, drawing-vs-model extents and drawn hole sizes;
    • reference: a .step or .FCStd model to compare against;
    • export {format:"fcstd"}: mapper fallbacks (lost editability) and, with FreeCAD, a real recompute compared against NativeCAD's volume.
  • The model sheet and the per-view diffs come back as MCP image content, and are also written under <output dir>/verify/<label>/.

Edit behind a firewall: cad_edit_guarded

  • New tool cad_edit_guarded. It applies param, feature or append edits to the session document or to an .FCStd, then proves nothing outside the edit changed. It derives the affected region from dependents and footprints (or takes expect), recomputes, compares the surfaces outside that region, checks validity and any targets, and rolls back on failure. An .FCStd is restored byte-identical, and the report says so. out writes elsewhere; dryRun writes nothing.

Other changes since 0.22.1

  • Mesh to B-rep: the near-flat cone gate is on by default. A near-flat cone region that passes by its apex becomes a B-spline sheet instead of a pole cap. On the Rotor 67 blade, the body comes back closed (2.4 mm² of #repair fill, was 557 mm²) and within -0.056 % of the mesh volume. nearFlatWrap:false restores the old behaviour.
  • The cad_dfm_check listing summary is shorter (5164 to 5120 B), and the tools/list budget line moves from 385 KiB to 392 KiB (the 400 KiB ceiling is unchanged). The repro-synth and failure-report tools read their environment through env.js.
  • cad_verify shares the preview/silhouette family budget, and cad_edit_guarded shares the session-state family budget.
  • The FreeCAD paths work from the .mcpb, plugin and npm bundle. The FreeCAD scripts (resolve_refs.py, step_to_brep.py, mapper_check.py, fcstd_to_step.py, edit_guard_check.py) now ship in the bundle and are written once per build under <cadsolverHome>/freecad/<sha12>/, because FreeCAD is another process and cannot read the bundle; CADSOLVER_FREECAD_SCRIPTS overrides the directory. The body-scoped STEP filter moved into the kernel (ops/stepFilter.js; the old module re-exports it), so the bundle builds.
  • A missing FreeCAD is a clear error. The .FCStd export, cad_verify and cad_edit_guarded paths name FreeCAD 1.1 and FREECADCMD instead of surfacing spawnSync freecadcmd ENOENT. A FREECADCMD that points at nothing no longer raises an unhandled child-process error event from cad_edit_guarded.

↑ all releases

NativeCAD 0.22.x

v0.22.1 “Dutoit”

released October 16, 2026

sha256 nativecad-mcp-0.22.1.mcpb 1bf20613e154b77c669f0563f86e25d841ee9127bbe8704bb1684e1a084040bfsha256 nativecad-mcp-0.22.1.plugin 4f1309959e166513ef581aff69e472f9ec2b99ba9d85a27016377efaecbbe54dsha256 nativecad-mcp-0.22.1.tgz e01b63a0e012496cde912b591563eecda2fa44e78b16eab8daec98b0544cf6ef

Fasteners through existing holes, a fastener audit that recognises nuts and washers, a fast import replay, STEP trimmed curves read correctly, and self-update on by default for .mcpb and plugin installs. 180 tools, unchanged from 0.22.0. The command schema is unchanged.

Self-update

  • .mcpb and plugin installs with autoUpdate unset now update themselves. When autoUpdate has never been set, the effective mode is auto for the self-installing editions (.mcpb, plugin) and notify for npm, dev and unknown installs. An explicit setting (off, notify, auto) still wins. Every gate is unchanged: signature and sha256, install only while the server is idle, an app reload finishes it, rollback after 3 failed starts, haltVersions, rollout. The update status reports where the mode came from (modeSource: setting or default), and the reload notice says the install updates itself by default and how to change it. To keep the old behaviour: cad_configure {autoUpdate:"notify"}. See docs/UPDATES.md.

Fasteners

  • cad_fastener goes through existing holes and slots. A part whose existing hole or slot the axis runs down now joins the stack instead of reading as a gap. A hole or slot already at least the bore the part needs, and open all the way through, is kept (the part is not drilled); a narrower or blind one is drilled to size. Each is reported in fastener.existingHoles[] (part, dia, shape, through, kept, enlarged, toDia,...). A threaded landing in an existing hole already as wide as its bore is refused fastener-bad-parameter, naming nut: true or tapped: false.
  • The head-side washer sits on the first part's entry face, and the head on the washer; it used to be placed inside the part. fastener.headUnderside records where the head bears.
  • cad_fastener_audit recognises nuts and washers by their geometry. A body with no declared role and no hardware name is measured: a bored hexagonal prism matching ISO 4032/4035 is a nut, an annulus matching DIN 125-A / ISO 7089 is a washer. A recognised nut is scored as a nut (no more "8 mm vs 10 required" tapped-landing reports) and must be coaxial with the shank. New stats.washers and stats.detectedHardware; rules.detectHardware:false turns it off.
  • cad_fastener_audit audits across documents. Bodies from several live documents are measured together in world space instead of refusing fastener-audit-cross-document; the new documents field names them and the response lists them.

Import

  • Trivial ops beside a big STEP import are fast again. A document whose first op is a cad_import caches that import's result, keyed on the op, the resolved file's size, mtime and content hash, and the build options. A cold replay (the first write after an import, a write after cad_delete_body, a journal restore, a branch, a second document importing the same file) restores it instead of translating the file again: about 12-21 s down to under 100 ms on a 23-body assembly.

STEP reading

  • TRIMMED_CURVE trims are honoured: an edge on a trimmed curve is its basis curve between the edge's own vertices, so a cylinder's trimmed rims no longer import as straight chords (a filter canister imported flat before).
  • Two halves of one closed curve are not duplicates: a rim written as two complementary edges on one circle, ellipse or periodic B-spline is no longer reported as a duplicate pair (a FreeCAD fan reported 166 on a clean shell).
  • Import health checks volume against the bounding box: a closed shell that is flat, empty or larger than its own box is flagged instead of passing as clean and watertight.
  • Planar outlines project along the plane's normal, so a tall, thin wall standing at an angle is no longer collapsed into a segment that "crosses itself".

↑ all releases

v0.22.0 “de Waart”

released October 13, 2026

sha256 nativecad-mcp-0.22.0.mcpb eadc05df3041f6042332cdcd01671e51d6f3dd041b0d462aee8137d96fa1c9f1sha256 nativecad-mcp-0.22.0.plugin 1b958f567431f5f33c9703e9555e39bf7920d46a454b1debbf23dad1d82fcc7fsha256 nativecad-mcp-0.22.0.tgz 7101577b1d18b594637e7208278d99c6ac72c9e5087bf4637762842353ba92a3

Coupled simulations, easing edges, the Space Program add-on, and sharper sketch and mesh answers. 180 tools: the 178 from 0.21.0 plus cad_couple and cad_ease_edges. The command schema moves from 1.62.0 to 1.63.0, and every change is additive. Installs on 0.17.2 and later pick this release up through the self-update once it is published.

Coupling simulations: cad_couple

  • New tool cad_couple (plan | run | replay | status). It couples a fast reduced-order model with higher-fidelity solvers used as calibrators: the model is re-solved with each measured correction factor as an explicit input, in an outer fixed-point loop (under-relaxation or Aitken) with a pass cap. Every run ends with a named status: converged, not-converged, oscillating, step-failed (a solver timeout included), replay-exhausted or budget-exceeded. A spec is JSON (coupling.spec.v1: factors, model, calibrators, gates, loop, overrides), passed inline or as a path.
  • Provenance and replay. Every factor comes back as a calibration record carrying the source run id, solver and version, host, ticket, commit, file sha256 and the source run's own timestamp. run returns a record (coupling.run.v1) with a content hash. replay re-runs it with no solver at all and reports whether it reproduced that hash.
  • Gates with cited targets. Each gate checks one physical quantity against a target fixed in advance, and names the ticket that set the target. A missing quantity fails its gate. A run is ok only when it converged and every gate passed.
  • Solvers through the Solver Connectors. A live step runs SU2, OpenFOAM, Cantera or CalculiX through the connector contract, with a validated job spec and a wall-clock budget. Where the solver is not installed, the call is refused before anything is written: E_PRECONDITION, reasonCode SOLVER_MISSING, with a hint to run on the solver host or replay. Recorded calibrators replay results on any machine.
  • The turbojet's calibrations, ported. The Cantera burner fuel/air calibration reproduces today's cycle exactly. The SU2 nozzle Cd loop replays the two recorded SU2 runs, and its gate rows match what the original post-processing wrote. With the tighter design tolerance (|dCd| < 0.005) the replay reports that a third live pass is still needed. Method and limits: docs/coupling/COUPLING.md. There is no per-timestep CFD coupling, no conjugate heat transfer and no blade-row CFD yet.

Modeling

  • New tool cad_ease_edges: ease every exposed edge to a maker's radius (fillet, with a chamfer fallback) and leave the faces that mate with other parts sharp. The radius comes from radius or a rule ("hardwood" 2 mm, "plywood" 1 mm, "aluminium" 1.5 mm); mating faces come from mateWith ("auto" finds every face touching another body).
  • Blade root blends are faster and lighter. A root blend that took 7.4 s now takes about 2 s, and its STEP drops from 2.03 MB to 248 kB (an HP turbine: 17.8 s to 2.6 s, 3.2 MB to 456 kB).
  • Tessellation on spline faces: the deflection refine's start check measures the distance to the surface instead of a same-uv offset.

Sketches

  • A conflict reply names every conflict it found. When a point is shared beside a collapsed line, or put on a line next to a conflict, the reply names both conflicts instead of the first one found, and a zero-length line found before the solve is named as a second conflict.
  • conflictSearch.minimal is one measured rule: it is true exactly when deleting any one member of conflictSet leaves a set the solver answers.

Meshing and analysis

  • Volume meshing ignores stray vertices. Unreferenced input vertices are dropped before meshing (stats.unreferencedVertices reports them when there are any), and the boundary is now reported two ways: stats.boundaryVerticesKept (the vertex count) and stats.boundaryAreaCovered (the share of the boundary area covered by mesh faces), with ...AfterImprove readings on the final mesh. stats.boundaryPreserved is retired; read boundaryVerticesKept instead.
  • Cyclic modal results from CalculiX 2.17 are read correctly (one displacement block per degenerate mode pair), and a cyclic-disk solve can run in resumable chunks with a cache, so a long nodal-diameter sweep survives an interruption.
  • Supports for metal printing (LPBF) follow down-facing skin, respect keep-out faces, and no longer bridge from one part to another by default.
  • Host jobs are launched detached and tracked through their completion marker (CADSOLVER_HOST_JOB_INLINE=1 restores the old in-process path).

Space Program add-on

  • Design a craft, then fly it. cad_export_body with format:"vehicle" writes an ncsp.vehicle/1 manifest and mesh payload from the session's bodies, and cad_configure {addon:{action, id:"space-program"}} installs, opens and removes the signed Space Program add-on. An exported craft gets its systems derived from the manifest. The plugin carries a space-program-missions skill that goes from a concept to a mission file.

Bug reports

  • Improvements to the bug feedback loop and gathering. IP protections and checks enhanced.

↑ all releases

NativeCAD 0.21.x

v0.21.0 “DePreist”

released October 12, 2026

sha256 nativecad-mcp-0.21.0.mcpb 97afac58a26c4d4068306ad77fce975de3c1394e164b2f42bd12b4b3b22b7f33sha256 nativecad-mcp-0.21.0.plugin c6423c9d949a38ac8e8248b8bb02906af230d072b119806bc80ea178be7772d0sha256 nativecad-mcp-0.21.0.tgz 690d215b3d4ef0833e1f9cc968772ce576b8ce2e9591d2f00dce26b7ff089c30

Solver Connectors, and a turbojet designed, simulated and costed end to end. 178 tools: the 177 from 0.20.0 plus cad_feature_edit. The command schema moves from 1.57.0 to 1.62.0, and every change is additive. Installs on 0.17.2 and later pick this release up through the self-update once it is published.

Solver Connectors: SU2, Cantera and OpenFOAM

  • One connector contract for external solvers. A job spec and registry that every solver plugs into, live progress readers that report residuals and percent complete while a run is going, field export that maps solver results back onto the model, and solverd, the background service that queues, runs and tracks solver jobs.
  • SU2, Cantera and OpenFOAM host tooling. One-command setup for SU2 (v8.5 release builds), Cantera and OpenFOAM, on plain Linux, Fedora or Docker, with convergence checks on every run and the results written back next to the design.

The turbojet pilot, end to end

  • Tail cone and a re-sized exhaust nozzle, confirmed with an SU2 compressible-flow result.
  • Cantera combustor network: stability margin, emissions (NOx, CO, unburnt hydrocarbons) and the blowout limit, on one chart.
  • Rotor integrity: a turbine-disc packet with burst margin and stresses from a 1-D disc model.
  • Off-design maps for compressor and turbine, a costed bill of materials, and a station diagram of the whole gas path.
  • Start-up animation with balanced streamlines, held to a gas-path rule so flow only goes where the hardware routes it.

Modeling

  • New tool cad_feature_edit: change an existing feature's parameters in place ({"diameter": 6.6}) and rebuild everything that follows, instead of re-creating it. Returns the before/after diff.
  • constrain_face and dimension_face document operations: drive a face by a constraint or a dimension (schema 1.62.0).
  • Boolean fuzzy tolerance: booleans take a fuzzy tolerance so near-coincident faces from imported or hand-placed parts merge cleanly (schema 1.60.0). Cone, sphere and torus intersections are now computed in closed form inside the boolean.
  • Chamfers that hold up: a chamfer band can run onto a spherical face, and saddle corners no longer use up the operation budget before the chamfer is tried.
  • Thread run-out: the run-out strip checks that the diameter fits its supports before building, and modelled bolt shanks can be partly threaded to the ISO 4014 / ISO 4762 thread length.
  • PMI carries through copy, mirror and pattern, with a warning naming anything that could not be carried. Patterns are written as instances under AP203e2 / AP214 so hole callouts survive.
  • Gears: straight bevel, spiral bevel and hypoid (zero offset) through cad_gear.

Lattices, additive and export

  • Lattice cellAspect and buildDir: stretch the cell per axis and set the build direction (schema 1.58.0 / 1.59.0). The lattice report gives selfSupport, the minimum strut elevation against that direction, so a lattice that prints without supports can be designed rather than guessed.
  • Conformal lattices run as one continuous frame across multi-face skins, and TPMS wall thickness is calibrated.
  • cad_export_body format "ply" and print profiles with resolution (schema 1.61.0). Field bodies too large for memory now stream to 3MF and to the print profiles instead of failing.

Viewing and checking

  • Section caps without z-fighting: cut faces in section views render cleanly, and a coincident-face check flags overlapping faces before they cause trouble in a render, a boolean or an export.

Long calls, fonts, sandbox mode

  • Long calls detach into async jobs by estimated cost. cad_fea_run whose server-side estimate (target cells x bodies x refinement solves) is over ~40 s, and cad_import of an STL/OBJ of >= 100k triangles, now answer with {jobId, stage, etaMs, auto} instead of the inline result; fetch it with cad_job_result {jobId, waitMs} (same response as the inline call). In-process tools.call callers stay synchronous. CADSOLVER_MCP_JOB_COST_S moves the line (off disables). Jobs submitted with a pre-flight estimate (mesh_to_brep) now report a non-null pct (elapsed / estimate, capped at 95) and etaMs while running, flagged estimate: {ms, basis:"preflight"}; measured component progress still wins where the build reports it.
  • Bundled fonts now ship in the .mcpb. cad_engrave_text {font:"dejavu-sans" | "dejavu-sans-bold" | "liberation-sans"} failed from an installed extension with ENOENT ...kernel/fonts/<file>.ttf because the staged kernel never carried fonts/. The .ttf files and their licence texts are now staged assets (src/font-assets.js). A bundled font id whose file is missing is now refused on field font with "bundled font ... is missing from this install", naming the fonts that can be used, instead of a raw ENOENT. cad_server_info {section:"services"} gains services.fonts = { bundled, missing, unmanifested }.
  • cad_picogk fixes from the first real picopie run. volumeMm3 of a hollow result (shell, TPMS sheet) is now its solid volume; it used to be the enclosed volume, about 3.7x too high on a 1 mm shell. kind is "brep" only when to_brep actually built a B-rep; when it did not (TPMS and lattice meshes trip mesh_to_brep's inline budget), the result is kind: "mesh" with a to_brep warning and a next. The result STL is imported welded, so it is watertight. imported is populated (it was always null). A result of 100k or more triangles no longer comes back with handle: null from a jobMode: false call.
  • Sandbox mode (NATIVECAD_SANDBOX=1): the server keeps file access inside its workspace and refuses outbound network at runtime, for regulated and air-gapped deployments.

↑ all releases

NativeCAD 0.20.x

v0.20.0 “de la Parra”

released October 9, 2026

sha256 nativecad-mcp-0.20.0.mcpb 9832fa56b95f31304dfb3b4416932b4b5612cfe291a921f3e2f1868ea442b90asha256 nativecad-mcp-0.20.0.plugin aad48560539bcb4ac3b9f9f485728281cf5490be4670b689c267383ba73db807sha256 nativecad-mcp-0.20.0.tgz a83120a4930eee566171759c6c59a4f5921f69fe2fddfc36f279071b20381e5f

Metal 3D printing: DMP Flex 350 build prep and quoting in one call. 177 tools: the 172 from 0.19.0, plus cad_am_prep, cad_picogk, cad_combustion, and two document ops that are now listed as tools, cad_face_region and cad_mutual_trim. The metal-AM chain (NLAM [2]–[9]) and the PicoGK interop (NLAM [20]–[22]) land together, alongside STEP-version save and compressed STEP. Installs on 0.17.2 and later pick this release up through the self-update once it is published.

Kernel SDKs now need a licence key: @nativecad/kernel 0.3.2 and @nativecad/kernel-sdk 0.2.2 refuse with E_LICENSE_REQUIRED until a key is active — a license key is required. Commercial: https://featureboard.ai/nativecad/web/pricing.html. Non-commercial (education, research, makers): keys are issued case by case — request one at support@featureboard.ai. The self-serve free key from featureboard.ai/register covers the MCP plugin only: the kernel and kernel-sdk refuse it with E_LICENSE_REQUIRED, reason free_tier_not_kernel (a key issued case by case carries the kernel entitlement). The kernel ships as nativecad-kernel-0.3.2.tgz. The MCP server's own licensing is unchanged.

Metal LPBF on the 3D Systems DMP Flex 350 (NLAM [2]–[8])

  • LPBF DFM pack lpbf_dmp_flex350: cad_dfm_check {process:"lpbf_dmp_flex350"} checks minimum wall (0.8 mm), part envelope (275 x 275 x 420 mm), self-supporting overhang (45 deg) and trapped powder (lpbf_trapped_powder, blocking for an enclosed cavity with no drain hole). Rules live in data/dfm/lpbf_dmp_flex350.json.
  • Plate nesting: cad_nest target dmp_flex_350 nests on the DMP Flex 350 plate at the 5 mm LPBF default spacing, opens a second plate when one is full, and exports format "3mf" (one object per part). Asking an LPBF nest for "3mf-bambu" is refused E_INVALID_ARGS.
  • Build orientation: cad_build_orientation pack lpbf_dmp_flex350 ranks the six axes, the planar faces and a hint by support volume, the LPBF metrics and a recoater-risk proxy, and explains every term.
  • Time and cost quote: an LPBF build-time and cost model (ops/lpbfBuildCost.js, shop profile dmp_flex_350_default). Scan time scales with laser count, recoat time does not. Profile fields left null are listed in quote.assumptions, and sensitivity names at least three cost drivers. Calibration is in docs/additive/LPBF_COST_MODEL.md.
  • Metal supports: cad_support profile lpbf makes block, line and cone supports with teeth and fragmentation, only under real overhangs past the pack's self-support limit.
  • Plate export and CLI: a 3DXpert-ready plate folder with one 3MF (and an STL fallback) per object, covering each part copy and each support region, plus plate.json with the transforms and open CLI layer slices. The machine's job file is still written in 3DXpert, since DMP Control accepts no public job format.

Metal build in one call: cad_am_prep (NLAM [9])

  • New tool cad_am_prep (group print, budget family report): LPBF DFM (pack lpbf_dmp_flex350) -> orientation -> machining-allowance print body -> LPBF supports -> DMP Flex 350 plate nest -> time/cost quote -> files (plate folder with 3MF/STL per object, plate.json, CLI; STEP print body, AP214). The schema is published top-level only, and cad_help has every field.
  • New refusal vocabulary in details.code: AM_PREP_DFM_BLOCKED (E_GEOMETRY; details.blocking, details.findings, details.remedies), AM_PREP_NEST_UNVERIFIED / AM_PREP_NEST / AM_PREP_ORIENTATION (E_GEOMETRY). force: true carries on past a DFM block and records it.
  • Sales proof pack for L41: samples/leads/L41-next-level-am/am-prep/ has an NGV doublet (qty 1) and a bracket (qty 20) run end to end.

PicoGK interop and cad_picogk (NLAM [20]–[22])

  • PicoGK file io: cad_export_body takes format "picogk_stl" (a binary STL that is watertight, has outward normals and uses chord tolerance voxel/4, with voxelSize in mm) and "vdb" (OpenVDB, through the optional picopie add-on). cad_import reads a PicoGK STL or .vdb back with voxelSize and toBrep, which rebuilds a B-rep and reports fidelity. voxelSize on any other format is refused E_INVALID_ARGS. When picopie is missing, the refusal is named PICOGK_ADDON_MISSING.
  • New tool cad_picogk: runs PicoGK voxel operations (voxelize, offset, shell, smoothen, boolean, TPMS implicit, beam lattice) on NativeCAD bodies as an async job, and brings the result back as a mesh body, or a B-rep with to_brep. The default voxel size is 0.25 mm. PicoGK runs outside this process. With no host, the call is refused E_UNSUPPORTED / PICOGK_RUNTIME_MISSING with the install remedy before a job is queued. Host-side setup is in docs/interop/PICOGK.md.
  • cad_picogk mode cem: cem: {project_path, entry, params?, output_dir?, maxOutputMB?} builds a customer's own PicoGK / ShapeKernel C# project with the.NET SDK, runs it headless, and imports every STL / VDB it writes under output_dir. Paths are sandboxed to CADSOLVER_CEM_ROOT (by default the output directory), and absolute, .., symlink and output_dir escapes are refused by name. When there is no.NET SDK, the call is refused E_UNSUPPORTED / CEM_DOTNET_MISSING with the install remedy. PICOGK_IMPORT_FAILED is returned when an output cannot be imported.
  • Explicit + PicoGK showcase: samples/leads/L41-next-level-am/explicit-picogk/ contains a cooled liner (one exact solid) and a lattice route, with the stand-in labelled when PicoGK was not used.

Combustor chemistry: cad_combustion (CCOMB [2/9])

  • New tool cad_combustion: Cantera chemistry for early combustor design — equilibrium (adiabatic flame temperature), laminar flame speed, ignition delay, and PSR/PFR reactor networks with staged air, NOx/CO/UHC, combustion efficiency and a PSR blowout search. The listing is top-level only (mode, fuel, phi, T, p, the rest under spec); cad_help({tool:"cad_combustion"}) has the full schema.
  • Cantera is an optional add-on and is never bundled. Without it the call is refused before any job is queued, E_PRECONDITION with reasonCode CANTERA_MISSING (or CANTERA_PYTHON_MISSING / CANTERA_WORKER_MISSING) and the pip/conda remedy in details.install. Bad requests are refused E_INVALID_ARGS / CANTERA_BAD_REQUEST.

Turbojet sample, cycle, meanline and SU2 (kernel only, not MCP tools)

  • Turbojet sample: samples/leads/L41-next-level-am/turbojet/ has the impeller, diffuser, NGV, turbine and liner built from the meanline, with each part watertight, re-read from STEP AP242 within 0.1 % volume, with LPBF DFM and orientation recorded and nested on the DMP Flex 350. The assembly is in assembly.step.
  • The 1D turbojet cycle, the meanline-to-geometry step (packages/cfd/src/cycle/meanline.js) and the SU2 compressible RANS provider (packages/cfd/src/providers/su2*) are in the kernel and are used by the sample. They are not exposed as MCP tools in this release.

Two document ops now listed as tools

  • cad_mutual_trim (command schema 1.52.0) and cad_face_region (command schema 1.54.0) are derived from the command schema like every other document op, so they appear in tools/list. The command schema is now 1.57.0 (lattice_fill keepSolid / frame / field output and the field op). Every change is additive, so documents valid at 1.54.0 still validate.

STEP versions: save as AP203, AP214 or AP242

  • cad_save and cad_export_body (format:"step") take schema: "ap203e1", "ap203e2" ("ap203"), "ap214", "ap242e1", "ap242e2" ("ap242", the default), "ap242e3", or "source" (keep the schema an imported file declared). "Save this as AP214" writes FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')) with the matching application context.
  • The result carries schema {id, fileSchema} (on cad_save as report.saved.schema), plus dropped / expanded / warnings when the schema could not carry part of the document — semantic PMI and tessellated are AP242-only; AP203 ed1 has no colours, layers or validation properties and is written flat.
  • strict: true refuses such a write instead: E_STEP_SCHEMA_UNSUPPORTED, nothing written. On cad_export_body it is the same strict that already makes the assembly check refuse.
  • schema on any other cad_export_body format is refused E_INVALID_ARGS / STEP_SCHEMA_FORMAT; ap242-xml stays AP242. A call that names no schema writes exactly the bytes it did before and reports no schema key.

Compressed STEP:.stpZ

  • A cad_save / cad_export_body (format:"step") path ending in .stpZ / .stpz, or compress: true, writes the file gzipped — the compressed STEP ("STEP-Zip") CATIA, SolidWorks, NX and most PDM systems open. bytes is then the file's size and uncompressedBytes the Part 21 text's (a NIST CTC-01 part: 235 KB -> 40 KB, 5.8x). Output is byte-reproducible (gzip MTIME 0, OS byte 255).
  • cad_import reads .stpZ back, and any STEP whose bytes start with the gzip magic, whatever its name; a truncated one is refused STEP_GZIP_CORRUPT.
  • compress on any other format is refused E_INVALID_ARGS / STEP_COMPRESS_FORMAT, nothing written.

↑ all releases

NativeCAD 0.19.x

v0.19.0 “Christie”

released October 6, 2026

sha256 nativecad-mcp-0.19.0.mcpb 61ba9d2cebe3bb41fbb966a975b3e431f35f0acee7b5d9e9522a7f2d4f9d441dsha256 nativecad-mcp-0.19.0.plugin fc2bf26357e881c14713cc40b87fa6a7ea2de9287fd006f73789394fe0c0b7aesha256 nativecad-mcp-0.19.0.tgz 8f3166aa8091213737f00d768a649c6630f87cbec289431e0cb7dea8297b453c

172 tools: the 170 from 0.18.3, plus cad_rtl and cad_synth. Kernel and tool fixes from master since 0.18.3: a new boolean rung that trims a solid of revolution by one analytic surface, an FEA path with no dead end when no solver is installed, cad_pcb action:"package", and eight reported-issue fixes. Installs on 0.17.2 and later pick this release up through the self-update.

Modelling kernel

  • A round boss on a cone or cylinder can be trimmed at the wall. A revolved body cut by one plane, cylinder or cone (subtract, intersect, or cad_split_plane with a cylinder or cone cutter) now goes through an exact trim: the cut rim is a closed B-spline within 1e-6 of the part size of both surfaces, with that tolerance recorded on the edge, and one analytic cap face. These cuts used to be refused (unsupported_overlap, the genBool validity gate, or "crosses 3 faces") or came back as hundreds of faceted faces; they now answer in about a second with 9 faces. Refusals are named rvtrim_* and leave nothing behind. CADSOLVER_NO_REVOLVE_TRIM=1 turns it off.
  • Cylinders unioned onto a prism's flat cap stay analytic. Four r16 pads onto a drone cross fold in about 0.3 s (was 1.6–1.9 s, and 39 s on 0.17.x), with an exact closed-form volume. CADSOLVER_NO_ARC_PRISM_UNION=1 turns it off.
  • cad_shell on a part with a narrow neck refuses by name (shell-cavity-pinched, with the widest wall that fits) instead of failing in the general engine.
  • Sliver and spike fixes in surface continuity analysis (cad_surface_analysis): a face inversion next to a C0 kink no longer reads a leaning normal, so phantom G1 breaks are gone.
  • Sketch solves that converge through zero radius are read by their mirror instead of answering a negative arc; a line collapsed to nothing returns a warning.
  • Drawings: a part with two equal principal moments (a symmetric frame) is oriented by its model or symmetry axes, not by eigen-solver noise.
  • Import health reports faces whose tessellation does not cover the face (tessellation_coverage_mismatch).
  • The kernel bundle is about 150 KB smaller gzipped: the DFM family, tolerance stack and sketch load lazily again.

FEA without a solver

  • cad_fea_case no longer needs a solver installed, and exports the case as an Abaqus/CalculiX .inp or Nastran .bdf deck from one mesh, each with how to run it.
  • cad_fea_run takes deckOnly. When no solver is found, the refusal solver_missing now carries the deck and a setup block (download, package-manager line, retry) instead of a dead end. A solve is refused only when a solver is declared absent. cad_modal behaves the same way.

PCB and chips

  • New tool cad_rtl: action lint (verilator), sim (iverilog + vvp over a JSON test-vector table or a cocotb module: per-test pass/fail, the first failing cycle with the signals around it, a waveform excerpt, an optional VCD) and cov (toggle coverage). A missing simulator is refused E_PRECONDITION with reasonCode:"tool_not_found" and how to install it; long simulations run as a job.
  • New tool cad_synth: yosys synthesis against a PDK liberty, with a lint gate, area and cell statistics, latch / combinational-loop / unmapped-cell findings and a reproducible script. A missing yosys or liberty is refused by code.
  • cad_pcb takes action:"package" for package-level checks (FCBGA, chiplet, HBM, wire-bond) and a die/package/board co-design check.
  • Ampacity is compared against openly published data, and ODB++ reading gains the KiCad 9 dialect, .Z/.zip containers and pinned stackup attribute names.

Fixed reported issues

  • A round boss at an angle on a cone could not be trimmed at the wall.
  • A multi-tool union refused non_connected_result when one tool only joined after the others; it is retried and the union refuses only when the set is really disconnected.
  • An extrude with a hole loop crossing or leaving the outer profile, or with crossing holes, is refused by name (E_PROFILE_HOLE_CROSSES_OUTER, E_PROFILE_HOLES_CROSS).
  • A closed B-spline rim was flagged as a zero-length degenerate edge, so watertight spline lofts failed the check.
  • A union through a planar face with holes did not weld on every side.
  • cad_tube_sweep reports its volume from the analytic result without tessellating.
  • A lug on a revolve's seam plane fanned flat triangles across the cylinder.
  • build_scene --part audits only the named parts.

↑ all releases

NativeCAD 0.18.x

v0.18.3 “Chan”

released October 4, 2026

sha256 nativecad-mcp-0.18.3.mcpb ac8a17c9441288b598f8815ba7d15563648f4482571b7f83145a8dcd4e9d312dsha256 nativecad-mcp-0.18.3.plugin e4c30887f8f881d92df427f79754b79588ff726e8bcb193f7e43e744ccdb801asha256 nativecad-mcp-0.18.3.tgz d0b20ea1690fdfe17ec3612794ffb08b9a0a8d51d05c54a5eafde698f151385b

170 tools, the same as 0.18.2. New command: nativecad-impactdiff, the PCB revision impact diff for servers. No tool changes. Installs on 0.17.2 and later pick this release up through the self-update.

  • A second bin in the package: npx -p nativecad-mcp nativecad-impactdiff --before a.kicad_pcb --after b.kicad_pcb prints impactDiff v1 (the same JSON cad_pcb {action:"diff"} returns). --repo/--before-ref/--after-ref/--path reads two commits without a checkout.
  • --batch jobs.jsonl runs many pairs through one engine (boot once), one result line per job: the shape of "every commit of this repo".
  • --targets targets.json passes the project's target values (impedance classes, rails, cert, stackup, ...) to the diff, so impacts are judged against limits.
  • Offline by default: telemetry and auto-update are off for this command; with CADSOLVER_FLEET_TOKEN the engine runs in worker mode (licence never written to disk).
  • Exit codes for CI and workers: 0 ok, 1 an impact at or above --fail-on, 2 usage, 3 licence required, 4 refusal or engine error. Output is deterministic (no wall time, no temp paths). Guide: docs/pilots/bomblame-embedding.md.
  • Needs nothing from node_modules (a built-in stdio client talks to the bundled engine), so an air-gapped server can unpack the .tgz and run node package/bin/nativecad-impactdiff.js in place.

↑ all releases

v0.18.2 “Canellakis”

released October 4, 2026

sha256 nativecad-mcp-0.18.2.mcpb ea26315b958e1e890481cb0ffc05d95857b59aa1af924176c1b1876884c88e8fsha256 nativecad-mcp-0.18.2.plugin d9afaee9592a221209a054bb0b994b74d3274b20f06d02a1131e4712f8a5d528sha256 nativecad-mcp-0.18.2.tgz b06c11f6b0f1ebc013eec1a5fb0bba1f1a436eef11799938d86b4c1d16249dd0

170 tools, the same as 0.18.1. US spellings for the colour options. Installs on 0.17.2 and later pick this release up through the self-update.

  • cad_set_part takes color as well as colour; cad_save and cad_export_body take colors as well as colours. The two spellings are the same option and write the same file. Giving both with different values is refused COLOUR_ALIAS_CONFLICT; the same value twice is accepted.

↑ all releases

v0.18.1 “Bülow”

released October 4, 2026

sha256 nativecad-mcp-0.18.1.mcpb 69b73c66762935029e49b431e03cac9701102b33f80f4312102610b69e8a9dbfsha256 nativecad-mcp-0.18.1.plugin c5afb06649228173c09ec570a21f0cec22cbf769224e67c4e9a9e512e955d0a8sha256 nativecad-mcp-0.18.1.tgz 898fb81ea3f527af362a77aeaca88a8fb4531f1bd4753ab1df37a041d4a99161

170 tools: the 168 from 0.18.0, plus cad_cert (certification evidence plan and packet) and cad_feature_pattern (pattern a pad, pocket or hole inside one body). Part colours in STEP exports. Installs on 0.17.2 and later pick this release up through the self-update (cad_configure {update:"now"}, or by themselves under autoUpdate:"auto").

Colours in STEP exports

  • cad_set_part takes a colour ("#RRGGBB", "#RGB", or [r,g,b] with 0..1 or 0..255 components). The colour is written into every STEP export (cad_save, cad_export_body format step and ap242-xml) as one body-level STYLED_ITEM / COLOUR_RGB, the AP214/AP242 form Onshape, SolidWorks, Fusion and OCCT read, and into x_t as SDL/TYSA_COLOUR. cad_set_part echoes it back as hex.
  • cad_save and cad_export_body take colours, a map from a body handle, op id or part name to a colour, for a one-off colouring at export time. A key that names no body is refused COLOUR_NO_BODY, with the document's bodies listed; nothing is written.
  • Colours persist by default. A coloured STEP that is imported and saved comes back out coloured. A coloured part keeps its colour through later edits (fillet, chamfer, boolean, transform, pattern): a face the edit created takes the colour of the face it came from, or the part's colour. Part names and colours set with cad_set_part also survive later edits of the body; before this release a stamp was dropped by the next modelling call.
  • CAx-IF model styling. Body colours with face overrides and layer assignments (including hidden layers) follow the CAx-IF Recommended Practice, checked by tools/caxif/styling_check.js. The written colours, layers and assembly tree were read back by OCCT (cadquery-ocp 7.8.1) and match.
  • Known gap: cad_save still names bodies by op id; cad_export_body writes the cad_set_part names.

↑ all releases

v0.18.0 “Brico”

released October 2, 2026

sha256 nativecad-mcp-0.18.0.mcpb 64c5e8108fde6d717df7f3d12f0add768037eafc7fff3f18af7dce4d61ce7c0bsha256 nativecad-mcp-0.18.0.plugin 29330abe734ea021699f04cd3f571f031436a5db3e09112ef5aaadb1844dfcdbsha256 nativecad-mcp-0.18.0.tgz 4e2ce764e8e4e6e14a8e7d5ce6e385f388d6712f00008a9b64722806f4f6393d

168 tools: the 166 from 0.17.2, plus cad_configurator and cad_interfaces. Nothing was removed or renamed. Installs on 0.17.2 pick this release up through the self-update (cad_configure {update:"now"}, or by themselves under autoUpdate:"auto").

New tools

  • cad_configurator works with product families: a JSON file that lists the modules a product can be built from, which ones are required, how they may be combined, and where they connect. Its actions are:
    • validate: checks a family file and a chosen configuration, and names every rule it breaks.
    • enumerate: lists the valid configurations.
    • nearestFix: for an invalid configuration, the smallest change that makes it valid.
    • build: places each module at its interface frames (with offset, rotate and flip), builds the parts, names them, and returns the bill of materials. It can also export the product as one named STEP file with an interference report.

The family can be given inline or as a path. Schema v1.2 adds interface mate frames, module geometry given as a tool call, a STEP path or a batch, part quantities, and a root. v1 and v1.1 files still load. Placement errors are refused by name.

  • cad_interfaces finds how a part connects to other parts: hole patterns (rectangular, circular, linear, with pitch, diameter and thread, from PMI or an ISO 273 guess), pin headers, threaded bores (CS-mount, C-mount, tripod), named port bodies, and mounting faces. Each one comes back with a coordinate frame, in the form Onshape uses and in the form a product-family interface entry uses. On the official Raspberry Pi 5 STEP it finds the 58 × 49 mm M2.5 pattern and the 40-pin header frame exactly.

Drawing to STEP, in the plugin

  • The Cowork .plugin now ships a drawing-to-step skill. It walks Claude through turning a PDF or DXF drawing into a STEP model, a Parasolid x_t and a dimension table, and through checking the result against the drawing. It includes a batch read-out script for a folder of drawings (inventory and a summary CSV) and a fallback for scanned (raster) PDFs.
  • Tested on Raspberry Pi's own drawings for the HQ Camera, Camera Module 3 and Active Cooler. cad_import drawing mode reads a sheet whose line work is all on one annotation-named layer (e.g. "Dims") by stroke style instead of refusing it (PDF_NO_VIEWS).

Cleaner exports

  • The STEP header no longer carries an author or organisation by default. Both are empty unless you set them (an export option or an environment variable), and originating_system names NativeCAD and its version.
  • STEP assemblies carry AP242 assembly validation properties by default, next to the per-part volume, area and centroid they already had. The CAx-IF validation-property checker grades the Raspberry Pi GS camera assembly 28 geometric and 2 assembly rows OK, 0 bad. A receiving system can use them to confirm it read the parts and the assembly correctly.
  • The Parasolid x_t writer no longer splits a token across two lines. Long strings used to be wrapped at 80 columns, which some readers rejected.
  • Part names, colours and the assembly structure survive a STEP round trip, which is what an Onshape import keeps (Onshape reads AP242 geometry and face colours, not PMI).

↑ all releases

NativeCAD 0.17.x

v0.17.2 “Berglund”

released October 1, 2026

sha256 nativecad-mcp-0.17.2.mcpb cb3e61dd66ae56601fcc1198b9058eeabac62181386edfacee4d547782360195sha256 nativecad-mcp-0.17.2.plugin a195e27bc303661e6e4016beb73ad88a44ba46692c737336240c407bd9188eaesha256 nativecad-mcp-0.17.2.tgz 45234d505d882175c99ec3819da819e0138a53822ffa848f536cb8a04776a685

166 tools, the same as 0.17.1. The self-update fixed for real installs. Installs on 0.17.0 and 0.17.1 cannot install this release by themselves, because of the two bugs it fixes, so install it by hand once. From 0.17.2 on, updates install themselves.

  • A check answered from the cache no longer locks every later check. In 0.17.0 a cache hit finished synchronously, so the code meant to clear the in-progress marker ran before the marker was set. The finished check then stayed in place, and every later check, update:"check" and update:"now" included, kept answering whatever the cache said. On the first install seen this way, that was "0.16.0 is the latest" for a day, while a signed 0.17.1 was live.
  • A Claude Desktop install is recognised as one even when it runs on the system Node. The install type now comes from the files around the running bundle (manifest.json → mcpb, .claude-plugin/plugin.json → plugin, node_modules → npm, src/server.js → dev), not from process.execPath. A .mcpb that Claude Desktop launched with the system node used to report edition: "dev" and only ever notify.
  • A cached manifest older than the running version is ignored and fetched again, rather than trusted for 24 hours.
  • The build smokes (bundle-smoke, mcp-smoke) run with NATIVECAD_AUTO_UPDATE=off, so a build can no longer write a stale cache into the builder's own ~/.nativecad.

↑ all releases

v0.17.1 “Bátiz”

released October 1, 2026

sha256 nativecad-mcp-0.17.1.mcpb 3d2521ff3f92524924a9e9045ca54937d419e75c99ade42f2bc03d30bcd464a4sha256 nativecad-mcp-0.17.1.plugin 64d1ae92879556716b427eb01b055ce7f65f9302f188320d3490b5f18d783ba5sha256 nativecad-mcp-0.17.1.tgz 493472b7728e707402b5628e59de7d90a9627ded9b4c080aaf398a7a8e280d80

166 tools — the same 166 as 0.17.0. The only change is the listing copy. This is the first release delivered by the self-update that shipped in 0.17.0: an install on 0.17.0 sees it on _meta.update and in cad_server_info.update. Run cad_configure {update:"now"} to install it, or set autoUpdate:"auto" and it installs itself.

  • The listing says what NativeCAD does. The Claude Desktop extension page (.mcpb description and long_description), the Cowork plugin description, and the npm package.json description now describe the product: modelling, the import formats, DFM, FEA, drawings, PCB analysis, printing, and the self-update. Keywords were added for dfm, fea, drawings, pcb, 3d-printing and sheet-metal.

↑ all releases

v0.17.0 “Asahina”

released October 1, 2026

sha256 nativecad-mcp-0.17.0.mcpb ddf040d1638547066363ae4af490c382ec1a4dd102fc865839e1c6a1a6515b60sha256 nativecad-mcp-0.17.0.plugin ab8eb44513abbdd0d86a5aa4871368570e96bc9f5d99e67b5be1c635228368efsha256 nativecad-mcp-0.17.0.tgz 0e085391dc385c3853787ca32272a11c5c4460065a55b62e1014a5e632613abb

166 tools: 159 from 0.16.0, plus 7 new ones. Nothing was removed or renamed. The new tools are cad_drawing, cad_enclosure, cad_gdt_position, cad_pcb, cad_sheet_convert, cad_sketch_solve and cad_surface_analysis. The headline is that NativeCAD now updates itself. This is the first release that can, so 0.16.0 and older have to install 0.17.0 by hand once.

New tools since 0.16.0

  • cad_pcb analyses a PCB layout without building solids. It reads .kicad_pcb files, Gerber X2 folders and IPC-2581 .xml. Its actions are:
    • analyze: impedance, DC drop, ampacity, thermal, UL 796 / IPC-6012 / creepage checks, and return path.
    • bom: the parts list.
    • diff: compares two revisions, given as files or git refs, and answers impactDiff v1 (below).

Every result carries an assumptions ledger, and a missing input is refused by name. cad_pcb needs a licence key.

  • cad_drawing turns a part or an assembly into a shop-ready drawing set: PDF, plus SVG and DXF per sheet (below).
  • cad_enclosure builds a base-and-lid enclosure around a board envelope, with port cut-outs, mounts suited to the manufacturing process, a snap, screw or tray lid, and vents.
  • cad_gdt_position sizes and checks the position tolerance of a hole pattern from its fasteners (ASME Y14.5 floating and fixed fasteners, with an Ⓜ/Ⓛ bonus-zone wall check).
  • cad_sheet_convert turns a solid mould into sheet metal: flat blank, rips, unfold and cuts.
  • cad_sketch_solve solves a constrained 2D sketch. It returns exact coordinates and the remaining degrees of freedom; when the sketch cannot be solved, it names the smallest set of constraints to delete.
  • cad_surface_analysis is a read-only class-A surface check: continuity (G0/G1/G2 per shared edge), curvature, zebra stripes, deviation from a reference, and draft.

NativeCAD updates itself

  • Release check. The first tool call of a session starts one background GET of https://featureboard.ai/nativecad/releases/latest.json, cached for 24 hours. It sends no body and no identifiers, and it never blocks or fails a call. When a newer release exists, every response carries _meta.update ({current, latest, verified, action, message}), and cad_server_info has a new update section (mode, channel, edition, install route, latest, verified, rollout, any rollback).
  • cad_configure settings. autoUpdate: "notify" (the default) reports new releases, "auto" installs them, "off" makes no network call at all. updateChannel: "stable" (the default) or "beta". Both are persisted like the other settings. update: "check" forces a fresh check, and update: "now" installs the latest verified release in any mode except off. Under off, update is refused with E_PRECONDITION.
  • Only signed releases install. latest.json is now signed with ed25519 (signature, keyId, and files[{key, edition, file, url, sha256, size}]; tools/sign_release.js). It is verified against the public key built into the server (key id d2791510af4077e7), and the download must match the signed sha256 and size. A release that is unsigned, or signed with any other key, is reported but never installed, and the notice says why. The key can be rotated through nextPublicKey, inside the signed body.
  • Where installs happen. Updates install into Claude Desktop / Cowork .mcpb installs and Cowork .plugin installs: the verified archive is written over the directory the running bundle was loaded from. An npm install is told the npm command, and a source checkout is told to use git. Nothing is written while a tool call is in flight. The running process stays on its own version, because the kernel worker is pinned to a snapshot of the old bundle. Restarting Claude finishes the update.
  • Rollback. The previous version is kept under ~/.nativecad/install-backups/. A version this updater installed is on probation until one of its boots proves healthy (the first successful tool call, or 60 s up). The start that finds 3 boots of it died first restores the previous version, skips the bad one from then on, and says so on the next response. A version the publisher lists in haltVersions is rolled back the same way. channel, rolloutPct and haltVersions in the manifest stage a release.
  • State lives in ~/.nativecad/update/ (NATIVECAD_HOME moves it). The other environment variables are NATIVECAD_AUTO_UPDATE, NATIVECAD_UPDATE_CHANNEL, NATIVECAD_UPDATE_URL (https, or a 127.0.0.1 address for tests) and NATIVECAD_UPDATE_DIR. See docs/UPDATES.md.
  • 0.16.0 and older have no updater, so 0.17.0 has to be installed by hand once. Releases after it install themselves.

cad_pcb: action: "diff" and action: "bom"

  • cad_pcb {action:"diff", before, after} compares two revisions of a board and answers impactDiff v1 (schema: "nativecad.impactDiff/1", docs/PCB_MCP.md section 6.3): the layout diff, the BOM diff, then the analyses re-run on both revisions for the changed nets and parts only, as impacts[] with before / after / delta / limit / severity / why / causes, worst first, plus summary {counts, headline, markdown}. The same inputs give byte-identical JSON; a pair with no change gives impacts: [].
  • Revisions from git. board, before and after take {repo, ref, path}: a local repository read with git plumbing (rev-parse, cat-file, show, ls-tree; execFile, no shell), never checked out. The .kicad_pro and .kicad_sch next to the board come from the same commit. New refusals: pcb_git_unavailable (E_UNSUPPORTED), pcb_git_ref_not_found (E_NOT_FOUND), and pcb_diff_incomparable (E_PRECONDITION, one side has nets and the other does not). A ref starting with -, or a path that is absolute or contains .., is pcb_bad_input.
  • cad_pcb {action:"bom", board, schematic?} answers the parts list in BOMblame's split: lines[] with category SCH / PCB / BOARD_FEATURE, groups (totals per category) and excluded.
  • Changed meaning: diff and bom no longer refuse with E_UNSUPPORTED / pcb_option_not_built. Without their boards they refuse E_INVALID_ARGS / pcb_bad_input, naming the missing argument.
  • A diff whose changed set needs dc or thermal on both revisions runs as an async job unless jobMode: false. cad_pcb's tools/list entry grows 392 B, to 2984 B, inside its 4 KiB line.

cad_drawing: make the drawings

  • New tool, cad_drawing. One call turns a part or an assembly into a shop-ready drawing set. The input is a session body (of) or a customer STEP file (file). It plans the views, sheet size and scale; adds a shop profile's title block (from docMeta), general tolerance block and revision table; applies the default GD&T scheme and its table (gdt: "auto", gdtStyle, process, fasteners); adds dimensions with hole notes; and, for an assembly STEP, adds a parts list from its product structure. It writes a PDF (one page per sheet) plus an SVG and a DXF per sheet. The reply is {sheets:[{n, size, scale, views, files}], lint:{errors, warnings}, gdt:{datums, frames, rationale, tablePlacement}, warnings}: paths, never bytes, under 32 KB.
  • The tolerance lint is a gate. A sheet with a dimension that resolves to no tolerance source, or to more than one, is refused with E_PRECONDITION, details.reasonCode: "drawing-lint-errors", and details.offending naming the dimensions. No file is written. allowLintErrors: true writes the sheet and says so in warnings. The other refusals are drawing-file-not-found, drawing-file-not-step, drawing-bad-scale, drawing-no-views, drawing-no-solids, drawing-failed (E_GEOMETRY) and drawing-write-failed (E_IO).
  • It runs on the kernel worker when the worker holds the document, through the read seam (READ_OPS.DRAWING), and emits one progress event per sheet. It draws from a scratch copy, so the session document is not changed: section cuts never add bodies to it.
  • cad_help({topic: "drawings"}) is the playbook. cad_design_knowledge's gdt pack points at the tool, and docs/TOOLS.md has a worked example.

↑ all releases

NativeCAD 0.16.x

v0.16.0 “Talich”

released September 29, 2026

sha256 nativecad-mcp-0.16.0.mcpb d46fee843a29c4ed77203edab019e9dd000b03fdebb4cf7b8b5428815b0e4a51sha256 nativecad-mcp-0.16.0.plugin 3d7eb0384c88005b95da106514a1be31c15f3f85ddb0a78922b62e9dce51719esha256 nativecad-mcp-0.16.0.tgz 94bf5e1897487831cdfe19b95e44b3473001edd3179f2bf063e60c34f0b9263b

159 tools — the same 159 as 0.15.0: nothing added, removed or renamed. Command schema 1.41.0 → 1.42.0, additive: import gains page, and mode: "drawing" accepts a .pdf. The headline is PDF: cad_import reads one page of a vector PDF drawing with mode: "drawing" and answers what the same sheet as a DXF answers. DXF import now also reads profiles on tilted planes, and a big nest or drawing fits in the reply. docs/DXF_IMPORT.md is the reference.

cad_import reads PDF drawings

  • cad_import { path: "sheet.pdf", mode: "drawing", page? } reads one page of a vector PDF (the usual manufacturer or datasheet drawing: outlines as path operators, dimensions as positioned text). The reply is the same drawing a DXF sheet gives: views, projection (first or third angle), each view's scale measured from its own dimensions, dimensions, title block, border and warnings. Nothing is built, and the reply carries a drawingId. saveCurves and drawingView (below) work as they do for a DXF.
  • Options. page is 1-based, default 1. units is "mm" or "in" and names the unit the dimension text is written in; the sheet itself is always paper millimetres. layers / excludeLayers filter on the PDF's own layer names, or on the reader's stroke-style names when it has none.
  • A .pdf is only ever a drawing. Without mode: "drawing" it is refused by name at validation, with the remedy (add the mode; for a sketch or a flat part, import the drawing's DXF). thickness, gapTol, space and layout on a .pdf are refused by name too, and so are units other than mm / in and page on any other path. All of these are E_INVALID_ARGS.
  • The report. entry.imported.pdf carries the page (index, count, size in mm, rotation), the PDF version, the layers and the role each one got, what the reader skipped (filled paths, images, clipping paths, invisible OCR text), the reader's own whole-sheet scale vote and the dimension text's unit. entry.drawing.page says which page was read.
  • A scan is refused, never guessed at. A page of images with no vector line work is PDF_RASTER_ONLY: detail.ocrText carries any invisible OCR text, and the remedy asks for the vector PDF, a DXF or a STEP. The other refusals, each on details.reasonCode with a remedy, are PDF_NO_VECTOR, PDF_NO_VIEWS, PDF_EMPTY_PAGE, PDF_ENCRYPTED, PDF_NOT_PDF, PDF_NO_PAGES, PDF_MALFORMED and PDF_UNREADABLE. PDF_PAGE_RANGE (a page past the end; the hint gives the page count) comes back as E_INVALID_ARGS, on the import and on drawingView.
  • Limits. A 3D PDF's embedded PRC / U3D model is not read. Text drawn as outlines is skipped, so a title block set in outlined glyphs has no fields. On a sheet dimensioned in inches, a view's measured scale reads the values as millimetres; a warning says so, and a title-block SCALE stands.
  • cad_import still needs the licence key, for a PDF as for every import since an earlier release: an unlicensed server refuses it with E_LICENSE_REQUIRED before reading the file.

DXF import

  • Profiles on tilted planes. Circles and arcs whose extrusion direction is off +Z, 3D LINEs and 3D POLYLINE / SPLINE points are grouped by the plane they lie in and chained on that plane, arcs kept exact. With thickness, a profile on such a plane is extruded along the plane's normal from the plane. Without it, the sketch is drawn on one plane (world XY when that has profiles, otherwise the tilted plane), and profiles on any other plane are warned about and left out. Each plane is reported on imported.dxf.planes3d with its frame, counts and normalSource. An entity that states an extrusion direction decides the normal ("extrusion"); otherwise it points +Z, else +Y, else +X ("canonical"), so an outline extrudes the same way whichever direction it was drawn in. A 3D SPLINE's end tangents are projected into the fitted plane.
  • Closed fit-point-only SPLINEs are interpolated periodically, so the seam is smooth (C2) instead of kinked. Open splines are unchanged.
  • Multiline attributes. An MTEXT-backed ATTRIB or ATTDEF is read whole, in either layout the DXF reference allows, and flagged multiline: true, so a long title or revision field carries its full value. In mode: "drawing" a multiline attribute is framed like an MTEXT, from its attachment point (group 71, reported as attachment), instead of as a one-line baseline.
  • Many bodies fit in the reply. Past 24 bodies (a DXF nest with thickness, or a many-solid STEP), each bodies[] row is compact {handle, name, bbox, volume, watertight}, the first 100 are listed, and bodyRows says so and where the rest are. imported.dxf.bodies[] rows become {bodyRef, name, holes} and profiles {id, holes, area}. A 44-outline nest now answers in about 47 KB (it used to spill to a file) and 400 outlines in about 67 KB. The budget hint on a spilled cad_import reply names the DXF remedies: import in pieces by layer, or as a sketch.
  • drawingView is a new argument that the MCP layer serves itself. With mode: "drawing", drawingView: {view, kinds?, offset?, limit?} returns one view's curves inline, about 40 KB a page, with next.offset for the next page, for a client that cannot open the saveCurves file. A page is a read and adds nothing to the document. drawingView with saveCurves, or without mode: "drawing", is E_INVALID_ARGS, and so is an unknown view (the error lists the views).
  • imported.dxf.bodies[].outlineLayers is unchanged on the wire; the executor now takes it from the bodies alone.

Also on the surface since 0.15.0

  • cad_drill_hole and cad_hole_pattern with type: "tapped" take ASME B1.1 Unified sizes (UNC / UNF #0 to 1", plus 1"-32 UN): "1/4-20 UNC", "1/4-20", "#10-32 UNF", "1-32 UN", with an optional "-2B" class. The bore is the 75 % thread tap drill (1/4-20 → #7, ⌀5.105 mm), and the callout reads like 1/4-20 UNC-2B ↧6.4, TAP DRILL #7 ↧8. Such a size used to be refused as an unrecognised thread size.
  • cad_rib without attachZ attaches to the base's top face, not z = 0, which used to refuse a rib on a plate. Its analytic.volume is now the whole resulting solid (base + rib), not the rib alone, so the mesh cross-check no longer reports a false mesh_volume_heavy on a good body.
  • cad_print / cad_print_status refuse a saved printer on a transport they cannot drive (anything but Bambu today) with E_PRECONDITION, details.code: "transport_not_implemented", naming the transport. A non-Bambu printer used to fall through into the Bambu LAN path and be refused for its file format.
  • Volume conservation for a union or subtract with a sphere is now checked over the intersection only. A correct result on a large, thin operand is no longer refused as volume_not_conserved and sent down the slow general path.
  • cad_server_info stays inside its budget: the perf section is capped at 4 KiB. It used to grow with the 512-row history until the reply spilled, and the spill then failed the tool's own output-schema check (/server must have required property 'server'). A spilled reply is no longer checked against a tool's output schema.
  • A declared per-entity tolerance now lifts to what it bounds, so tol(vertex) ≥ tol(edge) ≥ tol(face) holds for declared tolerances as it does for derived ones. A coincidence the declaration admits is no longer refused.
  • The blend-overflow keep-surface refusal names the heal codes that actually apply (healface_neighbours_not_planar, healface_seam_not_ruled, healface_seam_no_intersection), not healface_neighbours_parallel. Its reasonCode is unchanged.
  • A smaller listing. Descriptions are capped at 393 B (was 420 B) behind a shorter cad_help pointer, with no sentence lost, and shared fields have short briefs. A cut description no longer ends at "e.g." or inside a parenthesis.
  • NativeCAD also ships as a Claude chat plugin: nativecad-mcp-0.16.0.plugin carries the same dist/server.min.js as the .mcpb, plus the nativecad skill.
  • The browser modeller opens a .pdf straight into "Read as a drawing", with a Page box, and reads a DXF as a drawing too. That ships with the site, not in this package.

tools/list at 159 tools is 372,089 B (363.4 KiB), down from 382,298 B. That is inside the 385,024 B (376 KiB) line, and tool-list-budget is green.

↑ all releases

NativeCAD 0.15.x

v0.15.0 “Svetlanov”

released September 28, 2026

sha256 nativecad-mcp-0.15.0.mcpb 175540172f4c959a3f6c2c01d530f01a7dafaf09878b190c3f627ce27ad26ab6sha256 nativecad-mcp-0.15.0.plugin cf248183fe018339c64007cd898e21caf8c7acc996d166327b5b47c57f0e8e77sha256 nativecad-mcp-0.15.0.tgz b34db023749f89b9c88585056cb2d268108a83684959b21df7e75d187176995d

159 tools — seventeen added since 0.14.1; nothing removed or renamed. Command schema 1.27.0 → 1.41.0 (97 document operations, was 81), additive throughout. The headline is DXF: cad_import reads a .dxf. A laser-cut drawing becomes a 2D sketch, or, given the sheet gauge, an exact flat part named from its layers that cad_export_body writes out as STEP. An engineering drawing (mode: "drawing") is split into its views. docs/DXF_IMPORT.md is the reference.

cad_import reads DXF

  • One option decides what lands. Without thickness you get a 2D sketch in world XY. Every arc is still an arc, a sketch handle is minted over MCP, and cad_extrude_profile { profile: { sketch } } sweeps it exactly. With thickness: <mm> you get a flat part: each outer loop minus its holes, extruded +Z from z = 0, one exact solid per profile, largest area first. A hole is a real cylinder. An ELLIPSE, SPLINE or spline-fit POLYLINE loop becomes a rational B-spline wall whose volume matches the closed form. Block INSERTs and MINSERT arrays are expanded, mirrored and non-uniformly scaled ones included.
  • Bodies are named from the drawing. A body's name is the layer of its outline loop; layer 0 falls back to the file stem, and repeats get _2, _3. That name is the STEP PRODUCT name cad_export_body / cad_save write, what cad_list_model_state lists under parts, and what cad_assembly_bom groups by. It rides on the reply as name, imported.names and imported.dxf.bodies[].name. imported.dxf.bodies[].outlineLayers carries the outline's layer(s), taken from the import's own chain rather than a second read of the file.
  • Options. units overrides $INSUNITS; a file that declares nothing is read as mm and says so (sourceUnits.assumed). There are also layers / excludeLayers and gapTol (default 0.01 mm). On a non-DXF path, thickness, layers, excludeLayers and gapTol are refused by name, never silently ignored.
  • mode: "drawing" builds no geometry. It splits the sheet into views, frame, title block (fields read first from ATTRIB tags) and annotation, names each view's projection (first or third angle), and scales each view on its own. saveCurves: <path> writes every view's curves to a JSON file and reports it on drawing.curves. A drawing kept in a paper-space layout is read through its VIEWPORTs, with per-entity linetypes and ATTRIB values kept. space ("auto" by default, or "model" / "paper") and layout (a layout name, matched case-insensitively) choose the sheet instead of leaving it to the file (schema 1.41.0). The choice is reported on drawing.space; space.reason and the paper-space warnings name those two arguments. The title block carries its raw ATTRIB records as titleBlock.attributes ({tag, value, field, invisible, insert}, the first 50), with attributeCount and attributesTruncated. A value over 200 characters is clipped and flagged valueTruncated.
  • Nothing is dropped silently. entry.imported.dxf reports every layer, the entity types that were skipped, the profiles, the healed gaps, open chains with their gaps, withheld loops, and the drawing's dimensions / texts / ATTRIB values / hatches as data. If at least one closed profile exists, an open chain is a warning, not a failure.
  • New refusal vocabulary, each on details.reasonCode with a remedy: DXF_NO_CLOSED_PROFILE (the detail names the gap that would close the outline, e.g. retry with gapTol >= 0.5 (mm)), DXF_EXTRUDE_FAILED, DXF_UNREADABLE, DXF_BINARY, DXF_IS_DWG, DXF_NOT_DXF and DXF_NO_VIEWS. Two more, DXF_UNKNOWN_LAYOUT and DXF_BAD_SPACE, come back as E_INVALID_ARGS (the argument is wrong, not the geometry), with the file's layouts in the hint.
  • cad_import still needs the licence key, as every import has since an earlier release: an unlicensed server refuses it with E_LICENSE_REQUIRED before reading the file.
  • The browser modeller opens a .dxf through the same importer. Its dialog shows $INSUNITS and a preview of the drawing and its detected profiles. That ships with the site, not in this package.

Inline line/arc loops for cad_extrude_profile / cad_pad

A profile loop can now be written inline as { start, segments: [ {to} | {to, center, ccw} ] }, a third PROFILE_LOOP form (schema 1.39.0). It goes through the same self-intersection and orientation checks as the other two forms. New refusals: PROFILE_ARC_LOOP_SHAPE, PROFILE_ARC_BAD_CENTER, PROFILE_ARC_ZERO_SEGMENT. The DXF reader emits this form, so a filleted or scalloped outline extrudes exactly (profile.exact: true).

Also on the surface since 0.14.1

  • cad_defeature: removes every hole, boss, fillet and chamfer under a given size, or suppresses the blend networks of the given faces.
  • cad_feature_diagnose / cad_feature_suppress: diagnose reports every feature's state (ok / warning / error / suppressed / suppressedByFailure / stale) and, for one named feature, its refusal and the feature that actually broke. Suppress is the eye toggle that lets the rest of the tree build again.
  • cad_offset_faces / cad_replace_face: direct editing. Offset a set of faces together by one distance, or swap one face's surface; either way the neighbours are re-trimmed.
  • cad_ruled_surface / cad_offset_surface / cad_flatten_surface: ruled sheets, offset copies of B-spline faces as sheets, and a curved face developed flat with the stretch it costs measured.
  • cad_lattice_fill: a strut or TPMS lattice inside a solid, under an optional skin, with volume, density and cell/strut counts.
  • cad_topopt: topology optimisation to a volume fraction, over several load cases at once.
  • cad_nest: nests bodies on a Bambu X1C plate or in an SLS/MJF powder volume, proves clearance on the real triangles, and writes one 3MF.
  • cad_support: tree, grid or linear breakaway supports for non-Bambu FDM and resin, proven clear of the part.
  • cad_printer: a saved-printer list, so every print tool can take printer: "<name>" instead of an ip / serial / access code on each call.
  • cad_job_status / cad_job_result / cad_job_cancel: async jobs for long ops, starting with cad_mesh_to_brep jobMode: true. A cancelled job is rolled back, and cad_job_result then answers E_CANCELLED.
  • cad_gpu: manages the optional WebGPU addon (dawn.node). Nothing GPU-related is installed unless you ask for it.

tools/list at 159 tools is 382,298 B (373.3 KiB). That is inside the 388 KiB line and the 400 KiB ceiling, and tool-list-budget is green.

Fixed reported issues

  • Welded tube and box frames. A plate on an inclined tube now stays analytic (378 → 25 faces, worker ~500 → ~175 MB), and so does a cross tube butted onto an extruded spar. {convexity: "concave", all: true} now reaches tube-saddle edges, and the r = 3 weld fillet on the butted cross tube builds. The tube-sweep, elbow, fillet-radius and mitre parts of the report are filed as follow-up work.

↑ all releases

NativeCAD 0.14.x

v0.14.1 “Sawallisch”

released September 22, 2026

sha256 nativecad-mcp-0.14.1.mcpb cb3d2450231be3da8c79c36045015c1592119b26ee3891582f34efe64e51113esha256 nativecad-mcp-0.14.1.tgz 49c01136cc3c6db79d3e526bb65f3fb21b71e00ec6e7ec5b7e3cfa2ccc97a919

142 tools — two added since 0.14.0: cad_gear and cad_print_in_place_check; nothing removed or renamed. The point release exists so the .mcpb a person installs can be handed to someone with an X1 Carbon and nothing else: the unlicensed tier now BUILDS (below), tools/list is back under its ceiling, every x1c_* DFM rule carries a remedy, and the walkthrough that person reads ships with the release — docs/PRINT_WITH_CLAUDE.md, rendered at web/print.html.

The neighbour can install it without a terminal

  • Two OPTIONAL install-dialog fields on the .mcpb: Bambu Studio (bambu_studio_path → BAMBU_STUDIO_BIN) and Bambu Studio resources folder (bambu_studio_resources → BAMBU_STUDIO_RESOURCES). Both are fine blank: a blank field, or the unexpanded ${user_config.…} template a host may hand over for one (the shape), reads as UNSET and the slicer search falls through to PATH and the well-known install locations exactly as before. server.json declares the same two variables for a registry install.
  • docs/PRINT_WITH_CLAUDE.md — checklist, install, three first prompts, what cad_print_prep does stage by stage, how to read method:"slice" vs "estimate" and a dfm_blocked refusal, printing through Bambu Studio or Bambu Connect (LAN mode is explicitly OUT of the guide: the .mcpb does not ship packages/print, so mode:"lan" refuses print_deps_missing there and mode:"connect" is the path), a troubleshooting table keyed on the refusal codes.

tools/list is back under budget

The four budget assertions 0.14.0 carried red are green at 142 tools: packages/mcp/test/tool-list-budget.test.js and cadsb-1273.test.js pass. Field prose that only restated the keyword beside it is no longer published, dedupeSchema folds the most profitable repeated subtree (cad_face_blend 12,307 → 4,848 B with no prose touched), cad_export_body's 15-byte field-description stubs are gone (18,734 → 13,157 B, back inside its 16 KiB exception), and the per-tool budget tightens itself over 3 KiB / 6 KiB of schema. Nothing a validator reads changed and nothing left the surface. The total line is re-based to 388 KiB — 360 KiB is not reachable at this tool count without deleting published arguments, and the ticket records the measured floor — with the 400 KiB absolute ceiling unchanged.

Every x1c_* verdict has a remedy

ops/verdictRemedies.js now answers for the twenty single-body x1c_* rules the fdm_x1c pack measures (walls, pins, overhangs, bridges, text, elephant foot, the exclusion corner …) and for the six x1c_pip_* codes cad_print_in_place_check introduces (gap, fuse, zgap, escape, interference, ratio). docs/VERDICT_REMEDIES.md lists them; tools/design_loops/fdm_x1c.js converges on those remedies rather than on the two it had.

Also on the surface since 0.14.0

  • cad_gear: involute spur / helical / herringbone gears and a planetary set as an executor op (ops/gear.js, ops/helicalSweep.js), in the command schema with its validator; docs/GEARS.md.
  • cad_print_in_place_check: sweeps a gear train through its motion and measures every body pair against the X1C's printed-clearance floor, the fusion floor and the one-layer Z gap BEFORE a slice, and says whether a body can escape (the part was never print-in-place).
  • The fdm_x1c knowledge pack is published: data/dfm/packs/fdm-x1c.json → dist/fdm-x1c.{core,standard,full}.json, and on web/dfm-knowledge.html (29 packs, 1,571 entries).
  • executor report assembler / ops/transform.js, pick-anywhere placement invariants I1–I9, a flush-at-both-ends through cut stays analytic in ops/boolean.js, piping pack re-pinned, browser build-orientation panel, browser padstack panel, Labs pendant part 3.

The unlicensed tier is now EXPORT-gated, not verb-gated

A call that used to refuse now succeeds. An unlicensed server previously ran ten tools and refused the other 130 with E_LICENSE_REQUIRED — 7% of the surface. It now runs 130 and refuses ten: cad_export_body, cad_save, cad_restore, cad_versions, cad_import, cad_assembly_import, cad_mesh_to_brep, cad_mesh_payload, cad_quote_payload, cad_quote_submit. Modelling, query, measurement, DFM, FEA and the evaluation loop are open with no key. The boundary is the one contest mode has enforced since an earlier release — build anything, take nothing away — promoted to the default so there is one gate vocabulary rather than two.

What a caller keys off, and what changed under it:

  • E_LICENSE_REQUIRED still means the same thing and is still raised before argument validation. The tools it can name are now exactly the ten above.
  • details.allowedWithoutKey is GONE, replaced by details.deniedWithoutKey. A caller reading the old field gets undefined. The refused set is the short one now, and it is the one an agent that just hit the refusal needs.
  • cad_server_info's gate block carries deniedWithoutKey for the same reason, and gate.statement no longer says "only the evaluation loop is available" — a model that plans around that sentence will not try to build.
  • cad_license status returns gatedWithoutKey where it returned evalTools. gated still means "this server is unregistered".
  • licensing.EVAL_ALLOWLIST is replaced by licensing.UNLICENSED_DENYLIST; licensing.gateInForce() is new (isGated(null) still answers the same question, but the readouts now ask for it by name).
  • CONTEST_DENYLIST is unchanged — same twelve names, same order — but is now derived from UNLICENSED_DENYLIST plus cad_eval_packet / cad_eval_share. docs/CONTEST_CONTRACT.md §7 did not move.

↑ all releases

v0.14.0 “Rozhdestvensky”

released September 22, 2026

sha256 nativecad-mcp-0.14.0.mcpb 5415e20f510dcde5685dcc063386a3f624fff5df734d74326a926ea6f0c77c4dsha256 nativecad-mcp-0.14.0.tgz a4afe740311ad864a68baa53348553f7fda00e21228f9114dbbf670558920069

140 tools — ten added since 0.13.0: cad_build_orientation, cad_slice, cad_print, cad_print_status, cad_print_prep (a new print subsystem), cad_face_blend, cad_sheet_feature, cad_sheet_miter, cad_padstack, cad_trace_section; nothing removed or renamed. The theme is the Bambu Lab X1 Carbon: a body built in a session becomes a .gcode.3mf, and — on a printer whose owner has turned Developer Mode on — a job running on the plate. Command schema 1.27.0, 80 document operations, additive throughout.

The print pipeline

Six stages, each its own tool, and one tool that runs all six. docs/PRINT_PIPELINE.md is the reference; what a caller keys off:

  • cad_dfm_check pack fdm_x1c — 23 sourced rules over 14 concerns read off the X1C's own published profile numbers: the 256x256x250 mm envelope including the 18x28 mm front-left exclusion corner, minimum wall, overhang, bridge, free-standing pin, embossed text, elephant foot, Z anisotropy, multicolour purge cost. Rule codes are x1c_* alongside the generic min_wall_thickness / max_overhang_angle; the pack auto-orients by default before it judges.
  • cad_build_orientation — ranks plate orientations by a candidate sweep over the same pack's analysis, six normalised penalty terms, exclusion-corner fit; returns a row-major 4x4 transform the Bambu 3MF writer consumes directly.
  • cad_export_body format:"3mf-bambu" — a Bambu project, not a bare mesh: Metadata/model_settings.config, Metadata/project_settings.config, thumbnails, per-body filament slots mapped by colour. Bambu Studio opens it with no "not from Bambu Lab" dialog. 3mf (the plain one) is unchanged.
  • cad_slice — spawns a user-installed Bambu Studio / OrcaSlicer (BAMBU_STUDIO_BIN, then PATH, then well-known install locations) and hands back the .gcode.3mf plus the slicer's own predictionS and weightG. Always read method: "slice" is measured, "estimate" is ops/printEstimate.js — a {low, high} band, no file, refusal carrying slicer_missing. method is required by the output schema precisely so an estimate can never be read as a measurement.
  • cad_print / cad_print_status — mode:"lan" pins the printer's self-signed certificate against its serial (TOFU, in ~/.nativecad/printers.json or CADSOLVER_PRINTER_TRUST, never in the checkout), reads the AMS over mqtts://…:8883, maps each filament slot to a tray before the upload, uploads over implicit-TLS FTP on 990 (not AUTH TLS on 21), publishes project_file with the uploaded file's md5 and the ams_mapping, and waits for the printer itself to say PREPARE/RUNNING — started:true is the machine's word. mode:"connect" returns a bambu-connect://import-file?… link instead and is stamped started:null, so a Connect link is never read as a started print. IP / serial / access code come from the call or from CADSOLVER_PRINTER_IP / CADSOLVER_PRINTER_SERIAL / CADSOLVER_PRINTER_ACCESS_CODE and from nowhere else; the serial is masked in the report and the access code is never echoed.
  • cad_print_prep — stages 1–5 in one call and one report, each stage still dispatched through the registry. It refuses at the first failing stage under that stage's own code, with details.stage naming where it stopped; force:true carries a DFM block, orient:"keep" carries an orientation refusal, slicer_missing falls back to the estimator rather than failing.

packages/print (the mqtt + basic-ftp adapter) is the only part of this repository with npm dependencies, is loaded lazily, and is deliberately not in the .mcpb: in a packaged install mode:"lan" refuses with print_deps_missing naming npm install --prefix packages/print, and mode:"connect" keeps working.

New refusal codes

Every one carries its code in details.code.

  • cad_slice: slicer_missing (E_PRECONDITION, or the estimate), slicer_failed (E_INTERNAL, with the exit code and the stderr tail), slicer_object_outside (E_GEOMETRY), slicer_profile_mismatch (E_PRECONDITION).
  • cad_print / cad_print_status: printer_target_missing (E_INVALID_ARGS), printer_unreachable, printer_auth_failed, printer_not_developer_mode, print_ams_slot_unmapped, print_file_rejected, print_deps_missing, printer_cert_mismatch (all E_PRECONDITION). Each names the Bambu Connect fallback as the way to print anyway.
  • cad_print_prep: dfm_blocked with blockedBy naming every blocking rule, plus whatever the failing stage refused with and details.connectUrl when the send stage is what failed.
  • Booleans: tangent_contact_pinch — a nested tangency is now recorded rather than chained through, and the pinch is refused by name instead of producing a body with a zero-thickness contact.
  • cad_face_blend: 23 named face-blend-* reasons (face-blend-no-chain, face-blend-chain-ambiguous, face-blend-radius-unsolvable, face-blend-not-prismatic, face-blend-hold-line-unsupported, …) — a full round is solved, not chosen, and an unsolvable one says which of the 23 it is.
  • cad_padstack: 19 padstack_* reasons, including padstack_span_not_copper, padstack_pad_off_via_span, padstack_pad_smaller_than_drill, padstack_antipad_smaller_than_hole, padstack_microvia_not_adjacent, padstack_backdrill_ends_in_copper / _exceeds_span / _not_larger, padstack_stub_exceeds_max, padstack_plating_exceeds_drill, padstack_vias_overlap, padstack_volume_audit_failed.
  • cad_trace_section: trace_section_bad_route, trace_section_bad_parameter, trace_section_overlapping_conductors, trace_section_mask_thicker_than_gap, trace_section_volume_audit_failed.
  • Shaft keyways: keyway_unreadable — a keyway the B-rep cannot be read back off is refused by name rather than guessed at — beside the existing keyway_overlap, keyway_wider_than_shaft, keyway_reaches_axis, keyway_floor_outside_shaft.

topoMap on every modeling result

Every executor result now carries one map of what the operation did to the topology it was handed: { modified: [[oldId, newId], …], generated: [newId, …], deleted: [oldId, …], counts, source, derived }, keyed on STEP entity ids of faces and edge curves. An input id still present unchanged appears in none of the three lists and is counted in counts.unchanged — the OCCT / FreeCAD convention, and the reason the common case costs nothing. source says whether the operator handed its own correspondence up (operator — blends, chamfers and the booleans do) or the map was derived (derived-topology). Additive: nothing that existed on a result moved or changed meaning.

PCB: cad_padstack and cad_trace_section

  • cad_padstack builds analytic PTH / microvia solids from a stackup, and emits the via bore column as real bodies — fill, air void, back-drill void. A pad or antipad on a layer the via never reaches is refused. This release finishes the solid model: a meniscus fill top (a spherical cap on the ri ring, with a closed-form cap volume), resin bodies in the etched-away copper that share the pad and antipad walls, cap plating trimmed against same-side solder mask (and measured gone, not assumed), and split heights that agree across mixed vias because the bodies share edges, not merely coincident surfaces. BrepMassProperties closes a .F.-oriented wrapping face on the correct pole, which is what made those volume audits trustworthy.
  • cad_trace_section turns a trace cross-section into solids; this release sweeps it along a route (volume by Pappus, per body), makes neighbouring bodies share their interface faces by identity rather than by coincidence, and adds conformal plating in which paired conductors may differ in tFoil / tPlating.

Also on the surface since 0.13.0

  • Sheet metal. cad_sheet_feature (hem, jog, sketched bend, tab, cross-break, vent) and cad_sheet_miter (plane–plane miter). Bend methods beyond K-factor on the sheet verbs: bendAllowance, bendDeduction, bendTable, and gauge + material resolved through data/sheetmetal/gauge-table.json. Sheet-metal overrides are scoped to the document and autoDimension ids to the call, so one caller's override no longer leaks into another's session.
  • Chamfer. Asymmetric (two distances) and distance–angle chamfers.
  • Blend networks are on by default (after shipped them behind a flag at 0.13.0): a whole-selection blend is classified as a network and solved as one. CADSOLVER_BLEND_NETWORK=0, or networkSolver:false on a single call, is the opt-out.
  • cad_wind_tunnel gains sizing — a refinement zone with the uniform-vs-refined Cd delta stated.
  • Shafts. surface:'bore' on the shaft verbs, with shoulder and centre drill built on a closed meridian loop; an internal keyseat in a bore, and recoverKeyways naming a non-keyway axis-parallel face instead of mistaking it for one; _revolveProfiles pruned like _shaftProfiles.
  • Assembly. cad_assembly_contacts no longer reports a false interference between coaxial cylinders — two parts on the same axis with clearance between them read as clear.
  • Kernel, under the surface: Shewchuk exact predicates wired into ops/polygonBoolean.js; a .kicad_pcb reader into the neutral layout model; healFace's seam solve widened to plane/cylinder, cylinder/cylinder and plane/cone with arc re-mint; ops/envFlag.js centralises CADSOLVER_* gate parsing and IncrementalEval.tessKey keys the raw environment through envSig, so a cache entry can no longer outlive the flags that produced it; RENDER.writePNG is deterministic — measured, pinned, then tested.
  • cad_fea_run.meshQuality — the boundary re-cut and tet-quality pass on the public tool, no schema default.
  • Build and release gates (no runtime effect, but they are why this build can be trusted): the bundle-freshness gate re-hashes every file esbuild bundled rather than a recorded subset, commands/schema/generated/command-schema.v1.json has a staleness gate of its own, and tools/worker.sh repairs an unrepaired gitdir.

Known reds carried

packages/mcp/test/tool-list-budget.test.js is red in four of its budget assertions at 140 tools. tools/list measures 431,283 B across 140 tools — 421.2 KiB, over both the 360 KiB total line and the 400 KiB absolute ceiling; cad_export_body is 18,734 B, over the 16 KiB backstop its documented exception carries; and cad_face_blend at 12,307 B is the first non-exception tool past the 12 KB top-tool line. What still holds: 3,080.6 B/tool, a 39.9 % reduction off the 5,128 B/tool baseline (the growth-proof per-tool gate), the 3.3 KB/tool line, every tool description inside the 420 B cap (max 420 B), the cad_dfm_check ajv round-trip, and the cad_help round-trip block. Nothing was dropped from the surface to make the number smaller; the fix belongs to follow-up work.

What a caller might key off

  • Ten new tools; nothing removed or renamed. cad_server_info's subsystem list gains print and no longer needs an other bucket.
  • cad_export_body accepts format:"3mf-bambu" alongside 3mf.
  • Every modeling result carries topoMap (additive).
  • A boolean across a nested tangency refuses tangent_contact_pinch instead of building a pinched body.
  • A whole-selection blend is solved as a network by default — set networkSolver:false (or CADSOLVER_BLEND_NETWORK=0) for the pre-0.14.0 per-edge behaviour.
  • cad_assembly_contacts no longer reports interference for coaxial cylinders with clearance — a caller that filtered that false positive out can stop.
  • Sheet-metal overrides are document-scoped and autoDimension ids call-scoped; a caller that relied on an override outliving its document must set it again.
  • cad_slice's method field distinguishes a measured slice from a modelled estimate and is required by the output schema; a caller that reports a print time must read it.
  • mode:"connect" answers carry started:null — never treat a Connect link as a started print.
  • Command schema 1.27.0, 80 document operations (commands/schema/generated/command-schema.v1.json): additive fields only.

↑ all releases

NativeCAD 0.13.x

v0.13.0 “Rostropovich”

released September 22, 2026

sha256 nativecad-mcp-0.13.0.mcpb 7a911841be672c6640191609441d62a1b4cb0e977a472b1f17cc645b2a5f3434sha256 nativecad-mcp-0.13.0.tgz af5f7f7e4f7f95dc75443ac43e7e20cb385b5b2ee3e998d536732560dda8fff1

130 tools — three added since 0.12.4: cad_fea_case, cad_fea_run, cad_modal; nothing removed or renamed. The CalculiX / MYSTRAN finite-element arm ships in the npm tarball and the .mcpb for the first time.

The FEA tools ship

They were the private fea group since an earlier release because the group's preferred solver was a partner's product. This release splits the group: the CalculiX / MYSTRAN arm (the case contract, ops/feaCalculix.js C3D10 decks, ops/feaNastran.js, the CaeExporter writers) ships; the partner-solver adapter and cad_blade_qualify (blade qualification) stay in a private group, pending the commercial agreement. docs/AGENT_FEA_LOOP.md's five-call loop and tools/design_loop.js --instance bracket_fea run from an install with ccx on PATH.

  • cad_fea_run.solver is no longer an enum. "auto" (default) runs every registered solver adapter in order, then CalculiX; "calculix" forces ccx. A name this build does not know is E_INVALID_ARGS with details.reason: "solver_unknown" and details.available, never a silent substitution. An edition that carries a further solver adapter registers it under its own name and "auto" tries it first.
  • solver_missing hints name the CalculiX install routes (apt / brew / conda-forge / CADSOLVER_CCX).
  • cad_server_info.privateGroups no longer lists fea as withheld on a public boot; only the blade-qualification group is.

Two release gates, fixed

  • scripts/stage-kernel.js's dynamic-require scan matched any identifier that merely ended in require; the wave-3 landing's Tolerance.js guard _require(scope, fn) tripped it and no release build after 4d5c4097 could produce a bundle. The scan now matches only the loader's own require(.
  • With the three FEA tools public, tools/list was 354 KB against the 340 KiB line. cad_fea_case.rotor is published by reference (the full shape is on cad_modal; ops/feaCase.js validates every field either way — a static case with a rotor is still rotor_requires_modal), fatigueCorrection is one additionalProperties shape (an unknown factor is still material_invalid, by name), and cad_fea_case / cad_modal take the registry's 25 B field budget — full prose stays reachable through cad_help. 350.9 KB; the total line is rebased to 360 KiB at 130 tools, the 400 KiB absolute ceiling unchanged.

Also on the surface since 0.12.4

  • CADSOLVER_TOOLSET=full|core+expand|verb-payload is enforced at listing and dispatch (E_TOOL_NOT_AVAILABLE; cad_help opens a tool for the session).
  • server.json for the official MCP registry, with a version-drift gate.
  • Per-client journal call lines and pending slots; restoreDocument claims the default document only on crash restore; of:"all" is scoped to the caller.
  • This release descopes the cross-solver MODAL agreement band.
  • Live-body quotes no longer throw into file_unavailable.
  • cad_fastener_audit names stated-but-unmodelled fixings (unfastened[]).

Known reds carried

cadsf-1296.test.js (blade-qualification fixture predates the rotor.vaneCount requirement — private group) and cadsf-1448.test.js (a modeCount description regex written for the 100 B field budget lowered to 50). Both were red at 0.12.4; neither is a shipped behaviour.

↑ all releases

NativeCAD 0.12.x

v0.12.4 “Masur”

released September 21, 2026

sha256 nativecad-mcp-0.12.4.mcpb 1dd9a4879269b1370c421e7051a54501becde8edf3ff8e4217ac6626ea8777c8sha256 nativecad-mcp-0.12.4.tgz 9658319cdba251d6da9ad96b8dd813fc161a9c7f1cda2bdb3bc5305558f44e89

127 tools — one added since 0.12.3, cad_weld_schedule (landed after the 0.12.3 build); nothing removed or renamed. The theme is that the MCP surface reports what it actually did, and the known red 0.12.2 and 0.12.3 both shipped with is closed.

A partial batch comes back partial

cad_batch with stopOnError:false used to answer a failed op with an ERROR envelope that discarded every other op's result — the one setting whose entire purpose is "keep going" threw away what kept going. It now returns ok:false carrying every op's inline result or error, plus a skipped entry for an op that referenced a failed op's name. batch.test.js 66/66.

cad_gap_registry ships at all

Every .mcpb and .tgz shipped so far answered E_PRECONDITION for cad_gap_registry: registry.js located data/gaps/registry.json through a private __dirname escape that no packaged layout satisfies. It goes through kernelAsset() now, so stage-kernel.js and bundle-server.mjs stage and embed it like every other data file — 4 verbatim assets, was 3. The tool works in a packaged build for the first time.

cad_list_model_state names consumed inputs

Additive: handles.retired.solid and a consumed[] of {handle, kind, consumedBy:{op, opIndex}, stale}, so a caller can tell a handle that was eaten by a boolean from one that never existed. SessionStore.peekValue() keeps a listing from warming the LRU. cadsb-529.test.js 55 → 75.

An exact B-rep centroid and inertia

cad_mass_properties carries a centroid and inertia integrated over the same analytic faces as the exact volume — closed forms to 1e-15 on the box, the off-origin box, the cylinder, the sphere, the cone and the torus, where the tessellated inertia was 0.3–0.6 % out. Published under the agreement guard with per-field centroidSource / inertiaSource / meshCentroid / exactCentroidRefusal, and volumeSource now reaches the payload. 84 assertions.

The known red is closed

cad_license's offline, no-network posture moves into the first sentence-and-a-half of its description, so it fits inside the 420 B tools/list description cap. license.test.js is green again at 145 (201 with the gate file). 0.12.2 and 0.12.3 both shipped naming this red; 0.12.4 does not.

Kernel

  • A union whose tool is contained in the base (B ⊆ A) answers as a copy of the containing operand rather than a contact refusal.
  • The coplanar-continuation run-out's free end builds, and the free-end/split-rail pair refuses by name: 3 of the 6 rows build, was 1.
  • The equal-radius "ear" regime is analytic — 12 faces in ~25 ms where the general engine gave 907–1205 PLANE faces in 5–7 s.
  • A union across a tangent circle is non_connected_result and a subtract across one is no_material_removed, both naming the contact; the measured shared volume is exactly 0.
  • A blend fit probe with one unreadable end no longer discards the whole pick's overruns — LED_1812 at r = 0.045 goes dressed 24 → 28 and blend-result-not-watertight 3 → 0.
  • A root fillet running out onto a cylindrical wall the body carries as several faces of one cylinder is named blend-sel-cyl-wall-runout-split with its census.
  • Mass-property no-ops are no-ops again; surfaceArea comes off the same B-rep as volume, and an inverted shell no longer reports negative area.
  • cad_sheet_fold and cad_sheet_box self-gate on the scs_bend DFM pack they used to build past silently, with a material-independent min_bend_radius rule.
  • The bundle-freshness hash is a function of the commit, not the host.

What a caller might key off:

  • cad_batch with stopOnError:false no longer returns an ERROR envelope on partial failure — it returns ok:false with per-op result / error / skipped. Code that read the envelope and gave up must read the entries.
  • cad_gap_registry answers in a packaged build instead of E_PRECONDITION.
  • cad_list_model_state gains handles.retired.solid and consumed[] (additive; nothing removed).
  • cad_mass_properties centroid and inertia are exact on analytic bodies, with centroidSource / inertiaSource saying which path produced them; volumeSource is now present on the payload.
  • cad_sheet_fold / cad_sheet_box refuse a bend tighter than the DFM pack's min_bend_radius instead of building it.
  • New tool: cad_weld_schedule. Nothing removed or renamed.

↑ all releases

v0.12.3 “Marriner”

released September 20, 2026

sha256 nativecad-mcp-0.12.3.mcpb 167dedf4905a2938e6dc986c0abe2c8fd4ef2e5cfa5e654361f7dc948276117dsha256 nativecad-mcp-0.12.3.tgz 1ac423fb2efaa1bc6783eac1dac636c9361dddd0ed5e9bba0b42eb3ed3c9fe9c

Bumped on 2026-09-19 with the boolean-pair entry below and never cut; the release that carries this number was built on 2026-09-20 and also ships the day's work, summarised first.

A saddle corner whose concave edge is not picked now BUILDS

A cad_fillet_edges whole-body call, and any selection that picks the two CONVEX edges meeting a trihedral saddle vertex while leaving the CONCAVE one out, used to refuse blend-sel-vertex-saddle ("edge #N meets this saddle vertex but was not selected — the setback patch is bounded by ALL THREE strips' end arcs"). A blanket convex fillet can never select a concave edge, so on a mixed body that was every saddle corner. It builds now: the two strips lie on opposite sides of the concave crease and never overlap in material, so each simply ends against the other edge's support plane, the crease splits at the one point both wall rails reach (its point at distance r from the shared cap face), and the wall plane below that point carries a separate planar sliver. Nothing is set back — the volume removed is both strips in full. Measured: the L-block's blanket fillet goes from 15 of 18 edges to 17 of 18 (only the reentrant edge is skipped), its 2-of-3 pick at the notch vertex is watertight with a clean checkFull and removes 2·(1−π/4)r²·6 to 2.9e-5 relative; on 0001282f6783_LED_1812_4532Metric.step the blend-sel-vertex-saddle row of the whole-body histogram goes 16 → 0, and on 007bc1f1b1ea_PinSocket_1x05_P1.27mm_Vertical.step 4 → 0 (r = 1% of max extent, this tree).

Unchanged: a pick that takes ALL THREE edges at the vertex is still the setback family; a {corner, mid} variable-radius law has no single r to pin the split with, so the corner is declined by name and the blanket law refusal is byte-identical; single-edge results are unchanged; and cad_chamfer_edges has no such corner construction, so its blanket prune still drops one convex edge per saddle vertex.

Refusals name themselves, and so do kernel deaths

786 of the 1,202 previously unnamed refusals now carry a structured error.details.reasonCode. 418 of them were a leak, not a missing vocabulary: ops/blend.js, shell.js, boolean.js and chamfer.js already stamped .reasonCode at every throw, but the E_GEOMETRY envelope in tools/modeling was rebuilt from the prose and dropped it. cad_server_info publishes the vocabulary under reasonCodes; tools/refusal_reason_census.js keeps the count re-measurable. No message text changed.

Every kernel-worker death is now one of five codes — kernel-worker-module-missing (with details.missingModule), kernel-worker-exited, kernel-worker-shutdown-in-flight, kernel-worker-killed, kernel-worker-threw — carried on E_KERNEL_CRASHED as reasonCode and registered alongside the error codes. Module-missing and shutdown-in-flight are recoverable:false with their own hint. Four tool layers that used to overwrite the details bag with recoverable:true now merge into it.

The diagnostics payload family's ceiling moves from 48 KiB to 64 KiB (packages/mcp/src/budgets.js, docs/PAYLOAD_BUDGETS.md): cad_server_info at section:"all" with an open contest scope measured 54,987 B once the reason-code vocabulary was published, and the frame — not the field — was the binding constraint. The published-shape matrix workflow also installs the repo root's deps so its npm pack can build dist/server.min.js at all.

Six comparison thresholds (15°, 35°, 5°, 1° sines/cosines in DFM off-axis faces, crease splitting, coplanar-edge and ruled-along tests, smooth clustering) are literal constants instead of run-time transcendentals, so the same body classifies identically under V8 and JavaScriptCore.

What a caller might key off:

  • E_GEOMETRY refusals from blend / shell / boolean / chamfer that used to arrive with prose only now carry details.reasonCode; code that fell back to substring-matching the message can switch.
  • E_KERNEL_CRASHED always has a reasonCode; a module-missing death is no longer reported recoverable:true.
  • A cad_server_info document up to 64 KiB is a normal answer, not an E_LIMIT.

Kernel: the lattice builds, and three long-standing refusals move

The lattice scenario builds and the rounded cube reaches the general engine inside its meshFallback budget. A lathe-turned body whose wall is authored as a line-swept SURFACE_OF_REVOLUTION is recognised as the cylinder or cone it is, so it is a solid of revolution to blendRevolved again (the fillet itself still waits on). A multi-turn helical ribbon is no longer mistaken for a trimmed full-turn band by the tessellator — the thumb-screw thread's repair skin drops from 33,016 mm² to 160 mm² (flanks in). Fillet corner ends where one wall is a cylinder now get a construction instead of "a wall is not a PLANE" (they stop at the next named clause).

Four more boolean pairs answer exactly: the countersink, the O-ring groove, the ball end — and two silent lies

The sweep measured 30 boolean scenarios and ranked what was missing. The top three were not exotic — a countersink, a ball end and an O-ring groove — and all three are now exact, along with the two cases where a boolean returned a clean-looking answer that was not one.

An O-ring groove (a torus subtracted from a cylinder) was refused outright: the general engine's validity gate rolled back on 374–418 open_edge / non_manifold_vertex findings and reported unsupported_overlap. New ops/torusGrooveBool.js answers it in closed form: 8 analytic faces (2 plane, 4 cylindrical, 2 toroidal), the removed volume matching an independent quadrature oracle to 1e-9, checkFull clean, and the general engine never reached. It refuses by name where the shape stops being a groove — torusgroove_breaks_out when the groove is deeper than the wall is thick.

A CONE tool facetted on subtract: a countersink in a plate came back 200 PLANE faces, a bore lead-in chamfer 325, while the same cone UNIONED as a boss stayed analytic at 8. New ops/seatedPocketBool.js recognises the seated pocket — a tool whose mouth lies flat ON the base face and whose widest section IS that opening — and promotes it to a pierced face, but only when the tool straddles nothing anywhere, which is what leaves the tangency refusal untouched. Measured: 8 faces.

Every sphere boolean except subtract-from-a-plane facetted: a ball-end rod (sphere ∪ cylinder) returned 3,503 PLANE faces with the sphere gone, box ∩ sphere 2,125. New ops/sphereBool.js adds the coaxial ball-end rung to union (7 faces) and the clipped-ball rung to intersect — a sphere cut by planes all perpendicular to ONE axis, which is a half-ball, a lens or a spherical segment. A ball clipped on SEVERAL axes (the rounded cube) still refuses sphbox_multi_axis_clip by name and lands on the general engine exactly as before; that is the one remaining gap in the matrix.

A subtract whose tool never touched the base silently republished the base unchanged AND consumed the tool, while the identity block already knew vComplement was 0. It now refuses by name, quoting the measured vol(A) and vol(A∩B).

A boolean that removed everything published a 0-volume, 0-face, 0-edge body flagged watertight: true and ok. Refused as EMPTY_RESULT.

And the words now match the engines. The refusal hints named remedies that did not fit ("intersect analytic bodies" to someone who had just intersected two analytic primitives); the tool descriptions warned callers off a union measured at 29 faces and 0.15 s as costing "thousands of faces, tens of seconds". subtract, union and intersect now carry measured support matrices, and ops/booleanCensus.js routes by (operation, lost surface family) with an ABSENT entry meaning there is no analytic path — said plainly, rather than filled in with advice that costs a caller a turn.

What a caller might key off:

  • Three pairs that used to come back geometryDegraded with surfaceTypesLost naming cone, sphere or torus now return exact bodies with those families intact and no degradation flags. Code branching on report.geometryDegraded for a countersink, a ball-end rod or an O-ring groove will now take the other branch.
  • subtract with a tool that misses now THROWS instead of returning the base. Pass allowNoOp: true to keep the old behaviour — the tool is then left live and unconsumed rather than silently discarded.
  • A boolean whose result is empty now throws EMPTY_RESULT instead of publishing a 0-face body.
  • New refusal codes on the exact rungs, all of which let the ladder continue: torusgroove_breaks_out, sphbox_multi_axis_clip.
  • meshFallback is a smaller part of the story: the three shapes that most often sent a caller looking for it no longer need it.

↑ all releases

v0.12.2 “Mäkelä”

released September 19, 2026

sha256 nativecad-mcp-0.12.2.mcpb a6aec81f2e2f81f69fce6efb8e5cb8edccef7993e34022c692e284ddacd0d3f9sha256 nativecad-mcp-0.12.2.tgz a1752b9e262308fcd53afd2ca46a05bfe004ac3a03d4327525432541cb5e82ee

The equal-radius T-junction is analytic, and tools/list is back under its ceiling

A rod butting INTO another rod of the same radius (a T, as opposed to the cross 0.11.2 made analytic) was refused cyleq_not_through and fell to the general engine: 867 faces with the tool ending on the base's axis, 443 past it, and an outright unsupported_overlap refusal at tangency. Reported live from the web modeller. The boss path could not take it either: at r = R the tool's cap rim always reaches sqrt(s² + R²) ≥ R, so a root is never embedded and the cap always breaks the base's surface near the pinch directions — which is why this is a third construction rather than a parameter of the other two.

New cylCylEqualTee in ops/cylCylEqual.js builds it exactly, every curve a STEP ELLIPSE, CIRCLE or LINE and nothing fitted: the intersection is the same two planar ellipses the cross authors (a tool wall point is outside the base iff |s| > R|sin φ|), and the near cap's plane, being parallel to the base axis, is cut by the base in two straight chords at u = ±√(R²−sEnd²) — chords that are generators of the through cylinder, so the same two lines bound the cap slivers and cut the through wall. Measured: 8 faces (14 edges, 8 vertices) with the tool ending on the axis, 10 faces (20 edges, 12 vertices) short of it, V − E + F = 2, checkFull clean, worst edge tolerance 3.6e-15, volume within 1.5e-4 of a closed form that is itself pinned against its own integral. Either operand order works. The cross is unregressed at 12 faces.

tools/list was 416,675 B against a 409,600 B ceiling and had shipped red since before 0.11.1. Now 335,220 B across 126 tools: the per-description budgets tightened, seven opening sentences rewritten shorter and complete rather than hard-cut, and the boolean tools' duplicated engine-ladder paragraph deduped. The long-form notes remain in cad_help and docs/TOOLS.md, which is where they belong. No tool, parameter or first-sentence meaning was removed.

What a caller might key off:

  • The T-junction answers with regime: 'cyl-cyl-equal-analytic' and cylCyl.tee; it refuses by name — cyleq_tee_end_beyond_axis (the tool's cap past the base's axis but still inside it: two extra wall regions above the pinch, still the general engine's), cyleq_tee_no_overlap (external tangency — contact, not overlap), cyleq_tee_far_end_not_clear, cyleq_tee_is_cross — and the ladder continues on every one.
  • Tool descriptions are shorter on the wire. cad_help / describe_tool still carry the long form; nothing was dropped.

↑ all releases

v0.12.1 “Kertész”

released September 18, 2026

sha256 nativecad-mcp-0.12.1.mcpb d86a368e3a42f4d10b4e63ae4af5a70b57c3066f456ff91cb2da3079baf94921sha256 nativecad-mcp-0.12.1.tgz f777d2ecbdf7a3535067f53cb3e3227f8f90f4baf2023821649be6d990a56ccd

The fillet on a cylinder/cylinder saddle BUILDS — a rod through a rod, a boss on a rod, the equal-radius cross

The other half of Chris Wilson's report: with the union analytic, cad_fillet_edges on the junction still refused by name — blend-sel-non-planar-support, "a corner between TWO CURVED supports … a general CHANNEL SURFACE with no exact circle/torus answer". For this pair the offset surfaces are two more coaxial cylinders (R + ρ, r + ρ) on the same perpendicular, intersecting axes, so the rolling ball's spine is the SAME closed-form Steinmetz family as the union edge, and NEW ops/blendSaddleCyl.js builds the canal band from it: contact curves fitted on each wall with a measured, recorded tolerance; the band as a bicubic B-spline surface (one face per tool half) with exact ρ rim arcs; the wall's hole loop and the tool's wall pieces re-trimmed; the quartic edges retire. Concave — a weld fillet at the boss root — so it ADDS material. Measured on Ø10 through Ø20 at ρ 2: 14 faces, checkFull clean, contact curves on their walls to 1.4e-7, band at ρ from its spine to 4e-15, added volume within 2.5 % of a Pappus oracle, STEP round-trips. Works on both crossings of a through rod, one crossing, one picked half (expanded to its loop and reported), the single boss, the swapped-operand union, a tube's outer wall.

The EQUAL-radius cross fillets too: its four elliptic creases meet at two pinch points where the two walls share a tangent plane, so the band closes to a point there — one B-spline face per crease bounded by its two contact curves alone, no rim arcs, the pinch vertices going from valence 8 to

  1. Both variants report method: 'saddle'.

What a caller might key off:

  • cad_fillet_edges on these edges returns ok: true with method: 'saddle' where it returned blend-sel-non-planar-support.
  • New coded refusals when the shape is right but the radius is not: saddle-runs-off-tool-end (the tool's cap is inside R + ρ), saddle-runs-off-wall (the ±(r + ρ) contact band leaves the wall's end), saddle-runs-onto-seam, saddle-tool-not-capped (a bore bridge between two crossings), saddle-fit-tolerance. Every other pick falls through to the route it had before.

↑ all releases

v0.12.0 “Jurowski”

released September 18, 2026

sha256 nativecad-mcp-0.12.0.mcpb 22c4c39c801365ecf10159bad6aa98ee22c381b8323f4ca0a6531da4e0a34e76sha256 nativecad-mcp-0.12.0.tgz 72b8056a9b8d43578020252fe1b1e03a2c2cfbd3aa58d2626be73aed771adff2

cad_contest: enter a showcase contest without a licence key

One new tool, on the evaluation allow-list, and a fourth licence state under it. cad_contest start with a ticket from a contest page on featureboard.ai opens a contest scope for the server process: while it is open on an UNLICENSED server (and the contest's entriesCloseAt has not passed) the ordinary modelling tools run, and the twelve tools that would let the build leave the machine for the entrant's own use — cad_export_body, cad_save, cad_restore, cad_versions, cad_import, cad_assembly_import, cad_mesh_to_brep, cad_mesh_payload, cad_quote_payload, cad_quote_submit, cad_eval_packet, cad_eval_share (licensing.CONTEST_DENYLIST) — answer the NEW code E_CONTEST_SCOPE. cad_contest submit (with agree:true) exports the active document to glb in a private temp directory through an internal, Symbol-keyed bypass on the call context, renders a 512 px poster with cad_preview_body, POSTs the docs/CONTEST_CONTRACT.md §4 envelope and deletes the temp files whatever happens — there is deliberately NO offline bundle, unlike cad_eval_share. contests, status and end round it out. A LICENSED server is unaffected by the scope. Wire contract: docs/CONTEST_CONTRACT.md; build notes: docs/CONTEST_TOOL_NOTES.md.

What a caller might key off:

  • Three new codes: E_CONTEST_SCOPE (built-in, raised by the dispatch gate, details.deniedInScope lists the denylist, details.submitWith the way out), E_CONTEST_REJECTED (the site said no: details.status, details.reasons[], details.phase) and E_CONTEST_OFFLINE (the site could not be reached). All three are recoverable: true and in cad_server_info's errorCodes.
  • E_LICENSE_REQUIRED now carries details.contestHint naming the contest route. Message, hint and every existing detail are unchanged.
  • cad_server_info.gate.contest is present (unlicensed server, scope open) with the contest, deadline and deniedInScope; cad_help marks a denylisted tool with gate.code: "E_CONTEST_SCOPE" and no longer marks an ordinary tool as gated while a scope is open.
  • licensing.EVAL_ALLOWLIST gained cad_contest (ten names); cad_license status's evalTools reports it.
  • New env: CADSOLVER_MCP_CONTEST_ENDPOINT (site base, default https://featureboard.ai), CADSOLVER_MCP_CONTEST_TIMEOUT_MS (default 20000). Tool count 125 → 126.

(nothing yet)

↑ all releases

NativeCAD 0.11.x

v0.11.2 “Jochum”

released September 18, 2026

sha256 nativecad-mcp-0.11.2.mcpb 9cc1bc9a25749af40f9e0355701789099e8a1a490668bc6cf61fee435865e431sha256 nativecad-mcp-0.11.2.tgz 2c789c094662c04edf2519e738c6086fb1a00bf823a2cd36c3bf9621f86a3f92

Two crossing cylinders union analytically whichever is named first, and at EQUAL radii

Two cases 0.11.1 left on the general engine, both hit live within minutes of that release:

  • The thicker rod named as the tool. cad_boolean_union with the r5 rod as of and the r10 rod as tool (the web app's Union — f1 + f0) refused the cylinder/cylinder rung with cylcyl_tool_radius_too_large (it reads r_tool < R_base) and fell to the general engine — 500+ PLANE faces and several seconds — while the same two rods the other way round were analytic in 0.11.1. Union is commutative: ops/boolean.js now asks the rung the other way round before giving up on it (_tryCylCylSwapped, gated on checkLocal like the swapped pierce). The result carries operandsSwapped: true.
  • Equal radii — the Steinmetz cross. Two Ø2 × 20 rods crossed into a plus sign: r = R, which the rung refuses by name because its quartics degenerate there. The general engine answered with 1410 faces (or refused outright at 200 mm). At r = R the intersection is two exact planar ELLIPSES crossing at two pinch points, and NEW ops/cylCylEqual.js builds exactly that: both walls re-authored with seams through the pinch points, the eight kept wall pieces bounded by the four elliptic arcs. 12 faces, 20 edges, 10 vertices; checkFull clean; volume within 0.02 % of πR²(H+L) − 16R³/3; every edge an exact conic, measured and recorded (worst 2e-16); round-trips through STEP. regime: 'cyl-cyl-equal-analytic', union only.

Both were also the "unperformant" part: every one of these clicks was paying for a general classify-and-stitch boolean whose answer nobody could use.

What a caller might key off:

  • engineTrail gains the engines cylCylSwapped and cylCylEqual; a refusal by either is carried on the error as .cylCylSwappedRefusal / .cylCylEqualRefusal (codes cylcyl_swapped_invalid_result, cyleq_*).
  • cylCyl.equalRadii === true and cylCyl.pinchVertices on the equal result; cylCyl.crossings there are two kind: 'ellipse' entries.
  • A subtract at equal radii is NOT taken (it would sever the base into two lumps touching at the pinch points) and answers exactly as before.

↑ all releases

v0.11.1 “Järvi”

released September 18, 2026

sha256 nativecad-mcp-0.11.1.mcpb 9a4a9269a599d5a70d106179ea4d8c7a20d059174d0b1ebec7db166aee5564d2sha256 nativecad-mcp-0.11.1.tgz 5168caa599bf27897ed808fd265c27b5cdcf5bed3c179a4c6c8d79ef8a4db202

A rod unioned straight THROUGH a rod is analytic — two crossing cylinders no longer come back as triangle soup

Reported by a user (Chris Wilson): cad_cylinder × 2 with perpendicular, intersecting axes → cad_boolean_union → the junction and the thinner rod's wall rendered as dense striations, and cad_fillet_edges had nothing to pick. The cylinder-on-cylinder analytic path (ops/cylCylEdge.js) built a union only as a single BOSS — one end of the tool outside the wall, the other embedded — and refused a tool whose two ends are both outside on opposite sides (cylcyl_boss_root_not_embedded, "a union of a through rod is not the boss this path builds"). The pair then fell through to the general classify-and-stitch engine, which answered a Ø10 rod through a Ø20 × 60 rod with 554 PLANE faces: watertight, −0.14 % on volume, and no cylindrical face left on either rod.

The through rod is now BUILT on the analytic path as two bosses back to back: one Steinmetz quartic per side of the outer wall (the same (Ro, ∓1) crossings the subtract branch already takes), each bounding the tool's wall out to its own cap. On a TUBE the tool wall bridging the bore between the two bore quartics is kept as material boundary too. Measured: 10 faces for the rod pair (6 CYLINDRICAL + 4 PLANE, was 554), 14 for a Ø60 × 3 tube, checkFull clean, volume within 0.014 % of πR²H + πr²L − V_bicyl(R, r) at chord 0.001, every authored edge with a recorded tolerance, and the executor route (cad_cylinder + rotate → cad_boolean_union) returns the same 10-face body. The single boss, the skew pair and a rod that ends inside the far wall answer exactly as before.

What a caller might key off:

  • engineTrail / regime for this pair now reads cyl-cyl-analytic; the .cylCylRefusal cylcyl_boss_root_not_embedded no longer appears for a tool whose two ends both clear the outer wall on opposite sides. It still names a tool that ends inside the far wall (its cap disc crosses the cylinder).
  • The union's result carries cylCyl.crossings with TWO entries (side −1 and +1) for a rod, FOUR for a tube.
  • Filleting the four resulting saddle edges reaches ops/blend.js with a real cylindrical pick and refuses BY NAME, blend-sel-non-planar-support — the convex unequal-radius cylinder/cylinder canal band is follow-up work, filed and pinned by tests/test_cadsb-1005.js §F.

↑ all releases

v0.11.0 “Hrůša”

released September 16, 2026

sha256 nativecad-mcp-0.11.0.mcpb 38faadd3ec63b327309d99baf73a035b341d34e7de14ae1a53aa9fc8a94ae760sha256 nativecad-mcp-0.11.0.tgz cd330863cc8770f45b858dfba079605ad3b5fb709191a3c5da176a874eca2d00

A chamfer whose facet crosses a hole is BUILT against it; a fillet band merely tangent to one no longer refuses

cad_chamfer_edges on an edge whose facet footprint crosses a bore, pocket or island root used to do one of two wrong things on this line: publish ok: true with a self-intersecting body (a Ø6 bore at (10, 4) under a 3 mm chamfer on a 40x20x10 plate), or publish a body whose volume came back negative (the same place as a blind Ø6x4 pocket). It now BUILDS the right answer — the facet is trimmed against the feature's wall (plane ∩ cylinder, an ellipse arc, exact), the wall is re-trimmed with its new top boundary on the facet, and the rails are cut where the feature crosses them. The result is one watertight solid with the hole breaking through the chamfer; method reads selected-trimmed and the result carries footprintTrim naming the crossing edges and the setback that would have cleared them. Volumes measured within 0.008 % of plate − bore − wedge + (wedge ∩ bore counted once) on all five crossed constructions, and the bodies round-trip through STEP unchanged.

What a caller might key off:

  • A chamfer that previously came back ok: true and self-intersecting now comes back ok: true and clean, with one more face and a different face census. Nothing that built correctly before changed at all — an un-crossed pick still takes the loop rebuild (method: 'selected') byte for byte.
  • A NEW refusal code, chamfer-sel-footprint-crossed, is what is left when the trimmed build cannot run: a curved support face, an oblique run-out cap, two crossed picks meeting at a corner, or a cut the boolean declines. err.detail.why says which, err.measuredFeatureSize is the largest setback whose footprint clears the feature, and err.crossingEdgeIds names it. It is registered in the operator remedy table and classified in tools/blend_matrix.js under the existing footprint-crossed key.
  • cad_fillet_edges no longer refuses blend-sel-footprint-crossed when the band's rail is TANGENT to the feature (it touches at a point and crosses nothing — "exactly at the clearing radius" case). That body builds exactly, 6 PLANEs + 3 CYLINDRICAL_SURFACEs, checkFull clean. A band that STRICTLY crosses the feature still refuses by the same name, unchanged: trimming a fillet band against a bore is still open work (the band ∩ bore edge is a quartic with no closed form when r == R and the axes are offset), and parity B5 therefore stays ['REFUSED', 'PASS'].

↑ all releases

NativeCAD 0.10.x

v0.10.0 “Honeck”

released September 10, 2026

sha256 nativecad-mcp-0.10.0.mcpb ed779b4be51d0cb7f79d5b248e0ea622189c33e9353b022ce323d7c14c624f67sha256 nativecad-mcp-0.10.0.tgz 550eb3a844ed98e27712642840487e21030cc29a73168293375a198ba2f4fde7

Modelling writes build on the kernel worker — the protocol thread no longer parses a build the worker proved (the commit step)

The half an earlier change left open is closed. Every modelling write (cad_box, cad_extrude_profile, the booleans, blends, shells, patterns — everything routed through the document-op surface) is now replayed ONCE, on the kernel worker, and the protocol thread never runs Executor.run for it. Concretely, with two chats or two sub-agents driving one server: a 40-op part being rebuilt in one no longer stalls the other for the length of that rebuild — the other's calls answer while the build runs, because the thread that answers stdio is not the thread building.

  • The worker's reply IS the response. REQ.BUILD with commitIndex returns a commit block computed where the parse already is: the per-body summary (bbox, volume, watertight — modeling/summary.js's tessellate + checkLocal, now run on the worker), the hole-route advisory, the replay-regression verdict and whether the op landed in the env. runSingleOp reports a proven write from that block. Every refusal keeps its shape and code (E_INVALID_ARGS from the validator, E_GEOMETRY for a failed entry with its report, E_TIMEOUT for a cooperative budget, the E_INTERNAL regression refusal) — details.provenBy: "kernel-worker" says which side judged it.
  • The host's parse is lazy (src/session/lazy-parse.js). After a worker commit the document's docParser / env / consumedBy are accessors that replay the COMMITTED ops on this thread the first time a synchronous consumer (session.resolveBody, pmi, quote, review, output, evaluate) reads one — and only the committed prefix, never a candidate op still being proven. A chain of writes never parses on the protocol thread; a write followed by a read parses once. That is the "one parse per document per request". The cost that moved: a read that runs on this thread after a heavy write (a 500-copy pattern, say) now pays that document's host replay at the read rather than at the write — the same replay, later, off the write's critical path. Checkpointed replay applies to both sides now: the worker keeps its own checkpoint ring, so a build there replays the tail, not the journal.
  • One write at a time per document. A per-document write lock serialises runSingleOp, cad_create_sketch and cad_build_plan on the same document — two writes issued together both land, in order, and neither rolls back the other's candidate. Two documents still build concurrently.
  • The first op of a document builds on the host. A one-op document is a single primitive the host replays in milliseconds; the worker is spawned in the background by that call so the second op finds it warm. This keeps the first-call latency exactly.
  • CADSOLVER_MCP_WRITE_WORKER is ON by default, and CADSOLVER_MCP_OP_GUARD follows it (the pre-run is the build, so the guard no longer costs a second replay). CADSOLVER_MCP_WRITE_WORKER=0 restores the pre-1274 host-side build byte-for-byte — the A/B is one switch away. Inline kernel hosts (CADSOLVER_MCP_KERNEL_WORKER=0) are unchanged.
  • Behaviour a caller may notice: with writes keeping the worker warm, an offloaded read (cad_mass_properties) is now bounded by the worker's wall clock — under an impossible opTimeoutMs it is refused E_TIMEOUT naming itself, where it used to run unbounded on the host (the gap, now pinned either way). A per-document write is also what a crashing op costs: the thread that died is the worker's, the document rolls back, the session and every other document survive.
  • Pinned by tests/test_cadsf-1274-commit.js (30/30): the commit block, zero host builds across a chain of writes, one build at the first read, the acceptance measured (a read on the same document answers in ~1 ms while a 2.5 s union is stalled on the worker), the per-document lock, and the A/B. tests/test_cadsf-1274.js and tests/test_cadsf-1272.js updated for the flipped defaults and the closed non-goal.

Session isolation, batch ergonomics and the agent instructions

Found by the ten-chair text-to-CAD benchmark: two unguided agents sharing one server both built into the ACTIVE document — after a restart, the journal-restored previous part — so exports of "all" carried three parts and every whole-document op paid for all of them.

  • New tool cad_session_reset {confirm:true}. Closes every document and handle in the session, forgets the default document, drops the journal's restorable index and starts a fresh journal file. The epoch does NOT change. The next no-argument modelling call opens a fresh document. Refuses with E_INVALID_ARGS without confirm:true.
  • cad_batch accepts host-namespaced tool names (mcp__…__cad_box is cad_box), and each op in the result now carries ms. A batch is still not a transaction (see batch.js's header) — a failed batch keeps the ops that succeeded.
  • Instructions gained a "Documents, sessions and multi-part builds" section: no-argument calls land in the active document, start a part with document:"new" or a reset, multi-part objects are named bodies in one document checked for contact before export, sweep sections are line/arc loops, plan ids are not handles, sweep/loft bodies tessellate at tolerance ≥ 1. Everything a prompt previously had to say.

Every tool call is now timed, always

  • ToolRegistry.call records the last 512 calls — {tool, ms, ok, code, in, out} — into an in-memory ring (src/perf.js). Always on, no flag: the measurement has to exist for the call that was already slow. Measured overhead is well under the ticket's 50 µs/call bound (test/cadsf-1214.test.js).
  • cad_server_info gains a perf section: p50/p95/max per tool over that ring, plus the ten slowest calls still in it. Purely additive — no existing field moved or changed meaning.
  • Op-journal op lines now carry ms (the handler's own duration) when the journal is enabled. Additive to the NDJSON format; the reader ignores it, so journals stay readable in both directions.
  • CADSOLVER_PERF=<path> streams one JSONL row per call to that file. Buffered and never awaited by dispatch, with src/session/journal.js's write-path rules: one batched append, a 32 MB cap, and a fall back to memory-only after a single flush over 250 ms (the slow-mount case).

↑ all releases

NativeCAD 0.9.x

v0.9.0 “Hogwood”

released September 10, 2026

sha256 nativecad-mcp-0.9.0.mcpb 38a6c9621e5e9d46af464bf247f34483119011b2fde53224a3af861bc1da2fccsha256 nativecad-mcp-0.9.0.tgz 07bba19fd024ed24101010168f1053b4827342903ca4d62f5d499f7620d73300

Claude Desktop installs of 0.8.0: the kernel worker booted Electron instead of the bundle, and the first call replayed the whole journal

Both shipped early as the unofficial nativecad-mcp-0.8.0-hotfix2.mcpb; this is the release that carries them.

  • src/kernel-host/worker.js located the bundle from require.main.filename. Under Claude Desktop's Electron UtilityProcess that is Electron's own bootstrap script, so every tessellating tool on the installed .mcpb (cad_preview_body, cad_tessellate, exports) died with E_KERNEL_CRASHED — "the kernel worker exited (code 1)" — while cad_box and the other non-tessellating calls looked fine. The bundle prelude now records its own path (globalThis.__CADSOLVER_BUNDLE_FILE__) and the worker trusts that first, then argv[1], then require.main. test/cadsb-825.test.js loads a real bundle under the Electron-bootstrap condition.
  • Boot used to replay every document of every past epoch from the op journal before answering the first call — 109 s on a 25-document / 6-boot journal, past Claude's 60 s tool timeout, so the first call after every restart failed. Boot now only indexes the journal; a document is replayed by the first call that references it, and only documents touched in the last two boots are restorable (older ones are listed in the boot notice as trimmed, not replayed). Measured on the same journal: cad_server_info 194 ms, cad_box {document:"new"} 68 ms. test/cadsb-828.test.js, 24 assertions.

The modelling write path's pre-run returns the kernel worker's VERDICT, behind CADSOLVER_MCP_WRITE_WORKER (partial)

The op guard replayed every guarded write on the kernel worker purely to learn whether it terminates, then threw the answer away and replayed it again on the protocol thread. With CADSOLVER_MCP_WRITE_WORKER=1 (off by default — the old path is the A/B, byte-for-byte) the pre-run is kernelHost.build (contract.js REQ.BUILD) and the write path acts on the whole verdict: a validator refusal the worker already found is returned as that refusal, not re-discovered; a worker that DIED replaying the candidate becomes a coded, recoverable refusal with the document rolled back and the session intact, instead of re-running the op on the thread that cannot be interrupted; a build the worker proved is reported as proven. The open items each get an answer in code and in tests/test_cadsf-1274.js (34/34): item 2 — cancelling one request no longer refuses its neighbours as "cancelled at the client's request" (bystanders are re-sent and answer); item 3 — a read carries its tool family's ceiling across the boundary and one that outlives it is refused E_TIMEOUT rather than falling back onto the thread it was protecting (transferList is a documented non-goal); item 5 — a query-only session does not pay a second parse for its first offloaded read; item 6 — the worker path and the local fallback return the same value, and a refused document is not re-attempted on every later read. Still on the protocol thread: the commit. A successful write's response needs the live parser here (modeling/summary.js tessellates and runs checkLocal on every published body), so a build the worker proved is still run on the host — that last step is what deletes the host's parse and it is deliberately not smuggled in behind the flag. The ticket stays open for it.

x_t import takes "coarse but located" intersection charts, reads rational vertices homogeneous, and samples fin SP-curves over their own window

  • Chordal-error band. The cap on a CHART's declared chordal_error — max(file tolerance, 1e-3 × the chart's extent) — refused seven previously-green corpus pins (cadsb-548/628/633, cadsf-735/773/880/899): SolidWorks routinely declares 2–15 % on SHORT charts (a 0.6 mm fillet-corner arc at 20 µm, a 2.8 mm thread-crest chart at 0.43 mm), and every one of those parts imported within 1 % before the cap. A declared error above the cap but within 1 × the chart's own extent is now ACCEPTED on the plain path — recorded on the edge as its tolerance (the tolerant topology sizes its bands from it), counted under stats.curves.intersectionChartCoarse, and said once as a accepted coarse chart warning. Above the extent the worm-gear refusal (XT_UNSUPPORTED_CURVE / XT_CHART_CHORDAL_ERROR_EXCEEDS) is unchanged, and its detail now carries refuseAbove. Callers keying off the refusal for the 0.1–100 % band will see an import succeed with a warning instead.
  • Rational B-splines are homogeneous. An earlier change read a vertex_dim-4 XT vertex as Euclidean (x, y, z, w). The format stores (x·w, y·w, z·w, w): on the 90545A111 nut's thread flank (w = cos 30°, 60° conic spans) the raw mid-span vertices sit at exactly the end vertices' radius and z/w steps uniformly along the helix — only true of homogeneous storage; on the SolidWorks 2024 thumb screw the fitted flank missed the shared edge's cylinder-fin version by 0.119 mm read Euclidean, and meets it read homogeneous. XtReader._bcurve/_bsurface divide by w; _evalNurbsCurve/_deBoorSurf run de Boor on the stored homogeneous vertices (they used to weight them a second time); XtGraph writes STEP weights pre-multiplied, so step→x_t→step still round-trips (test_cadsf-808 21/21). Every rational x_t/x_b/.sldprt surface now evaluates differently — the nut moves 213.81 → 229.34 mm³ (re-pinned in test_cadsf-880).
  • Fin SP-curves honour the TRIMMED_CURVE window and sample adaptively. A tolerant edge's fin SP-curve was evaluated over the whole basis curve in 6 × n_vertices chords: the thumb screw's 18-turn crest helix is one 2-vertex (u,v) line, so each one-turn edge came back as the entire helix in 24 chords and the shank meshed inside out. _spCurveSpline now takes the fin's [param_1, param_2] window (ignored when outside the knot range, normalised when reversed) and refines chords whose surface midpoint sits more than 2e-3 mm off, to a 4096-point cap (stats.curves.tolerantSpFinRefined).
  • Not changed: bare .x_t/.x_b still default to mm through cad_import — the metres default the thumb screw wants (Parasolid's own convention, what every vendor transmit in the corpus uses) breaks round-trips with our own XtWriter, which emits mm. Follow-up filed.
  • tests/test_cadsb-810.js 49/49; the seven suites above and cadsf-948/963/292, cadsb-692, cadsf-808 green.

x_t units now agree with Parasolid's own convention — exports scale mm→m, bare imports default to m

The follow-up, landed. Parasolid's convention is ONE UNIT = ONE METRE ([S] §4.2.1), evidenced on every vendor transmit in the corpus (Inventor sphere r=0.0508 = 2 in; a SolidWorks 2024 thumb screw 0.006 × 0.019 = a 6 × 19 mm part) — but exporters/XtGraph.js wrote kernel millimetres straight into the transmit unscaled, so a 10 mm box exported to .x_t/.x_b opened as a 10 metre box (1000× too big) in SolidWorks/NX, and importers/ParasolidImport.js kept XtReader's historical mm default for a bare .x_t/.x_b, so a genuine vendor file (its own numbers already in metres) came in 1000× too small — the thumb screw reported as 6 × 19 microns. Could not simply flip the import default alone: doing that broke test_cadsf-735's own-writer round trip, because the writer and reader would then disagree on which side applies the factor.

  • XtGraph scales every length by 1e-3 (mm → m) by default. Points, radii (CIRCLE/ELLIPSE/CYLINDRICAL_SURFACE/CONICAL_SURFACE/SPHERICAL_SURFACE/TOROIDAL_SURFACE), B-spline/NURBS control points (weights untouched — homogeneous x·w/y·w/z·w terms carry the scaled point, not the weight), and the FACE/EDGE/VERTEX declared tolerances all move through one _len() helper. Directions and normals are never touched (a uniform scale cancels before unit() normalises them). opts.units: 'mm' is the escape hatch that restores the earlier byte shape exactly — for a file KNOWN to already break the convention, which today means only our own pre-fix exports. The export report (XtGraph#fromSolid/fromSheet/fromBody's returned .units) carries { applied, scale } for a caller to log or check.
  • ParasolidImport.fromBuffer defaults a bare .x_t/.x_b to { units: 'm' }, matching fromSldprt's existing SolidWorks-embed default. XtReader.build()/XbDecoder called directly (not through this module) deliberately keep their historical mm default — that call has no way to tell a genuine vendor transmit from one of our own earlier exports sitting on disk in kernel mm, and mm was the reading every direct caller and byte-golden test in this repo was written against.
  • The two sides move together, so a same-kernel round trip is unit-neutral by construction: default export → default import applies the 1e-3 factor once on the way out and the reciprocal 1e3 on the way back in, netting to the original geometry. cad_export_body format:"x_t"/"x_b" gained a matching units argument (default "m").
  • Every x_t/x_b suite that pairs XtGraph directly with a raw XtReader.build()/XbDecoder.build() call (bypassing ParasolidImport, which is most of them — they pin writer/reader MECHANICS, not the vendor unit convention) now passes { units: 'mm' } explicitly on the write side to stay on the unscaled footing those pins were written against; nothing about their own assertions changed.
  • tests/test_cadsb-818.js 13/13 (scaled export bytes, a vendor-style bare import at the true size, a legacy unscaled export importing correctly with the units:'mm' override, a default round trip, and the export report). test_cadsf-735 49/49, test_cadsf-773 24/24, test_cadsb-810 42/42, and every other x_t/x_b suite in tests/ green.

Turbogenerator demo: the exploded view is a hemisphere, and the engine is bolted down

Two scene changes to web/turbogenerator.html's shipped assets, rebuilt on the host with the placement audit at zero findings and re-exported (scene LOD0–2 + engine file; 171 parts, 941 placements).

  • Exploded view.** Every part used to travel straight up, so the plant exploded into one column: a ring of 30 fuel nozzles never left its casing and a gearbox hung under the compressor barrel moved into it. Each placement now travels away from the placement it is mounted to, measured on the two world bboxes' protrusion (which axis the child sticks out past its parent's slab — centre-to-centre cannot tell a nozzle's radial stand-off from its ring station's axial offset), rests-on special-cased to straight up (the 12 m floor plate protrudes 2.75 m past the 250 mm channel it rests on), and the result pushed onto the upper hemisphere: downward component dropped, 0.35 of lift added so a sideways mover clears the pad, normalised. Shipped manifest: 171/171 part directions have a strictly positive up component, 106 leave at an angle, 0 point down; explodeNodes carries a per-node unit; scene.explode.hemisphere/upBias; the page uses the node's own direction.
  • Hold-down.** The four engine feet rested on the 8 mm chequer floor plate and their sixteen M24 holes went to nothing. One HOLD definition now drives both sides: the skid's crossmember nearest each mount station moves under the foot (the grid keeps its nine members — two extra members took the enclosure past its 12–18 t literature band) and carries a 420 × 420 × 40 tapped sole plate whose top face meets the floor plate's underside; the feet are drilled from the same hole list the bolts are placed from; 16 M24 hold-down bolts (grip summed from the clamped stack) + 16 washers go through the floor into the sole plates. Load path engine → mount → floor plate → sole plate → crossmember → skid channels → pad → ground, every hop a declared, audited joint. The forward feet's inboard holes sit 230 mm past the leg face (at 200 the Ø44 washer straddled the leg by 4 mm — the audit caught it). audit_scene.js gains a fastener census (r.fasteners): every bolts-to / flange-to joint asked whether real hw-* hardware is declared on it — reported, not a finding, so it can be ratcheted down (12 of 622 today). Fastener recipes (hexPts, HEX ISO 4017/4032/7089 proportions for M6/M10/M16/M24 with clearance holes, hexBolt) are shared at module scope.

Tests: test_cadsb-788 78/78 (hemiUnit / escapeUnit / meanUnit fixtures, every part and node direction on the hemisphere, > 20 parts leave at an angle), test_cadsf-1259 4732/4732 (feet bolted, 16/16/4, load path, each bolt clamps its 25 mm plate through the floor into its sole plate), test_cadsf-1251 3660/3660 (enclosure 30 parts / 17.7 t, engine 43), test_cadsf-1253 212/212, test_cadsf-1252 731/731.

The selective chamfer runs out at a REFLEX in-face corner instead of refusing

chamferSelectedEdges' 1-of-3 run-out measured where the chamfer rail lands on the side edge as an unsigned distance and refused when the landing fell "PAST the vertex" — every one of the 193 chamfer-sel-setback-geometry-mismatch rows of the 2026-09-02 sweep. The landing parameter is |offset| / sin(phi) with phi the face's interior angle at the vertex, so it is negative exactly at a reflex corner: every vertex of a polygonal hole or pocket, read from the plate's cap face. The parameter is now signed; a reflex landing lengthens the side edge onto the landing instead of shortening it, and the in-cap wall triangle (V, landing on f, landing on the other support) is absorbed by the cap face through the existing capExtras splice, so the vertex dissolves and no extra face is emitted. Bodies are watertight (checkFull ok, mesh census clean) and exact in volume: d²L/2 for a perpendicular cap (square hole, d=1, L=12 → 6.000 mm³), d²L/2 + cot(phi)·d³/3 for an oblique one (hex hole, d=1, L=8 → 3.807550 mm³); the result reports runoutWalls. Still refused: a setback longer than the side edge (same chamfer-sel-setback-too-large code, now worded "would overhang") and, defensively, a run-out landing past the vertex on BOTH side edges. Convex run-outs are byte-identical. Consequence: the partitioned blanket chamfer now bevels the polygonal-hole rim edges it used to skip (hex-hole plate 12 → 18 strips; pocket plate 12 → 14).

Periodic faces whose period is "closed by an inner bound" split instead of refusing curved_seam_wrap

Each bound of a periodic face (cylinder, sphere, torus) was branch-continued only within itself, so a hole whose first sample inverted onto the far side of the atan2 cut sat a whole period away from the outer bound that contains it; the chart then read as spanning more than a period and the face was refused curved_seam_wrap ("… its OUTER bound spans only X of 2pi: the period is closed by an inner bound") — 11 of the 26 rows of the 2026-09-06 planar/planar battery. reconcileBranches now shifts every loop by whole periods onto the FACE_OUTER_BOUND loop's branch (the largest-chart-area loop when none is flagged) before the domain is measured; a whole-period shift renames the branch and moves no point, and k = 0 leaves a loop value-for-value untouched. Separately, the artificial seam was cut at the rail's first sample regardless of holes, refusing a wrapping face for the accident of where its rail started (6 of 26); chooseSeamOrigin keeps that position when both cuts miss every hole, else takes the first rail sample where neither cut lands in a hole, else a free-gap midpoint, and keeps the default — and the existing refusal — when no position is free (a hole wider than half the period). The straddle refusal now names the cut the hole actually crosses. Synthetic tube-with-window bodies that refused on both mechanisms now build valid watertight bodies at 0.16% / 0.013% of analytic volume; 11 neighbouring boolean/faceSplit suites are byte-identical.

Tessellation repair closes the last open rim quads with a Steiner centroid fan

_repairWatertight's hole filler ear-clips each open rim, and an ear may not reuse a diagonal the surviving skin already covers twice (a third cover is a non-manifold edge). A four-vertex rim has exactly two diagonals; when both are saturated every ear is refused, the loop is re-walked and re-refused on every later pass, and the mesh stays open — after an earlier release removed the figure-eight walks this was the ONLY shape left open on the 38-file robustness subset (4c6573f095dd_ROB-12309 4 open, 67010a9d36af_ROB-12537 4, 1fb99b00c6ae CP_Radial 7). When ear clipping leaves a simple rim wholly unfilled and every rim edge sits at exactly one use, the repair now mints ONE vertex at the rim centroid and emits one triangle per rim edge: no diagonal is consumed, so saturation cannot block it, and the census effect is exact (every rim edge 1 → 2 uses, every spoke used by its two fan triangles). A/B over the 38 files: open edges 15 → 0, non-manifold 0 → 0, duplicate windings 32 → 12, closed shells 35/38 → 38/38. meta.repair.steinerFans counts the fans, meta.repair.steinerFanWindingCost the irreducible directed-edge conflicts a fan on an already-conflicted rim costs (67010a9d36af: 2, buying 4 open edges). Guarded to a genuine last resort (simple cycle, rim edges at one use, no fan triangle under AREA_EPS, at most FAN_MAX rim edges); CADSOLVER_NO_STEINER_FAN=1 restores the old behaviour. Meshes that already repaired clean are byte-identical.

A blanket fillet no longer returns a self-intersecting body when two bands meet on one face

Found under an earlier change: a 10x10x10 plate with a 9x9 hole (a 0.5 mm frame) asked for cad_fillet_edges r=1 came back ok, 16 strips built, gate {openEdges: 0} — and cad_dfm_check reported error-severity self_intersection. The wall gate has since closed that frame; what it leaves by design is two bands each fitting its own face and built THROUGH EACH OTHER — the top and bottom fillets of a 10x10 plate 1.5 (or 1.9) mm thick at r=1, the outer and inner rims of a 1.5 mm frame wall. Three changes:

  • The dry run is asked before a blanket or multi-edge build. The pre-build probe used to decline any selection whose edges share a vertex ("two picked edges share a vertex (corner case)"), so the whole-body path and every corner pick were built blind. It now analyses them, skipping only the corner-station tests at a vertex two picked edges share (the corner construction's own business) and the saddle-vertex ends of a multi-edge pick; single-edge picks are byte-identical.
  • Band collision is measured pre-build, with a number. Two picked edges bounding one support face, not sharing a vertex, whose footprint strips overlap are refused before anything is authored: the blanket path SKIPS the pair with the existing partition-radius-exceeds-face (the skip record carries collidesWith and rMax, the largest radius at which the two strips clear — 0.75 on the 1.5 mm plate) and dresses the rest; the selective path REFUSES blend-sel-radius-exceeds-face with the same detail and measuredFeatureSize. The wording says the OTHER band is in the way, not the far side of the face. An edge already reported by the per-edge wall measurement keeps that finding untouched.
  • The partition gate sees self-intersection. The whole-body validity gate counted open edges only; a closed shell whose faces cross passed it. It now also runs the face-face scan (as a delta against the input body, so an imported body that already self-intersects is not refused over it), and a self-intersection that survives the chain bisection is REFUSED (blend-result-not-watertight, the document rolled back) rather than returned. result.gate gains selfIntersections / selfIntersectionsInput; openEdges is unchanged for existing readers.

Behaviour that moved besides the ticket's cases: a body whose every convex edge collides with a neighbour and whose remaining edges are too short (a 3x3x10 bar at r=1.6) now refuses blend-edge-too-short instead of returning a 3-edge partial; the chamfer-then-fillet composition (C3 then r=2 on a 30x30x12 box) now refuses blend-convex-candidates-failed with a per-edge report (the truncation facets' 60° corners cannot carry an r=2 rail) instead of dressing 4 of 48 edges for 0.7 mm³. A dedicated partition-gate-self-intersecting code needs the remedy table regenerated (tools/gen_refusal_remedies.js) and is left for the next pass.

glTF export: 4-byte-aligned quantized vertex attributes are now the default, and the poster renderer honours bufferView.byteStride

An earlier change added GltfExporter's options.alignVertexAttributes (pads each KHR_mesh_quantization SHORT/BYTE VEC3 vertex element — 6 or 3 bytes — up to a 4-byte multiple, per glTF 2.0 §3.6.2.4) but left it off by default, so every quantized export the pipeline had shipped was technically invalid: MEASURED at 204 MESH_PRIMITIVE_ACCESSOR_UNALIGNED gltf-validator errors per LOD file on the turbogenerator scene, 0 with the flag on. It stayed off because (a) tests/test_cadsf-752.js pinned quantization's own size win against the unpadded baseline (> 25 %), and (b) tools/render_glb_shots.js — the zero-dependency poster renderer, an independent GLB reader — de-interleaved vertex accessors WITHOUT reading bufferView.byteStride, so a padded (strided) file decoded to the wrong world bounds there.

Both blockers are fixed, and alignVertexAttributes now defaults to true. false remains a supported explicit opt-out that reproduces the exact earlier byte layout.

  • packages/interop/src/GltfExporter.js: alignAttributes is now options.alignVertexAttributes !== false (was !!options.alignVertexAttributes). An UNQUANTIZED (FLOAT) export is unaffected either way — every FLOAT VEC3/VEC4 element is already a multiple of 4 bytes, so there is nothing to pad — and stays byte-identical to before this ticket.
  • tools/render_glb_shots.js's accessorArray() now reads bufferView.byteStride and de-interleaves element-by-element via DataView when the stride is wider than the tightly-packed element size, the same rule web/src/glb-read.js already followed. The fast contiguous new TypedArray(buffer, offset, length) path is kept for the (still common) case where the view has no stride or is already packed at the element size.
  • tests/test_cadsf-752.js's size-cut assertion is rebased: it now measures BOTH the unpadded opt-out (alignVertexAttributes: false, pinning the original > 25 % invariant against the baseline it always meant) and the new aligned default (measured ~23.6 % on drilled_bracket.glb, asserted > 20 %), plus that the aligned file is strictly larger than the unpadded one.
  • tests/test_cadsf-745.js's checkAccessorBounds no longer requires a vertex bufferView to be tightly packed at the raw element size — a 4-byte-aligned byteStride is now a legal, and expected, layout; it additionally asserts any declared byteStride is itself a multiple of 4.
  • demos/turbogenerator/export_scene.js already passed alignVertexAttributes: true explicitly (added alongside landing) — the shipped web/turbogenerator/scene-lod{0,1,2}.glb were already validator-clean before this ticket; this ticket makes that the behavior for every OTHER caller that was relying on the (unsafe) default.
  • New tests/test_cadsf-1256.js: a quantized export has every bufferView.byteStride % 4 === 0 and every accessor's total byte offset aligned to its componentType size; a FLOAT export is byte-identical regardless of the flag; render_glb_shots.js's reader decodes an aligned quantized file to the same positions as the unaligned one; the explicit alignVertexAttributes: false opt-out still produces the old, unpadded layout.

SolidWorks multi-body Parasolid exports open: ASSEMBLY / INSTANCE / TRANSFORM roots are read

SolidWorks writes every multi-body part it exports to .x_t as an ASSEMBLY at index 1 with one INSTANCE per body, each carrying a TRANSFORM — and cad_import refused the first record by name (XT_UNSUPPORTED_NODE: node ASSEMBLY (10)). The three layouts are now read at base schema 13006 (every embedded-schema SolidWorks transmit), evidenced on two genuine files — a 2024 McMaster-Carr 92545A199 thumb screw (two bodies) and the 2010 Sparkfun display assembly (eighteen) — and every BODY under the root is grafted, in stream order, exactly as a partition transmit's bodies are. The instance graph is reported on the import's header.assembly (one entry per instance: part, transform, identity); a non-identity transform is a warning naming the instance, never a silently moved body, because only identity records are evidenced. The same files pinned the text layouts for INTERSECTION / CHART / LIMIT / TRIMMED_CURVE / SP_CURVE / GEOMETRIC_OWNER / SURFACE_DATA / BLENDED_EDGE / BLEND_BOUND / SWEPT_SURF / HELIX_*_FORM (the binary decoder's, now in text), the ATTRIB_DEF grouping edit scripts address (six fixed fields), and definition records for node types absent from base 13006 (INTERSECTION_DATA). Rode along: XtReader.build() ignored opts.bodyIndex, so every multi-body partition had imported as N copies of its first body — the thumb screw now imports as its 8-face shank AND its 1404-face knurled head.

A hung kernel op is cancelled and refused E_TIMEOUT by name instead of taking the session

An earlier change bounded every op that POLLS a deadline; a non-cooperative loop with nothing to poll (blend/loft/sweep/tessellation — original symptom) still ran until the client gave up and the host app restarted the server, which reset sessionEpoch and dropped every body.

  • The kernel host now enforces a HARD ceiling per request. tessellate, query and sync accept {timeoutMs | deadlineAt, opName}; a request that outlives it terminates the worker thread (mechanism, on a timer) and is refused E_TIMEOUT with details {op, reasonCode: "op-time-budget-exceeded", budgetMs, elapsedMs, timedOut: true, sessionSurvives: true, serverRestarted: false} — the same code and reason code a cooperative budget refusal already uses, so a caller never has to branch on which clock noticed. The session, its epoch and every body survive: the host owns the op journal and replays it into the next worker. cad_server_info-style host stats gain timeouts. Off unless a ceiling is given (or CADSOLVER_MCP_KERNEL_OP_TIMEOUT_MS is set) — no existing call gains a deadline it did not have. The clock starts when the worker announces itself, not when the request is queued: the worker is spawned lazily and its first act is to require the whole kernel (seconds on an idle box, ~49 s measured on a loaded 2-vCPU one), and charging that to the op would refuse healthy calls — hardest on the first call after any death, i.e. exactly the call a client is retrying.
  • Opt-in guard for the modelling build (CADSOLVER_MCP_OP_GUARD=1). With it on, a modelling call pre-runs its document on the worker under the call's own opTimeoutMs before the protocol thread commits to a build it cannot interrupt; a document that does not finish is refused with the existing E_TIMEOUT budget refusal naming the op, documentRolledBack: true. Off by default because it costs a second replay (the double parse only closes when Executor.run itself crosses the boundary), and because pricing the ceiling on the whole rebuild rather than on the new op is the open question.
  • Test knob CADSOLVER_TEST_SLOW_OP (+ CADSOLVER_TEST_SLOW_OP_MS) makes a named op stall inside the worker's replay, in a loop with nothing to poll. Unset in every normal boot.

cad_fit_surface: degenerate clouds now REFUSE, refusals carry a reasonCode, and a fit can be trimmed

Three review follow-through changes to one tool. The first is a behaviour change a caller can key off:

  • A degenerate point cloud is refused instead of fitted. cad_fit_surface with points that are all coincident, or all on one line, used to answer ok: true with a meaningless near-zero-extent sheet — the thin-plate smoothing escalation always found a solvable system. Such a cloud now refuses with fit_surface_degenerate_cloud (no extent) or fit_surface_rank_deficient_cloud (rank 1: it determines a curve, so fit it with cad_fit_curve). The gate is a measured extent RATIO — a cloud whose span across its own principal axis is under 1e-6 of the span along it — so no cloud that carried real two-parameter information before is affected.
  • Every cad_fit_surface refusal now carries a reasonCode, the way cad_mesh_to_brep's already did: nine fit_surface_* codes from the fits themselves plus the seven freeform_sheet_* codes from publishing the patch as a sheet. All sixteen have a row in the operator remedy table, so a refused op comes back with remedy attached.
  • New optional trim (plus trimSamples, trimTol). Loops in the fitted surface's own (u,v) square — first the outer boundary, the rest holes — bound the published face instead of the patch's four natural sides, which is what finally gives NurbsSurface.trim()'s TrimmedSurface a consumer. Each (u,v) side is sampled on the surface and authored as the B-spline through those samples; the deviation from the true surface curve is measured and reported on analytic.trim.deviation, and trimTol is the value above which it is refused (freeform_sheet_trim_deviation) rather than delivered. Command-schema 1.20.0 → 1.21.0 (three added OPTIONAL fields, one new field kind trimLoops); a cad_fit_surface call without trim is unchanged.

cad_thicken: a planar sheet with a curved rim is no longer refused

thicken_edge_not_straight is retired. A single-face sheet whose surface is a PLANE but whose boundary carries a CIRCLE or a B-spline — a disc, a rounded-corner blank, an arc-profiled part's flat cap — used to refuse with

> thicken: edge #… is CIRCLE — v1 thickens straight-edged sheets only

and now thickens, taking the same analytic offset path a curved sheet takes (report.path === "analytic"). The offset of a plane is the parallel plane exactly, so nothing about the answer is approximate except the rim curves, which are re-solved and recognised — a LINE stays a LINE, a CIRCLE stays a CIRCLE — with the deviation reported in report.deviation. A flat sheet with a polygon rim still takes the v1 translation path and still reports an EXACT volume; report.path is absent there, which is how a caller tells the two apart.

Refusals that remain, and now cover everything that used to hide behind the planarity codes: thicken_surface_unsupported (a surface whose constant-distance offset this kernel can neither state exactly nor measure, e.g. SURFACE_OF_REVOLUTION), thicken_offset_refused (the offset surface itself collapses — relayed with the offset's own code on .causeCode), thicken_face_has_holes, thicken_face_degenerate. Both thicken_face_not_planar and thicken_edge_not_straight stay published in REFUSAL_CODES so a caller switching on them still compiles; nothing raises either for a curved sheet any more.

cad_chamfer_edges: a corner where MORE THAN THREE picked edges meet now builds

chamferSelectedEdges refused every vertex past a trihedral one — chamfer-sel-vertex-too-many-selected when all k picks met there, chamfer-sel-vertex-valence otherwise — so a pyramid apex, an n-gonal boss top or an octahedron vertex could not be chamfered by selection even though the whole-body path (chamferConvexEdges) has beveled them since day one. A selection that names every edge at a valence-k vertex (k ≥ 3) now builds the planar k-gon truncation facet there.

What a caller keys off:

  • Fewer refusals, no new success shapes. Nothing that built before builds differently: the trihedral corner still goes through the exact 3×3 setback solve it always did, byte for byte.
  • chamfer-sel-vertex-too-many-selected is effectively retired. It now fires only for an impossible count (more picks at a vertex than it has edges). A k-of-k pick that used to raise it builds.
  • chamfer-sel-vertex-valence narrows to the PARTIAL pick. Past a trihedral corner the facet is bounded by all k end segments, so leaving one edge sharp leaves it open; the message now names every edge at the vertex so the fix is in the refusal.
  • A new coded refusal case, on an existing code. A valence-k corner only admits a setback point when its k support planes are tangent to one cone of revolution about the corner axis (ops/blendVertex.js). A corner that is not — a skewed apex — refuses as chamfer-sel-corner-inconsistent, with the measured miss in the message and the note that the condition is homogeneous in the setback, so a smaller distance will not rescue it.

cad_chamfer_edges: a mitred corner no longer needs square dihedrals

A selective chamfer (edges: [...]) where two picked edges share a corner with a third, un-chamfered one used to refuse with chamfer-sel-corner-inconsistent (or chamfer-sel-miter-geometry-mismatch) unless BOTH support faces stood at 90 degrees to the face the two picked edges share. The mitre points were placed "one setback inside each outer face", which is where the chamfer plane actually falls only in that square case. They are now the chamfer planes' own intersections, so a tetrahedral corner, a hex boss and a drafted wall mitre exactly; where the two dihedrals DIFFER the two chamfer planes cross the third edge at two points and the seam picks up one extra segment (a kink on the shallower edge's outer face) instead of being refused. A 90-degree corner is unchanged, and no refusal code was added or retired.

↑ all releases

NativeCAD 0.8.x

v0.8.0 “Fricsay”

released September 6, 2026

sha256 nativecad-mcp-0.8.0.mcpb 7fedd38874da10e4ca51ba3bf814970375e660c62bdff7014189760d05d90bf2sha256 nativecad-mcp-0.8.0.tgz 6b272c48483ac270f99d59feffe2f41b78365cd9846a57ac8b7d0084fb7ec2b4

glTF: B-rep edges are a pipeline primitive, primitive extras survive, and a LINES primitive no longer refuses

Three additive changes in packages/interop. Nothing existing changes shape; an export that asks for none of them is byte-for-byte what it was.

  • GltfScene.edgesPrimitive(edges, opts) is new. It emits the B-rep edge convention — mode: 1 (LINES), POSITION only, tagged extras: {kind: "edges"} — which web/src/glb-view.js already honours as the edge overlay instead of inferring one from triangle angles. GltfScene.fromMeshes now takes body.edges and GltfAssembly.fromAssembly takes component.edges, both putting the edge set on the same glTF mesh as the triangles, so an instanced part places its shaded surface and its edges through one node. edges is part of the assembly's mesh identity: an edged and an un-edged component sharing a meshKey are two meshes, not one.
  • GltfExporter passes primitive-level extras through. Only mesh.extras survived before, and the edge tag lives on the primitive — a mesh carrying both its triangles and its edges cannot say kind: "edges" at the mesh level without mislabelling the triangles.
  • GltfExporter's index-count divisor follows primitive.mode. It was hard-coded to 3, so every LINES (and POINTS) primitive was refused with index count N is not a positive multiple of 3 — an error about triangles that was true of nothing in the file. LINES now divide by 2, POINTS by 1.
  • GltfExporter gained options.alignVertexAttributes (default false). glTF §3.6.2.4 requires each vertex-attribute element to be 4-byte aligned inside its bufferView. A KHR_mesh_quantization SHORT VEC3 is 6 bytes and is not: measured, each LOD file of the turbogenerator scene reported 204 MESH_PRIMITIVE_ACCESSOR_UNALIGNED errors from the Khronos validator, and zero with the flag on. It is opt-in because switching it on everywhere costs 2 bytes per quantized VEC3 vertex and because tools/render_glb_shots.js reads accessors without honouring bufferView.byteStride; web/src/glb-read.js does honour it. Making it the default and fixing that reader wants a ticket of its own.

A plane × B-spline blend LOOP is no longer declined for its size, and a partial one is a result

A call that used to refuse now succeeds. An earlier change declined a reentrant plane × B-spline loop whose edges × faces product was over a rebuild budget (4 000) — the turbine blade's ~58-edge airfoil root on a 387-face body came to 22 446 — re-throwing the original E_GEOMETRY / NON_PLANAR_SUPPORT with the estimate on .planeSplineChain. Every band re-authored the whole solid; a band now re-authors three faces (its two supports and itself) whatever the body's face count, and everything else survives by identity. The budget is retired.

What changed for a caller:

  • report.faceRebuilds is new: { perPickMax, total, bodyFaces }, measured.
  • report.partial, report.refused and report.refusedByCode are new. A loop where some edges cannot take the ball now blends the rest and says so instead of the pick standing or falling as a whole — a root fillet with a too-tight trailing edge left sharp is reported as partial: true with that edge's own BLEND_PS_BALL_EXCEEDS_CURVATURE (or other) code in report.edgeReport. A caller that read report.blended === report.selected as "the fillet is complete" still reads correctly; one that assumed success meant every picked edge was blended should read partial.
  • a loop now stops instead of running past your timeout. report.timeBudget { ms, budgetMs, exhausted } is new: the chain spends a 20 s wall-clock budget on the picks (opts.planeSplineChainMs, Infinity lifts it) and reports every edge it did not get to as status: "skipped" with code: "BLEND_PS_CHAIN_TIME_BUDGET", returning the bands it built. Measured: the turbine blade's 177-edge root loop is 103–111 s of geometry, which used to mean the call died and the caller got nothing; one call now returns in 49 s with 17 bands built, 32 edges refused by code and 128 named as not attempted. Pick the remaining edges in a second call — ids follow across the rebuild.
  • report.auditedPerPick / report.auditRetried say whether the per-pick watertight audit had to be paid (the chain defers it and pays one audit on the body it returns; if that body is defective the whole loop is re-run audited, so nothing is published that the per-pick audit would have refused).
  • the size cap that remains is an absolute 512-edge cap (opts.planeSplineChainBudget, now read as an edge count). Over it the original classified refusal is re-thrown byte for byte, with .planeSplineChain.code = BLEND_PS_CHAIN_TOO_MANY_EDGES — the faceRebuilds/budget fields that field used to carry are gone.
  • a ball that runs off a narrow support in the SPINE_OPEN diagnostic used to throw an uncoded ReferenceError: clip is not defined instead of BLEND_PS_SPINE_OPEN with the measured face width. Fixed; the coded refusal and its wording are what a caller now sees.

Appending an op no longer replays the whole document

Nothing about a call's ANSWER changes; what changes is what it costs. Every modelling call used to re-execute the document's entire op list, so the price of any op grew with the document's history — a trivial cad_box measured 44 ms in an empty document and 663 / 1369 / 2524 ms as three general-engine booleans landed in it (root cause; itself only bounded the batch clock so this could not take the session down with it). The server now retains the kernel's per-op checkpoint (commands/rebuild.js) after each successful op and replays only the appended one: the same four measurements are 6 / 32 / 43 / 84 ms.

The appended op's result is byte-identical to a full replay — same handles, same entity ids, same provenance, same reports, same refusals — and packages/mcp/test/cadsf-1152.test.js asserts exactly that by running each scenario twice, once with the checkpoint and once without, and comparing the transcripts whole. A checkpoint is dropped rather than trusted whenever it cannot be proven equivalent: an edited or removed earlier op, a cad_param_set rebuild, a change in the executor options the document replays under, a PMI-bearing document. A refused op still leaves no residue and no checkpoint. CADSOLVER_INCREMENTAL_REPLAY=0 restores full replay.

cad_fillet_edges now builds the REENTRANT plane × B-spline root fillet

A call that used to refuse now succeeds. A fillet on a reentrant (concave) edge between a PLANE face and a B_SPLINE_SURFACE_WITH_KNOTS — the root of any boss, rib or blade sitting on a freeform face — refused E_GEOMETRY / NON_PLANAR_SUPPORT, "the rolling-ball blend is only defined between PLANAR faces" (the underlying kernel code was BLEND_PS_EDGE_NOT_CONVEX, since ops/blendPlaneSpline.js was convex-only). It now builds: the ball rolls on the OUTSIDE of both supports and the band is emitted as an exact b_spline_surface_with_knots that adds material in the corner, G1 to both supports (measured, refused if not met), watertight, and a clean STEP round trip. A whole intersection LOOP is applied edge by edge and reported per edge in report.edgeReport; report.method is plane-spline-rolling-ball for one edge and plane-spline-rolling-ball-chain for a loop, and each row carries side: "concave".

New error codes a caller may now see on such a pick, both carrying the measurement they name:

  • BLEND_PS_BALL_EXCEEDS_CURVATURE — the radius is at or past the B-spline support's minimum radius of curvature on the side the ball rolls on (minCurvatureRadius, curvature, at: {u, v, point}, face, edge). The turbine blade's trailing edge is the real instance.
  • BLEND_PS_RADIUS_OVERRUNS_SUPPORT — the planar support has less than the radius left beyond the picked edge (width, face, edge).

Changed meaning: BLEND_PS_EDGE_NOT_CONVEX no longer means "reentrant". It now fires only for a TANGENT edge, or one whose convexity cannot be read — both senses of a real corner are built.

A loop costs one full-body rebuild per edge (each band re-authors the whole solid — ~0.6 s per edge on a 23-face body). demos/turbine_blade's ~58-edge root loop on a 387-face body therefore reaches the construction but runs past the tool call's own request timeout; it is no longer refused, and it does not yet complete in one call. tests/test_cadsf-1199.js's "root fillet refused" pin is therefore NOT flipped by this ticket.

Unchanged: convex plane × B-spline picks, the general ops/blendNurbs.js ball, and every analytic (plane × plane, plane × cylinder) blend are byte-for-byte what they were; a pick that refused before and still cannot be built refuses with the same code it did. Not fixed: the volume over-move on the fitted band — measured at 1.78× the rolling-ball bound on a 90° corner, identically for the convex and the reentrant sense of the same corner (see docs/BLEND_EVALUATION.md §5.4).

Turbine-blade showcase — a four-wall public page

No tool behaviour changes. demos/turbine_blade/build_page.js now carries an omit set, OMIT_DEFAULT = ['structural']: the generated web/turbine-blade.html is a four-wall page — castability, rim load, tip clearance, aero. The structural study is internal, stays in demos/turbine_blade/results/box.json in the private repo, and appears nowhere on the site: there is no card, no placeholder, no marker, no label, no manifest key, and no sentence that hints a fifth measurement exists. The constraint box's neck-width panel shows the rim-load points and its note only; the nominal fails one wall (castability) and the picked point (tTE 1.6, neck 9, h 60) passes all four. A build-time guard throws if the HTML or the manifest contains structural, withheld, private study, partner, meshfree, basis order, 8000, cell resolution, safety factor, an SF <digit> figure, von Mises, fea_, or MPa outside the rim-load card (whose hoop stresses are our own ops/discStress.js closed-form arithmetic and must be labelled as such); the FEA vendor's name is refused unconditionally. --include-structural lifts the omission for the gated/internal build only and must never be used for the public site. The page also gains a view control bar — five display modes (shaded, shaded + edges, wireframe, x-ray, drawing/hidden-line), four material presets (graphite, satin, nickel, ivory), four standard views, a ground toggle and a reset, remembered in one localStorage key and re-applied after every point swap — and now defaults to graphite at exposure 0.82 instead of the near-white fallback that ACES clipped to a flat block. The blade still rendered white after that, for a reason the preset could not fix — three r128 predates colour management, so an authored sRGB hex was used as a linear albedo (0x5f6670 at ~0.37 instead of ~0.11) and graphite / satin / ivory came out ten per cent apart — so glb-view.js now decodes the fallback colour out of sRGB, takes an opts.materialOverride that applies the picked material to every primitive, and the GLBs are built from each point's blade.step through the kernel rather than its blade.stl: SurfaceTessellator at a 0.05 mm chord tolerance with a 35° crease gate gives the surfaces' own normals, and its extractStepEdges() ships every B-rep edge, sampled off its own curve, as a mode 1 LINES primitive tagged {kind:"edges"} that the viewer uses as the edge overlay in place of an EdgesGeometry guessed from triangle angles. It is driven by an additive web/src/glb-view.js handle API — setDisplayMode(mode), setMaterial(preset | {color, roughness, metalness}), setView(name), setGround(on), setExposure(x) plus displayMode / material / view / ground getters and the exported DISPLAY_MODES, MATERIAL_PRESETS and VIEW_DIRS — with frameCamera() taking an optional direction. Every setter is a no-op before a scene loads (the request is queued and applied on load), and a page that never calls one renders exactly as it did.

Licensing + evaluation loop

Three changes in this release alter what an existing caller gets back from a call that used to succeed. They are listed first, in the order a caller meets them.

BREAKING — an unlicensed server now refuses every tool outside the evaluation loop. Until 0.7.0 a missing licence key cost a watermark and nothing else ("nothing is disabled either way" — that sentence is gone from cad_server_info, licensing.js and the docs, because it is no longer true). An unlicensed process now answers E_LICENSE_REQUIRED — recoverable: true, with details.registerUrl, details.tier: "unlicensed", details.evalHint and details.allowedWithoutKey — for every tool outside licensing.EVAL_ALLOWLIST (cad_server_info, cad_help, cad_license, cad_evaluate, cad_eval_report, cad_eval_packet, cad_eval_share, cad_report_bug). The refusal is raised in ToolRegistry._dispatch, BEFORE argument validation, so junk arguments to a gated tool return the licence code rather than leaking E_INVALID_ARGS; it is returned on every call, not once per session; and an unknown tool is still E_UNKNOWN_TOOL. cad_server_info gains a gate block (enforced, tier, errorCode, registerUrl, statement) and cad_help marks a gated tool as gated. Registration is FREE: the new free tier is a real CSLV1 key issued after e-mail confirmation, opens the full toolset on Education / Maker terms, and is the one licensed tier that still watermarks — the gate is about registration, not payment. cad_license activate now PERSISTS the key (~/.nativecad/license.json, override CADSOLVER_LICENSE_FILE, written 0600); deactivate deletes it. Offline validation as before — no activation server exists to be down.

BREAKING — cad_evaluate requires recorded terms when unlicensed. An unlicensed caller must pass criteria[] ({id, statement, mustPass, category?}) and consent {sendReport: true, contact}; without both the call is refused with E_EVAL_CONSENT_REQUIRED, carrying the consent statement verbatim, its sha256 and the 30-day approval duration — nothing is evaluated and nothing is written. A LICENSED caller's existing shape is unchanged. When criteria are supplied they are scored pass / fail / not-assessable (an unroutable must-pass criterion makes the verdict incomplete, never pass), and the LICENSE §2 approval is filed beside the run as <runId>.approval.json. The contact identity lives in that file and nowhere else: the run document — the thing the packet and the share are built from — carries a consent hash and a statement digest instead.

BREAKING — cad_eval_packet defaults to disclosure L1: no customer geometry. The packet used to ship cropped snippets of the customer's model by default. The ladder is now L0 (descriptions only), L1 (the default: a synthetic reproducer rebuilt from primitives, verified to reproduce the same failure mode and error code before it is included) and L2 (cropped customer geometry, opt-in only via disclosure: "L2" or a per-issue byIssue table). The manifest states a level for EVERY selected issue, with the level asked for beside the level shipped and a fallbackReason when they differ. Anything downstream that assumed a snippet by default must now ask for L2 explicitly.

cad_eval_share posts by default. A confirmed share now POSTs one JSON document (schema 2) to <CADSOLVER_MCP_VENDOR_BASE_URL>/api/ eval-intake — default https://featureboard.ai/api/eval-intake — carrying the archive base64-encoded, the consent block (the only identity in the document), the criteria, the approval record and the scorecard's gap-close rundown as plain text. It was previously local-only. Set CADSOLVER_MCP_SHARE_OFFLINE=1 to force the ready-to-send local bundle and open no socket (the air-gapped image sets it), or point CADSOLVER_MCP_SHARE_ENDPOINT elsewhere. An unreachable endpoint always falls back to the bundle — a failed send never loses the packet. The preview stage is unchanged and still touches no network.

New: cad_gap_registry. The register of what this kernel cannot do — each gap with its {opClass, failureMode, geometryPattern} signature, status, ticket, day estimate and evidence. action: "list" reads it; action: "match" classifies a run's issues against it and returns the gap each one evidences, ranked, with unexplained issues coming back as unclassified:<opClass>:<failureMode> stubs. cad_eval_report uses the same classifier to cost what stands behind a failed criterion ("to meet your criteria we need to close N gaps, ~X weeks" — always a floor, and it says so), scores the caller's criteria, and renders an agent-authored gapCloseNarrative in a labelled block. The report still names no file, no path and no geometry. cad_gap_registry is on the eval allowlist — the register holds only our own gaps, so an unlicensed session can classify before any key exists.

Verification. packages/mcp/test/cadsf-1229-e2e.test.js walks the whole journey through the real protocol boundary in one process: unlicensed boot → E_LICENSE_REQUIRED → E_EVAL_CONSENT_REQUIRED → an evaluation on recorded terms → the report → an L1 packet → an offline share bundle → a free key → the same call succeeding, watermark on → a studio key, watermark off → deactivate, gated again.

The turbine-blade design loop, in public. web/turbine-blade.html is generated by demos/turbine_blade/build_page.js from demos/turbine_blade/results/box.json — the constraint box as inline SVG, one card per wall with its verdict at the nominal and at the picked point, each verdict's details.basis verbatim, the open findings (min_fillet_radius, max_section_ratio) named as findings rather than walls, and the root-fillet refusal (E_GEOMETRY/NON_PLANAR_SUPPORT) shown as the kernel finding it is. No behaviour change to any tool; nothing in it re-solves. Structural numbers are published as "meshfree FEA, basis order 2, 8000-cell resolution" — the solver vendor is not named, and the generator refuses to write a page that names it. Assets: eight GLBs converted from the host jobs' STL by the generator (no new dependency), plus the picked point's STEP/STL.

The five walls of the turbine blade, measured. demos/turbine_blade/walls.js gives the first-stage HP blade of a 42 MW-class aero-derivative turbogenerator five independent verdicts as tools/design_loop.js adapters — castability (cad_dfm_check investment_casting, section rule), aero (cad_wind_tunnel, Cd as a ratio to the thinnest-TE point at one grid and one residual target), structural (private cad_fea_case/cad_fea_run: sliding fir-tree flanks + a lockplate face, ω²·r_cg body load at 44,900 g, SF read on the NECK net section from the solver's surface field — the global peak is a fixture-edge singularity and is reported beside it), rim load (cad_mass_properties → new ops/discStress.js, Timoshenko rotating annulus + Lamé rim tension, Robinson average-stress burst margin, pinned to three closed-form limits) and tip clearance (cad_tolerance_stack, judged on RSS, worst case reported) — with targets and alloy data read from the turbomachinery pack. Run on the host as one job per point and folded by demos/turbine_blade/collect_results.js into results/box.json: castability boxes tTE from below (fails 1.2, passes 1.6 — the nominal 0.9 mm edge is uncastable at the 1.0 mm floor); structural boxes the neck from below (SF 1.00 at 7, 1.62 at 9, 1.88 at 11, 2.50 at 13); tip clearance boxes the height from BOTH sides to [59.5, 60.5); the rim load sits inside at 1.32–1.33 everywhere; the aero wall cannot see a 0.4–2.0 mm trailing edge at a 4.3 mm cell (Cd flat to ±5 %, no trend) and says so. The nominal fails one wall; the point picked inside (tTE 1.6, neck 9, h 60) passes all five (1.49 mm / 0.95 / SF 1.53 / 1.32 / 0.75 mm). Along the way the driver learned to give each rebuild a fresh session (a sweep into one session slows quadratically — s → 462 s per point, then a timeout) and to resolve its output dir absolutely. tests/test_cadsf-1200.js (29).

CADSOLVER_REPRO_DUMP=1 no longer goes silently inert under CADSOLVER_TELEMETRY=off. src/kernel-host/repro.js's dumpEnabled() used to treat the telemetry opt-out (and DO_NOT_TRACK) as a privacy veto over the repro dump too. The dump is local-only — tmp/repro/<ts>/{ops.json, verdict.json[, body.step]}, never uploaded — so it isn't telemetry, but the bench runners and the design-loop driver force CADSOLVER_TELEMETRY=off, which meant every refusal they hit produced zero repros. The dump is now governed only by its own CADSOLVER_REPRO_DUMP (default unchanged: on); DO_NOT_TRACK / CADSOLVER_TELEMETRY=off still veto anything that uploads, just not this. No caller-visible error vocabulary changed. See docs/DESIGN_LOOPS.md and benchmarks/text-to-nativecad/README.md.

Portfolio coverage model gains investment_casting. The ops/dfmRules.js PACKS.investment_casting (lost-wax, nickel-superalloy aerofoil class) had no entry in tools/portfolio_coverage.js PROCESS_FAMILIES or TRIGGERS, so the model's own integrity check was red — the tests/test_cadsf-675.js caught it. It now carries a 3% [DERIVED] family weight (cited and derived in docs/portfolio/PART_LANDSCAPE.md §2.3; the other six families rebalance by × 0.97 to keep the table summing to 1), two archetypes (cast turbine blade/aerofoil, cast valve/pump housing), a superalloy-nickel / stainless-cast material split, and TRIGGERS for its four rules — min_section reuses the body-general wall-thickness pattern (always triggers, like min_wall_thickness), min_trailing_edge_thickness triggers on thin_wall, min_fillet_radius on internal_corner, and max_section_ratio on a new section_taper feature class (no existing class named a thick/thin section transition). The greedy selection grows from N=15 to N=16 (12 core + 4 repair); ic_turbine_blade_inconel earns a core slot on its own weight since no other candidate can ever trigger this pack's rules.

Quieter tessellation logs. The per-face [CURVED-TESS] and [CDT-DEBUG] BoundaryEdges lines are DEBUG now (one INFO line per curved face was printed on every tessellation); WARN/ERROR retry and recovery lines are unchanged.

Design loops: one driver for target → build → verdict → remedy → iterate → stop. tools/design_loop.js runs the loop docs/AGENT_FEA_LOOP.md prescribes for stress against ANY verdict tool — cad_dfm_check, cad_moldability, cad_mass_properties, cad_tolerance_stack, cad_assembly_fit, cad_wind_tunnel, and privately cad_fea_run — through the in-process MCP surface, in the knowledge packs' recipe format ({tool,args} steps, $N handles). A wall is an async (ctx) => {pass, measured, limit, margin, remedy?}; the sweep policy scans one variable until the verdict flips and bisects to the WALL (not "fail" but "you are 0.15 mm short"), apply-remedy sends a verdict's complete {tool,args} to the live body, and a failing verdict with no mechanical remedy stops the loop honestly. Every iteration lands in loop.json (params, every step with its ms/handle/refusal code, every verdict, the decision) + a loop.md table; --replay re-runs the final iteration after a kernel change. Two instances ship: dfm_wall (machined tray, floor thickened until machining min_wall_thickness passes, wall bracketed in [0.7, 0.8] mm) and the private bracket_fea (the doc's L-bracket radius ladder as MCP calls, faces named by cad_body_topology normals + cad_measure ray_fire, solved on the host). The server instructions gain "A verdict is an instruction" (order 85); tools/fea/host_jobs.js allow-lists the driver and its test as repo jobs. docs/DESIGN_LOOPS.md; tests/test_cadsf-1190.js.

Shipped in compiled form only. Both artifacts — the npm tarball and the .mcpb — now carry ONE minified dist/server.min.js instead of readable src/** and kernel/** trees (and, in the .mcpb, instead of a bundled node_modules/): the tool modules, the CAD kernel require-closure and the three runtime dependencies are compiled in, with only Node's built-ins external. This is what LICENSE/COMMERCIAL.md have promised since an earlier release. Nothing changes on the wire: same tool inventory, same schemas, same error vocabulary, and CADSOLVER_MCP_TOOL_PATH third-party tool directories still load from disk. A host config that spawns nativecad-mcp / cadsolver-mcp is unaffected; the .mcpb manifest now names dist/server.min.js as its entry_point. cad_server_info's stagedKernel stamp still dates the kernel an install ships — it travels inside the bundle now, and is also readable in the file's banner. See docs/RELEASING.md.

cad_fillet_edges groups a large selective edge report instead of ever refusing a successful build for its own report's size. A big hand- or script-picked edges:[...] selection (no partition, since that only exists on the reentrant/blanket path) built a real, watertight body, but its report.edgeReport — one row per edge, plus a reason/group on any edge a multi-run split could not carry — measured 19+ KB on a 40-edge pick, over the "modeling" family's 16 KiB budget, so registry.js's enforceByteBudget refused an operation that had already succeeded. src/tools/modeling/index.js's new compactEdgeReport now groups rows by status + reason (id-neutralised, matching compactPartition's existing idiom) once the array is comfortably past a hand-picked few edges (>16 rows), into {...sharedFields, count, edgeIds: [first 5], truncated?} — deliberately NOT keyed on the kernel's own per-run group index, which a large pick often sets to a different value per edge and would otherwise defeat grouping entirely. A small selection (cadsb-143.test.js's pinned 8-edge case) is byte-identical to before this ticket — grouping never engages below the threshold. ops/blend*.js untouched; the fix is purely result-shaping.

The governor never suggests an unusable maxTriangles:0 retry. Its byte-budget refusal computed suggestion.maxTriangles as floor(bytesFree / TRIANGLE_BYTES), which floors to 0 when a document's baseline parse/executor cost alone already exceeds the headroom left in the budget — a "reduce maxTriangles to ~0" suggestion no smaller value can ever satisfy. src/governor/index.js's checkHeadroom now omits suggestion entirely and returns recoverable:false with a hint that says plainly the document cannot fit at ANY triangle count, whenever the computed value would be <= 0; a genuine fit still returns a real positive suggestion with recoverable:true, unchanged. src/tools/output/governor.js passes recoverable:false through to the thrown E_LIMIT. src/remedies.js needed no change — its retry branch already only builds a retry when suggestion.maxTriangles > 0. (Also fixed a latent gap in the pre-existing governor.test.js: its "hard cap" case used a byte budget so tiny it was already the "cannot fit" case, so its old assertion that maxTriangles was "a number" was trivially true for a worthless 0.)

cad_export_body maps unwritable-path errors to E_IO, not E_INTERNAL. A write that failed with a plain Node fs error (ENOENT, EACCES, ENOTDIR, EPERM, EISDIR) was an ordinary Error, not a CadMcpError — toEnvelope() downgrades any such error to E_INTERNAL ("this looks like a bug in NativeCAD"), the wrong signal for "the path you gave isn't writable," and one src/remedies.js's filesystem hint never even saw. The export handler now catches its own fs errors (a new rethrowExportFsError, keyed on err.code) and re-raises them as E_IO (already recoverable:true in the code vocabulary) with the ORIGINAL message text preserved verbatim and details.errno/details.path attached — the existing filesystemHint in remedies.js now fires correctly since the code it pattern-matches on is finally right.

cad_pad takes the same profile primitives as cad_extrude_profile, and imprints their arcs exactly. A pad's footprint used to be a list of points, so the only way to ask for a round boss on a face was to sample it — a 24-gon wall where a cylinder belongs, on the ONE verb whose premise is that the profile already lies on the target face. profile.outer (and the top-level outer) now accept one {circle} / {slot} / {rect+cornerRadius} primitive, expanded by the same commands/profile-primitives.js the extrude case uses, and ops/pad.js imprints the result as real CIRCLE edges with CYLINDRICAL_SURFACE walls: a {circle} boss is one cylinder — two half faces — and its volume is exactly πr²·length, matching what cad_extrude_profile has produced since an earlier release. A pocket (negative length) gets the same treatment; its arcs run the other way about the rail, which is the only difference between the two modes. A sketch whose entities curve is now resolved through the curve-preserving path too, so {sketch:"…"} pads round where it used to pad faceted. The result carries pad.hasCurve and pad.cylindricalWalls when the footprint curved (absent otherwise), and the same profilePrimitives note the extrude case returns. Refusals are unchanged in vocabulary: a non-planar target is nonplanar_host from BOTH entry points (the inline spelling used to surface a bare planeFrameFromFace message with no code), and a primitive that describes nothing still refuses with its PROFILE_PRIMITIVE_* code. A straight-sided pad or pocket is byte-identical to the previous release — same STEP, same analytic figures, no new report fields.

cad_dfm_check accepts maxIssues/issueOffset to group and page a large findings document. A real part's rule-pack verdict can carry one finding PER OFFENDING ENTITY — a real SendCutSend route measured 309 KB against the "report" family's 128 KiB response budget. maxIssues now groups every finding (validity + rule-pack) by {severity, reasonCode} into one row with a count and a small entityRefs sample, pages the GROUPS (issueOffset + page.nextOffset), and caps each rulePack.rules[] entry's own violations sample too (violationCount/violationsTruncated) — the second array that scales with entity count and that grouping findings[] alone would not shrink. Omitting maxIssues is byte-for-byte the unbounded legacy shape. Separately, a byte-budget-family E_LIMIT (registry.js's enforceByteBudget) used to carry a hint written for the whole family ("...a maxIssues/maxNodes cap...") even when the FAILING tool had neither — src/remedies.js now appends that tool's own actual narrowing arguments, mined from its schema, and src/budgets.js's report family hint no longer hardcodes argument names not every member has. docs/mcp-tools.json / web/mcp-tools.html regenerated. Note: cad_dfm_check's published schema was already ~13.4 KB (over tool-list-budget.test.js's 12 KB top-3 line) before this ticket, from the shared commands/schema/analysis-ops.js op description and its 29 per-rule rules overrides — out of this ticket's scope to edit; the two new properties add ~360 B on top of that pre-existing overage.

PMI and datum attachments bind by NAME, not by entity id. cad_pmi_author stored its targets as raw face/edge ids, so an annotation survived a rebuild only when the rebuild happened to reproduce the id for the same face — luck, not identity. On the own box, dropping one edge from a fillet's pick list makes #276 name a different fillet, and dropping two makes it name a box wall: the old code kept the annotation, silently, in both cases. Each target is now stored as the id you gave plus the persistent reference minted beside it (ops/refresolve.js's captureRef, with stableTag), and every read re-derives the id from that reference. Three things change for a caller: cad_pmi_author's response echoes item.targets[].ref ({tag, stableTag} — the fingerprint stays on the document); cad_pmi_list gains resolution.rebound ({itemId, from, to, stage} for every attachment whose id moved — a fillet → chamfer swap now follows the face instead of dropping the annotation) and resolution.refused ({code:'persistent-name-mismatch', expected, found} where the only available answer carries a different persistent name — refused, and the item dropped, rather than re-pointed onto a wall); and a { ref } face selector on cad_datum_plane/_axis/_point refuses the same way rather than building a datum on different geometry, reporting a rebind on the feature-tree row as refRebinds. The wire format is additive: a set authored before this ticket carries no references, takes the old "is the id live?" path, and nothing new is even loaded for it. See docs/TOPOLOGY_NAMING.md § 6.

Parametric HP turbine blade demo, and the three refusals it walked into. demos/turbine_blade/build_blade.js builds a first-stage 42 MW-class turbine rotor blade — a cubic cad_loft_profiles airfoil (3 twisted, tapered sections of 59 points) unioned onto an analytic platform, shank and 3-lobe fir-tree root — over {tTE, neckWidth, rootFilletR, bladeHeight}, and writes STEP + STL + a census per parameter point. Nothing in the tool surface changed; what is new for a caller is the recorded behaviour of three calls, all in demos/turbine_blade/README.md with repro paths: (1) cad_fillet_edges on an airfoil-to-platform root loop refuses E_GEOMETRY "only defined between PLANAR faces" even for a single edge — the plane × B-spline blend is convex-only and a root fillet is reentrant (BLEND_PS_EDGE_NOT_CONVEX), so the demo ships with no root fillet rather than faking one; (2) a B-spline solid piercing a plane refuses E_GEOMETRY (splitByPlane: unsupported crossing edge curve B_SPLINE_CURVE_WITH_KNOTS, then genBool's SSI march declines) where the same solid seated flush on that plane unions exactly and in ~2.4 s; (3) cad_body_topology on the finished 975-edge body is 917 bytes over the 512 KiB topology budget with include:['edges'] — include:['faces'] fits. The generator also polices the open over-removal bug itself: every fillet's |ΔV| is checked against r²(1−π/4)·ΣL and a body over 1.5× the bound is refused and rebuilt without that fillet.

Investment-casting DFM process, and a turbomachinery knowledge pack to check it against. cad_dfm_check {process:'investment_casting'} adds four rules for lost-wax nickel-superalloy castings, each reusing the pack system's existing tessellation probe rather than a new one: min_section (delegates verbatim to min_wall_thickness, default 1.0 mm), min_trailing_ edge_thickness (a knife-edge dihedral detector — a face pair whose normals are within 30° of antiparallel reports thickness 0 by construction, independent of mesh density; default 0.6 mm), min_fillet_radius (unblended internal/concave edges via the existing corner detectors, default 0.5 mm) and max_section_ratio (thickest sampled section over thinnest, default 6:1) — all four name the edge id (a sorted, pipe-joined face-id pair — new edgeRef() helper) plus both adjacent faces, where an older rule would have named only a face. min_draft_angle is explicit not_applicable: the ceramic shell is broken away from the casting rather than drawn from a rigid die. data/dfm/packs/turbomachinery.json ships alongside it — 44 entries (16 core / 42 standard) covering first-stage HP rotor blade proportions for both aero-derivative (330 mm hub, 60 mm span, ~11,000 rpm, ~48,000 g) and industrial-frame (500–900 mm hub, 3,000/3,600 rpm) turbine classes, fir-tree root and platform geometry, tip-clearance tolerance-chain contributors, IN718/IN738LC properties at 20/650/850°C, and 3 kernel-verified recipes (fir-tree root, platform+shank block, disc sector with a rim slot). commands/schema/analysis-ops.js's hardcoded DFM_PROCESSES/DFM_RULES enums — the one other place this vocabulary is transcribed — are extended in the same order.

Text to NativeCAD prompt bank and scorecard. benchmarks/text-to-nativecad/ holds 85 natural-language part prompts across 24 sectors — 70 recipes lifted from the manufacturing knowledge packs, 15 hand-authored around the native circle/slot/rect profile primitives — each with an acceptance block (watertight, volume band, bounding box, face-type census, DFM verdict). run_replay.js scores the kernel with no model in the loop; run_live.js scores a real agent over stdio against the same blocks and skips cleanly without ANTHROPIC_API_KEY; score.js publishes web/text-to-nativecad.html, which renders offline. First baseline (2026-09-05): 79/85 accepted (92.9%), 84 built, 3.81 tool calls per part; the six reds are real kernel findings, filed as tickets.

tools/list is 15% smaller — 387 KiB / 3813 B per tool → 330 KiB / 3246 B per tool for the same 104 tools. The bloat was never prose: it was the SAME sub-schema inlined 2–25× inside one tool (cad_dfm_check's 25 per-rule overrides, the fillet/chamfer edge selector inlined inside its own array branch, the extrude profile shape 3×). registry.js now folds a tool's byte-identical repeated subtrees into $defs/$ref at define time, on the same clone that is validated and published. No property, enum value, default or required flag was removed; full prose remains in cad_help. A host whose schema reader cannot follow an internal $ref should say so — the conformance lint treats internal refs as a warning, not an error.

Robustness page: tolerant_ok. A body that is watertight under its per-entity tolerance but not under the global eps is now its own class beside watertight / not_watertight. Re-sweeping the pinned 150-file subset: 5 of 21 not_watertight files reclassify; the per-file max-tolerance histogram (1e-6 … 1e-1 mm; widest 5.75e-2 mm) and before/after counts render offline in a new section of web/robustness.html, with the delta in web/robustness/history.json. tools/tolerance_reclass_sweep.js is chunked and resumable.

A fillet on a PLANE × B-spline edge is built by the scoped path now, and an open spine names the narrow face. A selective fillet on ONE convex edge between a planar face and a B_SPLINE_SURFACE_WITH_KNOTS face is offered to ops/blendPlaneSpline.js (a marched spine, no constructed offset patchand a MEASURED G1) before the general rolling ball; the result reports method: "plane-spline-rolling-ball" with the two supports it resolved. Nothing else moves: blendNurbsEdge remains the path for spline × spline, for chains and for every case the scoped path declines — including its own G1 refusal, so a radius that built before still builds. Measured on the B-spline-faced corpus files: fillet_single_edge 1.69% → 1.69%, fillet_edge_chain 2.70% → 2.70%, fillet_concave 0% → 0% okPct, no row's status or code changed. New caller-visible refusal on that pair: BLEND_PS_SPINE_OPEN (in place of BLEND_NURBS_SPINE_OPEN where both agree), whose message now names the face that ran out — "face #148 is 2.3744 mm wide; radius must be < 2.3744" — with narrowFace / narrowFaceWidth on the error. The width is a measurement of the face itself, walked across the surface from the picked edge and clipped to the face's own boundary rather than to the padded patch the kernel evaluates a plane over.

Stable face/edge identity across a blend re-solve. Editing a fillet radius, or swapping a fillet for a chamfer, no longer moves the names downstream consumers hold. Dress-up features emit a PERSISTENT name derived from their INPUT rather than from the position of the face in the result: a blend face is named after the source edge it consumed (f:blend[edge=…]), a spring edge after (source edge, support face), a corner patch after its source edges, and an untouched face keeps the name it had before the feature ran. Fixes a silent re-point in which f:side[0] named a box wall on a fillet and a chamfer flat after the swap, at full resolver confidence. ops/refresolve.js consults the persistent name first, but only for references that carry one, so every reference minted before this release resolves exactly as it did (naming map version unchanged). cad_fillet_edges / cad_chamfer_edges results carry a persistentNames count. See docs/TOPOLOGY_NAMING.md.

Booleans against B-spline faces no longer lose volume to a one-quad tiling. ops/faceSplit.js sized a chart's interior grid from two straight mid-line probes; on a twisted ruled patch — what a two-section loft between misaligned polygons actually is — both read zero and the whole face was tiled as a single bilinear quad, so a watertight, gate-passing union could be >1% wrong (measured 2.7% on a lofted hull with a radial crossbar). The grid is now sized in 2-D against the diagonal chord, a B-spline face's chord budget is measured from its sampled principal curvature instead of guessed, and any face pair with a B-spline member is held to an absolute 1e-4 mm chord ceiling. Analytic pairs are byte-identical. New nightly boolean family O: 20 seeded loft × primitive operand pairs (tools/loft_operand_pool.js); the volume identity on that pool is pinned at 6e-3 relative — the bound this engine reaches while it bakes curved fragments into planar triangles.

Every coded refusal now names a next step, and a mechanical one ships the corrected call. Additive only — no error code and no error message changed, so anything keying off either is unaffected. Two things are new on the wire. (1) A refusal that previously fell back to its code's generic hint ("details.issues names the exact JSON path that failed") now carries a sentence built from what the envelope already knew: the schema's own field description and the value you sent for an E_INVALID_ARGS; the operator remedy note (tools/operator_remedy_table.js) for a kernel refusal; the errno for a filesystem failure. Measured over tools/audit_refusal_hints.js's matrix — one canonical wrong call per code class per tool family — specific hints went from 17/35 (48.6%) to 35/35 (100%). (2) error.retry — {tool, args}, where args is a complete argument object you may send verbatim, never a patch to merge (a merge-shaped field would be named argsPatch; this server emits none). It appears only where the remedy is mechanical: a mistyped tool name the registry already corrected (details.didYouMean), arguments rejected solely for fields the tool does not have (the fix is deletion), and an E_LIMIT the governor already solved (details.suggestion). All 6 mechanical rows in the matrix carry one and all 6 succeed when applied, asserted end-to-end in test/e2e/scenario-c-error-paths.test.js. retry is declared as an optional property on errorEnvelopeSchema(). See docs/REFUSAL_HINT_COVERAGE.md.

Native sketch primitives in profiles — stop sampling N-gons. Any profile loop of cad_extrude_profile (outer, holes, and profile's own pair) and cad_revolve_profile's profile may now be a primitive object instead of a point list: {"circle":{"cx","cy","r"}} (d accepted for r), {"slot":{"x1","y1","x2","y2","width"}} — the two points are the END-ARC CENTRES and width is the full width, so the overall length is |P2-P1| + width; the centre form {"cx","cy","length","width","angle"} takes length as the OVERALL tip-to-tip length (> width) and angle in radians — and {"rect":{"x","y","w","h","cornerRadius"}}, where x,y is the centre, not a corner. A primitive is a WHOLE closed loop; mixing one into a list of points is refused. The geometry is EXACT: a circle extrudes to one CYLINDRICAL_SURFACE bore (two half-faces sharing two seam edges, the same construction cad_cylinder uses) and revolves to a real torus of TOROIDAL_SURFACE legs, with volume equal to π r² h / 2π² R r² to 1e-9 relative. That removes the ~1% area error, the N-per-hole facet fan that made later booleans and fillets fragile, and the hand-written winding. Winding is no longer the caller's problem at all: in a primitive-bearing profile the role of every loop is inferred by containment (nested = hole) and reported back in the result's profilePrimitives.note, so a "hole" put in outer is silently corrected — visibly, in the result. New machine-branchable refusals: PROFILE_PRIMITIVE_MIXED, PROFILE_PRIMITIVE_FIELD, PROFILE_PRIMITIVE_DEGENERATE, PROFILE_PRIMITIVE_AMBIGUOUS, PROFILE_LOOP_SHAPE, PROFILE_ROLE_AMBIGUOUS, REVOLVE_TORUS_DEGENERATE, REVOLVE_PROFILE_CROSSES_AXIS, REVOLVE_PRIMITIVE_ARCS_CONFLICT. The server instructions and the extrude/revolve tool descriptions now lead with this and demote the "approximate a circle with a 24..32-gon" advice to a fallback for shapes that genuinely have no primitive. Behaviour to be aware of: cad_pad is unchanged and still wants a sampled polygon (its imprint path is polyline-based), and a polyline-only document produces byte-identical STEP.

Profile loops are auto-oriented — a reversed hole no longer fails two calls later on watertightness. An earlier change already fixed this for a primitive-bearing profile; this extends the SAME normaliser (commands/profile-primitives.js's expandProfile()) to every plain-polygon outer/holes loop cad_extrude_profile and cad_revolve_profile see. Every loop is classified by containment depth (even = outer, odd = hole), a loop whose signed area disagrees with its role is reversed, and the result carries a notes[] entry ({loop, flipped:true, reason}) for each one actually flipped — an already-correctly-wound profile gets an empty notes[] and byte-identical STEP output. A loop that crosses itself has no well-defined orientation and is refused by code, E_PROFILE_SELF_INTERSECTING, rather than guessed at. Opt out per-op with orient:false (a primitive-bearing profile still needs expansion to exist at all, so the flag has no effect there). cad_revolve_profile's r-z silhouette already normalized its own winding internally (ops/revolve.js's normalizeProfile()); this adds the same notes[] report and self-intersection refusal on top of that, without changing what gets built. cad_pad is unchanged (its imprint path is polyline-only, a follow-up own note). See tests/test_cadsf-1167.js.

The session survives a kernel crash — an op journal, and a replay. Every accepted op is appended to a per-session NDJSON journal under the OS temp directory (path in cad_server_info's new session.journal), one line per op with its args and the handles it produced, plus an in-flight line written before the handler runs. After a kernel death or a restart the journal is replayed: the documents come back, and every body handle is re-bound under the string it already had — ab12_solid_7 is still ab12_solid_7, so an agent holding forty handles from before the crash holds forty working handles after it. The op that was in flight is NOT replayed; it is recorded as poison with the error that killed it, and the call that died is refused with the existing E_KERNEL_CRASHED (no new code) and the hint "the document was restored; the op was not applied". cad_list_model_state gains restored ({restored:true, at, journal, documents, ops, handles, poison}, null when this listing was not rebuilt) and journal. A journal older than the store's idle session TTL (1 h) is discarded rather than replayed — a restart is a continuation, coming back tomorrow is not. CADSOLVER_NO_JOURNAL=1 turns the whole mechanism off; CADSOLVER_MCP_JOURNAL_DIR repoints the directory and CADSOLVER_MCP_JOURNAL_MAX_AGE_MS sets the window (0 = forever). Behaviour change to be aware of: a pre-restart handle that used to fail E_NOT_FOUND after a restart now RESOLVES, to the same geometry it named, because the document behind it was rebuilt from the same ops in the same order. A re-exported tolerantly-stitched import declares the tolerance it was judged at. cad_export_body to STEP wrote the 0.01 mm baseline into UNCERTAINTY_MEASURE_WITH_UNIT even when the import's seams had been accepted watertight only because Topology widened them over a gap (0.05 mm, say) — the widening lived on the Topology, not in the per-entity tolerance stores the writer read. The header now also folds in the widest widened seam (only tolerances above the model's own eps count, so a plain body's output is byte-identical), and a reader honouring GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT re-imports the body at the tolerance it was built at rather than 5× tighter. Faces whose file never marks an outer bound now blend and chamfer. STEP makes FACE_OUTER_BOUND optional and KiCad's exporter never writes it — every face of every KiCad footprint arrives with plain FACE_BOUNDs only — so cad_fillet_edges / cad_chamfer_edges on such a part refused blend-sel-face-no-outer-bound / chamfer-sel-face-no-outer-bound (~320 rows on the robustness subset) the moment they rebuilt a face with a hole in it. Topology now infers the outer loop once, at import, where the file is silent: the loop whose bounding box contains every other loop's, else the largest planar area, else the largest extent; a face with two equally large boundary loops (a seamless periodic band) is left unclassified and marked ambiguous rather than guessed. A face the file did flag is never second-guessed. Rebuilt faces are written back with an explicit FACE_OUTER_BOUND. On 16 of those files both codes went 54 → 0 and 40 of 78 blend/chamfer attempts now build clean (14 more build carrying only defects the import already had).

Fillets and chamfers on imports that author .F. FACE_BOUNDs now behave exactly as on .T. ones. STEP composes two direction flags — the ORIENTED_EDGE's and the FACE_BOUND's — and KiCad/FreeCAD footprints and NIST ctc_04/ftc_10 write the bound flag .F.. Several blend/chamfer sites still read the ORIENTED_EDGE flag alone: the whole-body and selective fillet classified every edge of such a face with the OPPOSITE convexity, put the rolling ball on the wrong side (so cad_fillet_edges refused "radius is larger than the feature next to edge" on a fit that was fine), cad_chamfer_edges refused "setback runs the wrong way", the blanket chamfer could publish a body with inconsistent face orientation, and the NURBS rolling-ball path re-emitted every untouched .F.-bound face wound backwards — the 25 + 28 orientation-inconsistent refusals on the robustness subset were that, not an import defect. cad_face_edges reported the same inverted convexity. All of these now read the effective direction (Topology's le.sense) and re-emit loops through the canonical .F.→.T. transform (Loop#effectiveEdges, whole-loop reversal). On a body with .T. bounds nothing changes (byte-identical results); on the same body re-authored with .F. bounds every op now builds the same solid to the same volume. The robustness battery's own edge classifier had the bug too, so fillet_single_edge / fillet_concave seeds on those corpus files were sometimes the wrong edge — re-sweep before comparing against the 09-02 baseline. Shell/offset/thicken carry the same raw read and are tracked separately.

New tool cad_assembly_contacts — which faces actually touch. The pre-flight a multi-body model owes any downstream analysis: a solver bonds whatever touches and welds nothing that does not, and neither half is visible in a viewer. Per body pair with overlapping bounding boxes — coplanar PLANE faces with opposing normals report their measured overlap AREA in mm² (exact on the tessellation, holes excluded) and the gap; coaxial CYLINDRICAL faces report signed radial clearance (negative is interference), diametral clearance, axis offset and axial engagement length, with the bore named; everything else reports the exact closest triangle pair and both face ids. unbonded lists every body touching nothing, with its nearest measured neighbour. Reads NAUO product structure when the file has it, one instance per solid when it does not. READ-ONLY (ops/contacts.js); a 149-body assembly takes ~4 s.

Tolerant topology, end to end. Every edge, vertex and face carries a tol (never null; the global eps by default, the file's declared UNCERTAINTY_MEASURE_WITH_UNIT on an imported body). An edge or vertex merged by stitch — and so by every boolean and heal — inherits max(tol) instead of the survivor's tighter claim; SSI publishes the tolerance a trace actually achieved and the boolean declares it on the edge it snapped to. cad_body_topology reports per-edge tol plus {eps, maxTol, maxTolEdgeId, countAboveEps}; STEP's global uncertainty carries the widest of those numbers and a reader restores it onto every edge. docs/TOLERANT_TOPOLOGY.md.

Fillet — rolling-ball blend between a planar face and a B-spline face. The spine is marched directly: the plane's offset is exact and unbounded and the B-spline constraint is evaluated by Newton point inversion instead of by intersecting two constructed offset patches — 2.5× faster and a tighter seam than the general path, which stays available as spineMode:'ssi'. The blend is emitted as an exact bicubic b_spline_surface_with_knots band with spring-curve trims of both supports, and its tangency to them is measured on the authored surface (50 samples per seam, refused past 1e-3 rad). Scoped to one convex plane/B-spline edge; chains, vertex blends and every other support pair refuse by code (BLEND_PS_*).

A fillet whose band would cross a hole is refused before it is built, by name. cad_fillet_edges on an edge whose blend footprint runs over a bore mouth, a pocket or an island root on one of its support faces now refuses with blend-sel-footprint-crossed, naming the crossing edge(s) (crossingEdgeIds) and the largest radius whose footprint clears the feature (measuredFeatureSize, also in the message), and the input body is untouched. Before, the blend's dry run polygonised face loops from edge vertices only — a circular hole mouth was a two-point polygon it could not see — and its reach test was a ray at three stations along the edge, so a hole anywhere else was missed: the band was laid straight across it and the result self-intersected. On the parity-B5 body (bore at mid-span) the mesh-tier gate caught that after the fact with a "27 duplicated windings" message; with the bore at x = 10 or 33, or as a blind pocket, the same call returned ok: true and only cad_dfm_check's full tier found the two faces overlapping. Those calls refuse now. A hole clear of the band, and every legitimate fillet, builds exactly as before; a notch in a face's OUTER boundary between the three ray stations is now seen too and refuses as blend-sel-radius-overshoots-neighbour with its own measuredFeatureSize. Trimming the band against the hole (so these calls build) is follow-up work.

cad_measure gains a section kind. Slice a solid with a plane and get area, centroid, second moments (Ixx/Iyy/Ixy), principal axes and the torsion constant J (polar approximation for solid sections, thin-wall Bredt for closed hollow ones — jMethod says which). Catches a structural member extruded from its plan outline instead of its cross-section before any downstream analysis runs on a wrong I.

cad_tessellate_body / cad_mesh_payload gain maxEdgeLength and minTrianglesPerFace. A conforming (watertight-preserving) midpoint-split refinement runs after the chordal pass; new vertices sit on the chord midpoint with normals read from the analytic surface where one exists. Face ids are preserved per triangle. Omitting both keeps the mesh byte-identical.

Third-party ops can join the tool surface: the kernel plugin SDK. New @nativecad/kernel-sdk (packages/kernel-sdk) is the semver'd surface an external package builds a feature op against — createDocument, registerOp, ops, topology, export, tessellate, license, with opaque body handles that carry no kernel entity id — plus an in-process transport and a JSON-RPC-over-stdio host (nativecad-kernel-sdk) for a caller that is not written in JavaScript. Set CADSOLVER_PLUGINS to a comma-separated list of packages or paths and each plugin's ops mount under their own namespace: gears.spur appears in tools/list, runs in a fresh document, and reports its topology plus the STEP it produced (pass _export to have the file written). Refusals use the SAME error.code vocabulary as the built-in tools, so nothing about error handling changes for a caller. CADSOLVER_PLUGIN_TOOL_SEPARATOR=_ renames gears.spur to gears_spur for a host whose tool-name grammar rejects dots; a plugin that fails to load is warned about and skipped rather than taking the server down. With CADSOLVER_PLUGINS unset the tool surface is unchanged — no new tool, no new bytes in tools/list. Reference plugin: packages/example-plugin-gear (involute spur gear, SDK ops only). Author's guide: docs/PLUGIN_AUTHORS.md.

The certified-MCP program: a conformance suite, a signed registry, and a partner licence tier. tools/conformance/run.js drives ANY MCP server over its own stdio through a fixed case set — primitives against closed-form volumes, the boolean identity V(A∪B) + V(A∩B) = V(A) + V(B) to 0.5%, a blend that has to change the topology, a STEP export that has to re-import watertight at the same volume, four deliberate mistakes that have to come back as structured refusals in the server's own published vocabulary, and a tool-schema lint — scores it 0–100 and prints a readable report naming the measurement behind every failure. It emits a conformance.json hashed over canonical bytes and, with the release key, counter-signed with Ed25519; the public key is committed under tools/conformance/keys/ and run.js verify <file> checks one offline. registry/certified.json lists the servers that passed (name, version, kernelVersion, sdkVersion, manifestHash, date) and is rendered at web/registry/. New for callers: cad_server_info gains a certified block quoting this build's registry entry when a registry is visible (absent in a published install, which ships none), and a plugins block when CADSOLVER_PLUGINS is set. New licence tier: partner — required to mount plugin namespaces outside the public SDK set (today gears) and to boot a kernel snapshot staged on a gated channel; CADSOLVER_PARTNER_NAMESPACES is the development escape hatch. Existing tiers are unchanged, no tool is gated by a licence, and a build with CADSOLVER_PLUGINS unset has exactly the tool surface it had before. Rules, including re-certification and revocation: docs/CERTIFICATION.md.

A refused/thrown op no longer leaves a residue body. A boolean that folds several tools inside one op (cad_boolean_subtract/_union/ _intersect with a tool list) only deleted its inputs after every fold succeeded; if an earlier fold severed the base into several disconnected lumps and a later fold was then refused, the op threw before that cleanup ran — but the severing fold's own new solid(s) were already live in the document and never published to any handle, so cad_export_body's of:"all" (and a plain document-wide export) picked them up as an untracked phantom body. commands/executor.js now rolls a failed op's document state back to exactly what it was before the op ran — no partial credit, no orphaned geometry — whether the op is invoked through the MCP tool surface or batch/cli.js. (The batch demo scripts' keep_result_solid STEP-surgery hack in demos/desk/recipe_lib.py, which existed only to paper over this by keeping the biggest solid in an exported file after the fact, is removed.)

One tool joins the surface (103): cad_design_knowledge. The DFM knowledge-pack database (docs/DFM_KNOWLEDGE_PACKS.md) ships inside the staged kernel — 17 sector packs, 952 entries, 42 kernel-verified recipes — and cad_design_knowledge {action: list | load | search} reads it: search with the user's words, load one sector at size core (paged; standard/full page further; a page over the new 48 KiB knowledge family refuses E_LIMIT naming the size that fits; an unknown sector refuses E_NOT_FOUND with the known list). Server instructions now tell an agent to consult it BEFORE modelling a named product. Steering sentences open the cad_boolean_union, cad_cylinder, cad_fillet_edges and cad_shell descriptions; they go when that follow-up lands. cad_fillet_edges on the partition path no longer returns E_LIMIT for a body that built: skipped-edge reasons are grouped with their #ids neutralised.

Nine object packs join the knowledge database (26 packs, 1379 entries, 69 kernel-verified recipes). Round one was organised by sector; round two is organised by what a text-to-CAD tester actually types. A coverage check of 174 common prompts ("coffee mug", "phone stand", "20-tooth spur gear", "Raspberry Pi case", "planter", "ring size 7") found 106 with no alias or entry; these packs close that gap: desk-everyday, kitchen-tableware, power-transmission, toys-games, maker-enclosures, mounts-brackets, garden-outdoor, jewellery-wearables, desktop-robots (the 2025–26 wave of small companion robots — Reachy Mini, Bittle, SO-101-class arms — with STS3215 servo pockets, round eye-display cut-outs, 18650 bays). Facts already owned by robotics / electronics / consumer are referenced with see, not copied. Every recipe's expect block is what the kernel returned on 2026-09-04.

Housekeeping visible from outside: docs/mcp-tools.json / the tool reference and the web parity matrix are regenerated for the 103-tool surface, and the NURBS and tolerance showcase pages on featureboard.ai carry their data again.

Private (not shipped)

Work that exists in the repo and is deliberately NOT in this package. A private tool group (docs/PRIVATE_TOOLS.md) registers only under an owner licence or CADSOLVER_PRIVATE_TOOLS=<group>, and its directory, kernel modules and assets are excluded from the npm tarball and the .mcpb. If you are reading this from an installed package, nothing below is present in it — the section is here so the history stays honest about what the version contained, not as a menu. cad_server_info remains the authoritative inventory of what you have.

fea — cad_fea_case and cad_fea_run, the stress-analysis loop (1144, 1145, 1146; withheld by). Withheld because cad_fea_run's preferred solver is a partner's product and the commercial conversation has not closed. cad_fea_case turns a built body plus face-selected boundary conditions (fixed / sliding / force / pressure / bearing, faces = cad_body_topology ids, material a preset or SI values) into a case directory: body.stl, one patch_<n>.stl per BC sliced from the SAME tessellation, triangle_faces.json and case.json (docs/FEA_CASE.md v1). Refuses E_INVALID_ARGS with reason unrestrained when no face is fixed, E_NOT_FOUND for a face not on the body. cad_fea_run solves it with the first solver installed — the partner's meshfree solver (needs its wheel) then CalculiX (ccx on PATH or CADSOLVER_CCX; the kernel's own VolumeMesher tets, C3D10) — and returns results.json: max von Mises and where it is, maxVonMisesFace (the kernel face under the peak), displacement, safety factor vs yield, plus a one-line summary. No solver installed ⇒ ok:false, error:"solver_missing" with install advice; a stress number is never fabricated. case.json.provenance is echoed so iterations stay self-describing. docs/AGENT_FEA_LOOP.md walks the bracket loop (tools/fea/bracket_demo.js: inside-corner fillet r = 1 → 6 mm takes the peak from 179 → 44 MPa, peak located on the fillet face each time). Not on the public tool reference, not in the web parity matrix, and no parity release-exception — a withheld tool is not a web gap, it is not a surface at all.

↑ all releases

NativeCAD 0.7.x

v0.7.0 “Fischer”

released September 3, 2026

sha256 nativecad-mcp-0.7.0.mcpb cda028f7988db22f908528d8d2cda1adbecdb0061050703a81194abf68e8e7ecsha256 nativecad-mcp-0.7.0.tgz ca5125dc1799f8b0bf02766d36ed4f8e2ef3e2015ef8750a70d70a5197991099

One tool joins the surface (102, was 101) and whole-body blends change behaviour. Nothing was removed or renamed; every 0.6.0 call keeps working, but two classes of call now SUCCEED where they used to refuse — read "Behaviour changes" before keying off a blend refusal. cad_server_info remains the live inventory.

New tools

  • cad_tube_sweep — a closed planar section carried in the path's normal plane along a polyline/arc path: the pipe-and-tube case cad_sweep_profile had to approximate.

Behaviour changes (calls that used to refuse now build)

  • cad_fillet_edges / cad_chamfer_edges with NO edges selection (whole body) now partition and continue by default: every convex edge that can take the blend is dressed and the rest are reported under report.partition.skipped with a reason code each, instead of one bad edge refusing the whole body. Pass edges:[...] for the old all-or-nothing contract on a selection.
  • cad_fillet_edges on a partial pick of a tangent chain falls back to the whole chain rather than refusing blend-rim-partial-selection; circular edges are now first-class on the selective path; the concave (reentrant) chamfer builds; an island's full root loop blends.
  • cad_boolean_* on cylinder∩cylinder produce an analytic edge, and radial bore patterns stay analytic past the quarter turn.
  • cad_shell offsets curved faces exactly — plane/cylinder/cone/sphere/torus — and a half-open periodic surface no longer votes its seam.
  • cad_import: orient-shell pass and sewing at import; a hole landing on a wall's seam no longer refuses.

Budgets (new refusals, all recoverable by name)

  • Booleans carry budgetMs (default 120 s) and refuse boolean-budget-exceeded with phase/elapsed/counts; shell likewise (shellBudget, 20 s); tessellation caps CDT sites per face at 60 000 and degrades chord tolerance before refusing tess_site_budget_exceeded. Every bare blend/chamfer throw now carries a reason code; loft and split refusals too.

Response-shape changes

  • cad_fillet_edges / cad_chamfer_edges on the whole-body (partition-and-continue) path: report.partition.skipped is now grouped — one {code, reason, edges[], count} per distinct reason instead of one {edge, code, reason} per edge — and report.edgeReport entries that only repeat what partition.blended / partition.skipped already say are dropped (an emptied array becomes a pointer string). The per-edge edgeReport array is unchanged on the selective (edges:[...]) path and whenever there is no partition block. Why: the per-edge prose put a plain 16-edge plate's response at 31 KB against the modeling family's 16 KB budget, so a fillet that BUILT came back as E_LIMIT.

Fixes

  • cad_design_review drawing sheet: the polyline simplifier deleted geometry — the tip of a doubled-back run (two coincident arcs) and up to 0.1 mm of sag on dense arcs — so a watertight solid's outline drew open at a bend tangent. Now Douglas-Peucker with distance-to-segment, honest to the stated 0.02 mm sheet tolerance; sub-tolerance stubs no longer draw as dots.

↑ all releases

NativeCAD 0.6.x

v0.6.0 “Elder”

released August 30, 2026

sha256 nativecad-mcp-0.6.0.mcpb 9d4af9857c2cc5df73cb614350a10b5f1e5669685602ee357942b70509c77f27sha256 nativecad-mcp-0.6.0.tgz a34c6cfd55e46d53862da70c8a05a8f77e703e6847e8dbb2e8a8f621c9734412

Five tools join the surface (83, was 78) — sheet metal arrives. Nothing existing was removed or renamed; every 0.5.0 call keeps working. cad_server_info remains the authoritative live inventory, and docs/mcp-tools.json is now generated by booting the real server and taking what tools/list answers.

New tools

  • cad_sheet_fold — sheet metal: a constant-thickness strip runs baseFlange flat, then each bends[] entry bends (angle in DEGREES — a brake callout, not radians — inside radius defaulting to one thickness) and runs its flange; optional kFactor (0 < k ≤ 0.5). The developed FLAT PATTERN is reported on the entry, so the blank a laser cuts ships with the folded solid. Geometric refusals are NAMED PER BEND by ops/sheetMetal.js.
  • cad_hole_pattern — hole arrays: linear / grid / circular / points.
  • cad_build_plan — batch plan runner: a spec result, plan, or ops[] in one call.
  • cad_list_model_state — session-recovery listing after an MCP server restart; pairs with the session-epoch fix so stale handle ids from before a restart refuse by name instead of resolving to the wrong body.
  • cad_license — seat status/activation; shipped in 0.5.0 builds but missing from the published manifest until now.

cad_export_body

  • format: "flat-dxf" — the developed flat pattern of a folded sheet body as DXF.
  • glTF/GLB: UV unwrapping with TEXCOORD_0, seams on closed surfaces, and multi-LOD export — N tessellation levels in one call.
  • Fix (error-vocabulary relevant): A schema default for lodMode had the same shape as the view regression; the default is removed, so an export that never mentions LOD no longer takes the LOD path.

cad_import

  • stitch / stitchTol are pinned by a new field-level drift check over the whole schema surface, so a renamed or retyped field fails the suite the day it happens.

Other

  • Windows absolute paths (C:\...) are recognized on any host OS.
  • _boolProv is internal and no longer leaks into entries.
  • XT writer message/doc follow-ups for rational NURBS.
  • LICENSE/COMMERCIAL.md carry the 2026-08-30 licensing overhaul: every license is an annual term, Excluded Parties (competitors) are ineligible, competitive-use restrictions, removal on termination with downstream flow-down, and evaluations approvable by the licensor's authorized (incl. AI) agents.

↑ all releases

NativeCAD 0.5.x

v0.5.0 “Doráti”

released August 29, 2026

sha256 nativecad-mcp-0.5.0.mcpb 948f3cc1ae47f2e4874e79a6cfbf2698bbcf73a5f39a383c5bb48257c6667c39sha256 nativecad-mcp-0.5.0.tgz f44ba1a02c32c2943ce6c7a28dd8c45f97c545cd794fdc8c5bf9aba84cd1ccea

Nine new export formats. No new tools. The tool surface is the same 78 names 0.4.0 shipped — nothing added, nothing removed, nothing renamed. Everything in this release lands as new formats and options on tools that already existed, so a caller written against 0.4.0 keeps working unchanged; cad_server_info is still the authoritative live inventory.

cad_export_body

format grows from five values to fourteen:

formatwhat it writes
pdf / prc3D PDF and raw PRC (ISO 14739-1), with pattern-instance assembly placement
gltf / glbglTF 2.0 scene — welded normals from the B-rep, shared meshes for instances, provenance in asset.extras. gltf also reports binPath for its sibling .bin
x_t / x_bParasolid transmit, text and neutral-binary — single-body parts, graph-identical round-trip against XtReader
3mfthe additive core-spec package
satACIS SAT
ap242-xmlAP242 domain-model XML assembly structure plus one Part-21 .stp per part (partPaths); refuses a structure-less document by name (AP242XML_NO_PRODUCT_STRUCTURE)

New options, all optional and all ignored by the formats they do not apply to (the tool refuses rather than silently ignoring when an option is meaningless for the chosen format):

  • view: "wireframe" | "hlr" — svg/dxf only. "hlr" is vector hidden-line removal: real B-rep edges plus view-dependent silhouettes, occlusion-tested, emitted as dashed-hidden SVG groups / VISIBLE+HIDDEN DXF layers, with visibleEdgeCount / hiddenEdgeCount on the response. Default output is byte-identical to 0.4.0.
  • creaseAngleDeg, bakeTransforms, animations, materialsOverride — gltf/glb.
  • envelope / envelopeViews — gltf/glb Envelope Export: a measured exterior-visibility cull that ships only the bodies actually visible from outside, with a kept/dropped report.
  • pmiFrom — pdf/prc: graphical PMI carried into PRC markup and annotation views.
  • compression, quantize — gltf/glb, declared but NOT IMPLEMENTED; they are part of a separate change and are described as such in the schema rather than silently accepted.

cad_import

New options heal, stitch, stitchTol, structureOnly, subtree, and new readers behind the existing file-extension routing: ACIS SAT, 3MF, and SLDPRT / Parasolid XT including binary XT extracted by content. New SAT_* reason codes.

cad_boolean_union / _subtract / _intersect

New meshFallback option (default off): when the exact ladder refuses, run the faceted mesh-boolean path with its repair pipeline and leak census, returning a B-rep flagged meshFallback/faceted. Opt-in — nothing changes unless you pass it.

Fixed

  • cad_export_body refused every non-svg/dxf export. An earlier change gave view a schema default: "wireframe", and validate.js runs ajv with useDefaults: true — so args.view was materialised on every call and the "view only applies to svg/dxf" presence guard fired off the default alone. Between an earlier change landing and this release, step, stl, contour-dxf, pdf, prc, gltf, glb, x_t, x_b, 3mf, sat and ap242-xml all returned E_INVALID_ARGS. Fixed by removing the default; "wireframe" is unchanged as the behavioural default. If you are on a build between those two points, upgrade.

0.4.0 packaging note (2026-08-28)

npm run build:packages works again end to end: the three drifted copies of the browser esbuild flag list are now one sourced file (web/browser-bundle-flags.sh), which unblocks the @nativecad/viewer 0.2.0 dist and its publish. Build-output only — no behavior change in this package.

↑ all releases

NativeCAD 0.4.x

v0.4.0 “Dohnányi”

released August 27, 2026

sha256 nativecad-mcp-0.4.0.mcpb efe36f45b1e94cf1d5487d5d45919c0d8e11fd8b1b6f3ce1393db89392677ed3sha256 nativecad-mcp-0.4.0.tgz d4d9af13fced523ac06b546c69958b03ed7129f6f61544f0ac4dc8a292ddffba

The package is now nativecad-mcp, and the command is nativecad-mcp. NativeCAD (formerly CADSolver) is the same server, the same kernel and the same tool surface under the product's new name (docs/BRAND.md). Nothing about the geometry, the schemas or the error vocabulary changed in this release — this entry is entirely about names.

beforeafter
npm package@cadsolver/mcpnativecad-mcp
command / npxcadsolver-mcpnativecad-mcp
MCPB bundlecadsolver-mcp-<version>.mcpbnativecad-mcp-<version>.mcpb
kernel library@cadsolver/kernel@nativecad/kernel (0.2.0)
viewer library@cadsolver/viewer@nativecad/viewer (0.2.0)

The package name lost its scope on purpose. npx <name> resolves a registry package, and npm's exec refuses a package with several bins unless one of them is named after the package — so a scoped @nativecad/mcp could not have kept the documented one-liner working next to a compatibility bin. Unscoped, the package name and the primary bin name are the same string, so npx with the bare package name is exactly what it looks like.

Nothing that already works stops working

  • cadsolver-mcp is still a bin in this package. It is a shim: it writes one line to stderr (never stdout — that is the JSON-RPC wire) saying the command was renamed, then loads bin/nativecad-mcp.js, which recognises the alias path as a process entry and starts normally. Same server, same exit codes, same stdout bytes. Silence it with NATIVECAD_MCP_NO_DEPRECATION_NOTICE=1 if your host treats any stderr output as a boot failure.
  • An existing MCP host config keeps booting, whether it spawns the bin by name or by path (node …/bin/cadsolver-mcp.js), and whether it came from an .mcpb bundle built before the rename or a hand-written JSON block.
  • Every CADSOLVER_* environment variable is unchanged — CADSOLVER_MCP_LOG_LEVEL, CADSOLVER_MCP_OUTPUT_DIR, CADSOLVER_MCP_TOOL_PATH, CADSOLVER_QUIET, CADSOLVER_MCP_STOCK_STDIO. Renaming them would have broken running installs for no user-visible gain.
  • Tool names, resource URIs (cadsolver://…), error codes, the project file extension and the repository URL are unchanged. Internal module, class and variable names are explicitly out of scope (docs/BRAND.md).

One thing a caller may notice

initialize reports serverInfo.name from package.json, so a client that pinned the literal @cadsolver/mcp now sees nativecad-mcp. The same string is stamped as source in emitted robustness/eval artifacts (<name>@<version>). Key off the version, not the name, if you need to branch.

Migration

Change one line in your host config, when convenient:

{
  "mcpServers": {
    "nativecad": { "command": "npx", "args": ["-y", "nativecad-mcp"] }
  }
}

The old block keeps working until the alias bin is removed, which will not happen before the old name has been out of the docs for a full release cycle.

↑ all releases

NativeCAD 0.3.x

v0.3.9 “Davis”

released August 25, 2026·no downloadable artifact for this version (tarball builds began at 0.10.0)

Kernel-refresh release: no MCP tool surface changes since 0.3.8; the staged kernel picks up the day's kernel work. One new refusal a caller can hit:

  • steppedRevolve refuses boundary-shoulder profiles: a profile whose first or last segment is a same-z radius step used to silently build a SOLID at the larger radius (a washer/tube request came back +18.86% heavy); it now throws naming the disc it would have built and pointing at the closed-loop revolve constructions that do support bores. Reachable through cad_revolve_profile-adjacent shaft tooling only with a malformed profile; every shipped call site is unaffected.
  • Kernel additions staged but not MCP-exposed: ops/positioner.js (serial revolute axis chains + coordinated motion), ops/fixtureKit.js / ops/fixtureAudit.js (parametric fixtures + 3-2-1 fit audit), ops/weldWeave.js (weave patterns; sub-pitch seams report the ACHIEVED schedule), ops/productionReport.js (cycle-time/production reports).
  • MassProperties verified against closed forms for hollow revolves (tube/washer/tapered-bore, <0.5% chordal) as part of a separate change — no behavior change, the suspicion was disproven.

↑ all releases

v0.3.8 “Chung”

released August 25, 2026·no downloadable artifact for this version (tarball builds began at 0.10.0)

Everything under "Unreleased" below ships in this build, plus the day's kernel work it stages (kernel tree 89682d8fcafb…):

  • Import is 3.9× faster corpus-wide: Shewchuk stage-A filter in front of the exact in-circle predicate, shared-Topology health checks, one topology per import batch. The 9.2 MB cadsf-519 dice-inlay file: full import verb ~100 s → ~12 s. Geometry byte-identical (per-solid SHA-256 proven across the corpus).
  • STEP exports carry real body names: per-solid PRODUCT/NEXT_ASSEMBLY_USAGE_OCCURRENCE, read back on import; cad_import surfaces names per solid. Files written with no names are byte-identical to 0.3.7's output.
  • Units end-to-end: cad_import/cad_save accept units (mm/cm/m/in/ft/um); declared file units are honored by default — an inch-declaring STEP no longer imports 25.4× too small.
  • planar_sheet + grouped blends + CFD knobs in the command schema (schema 1.9.0); blend-on-blend band terminals build instead of refusing; cad_fillet_edges accepts perEdge/order.
  • Cost model: docs/COMPLEXITY.md + opt-in per-op costModel reporting (CADSOLVER_COST_MODEL=1).

Fixed

  • scripts/stage-kernel.js / plugin/build.js: staging degrades to overwrite-in-place where unlink is refused (CIFS mounts), failing hard if a stale file would survive; the review tool group's documented browser-branch require is allowlisted (staging was refusing to run at all).

↑ all releases

NativeCAD Earlier notes

Unreleased

Carried from the CHANGELOG as written; predates the versioned entries below.

Added

  • Document versions — cad_snapshot {name, document?}, cad_branch {from, name}, cad_restore {version, document?}, cad_versions {document?}. Lewis: "keep a few versions of something in memory as we do simulation and then keep modifying it without reloading." A version is a frozen copy of the document's op list and handle table (the op list IS the model, so a snapshot is a JSON copy; the B-rep is never stored) plus {bbox, volume, bodies} measured when it was taken. cad_branch forks a NEW document from a version — the ops replayed, every body under a fresh handle (handles maps old to new), the original untouched — and gives it its own baseline version (parent = the forked one). cad_restore rewinds a document to one of its versions: handles minted after it are released (they stop resolving), handles it had are re-bound under their original names, the kernel worker's copy is rebuilt from 0. cad_versions lists a document's versions newest first with current (the version the document sits at exactly) and, per version, the simulation results recorded while the document sat there: a successful cad_wind_tunnel / cad_fea_* call now attaches a bounded digest of its result to the document's current version (nothing in the session kept those numbers per document before). Journal-backed — new version, restore and result records — so versions and rewinds survive a restart; cad_session_reset discards them with the documents. New codes E_VERSION_UNKNOWN and E_VERSION_EXISTS (both recoverable); restoring a version of another document is E_CONFLICT (branch it instead).
  • cad_delete_body {body, document?} and cad_batch {atomic:true}. Nothing on the surface could remove a body: a model that re-sent one failing batch three times left two orphan seats behind, and the only remedy was cad_session_reset. cad_delete_body takes a body/sheet/mesh/group/sketch handle (or a datum op's id), removes the op that produced it from the document's op list, rebuilds the rest, releases its handles and rewinds the op journal — so the body stays gone across every later replay and does not come back on a crash restore. New code E_BODY_IN_USE (recoverable): a later op depends on the body — delete the dependents first, last to first. One body of a multi-body op is refused (E_INVALID_ARGS): delete the group handle. cad_batch {atomic:true} opts into all-or-nothing on the same primitive: on the first failing op (or the clock running out) every op the batch appended is removed again and the refusal carries details.rolledBack (newest first) with every $handle null; atomic implies stopOnError. Non-atomic batches are unchanged. Document op ids (box_3) are minted past any id the list still holds, so a delete followed by a new op never collides.
  • cad_design_review — automated design-review packet. One call runs the whole review of one part: STEP import (or a session body), topology inventory, ops/check.js tier-1 validity, mass properties, DFM (tier-2) and standard views with vector HLR on an ISO/ASME sheet. Emits a versioned JSON document (cadsolver://schema/design-review, kind: cadsolver.design-review, v1) plus a self-contained printable HTML report rendered from it, and a drawing-sheet SVG. Adds nothing to the kernel — every module it drives is an existing entry point.

Two behaviours a caller may key off: stage isolation — a stage that throws becomes a fail record plus an error-severity finding and the remaining stages still run, so the tool returns a packet where six separate calls would have returned an error; and the verdict roll-up — blocked on any error-severity finding or any failed required stage, advisory on warnings only, otherwise clean. Findings from the two ops/check.js tiers are deduplicated, so summary counts distinct defects rather than mentions. A non-watertight body reports watertight: false with no volume/area/ centroid/inertia, matching cad_mass_properties' existing refusal.

Refusals (nothing written): E_INVALID_ARGS for both or neither of file/solid and for an out-of-range bodyIndex, E_IO for an unreadable or empty file, E_GEOMETRY for a STEP that parses but holds no solid.

NativeCAD 0.3.x

v0.3.8 “Chung”

released August 9, 2026·no downloadable artifact for this version (tarball builds began at 0.10.0)

Re-staged kernel from master at HEAD (previously staged 57fe9ceab255). Six commits landed after the 0.3.7 bundle was built and were not in the shipped artifact. cad_server_info's server.stagedKernel.gitSha reports the new sha.

Changed

  • Exact analytic normals in cad_mesh_payload. The encoder now substitutes the true surface normal (sphere / cylinder / cone / torus / plane) for the area-weighted facet normal. Measured on cad_sphere r=25: vertex normals went from 0.62° mean / 3.86° worst off the analytic normal to float32 noise, and duplicated periodic-seam vertices from 7.73° apart to agreement. The seam stripe and the diamond herringbone a viewer used to draw on curved faces were both artifacts of the old path, not of the geometry.
  • Ruled faces triangulate in the metric first. Cylinder walls carried roughly 6x the triangles a ruled mesh needs; interior nodes are now dropped after triangulating, −54.9% triangles.
  • Parity matrix regenerated. 87 capabilities, counts unchanged; the stale matrix had been blocking the post-commit site sync.

Note for callers keying off refusals

cadsb-122 §3 was inverted in this wave: a rim pick now blends (rim completion), with locality re-pinned under propagate:false. A caller asserting the old refusal text will see a success instead.

↑ all releases

v0.3.7 “Bychkov”

released August 8, 2026·no downloadable artifact for this version (tarball builds began at 0.10.0)

Re-staged kernel from master 57fe9ceab255 (previously staged c9f2f618) — the blend/CFD/PMI lane and an earlier change landed since the last stage and were not yet in the shipped/staged artifact. cad_server_info's server.stagedKernel.gitSha reports 57fe9ceab2551f193638f3004a0888bacc0cb5a5.

New capability

  • Cone/cylinder rim blends (new file ops/blendConeRim.js). cad_fillet_edges on a circular rim whose two support faces are CONICAL_SURFACE + CYLINDRICAL_SURFACE (a drill-point rim — the coaxial case blendCircularRim's plane/cylinder logic does not cover) now builds an exact toroidal-band blend when both supports share the rim's axis. Two new named refusals, conical-seam and curved-pair-channel, replace the old generic curved-support message on the ctc_01 sample's non-blendable conical edges (a single 118° drill-point cone's own seam is not a corner — no radius will ever blend it).
  • Channel/canal-surface blends (new file ops/blendChannel.js). A first cut at rolling-ball blends over an elliptical spine, producing a B_SPLINE_SURFACE blend face for curved-pair edges that previously refused outright (4 of 6 curved-pair edges on the reference part now build watertight).
  • Oblique blend runout and corner ends. planObliqueEnd plus a relaxed side-face test lets a blend run out onto a non-perpendicular cap; independent-edge blends now handle oblique corner ends the same way.
  • Oblique-cap chamfer runout. cad_chamfer_edges runout onto an oblique cap plane now builds watertight instead of refusing (chamfer-no-perpendicular-cap).
  • Rim-completion expansion (restored after a concurrent-commit base conflict by). A single selected arc on a partial rim now expands to the full rim via a bounded vertex-walk instead of refusing as a partial-rim pick.
  • Blend-onto-blend. Provenance-exact band detection lets a second blend land on a face a prior blend produced, with a true-cause refusal (.blendOntoBlend) when it can't.
  • PMI persistence across executor rebuilds. The presentation graph (PMI) now survives a graft-triggered executor rebuild instead of reverting to a stale snapshot.
  • CFD wind-tunnel setup fix. Setup mode now delegates to toggleCfdSetupMode() instead of proxy clicks; a previously-uninvoked preview UI now boots.
  • Web/mobile UI changes (File/Solver ribbons, mobile layout) landed in the same window but do not change this package's kernel or tool surface — see their own tickets.

What did NOT change

  • The MCP tool surface (names, schemas) is unchanged; this release changes what the kernel underneath can do, not the surface itself.
  • Every call that succeeded under the previously-staged kernel is expected to still succeed — this release is additive (new supported rim/channel families) or narrows a refusal's wording, not a behavior removal.

Verification

  • packages/mcp/test/run.js (43 files, 1444+ assertions) — green, with one known unrelated pre-existing gap: test/parity.test.js's b3-sequential-adjacent-fillets taxonomy case refuses for the right outcome but a mismatched reason string (a kernel refusal-wording gap, not a staging regression — out of scope for this release ticket).
  • packages/mcp/test/e2e/scenario-f-blend-smoke.test.js (unmodified) — 27/27 assertions, staged kernel SHA == master HEAD.
  • packages/mcp/test/e2e/scenario-g-conerim-smoke.test.js (new) — 20/20 assertions: 4/4 CONICAL+CYLINDRICAL rim edges on the NIST ctc_01 sample blend watertight through cad_import → cad_body_topology → cad_fillet_edges against the staged/packaged kernel, proving ops/blendConeRim.js reaches the shipped artifact (not just the kernel sources).
  • tools/parity-web/gate.js — exit 0, 87 capabilities tracked, every web gap named against a ticket.
  • packages/mcp/plugin/build.js — built cadsolver-mcp-0.3.7.mcpb (4546 KiB, 2276 entries); staged kernel verified in the archive (97 files, tree 1481df324d70…, git 57fe9ceab255).

↑ all releases

v0.3.5 “Blomstedt”

released August 6, 2026·no downloadable artifact for this version (tarball builds began at 0.10.0)

Re-staged kernel from master bbe6d08 (previously staged 1d207a35, 2026-07-31) — every blend fix landed since is now live in the shipped/staged artifact. cad_server_info's server.stagedKernel.gitSha reports bbe6d0845bee364e280d09aa2c40910d47b14c96.

Six sprints of blend work were sitting un-staged behind the 0.3.4 install. None of these change a successful call's output — they change which calls that used to be refused now succeed, and in one case, which calls that used to silently produce a non-manifold body now either blend correctly or refuse honestly instead.

Refusals that are now successes

  • Anti-parallel-axis circular rims. cad_fillet_edges on a circular hole/boss rim whose axis opposes the supporting plane's outward normal previously either mis-wound the result or refused outright. blendCircularRim now derives same_sense per rim edge instead of hard-coding .T., so both axis orientations blend to a watertight, correctly-wound, mirror-symmetric solid. Raised the coverage matrix's whole-rim floor 12 → 18 passes with zero anti-parallel refusals.
  • Rim seam duplication — a silent correctness bug, not a refusal (root cause). blendCircularRim emitted each seam edge of a cylindrical rim twice (once per half-face), producing a shell that passed every existing mesh-based watertightness check (the tessellator welds by position) but was topologically non-manifold — every edge used by four faces instead of two. Fixed by emitting each seam once with all moves applied. Any downstream tool that consumes STEP topology directly (rather than a tessellated mesh) — cad_body_topology, a re-import, a round-trip through another CAD system — now sees a correct shell instead of a silently broken one. faceRing family 3/13 → 16/0 in the coverage matrix; overall handled rate 72.2% → 76.1%.
  • Partitioned mixed-body blends.
  • Rim seam dedup, second pass (follow-through).
  • Setback vertex blends and blend propagate now succeed on selections that previously refused.

Refusals that now name themselves correctly (still refusals — reclassify, don't retry)

  • Six anti-parallel-axis rims that cannot be resolved by the same_sense fix above (no perpendicular cap to run out into) now refuse by name — error.message names the specific geometric gap — rather than mis-winding a band silently. This is a new, narrower refusal reason, not a widening of an old one: a caller that pattern-matched the old generic message should re-check against cad_server_info's errorCodes section.

New capability, not a refusal→success change

  • Overflow policy and variable-radius / laws-DSL blends — radius may now be a law expression (r(t)); see cad_fillet_edges' schema.
  • Conforming Delaunay volume meshing and gift-wrap cavity fallback — not blend-related, but landed in the same re-stage window; both change meshing/cavity behavior on bodies that previously refused or produced degenerate output.

What did NOT change

  • Every call that succeeded under kernel 1d207a35 still succeeds and still returns byte-identical geometry — the gate (tests/test_cadsf-377.js) pins "existing successes stay exact", and the "rate does not go backwards" floors are asserted, not just measured.
  • The MCP tool surface (names, schemas) is unchanged; this release changes what the kernel underneath the surface can do, not the surface itself.

Verification

  • tests/test_cadsf-377.js — the gate, run against the re-staged kernel: manifold check on the synthetic rim, the NIST rim, and a bounded slice of the full selection matrix.
  • tools/blend_matrix.js --file nist_ctc_01_asme1_ap242-e1.stp --gate — the full 967-case matrix; zero UNCLASSIFIED refusals.
  • packages/mcp/test/e2e/scenario-f-blend-smoke.test.js (new) — the same class of case, driven through the actual MCP tool surface (cad_import → cad_body_topology → cad_fillet_edges) rather than the kernel's internal API, so this proves the staged/shipped artifact behaves this way, not just the kernel sources.

↑ all releases

v0.3.4 “Ashkenazy”

released July 31, 2026·no downloadable artifact for this version (tarball builds began at 0.10.0)

Staged kernel 1d207a35. See staleness gate: kernel/BUILD_INFO.json stamp + build-time verification that the archive's stamp matches the run that staged it.

↑ all releases