# Ask an AI for a CAD part

You describe the part. Your agent writes the recipe, runs it through Zenve's
real geometry kernel, reads back what it built, and fixes what is wrong — then
hands you a **project**, not a mesh.

A sketch, its dimensions, an extrude, a fillet, a timeline. Every number is an
expression over named parameters. Open it in Zenve 3D, drag `wall` from 2 mm to
3 mm, and the whole part rebuilds. Nothing is baked.

That is the difference worth the page. An LLM that emits STL hands you geometry
you can only accept or throw away. An LLM that emits a *recipe* hands you
something you can keep editing for the next two years.

## Install

One command, and it carries everything — the engine, and the language reference
the agent reads before it writes a line.

```jsonc
// Claude Desktop ▸ Settings ▸ Developer ▸ Edit Config
{
  "mcpServers": {
    "zenve3d": { "command": "uvx", "args": ["zenve3d-mcp"] }
  }
}
```

Any MCP client works — Claude Desktop, Claude Code, anything else that speaks
the protocol. It asks for the same thing: run `uvx zenve3d-mcp`.

macOS on Apple silicon, and nothing else to install: the wheel carries the
engine as a self-contained binary. Prefer a terminal? `uv tool install zenve3d-mcp`
puts `zenve3d` on your PATH beside the server, and it is the same engine either
way.

Zenve 3D itself — the app you open the finished part in — is a separate
download.

## How it works

The agent writes a few lines, runs them, reads what the engine saw, and writes
the next few against the truth instead of a guess.

That loop is not a stylistic choice. **A script cannot predict the numbers a
geometry kernel produces.** Which face is the top one, how many closed regions
a sketch encloses, which edge index is the front-left corner — these come out
of a decomposition only the kernel can run. A plate with a hole in it is *two*
regions, not one, and no amount of careful prompting makes that guessable.

So the agent asks. Here is the first pass at a mounting plate:

```
zcmd 1

param wall  = "3"
param width = "60"
param depth = "40"
param bore  = "4"

sketch on XY                                     -> $s1 $o
rect $s1 at (0, 0) width "width" height "depth"  -> $p1
constrain $s1 coincident $p1 $o
extrude profile($s1, 0) distance "wall"          -> $plate
!pump
```

And here is what comes back — not "OK", but the whole state of the document,
written to be read:

```
ok: 9 statements, 0 asserts passed

timeline 2:
  sketch 1028: ok
    frame: origin (0.000, 0.000, 0.000) u (1.000, 0.000, 0.000) …
    solve: dof 0, free [], conflicts []
    regions 1:
      region 0: outer [1038, 1039, 1040, 1041], holes 0
  extrude 1050: ok
    profiles: profile(1028, 0)
    distance: d3 (1049) = 3.000 mm "wall"

bodies 1:
  body 0: 1050
    bounds: (0.000, 0.000, 0.000) .. (60.000, 40.000, 3.000)
    faces 6:
      face 0: planar, normal (0.000, -1.000, 0.000), centroid (30.000, 0.000, 1.500), area 180.000
      …
      face 5: planar, normal (0.000, 0.000, 1.000), centroid (30.000, 20.000, 3.000), area 2400.000
    edges 12:
      …
```

*(Abridged — the real report also carries the parameter table, the parts, every
feature's state and the full edge list.)*

`dof 0` says the sketch is fully constrained — nothing in it can drift. And
face 5 is the top: normal straight up, sitting at `z = 3`. Now the agent knows
where to put the bore, because it read it rather than assumed it:

```
sketch on face($plate, 5)                        -> $s2 $o2 $xt2 $yt2 $xa2 $ya2
circle $s2 at (15, 10) radius "bore / 2"         -> $bc
dim $s2 from $bc to $ya2 = "15"
dim $s2 from $bc to $xa2 = "10"
extrude profile($s2, 0) distance "-(wall * 2)" cut $plate
!pump
```

Run that and the face list has already **re-dealt** — cutting a hole changes
the topology, so face 5 now means something else entirely. Which is exactly
why the loop exists, and why an agent that reads the report between steps
succeeds where one writing a whole file blind does not.

When something is refused, the refusal is written to be acted on: it names the
line, keeps everything before it, and prints the report underneath — usually
including the enumeration that says what the right answer was.

```
refused at line 19
  extrude profile($s1, 2) distance "3" -> $base
  lost-reference: no profile 2 in this sketch — profiles run 0 to 1
```

## The four tools

| Tool | What it does |
|---|---|
| `guide` | The complete language reference. The agent reads it once, then writes parts. |
| `run` | Runs a script through the real engine and reports what it saw — regions, faces, edges, solver state, the body table, every feature's state. Saves the script, and an STL on request. |
| `describe` | The same report for the project you have **open in the app**, so the agent can work from what you already have. |
| `apply` | Edits that project, live in your viewport, as one undoable step. |

Each call gets a fresh engine and nothing is remembered between them. The
script *is* the state: to change the part, the agent sends it again with the
change in it. That is also why you never lose work to a crashed conversation —
the file is on disk the whole time.

## Getting the part into the app

`run` saves the finished script as `~/Downloads/Zenve/<name>.zcmd` and names
the path in what it hands back. One file per part, rewritten on every run, so
what is in the folder at the end is the finished thing rather than a dozen
numbered attempts.

Open it with **File ▸ New from Script…** in Zenve 3D and it becomes a project —
timeline, parameters, everything editable. On iPad, send the file over AirDrop
or through Files and import it there.

Ask for an STL alongside it once the part is right, and it lands beside the
script ready to slice. Set `ZENVE_OUTPUT_DIR` to put both somewhere else.

## Editing the project you already have

The other direction, and the better one once a project exists.

Turn on **AI mode** — the lock beside the panel buttons — and the agent can
read your open project with `describe` and edit it with `apply`. The edit
appears in the viewport straight away, and ⌘Z takes the whole thing back
however many commands were in it.

The lock is the whole contract. It stops the app's own editing while an agent
is working, which is what keeps the part from moving underneath the faces and
edges it is picking, and it is what tells the server which project you mean.
Turn it off and both tools say so rather than guessing.

Nothing lands silently. `apply` refuses if the lock is off, or if the document
has moved since the agent last read it; and if the app does not take the edit
within ten seconds it withdraws it and names the three likely reasons to check.

Your projects live in `~/zenve3d` — a folder you own, not a folder inside the
app, which is what makes any of this reachable by a second process. The app
asks you to pick it on first launch and offers the default. If you put it
somewhere else, **Settings ▸ AI** names the path and the environment variable
to set: `ZENVE_SESSION_DIR`.

## What it deliberately is not

**Not a chat panel in the app.** No API keys in Zenve 3D, no account, no
network dependency. The app talks to a folder; your agent talks to the same
folder. Either one works with the other switched off.

**Not a mesh generator.** If what you want is a printable blob, there are
faster tools. What this produces is a recipe you can open, read, and argue
with — the part stays yours after the conversation ends.

**Not a natural-language modelling language.** Your agent writes the same
`.zcmd` a person would, and every line of it survives into the project. There
is nothing between the words and the geometry that only an LLM can read back.
