cad-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@cad-mcp make a 19-tooth helical gear with a 12mm bore, verify it's watertight, export STL"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
cad-mcp
An MCP server that lets an LLM design parametric 3D parts — and actually verify they are printable.
Most "AI + CAD" demos stop at generating geometry. That is the easy half. The hard half is that a mesh can look correct in a screenshot and still be a broken solid — and the model has no way to notice.
cad-mcp closes that loop. Every part the model builds is checked for topology before it can be exported, and the verdict comes back as structured numbers (watertight? genus? volume? wall thickness?) instead of a picture the model has to squint at.
you: make me a 19-tooth helical gear with a 12 mm bore
model: make_part("involute_helical_gear", {"teeth": 19, "bore_dia": 12})
→ watertight=True genus=1 volume=31140.45 mm³ 19,912 triangles
└ genus=1 is the check that matters: exactly one through-hole, i.e. the bore.
genus=0 would mean the bore is missing; genus=2 would mean the blank is split.
model: look_at_mesh("gear", views="four") → the image below comes back into the context
export_mesh("gear", ["stl", "obj"]) → out/gear.stl
Rendered by the server itself, in pure NumPy — no OpenGL, no CAD kernel. 19 teeth, module 2.5, 22° helix, 16 mm face width, DIN 6885 keyway. Watertight, genus 1, 31,140.45 mm³.

What a refusal looks like
The interesting case is not a part that builds. It is a part that builds, looks fine, and is not printable. Ask for a lattice with walls thinner than a voxel and this comes back:
{
"ok": false, "watertight": true,
"genus": -928.0, "shells": 737,
"fragmented": true, "volume_mm3": 11.7538,
"warning": "The mesh shattered: 737 disconnected shells, overall genus -928 (negative). This is not a part, it is a pile of flakes; exporting will be refused. Raise the wall thickness or lower res."
}Every signal a human would check says fine: watertight, zero non-manifold edges, positive
volume, and a screenshot that looks like a dense lattice. The mesh is 737 disconnected shards,
each one a perfectly closed surface — which is why only the shell count and the sign of the
whole-mesh genus reveal it. export_mesh then refuses before touching the disk; force=True
overrides, and says so in its return value.
A full session — build, verify, look, change the bore, export, get refused — is recorded in
docs/demo-session.md, generated by scripts/make_session_demo.py
against the real server. Every JSON block in it is an actual tool return value, including an
independent check that predicts a volume the server later reports to 0.01%.
Related MCP server: gemini-cad-mcp
Install
python -m venv .venv
.venv/bin/pip install -e ".[render]" # Windows: .venv\Scripts\pipcad-mcp needs Python 3.10+ and NumPy. The render extra adds Pillow, which is only required
for the visual half (look_at_mesh). Without it, the modelling and verification tools still work.
There is no CAD kernel, no OpenCASCADE, no trimesh, no compiled extension.
Modelling every built-in part — including the vase, whose radius profile is a smooth spline —
works on that install alone. The spline has two implementations: scipy.interpolate.CubicSpline
when scipy is present, and a built-in NumPy one otherwise. They agree to 4.4e-16, and
tests/test_kernel.py asserts that, because "scipy is optional" is only true if the parts
actually build without it. Verified by installing NumPy alone into a fresh venv: all four parts
build, and the vase comes out with bit-identical vertices (12432×3, max|Δ| = 0.0) and the same
62,960.4021 mm³ as the scipy environment.
Check the install before wiring it into a client:
.venv/bin/python -m cadmcp.server --check # versions, renderer, font, output dirTroubleshooting
Almost always a non-ASCII path on Windows. An editable install works by dropping a .pth
file into site-packages; pip writes it as UTF-8, but site.py decodes it with the locale
encoding (cp936 on a Chinese Windows). The path decodes to mojibake, the directory does not
exist, and Python silently skips it — pip list still shows cad-mcp 0.1.0, so the install
looks fine while the import keeps failing.
Three ways out, best first:
Give the server the source tree directly — no
.pthinvolved. AddPYTHONPATHto the client'senv(see the last config block below).Or do a regular install instead of an editable one:
pip install ".[render]".Or move the checkout somewhere ASCII-only, e.g.
C:\dev\cad-mcp.
Pillow is missing. pip install "cad-mcp[render]". Modelling and verification work without it.
The bundled fallback font has no CJK glyphs. --check prints which font was picked. Point
CADMCP_FONT at any font file that has the glyphs, e.g. C:\Windows\Fonts\msyh.ttc, and pass it
in the client's env.
Configure your client
{
"mcpServers": {
"cad": {
"command": "/absolute/path/to/cad-mcp/.venv/bin/python",
"args": ["-m", "cadmcp.server"],
"env": { "CAD_MCP_OUT": "/absolute/path/to/output/dir" }
}
}
}On Windows the interpreter lives at .venv\Scripts\python.exe, and backslashes in JSON have to
be doubled:
{
"mcpServers": {
"cad": {
"command": "C:\\dev\\cad-mcp\\.venv\\Scripts\\python.exe",
"args": ["-m", "cadmcp.server"],
"env": { "CAD_MCP_OUT": "C:\\dev\\cad-mcp\\out" }
}
}
}claude mcp add cad -- /absolute/path/to/cad-mcp/.venv/bin/python -m cadmcp.serverRun the server as a subprocess and speak MCP over stdin/stdout:
command: /absolute/path/to/cad-mcp/.venv/bin/python
args: ["-m", "cadmcp.server"]
env: CAD_MCP_OUT=/absolute/path/to/output/dir # optional, defaults to ./outCAD_MCP_OUT is the only knob. Everything the server writes — STL, OBJ, PNG, GIF — lands there.
That is the whole setup. If you would rather run the server straight out of a checkout without
pip install -e ".[render]", point Python at the source tree instead:
{
"mcpServers": {
"cad": {
"command": "/absolute/path/to/cad-mcp/.venv/bin/python",
"args": ["-m", "cadmcp.server"],
"env": {
"PYTHONPATH": "/absolute/path/to/cad-mcp/src",
"CAD_MCP_OUT": "/absolute/path/to/output/dir"
}
}
}
}The seven tools
Tool | What it does |
| Lists the built-in parametric parts and their parameters, with defaults and ranges read from the implementation, so the model never has to guess an argument name |
| Instantiates a parametric part ( |
| Builds free-form geometry from signed distance fields: spheres, boxes, gyroid / Schwarz-P TPMS lattices, shells, with optional corner notches to expose the interior. Reports |
| Re-runs the topology report, cuts cross-sections, estimates minimum wall thickness — pass/fail gates for printability. Reports |
| Renders one or more viewpoints (or a turntable GIF) and returns the images as MCP image content, so the model sees its own work |
| Writes binary/ASCII STL or OBJ+MTL. Refuses at the gate: non-manifold edges, non-positive volume, NaN vertices, or a fragmented shell are blocked before anything touches the disk. Non-watertight only warns. Override with |
| Replays a session model: the exact parameters it was built from, its topology, and every artifact on disk. This is what makes "now make the bore 14 mm" work without the model having to remember its own earlier tool call |
Arguments like involute_helical_gear(teeth=19, module=2.5, helix_deg=22) are validated against
the real signature — call it with teath=19 and you get
involute_helical_gear 不认识这些参数: ['teath'] instead of a silently wrong gear.
One thing worth knowing when writing a client: a wrong type is caught earlier, by the SDK's
own argument validation, and comes back as isError=true with a plain-text message — there is no
JSON body for that case. Only the field name reaches the server log. So parse the JSON payload of
a tool result, but check isError first.
Why the verification numbers are trustworthy
cad-mcp does not grade its own homework. The checks below compare the tessellated mesh against
closed-form results computed independently of the mesher, so a bug in the mesher cannot
cancel itself out. All values are reproducible with python scripts/make_verification.py
and stored in docs/verification.json.
Check | Measured | Independent value | Error |
Gear circular tooth thickness at the pitch circle | 3.92488 mm | πm/2 − backlash/2 = 3.90699 mm | +0.458 % |
SDF sphere volume, r = 10 mm, 96³ grid | 4184.7218 mm³ | 4/3·πr³ = 4188.7902 mm³ | 0.097 % |
Binary STL round-trip (gear, 19,912 tris) | 995,684 bytes, volume Δ 0.0 mm³ | 84 + 50n = 995,684 bytes | exact |
Gear minimum wall thickness (240 samples) | 2.1124 mm (p05 = 2.4258, median = 7.5016) | — | — |
The residual 0.458 % on tooth thickness is not a bug: it is the polygon approximation of the
involute flank at the pitch circle (12 flank samples per tooth). Tightening flank_samples
converges it toward πm/2, which is exactly the behaviour you want a validator to show.
Every part below is watertight with zero non-manifold edges:
Part | Triangles | Genus | Volume | Notes |
| 19,912 | 1 | 31,140.45 mm³ | genus 1 = the bore, nothing else |
| 24,860 | 0 | 62,960.36 mm³ | hollow, superellipse cross-section |
| 18,480 | 1 | 125,254.19 mm³ | swept tube, self-intersection free |
| 128,284 | 87 | — | 33.3 % solid fraction |
SDF gyroid, 64³ grid | 33,900 | 197 | 2,682.08 mm³ | 20 mm cube, 0.625 mm voxels |
Speed
Single-threaded, pure NumPy, on an ordinary desktop CPU (scripts/make_verification.py):
Operation | Time |
| 0.54 s |
| 0.09 s |
| 0.07 s |
| 0.003 s |
| 0.009 s |
| 3.5 s |
| 2.3 s |
| 4.5 s |
Modelling is cheap; rendering is the slow part, which is why look_at_mesh takes a views
count and a size — ask for one 640×480 view while iterating, four larger ones when you are done.
What is inside
src/cadmcp/
├── geom.py 34 KB Mesh, welding, topology report (Euler characteristic, genus,
│ manifold audit), slicing, min wall thickness, ear-clipping,
│ solid/tube sweeps
├── models.py 16 KB the four parametric parts, involute flanks + root fillets
├── implicit.py 36 KB SDF primitives, booleans, smooth min/max, twists, gyroid /
│ Schwarz-P, surface-nets mesher
├── render.py 52 KB software rasteriser: supersampled triangle fill, Phong shading,
│ contact shadows, ground pool, perspective/ortho cameras,
│ turntables, multi-view grids
├── meshio.py 5 KB binary/ASCII STL, OBJ+MTL round-trip
└── server.py the MCP layerThe kernel is deliberately analytic where it can be, numeric where it must be: parametric parts are exact sweeps of closed profiles, while lattices come out of an implicit field.
Four implementation notes that cost real debugging time, in case you fork this:
sdf_to_mesh(f, lo, hi, res)takes a resolution count per axis, not a voxel size. Passing a voxel size (~0.3) makesmin(nx,ny,nz) < 2and the mesher refuses. The compute domain is also padded 12 % so the iso-surface never touches the boundary.Pillow's built-in font has no CJK glyphs. Every Chinese label in a
render_gridcame out as□□□— in the exact images meant for this README. The renderer now resolves a system CJK font (Microsoft YaHei / SimHei / PingFang / Noto CJK), centres the label on its real glyph bbox, and exposesfont_supports_cjk()so callers can fall back to English. Override withCADMCP_FONT.A watertight mesh can still be garbage. Set a lattice wall thinner than one voxel and the surface-nets mesher happily returns 737 disconnected shards — every one of them a perfectly closed surface,
watertight=True,non_manifold_edges=0, positive volume. Every "does it look broken?" signal says no. What catches it is the shell count, plus the fact that genus, being a whole-mesh formula (g = (2 − χ)/2), turns negative when the mesh is really a pile of fragments. That is whyexport_meshrefuses onfragmentedand whycheck_meshreportsshells— the shard detection is the check that a picture could never have made.Vertex normals belong to a smoothing sector, not to a vertex. The gear rendered with a fine vertical stipple across its hub and tooth tips while flat panels and every other part came out clean. Three hypotheses died on data first: six
specular/shininesscombinations, eight tessellation settings, and crease angles from 5° to 90° all produced the identical stipple — the last one because a fixed count of 92 edges sit at exactly 180°, independent of sampling. The cause was real: normals were one area-weighted average over every incident face, and the hub cylinder's vertices sit directly against a coplanar annular face, which dragged the average toward +Z. Measured on the tip cylinder, normals deviated from radial by a median of 26.4° and up to 81.2°. They are now accumulated per smoothing sector — connected components along non-hard edges, the equivalent of a smoothing group. Smooth surfaces collapse back to one sector, so the fix is provably inert where it should be, andtests/test_kernel.pyasserts both directions: the torus knot matches the naive average to 1e-6°, the gear does not.
Development
python tests/test_e2e_stdio.py # 62 checks, drives a real server subprocess over stdio
python tests/test_kernel.py # geometry kernel unit tests
python scripts/preflight.py # 54 checks that every claim in THIS README still holds
python scripts/make_demo.py # regenerate docs/gallery.png + docs/gear-turntable.gif
python scripts/make_verification.py # regenerate docs/verification.json
python scripts/make_session_demo.py # regenerate docs/demo-session.mdscripts/preflight.py exists because a README is the part of a project that rots silently. Code
gets refactored, an argument is renamed, a verification number is recomputed — and no unit test
notices, because unit tests test code. So every factual claim in this file is an assertion: it
checks that each referenced file exists, that the numbers printed in the verification table match
docs/verification.json character for character, that CI really runs both test scripts on all
three platforms, and that the four built-in parts still build with the genus this README claims.
The end-to-end test is not a mock: it spawns python -m cadmcp.server, speaks the real MCP
protocol over stdio, calls every tool, and asserts that look_at_mesh returns an image
content block before the text summary — that ordering is what makes the model look at the
picture instead of skipping it.
Both test scripts reconfigure their output to UTF-8 on startup. A Chinese Windows console
defaults to cp936, where a single mm³ in an assertion message raises UnicodeEncodeError —
turning a clean pass/fail report into a stack trace. CI hides this behind
PYTHONIOENCODING: utf-8; running the tests locally does not, so they fix it themselves.
Limitations
Three parts families, not a general CAD kernel. No fillets/chamfers on arbitrary edges, no B-rep booleans, no constraints or assemblies.
Models live in server memory for the session. Restart the server and they are gone — export what you want to keep.
look_at_meshneeds Pillow; the modelling and verification tools do not.STL/OBJ only. No STEP, so nothing here round-trips back into a parametric CAD system.
SDF meshing is grid-bound: sharp features below one voxel are lost, and
resis a memory knob as much as a quality knob (capped at 200 M voxels).
License
MIT — see LICENSE.
Available Tools
7 toolscheck_meshA
Geometrically inspect a model you already built; returns numeric evidence you can use to decide whether it came out right.
Includes watertightness, orientation consistency, Euler characteristic, genus, volume, surface area and bounding box. genus tells you whether the hole count is right (a part with one through-hole has genus=1, a solid has genus=0, a lattice coupon is high). It is a topological invariant, which makes it far more reliable than "looks about right". slices: z heights in mm to also measure cross-section areas; wall_samples>0 samples the minimum wall thickness.
Inputs: name (required), slices, wall_samples.
describe_modelB
Full information about a model you built: parameters, topology, bounding box, files.
Use it mid-session to recall which parameters this model was actually built from.
Inputs: name (required).
export_meshA
Export the model as files for downstream tools: STL (printing / slicing), OBJ+MTL (DCC).
Defaults to both STL and OBJ. There is a hard gate before anything is written: structural damage -- non-manifold edges, non-positive volume, NaN vertices, a fragmented shell -- is refused, because such a mesh punches holes in a slicer or errors out. Watertightness only warns (some shapes are meant to be open). Pass force=True to override.
Inputs: name (required), force, formats, stl_binary.
list_partsA
List the built-in parametric parts and their tunable parameters (with defaults and types).
Call this before modelling to get the exact parameter names -- guessing one raises unknown_parameter.
Inputs: no named parameters.
look_at_meshB
Render the model and return the image directly to you -- this is the core of self-verification.
The render comes back as image content, so you can actually see what you built and decide what to change.
views: 'four' (a 2x2 montage; start here) / 'single' (one viewpoint) / 'grid' (six). turntable_frames>0 also writes a turntable GIF for the human, and returns its path. Returns [image, ...] followed by a JSON summary -- the image always comes first.
Inputs: name (required), size, views, azimuth, +4 more.
make_latticeA
Model free-form geometry with signed distance fields: lattices, porous structures, implicit surfaces.
primitive: sphere / box / rounded_box / gyroid / schwarz_p. size_mm: the part's bounding box in mm. A single number means a cube with that edge length, [x, y, z] an arbitrary box; omit it to use cell_mm as a cube. res: samples per axis -- a precision knob, not a dimension. Start at 64; 128 is fine but 8x slower. The resulting voxel edge (size/res) is reported back. wall_mm: wall thickness for shells and lattices; period_mm: lattice period (defaults to cell_mm). lattice: shell (hollow it out) / gyroid_grading (fill the interior with lattice). subtract: corner_notch / corner_notch2 (cut a corner away to expose the interior).
Inputs: res, name, cell_mm, lattice, +5 more.
make_partB
Build a part from the built-in parametric library (gear / vase / torus knot / lattice coupon).
Returns a topology verdict. See list_parts for valid part values; an unknown key in params
is reported explicitly rather than silently ignored. The model is stored in the session and
referenced by name from the other tools.
Inputs: part (required), name, params.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 7 tools
The three build/inspect verbs are well-separated: make_part (library parts) vs make_lattice (free-form SDF) vs list_parts (discovery). check_mesh, look_at_mesh, and describe_model overlap somewhat (all report topology/bbox), but their descriptions clearly split numeric evidence, visual rendering, and parameter recall.
Nearly all names follow a verb_object snake_case pattern (list_parts, make_part, check_mesh, export_mesh, describe_model). The sole deviation is look_at_mesh, which inserts a preposition but remains easily readable.
Seven tools is well-scoped for a model-build-inspect-export workflow; each tool covers a distinct stage and none feels redundant or padded.
Covers discovery, two build paths, numeric and visual verification, export, and parameter recall. Minor gaps: no delete_model, no list_models to enumerate session models, and no pure edit/parameter-update tool, though rebuilding via make_part/make_lattice can work around these.
Maintenance
Related MCP Connectors
Parametric 3D CAD for AI agents: build print-ready parts, check them, export STL, 3MF or STEP.
Parametric CAD as code: model 3D-printable parts on an exact B-rep kernel, measured.
Design 3D-printable parts by chatting: create, edit, render and publish parametric forges.
Text-to-CAD: build exact STEP or 3MF/STL on the server, each stated requirement measured.
1
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables conversational 3D modeling by providing CAD-Query functionality to validate parametric 3D models against criteria and export to STL/STEP formats for 3D printing and CAD applications.20-
- AlicenseAqualityBmaintenanceEnables multimodal AI to convert photos with reference objects into parametric OpenSCAD models and watertight 3D-printable STL files, including calibration, part generation, and printability inspection.7MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to design, modify, and verify exact B-rep 3D CAD models by running build123d scripts on an OpenCascade kernel, with live three.js preview, collision and FDM printability checks, text-based spatial perception, user notes/sketch annotations, version history, and one-click export to OrcaSlicer.1MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI coding agents to turn natural-language descriptions and dimensional sketches into validated, watertight 3D-printable STL files by compiling build123d scripts, inspecting meshes, and rendering deterministic multiview previews.MIT