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:
co-authored by
Claude Opus 4.8
parent
5583730a79
commit
dfb21a734a
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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) ──────────────────────────────────
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user