Skip to main content
Glama
helenkwok

OCS WebMCP

by helenkwok

OCS WebMCP: agentic DWG/DXF editing in the browser

Open CAD Studio's drawing editor, with its editing verbs registered as WebMCP tools, so an AI agent in the browser can inspect and edit DWG/DXF drawings. Every change waits for a human.

Ask for "draw a 100 × 50 rectangle on layer WALLS" and the agent calls the editor's own command engine (PLINE 0,0 100,0 100,50 0,50 C). It is the same engine Open CAD Studio's desktop MCP server drives, running in the browser tab.

What is ours, and what is not

This repository (MIT)

The WebMCP layer (src/): 24 tools, the registration gate, the human confirm dialog, the activity trail, the control-channel client. The shell (shell/). The build and test scripts (scripts/).

Not ours (GPL-3.0-only)

The CAD application itself: Open CAD Studio by Hakan Seven and contributors, including its DWG/DXF engine. Built unmodified from a pinned upstream commit; no upstream file is edited.

The combined deployment is a GPL-3.0 work. See NOTICE.md for its Corresponding Source.

Related MCP server: autocad-mcp

How it works

 browser tab (document.modelContext: 24 tools)
 ┌──────────────────────────────────────────────┬──────────────────┐
 │ /  shell  (MIT)                              │ agent activity   │
 │   registers tools → gate → confirm dialog     │  (every call)    │
 │   ┌──────────────────────────────────────┐   │                  │
 │   │ /ocs/  Open CAD Studio (GPL-3,        │   │                  │
 │   │        unmodified, same-origin iframe)│   │                  │
 │   │   window.wasmBindings                 │   │                  │
 │   │     .ocs_control_submit / _take  ◄────┼── src/control.js    │
 │   └──────────────────────────────────────┘   │                  │
 └──────────────────────────────────────────────┴──────────────────┘

Open CAD Studio's web build compiles the same semantic control core as its native MCP server, and exports it to JavaScript as ocs_control_submit / ocs_control_take. The shell reaches those exports in a same-origin iframe, so nothing upstream needs a plugin hook or a patch.

Tools

Read (no confirm): ocs_get_state · ocs_get_capabilities · ocs_count_entities · ocs_query_records (filters, JSON-Pointer projections, paging) · ocs_get_record_schema · ocs_list_commands (vocabulary, or one command's exact prompts) · ocs_get_history · ocs_measure (bounds, curve length and area, solid/mesh mass properties) · ocs_spatial_query (nearest to a point, containing a point, inside a box, by type/layer; or two entities' intersections: how an agent finds the handles to pick)

See (no confirm): ocs_capture_view (screenshot: JPEG/PNG, max_width, if_changed + threshold to skip unchanged views and save tokens) · ocs_set_view (zoom_extents / home, preset views top, iso_se/iso_sw/iso_ne/iso_nw, front/back/right/left, and a visual style such as shaded_with_edges; never the drawing's geometry)

Preset views: the web build has no command or control action for them, so ocs_set_view clicks Open CAD Studio's ViewCube the way a user would (src/viewcube.js): it finds the cube in a captured frame, clicks the position upstream's own hit test maps to that view, and checks the camera actually reached the expected pitch before reporting success. Visual styles go through start with the whole line (VSCURRENT <style>): through run, upstream's keyword picker announces the style but drops the message that applies it.

Record (starting it waits for a human): ocs_start_recordingocs_stop_recording. The human gets the video (MP4, or WebM where MP4 can't be recorded) as a download in the activity panel. The agent gets a contact sheet: one image of the frames that changed, with timestamps and the changed region outlined. An agent can't watch a video, but it can read a contact sheet.

Write (each waits for a human): ocs_new_drawing · ocs_open_drawing (by url for real drawings; tool arguments are size-limited, e.g. agent-browser caps them at 1 MB; or base64/DXF text for small files) · ocs_add_text (single-line note; TEXT's in-canvas editor can't be filled by a command line) · ocs_run_command · ocs_cancel_command · ocs_set_properties · ocs_undo · ocs_redo · ocs_command_steps (a command answered prompt by prompt, with entity picks by handle: FILLET, TRIM, OFFSET, MOVE on a selection, and geometric constraints on builds that have them; reports the handles added, removed and modified, since such edits replace entities) · ocs_set_layer (visible / frozen / locked / current, set rather than toggled) · ocs_batch (several of the edits above under ONE approval; the dialog lists every step; stops at the first failure)

The editor reports what each prompt accepts; ocs_command_steps refuses a step of another kind before sending it. One exception: on builds after v2026.37 a point step that also takes keyword letters (MOVE/COPY's base point with [Displacement]) is reported as accepting only token, so a point is let through wherever a token is.

Not possible through automation yet: creating a layer (no command, action or record op for it upstream).

Screenshots and recording: how

  • Frames come from canvas.captureStream(), never toDataURL(). The editor renders through WebGL without a preserved drawing buffer, so toDataURL() from outside the render loop is all black (measured: mean 0, spread 0). Stream frames are the composited output. The web build's own capture op answers gui_required.

  • Change detection counts changed pixels per tile at 480 px, not mean change. CAD is thin lines on a flat background, and averaging dilutes a 1-px line to nothing (a new circle measured 0% that way).

  • MP4 by default: Chrome's MP4 has a real duration (2.03 s measured on a 2 s take) and plays in QuickTime/Keynote. Its WebM has no duration header (ffprobe: N/A).

  • The contact-sheet and if_changed design follows vercel-labs/agent-browser's recording and screenshot options (Apache-2.0), reimplemented in-page. No code copied.

Safety model

  • One gate (src/gate.js): every tool is registered through it, and there is no path around it. Writes stop at a confirm dialog before anything reaches the editor. A declined call returns REFUSED and never runs.

  • Confirms are serialised, so approving one dialog is never consent to a second one.

  • Optimistic concurrency: writes quote the drawing's revision, and Open CAD Studio itself refuses a stale one. Agents may pass the revision they last read; ocs_set_properties supports per-field expected values, compared by value.

  • Output is capped, and truncation is announced, never silent. All tools carry untrustedContentHint, because drawing content (text, layer names) reaches the agent verbatim.

  • Recording needs consent. It changes nothing in the drawing, but it continuously captures the user's screen content, so starting it is confirmed like a write, and a red REC indicator stays visible until it stops. A max_seconds limit (default 120) always ends it.

  • Only Open CAD Studio's donation prompt is dismissed automatically, and only on a path the human has already approved. Any other dialog is reported to the agent, not closed.

Build and run

Requirements: Node ≥ 22, git, rustup with rustup target add wasm32-unknown-unknown, trunk, and wasm-bindgen-cli at exactly the version in upstream's Cargo.lock (0.2.108 at the pinned commit: cargo install wasm-bindgen-cli --version 0.2.108 --locked).

npm run build      # clone upstream at the pinned commit, build it unmodified, assemble dist/
OCS_COMMIT=<sha> npm run build   # same, at another upstream commit (dist/ocs-source.json: pinned=false)
npm run serve      # http://127.0.0.1:8787/ with the COOP/COEP headers upstream expects
npm run test:e2e   # 56 checks through real WebMCP in headless Chrome (57 where constraints exist)
node scripts/open-file.mjs plan.dwg   # open a real DWG/DXF through the tools; report + zoomed screenshot
node scripts/demo.mjs plan.dwg        # record a demo video, driven through agent-browser (needs ffmpeg)

Real-file check, 2026-09-18: a 2.6 MB AutoCAD 2007 DWG (AC1021, a canteen from dwgmodels.com) opened through ocs_open_drawing in 4.7 s. It had 29,212 entities (24,947 lines, 750 block references, 279 hatches, 216 MTexts, 141 3D solids, …), 14 layers and 110 blocks, and rendered correctly after ocs_set_view. The web build does not draw hatches (an upstream WebGL2 limitation), but they are in the data and can be queried.

Driving it from an agent: agent-browser

agent-browser enables WebMCP in the Chrome it manages, so it works as the agent-side client with no flags. Verified 2026-09-18 with agent-browser 0.37.1:

npm run serve &
agent-browser open http://127.0.0.1:8787/
agent-browser webmcp list                                   # all 24 tools
agent-browser webmcp invoke ocs_get_state
agent-browser webmcp invoke ocs_capture_view --params '{"max_width":900}'   # returns image/jpeg
# a write blocks on the human's dialog, so detach it and collect the result after approval:
agent-browser webmcp invoke ocs_run_command --params '{"cmd":"PLINE 0,0 100,0 100,50 0,50 C"}' --detach
agent-browser webmcp result <invocation-id>                 # "pending" until approved

WebMCP is behind a flag in Chrome: enable chrome://flags/#enable-webmcp-testing (headless: --enable-features=WebMCPTesting).

Running a whole task with any agent: scripts/agent-run.mjs

scripts/agent-run.mjs gives any agent that can run shell commands a task and the tools. It serves the editor and opens it in an agent-browser session. With --pdf, it also opens a drawing in Chrome's PDF viewer in a second session, so the agent needs nothing but agent-browser to read it. It then writes the agent a prompt explaining how to reach both sessions, runs the agent command you give it, and records the run.

A stand-in human answers every confirm dialog and logs exactly what each one showed (approvals.json). It approves everything unless the request matches --decline <regex>. It is for demos and tests. It does not replace a person reviewing edits.

With --record, the videos are:

  • run.mp4: the drawing on the left, as the agent views it, and the editor on the right. Idle stretches are compressed.

  • run-editor.mp4: the editor alone, starting at the first edit.

--start-blank dismisses Open CAD Studio's donation prompt and opens a blank drawing before the agent starts.

Both of these were verified on 2026-09-19. The task was to rebuild a 7th-floor plan from a PDF of one sheet as a 3D model (grid, columns and walls, extruded to 3600 mm):

npm run build   # once

# Claude Code (headless)
node scripts/agent-run.mjs task.md --pdf sheet.pdf --start-blank --record -- \
  sh -c 'cd "$(dirname "$OCS_PROMPT_FILE")" && claude -p "$(cat "$OCS_PROMPT_FILE")" \
         --allowedTools "Bash(agent-browser:*)" Read'

# Codex CLI
node scripts/agent-run.mjs task.md --pdf sheet.pdf --start-blank --record -- \
  sh -c 'cd "$(dirname "$OCS_PROMPT_FILE")" && codex exec --skip-git-repo-check \
         --sandbox workspace-write -c sandbox_workspace_write.network_access=true \
         -c "sandbox_workspace_write.writable_roots=[\"$HOME/.agent-browser\"]" \
         "$(cat "$OCS_PROMPT_FILE")" < /dev/null'

Notes on the two runs:

  • Both got the grid exactly right. Codex also matched the sheet's overall 29,600 × 15,400 mm footprint and column sizes, in 5 approvals and 9.5 minutes. Claude Code (Sonnet) took 8 approvals and 17 minutes.

  • What each agent is allowed to do: Claude Code may run agent-browser and read files (it looks at its own screenshots). Codex runs in its workspace-write sandbox with network access, plus agent-browser's socket folder ~/.agent-browser made writable.

  • Codex's sandbox cannot launch Chrome. That is why the harness starts both browser sessions and the agent only connects to them.

  • Other agents: any other agent that can run shell commands plugs in the same way. Pass its command after --; it reads the prompt from $OCS_PROMPT_FILE.

  • Writes wait for the human. agent-browser's CLI gives up on a call after 25 s, so the prompt tells the agent to use --detach for writes and collect the result with webmcp result.

macOS + Homebrew rustup: the rustup proxies live in /opt/homebrew/opt/rustup/bin, which is not on PATH by default. Calling a toolchain's cargo directly makes rust-lld fail to load libLLVM.dylib. scripts/vendor.mjs finds the proxies itself.

Why this upstream commit

95bad2a3 is exactly what the official web app at opencadstudio.com/app was built from (/site-version.txt): release v2026.37 plus one web hotfix. As of 2026-09-18, the bare release tag and upstream main both failed to compile for wasm32 (desktop-only calls in shared code). Upstream fixed main on 2026-09-18 (#1347). The pin moves to the first release after that.

Upstream internals this depends on

src/control.js is the only file that touches them:

  1. window.wasmBindings, which is the Trunk loader's default, not a documented API;

  2. polling ocs_control_take for replies;

  3. opening a file, on builds before upstream #1351: writing it into the app's OPFS "recent files" cache, then open by name. Builds with #1351 (merged 2026-09-18) take the file itself ({"op":"open","name":…,"data_base64":…}). openBytes() tries that first and falls back when the app answers "Missing path"; ocs_open_drawing reports which route it used (open_method), and the e2e checks it matches the build.

A documented promise-based entry point and no donation prompt while automation drives are proposed upstream (#1349). When they land, only src/control.js changes.

Licence

MIT for this repository (LICENSE). The deployed combination with Open CAD Studio is GPL-3.0-only (NOTICE.md).

Related MCP Connectors

Related MCP Servers