From jarvis-onshape-mcp
Guides Onshape CAD operations via onshape-mcp plugin with protocols for render/entity workflows, units/coordinates, FeatureScript, iteration, and gotchas like REMOVE-on-face auto-flip. Load before any Onshape builds.
npx claudepluginhub adityacodepublic/onshape-cad --plugin jarvis-onshape-mcpThis skill uses the workspace's default tool permissions.
Loaded as context for any Claude session driving the Jarvis Onshape MCP plugin.
Guides Onshape CAD operations via onshape-mcp plugin with protocols for render/entity workflows, units/coordinates, FeatureScript, iteration, and gotchas like REMOVE-on-face auto-flip. Load before any Onshape builds.
Generates Python scripts for FreeCAD CAD: 3D models, parametric objects, Part/Mesh/Sketcher, workbenches, PySide GUIs, Coin3D scenegraph manipulation, macros, and automation.
Generates 3D CAD models from text prompts using AI coding agents with build123d/OpenCascade. Exports STEP/STL/URDF and previews in local React/Vite CAD Explorer viewer.
Share bugs, ideas, or general feedback.
Loaded as context for any Claude session driving the Jarvis Onshape MCP plugin. Encodes the protocols that keep CAD builds from silently failing. Short, imperative.
Before every non-trivial tool call (sketch, extrude, boolean, fillet, chamfer, describe, export), emit a short plan text (1-3 sentences, plain assistant output) saying WHY you're making this call and WHAT you expect to see / build. Example before an extrude:
"Extruding the base rectangle 30 mm up. Expect a flat plate with the four corner arcs intact, bbox (500, 780, 30)."
After each result, also say 1-2 sentences about what actually happened — especially if the result surprised you. If a describe_part_studio shows a feature at the wrong Z, name that out loud before deciding how to fix.
Don't let your only visible output be tool-call JSON. The observer watching the run relies on these thought lines to catch bugs in your reasoning.
Onshape's API works in meters. Two safe ways to pass lengths:
"30 mm", "0.5 in", "15 deg". Always unambiguous; prefer this form.60 means 60 mm.Never assume inches when reading a tool's schema even if the legacy description says "in inches." The builders were ported from an inch-default codebase; the bare-number default is now mm. When a tool response shows unexpected geometry sizes, suspect units first.
This applies on the assembly side too: transform_instance translations, set_instance_position x/y/z, and every create_*_mate / create_mate_connector offset accept bare numbers as mm or explicit unit strings. Slider / cylindrical mate minLimit/maxLimit are lengths (mm-default). Revolute minLimit/maxLimit are ANGLES in degrees (float). get_assembly_positions returns mm in its report text.
corner1: [0, 0] in a Top-plane sketch is on the world origin.When sketching on a picked face (faceId from list_entities), the sketch-local axes are defined by that face's own coordinate system. Geometry you sketch is interpreted in the face's plane, not world space.
After every feature that creates or modifies visible geometry:
describe_part_studio (not individual render_ calls — describe gives structured text + images in one shot).FEATURE TREE with status OK/INFO? Are the expected new faces/edges in BODIES? Does MASS PROPERTIES: volume line up with what you predicted?crop_image on the specific area. Normalized [x1,y1,x2,y2] in [0,1].Text checks catch arithmetic and counting errors (my weak spot). Image checks catch orientation and topology errors. Neither alone is sufficient.
create_sketch has two surfaces. The right one depends on what you're
modeling.
Pass entity dicts without id and no constraints. Positions are what
you type.
{
"entities": [
{"type": "circle", "center": [0, 0], "radius": "25 mm"},
{"type": "circle", "center": [100, 0], "radius": "12 mm"}
]
}
Fast. Use for mounting plates, single holes, obvious rectangles. Don't use this when the drawing specifies tangencies, concentricities, or dimensional chains — the solver makes those easy, hand-computed coordinates don't round-trip cleanly.
Give each entity a user-level id, list constraints. Onshape's
solver resolves positions from the constraints.
{
"entities": [
{"id": "hub", "type": "circle", "center": [0, 0], "radius": "25 mm"},
{"id": "tip", "type": "circle", "center": [100, 0], "radius": "12 mm"},
{"id": "upper", "type": "line", "start": [0, 25], "end": [100, 12]}
],
"constraints": [
{"type": "DIAMETER", "entity": "hub", "value": "50 mm"},
{"type": "DIAMETER", "entity": "tip", "value": "24 mm"},
{"type": "DISTANCE", "entities": ["hub.center", "tip.center"], "value": "100 mm", "direction": "HORIZONTAL"},
{"type": "COINCIDENT", "entities": ["upper.start", "hub"]},
{"type": "COINCIDENT", "entities": ["upper.end", "tip"]},
{"type": "TANGENT", "entities": ["upper", "hub"]},
{"type": "TANGENT", "entities": ["upper", "tip"]}
]
}
Entity refs are ids with optional sub-point suffixes: line.start,
line.end, circle.center, arc.center. Seed positions (center,
radius on circles/arcs; start/end on lines are optional) are just
solver starting guesses — the constraints drive final geometry.
Entity-ref only (no value): HORIZONTAL, VERTICAL (lines only),
COINCIDENT, TANGENT, CONCENTRIC, PARALLEL, PERPENDICULAR,
EQUAL, MIDPOINT.
Dimensioned (require value): DIAMETER, RADIUS,
DISTANCE (with direction: MINIMUM | HORIZONTAL | VERTICAL),
ANGLE (value in degrees default; "90 deg" / "1.57 rad" for units).
Binary pair: OFFSET (offset entity ↔ master; pair with a DISTANCE
constraint on the same two entities for the offset length).
Aliases: HORIZONTAL_DISTANCE → DISTANCE(direction=HORIZONTAL),
VERTICAL_DISTANCE → DISTANCE(direction=VERTICAL), LENGTH →
DISTANCE(direction=MINIMUM) (for line length or slot end-to-end).
POINT_ON is rejected — use COINCIDENT with a point sub-ref.
There's no magic origin keyword. To anchor a sketch to the plane
origin (prevents drift on parametric resize), add an explicit point
entity at [0, 0] and COINCIDENT the geometry you want anchored
to it:
{
"entities": [
{"id": "origin", "type": "point", "at": [0, 0]},
{"id": "hub", "type": "circle", "center": [0, 0], "radius": "25 mm"}
],
"constraints": [
{"type": "COINCIDENT", "entities": ["hub.center", "origin"]},
{"type": "DIAMETER", "entity": "hub", "value": "50 mm"}
]
}
The sketch-local origin point acts as a fixed anchor. Without it, dimensions parametrize fine but positions drift when variables change — hub.center can slide ~1 mm when you retarget dimensions.
hub.center), use DISTANCE(direction=VERTICAL, value="0 mm")
to pin to the horizontal axis.arc specs take start_angle
and end_angle (degrees default; strings "38 deg" / "1.5 rad"
for explicit units). If the CCW sweep from start to end exceeds
180°, the builder silently swaps endpoints so the arc goes the
shorter way — matches Onshape UI's three-point-arc default. Need
the long way? Pass "short_arc": false on the arc entity.circle.start makes no sense
but the builder won't catch it; Onshape rejects at solve time.SKETCH_SOLVE_FAILED /
SKETCH_UNSOLVABLE_CONSTRAINT WARNING. Onshape's REST API does
NOT return per-constraint diagnostics — silence is a platform
limitation. Recovery is client-side bisection: give every
addConstraint an explicit id, and when a solve fails use
edit_sketch with removeIds to drop half the last-added
constraints, re-POST, and binary-search. Typical culprits:
mutually exclusive pairs (PARALLEL + PERPENDICULAR on the same
two lines), redundant positional constraints (COINCIDENT chain
over-specifying endpoints), or tangent-line geometry that forces
an arc into an impossible shape.Don't delete-and-rebuild a sketch. edit_sketch takes a
sketchFeatureId + addEntities / addConstraints / removeIds
and splices.
edit_sketch({
"sketchFeatureId": "Fabc_0",
"addEntities": [{"id": "bore", "type": "circle", "center": [0,0], "radius": "18 mm"}],
"addConstraints": [
{"id": "d_bore", "type": "DIAMETER", "entity": "bore", "value": "36 mm"},
{"id": "c_bore", "type": "CONCENTRIC", "entities": ["bore", "hub"]}
]
})
Removing an entity cascades: any constraint referencing it (directly
or via sub-point) gets auto-dropped and reported in
cascaded_removals: [{constraint_id, referenced}]. Read that field
— otherwise a silent 48→12 constraint scrub bites three turns later.
Never sketch on a face, fillet an edge, chamfer an edge, or mate to a face without calling list_entities or describe_part_studio first and picking the entity by reading its description.
Good picks:
type == "PLANE" and normal_axis == "+Z", then max(by origin[2]).type == "CYLINDER", then by radius.direction_axis in ("+X","+Y","-X","-Y") at z_max.Face/edge IDs are strings like JHK, JNC, JHl. Drop them verbatim into tool args as faceId or edgeIds: ["JHK", ...].
apply_feature_and_check (used by every mutating tool) returns {ok, status, feature_id, feature_name, error_message}. Interpret:
status == "OK" → feature built cleanly.status == "INFO" → Onshape auto-adjusted something. ok=True but READ error_message: common notes include "extrude was through-all auto-clamped" (fine) and "nothing was cut" (bad — you probably got the extrude direction wrong).status == "WARNING" → feature built but Onshape is concerned. Read and decide.status == "ERROR" → feature did not build. Do NOT add more features on top of it. Either fix the parameters and re-POST via update_feature, or delete_feature_by_name and retry.Mate handlers (create_fastened_mate / create_revolute_mate / create_slider_mate / create_cylindrical_mate / create_mate_connector) share the same {ok, status, ...} contract via apply_assembly_feature_and_check. A mate that silently flips an instance still shows up as status="ERROR" or "WARNING" on the mate-level response — no need to visually check every mate just to catch a solver rejection. The 4-mate-for-2-part bracket dogfood burned ~50 turns to the now-fixed prose-return of this path.
Cutting a hole from a +Z face (e.g. sketch on the top of a plate, then REMOVE-extrude to make a hole): the default direction is the sketch normal, which points away from the material (+Z into air). The cut removes nothing; Onshape returns INFO: nothing was cut.
Auto-default (current): when create_extrude sees operationType=REMOVE on a sketch placed on a picked face (any non-standard plane), it now defaults oppositeDirection=true so the cut goes INTO the material. The structured response includes a notes entry like "auto-set oppositeDirection=true because this REMOVE extrude is sketched on a picked face -- cutting INTO the material, not out into air" so you know what got auto-decided.
Override: pass oppositeDirection: false explicitly on create_extrude if you actually want to cut away from the face (e.g. cutting through from underneath into a body that hangs below). The auto-flip only fires when oppositeDirection is omitted from the tool args.
measure(entityAId, entityBId) returns point_distance_m always, angle_deg, parallel/perpendicular flags, and — for the special cases (face-face with parallel normals, face-point) — a projected_distance_m which is the actual geometric distance.
If GEMINI_API_KEY is set, every inspect step in the CAD driver harness auto-routes the render through Gemini for a strict visual review. Output goes to the snapshot .txt file. If Gemini disagrees (missing or wrong features) and you're confident your text checks pass, go look at the render yourself — Gemini can be wrong but is often right when its signal says "I don't see the ø30 housing".
Parametric variables live in a separate Variable Studio element, not in the Part Studio itself. Workflow:
create_variable_studio(...) once per document to create a VS element. Reuse its element id across all variable writes in the same workspace.set_variable(vs_element_id, name, expression) — writes to the VS element, NOT the Part Studio.variableWidth/variableHeight/variableRadius/variableCenter args that reference these names as #name expressions. The variable resolver walks workspace VS elements, so any Part Studio in the same workspace can use them.Trap: GET variables on a Part Studio element id returns [] even when variables are set — you must read from the VS element. If a diagnostic says "no variables" after a set, you're reading the wrong element.
Historical gotcha (fixed 2026-04-17, commit [sketch-vars-face] e6dd198): earlier set_variable used a POST that REPLACED the VS contents, so each call silently wiped prior variables. Now it upserts by name. If you see a sketch WARNING referencing #varname and think "but I just set that variable," verify the VS still contains ALL the variables you expected — not just the most recent one.
Known-broken: variableCenter. The per-axis DISTANCE-from-origin constraint references localFirst: "origin", but Onshape sketches don't expose the Part Studio origin as a local sketch entity, so the payload produces SKETCH_MISSING_LOCAL_REFERENCE (WARNING) and the center is never actually driven by the variable. Investigation: scratchpad/signed-variable-center-investigation.md. Until the helper is fixed to use externalFirst with a query of the Origin feature (or a mirror-pattern helper is built):
variableCenter. It fails silently (sketch still places the seed geometry, so it often looks correct until you change the variable).#hole_offset and #minus_hole_offset variables, set the second to -hole_offset in your VS, and pass them as separate values in each hole's center field: center: ["#hole_offset", "#hole_offset"] for the +,+ corner, ["#minus_hole_offset", "#hole_offset"] for -,+, etc. Bare numbers still work for non-parametric cases.variableRadius path is safe — it uses a RADIUS constraint that resolves correctly.The plugin wraps sketches, extrudes, revolves, thickens, fillets, chamfers, patterns, booleans, mates, shells (create_shell), and offset planes (create_offset_plane). Anything else — threads (ISO, UTS), drafts, lofts, sweeps, helical cuts, patterns along a path, variable-radius fillets — needs FeatureScript via write_featurescript_feature.
Before writing FS from scratch, check docs/fs-cookbook/ in this repo for a proven snippet to adapt. Each cookbook file is a complete defineFeature(...) you can paste into featureScript, change a few numbers, and ship. Direct authorship of less-common ops burns 5-15 turns on opaque REGEN_ERRORs that the cookbook recipes have already worked through. Today's index: helix.fs (threads, springs, augers, helical ribs — avoids the broken opHelix API). Add a recipe when you find a pattern that works after >2 failed attempts.
Tool responses now include a hints list that points at this section; don't ignore it. Three triggers:
create_shell. Offset construction planes DO too — create_offset_plane. Reach for FS only when you need the non-uniform / multi-face version.)set_variable(hole_d_m3) to retarget 8 through-holes and 8 counterbores in one shot, package the pair as a custom feature whose definition takes hole_d + cbore_d parameters. update_feature only tweaks one feature's params; a custom feature lets one variable bump drive the whole chain.Minimal template (copy, adapt, pass as featureScript to write_featurescript_feature):
FeatureScript 2909;
import(path : "onshape/std/geometry.fs", version : "2909.0");
annotation { "Feature Type Name" : "My Custom Feature" }
export const myCustomFeature = defineFeature(function(context is Context, id is Id, definition is map)
precondition
{
annotation { "Name" : "Length" }
isLength(definition.length, LENGTH_BOUNDS);
// Add more parameters here: isAngle, isReal, isInteger, isBoolean, etc.
}
{
// Body of the feature -- call op* primitives from onshape/std.
// opPlane(context, id + "plane1", { "plane" : plane(vector(0,0,1)*definition.length, vector(0,0,1)) });
// opExtrude(context, id + "ext1", { "entities": ..., "direction": ..., "endBound": ..., "endBoundEntity": ... });
});
Call from the MCP layer:
write_featurescript_feature(
documentId, workspaceId, elementId,
feature_type="myCustomFeature",
feature_name="Instance name",
feature_script="<the FS source above>",
parameters=[{"id": "length", "type": "quantity", "value": "15 mm"}],
)
The orchestrator creates a Feature Studio element, uploads the source, pulls the microversion, and instantiates via BTMFeature-134 with the correct e<eid>::m<mv> namespace. You get back a FeatureApplyResult with the usual {status, feature_id, error_message, hints} contract — regen errors in your FS body propagate through exactly the same way as starter-feature errors.
This MCP server exposes many deferred tools. Every time you call a tool that hasn't been loaded, the runtime spends a round-trip loading the schema. Batch-load the tool surface in one ToolSearch call upfront:
ToolSearch(query="select:mcp__onshape__create_sketch,mcp__onshape__create_sketch_rectangle,mcp__onshape__create_sketch_circle,mcp__onshape__create_extrude,mcp__onshape__create_fillet,mcp__onshape__create_chamfer,mcp__onshape__create_shell,mcp__onshape__create_offset_plane,mcp__onshape__list_entities,mcp__onshape__describe_part_studio,mcp__onshape__measure,mcp__onshape__get_mass_properties,mcp__onshape__export_part_studio,mcp__onshape__create_document,mcp__onshape__create_part_studio", max_results=15)
Saves 3-5 individual search calls per session. The multi-entity create_sketch collapses a lot of small cases; prefer it over per-primitive tools.
This is a CAD session, not a script. When a feature builds wrong:
update_feature to tweak parameters if the shape is close. delete_feature_by_name + re-add if it's the wrong shape entirely.