{
  "markdown": "# cadloop\n\n<!-- mcp-name: io.github.richardofortune/cadloop-openscad -->\n<!-- mcp-name: io.github.richardofortune/cadloop-slicer -->\n\nA closed loop from parametric source to G-code, with the verification step in\nthe middle that neither the CAD tool nor the slicer can do.\n\nTwo MCP servers. A model writes OpenSCAD, compiles it, reads computed values\nback out, looks at a render, checks it fits the bed, slices it and pulls out\nthe G-code, with every plate proven against the printable area. No GUI at any\npoint.\n\nThe verify step in the middle is yours to write. cadloop makes everything\naround it cheap enough that you get to run it often; it cannot tell you\nwhether the part works.\n\n![A walkthrough of the loop, ending on the verifier catching a wheel that jams](https://raw.githubusercontent.com/richardofortune/cadloop/main/docs/walkthrough/out/cadloop-hero.gif)\n\n*Every number in that walkthrough is real output. The failing wheel is a real\nmeasurement, not an illustration. How it's built and where each number comes\nfrom is in\n[docs/walkthrough](https://github.com/richardofortune/cadloop/tree/main/docs/walkthrough).*\n\n```text\n setup_printer   once, no arguments: which printer is this for\n      |\n  write .scad\n      |\n   check          syntax and references, about a second\n      |\n   echo           read computed values without rendering\n      |\n   preview        look at it\n      |\n   verify         does it actually work  <- yours; nothing here does it for you\n      |\n   measure        bounding box and volume\n      |\n check_bed_fit    against the printer you set up\n      |\n  slice_model     -> .gcode.3mf\n      |\n extract_gcode    -> .gcode\n```\n\nThe ordering is the point. `check` and `echo` skip geometry evaluation\nentirely, so they cost a second against models where a full render takes\nminutes. Pay for the expensive steps only once the cheap ones pass.\n\nEverything from the bed check down is also one call, `make_printable`, which\nrenders, packs, slices and proves the result on the bed and reports what it\ndid in a screen of text. See [the worked example](#the-worked-example).\n\n## Install\n\n```console\npip install cadloop\n```\n\nThe only dependency is `mcp`. Or from a checkout, which is what you want if you\nintend to run the worked example or the tests — the `verify` extra is shapely,\nneeded by the example's own checker rather than by cadloop:\n\n```console\ngit clone https://github.com/richardofortune/cadloop\ncd cadloop\npip install -e \".[verify]\"\n```\n\nNeeds OpenSCAD on `PATH` or at `OPENSCAD_BIN`, and for slicing, OrcaSlicer,\nBambu Studio, ElegooSlicer or Creality Print. They share a CLI, so any of them\nworks, and all four are auto-detected along with the profiles they ship.\n\nNo slicer is preferred over another here. `setup_printer` probes each one it\nfinds and then proves the winner by slicing a 20mm cube with your profiles, so\nwhich one you get is decided by which one works on your machine rather than by\nthe order of a list in this repository. Creality Print's CLI is broken headless\non macOS today, and that is something this discovers rather than something it\nasserts, so it corrects itself when upstream ships a fix; see testing status\nbelow. `SLICER_BIN` overrides the choice.\n\n## Configure\n\nCopy `mcp.json` into your client's config and fix the paths. Both servers\nshould point at the same workspace directory, which is the only place either\none reads or writes.\n\n| Variable | Default | Meaning |\n| --- | --- | --- |\n| `OPENSCAD_BIN`, `SLICER_BIN` | auto-detected | binary paths |\n| `OPENSCAD_WORKSPACE`, `SLICER_WORKSPACE` | `~/cad` | the sandbox |\n| `SLICER_PROFILE_DIRS` | auto-detected | extra profile roots |\n| `CADLOOP_MACHINE` | `$XDG_CONFIG_HOME/cadloop/machine.json` | where the machine record lives |\n| `OPENSCAD_TIMEOUT`, `SLICER_TIMEOUT` | 300, 600 | seconds before a kill |\n\n## The servers\n\n**openscad** exposes `check`, `echo`, `render`, `measure`, `preview` and\nworkspace file access. `preview` returns the PNG as an MCP image, so the model\ncan look at what it built rather than inferring from numbers. `render` returns\nOpenSCAD's manifold report alongside a bounding box and volume, where\n`simple: yes` with a sensible volume count is the signal the mesh is printable.\n\nThe two split on what happens to the picture. `preview` hands it back and keeps\nnothing; `render` writes it to the workspace when the output ends in `.png`,\ntaking the same camera, `imgsize`, `projection` and `full_render` options. Use\n`render` when the image is the artefact rather than the answer. Since `render`\nalso takes `source` as text rather than a path, the before half of a comparison\nis `git show <rev>:model.scad` piped straight in — no checkout, no temp file.\n\n**slicer** exposes `setup_printer`, `machine_info`, `make_printable`,\n`slicer_info`, `list_profiles`, `check_bed_fit`, `slice_model`,\n`slice_summary` and `extract_gcode`.\n\n`make_printable` is the whole chain in one call. Give it a `.scad` and the\nparts you want out of it, and it renders each one, measures it, packs what\nfits onto as few plates as it can, slices them, and then reads the finished\nG-code back to prove every extruding move lands on the bed. It never edits\nthe model: a part that cannot print as designed is reported, not quietly\nshrunk or split. The one thing it changes is which plate a part lands on, and\nturning a part ninety degrees when it will not fit square, which it names in\nthe report. It answers with every fact it established and, in `summary`, the\none screen of text those facts add up to, ending in what to do next.\n\nCall `setup_printer()` once, with no arguments. It reads what your slicer is\nalready configured with, resolves that to a machine, process and filament\nprofile from one install, proves the combination by slicing a 20mm cube, and\nremembers it. It reports the printer, the quality and the filament it settled\non, and stores nothing at all if the test slice fails. Pass `printer`,\n`filament` or `process` only to override one of those fields; the rest still\ncome from your slicer's own settings. A profile you name is either used or\nrefused by name, never quietly swapped for a different one.\n\nAfter that, `check_bed_fit` and `slice_model` need no profile arguments, which\nis the point: a caller cannot supply three profiles that disagree if it\nsupplies none. Explicit arguments still win where you pass them.\n\n`machine_info` says which printer this workspace is set up for and whether it\nis still current. The record is a cache, not a source of truth. If the slicer\nmoves, a profile is edited, or the slicer's version changes, every tool that\nwould have used it refuses with the reason instead - `ok: null`, nothing\nwritten - until you run `setup_printer` again. A setup that no longer matches\nreality is never used to produce G-code.\n\nCall `slicer_info` before doing anything unusual: the Orca-family CLI is\nundocumented, changes between releases, and Creality's fork diverges, so the\nflag list it reads off your install is more trustworthy than anything assumed.\n`slice_model` has an `extra_args` escape hatch and a `dry_run` mode.\n\n`check_bed_fit` is where the two meet. The slicer will emit out-of-bounds\nG-code without complaining, so this measures the STL, reads `printable_area`\nout of the machine profile, and compares. It checks the 45 degree diagonal too,\nsince a part that misses square-on often fits rotated.\n\nReading `printable_area` back out is fiddlier than it looks. Stock profiles\nwrite it both as a list of `\"XxY\"` strings and as one comma-separated string,\nand most Bambu machines carry no bed of their own at all, inheriting it through\n`inherits` from a common base. So the lookup parses both forms and walks the\ninheritance chain. Of the 473 concrete machine profiles shipped with Creality\nPrint 7.1.1, 469 resolve; the remaining four define no bed anywhere in their\nchain, and those report `ok: null` with a reason rather than guessing.\n\n## The verification step\n\nThis is the part cadloop does not do for you, and the reason it is a worked\nexample rather than a feature. `models/verify_spirograph.py` ships beside the\nmodel, not inside the package: every project's version of \"does it actually\nwork\" is different, and none of them generalise. Yours will look nothing like\nthis one.\n\nIt runs two checks. For the spirograph the first\nlays each wheel's pitch curve onto the ring's pitch circle, walks a full\ncircuit, and measures overlap between the two solids at every position. Zero\noverlap across the whole circuit at some meshing phase is the pass condition.\nThe second checks the parts are not laid on top of each other on the sheet.\n\n```console\n$ python models/verify_spirograph.py\npart     teeth  overlap mm2  result\n24T         24     0.000000  pass\n...\ntrefoil     23     0.000000  pass\n\n14/14 parts mesh cleanly\n\ngroup        volume mm3\nring            23939.8\nouter_ring      25652.8\nwheels         197113.5\nshapes          10078.7\nsum            256784.8\nsheet          256784.8\n\nno parts overlap on the sheet\n```\n\nThe second check is the same idea one level up. A sheet that lays two parts\non top of each other still renders as a clean manifold, still fits the bed,\nand still slices without a word; it just prints as one fused object. So it\nrenders the sheet and each of its groups and compares the union against the\nsum, which is the only place the collision shows up. It needs OpenSCAD and\nskips without one; `--skip-layout` and `--skip-mesh` run one half alone.\n\n## What the tooth counts decide\n\nA pen in a wheel of `r` teeth rolling in a ring of `R` traces a figure with\n`R / gcd(R, r)` lobes, closing after `r / gcd(R, r)` circuits. The pattern is\nsettled by the tooth counts before any geometry exists, so the checker's\n`--patterns` mode reads it straight off the set:\n\n```console\n$ python models/verify_spirograph.py --patterns\n\nmain ring, 96 teeth\n      part  teeth  lobes  circuits\n       32T     32      3         1  plain\n       24T     24      4         1  plain\n       72T     72      4         3  plain\n...\n   trefoil     23     96        23\n```\n\n96 is 2^5 x 3, so it shares factors with most of an even wheel set and a good\nhalf of these wheels draw eight lobes or fewer. There is no 48 in the set for\nthis reason: 48 is exactly half of 96, the degenerate ratio whose pen traces an\nellipse and nothing else, so it earned no slot. The outer ring, 105 = 3 x 5 x 7,\nbehaves far better, and the trefoil at 23 teeth is coprime to both rings, which\nis what makes it the richest wheel in the set.\n\nThis is worth running before choosing tooth counts rather than after printing\nthem.\n\nThis is model-specific by nature, which is the honest lesson. A manifold mesh\nthat fits the bed and slices cleanly can still be a part that does not work.\nWhatever your equivalent of \"does it actually roll\" is, it belongs in the loop\nbetween preview and measure, and you have to write it yourself.\n\n## The worked example\n\n`models/spirograph.scad` is the model the loop was built around. A 96 tooth\ninternal ring in a flanged body, an outer ring, eleven circular wheels and\nthree non-circular ones (ellipse, egg, trefoil), all involute geared at module\n1.5 with pen holes on a golden-angle spiral.\n\nThe non-circular wheels are why the verifier exists. A first attempt used\ncapsule and teardrop outlines built from tangent lines; those looked right,\nrendered as clean manifolds, and would have sliced without complaint, but the\nrolling check showed them ploughing 30 to 80 mm² into the ring teeth. A flat\nsection of pitch curve touches the ring at one point and stands proud of it\neverywhere else. Nothing downstream of CAD would have caught that. The shapes\nthat shipped are smooth convex curves whose radius of curvature stays inside\nthe ring's everywhere.\n\nTaking the whole set from source to plates is two calls, and the first one is\nonly needed once:\n\n```python\nsetup_printer()\nmake_printable(\"spirograph.scad\",\n               [\"ring\", \"outer_ring\", 24, 30, 32, 36, 40, 45, 52, 56, 63, 72,\n                80, \"ellipse\", \"egg\", \"trefoil\"])\n```\n\nOn an Ender-3 V3 SE with OrcaSlicer 2.4.2 and generic PLA, that is sixteen\nrenders, five plates, five slices and five G-code files read back and checked\nagainst the bed, in about half a minute:\n\n```text\nok      16 parts on 5 plates, every plate proven on the bed\n\n  machine ...... Creality Ender-3 V3 SE 0.4 nozzle, 220 x 220 mm bed, PLA\n  parts ........ 16 of 16 fit this bed\n  plates ....... 16 parts packed onto 5\n  sliced ....... 5 of 5, every extruding move on the bed\n  ready ........ ~/cad/plates/spirograph, 13h09m, 63.17 m PLA\n\n  worth a look:\n    plate_2 sits 2.2 mm from the bed edge, consider a brim\n\n  next: print the plates in ~/cad/plates/spirograph. Nothing above stops the\n        print — the notes are advisories — so read them and go.\n```\n\nEvery number there is from that run. The brim note is an advisory and not a\ndefect: plate 2 is on the bed with 2.2 mm to spare, which is close enough to\nthe edge that a warped first layer would show. Anything the run could not\nprove says so in its own words — `ok` for what came out, `FAILED` for\nsomething attempted that did not, `UNKNOWN` for anything it could not tell —\nand the last line is always the call to make next.\n\n```console\nmake verify              # rolling interference and layout, all 14 parts\nmake render PART=ring    # one part to an .stl, no printer set up needed\nmake smoke               # both servers, end to end\n```\n\n`make render` is the one step of the old walkthrough worth keeping by hand:\nit wants nothing but OpenSCAD, so it is the quickest way to look at a single\npart while you are still changing the model. Everything after it is\n`make_printable`.\n\n## Testing status\n\n`make smoke` drives both servers over real MCP stdio sessions and asserts on\nforty-five behaviours: tool surface, defines reaching the script, a\ndeliberate syntax error being caught, measured geometry matching known\ndimensions, an image coming back from preview, profile classification,\nargument ordering, archive parsing, G-code extraction, bed fit passing and\nfailing, both `printable_area` spellings and an inherited bed, and the\nworkspace guard rejecting traversal.\n\nIt also drives the zero-argument path end to end against a slicer install\nbuilt for that run alone: `setup_printer()` with no arguments has to name the\nprinter, quality and filament it chose and store a triple from one install,\nand `check_bed_fit` and `slice_model` then have to work with no profile\narguments at all. Editing a profile afterwards has to make both of them refuse\nand write nothing.\n\n`make_printable` is driven the same way, over the same session: a two-part\n`.scad` has to come back as at least one plate, with every plate proven on the\nbed by reading its G-code rather than by assumption, and a report of thirty\nlines or fewer that names the next step. That is the only automated end-to-end\ncheck of the one-call path.\n\nThe OpenSCAD half runs against a real OpenSCAD and skips if none is installed.\nThe slicer half runs against a mock binary that emits an Orca-shaped help text\nand a representative `.gcode.3mf`, so everything except the real slicer's own\nbehaviour is covered.\n\nAgainst a real install, the OpenSCAD half and `check_bed_fit` are confirmed end\nto end: a 24 tooth wheel rendered out of `spirograph.scad` measures 38.99 mm and\npasses the K1's 220 mm bed. Every flag `slice_model` builds is present in\nCreality Print 7.1.1's `--help`.\n\nThe loop is confirmed end to end against OrcaSlicer 2.4.2 on macOS: the 96\ntooth ring slices to 113,808 lines of G-code over 35 layers, a 51 minute print\nusing 7.27 m of PLA, with every coordinate inside the bed.\n\n**Creality Print's CLI does not work headless on macOS.** On 7.1.1.4472 under\nmacOS 26, every operation except `--help` segfaults, including `--info` with no\nprofiles loaded at all. It dies in `Slic3r::GUI::PartPlate::set_shape` called\nfrom `Slic3r::CLI::run`, a null dereference in GUI bed setup that the headless\npath never initialises, and no combination of `--datadir`, `--outputdir` or\nprofile arguments avoids it. Point `SLICER_BIN` at OrcaSlicer, Bambu Studio or\nElegooSlicer instead, or run Creality Print's CLI on Linux. If you do pass\n`--datadir`, it must be writable: an unwritable one aborts in `set_data_dir`\nrather than segfaulting, which is a different failure with the same outcome.\n\nThe workspace guard covers tool arguments, not the scripts themselves.\nOpenSCAD's `include`, `use` and `import` can reach any file the process can\nread, so running an untrusted `.scad` is running untrusted code.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n\nNot affiliated with Creality, Bambu Lab, Elegoo, the OrcaSlicer project or the\nOpenSCAD project. Those names appear here only to describe what this drives.\n",
  "bytes": 16735,
  "sha": "577b0d096efcf17119349d0719c637b32ed27195d614e2d914a5678bc3ad8f4a",
  "repo_slug": "richardofortune/cadloop",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_richardofortune_cadloop_slicer_a64c1ac9/readme"
}