OCS WebMCP
by helenkwok
README.md
# OCS WebMCP: agentic DWG/DXF editing in the browser
**[Open CAD Studio](https://github.com/HakanSeven12/OpenCADStudio)'s drawing editor, with its
editing verbs registered as [WebMCP](https://github.com/webmachinelearning/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](NOTICE.md) for its Corresponding
Source.
## 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_recording` → `ocs_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](https://github.com/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](https://rustup.rs) with
`rustup target add wasm32-unknown-unknown`, [trunk](https://trunkrs.dev), 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`).
```sh
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](https://github.com/vercel-labs/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:
```sh
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):
```sh
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](LICENSE)). The deployed combination with Open CAD Studio is
GPL-3.0-only ([NOTICE.md](NOTICE.md)).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues