Files
OneCAD-Draft/docs/automation
Afonsop 55e3a6a9e2 merge origin/main: unify our web branch with 277 upstream commits
Conflicts resolved as unions of behavior wherever possible:

- #1350 (set_properties numeric CAS): kept our stricter
  json_values_match (guards u64>f64 precision loss); upstream's
  duplicate removed.
- #1351 (web open-from-bytes): our implementation wins (guarded
  queue, settle semantics, protocol 2) using upstream's validation
  helper so both sides' tests pass.
- #1358/#1359 (Windows printer routing/media): merged both engines —
  upstream's route planning, media tables, GDI raster path and driver
  options on top of our level-2 enumeration fallback, true-default
  resolution, quoted printto params, existence probe, and error
  descriptions.
- #1360 (plot-style leniency): decompress_ctb now carries both our
  CTBVER1/STBVER1 container tags and structured flag, and upstream's
  lenient reader with Windows-1252 handling.
- 3D snapping precedence: upstream's F4 3D osnap system takes
  precedence over our #1030 solid edge wires when the mesh cache has
  geometry edges; our wires remain the fallback for triangle-only
  caches and mesh entities (both test suites pass).
- Fixed upstream-side bugs surfaced by the merge: parametric arc
  grips impl/test contradiction, psetupin test path separators, and
  our pdf_raster regression where the new dpi argument was threaded
  but ignored (GDI 300-DPI would have cached at 150).
- ocs_save repinned to the upstream acadrust rev; docs/command-
  reference.md regenerated (the staleness test caught it).
2026-09-21 13:33:39 +01:00
..

OpenCADStudio MCP control

Every native OpenCADStudio build contains the same MCP server as the editor. OpenCADStudio --mcp starts it over stdio, opens the desktop editor when needed, and exposes the live document without Python, a package manager, a sidecar service, or client-specific code.

MCP lets an AI client inspect the open drawing, execute editor commands, and verify the result through a shared protocol. Install OpenCADStudio, then add a local MCP server in the client and set its command to:

OpenCADStudio --mcp

The client must start that command over standard input/output. If it asks for the executable and arguments separately, select the installed OpenCADStudio executable and enter --mcp as its only argument. The exact registration screen or configuration location belongs to the client. Once connected, the four tools below should appear in the client's MCP tool list.

The server provides four tools:

  • ocs_sessions finds running editor sessions and opens OpenCADStudio when none exists.
  • ocs_read discovers capabilities and reads document state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events, and operation status.
  • ocs_execute performs one operation, an atomic record update, or a sequential batch against the real editor.
  • ocs_capture returns a bounded PNG of the drawing viewport or complete window.

Native builds also accept a second, headless entry point (OpenCADStudio --serve, one JSON request per line) for scripts and CI; the two native channels, their startup dialogs and their limits are described in native.md.

The normal flow is to call ocs_sessions, pass its returned session_id as ocs_session_id to the other tools, read the chosen session with ocs_read, then pass the returned document state into ocs_execute. For example, an undo request has this shape:

{
  "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
  "request": {
    "op": "undo",
    "request_id": "undo-1",
    "document_id": 1,
    "revision": 12
  }
}

Call ocs_read with op: "capabilities" before unfamiliar work. It reports the command, geometry, transaction, capture, and database facilities supported by the running build. The records.collections list gives every available database collection, its record count, and whether it can be edited.

Call ocs_read with op: "record_schema" before editing an unfamiliar record. With no parameters it lists the complete generated type registry. A collection returns the record types accepted by that collection even when the current drawing has no instance of a type. Supplying both collection and type returns the type's complete dependency graph, flattened property paths, JSON types, optional and sequence markers, enum variants, integer bounds, unambiguous unit annotations, identity fields, and write rules:

{
  "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
  "op": "record_schema",
  "parameters": {
    "collection": "entities",
    "type": "Insert"
  }
}

Sequence fields include an item_path_template such as /attributes/{index}. Replace {index} with an index returned by records before using the path in set_properties. When a matching record exists, runtime_schema adds its exact JSON shape and bounded scalar examples. The embedded registry is generated from all 48 entity variants, all 36 non-graphical object variants, symbol-table records, header data, summary data, and the remaining stored record roots at build time.

op: "records" returns every serializable property of entities, non-graphical objects, symbol-table records, the complete header, document metadata, and derived database views. Omitting collection returns the same collection manifest; use collection: "all" for a paged query across the complete database. Records have a stable collection plus a handle or name, a type, and properties. Entity records also include their display and file record type names.

Property filters and projections use RFC 6901 JSON Pointer paths relative to properties. Multiple where filters are combined with AND. Supported operators are eq, ne, lt, lte, gt, gte, contains, starts_with, ends_with, in, exists, and not_exists:

{
  "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
  "op": "records",
  "parameters": {
    "collection": "entities",
    "type": "Insert",
    "where": [
      {"path": "/common/layer", "value": "Equipment"},
      {"path": "/attributes/0/tag", "value": "COMPANY"}
    ],
    "paths": ["/block_name", "/insert_point", "/attributes"]
  }
}

Use set_properties to replace one or more fields atomically. OCS verifies the record identity, JSON types, optional compare-and-set values, layer locks, document revision, and request ID before committing one undoable edit. Handles, ownership slots, table names, and other database identity fields are read-only; their dedicated editor commands preserve cross-record references.

{
  "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
  "request": {
    "op": "set_properties",
    "request_id": "attributes-1",
    "document_id": 1,
    "revision": 12,
    "collection": "entities",
    "handle": "2A",
    "updates": [
      {"path": "/attributes/0/value", "expected": "BPH", "value": "Updated"},
      {"path": "/insert_point/z", "value": 3.0}
    ]
  }
}

The same operation edits entries in objects, every symbol table, header, and summary_info. Collections marked mutable: false are derived or storage metadata and remain queryable. Their source records must be changed so OCS can rebuild those views consistently.

To discover command syntax without reading application source, call ocs_read with op: "commands". The unfiltered response lists commands and actions. Add parameters: {"name": "PLINE"} for a command's batch examples and interactive guidance.

A complete command can be sent with run. Prompt answers are separated by spaces, points use x,y or x,y,z, and option answers use their displayed token:

{
  "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
  "request": {
    "op": "run",
    "request_id": "polyline-1",
    "document_id": 1,
    "revision": 12,
    "cmd": "PLINE 0,0 10,0 10,10 C"
  }
}

Use batch when the steps are already known. OCS supplies each step with the state produced by the previous one and stops on the first failure. completed_steps and next_step show exactly what committed:

{
  "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
  "response_detail": "changed_entities",
  "request": {
    "op": "batch",
    "request_id": "shape-1",
    "steps": [
      {"op": "run", "cmd": "LINE 0,0 10,0"},
      {"op": "run", "cmd": "CIRCLE 5,5 2"}
    ]
  }
}

Execute responses use response_detail: "compact" by default and return only the state needed for the next edit. Use changed_entities to receive the current geometry of affected handles in the same response, or full when the complete editor state is needed.

ocs_read query accepts exact handles, type/layer filters, field projection, world-XY bounds, nearest-curve ranking, closed-curve containment, and exact intersections between two planar curves. detail: "full" adds the complete serialized entity properties. Nearest points, containment, curve length, area, and intersections are calculated by the geometry kernel:

{
  "ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
  "op": "query",
  "parameters": {"intersections": ["2A", "31"]}
}

For unfamiliar or conditional commands, use start, then inspect state.command in every response. Its accepts array gives the valid MCP input kinds, options gives the current tokens, and input_example gives the next request shape. Add the current state fields and a new request_id to each step.

Every ocs_execute request requires a caller-generated request_id. Reuse that ID only to retry the identical request after a timeout, together with the same session ID, document ID, expected revision, and selection. Commands report waiting_input while more input is required and asynchronous work remains running until its real callback finishes.

A run reply also carries two fields that describe what happened to the line:

  • blocked_by — null when the command finished, otherwise what this line left open and waiting: "command" (an interactive command), "text_editor" (the in-place editor opened by the TEXT content step), "mtext_editor", or "modal:<Kind>" for a dialog. A surface that was already open when the line started (an earlier line's editor, another tab, a startup dialog) is not attributed to this line, so a caller polling for completed is never stuck waiting on someone else's editor. blocked_by is set whenever status is waiting_input.
  • unconsumed — the tokens of the line that no prompt ever asked for, in order. A fully consumed line reports []; CIRCLE 0,0 5 9 reports ["9"] because the radius step ends the command. Over-typed input is reported instead of being dropped on the floor.

When a line ends at the TEXT content step (TEXT 0,0 5 0 hello) the text is typed into the in-place editor and committed, so the line behaves exactly as the same input typed into the GUI command line would: one Text entity, nothing unconsumed, and the command closed rather than left armed for a second line. Geometry stays in OpenCADStudio and its geometry kernel.

ocs_capture defaults to the drawing viewport and a longest edge of 1600 pixels. Set scope to window for the full interface or change max_dimension between 256 and 4096.

Clients using MCP 2026-07-28 can advertise io.modelcontextprotocol/tasks. OCS then returns a standard task handle when an operation is still running and accepts tasks/get, tasks/update, and tasks/cancel. Other clients continue to receive the existing operation status and can read it with ocs_read.

The source-tree protocol smoke test treats the executable as a black box and uses only the Python standard library. It checks both supported protocol styles, the published schemas, structured errors, and the four-tool surface:

cargo build
python3 docs/automation/mcp_smoke.py target/debug/OpenCADStudio

The repeatable live-editor evaluation draws three isolated entities in one batch, verifies exact intersections, nearest geometry and kernel measurements, removes the entities, and reports call count, elapsed time and wire bytes:

python3 docs/automation/mcp_eval.py target/debug/OpenCADStudio

Web automation

The web build exposes the same control protocol (protocol 2; protocol 1 requests are still accepted unchanged) through two wasm exports on window.wasmBindings:

  • ocs_control_submit(requestJson) returns a ticket id immediately.
  • ocs_control_take(ticketId) returns the JSON reply once the app has settled the request, or null while it runs.

web/ocs-control.js (loaded by web-app.html) wraps that pair in a small promise API:

await window.ocsControl.ready;              // resolves when the bridge exists
window.addEventListener("ocs-ready", () => {}); // ...or listen for the one-shot event
const state = await window.ocsControl.request({op: "state"});
const reply = await window.ocsControl.request({
  op: "run", request_id: "line-1",
  document_id: state.document_id, revision: state.revision,
  cmd: "LINE 0,0 10,0",
});

request resolves with the final reply, following an accepted/running operation through op: "operation" until it completes, fails, or waits for input. It rejects on timeout (default 120 s, override with {timeoutMs, pollMs}) or when the bridge is missing.

Opening an in-memory drawing (web only)

The open operation accepts base64 bytes instead of a path, so automation can open drawings it already holds in memory — no OPFS copy, no file picker. The bytes go through the same guarded open queue drag-and-drop uses:

const bytes = new Uint8Array(await file.arrayBuffer());
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
await window.ocsControl.request({
  op: "open", request_id: "open-1", name: "plan.dwg",
  data_base64: btoa(binary),
});

The operation stays pending until the open settles; if another open holds the slot, the reply reports result.queued and the poll opens the drawing as soon as the slot frees. state.capabilities lists open_bytes on the web build. Native builds keep the path-based open and reject data_base64 with invalid_request.

Startup prompts during automation

The first control request of a session marks it automated: a donation prompt that has not been shown yet is dropped, and no new one is queued while control stays enabled. A prompt already on screen is left for its reader — close it with {"op":"action","name":"close_modal"}. A web host that intends to automate from the first page load can set localStorage["opencadstudio.automation"] = "1" before loading the app so the prompt never appears (the same pre-load mechanism the settings use). The web build is driven through two JavaScript functions instead of a process; the protocol details are summarised above and in web.md.