- `--export ... --target-version`, and explicit target_format / target_version on automation and protocol saves; a conversion that would drop unsupported records is refused unless allow_lossy is set. - `audit` read op: dangling layers/blocks, duplicate handles, non-finite bounds, serialization, lossy records and the source DXF handle graph. - `save_verified`: writes the requested version from the same snapshot a normal save writes, reopens it, checks the version and the entity manifest, audits the ASCII DXF handle graph and returns a SHA-256. - ZOOM EXTENTS includes SDF text glyphs. - Live MCP acceptance script and an optional reference-console audit runner under docs/automation. Squashed from the PR with review fixes: merged with the DWT save path, the save snapshot, the tool lists added since, and vendor-neutral harness names; the dated verification record is left out. Co-Authored-By: Claude Opus 5.5 <[email protected]>
8.6 KiB
Driving the native builds
Every native build has three entry points besides the editor window: the MCP
server described in the README (OpenCADStudio --mcp), a headless
JSON-line channel for scripts and CI (OpenCADStudio --serve), and a one-shot
converter (OpenCADStudio --export IN OUT). The browser entry is described in
web.md.
Choosing a channel
--mcp |
--serve |
|
|---|---|---|
| Transport | MCP (JSON-RPC over stdio) with the four tools of the README | one JSON request per line on stdin, one reply per line on stdout; --port N listens on 127.0.0.1:N instead |
| Editor | attaches to a running desktop editor, or starts one when there is none (ocs_sessions, launch_if_none) |
no window; a document session of its own that lives as long as the process |
| Needs a display | yes (xvfb-run works on a headless machine; software rendering may raise the GPU warning dialog) |
no |
Needs a writable HOME / XDG_CONFIG_HOME |
yes — the session descriptors live in the user configuration directory; without one ocs_sessions answers No user configuration directory |
no |
| Startup dialogs | those of the editor (below) | none |
capture |
ocs_capture |
not available (gui_required): there is no window to capture |
| Good for | an MCP client driving a real editor | batch jobs, CI, agents without a display |
--serve
The first line written is a readiness banner, before any request is read:
{"ok":true,"ready":true,"version":"2026.38"}
Over --port N the banner opens every connection; the listener serves one
client at a time and the document session persists across reconnects.
Two request styles share the channel, told apart by the protocol field:
| Style | Shape | Operations |
|---|---|---|
| protocol 1 | {"protocol":1,"request_id":"…","document_id":N,"op":"…"} — the same requests and replies as ocs_execute / ocs_read, described in the README |
new, open, save, save_verified, activate, select, run, start, input, cancel, stop, action, property, set_properties, embed_image, undo, redo; reads state, capabilities, audit, query, entities, records, record_schema, layers, header, properties, measure, history, commands, events, operation |
| legacy | {"op":"…"} without protocol |
new, open, run, entities, audit, query, records, record_schema, capabilities, layers, header, select, save, undo, redo |
The two styles differ in how a waiting command is treated. A protocol-1 run
or start while a command is still active is refused with command_busy:
continue it with input or drop it with cancel. A legacy run line ends the
waiting command and starts the new one.
The legacy run line
{"op":"run","cmd":"LINE 0,0 10,10"} feeds the tokens after the command name
to the prompts in order and then presses Enter. The reply is
{"ok":true,"cmd":"CIRCLE 0,0 5 9","status":"completed","blocked_by":null,"entities":1,"added":1,"unconsumed":["9"]}
| Field | Meaning |
|---|---|
status |
completed, or waiting_input when the line left something open |
blocked_by |
what this line left open: "command", "text_editor", "mtext_editor", "modal:<Kind>", or null. A surface that was already open when the line started is not attributed to it |
entities, added |
entity count after the line, and the difference to before |
unconsumed |
tokens no prompt asked for, in order (["9"] above: the radius step ended the command); [] when everything was consumed |
waiting_input means input is still required, not that nothing happened:
SPLINE and ARC keep accepting input after the entity exists, and
zoom_extents reports it after the view has already moved.
TEXT 0,0 5 0 hello world creates one Text entity holding hello world: the
tail of the line is typed into the in-place editor, committed, and the command
is closed so it does not stay armed for a second line. A TEXT line without
content answers waiting_input with blocked_by: "text_editor".
Known gap (#1390): MTEXT, HATCH and POLYGON answer completed with
added: 0 on this path — they need interactive surfaces (the rich-text editor,
a pattern choice, a boundary pick) that a single line has no tokens to fill.
--mcp
The MCP server is a client of the desktop editor: ocs_sessions lists the live
editor sessions found in the configuration directory and starts one when none
is running, so several MCP connections can share one editor and its state.
A fresh profile opens two dialogs before any drawing exists, and both block document creation:
| Step | ocs_sessions reports |
|---|---|
| right after launch | modal: "AssocPrompt" |
after close_modal |
modal: "DonationPrompt" |
after a second close_modal |
modal: null |
While a dialog is open, {"op":"new"} answers ok but no tab is created.
Close the dialogs first:
while (info := sessions())["modal"]:
execute({"op": "action", "name": "close_modal", "document_id": info["document_id"]})
The donation prompt appears once per application version per profile, so an automated profile meets it once per version rather than on every run.
new makes the new drawing the active document. Requests carry document_id;
take it from the reply's state and pass it on the following requests, as the
README describes.
--export
OpenCADStudio --export IN OUT loads IN and writes OUT without a window or
a session. The output format follows the extension: .dxf writes DXF, any other
extension writes DWG at the document's own version. Add --target-version R14
(or 2000, 2004, 2007, 2010, 2013, 2018) to choose explicitly. The
exit code is 0 on success, 1 when the input/output fails, and 2 for an invalid
target version.
The verified interoperability lane uses version 2000 for a DWG -> DXF
round-trip. R14 DWG files can be written and reopened, but the current R14
DWG -> DXF path can emit object dictionaries that are orphaned from the root,
which leaves layout and placeholder ownership invalid. audit and
save_verified reject that ASCII DXF instead of reporting a false green. Do
not deliver that conversion unless save_verified passes. Raw
group-code handle validation is not applied to binary DXF and is reported as
skipped in the audit result.
Interactive steps
For commands whose answers cannot be written on one line, use start and then
input. start replies with state.command, whose accepts, options and
input_example say what the current step wants:
| Step | Request | Note |
|---|---|---|
| a point | {"op":"input","kind":"point","point":[x,y,0],"space":"wcs"} |
the point is an array; space is wcs (default), ucs or relative |
| a typed value (height, rotation) | {"op":"input","kind":"text","text":"5"} |
digits go through text, not token; {"op":"input","kind":"enter"} accepts the default |
a keyword (J, ST, C) |
{"op":"input","kind":"token","text":"C"} |
the payload field is named text |
| an object | {"op":"input","kind":"entity","handle":"HANDLE","point":[x,y,0]} |
kind: "structure" for a sub-entity pick, kind: "selection" to hand over the current selection |
the content of TEXT |
{"op":"action","name":"text_input","value":"hello"} then {"op":"action","name":"text_commit"} |
the command has already ended; the in-place editor owns the input |
the content of MTEXT |
{"op":"action","name":"mtext_insert","value":"hello"} then {"op":"action","name":"mtext_commit"} |
the rich-text editor has its own actions; the TEXT ones answer editor_closed |
Native-only operations
| Operation | --serve (protocol 1) |
--mcp |
|---|---|---|
| measure | {"protocol":1,"op":"measure","handles":["2A"]} — kernel-computed length, area, bounds and mass properties; no window needed |
ocs_read with op: "measure" and parameters.handles |
| capture | {"protocol":1,"op":"capture","path":"out.png"} — needs the editor window, so it answers gui_required here |
ocs_capture with scope (viewport or window) and max_dimension |
set_properties |
requires collection; colours are the serialised enum — {"Index": 1} or "ByLayer"; a colour name such as "Red" is rejected with invalid_value |
same request through ocs_execute |
| audit | {"protocol":1,"op":"audit","target_format":"dwg","target_version":"2000"} |
ocs_read with op: "audit" and the same target parameters |
| save verified | protocol-1 save_verified with absolute path, unique request_id, and target fields |
ocs_execute with op: "save_verified"; returns hash, reopened version and semantic manifest |