Skip to main content
Glama
gkdlwmzkdls-sys

zwcad-kr

README.md
# ZWCAD MCP

An MCP (Model Context Protocol) server that lets Claude read and edit ZWCAD
drawings directly — summarize a drawing, query layers and entities, read
dimensions and text, and create lines, text, and blocks.

**한국어 사용 안내는 [SETUP_KR.md](SETUP_KR.md) 를 보세요.**

This is a fork of [dalingo81/ZWCAD-MCP](https://github.com/dalingo81/ZWCAD-MCP)
(MIT), itself derived from
[puran-water/autocad-mcp](https://github.com/puran-water/autocad-mcp).
It is adapted for **Korean Windows** and hardened for day-to-day use.

---

## Install

Two lines inside Claude Code:

```
/plugin marketplace add gkdlwmzkdls-sys/zwcad-mcp-kr
/plugin install zwcad@zwcad-kr
```

Restart Claude Code afterwards.

The first launch creates a virtual environment and installs dependencies
(about a minute, once). During that window the MCP server may report
**Failed to connect** — this is expected. Wait for it to finish and reconnect;
progress is written to `bootstrap.log` in the plugin directory.

**Requirements**

| | |
|---|---|
| Python | 3.10+, on `PATH` |
| ZWCAD | 2020+ with AutoLISP (Professional or Standard) |
| OS | Windows |

There is also a script installer (`install.ps1` / `설치.bat`) for people who
would rather register the MCP server without the plugin. Use one or the other,
not both — two registrations drive the same ZWCAD and their commands interleave.

---

## How it works

The agent loop runs in Claude; the drawing operations run inside your ZWCAD.

```
Claude ──stdio/JSON-RPC──▶ Python MCP server
                              │ 1. write command JSON to a temp file
                              │ 2. PostMessageW(WM_CHAR) triggers
                              │    (c:zwmcp-dispatch) — no focus steal
                              ▼
                          ZWCAD (running)
                              │ 3. LISP dispatcher reads, executes,
                              │    writes the result JSON
                              ▼
                          Python collects the result
```

Because commands arrive via `PostMessageW`, **ZWCAD never steals focus** — you
can keep working in other windows while the agent edits the drawing.

The LISP dispatcher is loaded automatically. AutoLISP definitions are
per-document, so every newly opened drawing would otherwise need a manual
`APPLOAD`; the server injects the `(load ...)` itself when it finds the
dispatcher missing.

A second backend, **ezdxf**, runs headless without ZWCAD for offline DXF work.
The plugin pins the backend to `file_ipc` so a missing ZWCAD window fails loudly
instead of silently editing an unrelated in-memory document.

---

## Tools

Eight consolidated tools, each with an `operation` argument. Grouping them this
way keeps the always-on schema cost small.

| Tool | Operations |
|---|---|
| `drawing` | open, info, save, save_as_dxf, plot_pdf, purge, get_variables |
| `entity` | **summary**, list, count, get, create (line/circle/arc/polyline/rect/ellipse/mtext/hatch), copy, move, rotate, scale, mirror, offset, array, fillet, chamfer, erase |
| `layer` | list, create, set_current, set_properties, freeze/thaw, lock/unlock |
| `block` | list, insert, insert_with_attributes, get_attributes, update_attribute, define |
| `annotation` | text, linear/aligned/angular/radius dimensions, leader |
| `view` | zoom_extents, zoom_window, get_screenshot |
| `system` | status, health, get_backend, runtime, init, execute_lisp |
| `pid` | P&ID symbol library (optional) |

### Reading a drawing efficiently

Start with `entity(summary)` — it returns a type histogram instead of every
handle. On a 5,504-entity drawing that is 0.2 s versus 30 s, and a fraction of
the response size.

`entity(list)` defaults to `limit: 200` and reports `matched` and `truncated`
so you can page with `offset`. Filter by `layer` and `entity_type` to narrow
before fetching details with `entity(get)`.

---

## Differences from upstream

### Korean (cp949) support

Upstream hardcodes the Chinese locale. The encoding is now derived from the
system ANSI codepage, which fixes Korean, Chinese, and Western installs alike.

This mattered more than a normal bug: cp949 bytes decode as GBK **without
raising**, so Korean layer names and text silently turned into Chinese
characters (`치수선` → `摹荐急`) with no error anywhere. Command JSON is also
written with `ensure_ascii=False`, because the LISP-side parser has no `\uXXXX`
unescaping and would otherwise create a layer literally named `치수`.

### Entity properties

`entity(get)` upstream returns only type, handle, and layer for anything that
is not a LINE or CIRCLE — so text content could not be read at all, which makes
"read the drawing, then change it" impossible. TEXT, MTEXT, INSERT, ARC,
LWPOLYLINE, and DIMENSION are now supported. MTEXT content is reassembled from
its repeated group-3 chunks plus the group-1 tail; reading only group 1
truncates long strings.

### Result size limits

`entity(list)` had no cap. On a real drawing it returned 297 KB in 30 seconds —
far too much to hand to a language model. It now takes `limit` / `offset` /
`entity_type`, and `entity(summary)` was added for the common case.

### Session recovery

Restarting ZWCAD or closing a drawing invalidates the cached window handles.
That used to surface as a timeout requiring a manual re-init. The server now
verifies the handles before dispatching — including the command-line child
window, which dies with the document while the main window survives — and
recovers in about a second.

### Undo is refused, not faked

**Native UNDO does not work through this bridge on ZWCAD 2026.** Dispatching
`(command "_.UNDO" "1")` through LISP returns success while changing nothing,
because the dispatch call is itself part of the undo stream. Posting `_.U` to
the command line does nothing either, and the UNDO Mark/Back sequence leaves
ZWCAD parked at a prompt that swallows subsequent keystrokes.

Upstream returns `ok: true, "undone"` in this situation, which reads as a
working safety net that is not there. `undo` and `redo` now fail explicitly and
`can_undo` reports `false`.

**To roll back a creation**, erase the handle the create call returned:
`entity(operation="erase", entity_id=<handle>)`.

**Property-only edits cannot be rolled back.** Work on a copy before asking for
colour, layer, or size changes on a drawing you care about.

### Safety

The server **never saves on its own**. `drawing(operation="save")` must be
called explicitly, so the file on disk is untouched no matter what the agent
does to the in-memory drawing. `system`'s `readOnlyHint` was corrected to
`false` — it contains `execute_lisp`, which runs arbitrary AutoLISP.

---

## Known limitations

| | |
|---|---|
| Undo | Not supported — see above |
| `view(get_screenshot)` | Returned a black image on this setup; do not rely on it for visual verification |
| Concurrency | One ZWCAD instance only. Do not drive it from two Claude Code sessions at once |
| ZWCAD Mechanical | Not supported (no `ZwmToolKit`); standard ZWCAD only |
| 3D | Solid modelling is out of scope |

**The most common failure is ZWCAD sitting at a command prompt.** Every
keystroke the server sends is then consumed as an answer to that prompt, so even
the dispatcher auto-load fails. Click into ZWCAD and press `ESC` a few times.

---

## Development

```powershell
python -m venv .venv
.venv\Scripts\pip install -e ".[dev]"
.venv\Scripts\python -m pytest -q
```

`claude plugin validate .` checks the plugin and marketplace manifests.

### Syncing with upstream

```bash
git fetch upstream
git merge upstream/main
```

The push URL for `upstream` is deliberately broken so local changes cannot be
pushed to the public repository by accident. On conflicts, re-apply the encoding
changes carefully — reverting them fails silently rather than loudly.

---

## License

MIT. See [LICENSE](LICENSE). Upstream copyright is retained.