Control protocol
solvecraft --control PORT (or SOLVECRAFT_CONTROL_PORT=PORT) listens on 127.0.0.1:PORT for
JSON lines. Each request is one line {"id": …, "method": "…", "params": {…}}; each reply is one
line {"id": …, "ok": true, "result": …} or {"id": …, "ok": false, "error": "…"}. Requests are
handled on the UI thread between frames; pointer and keyboard input is injected as real egui
events, one pointer event per frame.
Engine
| Method | Params | Result |
|---|---|---|
engine.execute | command: command id, params: JSON object | the command’s result |
engine.script | commands: [{command, params}, …] | results, stops at the first error |
engine.commands | — | every command: id, label, tab, panel, icon, shortcut, params doc, enabled |
document.inspect | measure?: bool | parameters, timeline (errors, timings), bodies, sketches, selection |
Commands never open dialogs when run this way. solvecraft-cli commands prints the same registry.
UI
| Method | Params | Notes |
|---|---|---|
ui.inspect | — | UI state, viewport rect, camera, active tool/dialog, hover, renderer, timings, live preview (active, busy, error, ms, bodies it stands in for) |
ui.set | any UiState field (tab, perspective, showGrid, hiddenBodies, dark, …) | dark: false switches to the light theme |
ui.view | view: front, back, top, bottom, left, right, iso, home, fit; animate (bool, default false: snap) | |
ui.start | command | like a toolbar click: starts the sketch tool or dialog for the command |
ui.click | x, y (screen points), button?: left/right/middle, shift?, ctrl?, double? | real pointer input |
ui.move | x, y | |
ui.drag | x0, y0, x1, y1, button?, shift?, ctrl?, steps?, hold?, hold_end? | press, move, release; a left drag on the model is a box selection (left to right: window, right to left: crossing); hold keeps the button down that many frames first (a right press-and-hold opens the marking menu, releasing over a direction picks it), hold_end that many frames at the end (a sketch drag shows its snap) |
ui.menu | x?, y?, target?, close? | with a point: open a context menu there as a right-click would (the viewport’s marking menu, or a browser menu with target: {"type": "body", "name"} | {"type": "sketch", "id"} | {"type": "component", "id"}); without: the open menu (radial and items: id, label, shortcut, enabled) |
ui.menuPick | item: id or label | runs an item of the open menu, as clicking it does |
ui.rename | text?, commit? (default true) | finishes the rename box a Rename item opened |
ui.selection | the selection, the open dialog’s inputs, the hovered item, and the active sketch’s drawn dimensions (dimensions: id and text centre, dimension_selected, dimension_editing) | |
ui.worldToScreen | points: [[x, y, z]…] world points | their screen points |
ui.triad | screen points of the 3D sketch move triad’s handles (x, y, z, plane) | |
ui.sketchToScreen | points: [[x, y]…] in active-sketch coordinates | their screen points (null when behind the camera) |
ui.dialogInput | index | what clicking an input’s Select chip does: picks go to that input |
ui.at | world: [x,y,z] | sketch: [x,y] | plane: "XY" | axis: "Z" | dimension: "d1" (its value text) | handle: "arrow" (a manipulator handle, see scenario::publish_handle) | the screen point (a scenario step’s target; see crates/ui-egui/src/scenario.rs) |
ui.handles | prefix? | the handles drawn last frame, [{name, x, y}]: toolbar buttons and drop-down items toolbar:<command id>, panel labels panel:<tab>:<panel>, browser rows row:<label> and fold arrows fold:<label>, manipulator handles |
ui.editFeature | feature (id or name) | what double-clicking a timeline item does: rolls back to it and opens its dialog filled in (sketches open in sketch mode) |
ui.scroll | x, y, delta? | wheel zoom at the cursor |
ui.key | key (egui key name, Enter, Escape…), cmd?, shift? | |
ui.text | text | typed text |
ui.screenshot | path? | PNG of the window (needs a presented frame) |
ui.render | path, width?, height? | CPU render of the model with the current camera (no window needed) |
ui.confirm | accept?: bool | answers the Delete confirmation (shown when a delete takes other features with it or makes some fail); without accept, what it lists |
ui.documents | action?: new, switch, close, quit; index?; force? (close without asking); answer?: save, dont_save, cancel (answers “save changes?”) | the open designs (tabs): names, unsaved flags, the active one, a pending “save changes?” prompt, whether a quit is asking |
ui.home | open?: bool; sample?: index | the start page: show or hide it, open a built-in sample (once built); returns open and the recent designs |
ui.shortcut | command, key?, replace? | a command’s key, or rebinds it ("" clears; a key another command uses is refused unless replace) |
ui.help | item?: about, shortcuts (read only), report (copies diagnostics) | the version, commit, build date and diagnostic text |
ui.prefs | any of default_units, nav (fusion, solidworks, inventor), zoom_reverse, orbit_cursor, msaa, length_decimals, angle_decimals | sets preferences (kept between runs), returns them all |
ui.capture | action?: capture, revert, cancel | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without action, what waits |
ui.window | action: minimize, maximize, restore, toggle, close | what the title bar’s caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first |
ui.agent_cursor | show (bool), speed: instant, normal, slow, follow_camera (bool); all optional | the settings. While the cursor is shown, engine.execute first glides to the command’s toolbar button and to each pick, then runs and replies; later requests wait their turn. "animate": false on a call skips it |
ui.resize | width, height | |
app.quit | force? (quit without asking about unsaved designs) | quitting (the window closes), asking (a “save changes?” prompt is open) |
MCP
solvecraft-cli mcp --connect 127.0.0.1:PORT bridges an MCP client (an AI agent) to this
channel; see mcp.md.
Example
{"id":1,"method":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}
{"id":2,"method":"engine.execute","params":{"command":"sketch.rectangle.two_point","params":{"p0":[0,0],"p1":[40,30]}}}
{"id":3,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}
{"id":4,"method":"engine.execute","params":{"command":"solid.fillet","params":{"edges":[[0,0,10]],"radius":3}}}
{"id":5,"method":"ui.screenshot","params":{"path":"/tmp/part.png"}}