From dfb21a734a4699d2a82efdf013383ee7e38a8843 Mon Sep 17 00:00:00 2001 From: Umutcan Edizsalan Date: Sat, 20 Jun 2026 11:41:36 +0300 Subject: [PATCH] =?UTF-8?q?feat(selection):=20add=20selection=5Fget=20?= =?UTF-8?q?=E2=80=94=20read=20live=20viewport=20pickfirst=20selection=20(#?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New COM-only Entity Query tool that reads AutoCAD's implied ("pickfirst") selection set and returns the selected entity handles + EntityInfo, so the AI can scope work (e.g. dimension_auto) to exactly what the user picked in the viewport instead of the whole drawing. Resolves #1 — the layer-juggling workaround is no longer needed. - backends/base.py: abstract selection_get() returning a status dict - backends/com_backend.py: reads ActiveDocument.PickfirstSelectionSet, surfaces the PICKFIRST sysvar, handles the empty-selection case - backends/ezdxf_backend.py: headless stub (ok=False, empty handles, same shape) - server.py: @mcp.tool registration (readOnlyHint), SECTION 5 3 -> 4 tools - README + CHANGELOG + pyproject: 110 -> 111 tools, v1.1.0 -> v1.1.1 Verified live on AutoCAD 2027 (COM): selection_get correctly returned the user's grip selection across two distinct picks; 318 tests pass, ruff-clean. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 14 +++++++++ README.md | 11 +++---- backends/base.py | 20 +++++++++++++ backends/com_backend.py | 60 +++++++++++++++++++++++++++++++++++++++ backends/ezdxf_backend.py | 13 +++++++++ pyproject.toml | 2 +- server.py | 38 ++++++++++++++++++++++++- 7 files changed, 151 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42deeb3..ec1969a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 _Next: 1.2 — closed-loop validation moat (scalar drawing-score, pre-plan clarification, iterative refiner + transaction-stack isolation)._ +## [1.1.1] — 2026-06-20 + +### Added +- **`selection_get`** (Entity Query, COM backend): reads AutoCAD's implied + ("pickfirst") viewport selection — the entities the user highlighted with + grips before invoking the AI — and returns their handles + `EntityInfo`. + This lets the AI scope work to exactly what the user picked + (`dimension_auto(selection_get()["handles"])`) instead of acting on the whole + drawing. Resolves [#1](https://github.com/U-C4N/Autocad-MCP/issues/1) — the + layer-juggling workaround is no longer needed. Surfaces the `PICKFIRST` sysvar + state so an empty selection is self-explanatory. The ezdxf headless backend + has no viewport, so it returns `ok=False` with an empty handle list (same + shape, never raises). Tool count: 110 → 111. + ## [1.1.0] — 2026-06-19 Correctness, cross-backend parity, and an **enforced** quality gate, landed across four audited diff --git a/README.md b/README.md index 8bd48af..0793ffe 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **Production-grade AutoCAD automation for the Model Context Protocol.** Dual-engine. Battle-tested. Model-agnostic. -v1.1.0 · 110 tools · 5 resources · 5 prompt templates · COM + ezdxf backends · Python 3.11+ · MIT +v1.1.1 · 111 tools · 5 resources · 5 prompt templates · COM + ezdxf backends · Python 3.11+ · MIT --- @@ -15,7 +15,7 @@ My day job at **Anka-Makine** revolves around intensive AutoCAD work — product After months of leaning on this server inside my own daily workflow — and watching it shave hours off my week without a single misstep on critical drawings — I decided it deserved a public release. It works. It is fast. It stays out of the way. -The model on the other end of the wire does not matter. The Model Context Protocol is the contract; AutoCAD MCP Pro is one well-typed implementation of that contract. Any MCP-aware client — and the LLM behind it — sees the same 110 tools. +The model on the other end of the wire does not matter. The Model Context Protocol is the contract; AutoCAD MCP Pro is one well-typed implementation of that contract. Any MCP-aware client — and the LLM behind it — sees the same 111 tools. I will keep this repository actively maintained as my own use of it evolves, and **a public benchmark suite is on the way** — I want to give you numbers, not adjectives. @@ -63,11 +63,12 @@ AutoCAD MCP Pro takes a different stance: - **ezdxf backend**: headless DXF file operations powered by [ezdxf](https://github.com/mozman/ezdxf). Works on every platform, ideal for batch workloads and CI pipelines. - Automatic backend selection, with a clean override via `AUTOCAD_MCP_BACKEND`. -- **110 Tools, 12 Categories** +- **111 Tools, 12 Categories** - Drawing management — `drawing_new`, `drawing_open`, `drawing_save`, `drawing_save_as`, `drawing_export_dxf`, `drawing_export_pdf`, `drawing_purge`, `drawing_audit`, `drawing_undo`, `drawing_redo`, `drawing_close` - Entity creation — line, circle, arc, polyline, rectangle, text, mtext, hatch, spline, ellipse, point, block reference, batch create - Dimensions — linear, aligned, angular, radius, diameter - Entity modification — move, copy, rotate, scale, mirror, offset, delete, rectangular array, polar array, batch modify, set properties + - Entity query — `entity_get`, `entity_list`, `entity_delete_many`, and `selection_get` (read the user's live viewport "pickfirst" selection so the AI dimensions/edits only what was picked, not the whole drawing — COM backend) - Layer management — full lifecycle: create, delete, modify, freeze/thaw, lock/unlock, hide/show, isolate, set current - Block operations — list, insert, explode, attribute get/set, create-from-entities, find references - Analysis — entity stats, region select, distance/area measurement, bounding box, select by type/layer, layer statistics @@ -124,7 +125,7 @@ pip install -e ".[full]" python server.py ``` -That is it. The server starts in STDIO mode and your MCP client will discover all 110 tools. +That is it. The server starts in STDIO mode and your MCP client will discover all 111 tools. For headless / CI workflows: @@ -243,7 +244,7 @@ Copy `.env.example` to `.env` and edit: | Entity Creation | 13 | `entity_create_line`, `entity_create_polyline`, `entity_create_hatch`, `entity_create_block_ref` | | Dimensions | 5 | `dimension_linear`, `dimension_aligned`, `dimension_angular`, `dimension_radius`, `dimension_diameter`| | Entity Modification | 10 | `entity_move`, `entity_rotate`, `entity_array_polar`, `entity_offset`, `entity_array_rectangular` | -| Entity Query | 3 | `entity_get`, `entity_list`, `entity_delete_many` | +| Entity Query | 4 | `entity_get`, `entity_list`, `entity_delete_many`, `selection_get` | | Layer Management | 12 | `layer_create`, `layer_freeze`, `layer_isolate`, `layer_modify` | | Block Operations | 7 | `block_insert`, `block_explode`, `block_create_from_entities`, `block_find_references` | | Analysis | 8 | `analysis_entity_stats`, `analysis_bounding_box`, `analysis_select_by_layer`, `analysis_layer_stats` | diff --git a/backends/base.py b/backends/base.py index a2fb030..8a0d724 100644 --- a/backends/base.py +++ b/backends/base.py @@ -487,6 +487,26 @@ class AutoCADBackend(ABC): @abstractmethod async def analysis_select_by_type(self, entity_type: str) -> list[EntityInfo]: ... + @abstractmethod + async def selection_get(self) -> dict: + """Read the live viewport's implied ("pickfirst") selection set. + + COM-only / meaningful only with live AutoCAD. Returns a dict:: + + { + "ok": bool, # True on COM even for an empty pick + "count": int, + "handles": list[str], # entity handles to act on + "entities": list[EntityInfo], # _dc()-converted by the server + "pickfirst": bool | None, # state of the PICKFIRST sysvar + "message" / "error": str, # optional guidance / failure reason + } + + The ezdxf headless backend has no viewport, so it returns ``ok=False`` + with an empty ``handles`` list (same shape, never raises). + """ + ... + # ── view / screenshot ──────────────────────────────────────────────────── @abstractmethod async def view_zoom_extents(self) -> dict: ... diff --git a/backends/com_backend.py b/backends/com_backend.py index e986c0a..6efc415 100644 --- a/backends/com_backend.py +++ b/backends/com_backend.py @@ -1444,6 +1444,66 @@ class ComBackend(AutoCADBackend): return results return await self._run(_sync) + async def selection_get(self) -> dict: + def _sync(): + doc = _acad_doc() + + # PICKFIRST controls whether the noun/verb (grip) pre-selection is + # captured at all. Surface it so an empty result is explainable. + try: + pickfirst = bool(doc.GetVariable("PICKFIRST")) + except Exception as exc: + log.debug("selection_get: PICKFIRST read failed: %s", exc) + pickfirst = None + + try: + ss = doc.PickfirstSelectionSet + except Exception as exc: + log.debug("selection_get: PickfirstSelectionSet unavailable: %s", exc) + return { + "ok": False, + "error": f"PickfirstSelectionSet unavailable: {exc}", + "count": 0, + "handles": [], + "entities": [], + "pickfirst": pickfirst, + } + + try: + count = int(ss.Count) + except Exception as exc: + log.debug("selection_get: Count read failed: %s", exc) + count = 0 + + entities = [] + for i in range(count): + try: + entities.append(_entity_info(ss.Item(i))) + except Exception as exc: + log.debug("selection_get: skip selection item %d: %s", i, exc) + continue + + handles = [e.handle for e in entities] + result = { + "ok": True, + "count": len(handles), + "handles": handles, + "entities": entities, # _dc()-converted at the server layer + "pickfirst": pickfirst, + } + if not handles: + result["message"] = ( + "No entities are pre-selected in the AutoCAD viewport. " + "Select entities (so grips appear) before calling, and " + "ensure PICKFIRST=1." + if pickfirst is not False + else "PICKFIRST is 0; the noun/verb grip selection is disabled. " + "Set PICKFIRST=1 (system_set_variable) and re-select." + ) + return result + + return await self._run(_sync) + # ── view / screenshot ────────────────────────────────────────────────────── async def view_zoom_extents(self) -> dict: diff --git a/backends/ezdxf_backend.py b/backends/ezdxf_backend.py index bbf0fd0..94c7ac5 100644 --- a/backends/ezdxf_backend.py +++ b/backends/ezdxf_backend.py @@ -1466,6 +1466,19 @@ class EzdxfBackend(AutoCADBackend): ] return await self._async(_sync) + async def selection_get(self) -> dict: + # COM-only: headless ezdxf has no viewport / pickfirst grip selection. + # Return a not-supported result but keep the same shape (empty handles) + # so callers that iterate the selection do not KeyError. + return { + "ok": False, + "error": "selection_get not supported in ezdxf backend (no live AutoCAD viewport)", + "count": 0, + "handles": [], + "entities": [], + "pickfirst": None, + } + # ── view / screenshot ────────────────────────────────────────────────────── async def view_zoom_extents(self) -> dict: diff --git a/pyproject.toml b/pyproject.toml index 6b07535..746ef09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "autocad-mcp-pro" -version = "1.1.0" +version = "1.1.1" description = "Production-grade AutoCAD MCP server — COM API + ezdxf dual engine" readme = "README.md" requires-python = ">=3.11" diff --git a/server.py b/server.py index d2bbf6f..6d4d666 100644 --- a/server.py +++ b/server.py @@ -1141,7 +1141,7 @@ async def entity_set_properties( # --------------------------------------------------------------------------- -# ── SECTION 5: Entity Query (3 tools) ─────────────────────────────────────── +# ── SECTION 5: Entity Query (4 tools) ─────────────────────────────────────── # --------------------------------------------------------------------------- @mcp.tool( @@ -1207,6 +1207,42 @@ async def entity_delete_many( return {"ok": True, "deleted": deleted, "errors": errors} +@mcp.tool( + annotations={"title": "Get Viewport Selection", "readOnlyHint": True}, + tags={"entity", "query"}, +) +async def selection_get(ctx: Context = None) -> dict: + """Read the entities the user pre-selected in the AutoCAD viewport (COM backend only). + + Returns the implied "pickfirst" selection — the entities highlighted with + grips before invoking the AI — so work can be scoped to exactly those + entities instead of the whole drawing. Typical use:: + + sel = selection_get() + dimension_auto(sel["handles"], style="chain") + + Result keys: + ok — True on the COM backend (even for an empty selection) + count — number of selected entities + handles — list of entity handles (hex strings) to act on + entities — full per-entity info (type, layer, color, ...) + pickfirst — state of the PICKFIRST sysvar (None if unknown) + message — guidance when nothing is selected + + On the ezdxf headless backend there is no viewport, so this returns + ok=False with an empty handles list. + """ + result = await _backend(ctx).selection_get() + if result.get("ok"): + await ctx.info(f"Viewport selection: {result.get('count', 0)} entit(y/ies)") + else: + await ctx.warning(result.get("error", "selection_get returned not-ok")) + # The backend places EntityInfo dataclasses under "entities"; serialize them. + result = dict(result) + result["entities"] = [_dc(e) for e in result.get("entities", [])] + return result + + # --------------------------------------------------------------------------- # ── SECTION 6: Layer Management (12 tools) ────────────────────────────────── # ---------------------------------------------------------------------------