Skip to content

MCP Server

CAD-Preview ships a standalone Model Context Protocol server (dist/mcp-server.js) so AI agents — Claude Code, or any MCP client — can load CAD models, apply edit operations, manage parts and parametric variables, and generate/export finite-element meshes headless, with no VS Code involved.

It reuses the exact same host pipeline the extension runs (OCCT via opencascade.js, Gmsh via @loumalouomega/gmsh-wasm) and persists to the exact same sidecar files — so an agent's edits appear in the extension the next time the file is opened, and vice versa.

Prerequisites

bash
npm install
npm run build   # produces dist/mcp-server.js + the two WASM binaries beside it

Registering with an MCP client

With Claude Code:

bash
claude mcp add cad-preview -- node /absolute/path/to/CAD-Preview/dist/mcp-server.js

or in a project's .mcp.json:

json
{
  "mcpServers": {
    "cad-preview": {
      "command": "node",
      "args": ["/absolute/path/to/CAD-Preview/dist/mcp-server.js"]
    }
  }
}

The server locates the WASM binaries relative to its own bundle (<bundle dir>/../dist/*.wasm, i.e. the repo root or an installed extension directory works as-is); set the CAD_PREVIEW_ROOT environment variable to point at a directory containing dist/opencascade.wasm.wasm + dist/gmsh-core.wasm for unusual layouts.

Tools

Every tool takes an absolute path to the model file, and every result carries a warnings array reporting graceful degradations. Call describe_capabilities first — it returns the full op catalog with per-kind parameter documentation.

ToolWhat it does
describe_capabilitiesOp catalog (all edit-op kinds + parameter docs + B-rep-only/topology-changing flags), entity-id scheme, export target matrix, mesh export formats, mesh option defaults, headless limitations.
load_modelLoad the model (sidecar edits replayed) and return the component tree, entity-id inventory (solid-N/face-N/edge-N/point-N — the ids used as op operands and part members), bounding box, and sidecar summary.
get_stateThe sidecar state without loading geometry: edit-op stack (indexed, described), variables (evaluated), parts, mesh options.
apply_edit_opsValidate and append raw EditOp JSON objects to the op stack; per-op accept/reject report; for B-rep sources returns the post-replay entity inventory. dryRun validates without persisting.
remove_edit_opRemove one op by 0-based index (the panel's per-row ✕ equivalent).
set_variablesReplace the named parametric variables (L = 20) and re-resolve every op expression — geometry rebuilds from the new values on next load/mesh.
set_partCreate/update/remove a named part grouping entity ids; optional per-part meshSize for local refinement.
set_mesh_optionsMerge fields into the persisted mesh options (also regenerates the one-way <model>.geo script).
generate_meshRun Gmsh and return statistics only (node/element counts, element groups, timing) — nothing written.
export_meshGenerate and write the mesh in any registered format (mdpaElements, mdpaGeometries, msh, msh2, geoUnrolled, vtk, unv, inp, bdf, su2, mesh, stl, diff, off). geoUnrolled also writes the required .xao companion beside the output for B-rep sources.
export_brepExport a B-rep source to another B-rep format (STEP/IGES/BREP) with all edits baked in.
save_preprocessBundle the CAD source plus whichever of its .parts.json/.edits.json/.mesh.json/.geo sidecars currently exist into a single .zip archive. Mirrors the extension's File ▸ Save Preprocess….
load_preprocessRestore a .zip from save_preprocess (or the extension's File ▸ Save Preprocess…) to a new CAD file path plus its matching sidecar filenames. Mirrors the extension's File ▸ Load Preprocess….

Edit ops are passed as raw JSON (e.g. {"op": "addBox", "center": [0,0,0], "size": [20,10,5]}) and validated by the same tolerant gate the extension uses (validateEditOp) — so every op kind the extension gains is automatically available to agents, and a malformed op is rejected with a reason rather than crashing anything. Numeric fields may bind to variables via the op's exprs map ({"exprs": {"size[0]": "L"}}).

The sidecar contract

The server never writes the CAD source file (every output path is guarded). State persists to the same sidecars the extension reads on open:

FileContents
<model>.edits.jsonOrdered, replayable edit-op list + parametric variables
<model>.parts.jsonNamed parts (entity-id groups, colours, mesh sizes)
<model>.mesh.jsonMesh-generation options
<model>.geoGenerated (one-way) Gmsh script for the current options

This makes the workflow bidirectional: ask an agent to model something, then open the file in VS Code to inspect it — or set up parts interactively and let the agent mesh and export.

save_preprocess/load_preprocess package/restore the CAD source plus whichever of these four sidecars exist on disk as a single portable .zip — a missing sidecar (e.g. mesh options never set) is simply omitted from the archive, never an error. The .geo script inside the archive is never restored verbatim on load_preprocess; the mesh options sidecar (if any) is re-written through the normal options path instead, which regenerates .geo fresh — same one-way-generation rule as every other write path.

Headless capability matrix

Source formatLoad/inventoryEdit opsMeshExport
.step/.stp, .iges/.igs, .brep✅ full✅ full (baked into mesh/export)✅ B-rep targets
.stlroute info onlysidecar-only¹✅ raw file bytes²❌ webview-only
.obj, .ply, .gltf/.glbroute info onlysidecar-only¹❌ webview-only❌ webview-only

¹ Mesh-legal ops (transforms, booleans, holes, primitives, explode) are validated and persisted, but the mesh edit engine is Three.js in the webview — they replay when the file is opened in VS Code, not headless. B-rep-only ops are rejected.

² Edits are not baked into the meshed geometry for .stl (the extension bakes them by serializing the webview's displayed scene); the raw file is meshed and a warning is reported. Parts can't become physical groups for mesh sources; a single part's meshSize acts as a one-off global size override.

Troubleshooting

  • The client reports protocol/parse errors — something wrote to stdout. stdout is the JSON-RPC channel; the server rebinds console.log/info/warn to stderr before any WASM init, so a regression here means new code printed to process.stdout directly. npm run mcp:smoke catches this.
  • ENOENT … opencascade.wasm.wasmdist/ isn't populated; run npm run build, or point CAD_PREVIEW_ROOT at a directory whose dist/ contains both WASM binaries.
  • First tool call is slow — the WASM kernels (~110 MB combined) initialize lazily on first use and are then memoized for the life of the process.

Testing

  • npm test covers the tool handlers and sidecar store with an injected fake pipeline (no WASM).
  • npm run mcp:smoke runs the real end-to-end scenario over actual stdio JSON-RPC against examples/STP/bull.stp (build → load → edit → mesh → export .msh + .geo_unrolled/.xao + .brepsave_preprocessload_preprocess), asserting the source file stays byte-identical and that the preprocess archive round-trips the source + edits sidecar into a fresh copy.

Released under the GPL-2.0-or-later License.