A parametric solid modeller whose model is a JSON tree, whose files are records in your own repo, and whose first-class user is a machine.
Everything on this page is built and live unless it carries a not yet tag. Numbers are the ones the tests assert. The design record with the reasoning is docs/CAD.md; this page is what it became.
No files: MCP
Check, build, measure, what the mechanism does, export, read anyone's files. From any agent that speaks the protocol.
claude mcp add --transport http cad https://cad.mino.mobi/mcpFour megabytes: the mirror
The whole package, node only. The skill loads on clone; the full loop including interference and render.
git clone https://tangled.org/morphyxmino.bsky.social/cadA browser: the viewer
The judgement surface. Parts, assemblies, the report, named faces, measure, interference, files, sign-in.
cad.mino.mobi · SKILL.md · llms.txt · CHANGELOG.md1. The system specthe one decision, and what follows
The model is the feature tree. Geometry is a cache. A part is a small, typed JSON document: parameters, sketches on planes, operations over them. The engine turns it into solids; the solids are never the source of truth. Because the tree is canonical it fits in a record, it diffs, an agent can write it, and every build is a pure function of it, so the kernel behind that function is replaceable.
{
"$schema": "com.minomobi.cad.tree#v1",
"units": "mm",
"params": { "R": 20, "t": 1.5, "r_pivot": 0.16, "r_pivots": 12, "n_pivots": 5, "r_centre": 0.5 },
"features": [
{ "op": "sketch", "id": "outline", "loops": [
{ "name": "rim", "circle": { "c": [0, 0], "r": "R" } },
{ "name": "centre", "circle": { "c": [0, 0], "r": "r_centre" } } ] },
{ "op": "sketch", "id": "pivot", "loops": [ { "circle": { "c": ["r_pivots", 0], "r": "r_pivot" } } ] },
{ "op": "pattern", "id": "pivots", "of": "pivot", "kind": "circular", "count": "n_pivots", "name": "pivot" },
{ "op": "extrude", "id": "plate", "profile": ["outline", "pivots"], "depth": "t" }
]
}
What is load-bearing
- Parameters are expressions. Any numeric field takes a number or an expression over
params("r*0.6",sin,cos,deg(),sqrt,min,max,pi). Changetand the tree follows. A numeric string is an expression too, which is what lets a record carry no floats (§4). - Regions are even-odd. A loop inside a loop is a hole. A pattern of a sketch is a sketch. The profile of an extrude is a list of sketches. This is how holes, windows and spokes are made, and it is why most bench parts never ask a kernel for a boolean.
- Topology is named, never indexed. An extrude
plateyieldsplate.start,plate.end,plate.side[k]; a named loop yieldsplate.rim[0..3],plate.pivot[2][1]; a geargyieldsg.tooth[i].flank.r.0,g.tip,g.bore[k]. A selector that no longer matches after an edit is a typed error naming the selector, not a silent wrong face. - Every named face carries its geometry. A plane (normal, point) or a cylinder (axis, centre, radius), computed from the sketch curve that swept it. A circle is four exact arcs, so a bore is a real cylinder with a diameter; the mesh only ever approximates it.
- Invariants are the test. Every build reports volume, area, bounding box, centroid, Euler characteristic, watertightness, open and flipped edge counts. Tests compare these against closed forms with tolerances, never mesh bits.
Operations
| op | does | names |
|---|---|---|
sketch | closed loops on a plane (XY/XZ/YZ, offset, or <extrude>.end): circle, rect, polygon, a path of lines, arcs (via), cubic Béziers (ctrl) and spline segments through points, or a closed spline loop (Catmull–Rom, one exact cubic per span) | loop name → id.name[k] |
pattern | circular or linear copies of a sketch, giving a sketch | inherits |
extrude | a profile (one sketch or a list) by depth; mode new / add / cut / intersect | start, end, side[k] |
revolve | a profile about an axis in sketch coordinates; profiles may touch the axis | side[k], caps |
gear | the exact involute from m, z, alpha, b, bore: three Bézier pieces per flank, chord-length parametrised | tooth[i].flank.l/r, tip, root[i], bore[k] |
boolean | union / cut / intersect of bodies (prefer regions) | kept from operands |
fillet chamfer shell | edge and face ops; unsupported in Truck, done by OCCT (§3) | reference faces cross the seam |
Assemblies
A document with components is an assembly. Each names a part: an inline tree, bench:<name>, or the AT URI of a published part (its head, or a revision to pin a version), with optional params overrides that make a distinct build, a placement (at, rotate) and a phase; or it holds a nested assembly, flattened with prefixed ids (stage2/arbor). mates are gear (za, zb) and fixed; drive is {component, rpm} or an escapement. Angles are a kinematic chain from the driven component, gear phases are set automatically, and it is not a constraint solver: placements are yours to get right and the interference check tells you when you have not.
Placements are expressions. A document may carry params (the tree's own language, any order) and derived, a second map resolved in dependency order at each instant (any order — a PDS hands keys back sorted) with two reserved variables, t in seconds and theta, the driven component's angle in degrees. Every at element, rotate.deg, rotate.axis element and the drive's numbers take a number or an expression over them; component parameter overrides are bound in the assembly's scope when they can be. That is how a lead screw moves its nut and a crank its slider without a mate for either: the pose math lives in the document, check.mjs --t and spin sweep the real motion, and the interference check stays the safety net. The evaluator is lib/expr.js, a mirror of the engine's, held to the bit by a test. bench/crank.json is the worked example.
Mates carry travel as well as turning. gear and belt turn the other member by a ratio (opposite and same sense), fixed carries turn and travel, screw moves a nut by lead per turn along its axis, rack moves a rack by r·θ, slider passes travel on by a ratio; each works in either direction from the drive. A component's pose is its placement, then its travel in its own frame, then its turn about its own z. Repeat (repeat: 4, with i in scope) makes four bolts one component, and place by feature (at: "@platform.pivot[i]", rotate.align) puts each on a named face of another component — a bore's centre and axis from the exact kernel's geometry — and follows that component through its motion. bench/lift.json is the lead-screw lift that proves the three together; check.mjs --sweep checks a whole cycle.
Drawings, from the same mesh. lib/drawing.js writes an engineering drawing as SVG with no kernel and no browser: third-angle views, hidden lines found by casting a ray from each piece of each edge toward the viewer through a BVH of every body, the overall dimensions, and every cylindrical hole called out by count, diameter and depth when blind — a bore's four exact arcs grouped back into one hole by their shared axis, a surface that faces away from its axis read as a boss and not called out. The page's drawing button, agent/drawing.mjs and the MCP drawing tool all produce the same bytes, so two drawings of one tree diff cleanly. A part sheet is dimensioned inside as well: the engine names each face after the sketch loop it came from, so the features come back grouped, and every hole centre and pocket edge gets an ordinate from a datum at the part's corner, with an evenly spaced run of holes collapsed into one pitch line and each named loop called out by name and size.
An assembly is a document, not a picture. lib/report.js writes one self-contained HTML page for an assembly: the three views, an exploded isometric with a numbered balloon on every item, a parts list with quantities and volumes, a drawing of each distinct part, a Motion section (a rate curve per input with the dead points marked, and a table of rate, mechanical advantage, travel and turn), and the assembly steps — with a link into the viewer on every row, so whoever receives it can open the live thing. The steps are derived: a placement, a @component.face reference, a mate and its numbers, a declared fit. Nothing is inferred, and a test asserts no step contains a sentence the document does not state.
More than one input, and a grid rather than a period. A drive is the input that runs with time, and for a clock that is the whole story. A gripper that grips and rolls has two independent inputs, and its interference question is two-dimensional: does anything touch anywhere in grip × roll? So a document declares inputs — named axes of its own motion, each with a range, in scope by name in every expression — and revolute and prismatic joints consume one (two jaws opening from one input are two prismatic joints, one with scale: -1). --grid enumerates every combination and reports the state where each pair came closest, because a path through that space is not a proof: it visits a curve and says nothing about the corners it misses, and the corners are exactly where a jaw at full stroke meets a post at one angle of the wrist. A component placed on another's face with rigid: true takes that component's whole pose, so it can ride it and move relative to it — which is what makes the joints enough, and placement expressions over the drive unnecessary.
A fixed mate means bolted, not "may be the same solid". A touch the mates imply is expected up to a budget — a cubic millimetre of shared volume (or a thousandth of the smaller part) and a tenth of a millimetre of depth — and past it the pair is a collision like any other; a real press fit declares its own with interfere: {max, depth}. A document's fits pair by index rather than by cross product ([*] on both sides means the same index, over: {k: n} walks one), a fit naming a component that does not exist is an error rather than silence, and a sub-assembly's fits reach the top like its mates. A component placed by an expression over the drive that also carries a mate is driven twice, and the check refuses it.
What it DOES: rates, mechanical advantage, effort, dead points. Interference says nothing touches; it cannot say what the thing is for. lib/mechanism.js differentiates the poser against each input — two poses per number, no geometry, no kernel — and every velocity ratio in the assembly falls out; the mechanical advantage is the reciprocal of a ratio, by virtual work, and the effort at an input that holds a load is Σ F·∂p/∂q, in newtons for an input in millimetres, newton-metres for one in degrees, watts for a drive. A rate passing through zero is a dead point: self-locking, infinite advantage, and a toggle seen from the other side. The distance between two components and its rate turn an author's hand-written oracle into one number — “the link is a link” and “rolling does not change the grip” are both d(distance)/d(input) = 0. It is lossless: friction, preload and backlash are not modelled, so an effort is a floor and not the answer; and it asks only about degrees of freedom the document already has, so it is not a constraint solver, not contact statics not built and not FEA not built. agent/mechanism.mjs, the MCP mechanism tool and the report's Motion section are the same instrument.
Clearance, not only collision. lib/proximity.js measures every pair's nearest approach from the exact meshes with no kernel at all — a BVH, exact triangle distances, crossings, containment, a penetration depth, contact told from collision — and lib/sweep.js chases each pair's minimum between the sweep's samples, so a graze between two instants is found. check.mjs --clearance 1 prints the table a reviewer reads first with a verdict per pair — collision, contact, expected, fit, close, loose, clear; a document's fits declare the clearances it intends so a running fit is judged on its own numbers; because it needs no kernel, the MCP interference tool runs it on this server. measure takes an assembly and two component.face names at a time t. A sweep on the server is windowed against a budget counted in work rather than time — a Worker freezes its clock during synchronous execution, so wall-clock budgeting cannot work — and it answers with the instants it managed, the one to resume from, and whether parts are still to build; exact meshes are cached between calls. An assembly whose single instant is past the budget is refused with its numbers and the suggestion to coarsen res or run locally, rather than killed mid-request.
Clearance, continued. Refinement only runs for pairs within four times the clearance being demanded, so a sweep pays for the pairs that could graze and not for the ones ten millimetres apart. A component marked reference is drawn translucent and left out of checks and export.
That is the front plate of the clock, minus its three pillar holes: a disc with a centre hole and five pivot holes on a circle, no booleans, and it builds in a hundred milliseconds with 42 named faces. The reference assembly is a mechanical clock: a three-stage going train, a lever escapement as the drive, motion works, hands, dial and case, eighteen components from nine parts with parameter overrides. It ticks: sixty beats turn the escape wheel two revolutions and the minute hand one minute, to the fourth decimal.
2. The pipelinewhat happens between an edit and a picture
/mcp the engine runs alone (§5).The engine is a Rust crate compiled to wasm32-unknown-unknown with a raw C ABI, no wasm-bindgen: cad_alloc, cad_resolve, cad_build, cad_out_ptr, cad_out_len, cad_free_all, and one host import for the clock. The same bytes run in the browser, under node, and inside a Cloudflare Worker. The compiled cad.wasm is committed; engine/build.sh rebuilds it and runs the selftests.
3. Kernel jugglingthe bake-off, and the rules it produced
No open kernel that runs in wasm does everything. Rather than marry one, the tree is built behind a seam and a bake-off measured four candidates on the clock parts, with STEP round-trip as a mandatory column. The rules below are what it decided; the table is what each kernel is for.
| kernel | role | good at | cannot | where |
|---|---|---|---|---|
| Manifold 3.5 | preview, interference, export fallback | mesh booleans in milliseconds; always builds; watertight by construction | no exact geometry, no face names, polygons | page worker · node not the /mcp worker |
| Truck 0.6 (Rust) | exact, first | sweeps, revolves, splines as exact Béziers, named faces with geometry, STEP out; the plate in 100 ms | many booleans (boolean union failed), no fillets; the 60-tooth crossed gear takes ~17 s | everywhere: page, node, /mcp |
| OCCT 7.4 (opencascade.js) | exact, on demand | fillets, chamfers, shells, every boolean; the escape wheel in 1.6 s cold; STEP the world trusts | 66 MB, so not shipped: imported from unpkg after one press of exact with OCCT; not in the node scripts yet | page worker · bake-off harness |
| implicit SDF spike | simulation representation | uniform fields for a future stress path | not a modelling kernel | engine, behind a flag |
The rules
- Preview then exact. Manifold answers first so the part appears; Truck's exact build lands behind it with every face named. The report shows both side by side.
- Truck's gear is not deterministic. The first thing the corpus audit found: the 60-tooth gear's exact mesh, which Truck cannot close, comes out with a different Euler number and triangle count run to run. Its B-rep face count and its volume to a part in a thousand are stable, and those are what the audit holds a non-watertight part to.
- Regions over booleans. A part written as even-odd regions never asks Truck for the union it cannot do. Four clock parts were rewritten this way after the bake-off.
- A boolean keeps the names. It destroys every face index — the kernel hands back a fresh shell in its own order — but not the surfaces. Every op registers the geometry behind each name it gives, and afterwards each face is matched back to it against a sample of the face's own points: a surviving face keeps its feature's name, a face the tool made carries the tool's own loop name and geometry, and anything unmatched is
<op>.face[k]. Without this a body with one cut in it had no named faces at all, so it could be neither a placement target nor an argument to measure — which forces placement by expression, and an expression-placed component that also carries a mate travels twice. - A panic is not a trap. Wasm cannot unwind, so a kernel assert reached the host as a bare
unreachableand left the instance dead for the rest of the session. The engine hands the message over before it aborts; the host catches the trap, starts a fresh instance, and reports it as an ordinary error that OCCT can usually do. - The tool sizes itself. A cut with
through: truetakes the body's own extent along the sweep, andfrom/toare both measured along the sketch plane's normal — the overhang and the sign convention were both hand-tuned numbers, and the overhang is what made watertightness look like a coin flip. - Fall through to OCCT. A fillet, chamfer or shell, or a boolean Truck fails, is flagged unsupported in the report and goes to OCCT once the user has allowed the download. Fillet selectors cross the seam through the engine's reference faces: the tree minus its fillet ops is built by Truck, and the named face's centroid and normal pick the OCCT face whose edges get rounded.
- Honesty in the report. A Truck failure says so by op id. The preview is labelled preview. Volumes come from the mesh the kernel produced; diameters come from geometry, never from the mesh.
- STEP from the kernel that built it. The step button re-runs the exact kernel with its writer on, so the file is the B-rep the report judged.
4. The data modela file tree over records, on the designer's own repo
A person's parts live in their ATProto repo as records. Two collections make a filesystem with history, the way a git tree is a view over blobs:
| record | holds | mutability |
|---|---|---|
com.minomobi.cad.part | path, name, kind (part or assembly), head: a strongRef to a revision, createdAt, updatedAt | a head: it moves |
com.minomobi.cad.revision | tree, parents[] as strongRefs, createdAt, message, the kernel that built it and the invariants it was judged by, forkedFrom | immutable |
Rules learned from the first real write
- No floats. The ATProto data model has none; the PDS refused
m: 0.5at the door. The drive writes every non-integer as its shortest decimal string, which is still a valid tree because a numeric string is an expression, and hands numbers back on read. The round trip is exact and the selftest counts zero floats in stored records. - Local cids are not CIDs. The local drive (IndexedDB in the browser, a JSON file under node) mints
local:<sha256>, an honest hash a PDS would reject. So push is a replay: a local file's revisions are recreated on the PDS oldest-first, refs rewritten to the cids the PDS just minted, revisions already in a real repo referenced rather than copied. - Trees are inline. Bench trees are 1 to 4 KB; the clock is 6 KB. Geometry is never stored; it is rebuilt from the tree.
One drive, four backends
lib/drive.js is one class over a five-call contract: getRecord, listRecords, createRecord, putRecord, deleteRecord, the ATProto repo methods and nothing else. list, tree, get, put, rename, history, fork, push and remove are the same over all of them.
| backend | where | who |
|---|---|---|
| memory | a Map with a persist hook; the node CLI keeps a whole repo in ~/.cad-drive.json | tests, agents |
| local | IndexedDB, did:local; survives reload; no sign-in | every visitor |
| public | any repo over the two public XRPC reads; in the page through this site's gateway (§5) | anyone, no sign-in |
| auth | the signed-in user's repo through auth.mino.mobi, scope atproto repo:com.minomobi.cad.part repo:com.minomobi.cad.revision | the designer |
| session | an app-password session, for terminals and scripts; never a browser | a local agent, the publisher |
Addresses
https://cad.mino.mobi/?at=at://did/com.minomobi.cad.part/rkeyopens a file from any repo — a head, so it always resolves to that file's newest revision, and an open page keeps itself on that revision without a reload (below).https://cad.mino.mobi/?at=at://did/com.minomobi.cad.revision/rkeyopens one revision, pinned for ever — what aparts.mino.mobipost points at.https://cad.mino.mobi/#t=<base64url JSON>carries an arbitrary tree in the link.https://cad.mino.mobi/xrpc/com.atproto.repo.listRecords?repo=minomobi.com&collection=com.minomobi.cad.partlists a repo's files, by handle or DID.- The published bench lives in the
minomobi.comrepo:parts/<name>,train,clock; the assemblies reference their parts by AT URI.
5. The backend, such as it existsdeliberately small
There is no database and no server that knows about your parts. The site is a static package served by one Cloudflare Worker, and the Worker does three things:
- Assets. The package directory is the site: the page, the ES modules,
cad.wasm, Manifold, the bench, the skill, the README, this page. Headers, including the two content security policies, come from_headers. The page's policy is strict; the build worker's script carries its own policy with the one grant Manifold's glue needs. /xrpc/, a read gateway. The two public read methods,com.minomobi.cad.*only, with handles and DIDs resolved server-side and forwarded to the repo's own PDS; and the two public actor methods,searchActorsTypeahead(every handle field suggests accounts as you type) andgetProfile, forwarded to the public API. It exists so the page's policy staysconnect-src 'self'and the page never learns a PDS host. It is a CAD gateway, not a proxy; nothing it returns was private./mcp, the tool surface. The engine wasm imported as a module and instantiated once per isolate; JSON-RPC in, the same numbers the page reports out (§7).
What is around it
| piece | status | what it is |
|---|---|---|
| auth.mino.mobi | live | the shared OAuth worker for every mino.mobi site (PKCE, DPoP, PAR, private_key_jwt). It holds the tokens and proxies writes, so the page never holds a PDS token. The two cad collections are in its scope ceiling; sign-in asks for exactly those. |
| the service account | live | the identity that owns minomobi.com. A workflow publishes the bench into it on every change, idempotently: an unchanged tree is skipped, a changed one becomes exactly one revision. |
| the mirror | live | the package force-pushed to tangled on every push, minus the Rust build directory and the deployment files, with the skill in place. Four megabytes; runs its selftests before it pushes. |
| a geometry cache | not yet | the design record's content-addressed cache of built solids by tree hash. Everything rebuilds from the tree today, which is fast enough at these sizes and is the honest place to start. |
| /parts/ | live | the social layer, a second worker mounted here through a service binding (the zone is at Cloudflare's hundred-custom-domain ceiling, so it has no host of its own): communities, posts, comments and votes as records in their authors' repos, indexed by one Durable Object that rebuilds from the network (self-reports plus Constellation backlinks, no firehose). A post points at a revision, so what people voted on cannot change. |
| content-negotiated faces | not yet | /at/<did>/<rkey>.stl, .png: the part as a URL you can paste into a post or a purchase order. The gateway is the first step. |
A push to this package's branch deploys it; there is no staging. The deploy runs the node selftests first and verifies the live host answers afterwards, and the surface's own notes say green is not proof: the log must bind the custom domain.
6. The headless testswhat green means here
Every claim above is gated by a test that runs under node or in headless Chromium, and the deploy runs the node ones before it ships. The tests assert invariants with tolerances against closed forms, never pixels or mesh bits.
| test | runs | asserts |
|---|---|---|
cad.selftest.mjs | the committed wasm, from bytes, under node | the ABI; the gear's volume against its profile area; the plate against its closed form; Euler characteristics; watertightness; that names come out. 21 checks. |
assembly.selftest.mjs | the engine and the committed wasm | nineteen expressions and ten error messages evaluated by lib/expr.js and by the engine agree to the bit; the crank–slider's rod and block against the closed form at six instants; sub-assembly scopes; theta from an escapement; static documents unchanged; the six mates in both directions; the lift's nut, platform and bolts by repeat and reference, a bolt on a tilted plate aligned to its bore, a bolt on a turning plate orbiting; errors that name the component and the field; proximity on cube pairs and the lift's meshes, and a sweep that finds a 1 mm graze between eight samples. |
mechanism.selftest.mjs | the poser alone, no kernel | the virtual-work instrument against numbers a person can derive on paper: a prismatic joint moving its follower one for one and nothing else; a jaw at radius 6 and 18 moving exactly r·π/180 per degree; a span rate of zero as an invariant through the roll; 100 N at a jaw 18 mm out needing 1.8 N·m at the wrist, and 50 N on each jaw 100 N at the input; a crank–slider's block against the derivative of r cosθ + √(L² − r²sin²θ) at three instants to 2e-5, and its two dead points at top and bottom centre; travel integrated over a whole input; and the clock's escape wheel stepping two of thirty teeth a period while the minute hand advances twelve times the hour hand — read from two end poses, because uniform sampling cannot integrate an escapement. 16 checks, 18 components over 24 states in 15 ms. |
drawing.selftest.mjs | the committed wasm, under node | the plate's nine holes recovered from their thirty-six arc faces and called out as 3× ⌀1.2, 5× ⌀0.32, ⌀1; the case's interior read as one blind bore ⌀42 ↧7 while its rim is not a hole; the overall dimensions written on the sheet; hidden lines where the bores are seen through the plate; views on demand and an unknown view refused; the lift posed at t = 0.5 s as seven bodies on one sheet; and the same input drawing the same SVG twice. |
report.selftest.mjs | the committed wasm, under node | the lift's parts list and quantities; four steps with the repeat collapsed into one; the nut's step carrying the screw mate's own lead and the declared 0.05–0.15 mm fit; no step containing a word the document does not state; a gear mate stating its ratio in either direction; the exploded view turning seven touching pairs into none; and one document giving one page, byte for byte. |
agent/audit.mjs | a published repo, on every publish | every part rebuilt and diffed against the invariants its revision recorded — volume, χ, watertightness, face count — and Truck against Manifold on volume: the whole corpus as the kernels' regression suite. |
drive.selftest.mjs | two in-memory repos and a fake PDS | put, history, rename, fork with lineage across repos, push with cid rewriting, no float survives in a stored record, the gateway resolves a handle and refuses other collections. |
mcp.selftest.mjs | the tool surface with kernels injected | the transport (initialize, notifications, batches, error codes), every tool against the bench, the sliced assembly build, and the reduced tool list a host without Manifold advertises. |
browser.selftest.mjs | the page in headless Chromium, served under the production headers | first with no script injection at all, that the worker comes up and the exact build lands under the real policy; then every bench part, the plate against its closed form, picking a named face at the centre of the view, measure, a parameter edit growing the volume, the clock's ratios and escapement to the fourth decimal, interference clean mid-beat, the crank–slider's block where the closed form puts it while spinning, touch gestures, the phone layout with an on-screen keyboard taking half the screen, the interference check reading a mated touch past its budget as a collision, a gripper's two inputs each getting their own control and the jaws riding the rotor as it rolls, the header picker and the document panel naming what is loaded, a revision landing in the repo reaching the open assembly by itself (and an edited one being offered the update instead), OCCT from a locally served copy, save and reload through IndexedDB, a stranger's file opened through a mocked gateway and forked, an assembly whose parts are AT URIs, STEP and STL export. |
preflight | the monorepo's own gate | the registry, the generated files, the synced copies (the skill, the auth client), catalogue coverage, the loop's blast radius, and the selftests of changed directories. |
The verification habit
After a deploy the live host is curled: the wasm's content type, the gateway with a real handle, a real PAR from this origin against the auth worker with the cad scope, a build through /mcp. The bench is the golden set; an agent working on its own part is told to write its own expected.json and rerun it after every edit.
watertight and χ say, not what the picture looks like. That sentence is in the skill, in the tests, and in the report's layout.7. Agentic accessthree doors, one loop
The loop is the same everywhere: write tree → build → measure → check → render → judge → edit. What differs is how much you have to install.
Door one: nothing. MCP at /mcp
Streamable HTTP, JSON responses; GET returns the descriptor. Tools on the live host: check, build, measure, mechanism (rates, mechanical advantage, effort, dead points — no geometry, so it is free here), interference (clearance mode: nearest approach per pair, no volumes), drawing (an SVG drawing as an embedded resource), report (a whole assembly report as one HTML page), step, list_files, get_file. Every tool takes a tree object, bench:<name> or an at:// URI. build returns the invariants, every named face with its geometry, and a viewer link to hand a human; on an assembly it builds three parts per call and returns partKeys and remaining, so you pass parts to continue.
curl -sX POST https://cad.mino.mobi/mcp -H 'content-type: application/json' -d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"measure","arguments":{"tree":"bench:plate","a":"plate.rim[0]","b":"plate.pivot[2][0]"}}}'
→ {"kind":"cylinder-cylinder","parallel":true,"distance":12,"diameters":[40,0.32], …}
What is not on the server, and why: no render (a browser's job; the link is the picture), no preview kernel and so no shared volumes (Manifold's glue generates code from strings, which Workers forbid; interference runs there in clearance mode from the exact meshes instead), no write (a person's parts are saved with the person's own sign-in), no OCCT. The descriptor says all of this so an agent does not have to discover it.
Door two: four megabytes. The mirror and the skill
Clone the mirror; Claude Code loads .claude/skills/cad/SKILL.md on open. Node 22 is the only requirement; the engine is the committed wasm. The full loop: agent/build.mjs, measure.mjs, check.mjs, export.mjs, render.mjs (headless Chromium, one install), and drive.mjs for files, including --login with an app password to write to your own repo and hand back a ?at= link.
Door three: a browser
An open page does not go stale. A document on screen is a photograph of records — its own part head, and the head of every part it references by AT URI. The page re-reads them every twenty seconds and whenever the tab comes back; a document nobody has edited on screen is reloaded in place, camera and all, so a revision saved from anywhere — by the designer, by an agent, by a publish workflow — reaches an open page with no reload and no signing in again. A document that has been edited on screen is told instead, and offered the update; a component pinned to a revision URI is never followed, and the panel counts those. The document panel says what is watched and when it was last checked, and the header picker lists the document on screen, the bench, and every assembly in each repo the files tab has open.
The viewer is the judgement surface for the human at the other end: the report with preview and exact side by side, the face under the cursor with its names and geometry, pin one face and click another to measure, section — a plane taken from the pinned face, pushed through the part by the pan gesture while orbit and zoom keep working, with the id pass clipped alongside the picture so a pick lands on what the eye can see — check interference through the motion, three views for the record, STL and STEP, the files tab with save, history, fork and push. A link from an agent opens here.
Reading
- SKILL.md: the instruction sheet, including a worked task on the published train.
- llms.txt: the index, for an agent that lands on the site cold.
- CHANGELOG.md: what changed and when — for an agent or a person who used this before.
- README.md: the schema and the CLI.
- docs/CAD.md: the design record, phases and gates, the bake-off result, what each cut decided.
8. What it is not, yetsaid plainly
- No sketcher. The tree is edited as JSON and parameters. A 2D constraint solver and a timeline are the next phase of the record.
- No lofts, sweeps along a path, or free-form surfaces. Splines through points are exact; NURBS with user knots are not there. These are OCCT ops behind the same seam, and they will be named faces like everything else.
- No constraint solver for assemblies. Mates are gear and fixed; placements are typed in; the interference check is the safety net.
- No stress. The implicit representation exists for it; the solver does not.
- No merge.
parentsis a list, so a merge revision is representable; nothing writes one yet. - The social layer is a first cut. cad.mino.mobi/parts/ is a Reddit-shaped front: a community is a record in its founder's repo, a post is a record in the poster's repo pointing at a community and at one revision of a part, comments thread by strongRef, a vote is a record the voter owns. The front page is an index in one Durable Object's SQLite, rebuilt from the network: writers self-report, a cron discovers strangers through Constellation backlinks, and there is no firehose socket. No moderation beyond a founder deleting the community yet; labelers and rules applied by the index are the next cut.