DX-CAD for agents
dxcad serve is a Model Context Protocol server. An MCP client, Claude Code
or Claude Desktop, starts it as a child process and talks to it down its
standard input and output; the server holds one document of named solid
parts, in millimetres, and every tool below makes, changes, measures, reads
or writes those parts. Nothing is drawn: a tool that makes or changes a part
answers its facts, its volume, its size, its faces by kind and its round
holes, and that answer is the agent's only look at the part. There is no
window and no Qt in the server, so it runs on a machine with no display.
The worked example, a bracket with four holes from one sentence, is in
examples/bracket.md. It runs its model with run_model
and model_set, so the server needs --allow-run for it: see
What it runs.
Before anything else
Install DX-CAD and check that it works, as README's Install section says:
pip install dxcad
dxcad doctor
Claude Code
One line, typed in the directory the parts are to be written in:
claude mcp add dxcad -- dxcad serve
Or the same thing as a file, .mcp.json at the root of a project, which can
be committed so that everyone who opens the project has the server:
{
"mcpServers": {
"dxcad": {
"command": "dxcad",
"args": ["serve"]
}
}
}
Both of these find dxcad on the PATH, which a terminal with the virtual
environment active has. When it has not, use what the next section uses.
Claude Desktop
Claude Desktop reads claude_desktop_config.json, which Settings, Developer,
Edit Config opens. It is kept at
- Windows:
%APPDATA%\Claude\claude_desktop_config.json - macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
A desktop application is started by the desktop and not by a terminal, so it
has no virtual environment on its PATH and "command": "dxcad" is not found
from there; and it starts its servers in a directory of its own choosing,
which is not where the parts should land. DX-CAD writes the configuration
that answers both, naming the Python it is installed under by its whole path
and the directory to work in:
dxcad serve --config --directory ~/Documents/parts
It prints what goes in the file, which on Windows looks like this:
{
"mcpServers": {
"dxcad": {
"command": "C:\\Users\\you\\.venvs\\cad\\Scripts\\python.exe",
"args": [
"-m",
"dxcad",
"serve",
"--directory",
"C:\\Users\\you\\Documents\\parts"
]
}
}
}
If the file already holds an "mcpServers" object, add the "dxcad" entry
to it and leave the others. Restart Claude Desktop afterwards; the server is
listed under the tools it offers. The directory has to exist: a
--directory that is not there is said on the server's standard error, which
is what the client's log for the server shows.
What it runs
run_model and model_set run a model.py, which is Python, with your own
permissions: whatever you may do on the machine, a model may do. That is the
bargain dxcad run already makes, and it is what makes a saved project
parametric, but it is the one thing here that is not bounded by the kernel.
The server's annotations say so, run_model and model_set being the two
tools marked open-world, and a client that asks before a call will ask
before these. project_save and export write where they are told, and
refuse to write over a file that is already there unless force is given.
So the two are off unless you say otherwise. A server started plainly has
neither in tools/list, and a call of either is refused in the words a tool
the server has never heard of is refused in, naming the option. --allow-run
turns them on:
dxcad serve --allow-run
It goes into a configuration the same way, after --directory, and
--config prints it:
dxcad serve --config --directory ~/Documents/parts --allow-run
Every other tool is there either way: an agent can draw, drill, pattern,
measure, save and export a part without the server running a line of Python.
--allow-run is what a parametric model is worth, and what it costs.
Where it may write
The server works in one directory and reads and writes nowhere else. That
directory is --directory when there is one, and where the server was
started when there is not. Every path any tool is given, project_open,
project_save, export and import_part alike, is resolved against it
before the file is touched: a relative path is taken from it, an absolute one
has to be inside it, and .. and symbolic links are followed first, so a
path that leaves the directory by any road is refused in words naming the
path and the directory, with nothing read or written. A model run_model is
given has the same directory as its base.
So the directory named in the configuration is the whole of what an agent can reach through the tools. It is worth giving it one of its own.
The tools
| Tool | What it does | Inputs |
|---|---|---|
document_list |
List the parts in the document: each one's name, volume and whether it is a valid solid. | none |
describe |
Describe one part, or every part when no name is given: volume, area, bounds, size, centre of mass, counts of faces by kind of surface, and each round hole's diameter, depth, axis and centre. This is the only look at a part there is. | name: string |
printability |
Report how one part would 3D print: its volume, its mass in a named material or at a density, and which it assumed, its centre of mass, how many faces overhang and their area, the thinnest wall and where it is, and a warning for each check it fails. | name: string, required; density: number; material: string; min_wall: number; angle: number |
document_rename |
Give a part another name. | name: string, required; new_name: string, required |
document_remove |
Take a part out of the document. | name: string, required |
document_duplicate |
Copy a part under a new name. The original stays. | name: string, required; new_name: string, required |
undo |
Take back the last change to the document. Answers whether there was one. | none |
redo |
Put back the change the last undo took away. Answers whether there was one. | none |
make_box |
Add a box with its edges along the axes. Its lowest corner is at the origin given. | name: string, required; dx: number, required; dy: number, required; dz: number, required; origin: array of number (3); color: string; part_number: string; revision: string; properties: object |
make_cylinder |
Add a cylinder standing along Z. The centre of its base is at the origin given. | name: string, required; radius: number, required; height: number, required; origin: array of number (3); color: string; part_number: string; revision: string; properties: object |
make_sphere |
Add a sphere. Its centre is at the origin given. | name: string, required; radius: number, required; origin: array of number (3); color: string; part_number: string; revision: string; properties: object |
make_cone |
Add a cone or a frustum standing along Z. The centre of its base is at the origin given. | name: string, required; bottom_radius: number, required; top_radius: number, required; height: number, required; origin: array of number (3); color: string; part_number: string; revision: string; properties: object |
make_torus |
Add a torus, a ring, lying in the XY plane. Its centre is at the origin given. | name: string, required; major_radius: number, required; minor_radius: number, required; origin: array of number (3); color: string; part_number: string; revision: string; properties: object |
translate |
Move a part by a vector. | name: string, required; vector: array of number (3), required; as: string |
rotate |
Turn a part about an axis, by the right-hand rule. | name: string, required; point: array of number (3), required; direction: array of number (3), required; angle: number, required; as: string |
mirror |
Reflect a part in a plane. With "as", this makes the other hand of a part. | name: string, required; point: array of number (3), required; normal: array of number (3), required; as: string |
scale |
Make a part larger or smaller about a point, which stays where it is. | name: string, required; factor: number, required; origin: array of number (3); as: string |
cut |
Cut part b out of part a. On a sheet part, give holes and rectangular cutouts with the sheet tools' holes and cutouts, and shape where flanges or returns meet with a corner treatment, rather than cutting: a through cut normal to one face is kept as that face's cutout or hole, a cut across a bend zone or a corner is refused, and any other boolean makes a plain solid that cannot be unfolded. | a: string, required; b: string, required; keep: boolean; proceed: boolean; as: string |
fuse |
Join part b onto part a. | a: string, required; b: string, required; keep: boolean; proceed: boolean; as: string |
common |
Keep only what part a and part b share. | a: string, required; b: string, required; keep: boolean; proceed: boolean; as: string |
hole |
Drill a round hole into a flat face of a part, straight in. | name: string, required; facing: array of number (3), required; centre: array of number (3), required; radius: number, required; depth: number |
countersink |
Drill a hole with a 90 degree cone at its mouth, for a countersunk screw. | name: string, required; facing: array of number (3), required; centre: array of number (3), required; radius: number, required; head_radius: number, required; depth: number |
counterbore |
Drill a hole with a wider flat-bottomed bore at its mouth, for a socket head screw. | name: string, required; facing: array of number (3), required; centre: array of number (3), required; radius: number, required; head_radius: number, required; head_depth: number, required; depth: number |
fillet_all |
Round every edge of a part. Refused, with the part unchanged, if the radius does not fit. | name: string, required; radius: number, required |
chamfer_all |
Cut a flat across every edge of a part. Refused if the distance does not fit. | name: string, required; distance: number, required |
sketch_extrude |
Add a part by drawing a closed outline on a plane and extruding it along the plane's normal. This is how a plate of any outline is made, holes and all. Give constraints to square up a one-loop outline of move_to, line_to and close before it is swept. | name: string, required; plane: string or object; profile: array of object (at least 1), required; constraints: array of object; height: number, required |
sketch_revolve |
Add a part by drawing a closed outline on a plane and turning it about an axis. The outline must lie to one side of the axis. Give constraints to square up a one-loop outline of move_to, line_to and close before it is swept. | name: string, required; plane: string or object; profile: array of object (at least 1), required; constraints: array of object; point: array of number (3), required; direction: array of number (3), required; angle: number |
text3d |
Add a part that is the letters of a text, swept along a plane's normal. Fuse it with a part to raise the text on it, or place it sunk into a face and cut it to engrave. | name: string, required; text: string, required; size: number, required; depth: number, required; font: string; plane: string or object; at: array of number (2); angle: number; align: one of left, centre, right |
mate |
Place a part against another by one mate: coincident (two faces flush), concentric (two axes in line), distance (two faces parallel, value apart) or angle (value degrees). A part's mates are met in order, each keeping what the ones before fixed; answers what freedom is left. | part: string, required; ref: string, required; to: string, required; to_ref: string, required; kind: one of coincident, concentric, distance, angle, required; value: number |
suggest_mates |
List the mates a part's features allow against the parts near it, best first: a cylinder to a hole it fits as concentric, opposed faces as coincident (first where a head seats on a hole's face), and as distance with the measured gap. Adds no mate: propose them to the user, and call mate only for the ones the user confirms. | part: string, required |
measure |
Measure between two parts: the shortest distance from one to the other, the point on each where it is measured, whether they touch, and the volume they share, which is 0 for parts that fit and above 0 for parts that clash. The document is not changed. | a: string, required; b: string, required |
bill_of_materials |
List the document's parts as a bill of materials: parts placed from one source as one shape are one line with a count, every other part a line of its own. Each line gives its name, count, the parts it counts, one part's volume in mm3, its mass in g when a material or a density is given, and its size before it was placed. | density: number; material: string |
interference |
Check every pair of parts, or of the parts named, for interference: each pair is overlap, with the volume they share and its bounds, touching (they meet but share no volume), or clear. Answers the pairs and how many overlap. | names: array of string |
render_view |
Render a PNG picture of the document, or of the parts named, from a standard view in one of the window's display modes, so what was built can be seen. The picture comes back inline, and a copy is saved where the user can open it, beside the window's saved document, else in the server's folder, else in Documents\DX-CAD\renders; the answer says its whole path. | names: array of string; view: one of front, back, left, right, top, bottom, isometric; mode: one of shaded_edges, shaded, wireframe, translucent; width: integer; height: integer; explode: number |
sheet_base_flange |
Add a sheet metal part: a closed outline on a plane, a sheet's thickness thick, that sheet_flange can bend flanges on and flat_pattern can unfold, with any round holes and rectangular cutouts cut through it. Answers its facts. On a sheet part, give holes and rectangular cutouts with the sheet tools' holes and cutouts, and shape where flanges or returns meet with a corner treatment, rather than cutting: a through cut normal to one face is kept as that face's cutout or hole, a cut across a bend zone or a corner is refused, and any other boolean makes a plain solid that cannot be unfolded. | name: string, required; points: array of array of number (2) (at least 3), required; thickness: number, required; bend_radius: number, required; k_factor: number; plane: string or object; holes: array of array of number (3); cutouts: array of array of number (4) |
sheet_flange |
Bend a flange on one edge of a sheet part's outline, or on the part of it between start_inset and end_inset: a bend of its inside radius, its own bend_radius or the sheet's, then a straight leg with any round holes and rectangular cutouts cut through it, and with relief a bend relief slot beside each inset end. At angle 180 it is an open hem, its leg back over or under the base, refused with a bend radius of 0 or on an edge sharing a corner with another flange unless it is inset at least the thickness at that corner. With return_length, a 90 degree flange gets a second 90 degree bend at the leg's end and a return leg that long, as a channel's lip. With length_from "outside", length runs to the outside mould line, where the base's and the leg's outside faces meet, as a drawing dimensions it; with a return, length runs outside to outside and return_length to the return's outside mould line. Edge n runs from corner n to corner n + 1. Answers its facts. On a sheet part, give holes and rectangular cutouts with the sheet tools' holes and cutouts, and shape where flanges or returns meet with a corner treatment, rather than cutting: a through cut normal to one face is kept as that face's cutout or hole, a cut across a bend zone or a corner is refused, and any other boolean makes a plain solid that cannot be unfolded. | name: string, required; edge: integer, required; length: number, required; angle: number; direction: one of up, down; start_inset: number; end_inset: number; relief: boolean; bend_radius: number; holes: array of array of number (3); cutouts: array of array of number (4); return_length: number; length_from: one of leg, outside; corner_treatment: one of mitre, square, open, none; corner_gap: number |
flat_pattern |
Unfold a sheet part and write its flat pattern as a DXF for a laser or a plasma table: the outline on layer CUT, bend lines on BEND, notes on NOTES; or as an SVG when the output ends in .svg, or as a flat solid in STEP when it ends in .step or .stp. Answers the file, the flat's width and height, its bend count and its bend table for the press brake: each bend's number, angle, direction, inside radius, allowance and bend line length; then its cut length, in mm, and its area, in mm², for a quote. | name: string, required; output: string, required; force: boolean |
sheet_flat_part |
Add a sheet part's flat pattern as a part of its own: the flat outline extruded by the sheet's thickness on the XY plane from z = 0, with every hole and cutout cut through it. The sheet part stays as it is. Answers its facts. | name: string, required; result: string, required |
sheet_edges |
Read a sheet part's outline edge by edge, to choose where sheet_flange bends one: each edge's number, as sheet_flange takes it, its start and end corners, its length, and the flange on it as its note, such as UP 90 R2, and its straight leg, or null for both when it is free. Answers the sheet's thickness, bend_radius and k_factor and the edges. Changes nothing. | name: string, required |
linear_pattern |
Copy a part in a row, each copy one step further along than the last. Left alone the copies are new parts, name_2, name_3 and so on; with fuse they are joined into the part; with cut_from they are all cut out of another part. | name: string, required; step: array of number (3), required; count: integer, required; fuse: boolean; cut_from: string; keep: boolean |
circular_pattern |
Copy a part in a ring about an axis, the copies spread evenly over the angle. Left alone the copies are new parts, name_2, name_3 and so on; with fuse they are joined into the part; with cut_from they are all cut out of another part: a bolt circle. | name: string, required; point: array of number (3), required; direction: array of number (3), required; angle: number; count: integer, required; fuse: boolean; cut_from: string; keep: boolean |
hollow |
Shell a part to a wall of one thickness, taken from the inside, leaving the faces named open: a box becomes a tray. | name: string, required; thickness: number, required; open: array of array of number (3) (at least 1), required |
project_open |
Open a .dxcprt or .dxcasm project file, a part or an assembly (a .dxcad from before them opens too). The document becomes the project's, what was there before is gone, and the history starts again. Answers what describe would. | path: string, required |
project_save |
Save the document as a .dxcprt or .dxcasm project file: a .dxcprt for one part, a .dxcasm for an assembly. The dxcad window and command line open. It carries the model too, when there is one. | path: string, required; force: boolean |
export |
Write one part to a STEP, STL, 3MF or BREP file, by the path's extension: .step for other CAD, .stl or .3mf for a slicer, .brep for OpenCASCADE; or, ending .dxf or .svg, a drawing of the Sketches the document's model names, one layer each. | path: string, required; name: string; force: boolean |
import_part |
Read a part from a STEP or BREP file into the document as a new part, and answer its facts. The file is not changed. | path: string, required; name: string |
drawing |
Draw the document's parts on a drawing sheet and write it as a DXF or a PDF, by the path's extension: the front, top, right and isometric views, hidden edges dashed, at one scale that fits, in the title block with its fields filled, and a parts list. Answers the sheet, the scale, the views and the parts listed. | path: string, required; sheet: one of A4, A3, A2, A1, ISO-A4-portrait, ISO-A4, ISO-A3, ISO-A2, ISO-A1, ISO-A0, ANSI-A, ANSI-B, ANSI-C, ANSI-D; fields: object; file_name: string; force: boolean |
get_model |
The model.py the document carries: the Python that builds its parts, or null. | none |
run_model |
Run a model.py, Python source that builds parts with the kernel's functions (make_box, cut, hole, Sketch, ...) and registers each with part("name", shape). The document becomes what it builds, as one undoable step, and carries the source as its model, so the project saved afterwards is parametric. It runs with the user's own permissions. Answers what describe would, with what the model printed as output. | source: string, required; timeout: number |
model_values |
The numbers the model.py is made of: every top-level NAME = value line whose value is a number, a string or a boolean, by name. These are what model_set changes. | none |
model_set |
Change some of the model.py's numbers, as model_values lists them, and run it again: the way to make a parametric part longer or its holes wider without writing the model again. One undoable step, and a model that fails with its new numbers leaves everything as it was. It runs the model, with the user's own permissions. Answers what run_model would. | values: object, required; timeout: number |
Each tool's arguments are described by the server itself, in the schema
tools/list answers, which is what the agent reads. The table above is
generated from those same schemas by python -m dxcad.docsgen agents, so
it says what the server says. A tool that was asked
properly and cannot do it, a fillet too large for its part, a name already
taken, answers in words and leaves the document as it was, so the agent can
read why and try something else. A part named that the document does not
have is refused naming the parts it does have.
The booleans
fuse, cut and common take the same inputs, two parts by name:
a: The part that is kept and changed.b: The other part, which is taken out of the document afterwards unless keep is true.keep: Leave b in the document. False when left out.proceed: Go ahead with a boolean that turns a sheet part into a plain solid, losing its flat pattern; refused without it. False when left out.as: A name to add the result under, leaving a as it is. Without it the result replaces a.
a and b are required. To fuse a boss onto a plate, fuse with a the
plate and b the boss; to drill with a cylinder, cut with a the part and
b the cylinder. Both are made first with the tools that add parts, and
their names are the names those tools were given.
On a sheet part, a cut that passes cleanly through one flat face, a round
or rectangular prism normal to it and clear of the bends, is kept as that
face's hole or cutout, so the part still unfolds. A cut across a bend zone or
a corner is refused: shape corners with a corner treatment, or give cutouts
on the faces either side. Any other boolean on a sheet part is refused unless
proceed is true, since it makes a plain solid with no flat pattern.
The order of the tools in tools/list is not fixed: a new tool may be listed
beside the ones it belongs with and move the rest along. A client looks each
tool up by its name, never by its place in the list.
Without a client
The server is one JSON object a line in and one a line out, so it can be tried from a shell with nothing but a pipe:
echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' | dxcad serve