feat(selection): add selection_get — read live viewport pickfirst selection (#1)

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 <noreply@anthropic.com>
This commit is contained in:
Umutcan Edizsalan
2026-06-20 11:41:36 +03:00
co-authored by Claude Opus 4.8
parent 5583730a79
commit dfb21a734a
7 changed files with 151 additions and 7 deletions
+14
View File
@@ -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
+6 -5
View File
@@ -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` |
+20
View File
@@ -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: ...
+60
View File
@@ -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:
+13
View File
@@ -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:
+1 -1
View File
@@ -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"
+37 -1
View File
@@ -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) ──────────────────────────────────
# ---------------------------------------------------------------------------