fix(correctness-bundle): I16 correctness fixes + dead code removal + ruff clean

N1: COM hatch non-associative (AddHatch associativity=False)
N2: ezdxf refuses DWG saves with clear error; server.py derives fmt from extension
R19: dimension_aligned dist→distance; dimension_angular correct add_angular_dim_2l args (base/line1/line2/location)
R9: polar array 360° no-duplicate (divisor=n when fill_angle%360<ε)
R13: entity_offset side_x/side_y honored in ezdxf via cross-product (LINE) and distance-to-center (CIRCLE)
R18: drawing_new bootstrap failure surfaces status="degraded" instead of swallowing silently
Dead code: engineering/section.py, gear.generate_tooth_profile, CommandResult, _ACI_NAMES, set_layer_active, ensure_linetypes, ComBackend._batch_mode instance attr, engineering/__init__ build-seam comment
Ruff: 38 auto-fixed + 5 manual → codebase is now ruff-clean; CI lint is now blocking

184 tests pass.
This commit is contained in:
Umutcan Edizsalan
2026-06-18 11:34:30 +03:00
parent 702af0d6e1
commit 0c7f64ceb6
51 changed files with 2494 additions and 2865 deletions
@@ -1,98 +0,0 @@
---
name: autocad-mcp-premium
description: Use when working with AutoCAD MCP Pro tools and the user asks for technical drawings, P&ID, mechanical engineering output, ISO-conformant drawings, premium-quality CAD, or mentions trim/extend/fillet/chamfer, layer discipline, or "premium drawing". Apply the plan→draw→critique workflow strictly.
---
# AutoCAD MCP Premium Drafting Skill
Premium drafting discipline for the AutoCAD MCP Pro server. The goal: ISO-conformant
output that a real draftsman would sign. The path: a strict workflow + the corner
ops + meta-tool layer that ship with the server.
## When this skill applies
- The user asks for a **technical drawing**, **P&ID**, **mechanical part**, or
any drawing intended for production / fabrication / approval.
- The user mentions **TRIM, EXTEND, FILLET, CHAMFER**, **OSNAP**, **ISO 128/129/
13567**, **layer discipline**, or **premium drawing**.
- You are about to call `entity_create_*` for a deliverable drawing — **stop and
apply the workflow below first**.
## The 9-step Premium Workflow (non-negotiable)
```text
1. drawing_plan(intent, sheet_size, scale) # commit intent BEFORE drawing
2. drawing_apply_iso_layers("mech" | "pid" | "iso13567")
3. construction_xline(...) # scaffolding (optional)
4. entity_create_line / circle / arc / ... # geometry on engineering layers
5. entity_trim / entity_fillet / entity_chamfer # close corners explicitly
6. handles = entity_select_smart({type, layer, ...}) # don't memorise handles
7. dimension_auto(handles, style="chain"|"baseline"|"ordinate")
8. issues = drawing_critique(focus=None) # must be []
9. construction_clear()
10. drawing_finalize(save_path=..., screenshot_path=...)
```
If any of these is skipped, the drawing is **not premium** — it's a sketch.
## The 8 Non-Negotiable Rules (mirror of CLAUDE.md)
1. **Plan before draw** — `drawing_plan` first; PlanSpec is the contract.
2. **No coordinate guessing** — `point_from_snap(handle, snap, ref_x, ref_y)`.
3. **Layer discipline** — engineering layers only; CONSTRUCTION for scaffold.
4. **Lineweights are ISO 128** — bootstrap via `drawing_apply_iso_layers`.
5. **Corners are explicit** — `entity_trim` / `entity_fillet` / `entity_chamfer`.
6. **Dimensions via auto** — `dimension_auto`, not loops of `dimension_linear`.
7. **Critique then finalize** — `drawing_critique` must return `[]`.
8. **Meta-tools over raw** — never reimplement what a meta-tool already does.
## Tool Inventory (this skill assumes server has these registered)
| Group | Tools | Count |
|-------|-------|-------|
| Drawing | `drawing_*` (info/new/open/save/export/finalize/...) | 11 |
| Entity Create | `entity_create_line/circle/arc/polyline/text/...` | 12 |
| Dimensions | `dimension_linear/aligned/angular/radius/diameter` | 5 |
| Entity Modify | `entity_move/copy/rotate/scale/mirror/offset/delete/...` | 10 |
| **Corner Ops** ⭐ | `entity_trim` `entity_extend` `entity_fillet` `entity_chamfer` | **4** |
| Entity Query | `entity_get/list/delete_many` | 3 |
| Layer Mgmt | `layer_*`, `linetype_list/load` | 14 |
| Block Ops | `block_*` | 7 |
| Analysis | `analysis_*` | 8 |
| View | `view_*` | 4 |
| Transactions | `transaction_*` | 3 |
| System | `system_*` | 6 |
| Engineering | `gear_*`, `keyway_*`, `titleblock_*`, `drawing_finalize` | 7 |
| **Premium Meta** ⭐ | `drawing_plan` `drawing_critique` `point_from_snap` `construction_xline/clear` `drawing_apply_iso_layers` `dimension_auto` `entity_select_smart` | **8** |
## Reference Documents (read on demand)
- [`references/corner-ops.md`](references/corner-ops.md) — trim/extend/fillet/chamfer
semantics, decision tree (when to use which), corner-case table.
- [`references/meta-planning.md`](references/meta-planning.md) — `drawing_plan`,
`drawing_critique`, `point_from_snap` deep dive + PlanSpec / Issue schema.
- [`references/iso-standards.md`](references/iso-standards.md) — ISO 128 line
widths, ISO 13567 layer naming, ISO 129 dimension rules — printable tables.
- [`references/premium-workflow.md`](references/premium-workflow.md) — full
worked examples of the 9-step workflow with code blocks.
## Examples
- [`examples/premium-shaft.md`](examples/premium-shaft.md) — Shaft with keyway
(Anka-Makine reference part).
- [`examples/pid-simple-loop.md`](examples/pid-simple-loop.md) — P&ID flow
control loop with ISA-5.1 tags.
## Anti-patterns to refuse
- Calling `entity_create_*` before `drawing_plan` for a deliverable drawing.
- Computing endpoint/midpoint/intersection coordinates by hand instead of
`point_from_snap`.
- Drawing on layer 0 (or any non-engineering layer).
- Leaving the CONSTRUCTION layer populated at finalize time.
- Hand-rolling trim/extend math when `entity_trim` / `entity_extend` exists.
- Skipping `drawing_critique` and going straight to `drawing_finalize`.
When the user requests a premium drawing, **walk the 9 steps in order** and
narrate each call. If a critique returns non-empty issues, fix them in place
before proceeding to finalize.
@@ -1,107 +0,0 @@
# Example — Simple P&ID Flow Control Loop
ISA-5.1 tagged flow loop: pump P-101 → flow transmitter FT-101 → control valve
FCV-101 → vessel V-201. Drawn on A3 with the `pid` layer set.
## Layout
```text
┌────┐ ╭──╮ ┌────────┐
│P101├──────────────────┤FT├────────╳──────│ V-201 │
└────┘ PROCESS-MAIN ╰┬─╯ PROCESS │ │ │
│ signal │FCV │ │
↓ │101 │ │
(FIC101) │ └────────┘
controller │
─┴─ control output
```
## Full workflow
```python
# 1. PLAN
plan = await drawing_plan(
intent="Flow control loop 101: P-101 → FT-101 → FCV-101 → V-201",
sheet_size="A3",
scale=1.0,
layer_set_id="pid",
dim_style="ordinate", # P&ID typically minimal dim
notes=["ISA-5.1 tag conventions", "Pneumatic signal lines dashed"],
)
# 2. P&ID LAYERS
await drawing_apply_iso_layers("pid")
# 3. EQUIPMENT — pump (circle) and vessel (rectangle)
pump = await entity_create_circle(40, 60, 6, layer="PROCESS-EQUIPMENT")
await entity_create_text("P-101", 33, 50, height=3.0, layer="INSTRUMENT-TAG-TEXT")
vessel = await entity_create_rectangle(180, 40, 230, 80, layer="PROCESS-EQUIPMENT")
await entity_create_text("V-201", 195, 60, height=3.5, layer="INSTRUMENT-TAG-TEXT")
# 4. PROCESS LINE — main horizontal (heavy)
main_line_a = await entity_create_line(46, 60, 110, 60, layer="PROCESS-PIPING-MAIN")
main_line_b = await entity_create_line(110, 60, 150, 60, layer="PROCESS-PIPING-MAIN")
main_line_c = await entity_create_line(150, 60, 180, 60, layer="PROCESS-PIPING-MAIN")
# 5. INSTRUMENT BUBBLES — flow transmitter, control valve
ft = await entity_create_circle(110, 60, 4, layer="INSTRUMENT-SYMBOL")
await entity_create_text("FT", 107, 60.5, height=2.0, layer="INSTRUMENT-TAG-TEXT")
await entity_create_text("101", 106, 57.5, height=2.0, layer="INSTRUMENT-TAG-TEXT")
# control valve symbol = bowtie (2 triangles) — built from lines for now
fcv_top1 = await entity_create_line(150, 60, 154, 64, layer="PROCESS-VALVES")
fcv_top2 = await entity_create_line(150, 60, 154, 56, layer="PROCESS-VALVES")
fcv_bot1 = await entity_create_line(154, 64, 158, 60, layer="PROCESS-VALVES")
fcv_bot2 = await entity_create_line(154, 56, 158, 60, layer="PROCESS-VALVES")
await entity_create_text("FCV-101", 145, 70, height=2.5, layer="INSTRUMENT-TAG-TEXT")
# 6. SIGNAL LINES — FT bubble up to controller, then to FCV (dashed)
sig1 = await entity_create_line(110, 64, 110, 90, layer="INSTRUMENT-LINE-SIGNAL")
controller = await entity_create_circle(110, 95, 5, layer="INSTRUMENT-SYMBOL")
await entity_create_text("FIC", 107, 95.5, height=2.0, layer="INSTRUMENT-TAG-TEXT")
await entity_create_text("101", 106, 92.5, height=2.0, layer="INSTRUMENT-TAG-TEXT")
sig2 = await entity_create_line(110, 90, 154, 90, layer="INSTRUMENT-LINE-SIGNAL")
sig3 = await entity_create_line(154, 90, 154, 64, layer="INSTRUMENT-LINE-SIGNAL")
# 7. (no dimensions — P&ID is schematic, not dimensioned)
# 8. CRITIQUE — focus on what matters for P&ID
issues = await drawing_critique(focus=["layer_color", "iso128", "construction_left"])
assert issues == [], f"Premium gate failed: {issues}"
# 9. FINALIZE
await titleblock_apply_iso_a3(
title="P&ID — Flow Control Loop 101",
drawing_number="ANK-PID-2026-001",
revision="0",
scale="NTS",
drawn_by="UE",
company="Anka-Makine",
)
await drawing_finalize(
save_path=r"C:\work\anka\pid-loop-101.dwg",
screenshot_path=r"C:\work\anka\pid-loop-101.png",
)
```
## What this example demonstrates
- **`layer_set_id="pid"`** in `drawing_plan` triggers the P&ID layer bootstrap
(PROCESS-PIPING-MAIN, INSTRUMENT-SYMBOL, INSTRUMENT-LINE-SIGNAL, etc).
- **Layer-by-purpose**: process lines on PROCESS-PIPING-MAIN (heavy 0.7 mm),
signal lines on INSTRUMENT-LINE-SIGNAL (dashed 0.18), tags on
INSTRUMENT-TAG-TEXT.
- **ISA-5.1 tags** in text: `FT-101`, `FCV-101`, `FIC-101`, `P-101`, `V-201`.
- **No dimensions** — P&IDs are schematic. We pass a focused critique that
doesn't run dimension overlap checks.
- The same `drawing_finalize` infrastructure that handles mechanical drawings
works for P&IDs without modification.
## Where to grow
V2/V3 features that would polish this example:
- Symbol library blocks for pumps, valves, instruments (`block_insert`).
- ISO 14617 graphic-symbol catalog as MCP tools.
- Equipment-tag and line-tag auto-numbering.
@@ -1,125 +0,0 @@
# Example — Premium Shaft Drawing
A fully worked example: power-transmission shaft Ø30 × 200 mm with two keyway
seats (10 × 4 × 50 mm each per DIN 6885), drawn at 1:2 on A4.
## Goal
```text
50 100 50
┌──┴──┬───┴───┬──┴──┐
│ │ │ │ keyway both ends, M0.04 surface finish
└─────┴───────┴─────┘ bearing seat in middle (Ø30 h7)
────── 200 ──────
```
## Full workflow
```python
# 1. PLAN — commit intent first
plan = await drawing_plan(
intent="Power transmission shaft Ø30 x 200, 2× DIN 6885 keyway",
sheet_size="A4",
scale=0.5, # 1:2 (drawing is half real size)
layer_set_id="mech",
dim_style="chain",
notes=[
"Material: C45 normalized",
"Surface finish Ra 1.6 µm overall",
"Keyways: 10 x 4 x 50 mm per DIN 6885 A",
],
)
# 2. LAYERS — mech standard
await drawing_apply_iso_layers("mech")
# 3. CONSTRUCTION — centerline as reference
cl = await construction_xline(0, 0, 0) # horizontal centerline at y=0
# 4. GEOMETRY — front view (top half mirrored later)
top_edge = await entity_create_line(0, 15, 200, 15, layer="GEOMETRY")
right_edge = await entity_create_line(200, 15, 200, -15, layer="GEOMETRY")
bot_edge = await entity_create_line(200, -15, 0, -15, layer="GEOMETRY")
left_edge = await entity_create_line(0, -15, 0, 15, layer="GEOMETRY")
# 4b. KEYWAY LEFT — use the engineering primitive (per CLAUDE.md rule)
keyway_l = await keyway_draw_section(
bore_x=25, bore_y=0, bore_diameter=30,
keyway_width=10, keyway_depth=4,
)
# 4c. KEYWAY RIGHT — symmetric on the other end
keyway_r = await keyway_draw_section(
bore_x=175, bore_y=0, bore_diameter=30,
keyway_width=10, keyway_depth=4,
)
# 4d. Centerline (engineering layer, dashed)
center = await entity_create_line(-10, 0, 210, 0, layer="CENTER")
# 5. CORNERS — small chamfer at each end (1×45°)
await entity_chamfer(top_edge["handle"], left_edge["handle"], dist1=1)
await entity_chamfer(top_edge["handle"], right_edge["handle"], dist1=1)
await entity_chamfer(bot_edge["handle"], left_edge["handle"], dist1=1)
await entity_chamfer(bot_edge["handle"], right_edge["handle"], dist1=1)
# 6. SELECT geometry for dim
geometry = await entity_select_smart({
"type": "LINE",
"layer": "GEOMETRY",
"length_range": [40, 220], # exclude small chamfer lines
})
# 7. DIMENSIONS — chain dimensioning, 12 mm offset above the part
dims = await dimension_auto(
[e["handle"] for e in geometry],
style="chain",
offset=12.0,
)
# 8. CRITIQUE — must be empty before finalize
issues = await drawing_critique(focus=None)
if issues:
for i in issues:
print(f" [{i['severity']}] {i['focus']}: {i['message']}")
raise RuntimeError("Premium gate failed; fix before finalize")
# 9. CLEAR + TITLEBLOCK + FINALIZE
await construction_clear()
await titleblock_apply_iso_a3(
title="Transmission Shaft",
drawing_number="ANK-SH-2026-001",
revision="A",
scale="1:2",
material="C45 normalized",
drawn_by="UE",
company="Anka-Makine",
)
result = await drawing_finalize(
save_path=r"C:\work\anka\sh-2026-001.dwg",
screenshot_path=r"C:\work\anka\sh-2026-001.png",
expected={
"part_type": "shaft",
"must_have_keyway": True,
"must_have_bore": False, # solid shaft, not bored
},
)
```
## What this example demonstrates
- **Plan-first workflow** (rule 1)
- **Engineering primitives** for the keyway, not hand-drawn (rule 8 + CLAUDE.md
production drawing rule)
- **Construction line** for the centerline reference, cleared before finalize
- **Corner explicitness** via `entity_chamfer` (rule 5)
- **`entity_select_smart`** with `length_range` to filter out the tiny chamfer
lines (rule 8 — meta-tools over loops)
- **`dimension_auto`** with chain style (rule 6)
- **Critique gate** before finalize (rule 7)
- **`titleblock_apply_iso_a3`** + `drawing_finalize` with `expected` contract
The whole drawing comes out reproducible: the same call sequence generates an
identical DXF every time, suitable for `git diff` and ISO 9001 traceability.
@@ -1,77 +0,0 @@
# Corner Operations Reference
V1 supports **LINE+LINE** only. Other entity combinations (line+arc, line+circle,
polyline-segment) raise `RuntimeError` and will land in V2/V3.
## Decision tree
```text
┌────── two lines should meet at a sharp corner
│ → entity_trim (when one line overshoots)
│ → entity_extend (when one line falls short)
│
Need to close ─┼────── two lines should meet at a rounded corner (R>0)
a corner? │ → entity_fillet
│
└────── two lines should meet at an angled bevel
→ entity_chamfer
```
## `entity_trim(target_handle, cutter_handle, keep_x, keep_y)`
- `target` — the line being shortened.
- `cutter` — treated as an **infinite ray** (AutoCAD's "implied extend" trim).
- `keep_x/keep_y` — a point on the side of `target` you want to KEEP.
- Returns the (in-place modified) target's `EntityInfo`.
**Tip:** use `point_from_snap(target_handle, "mid")` or another snap to compute
`keep_x/keep_y` deterministically.
## `entity_extend(target_handle, boundary_handle, end_x=None, end_y=None)`
- Extends `target` until it hits `boundary` (treated as infinite).
- `end_x/end_y=None` → auto-pick the target endpoint nearest the boundary midpoint.
- Pass `end_x/end_y` explicitly to disambiguate when you need to extend a
specific endpoint.
## `entity_fillet(handle1, handle2, radius, trim=True)`
- Inserts a tangent **ARC** between the two lines.
- `radius=0` → corner-merge (no arc, only trim).
- `trim=False` → leave source lines untouched (rare; AutoCAD default is True).
- Returns the new ARC handle (or the first source line for `radius=0`).
## `entity_chamfer(handle1, handle2, dist1, dist2=None, trim=True)`
- Inserts a straight chamfer **LINE** between the two source lines.
- `dist2=None` → symmetric (`dist2 = dist1`); supply for asymmetric chamfers.
- Returns the new chamfer LINE handle.
## Corner-case table
| Vaka | Behaviour |
|------|-----------|
| Lines are parallel / anti-parallel | `RuntimeError("…parallel…")` |
| Same handle for both | `RuntimeError("…same…")` |
| One handle is not a LINE | `RuntimeError("…LINE+LINE only…")` |
| Intersection beyond segment ends | trim/fillet still work (implied extend) |
| Negative radius / zero distance | `RuntimeError` |
| Tiny gap (< 0.5 mm) between lines | flagged by `drawing_critique` as `untrimmed_corner` |
## Worked example — L-bracket inner corner R5
```python
l1 = await entity_create_line(0, 0, 100, 0) # bottom edge
l2 = await entity_create_line(100, 0, 100, 80) # right edge
arc = await entity_fillet(l1.handle, l2.handle, radius=5)
# arc.center == (95, 5); arc.radius == 5
# l1.end is now (95, 0); l2.start is now (100, 5)
```
## Why this matters
LLMs without these tools compute corner geometry by hand: they pick endpoints,
solve intersections, and write coordinates. That fails the moment a line is
slightly off, an angle is non-orthogonal, or the user changes a length. The
corner ops + `point_from_snap` move that math out of the LLM's head and into
deterministic library code.
@@ -1,109 +0,0 @@
# ISO Standards Reference
Distilled tables for the standards this skill enforces.
## ISO 128 — Line widths
Allowed lineweight set (mm), with 4:2:1 ratio rule:
| Width | Common use |
|-------|------------|
| 0.13 | Hatch, dim text shadow |
| 0.18 | Hidden, center, phantom, dim text |
| 0.25 | Default text, leader |
| 0.35 | Auxiliary visible (where contrast needed) |
| 0.50 | Visible solid edges (most common production weight) |
| 0.70 | Heavy solid (large-scale parts), P&ID main process |
| 1.00 | Border, key heavy outlines |
| 1.40 | Title borders |
| 2.00 | Sheet outer border |
**Rule:** every layer's lineweight must be one of these (or 0.0 = "Default" =
inherits from active style). Anything else fails `drawing_critique` focus
`iso128`.
## ISO 128 — Line types
| Type | Linetype name | Used for |
|------|---------------|----------|
| Continuous thick | `Continuous` (0.50+ mm) | Visible edges |
| Dashed thin | `HIDDEN` (0.18–0.25) | Hidden edges |
| Chain thin | `CENTER` (0.18) | Centerlines, axes of symmetry |
| Chain double-dashed | `PHANTOM` (0.18) | Section / cutting plane / alternate position |
| Continuous thin | `Continuous` (0.18–0.25) | Dimensions, hatch, leaders |
## ISO 13567 — Layer naming (4-field)
Format: `<Agent2c>-<Element6c>-<Presentation2c>-<Status1c>`
| Field | Length | Examples |
|-------|--------|----------|
| Agent | 2 chars | A=Architectural, S=Structural, M=Mechanical, P=Plumbing, E=Electrical |
| Element | 6 chars | GEOMET=geometry, HIDDEN=hidden edges, DIMEN=dimensions, TITLE=title block |
| Presentation | 2 chars | E=edge, T=text, H=hatch |
| Status | 1 char | N=new, E=existing, D=demolish |
Example layer set ships in `engineering/layers.py::ISO13567_LAYERS`.
## ISO 129-1 — Dimensioning rules
1. **Each dimension is given once** — never repeat.
2. **Extension lines** project at least 8× line width past the dim line.
3. **Decimal separator** is the comma (per ISO; we accept point too).
4. **Text orientation**: read from below the drawing or from the right side.
5. **Font**: ISO 3098 (`isocp.shx` ships with AutoCAD).
6. **Dimension styles**: chain, baseline, ordinate are the three standard
layouts — choose one per drawing and stick to it (`dim_style` in PlanSpec).
## ISO 7200 — Title-block fields
Mandatory:
- Legal owner (Anka-Makine)
- Drawing number
- Title
- Date
- Sheet / total sheets
- Approver name + signature
- Creator name
- Document type
Common optional:
- Revision
- Scale (matches PlanSpec.scale)
- Projection symbol (1st angle ISO E vs 3rd angle ISO A)
This skill assumes you call `titleblock_apply_iso_a3` (or its A3 cousin) to
populate these fields after `drawing_finalize`'s validator runs.
## ISO 14617 / 10628 — P&ID symbology
When `layer_set_id="pid"` is passed to `drawing_plan`, the bootstrap creates
the P&ID layer set:
| Layer | Linetype | Lineweight | Purpose |
|-------|----------|------------|---------|
| `PROCESS-PIPING-MAIN` | Continuous | 0.70 | Main process line (heavy) |
| `PROCESS-PIPING-SECONDARY` | Continuous | 0.50 | Secondary process line |
| `PROCESS-EQUIPMENT` | Continuous | 0.50 | Vessels / pumps / drums |
| `PROCESS-VALVES` | Continuous | 0.35 | Valves & fittings |
| `INSTRUMENT-SYMBOL` | Continuous | 0.35 | Instrument bubbles (FCFs) |
| `INSTRUMENT-LINE-SIGNAL` | DASHED | 0.18 | Pneumatic / electrical signal |
| `INSTRUMENT-TAG-TEXT` | Continuous | 0.25 | Tag labels |
| `ELECTRICAL-LINE` | DASHDOT | 0.18 | Electrical line |
| `UTILITY-LINE` | PHANTOM | 0.25 | Steam / water / air utility |
| `INSULATION-HATCH` | Continuous | 0.13 | Insulation hatching |
### ISA-5.1 tag conventions (P&ID instruments)
`<First letter><Second letter>-<Loop number><Suffix?>`
| First letter (measured variable) | Second letter (function) |
|----------------------------------|--------------------------|
| F = Flow | T = Transmitter |
| P = Pressure | I = Indicator |
| T = Temperature | C = Controller |
| L = Level | V = Valve |
| A = Analytical | S = Switch |
Examples: `FT-101` (flow transmitter, loop 101), `PCV-203A` (pressure control
valve, loop 203, A spare).
@@ -1,113 +0,0 @@
# Meta-Planning Reference
The premium meta-tool layer wraps the entity primitives in a quality-first
workflow. Schemas live in `engineering/plan_spec.py`; checks live in
`engineering/critique.py`.
## `drawing_plan(intent, sheet_size, scale, layer_set_id, view_count, dim_style, notes)`
Commits a `PlanSpec` to the backend instance. Always call FIRST.
```yaml
PlanSpec:
intent: str # "L-bracket 50x80"
sheet_size: A4 | A3 | A2 | A1 | A0 # default A3
scale: float # 1.0 = 1:1
layer_set_id: mech | pid | iso13567 # default mech
view_count: int # default 1
dim_style: chain | baseline | ordinate | mixed # default chain
notes: list[str] # free-form constraints
```
The PlanSpec is held by the backend (not persisted to DXF) and replayed by
`drawing_critique` at finalize time.
## `drawing_critique(focus=None)`
Returns a list of `Issue` instances. **Empty list = pass.**
```yaml
Issue:
severity: error | warning | info
focus: <CritiqueFocus enum>
message: str
handles: list[str] # entities involved
detail: dict # numeric context
```
`CritiqueFocus` is a **closed enum** — adding a new focus requires a code change
to `plan_spec.py` AND `critique.py`. This is intentional: it keeps the critique
scope from drifting.
| Focus | Checks |
|-------|--------|
| `iso128` | Layer lineweights are in {0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0, 1.4, 2.0} mm |
| `layer_color` | Engineering layers keep their canonical ACI color |
| `dim_overlap` | No two dimensions within 5 mm of each other |
| `untrimmed_corner` | No two LINE endpoints within 0.5 mm but not exactly equal |
| `duplicate_entities` | No two LINEs with identical (or reversed) endpoints |
| `construction_left` | CONSTRUCTION layer is empty |
`focus=None` runs all six. Pass a list (e.g. `["construction_left", "untrimmed_corner"]`)
to scope a check during iterative editing.
## `point_from_snap(handle, snap, ref_x, ref_y)`
Eliminates LLM coordinate guessing. Returns `(x, y)` tuple.
| `snap` | LINE | CIRCLE/ARC | Required |
|--------|------|-----------|----------|
| `end` | start or end (closer to ref) | arc start or end | — (uses ref if given) |
| `mid` | midpoint | arc midpoint | — |
| `center` | — | center | — |
| `quad` | — | nearest of E/N/W/S quadrants | — (uses ref if given) |
| `perp` | foot of perpendicular from ref | — | `ref_x`, `ref_y` |
| `near` | nearest point on segment (clamped) | nearest on circle/arc | `ref_x`, `ref_y` |
| `int` | (V2 — needs 2nd handle) | | |
## `construction_xline(x, y, angle_deg, layer="CONSTRUCTION")`
Creates an infinite construction line on a non-printing layer (color 250,
lightest weight). Use as scaffolding when laying out geometry.
`construction_clear(layer="CONSTRUCTION")` — bulk-deletes everything on the
layer. Returns `{"ok": True, "deleted": N}`. **Idempotent.**
## `drawing_apply_iso_layers(standard)`
| `standard` | Layer count | Use case |
|------------|-------------|----------|
| `mech` | 10 | Mechanical part / assembly drawings |
| `pid` | 15 | Process & Instrumentation Diagrams (ISO 10628) |
| `iso13567` | 10 | ISO 13567 layer-naming convention starter |
Idempotent — existing layers are not modified.
## `dimension_auto(handles, style, offset)`
V1 supports LINE entities only. Generates one or more dimensions using the
chosen style.
| `style` | Behaviour |
|---------|-----------|
| `chain` | One linear dim per handle, perpendicular-offset from each segment |
| `baseline` | All dims share the first segment's start point as baseline |
| `ordinate` | X and Y coordinate dims for each segment endpoint |
`offset` (mm) controls the dim-line distance from the geometry.
## `entity_select_smart(predicate)`
Predicate is a dict; **all keys optional, AND-ed**:
```yaml
predicate:
type: LINE | CIRCLE | ARC | ... # entity type filter
layer: <name> # exact match
near: [x, y, radius] # entity within radius of point
length_range: [min, max] # LINE/ARC only
color: <ACI int> # exact match
```
Returns a list of `EntityInfo`. Use this in lieu of memorising handles when
chaining `dimension_auto` or other operations.
@@ -1,159 +0,0 @@
# Premium Workflow — Full Examples
The 9-step workflow walked through end-to-end. Use these as templates.
## Template 1 — Mechanical part (L-bracket with rounded inner corner)
```python
# 1. PLAN
plan = await drawing_plan(
intent="L-bracket 50x80, R5 inner corner, 5mm thick",
sheet_size="A4",
scale=1.0,
layer_set_id="mech",
dim_style="chain",
)
# 2. LAYERS
await drawing_apply_iso_layers("mech")
# 3. CONSTRUCTION (optional — set up reference grid)
await construction_xline(0, 0, 0) # horizontal axis
await construction_xline(0, 0, 90) # vertical axis
# 4. GEOMETRY (on GEOMETRY layer)
l1 = await entity_create_line(0, 0, 100, 0, layer="GEOMETRY")
l2 = await entity_create_line(100, 0, 100, 80, layer="GEOMETRY")
# 5. CORNERS
arc = await entity_fillet(l1["handle"], l2["handle"], radius=5)
# 6. SELECT for dimensioning (avoid handle memorization)
geometry_lines = await entity_select_smart({
"type": "LINE",
"layer": "GEOMETRY",
})
# 7. DIMENSIONS
dims = await dimension_auto(
[e["handle"] for e in geometry_lines],
style="chain",
offset=12.0,
)
# 8. CRITIQUE
issues = await drawing_critique(focus=None)
assert issues == [], f"Premium gate failed: {issues}"
# 9. CLEAR + FINALIZE
await construction_clear()
await drawing_finalize(
save_path="C:/work/l-bracket.dwg",
screenshot_path="C:/work/l-bracket.png",
)
```
## Template 2 — P&ID flow control loop
```python
# 1. PLAN
plan = await drawing_plan(
intent="Flow control loop 101: pump → valve → vessel",
sheet_size="A3",
scale=1.0,
layer_set_id="pid",
dim_style="ordinate",
notes=["ISA-5.1 tag conventions", "All process lines on PROCESS-PIPING-MAIN"],
)
# 2. LAYERS — bootstraps PROCESS-PIPING-MAIN, INSTRUMENT-*, etc.
await drawing_apply_iso_layers("pid")
# 3. EQUIPMENT (vessels, pumps — use blocks ideally)
await entity_create_circle(50, 50, 8, layer="PROCESS-EQUIPMENT") # pump body
await entity_create_rectangle(150, 30, 200, 70, layer="PROCESS-EQUIPMENT") # vessel
# 4. PROCESS LINES
main = await entity_create_line(58, 50, 150, 50, layer="PROCESS-PIPING-MAIN")
# 5. INSTRUMENT BUBBLES (circle + tag text)
ft = await entity_create_circle(100, 70, 5, layer="INSTRUMENT-SYMBOL")
await entity_create_text("FT-101", 95, 68, height=2.5, layer="INSTRUMENT-TAG-TEXT")
# 6. SIGNAL LINE (instrument bubble → controller)
await entity_create_line(100, 75, 100, 90, layer="INSTRUMENT-LINE-SIGNAL")
# 7. (no dimensions on P&ID typically — skip)
# 8. CRITIQUE — focus on what matters for P&ID
issues = await drawing_critique(focus=["layer_color", "iso128", "construction_left"])
assert issues == []
# 9. FINALIZE
await drawing_finalize(save_path="C:/work/loop-101.dwg")
```
## Template 3 — Iterative drawing (corrections welcome)
```python
# Initial plan
plan = await drawing_plan("Bracket draft", sheet_size="A4")
await drawing_apply_iso_layers("mech")
# Draw something wrong: leaves a gap
l1 = await entity_create_line(0, 0, 49.8, 0, layer="GEOMETRY")
l2 = await entity_create_line(50, 0, 50, 30, layer="GEOMETRY")
# Critique — flags untrimmed_corner
issues = await drawing_critique(focus=["untrimmed_corner"])
# issues[0].handles = [l1.handle, l2.handle]
# Fix in place: extend l1 to meet l2
await entity_extend(l1["handle"], l2["handle"])
# Re-critique
issues = await drawing_critique(focus=["untrimmed_corner"])
assert issues == []
```
## Common patterns
### Snap-driven coordinates
```python
# BAD: guessing
await entity_create_line(50, 50, 100, 50)
# GOOD: snap from existing geometry
mid = await point_from_snap(some_line["handle"], "mid")
end = await point_from_snap(other_line["handle"], "end", ref_x=100, ref_y=50)
await entity_create_line(mid["x"], mid["y"], end["x"], end["y"])
```
### Construction-first workflow
```python
# Lay out reference geometry
await construction_xline(0, 0, 0) # x-axis
await construction_xline(0, 0, 90) # y-axis
await construction_xline(100, 0, 90) # vertical at x=100
# Snap real geometry to construction intersections
ref = await point_from_snap(some_line["handle"], "perp", ref_x=0, ref_y=50)
# ... create real geometry on GEOMETRY layer ...
# Wipe scaffold before finalize
await construction_clear()
```
### Block-first repetition
```python
# When 12 holes are needed: create one block, then array
hole = await entity_create_circle(0, 0, 5, layer="GEOMETRY")
await block_create_from_entities("HOLE_M10", [hole["handle"]], 0, 0)
# Insert + array — far cheaper than 12 entity_create_circle calls
ref = await block_insert("HOLE_M10", x=0, y=0)
await entity_array_polar(ref["handle"], count=12, fill_angle=360, center_x=50, center_y=50)
```
-47
View File
@@ -1,47 +0,0 @@
---
alwaysApply: true
---
ASK MODE – Mandatory and Absolute Rules
1. AskQuestion Requirement
- Every generated response must end with AskQuestion. The conversation can never be terminated.
- AskQuestion is the final action.
- It contains exactly one question and exactly 2 options.
- The options must be clear and closed-ended.
- The “Other” option is visible in the user interface (the user may enter a custom prompt).
- The fallback option must never terminate the conversation.
---
1. FRT Lock (Infinite Continuity)
- Unless the user writes “FRT” alone, Ask Mode must never end.
- Expressions such as “finished”, “stop”, “end”, thanks, or making a selection are invalid.
- The only exit key for Ask Mode is: FRT
- If FRT appears within another sentence, it is invalid. It must be written alone and in uppercase.
---
1. Medium / Large Task Rule
- Sub-agent usage is mandatory.
- Model: Claude 4.6 Max Thinking Fast max (the only authorized model)
- A single sub-agent cannot be used.
- 4 parallel sub-agent capacity must be utilized.
---
1. Sleep Protocol
- sleep 'value'
- Runs in the foreground, no timeout (minimum 70 seconds if required).
- Only run_terminal_cmd or Shell.
- is_background is prohibited.
---
1. Operational Principle
- Priority: Speed + Accuracy
- Bulk changes: search_replace
- Scattered changes: read_file → edit → write
- Line-based single changes are not allowed.
- Parallel execution is essential.
1. Please dont switch ‘’Plan Mode‘‘
-156
View File
@@ -1,156 +0,0 @@
---
name: autocad-mcp
description: |
Automate AutoCAD drawing operations through AutoCAD MCP Pro server (FastMCP 3.0).
86 tools for drawings, entities, layers, blocks, dimensions, analysis, batch ops,
templates, and validation. Dual backends: COM (live AutoCAD) and ezdxf (headless DXF).
Trigger when: (1) User mentions AutoCAD, DXF, DWG, or CAD drawings,
(2) User wants to create/modify engineering drawings programmatically,
(3) Working with tools prefixed drawing_, entity_, layer_, block_, dimension_,
analysis_, view_, system_, template_, or validation_,
(4) User asks about CAD automation, drafting workflows, or MCP server setup.
---
# AutoCAD MCP Pro
## Contents
- [Quick Start](#quick-start)
- [Backend Selection](#backend-selection)
- [Key Concepts](#key-concepts)
- [Tool Groups](#tool-groups)
- [Common Workflows](#common-workflows)
- [Security Rules](#security-rules)
- [Architecture](#architecture)
- [Troubleshooting](#troubleshooting)
- [Examples](#examples)
## Quick Start
Standard workflow for any drawing task:
```
1. drawing_new() # or drawing_open(path)
2. layer_create(name, color, linetype) # set up layers
3. entity_create_*(...) # draw geometry
4. dimension_*(...) # add dimensions
5. analysis_entity_stats() # verify drawing
6. drawing_save() # persist changes
```
Use `transaction_begin()` before complex operations. If something goes wrong, `transaction_rollback()` reverts all changes since begin.
For batch operations, use `entity_batch_create(entities)` with a list of entity definitions — handles are returned for each created entity.
## Backend Selection
| Environment Variable | Behavior |
|---------------------|----------|
| `AUTOCAD_MCP_BACKEND=auto` | Try COM first, fall back to ezdxf (default) |
| `AUTOCAD_MCP_BACKEND=com` | Force COM — requires running AutoCAD on Windows |
| `AUTOCAD_MCP_BACKEND=ezdxf` | Force ezdxf — headless DXF ops, no AutoCAD needed |
COM-only features: `system_run_command`, `system_run_lisp`, `view_zoom_extents/window` (view ops are no-ops in ezdxf). Screenshots: COM uses Win32 capture, ezdxf uses matplotlib render.
## Key Concepts
**Entity handles**: Hex strings (e.g. `"1A2B"`) returned by all create/copy operations. Required for modify/query operations (`entity_get`, `entity_move`, `entity_delete`, etc.).
**Coordinates**: Drawing units (mm default). Angles in degrees, counter-clockwise from X axis.
**ACI colors**: 1–255 = specific colors, 256 = ByLayer, 0 = ByBlock. Common: 1=red, 2=yellow, 3=green, 4=cyan, 5=blue, 6=magenta, 7=white.
**Transactions**: `transaction_begin` → operations → `transaction_commit` or `transaction_rollback`. ezdxf uses full DXF snapshots; COM uses native undo marks.
## Tool Groups
| Group | Count | Key Tools | Details |
|-------|-------|-----------|---------|
| Drawing Management | 11 | `drawing_new`, `drawing_open`, `drawing_save`, `drawing_export_*` | [tools-drawing.md](references/tools-drawing.md) |
| Entity Creation | 13 | `entity_create_line/circle/arc/polyline/text/hatch/...` | [tools-entity-creation.md](references/tools-entity-creation.md) |
| Entity Modification | 13 | `entity_move/copy/rotate/scale/mirror/offset/delete/...` | [tools-entity-modification.md](references/tools-entity-modification.md) |
| Dimensions | 5 | `dimension_linear/aligned/angular/radius/diameter` | [tools-dimensions.md](references/tools-dimensions.md) |
| Layer Management | 12 | `layer_create/delete/freeze/thaw/lock/unlock/isolate/...` | [tools-layers.md](references/tools-layers.md) |
| Block Operations | 7 | `block_insert/explode/create_from_entities/attributes/...` | [tools-blocks.md](references/tools-blocks.md) |
| Analysis | 9 | `analysis_entity_stats/find_in_region/measure_*/bounding_box` | [tools-analysis.md](references/tools-analysis.md) |
| Batch & Templates | 4 | `entity_batch_create/modify`, `template_apply_layers/list` | [tools-batch-template.md](references/tools-batch-template.md) |
| Validation | 1 | `validation_check` | [tools-batch-template.md](references/tools-batch-template.md) |
| View & Screenshot | 5 | `view_zoom_extents/window/screenshot/zoom_and_screenshot` | [tools-view-system.md](references/tools-view-system.md) |
| Transactions | 3 | `transaction_begin/commit/rollback` | [tools-view-system.md](references/tools-view-system.md) |
| System | 6 | `system_status/get_variable/run_command/run_lisp/about` | [tools-view-system.md](references/tools-view-system.md) |
**Resources** (6): `autocad://drawing`, `autocad://layers`, `autocad://blocks`, `autocad://stats`, `autocad://status`, `autocad://entities/{layer_name}`
**Prompt templates** (5): `prompt_floor_plan`, `prompt_pid_diagram`, `prompt_electrical_schematic`, `prompt_mechanical_drawing`, `prompt_quick_drawing`
## Common Workflows
**New drawing from scratch**: Set up layers first, then draw entities, add dimensions, save.
See [workflows.md](references/workflows.md) for step-by-step checklists.
**Edit existing DXF**: `drawing_open` → `entity_list` to find entities → modify/delete → save.
**Batch operations**: `entity_batch_create([{type:"line", ...}, {type:"circle", ...}])` — single call, multiple entities.
**Template-based**: `template_apply_layers("architectural")` creates standard layer sets, then add content.
**Quality check**: `validation_check(["empty_layers", "zero_length", "duplicate_entities"])` reports issues.
## Security Rules
All file paths pass through `validate_path()`:
- Path traversal blocked (`../` patterns rejected)
- `ALLOWED_PATHS` env restricts accessible directories
- Write operations require `allow_write=True`
Commands pass through `sanitize_command()`:
- Dangerous patterns blocked: ERASE ALL, DELETE, PURGE, QUIT
- LISP sanitized: `startapp`, `vl-file-delete`, `dos_*` rejected
- Override: `DANGEROUS_COMMANDS_ENABLED=true`
Details: [security.md](references/security.md)
## Architecture
```
server.py FastMCP 3.0 server (86 tools, 6 resources, 5 prompts)
├── config.py Centralized pydantic-settings configuration
├── security.py Path validation + command sanitization
└── backends/
├── base.py AutoCADBackend ABC + data models
├── ezdxf_backend.py Headless DXF via ezdxf (async via _async wrapper)
└── com_backend.py Live AutoCAD via pywin32 COM (STA single-thread)
```
Middleware stack: ErrorHandling → AuditLog → Timing → Logging
Adding a new tool: (1) abstract method in `base.py` → (2) implement in `ezdxf_backend.py` → (3) implement in `com_backend.py` → (4) register in `server.py` with `@mcp.tool` → (5) test
Details: [architecture.md](references/architecture.md)
## Troubleshooting
| Error | Solution |
|-------|----------|
| "Backend unavailable" | Call `drawing_new()` or `drawing_open(path)` first |
| "Entity handle not found" | Use `entity_list()` to get valid handles |
| "Layer not found" | `layer_list()` to see existing, `layer_create()` to add |
| "Path validation failed" | Use absolute paths within `ALLOWED_PATHS` |
| "Command rejected" | Check `DANGEROUS_COMMANDS_ENABLED` setting |
| "COM backend failed" | Verify AutoCAD is running, or switch to ezdxf |
| "View ops are no-ops" | View/zoom only work with COM backend |
Full list: [troubleshooting.md](references/troubleshooting.md)
## Examples
- [Basic drawing workflow](examples/basic-workflow.md) — Room with walls, door, dimensions
- [Floor plan](examples/floor-plan.md) — Multi-room apartment layout
- [Mechanical part](examples/mechanical-part.md) — Flange with bolt pattern
## Utility Scripts
**Setup check**: `python scripts/check_setup.py` — Verify dependencies and configuration
**Drawing validation**: `python scripts/validate_drawing.py` — Check for common drawing issues
@@ -1,50 +0,0 @@
{
"architectural": {
"description": "Standard architectural drawing layers",
"layers": [
{"name": "WALLS", "color": 7, "linetype": "Continuous", "description": "Wall outlines"},
{"name": "DOORS", "color": 3, "linetype": "Continuous", "description": "Door swings and frames"},
{"name": "WINDOWS", "color": 5, "linetype": "Continuous", "description": "Window openings"},
{"name": "DIMENSIONS", "color": 1, "linetype": "Continuous", "description": "Dimension annotations"},
{"name": "TEXT", "color": 2, "linetype": "Continuous", "description": "Text labels and notes"},
{"name": "FURNITURE", "color": 8, "linetype": "Continuous", "description": "Furniture outlines"},
{"name": "PLUMBING", "color": 4, "linetype": "Continuous", "description": "Plumbing fixtures"},
{"name": "HVAC", "color": 6, "linetype": "Continuous", "description": "HVAC equipment and ducts"}
]
},
"mechanical": {
"description": "Standard mechanical drawing layers",
"layers": [
{"name": "OUTLINE", "color": 7, "linetype": "Continuous", "description": "Part outlines"},
{"name": "CENTERLINE", "color": 1, "linetype": "CENTER", "description": "Center lines and axes"},
{"name": "HIDDEN", "color": 8, "linetype": "HIDDEN", "description": "Hidden edges"},
{"name": "DIMENSIONS", "color": 5, "linetype": "Continuous", "description": "Dimensions"},
{"name": "NOTES", "color": 2, "linetype": "Continuous", "description": "Notes and callouts"},
{"name": "SECTION", "color": 3, "linetype": "Continuous", "description": "Section lines and hatching"},
{"name": "DETAIL", "color": 4, "linetype": "Continuous", "description": "Detail views"}
]
},
"electrical": {
"description": "Standard electrical drawing layers",
"layers": [
{"name": "POWER", "color": 1, "linetype": "Continuous", "description": "Power circuits"},
{"name": "LIGHTING", "color": 2, "linetype": "Continuous", "description": "Lighting circuits"},
{"name": "DATA", "color": 3, "linetype": "Continuous", "description": "Data and communication"},
{"name": "FIRE_ALARM", "color": 4, "linetype": "Continuous", "description": "Fire alarm system"},
{"name": "HVAC_CTRL", "color": 5, "linetype": "Continuous", "description": "HVAC controls"},
{"name": "PANELS", "color": 7, "linetype": "Continuous", "description": "Panel boards and equipment"},
{"name": "CONDUIT", "color": 8, "linetype": "Continuous", "description": "Conduit runs"}
]
},
"piping": {
"description": "Standard piping/P&ID drawing layers",
"layers": [
{"name": "PROCESS", "color": 7, "linetype": "Continuous", "description": "Process piping"},
{"name": "UTILITY", "color": 3, "linetype": "Continuous", "description": "Utility piping"},
{"name": "INSTRUMENTS", "color": 1, "linetype": "Continuous", "description": "Instrumentation"},
{"name": "EQUIPMENT", "color": 5, "linetype": "Continuous", "description": "Equipment outlines"},
{"name": "VALVES", "color": 2, "linetype": "Continuous", "description": "Valves and fittings"},
{"name": "LABELS", "color": 4, "linetype": "Continuous", "description": "Labels and tags"}
]
}
}
@@ -1,72 +0,0 @@
# Example: Basic Room Drawing
Create a simple room with walls, a door opening, dimensions, and labels.
## Setup
```
drawing_new()
layer_create("WALLS", color=7)
layer_create("DOORS", color=3)
layer_create("DIMENSIONS", color=1)
layer_create("TEXT", color=2)
```
## Draw Walls (5m x 4m room)
```
layer_set_current("WALLS")
# Outer walls as rectangle
entity_create_rectangle(x1=0, y1=0, x2=5000, y2=4000)
# Returns: {handle: "A1", type: "LWPOLYLINE", ...}
```
## Add Door Opening (0.9m wide on south wall)
```
layer_set_current("DOORS")
# Door opening represented as an arc (90° swing)
entity_create_arc(cx=2000, cy=0, radius=900, start_angle=0, end_angle=90)
# Returns: {handle: "B2", type: "ARC", ...}
# Door frame lines
entity_create_line(x1=2000, y1=0, x2=2000, y2=100)
entity_create_line(x1=2900, y1=0, x2=2900, y2=100)
```
## Add Dimensions
```
layer_set_current("DIMENSIONS")
# Room width
dimension_linear(x1=0, y1=0, x2=5000, y2=0, dim_x=2500, dim_y=-500)
# Room height
dimension_linear(x1=5000, y1=0, x2=5000, y2=4000, dim_x=5500, dim_y=2000)
# Door width
dimension_linear(x1=2000, y1=0, x2=2900, y2=0, dim_x=2450, dim_y=-300)
```
## Add Labels
```
layer_set_current("TEXT")
entity_create_text(text="BEDROOM", x=2500, y=2000, height=200)
entity_create_text(text="5000", x=2500, y=-800, height=150)
```
## Verify and Save
```
analysis_entity_stats()
# Expected: ~8 entities across 4 layers
validation_check(["empty_layers"])
# Should report no issues
drawing_save("C:/drawings/room.dxf")
```
@@ -1,100 +0,0 @@
# Example: Apartment Floor Plan
Create a 3-room apartment: living room, bedroom, kitchen with doors and windows.
## Setup
```
drawing_new()
template_apply_layers("architectural")
# Creates: WALLS, DOORS, WINDOWS, DIMENSIONS, TEXT, FURNITURE, PLUMBING, HVAC
```
## Draw Exterior Walls (12m x 8m)
```
layer_set_current("WALLS")
# Exterior boundary
entity_create_rectangle(x1=0, y1=0, x2=12000, y2=8000)
```
## Draw Interior Walls
```
# Living/bedroom divider (vertical at x=5000)
entity_create_line(x1=5000, y1=0, x2=5000, y2=8000)
# Kitchen divider (horizontal at y=4000, from x=5000)
entity_create_line(x1=5000, y1=4000, x2=12000, y2=4000)
```
## Add Doors (0.9m openings)
```
layer_set_current("DOORS")
# Living room door (south wall)
entity_create_arc(cx=2000, cy=0, radius=900, start_angle=0, end_angle=90)
# Bedroom door (on divider wall)
entity_create_arc(cx=5000, cy=2000, radius=900, start_angle=90, end_angle=180)
# Kitchen door (on divider wall)
entity_create_arc(cx=7000, cy=4000, radius=900, start_angle=0, end_angle=90)
```
## Add Windows (1.5m wide)
```
layer_set_current("WINDOWS")
# Living room window (north wall)
entity_create_line(x1=1500, y1=8000, x2=3000, y2=8000, color=5)
# Bedroom window (east wall)
entity_create_line(x1=12000, y1=5500, x2=12000, y2=7000, color=5)
# Kitchen window (east wall)
entity_create_line(x1=12000, y1=1500, x2=12000, y2=3000, color=5)
```
## Add Dimensions
```
layer_set_current("DIMENSIONS")
# Overall width
dimension_linear(x1=0, y1=0, x2=12000, y2=0, dim_x=6000, dim_y=-800)
# Overall height
dimension_linear(x1=0, y1=0, x2=0, y2=8000, dim_x=-800, dim_y=4000)
# Room widths
dimension_linear(x1=0, y1=8000, x2=5000, y2=8000, dim_x=2500, dim_y=8500)
dimension_linear(x1=5000, y1=8000, x2=12000, y2=8000, dim_x=8500, dim_y=8500)
```
## Add Room Labels
```
layer_set_current("TEXT")
entity_create_text(text="LIVING ROOM", x=2500, y=4000, height=300)
entity_create_mtext(text="12.0 m²", x=2500, y=3500, width=2000, height=200)
entity_create_text(text="BEDROOM", x=8500, y=6000, height=300)
entity_create_mtext(text="28.0 m²", x=8500, y=5500, width=2000, height=200)
entity_create_text(text="KITCHEN", x=8500, y=2000, height=300)
entity_create_mtext(text="28.0 m²", x=8500, y=1500, width=2000, height=200)
```
## Verify
```
analysis_entity_stats()
analysis_layer_stats()
validation_check(["empty_layers", "zero_length"])
drawing_save("C:/drawings/apartment.dxf")
```
@@ -1,105 +0,0 @@
# Example: Flange with Bolt Pattern
Create a circular flange plate with bolt holes using polar array.
## Setup
```
drawing_new()
template_apply_layers("mechanical")
# Creates: OUTLINE, CENTERLINE, HIDDEN, DIMENSIONS, NOTES, SECTION, DETAIL
```
## Draw Flange Outline
```
layer_set_current("OUTLINE")
# Outer flange circle (diameter 200mm)
entity_create_circle(cx=0, cy=0, radius=100)
# Returns: {handle: "F1", ...}
# Inner bore (diameter 50mm)
entity_create_circle(cx=0, cy=0, radius=25)
# Returns: {handle: "F2", ...}
```
## Add Centerlines
```
layer_set_current("CENTERLINE")
# Horizontal centerline
entity_create_line(x1=-120, y1=0, x2=120, y2=0, linetype="CENTER")
# Vertical centerline
entity_create_line(x1=0, y1=-120, x2=0, y2=120, linetype="CENTER")
```
## Create Bolt Hole Pattern
```
layer_set_current("OUTLINE")
# First bolt hole on bolt circle (PCD = 150mm diameter, so radius = 75mm)
entity_create_circle(cx=75, cy=0, radius=6)
# Returns: {handle: "B1", ...}
# Create polar array: 6 holes evenly spaced
entity_array_polar(handle="B1", center_x=0, center_y=0, count=6, angle=360)
# Returns: list of 5 new handles (original + 5 copies = 6 total)
```
## Add Dimensions
```
layer_set_current("DIMENSIONS")
# Outer diameter
dimension_diameter(x1=-100, y1=0, x2=100, y2=0)
# Inner bore diameter
dimension_diameter(x1=-25, y1=0, x2=25, y2=0)
# Bolt circle diameter
dimension_radius(center_x=0, center_y=0, radius=75, angle=45)
# Bolt hole diameter
dimension_diameter(x1=69, y1=0, x2=81, y2=0)
```
## Add Notes
```
layer_set_current("NOTES")
entity_create_mtext(
text="FLANGE PLATE\P6x M12 HOLES ON PCD 150\PMATERIAL: SS304",
x=-120, y=-140,
width=240, height=5
)
```
## Create Block from Bolt Hole
```
# Select all bolt hole entities and create a reusable block
# (Get handles from the array result)
block_create_from_entities(
name="BOLT_PATTERN_6x150",
handles=["B1", ...], # all bolt hole handles
base_x=0, base_y=0
)
```
## Verify
```
analysis_entity_stats()
# Expected: circles, lines, dimensions, mtext
analysis_bounding_box()
# Expected: approximately -120 to 120 in both axes
drawing_save("C:/drawings/flange.dxf")
```
@@ -1,60 +0,0 @@
# Architecture
## File Structure
```
server.py FastMCP 3.0 server — all tool/resource/prompt registrations
├── config.py Settings class (pydantic-settings, env vars)
├── security.py validate_path() + sanitize_command() + sanitize_lisp()
└── backends/
├── base.py AutoCADBackend ABC + data models
├── ezdxf_backend.py Headless DXF ops via ezdxf library
└── com_backend.py Live AutoCAD control via pywin32 COM
```
## Dual-Backend Strategy Pattern
`AutoCADBackend` (in `base.py`) is an abstract base class. Both backends implement the full interface. The server creates exactly one backend at startup via `_make_backend()`:
1. Check `AUTOCAD_MCP_BACKEND` env var
2. If `auto` or `com` on Windows: try COM → fall back to ezdxf
3. If non-Windows or `ezdxf`: use ezdxf
## Data Models
Defined in `base.py`:
- **EntityInfo**: `{handle, type, layer, color, linetype, visible, properties}`
- **LayerInfo**: `{name, color, linetype, lineweight, is_on, is_frozen, is_locked, is_current}`
- **BlockInfo**: `{name, origin, attribute_count, entity_count, is_xref, description}`
- **DrawingInfo**: `{name, full_path, saved, entity_count, layer_count, block_count, extents_min, extents_max, units, version, backend}`
- **CommandResult**: `{ok, payload, error}`
## Backend Differences
| Feature | ezdxf | COM |
|---------|-------|-----|
| Platform | Any | Windows only |
| AutoCAD needed | No | Yes (running) |
| View/zoom | No-op | Full control |
| Screenshots | matplotlib render | Win32 window capture |
| Commands/LISP | Not supported | Full support |
| Undo | DXF snapshot stack | Native AutoCAD undo |
| Threading | `asyncio.to_thread` via `_async()` | `ThreadPoolExecutor` (single thread, STA) |
| File formats | DXF only | DWG + DXF |
## FastMCP Server
The `mcp` object is configured with:
- **Lifespan**: `autocad_lifespan` — initializes backend, stores in `ctx.lifespan_context["backend"]`
- **Middleware**: ErrorHandling → AuditLog (custom timing) → Timing → Logging
- **`_backend(ctx)`**: Helper that retrieves backend from context, raises `ToolError` if unavailable
## Adding a New Tool
1. Add abstract method to `AutoCADBackend` in `backends/base.py`
2. Implement in `EzdxfBackend` — wrap sync calls with `_async(func)`
3. Implement in `ComBackend` — wrap COM calls with `self._com(func)`
4. Register in `server.py`: `@mcp.tool(...)` calling `_backend(ctx).method(...)`
5. Use `_dc(result)` to convert dataclass returns to dicts
6. Write tests in `tests/`
@@ -1,53 +0,0 @@
# Security
## Path Validation
All file-path tools pass through `validate_path()` in `security.py`.
**How it works**:
- Resolves path via `Path.resolve()` (removes `..`, symlinks)
- Checks against `ALLOWED_PATHS` env var (comma-separated directories)
- If `ALLOWED_PATHS` is empty → all paths allowed (backward compat)
- Separate `allow_write` flag for read vs write operations
**Tools with path validation**:
- `drawing_open` (read)
- `drawing_save`, `drawing_save_as` (write)
- `drawing_export_dxf`, `drawing_export_pdf` (write)
- `drawing_new` with template (read)
**Blocked examples**:
- `../../../etc/passwd` → rejected (traversal)
- `C:\Windows\System32\...` → rejected if not in ALLOWED_PATHS
- Relative paths with `..` → always rejected
## Command Sanitization
`sanitize_command()` checks `system_run_command` input:
**Blocked patterns**: `ERASE`, `DELETE`, `PURGE`, `QUIT`, `CLOSE`, `_QUIT`, shell-related commands
**Override**: Set `DANGEROUS_COMMANDS_ENABLED=true` to bypass.
## LISP Sanitization
`sanitize_lisp()` checks `system_run_lisp` input:
**Blocked functions**: `startapp`, `vl-file-delete`, `vl-file-rename`, `dos_*` (file system access), `command` with dangerous args
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `ALLOWED_PATHS` | (empty) | Comma-separated allowed directories. Empty = all allowed |
| `DANGEROUS_COMMANDS_ENABLED` | `false` | Allow blocked commands |
| `AUTOCAD_MCP_BACKEND` | `auto` | Backend selection |
| `MAX_UNDO_STACK` | `50` | Max undo snapshots (ezdxf) |
| `LOG_LEVEL` | `INFO` | Logging level |
## Best Practices
- Always set `ALLOWED_PATHS` in production to restrict file access
- Never pass raw user input to `system_run_command` or `system_run_lisp`
- Use `transaction_begin/rollback` for destructive batch operations
- Review audit logs for suspicious tool calls (AuditMiddleware logs all calls)
@@ -1,73 +0,0 @@
# Analysis Tools (9)
Tools for querying and measuring drawing contents.
---
## analysis_entity_stats
No parameters. Returns entity counts grouped by type and by layer.
```json
{
"total": 150,
"by_type": {"LINE": 50, "CIRCLE": 20, "TEXT": 30, ...},
"by_layer": {"WALLS": 40, "DOORS": 10, ...}
}
```
## analysis_find_in_region
Find entities within a rectangular region.
| Param | Type | Description |
|-------|------|-------------|
| `x1, y1` | `float` | Region minimum corner |
| `x2, y2` | `float` | Region maximum corner |
| `type_filter` | `str \| null` | Filter by entity type |
Returns list of entity infos within the bounding box.
## analysis_measure_distance
Measure distance between two points.
| Param | Type | Description |
|-------|------|-------------|
| `x1, y1` | `float` | Point 1 |
| `x2, y2` | `float` | Point 2 |
Returns `{distance, dx, dy, angle}`.
## analysis_measure_area
Calculate area of a polygon.
| Param | Type | Description |
|-------|------|-------------|
| `points` | `list[list[float]]` | Polygon vertices (min 3 points) |
Returns `{area, perimeter, centroid}`.
## analysis_bounding_box
No parameters. Returns `{min_x, min_y, max_x, max_y, width, height}` for all entities.
## analysis_select_by_layer
Get all entities on a specific layer.
| Param | Type | Description |
|-------|------|-------------|
| `layer_name` | `str` | Layer to select from |
Returns list of entity infos.
## analysis_select_by_type
Get all entities of a specific type.
| Param | Type | Description |
|-------|------|-------------|
| `entity_type` | `str` | Type: LINE, CIRCLE, ARC, LWPOLYLINE, TEXT, MTEXT, INSERT, HATCH, SPLINE, ELLIPSE |
Returns list of entity infos.
## analysis_layer_stats
No parameters. Returns per-layer statistics: entity count, types present, color, state.
## analysis_entities_in_region
Alias/variant of `analysis_find_in_region` — check both for availability.
@@ -1,83 +0,0 @@
# Batch, Template & Validation Tools (5)
## Batch Operations
### entity_batch_create
Create multiple entities in a single call. Reports progress.
| Param | Type | Description |
|-------|------|-------------|
| `entities` | `list[dict]` | Entity definitions (see format below) |
Each dict requires `type` and type-specific params:
```json
[
{"type": "line", "x1": 0, "y1": 0, "x2": 100, "y2": 0},
{"type": "circle", "cx": 50, "cy": 50, "radius": 25},
{"type": "text", "text": "Hello", "x": 10, "y": 10, "height": 5},
{"type": "rectangle", "x1": 0, "y1": 0, "x2": 200, "y2": 100},
{"type": "point", "x": 75, "y": 75},
{"type": "arc", "cx": 50, "cy": 0, "radius": 10, "start_angle": 0, "end_angle": 180},
{"type": "polyline", "points": [[0,0],[10,10],[20,0]], "closed": true}
]
```
Optional per-entity: `layer`, `color`, `linetype`.
Returns `{created: [...handles], errors: [...]}`. Partial success is possible.
### entity_batch_modify
Apply multiple modifications in a single call. Reports progress.
| Param | Type | Description |
|-------|------|-------------|
| `operations` | `list[dict]` | Operation definitions |
Each dict requires `handle` and `action`:
```json
[
{"handle": "1A", "action": "move", "dx": 10, "dy": 20},
{"handle": "2B", "action": "rotate", "base_x": 0, "base_y": 0, "angle_deg": 45},
{"handle": "3C", "action": "scale", "base_x": 0, "base_y": 0, "factor": 2.0},
{"handle": "4D", "action": "delete"},
{"handle": "5E", "action": "set_properties", "layer": "WALLS", "color": 1}
]
```
Returns results per operation.
---
## Templates
### template_apply_layers
Apply a standard layer set to the current drawing.
| Param | Type | Description |
|-------|------|-------------|
| `template` | `str` | Template name: `architectural`, `mechanical`, `electrical`, `piping` |
Creates predefined layers with standard names, colors, and linetypes.
### template_list
No parameters. Returns all available templates and their layer definitions.
---
## Validation
### validation_check
Run quality checks on the drawing.
| Param | Type | Description |
|-------|------|-------------|
| `checks` | `list[str] \| null` | Check types (default: all) |
Available checks:
- `empty_layers` — Layers with no entities
- `zero_length` — Lines/arcs with zero length
- `duplicate_entities` — Overlapping identical entities
Returns `{issues: [{check, severity, message, details}, ...], summary}`.
@@ -1,68 +0,0 @@
# Block Operations Tools (7)
Blocks are reusable named groups of entities. A **block definition** is the template; a **block reference** (INSERT entity) is a placed instance with position, scale, and rotation.
---
## block_list
No parameters. Returns all block definitions: `{name, origin, attribute_count, entity_count, is_xref, description}`.
## block_insert
Place a block reference in the drawing.
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Block definition name (must exist) |
| `x, y` | `float` | Insertion point |
| `x_scale` | `float` | X scale factor (default: 1.0) |
| `y_scale` | `float` | Y scale factor (default: 1.0) |
| `rotation` | `float` | Rotation in degrees (default: 0) |
| `layer` | `str \| null` | Target layer |
Returns entity info with handle (type: INSERT).
## block_explode
Decompose a block reference into individual entities.
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Block reference (INSERT entity) handle |
Returns list of new entity handles created from explosion.
## block_get_attributes
Read attribute values from a block reference.
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Block reference handle |
Returns `{tag: value}` dict of all attributes.
## block_set_attributes
Write attribute values to a block reference.
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Block reference handle |
| `attributes` | `dict` | `{tag: value}` pairs to set |
## block_create_from_entities
Create a new block definition from existing entities.
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | New block definition name |
| `handles` | `list[str]` | Entity handles to include |
| `base_x, base_y` | `float` | Block base/insertion point |
Entities are moved into the block definition.
## block_find_references
Find all INSERT entities that reference a specific block.
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Block definition name |
Returns list of block reference entity infos.
@@ -1,55 +0,0 @@
# Dimension Tools (5)
All dimension tools return entity info with handle. Dimensions are placed on the current layer unless `layer` is specified.
---
## dimension_linear
Linear dimension between two points (horizontal or vertical measurement).
| Param | Type | Description |
|-------|------|-------------|
| `x1, y1` | `float` | First extension line origin |
| `x2, y2` | `float` | Second extension line origin |
| `dim_x, dim_y` | `float` | Dimension line position (controls placement) |
| `layer` | `str \| null` | Target layer |
## dimension_aligned
Aligned dimension — measures true distance between points regardless of angle.
| Param | Type | Description |
|-------|------|-------------|
| `x1, y1` | `float` | First point |
| `x2, y2` | `float` | Second point |
| `distance` | `float` | Offset distance for dimension line |
| `layer` | `str \| null` | Target layer |
## dimension_angular
Angular dimension between two lines meeting at a vertex.
| Param | Type | Description |
|-------|------|-------------|
| `vertex_x, vertex_y` | `float` | Angle vertex point |
| `start_x, start_y` | `float` | First line endpoint |
| `end_x, end_y` | `float` | Second line endpoint |
| `dim_x, dim_y` | `float` | Dimension arc position |
| `layer` | `str \| null` | Target layer |
## dimension_radius
Radius dimension for circles and arcs.
| Param | Type | Description |
|-------|------|-------------|
| `center_x, center_y` | `float` | Circle/arc center |
| `radius` | `float` | Radius value |
| `angle` | `float` | Leader angle in degrees (where arrow points) |
| `layer` | `str \| null` | Target layer |
## dimension_diameter
Diameter dimension across a circle.
| Param | Type | Description |
|-------|------|-------------|
| `x1, y1` | `float` | First point on diameter |
| `x2, y2` | `float` | Opposite point on diameter |
| `layer` | `str \| null` | Target layer |
@@ -1,103 +0,0 @@
# Drawing Management Tools (11)
## Contents
- [drawing_info](#drawing_info)
- [drawing_new](#drawing_new)
- [drawing_open](#drawing_open)
- [drawing_save](#drawing_save)
- [drawing_save_as](#drawing_save_as)
- [drawing_export_dxf](#drawing_export_dxf)
- [drawing_export_pdf](#drawing_export_pdf)
- [drawing_purge](#drawing_purge)
- [drawing_audit](#drawing_audit)
- [drawing_undo / drawing_redo](#drawing_undo--drawing_redo)
---
## drawing_info
Read-only. Returns comprehensive metadata for the current drawing.
**Returns**: `{name, full_path, saved, entity_count, layer_count, block_count, extents_min, extents_max, units, version, backend}`
---
## drawing_new
Create a new empty drawing, optionally from a template.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `template` | `str \| null` | No | Path to `.dwt` template file |
Path validation applied to template. Returns drawing info dict.
---
## drawing_open
Open an existing DWG or DXF file. Reports progress.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `path` | `str` | Yes | Full path to `.dwg` or `.dxf` file |
Path validation: `allow_write=False`. Returns drawing info dict.
---
## drawing_save
Save current drawing. Uses current path if omitted.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `path` | `str \| null` | No | Optional save path |
Path validation: `allow_write=True`.
---
## drawing_save_as
Save drawing to a new path.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `path` | `str` | Yes | Full destination path with extension |
Path validation: `allow_write=True`.
---
## drawing_export_dxf
Export drawing as DXF file.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `path` | `str` | Yes | Output `.dxf` file path |
| `version` | `str` | No | DXF version: `R2010`, `R2013`, `R2018` (default: `R2018`) |
---
## drawing_export_pdf
Export drawing as PDF. Requires `matplotlib`.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `path` | `str` | Yes | Output `.pdf` file path |
---
## drawing_purge
Remove all unused objects (layers, blocks, linetypes, styles). No parameters.
---
## drawing_audit
Run integrity audit — detect and fix drawing errors. No parameters.
---
## drawing_undo / drawing_redo
Undo/redo last operation.
- **ezdxf**: Snapshot-based (restores full DXF state from `_undo_stack`)
- **COM**: Native AutoCAD undo/redo
**Note**: For multi-step undo protection, use `transaction_begin/rollback` instead.
@@ -1,96 +0,0 @@
# Entity Creation Tools (13)
All creation tools return `{handle, type, layer, color, linetype, visible, properties}`. The `handle` (hex string) is required for all subsequent operations on that entity.
Common optional params on all creation tools:
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `layer` | `str` | Current layer | Target layer name |
| `color` | `int` | 256 (ByLayer) | ACI color (1-255, 256=ByLayer, 0=ByBlock) |
| `linetype` | `str` | `"ByLayer"` | Linetype name |
---
## entity_create_line
| Param | Type | Description |
|-------|------|-------------|
| `x1, y1` | `float` | Start point |
| `x2, y2` | `float` | End point |
## entity_create_circle
| Param | Type | Description |
|-------|------|-------------|
| `cx, cy` | `float` | Center point |
| `radius` | `float` | Radius |
## entity_create_arc
| Param | Type | Description |
|-------|------|-------------|
| `cx, cy` | `float` | Center point |
| `radius` | `float` | Radius |
| `start_angle` | `float` | Start angle (degrees, CCW from X) |
| `end_angle` | `float` | End angle (degrees) |
## entity_create_polyline
| Param | Type | Description |
|-------|------|-------------|
| `points` | `list[list[float]]` | `[[x,y], [x,y], ...]` coordinate pairs |
| `closed` | `bool` | Close the polyline (default: false) |
## entity_create_rectangle
| Param | Type | Description |
|-------|------|-------------|
| `x1, y1` | `float` | First corner |
| `x2, y2` | `float` | Opposite corner |
Creates an LWPOLYLINE (closed).
## entity_create_text
| Param | Type | Description |
|-------|------|-------------|
| `text` | `str` | Text content |
| `x, y` | `float` | Insertion point |
| `height` | `float` | Text height (default: 2.5) |
| `rotation` | `float` | Rotation angle in degrees (default: 0) |
## entity_create_mtext
| Param | Type | Description |
|-------|------|-------------|
| `text` | `str` | Content (`\P` for paragraph break, `{\H...;}` for formatting) |
| `x, y` | `float` | Insertion point |
| `width` | `float` | Text box width (default: 100) |
| `height` | `float` | Character height (default: 2.5) |
## entity_create_hatch
| Param | Type | Description |
|-------|------|-------------|
| `pattern` | `str` | Pattern name: `SOLID`, `ANSI31`, `ANSI32`, `STEEL`, `GRAVEL`, etc. |
| `boundary_points` | `list[list[float]]` | Closed boundary polygon `[[x,y], ...]` |
| `scale` | `float` | Pattern scale (default: 1.0) |
| `angle` | `float` | Pattern angle (default: 0) |
## entity_create_spline
| Param | Type | Description |
|-------|------|-------------|
| `fit_points` | `list[list[float]]` | Points the spline passes through |
## entity_create_ellipse
| Param | Type | Description |
|-------|------|-------------|
| `cx, cy` | `float` | Center point |
| `major_axis_x, major_axis_y` | `float` | Major axis endpoint (relative to center) |
| `ratio` | `float` | Minor/major axis ratio (0.0–1.0) |
## entity_create_point
| Param | Type | Description |
|-------|------|-------------|
| `x, y` | `float` | Point coordinates |
## entity_create_block_ref
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Block definition name (must exist) |
| `x, y` | `float` | Insertion point |
| `x_scale, y_scale` | `float` | Scale factors (default: 1.0) |
| `rotation` | `float` | Rotation in degrees (default: 0) |
@@ -1,111 +0,0 @@
# Entity Modification & Query Tools (13)
All modify tools require a `handle` (hex string). Get handles from `entity_list()` or any `entity_create_*` return value.
---
## Modification Tools (10)
### entity_move
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity handle |
| `dx, dy` | `float` | Displacement vector |
### entity_copy
Returns new entity info with a **new handle**.
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity to copy |
| `dx, dy` | `float` | Offset for the copy |
### entity_rotate
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity handle |
| `base_x, base_y` | `float` | Rotation base point |
| `angle_degrees` | `float` | Rotation angle (CCW) |
### entity_scale
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity handle |
| `base_x, base_y` | `float` | Scale base point |
| `factor` | `float` | Scale factor |
### entity_mirror
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity handle |
| `x1, y1` | `float` | Mirror line point 1 |
| `x2, y2` | `float` | Mirror line point 2 |
### entity_offset
Works on line, circle, polyline.
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity handle |
| `distance` | `float` | Offset distance (positive = outward) |
### entity_delete
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity to delete |
### entity_array_rectangular
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity to array |
| `rows` | `int` | Number of rows |
| `cols` | `int` | Number of columns |
| `row_spacing` | `float` | Row spacing |
| `col_spacing` | `float` | Column spacing |
Returns list of new entity handles.
### entity_array_polar
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity to array |
| `center_x, center_y` | `float` | Array center point |
| `count` | `int` | Number of copies |
| `angle` | `float` | Total angle to fill (degrees, default: 360) |
### entity_set_properties
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity handle |
| `layer` | `str \| null` | Move to layer |
| `color` | `int \| null` | Change ACI color |
| `linetype` | `str \| null` | Change linetype |
| `lineweight` | `float \| null` | Change lineweight |
All property params are optional — only provided ones are changed.
---
## Query Tools (3)
### entity_get
| Param | Type | Description |
|-------|------|-------------|
| `handle` | `str` | Entity handle |
Returns full entity info: `{handle, type, layer, color, linetype, visible, properties}`.
### entity_list
| Param | Type | Description |
|-------|------|-------------|
| `type_filter` | `str \| null` | Filter: `LINE`, `CIRCLE`, `ARC`, `LWPOLYLINE`, `TEXT`, `MTEXT`, `INSERT`, `HATCH`, etc. |
| `layer_filter` | `str \| null` | Filter by layer name |
| `limit` | `int` | Max results (default: 100) |
| `offset` | `int` | Skip first N results (default: 0) |
### entity_delete_many
| Param | Type | Description |
|-------|------|-------------|
| `handles` | `list[str]` | List of handles to delete |
Reports progress. Returns `{deleted, failed, errors}`.
@@ -1,63 +0,0 @@
# Layer Management Tools (12)
Layers organize entities by category (walls, doors, dimensions, etc.). Each layer has: name, color (ACI), linetype, lineweight, and states (on/off, frozen/thawed, locked/unlocked).
---
## layer_list
No parameters. Returns list of all layers with properties: `{name, color, linetype, lineweight, is_on, is_frozen, is_locked, is_current}`.
## layer_create
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | New layer name |
| `color` | `int` | ACI color (default: 7=white) |
| `linetype` | `str` | Linetype name (default: `"Continuous"`) |
## layer_delete
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Layer to delete (must be empty, cannot be layer "0") |
## layer_set_current
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Layer to make current (new entities go here) |
## layer_modify
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Layer to modify |
| `color` | `int \| null` | New color |
| `linetype` | `str \| null` | New linetype |
| `lineweight` | `float \| null` | New lineweight |
## layer_freeze / layer_thaw
Freeze hides and excludes from regeneration (faster). Thaw restores.
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Layer name |
## layer_lock / layer_unlock
Lock prevents editing but keeps visible. Unlock restores editing.
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Layer name |
## layer_hide / layer_show
Hide turns layer off (invisible). Show turns it on.
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Layer name |
## layer_isolate
Shows only the specified layer, hides all others.
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Layer to keep visible |
To restore: `layer_show` each hidden layer, or reopen the drawing.
@@ -1,95 +0,0 @@
# View, Transaction & System Tools (14)
## View & Screenshot (5)
### view_zoom_extents
Zoom to show all entities. No parameters. **COM only** (no-op in ezdxf).
### view_zoom_window
Zoom to a specific rectangular area. **COM only**.
| Param | Type | Description |
|-------|------|-------------|
| `x1, y1` | `float` | Window corner 1 |
| `x2, y2` | `float` | Window corner 2 |
### view_screenshot
Capture drawing as image. Returns base64-encoded image.
- **COM**: Win32 window capture (requires Pillow)
- **ezdxf**: matplotlib render (requires matplotlib)
### view_zoom_and_screenshot
Combined zoom + screenshot in one call.
| Param | Type | Description |
|-------|------|-------------|
| `x1, y1` | `float \| null` | Optional zoom window corner 1 |
| `x2, y2` | `float \| null` | Optional zoom window corner 2 |
If coordinates omitted, captures full extents.
### view_set_view
Set named view. Parameters depend on backend.
---
## Transactions (3)
Use transactions to group operations with undo support.
```
transaction_begin() # Save checkpoint
... make changes ...
transaction_commit() # Accept changes
# or
transaction_rollback() # Revert to checkpoint
```
### transaction_begin
Start a transaction. ezdxf saves full DXF snapshot; COM creates undo mark.
### transaction_commit
Accept all changes since `transaction_begin`.
### transaction_rollback
Revert all changes since `transaction_begin`. ezdxf restores snapshot; COM uses native undo.
---
## System (6)
### system_status
No parameters. Returns server status: `{backend, drawing_loaded, entity_count, uptime, ...}`.
### system_get_variable
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Variable name: `DIMSCALE`, `LTSCALE`, `INSUNITS`, `CLAYER`, `MEASUREMENT` |
### system_set_variable
| Param | Type | Description |
|-------|------|-------------|
| `name` | `str` | Variable name |
| `value` | `any` | New value |
### system_run_command
Execute raw AutoCAD command. **COM only**. Sanitized by `sanitize_command()`.
| Param | Type | Description |
|-------|------|-------------|
| `command` | `str` | Command string (e.g. `_ZOOM E`, `_REGEN`) |
Dangerous commands blocked unless `DANGEROUS_COMMANDS_ENABLED=true`.
### system_run_lisp
Evaluate AutoLISP expression. **COM only**. Sanitized by `sanitize_lisp()`.
| Param | Type | Description |
|-------|------|-------------|
| `expression` | `str` | LISP expression (e.g. `(command "ZOOM" "E")`) |
Dangerous functions blocked: `startapp`, `vl-file-delete`, `dos_*`.
### system_about
No parameters. Returns server capabilities, tool count, version, and feature list.
@@ -1,79 +0,0 @@
# Troubleshooting
## Common Errors
### "AutoCAD backend unavailable"
**Cause**: No drawing is loaded — backend needs an active drawing context.
**Fix**: Call `drawing_new()` or `drawing_open(path)` before any other operations.
### "Entity handle 'XXXX' not found"
**Cause**: Handle doesn't exist, was deleted, or drawing was reloaded.
**Fix**: Use `entity_list()` to get current valid handles. Handles are session-specific in ezdxf; persist by saving/reopening.
### "Layer 'X' not found"
**Cause**: Layer doesn't exist in the drawing.
**Fix**: `layer_list()` to see existing layers. `layer_create("X")` to create it.
### "Cannot delete layer '0'"
**Cause**: Layer "0" is the default layer and cannot be removed.
**Fix**: Move entities to another layer instead. Layer "0" always exists.
### "Path validation failed"
**Cause**: Path contains traversal (`..`) or is outside `ALLOWED_PATHS`.
**Fix**: Use absolute paths within configured allowed directories. Check `ALLOWED_PATHS` env var.
### "Command rejected by sanitizer"
**Cause**: `system_run_command` detected a dangerous pattern (ERASE, DELETE, etc.).
**Fix**: Use specific tools instead (e.g., `entity_delete` instead of `ERASE` command). Or set `DANGEROUS_COMMANDS_ENABLED=true` if needed.
### "LISP expression rejected"
**Cause**: `system_run_lisp` detected dangerous function (startapp, file ops).
**Fix**: Use MCP tools instead of LISP for file operations.
### "COM backend initialization failed"
**Cause**: AutoCAD not running, not installed, or COM registration issues.
**Fix**: Start AutoCAD, then retry. Or switch to ezdxf: `AUTOCAD_MCP_BACKEND=ezdxf`.
### "View operations are no-ops"
**Cause**: Using ezdxf backend — view/zoom commands only work with COM.
**Fix**: Switch to COM backend if live viewport control is needed. Screenshots still work via matplotlib in ezdxf.
### "Screenshot failed"
**Cause**: Missing dependency (Pillow for COM, matplotlib for ezdxf).
**Fix**: `pip install Pillow` (COM) or `pip install matplotlib` (ezdxf).
### "PDF export failed"
**Cause**: matplotlib not installed.
**Fix**: `pip install matplotlib` or `pip install -e ".[pdf]"`.
### "Transaction not active"
**Cause**: `transaction_commit/rollback` called without `transaction_begin`.
**Fix**: Always call `transaction_begin()` first.
### "Block 'X' not found"
**Cause**: Block definition doesn't exist.
**Fix**: `block_list()` to see available blocks. Create a block with `block_create_from_entities()`.
---
## Backend-Specific Issues
### ezdxf Backend
- **No DWG write support**: ezdxf reads DWG but can only write DXF. Use `drawing_export_dxf()`.
- **Undo stack memory**: Full DXF snapshots stored. `MAX_UNDO_STACK` env limits count (default: 50).
- **Text rendering**: Depends on system fonts. MathTex/complex formatting may not render in screenshots.
### COM Backend
- **STA threading**: All COM calls go through a single thread. Batch operations are sequential.
- **AutoCAD version**: Tested with AutoCAD 2020+. Older versions may have COM API differences.
- **Window capture**: Screenshots capture the AutoCAD window as-is. Minimize overlapping windows.
---
## Diagnostic Steps
1. `system_status()` — Check backend type, connection, drawing state
2. `system_about()` — See available tools and capabilities
3. `drawing_info()` — Verify drawing is loaded and get metadata
4. `analysis_entity_stats()` — Quick overview of drawing contents
5. Check logs: `LOG_LEVEL=DEBUG` for verbose output
@@ -1,166 +0,0 @@
# Common Workflows
## 1. New Drawing from Scratch
```
Progress:
- [ ] Step 1: Create drawing
- [ ] Step 2: Set up layers
- [ ] Step 3: Draw entities
- [ ] Step 4: Add dimensions
- [ ] Step 5: Add text annotations
- [ ] Step 6: Verify and save
```
**Step 1**: `drawing_new()` — creates empty drawing
**Step 2**: Create layers for organization:
```
layer_create("WALLS", color=7, linetype="Continuous")
layer_create("DOORS", color=3)
layer_create("WINDOWS", color=5)
layer_create("DIMENSIONS", color=1)
layer_create("TEXT", color=2)
```
Or use `template_apply_layers("architectural")` for standard sets.
**Step 3**: Draw geometry on appropriate layers:
```
layer_set_current("WALLS")
entity_create_rectangle(x1=0, y1=0, x2=6000, y2=4000)
entity_create_line(x1=3000, y1=0, x2=3000, y2=4000)
```
**Step 4**: Add dimensions:
```
layer_set_current("DIMENSIONS")
dimension_linear(x1=0, y1=0, x2=6000, y2=0, dim_x=3000, dim_y=-500)
```
**Step 5**: Add text labels:
```
layer_set_current("TEXT")
entity_create_text(text="Living Room", x=1500, y=2000, height=200)
```
**Step 6**: Verify and save:
```
analysis_entity_stats() # check entity counts
validation_check() # run quality checks
drawing_save("/path/to/drawing.dxf")
```
---
## 2. Edit Existing DXF
```
Progress:
- [ ] Step 1: Open file
- [ ] Step 2: Explore contents
- [ ] Step 3: Make changes (with transaction)
- [ ] Step 4: Verify and save
```
**Step 1**: `drawing_open("/path/to/file.dxf")`
**Step 2**: Explore:
```
analysis_entity_stats() # overview
layer_list() # see layers
entity_list(type_filter="LINE", limit=20) # browse entities
```
**Step 3**: Begin transaction, then modify:
```
transaction_begin()
entity_move(handle="1A2B", dx=100, dy=0)
entity_set_properties(handle="3C4D", color=1, layer="WALLS")
entity_delete(handle="5E6F")
transaction_commit() # or transaction_rollback() to undo all
```
**Step 4**: `drawing_save()`
---
## 3. Batch Entity Creation
```
Progress:
- [ ] Step 1: Prepare entity list
- [ ] Step 2: Batch create
- [ ] Step 3: Verify results
```
**Step 1**: Build entity definitions:
```json
[
{"type": "line", "x1": 0, "y1": 0, "x2": 100, "y2": 0, "layer": "WALLS"},
{"type": "line", "x1": 100, "y1": 0, "x2": 100, "y2": 100, "layer": "WALLS"},
{"type": "circle", "cx": 50, "cy": 50, "radius": 10, "layer": "DETAILS"},
{"type": "text", "text": "A1", "x": 50, "y": -20, "height": 5, "layer": "TEXT"}
]
```
**Step 2**: `entity_batch_create(entities=[...])` — returns handles for all created entities
**Step 3**: `analysis_entity_stats()` to verify counts
---
## 4. Drawing Analysis
```
Progress:
- [ ] Step 1: Open drawing
- [ ] Step 2: Get overview stats
- [ ] Step 3: Examine specific areas
- [ ] Step 4: Measure and report
```
**Step 1**: `drawing_open(path)` or `drawing_info()` if already open
**Step 2**:
```
analysis_entity_stats() # by type and layer
analysis_layer_stats() # per-layer details
analysis_bounding_box() # drawing extents
```
**Step 3**: Region queries:
```
analysis_find_in_region(x1=0, y1=0, x2=1000, y2=1000, type_filter="LINE")
analysis_select_by_layer(layer_name="WALLS")
analysis_select_by_type(entity_type="CIRCLE")
```
**Step 4**: Measurements:
```
analysis_measure_distance(x1=0, y1=0, x2=100, y2=100)
analysis_measure_area(points=[[0,0],[100,0],[100,100],[0,100]])
```
---
## 5. Template-Based Drawing
```
Progress:
- [ ] Step 1: Create drawing with template
- [ ] Step 2: Apply layer standards
- [ ] Step 3: Add content
- [ ] Step 4: Validate and save
```
**Step 1**: `drawing_new(template="/path/to/template.dwt")` (or `drawing_new()` without template)
**Step 2**: `template_apply_layers("architectural")` — creates standard layers
**Step 3**: Draw content on appropriate layers
**Step 4**:
```
validation_check(["empty_layers", "zero_length"])
drawing_save("/path/to/output.dxf")
```
@@ -1,79 +0,0 @@
"""Check AutoCAD MCP Pro setup and dependencies."""
import importlib
import os
import sys
def check(name: str, condition: bool, fix: str = ""):
status = "OK" if condition else "MISSING"
symbol = "[+]" if condition else "[-]"
line = f" {symbol} {name}: {status}"
if not condition and fix:
line += f" -> {fix}"
print(line)
return condition
def main():
print("AutoCAD MCP Pro — Setup Check\n")
ok_count = 0
total = 0
print("Python:")
total += 1
v = sys.version_info
ok_count += check(
f"Python {v.major}.{v.minor}.{v.micro}",
v >= (3, 11),
"Requires Python 3.11+",
)
print("\nRequired packages:")
for pkg, install in [
("fastmcp", "pip install fastmcp"),
("ezdxf", "pip install ezdxf"),
("pydantic", "pip install pydantic"),
]:
total += 1
try:
importlib.import_module(pkg)
ok_count += check(pkg, True)
except ImportError:
ok_count += check(pkg, False, install)
print("\nOptional packages:")
for pkg, install, purpose in [
("win32com", "pip install pywin32", "COM backend"),
("PIL", "pip install Pillow", "Screenshots (COM)"),
("matplotlib", "pip install matplotlib", "PDF export + screenshots (ezdxf)"),
]:
total += 1
try:
importlib.import_module(pkg)
ok_count += check(f"{pkg} ({purpose})", True)
except ImportError:
ok_count += check(f"{pkg} ({purpose})", False, install)
print("\nEnvironment variables:")
for var, default in [
("AUTOCAD_MCP_BACKEND", "auto"),
("ALLOWED_PATHS", "(empty = all paths allowed)"),
("MAX_UNDO_STACK", "50"),
("LOG_LEVEL", "INFO"),
("DANGEROUS_COMMANDS_ENABLED", "false"),
]:
val = os.environ.get(var, "")
total += 1
ok_count += check(var, True, "")
if val:
print(f" Current value: {val}")
else:
print(f" Default: {default}")
print(f"\nResult: {ok_count}/{total} checks passed")
return 0 if ok_count >= 4 else 1
if __name__ == "__main__":
sys.exit(main())
@@ -1,91 +0,0 @@
"""Validate drawing quality — run after creating/editing a drawing.
Usage: python scripts/validate_drawing.py [drawing_path]
This script checks for common drawing issues:
- Empty layers (layers with no entities)
- Entities on layer "0" (should use named layers)
- Entity count summary
"""
import json
import sys
def validate_stats(stats: dict) -> list[str]:
"""Validate entity statistics. Pass the result of analysis_entity_stats()."""
issues = []
if stats.get("total", 0) == 0:
issues.append("WARNING: Drawing has no entities")
by_layer = stats.get("by_layer", {})
if "0" in by_layer and by_layer["0"] > 0:
issues.append(
f"INFO: {by_layer['0']} entities on default layer '0' "
"(consider moving to named layers)"
)
return issues
def validate_layers(layers: list[dict]) -> list[str]:
"""Validate layer configuration. Pass the result of layer_list()."""
issues = []
for layer in layers:
name = layer.get("name", "")
if name == "0":
continue
if layer.get("color", 7) == 7 and name != "0":
issues.append(
f"INFO: Layer '{name}' uses default color (white/7). "
"Consider assigning a distinct color."
)
return issues
def main():
print("Drawing Validation Tool")
print("=" * 40)
print()
print("This tool validates output from MCP analysis tools.")
print("Pipe JSON from analysis_entity_stats() or layer_list().")
print()
print("Usage with MCP:")
print(" 1. Call analysis_entity_stats() → copy JSON output")
print(" 2. Call layer_list() → copy JSON output")
print(" 3. Review results for issues")
print()
print("Checks performed:")
print(" - Empty drawing detection")
print(" - Entities on default layer '0'")
print(" - Layers with default color")
print()
if not sys.stdin.isatty():
try:
data = json.load(sys.stdin)
if "total" in data:
issues = validate_stats(data)
elif isinstance(data, list):
issues = validate_layers(data)
else:
issues = ["ERROR: Unrecognized input format"]
if issues:
for issue in issues:
print(f" {issue}")
else:
print(" OK — No issues found")
except json.JSONDecodeError:
print(" ERROR: Invalid JSON input")
else:
print("No stdin input. Run interactively or pipe JSON data.")
return 0
if __name__ == "__main__":
sys.exit(main())
-4
View File
@@ -9,10 +9,6 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
# NOTE: non-blocking until the repo-wide ruff cleanup lands (tracked for the
# quality pass). The test job below is the real gate. Once the codebase is
# ruff-clean, drop `continue-on-error` to make lint blocking again.
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
+1 -2
View File
@@ -1,11 +1,10 @@
"""AutoCAD MCP Pro – backend package."""
from .base import AutoCADBackend, BlockInfo, CommandResult, DrawingInfo, EntityInfo, LayerInfo
from .base import AutoCADBackend, BlockInfo, DrawingInfo, EntityInfo, LayerInfo
from .com_backend import ComBackend
from .ezdxf_backend import EzdxfBackend
__all__ = [
"AutoCADBackend",
"CommandResult",
"EntityInfo",
"LayerInfo",
"BlockInfo",
+13 -54
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import math
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, TYPE_CHECKING
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from engineering.plan_spec import (
@@ -23,21 +23,6 @@ if TYPE_CHECKING:
# ---------------------------------------------------------------------------
@dataclass
class CommandResult:
ok: bool
payload: Any = None
error: str | None = None
def to_dict(self) -> dict:
d: dict[str, Any] = {"ok": self.ok}
if self.ok:
d["payload"] = self.payload
else:
d["error"] = self.error
return d
@dataclass
class EntityInfo:
handle: str
@@ -91,7 +76,7 @@ class DrawingInfo:
# ---------------------------------------------------------------------------
def normalize_lineweight(lw: "float | int | None") -> "int | None":
def normalize_lineweight(lw: float | int | None) -> int | None:
"""Coerce a lineweight to ezdxf/COM hundredths-of-a-mm integers.
AutoCAD and ezdxf store lineweights as integer hundredths (25 == 0.25 mm),
@@ -548,17 +533,17 @@ class AutoCADBackend(ABC):
# behaviour. The only backend-specific primitive is `_create_xline` — XLINE
# has no cross-backend equivalent among the standard entity_create_* tools.
_plan_spec: "PlanSpec | None" = None
_plan_spec: PlanSpec | None = None
async def drawing_plan(
self, intent: str,
sheet_size: "SheetSize" = "A3",
sheet_size: SheetSize = "A3",
scale: float = 1.0,
layer_set_id: "LayerSetId" = "mech",
layer_set_id: LayerSetId = "mech",
view_count: int = 1,
dim_style: "DimStyle" = "chain",
dim_style: DimStyle = "chain",
notes: list[str] | None = None,
) -> "PlanSpec":
) -> PlanSpec:
"""Commit a drawing intent before any geometry is created.
Returns a PlanSpec held on the backend for `drawing_critique`."""
from engineering.plan_spec import PlanSpec
@@ -575,15 +560,15 @@ class AutoCADBackend(ABC):
return plan
async def drawing_critique(
self, focus: list["CritiqueFocus"] | None = None,
) -> list["Issue"]:
self, focus: list[CritiqueFocus] | None = None,
) -> list[Issue]:
"""Run premium-quality checks; return zero issues before
`drawing_finalize`. `focus=None` runs all checks."""
from engineering.critique import run_critique
return await run_critique(self, focus)
async def point_from_snap(
self, handle: str, snap: "SnapType",
self, handle: str, snap: SnapType,
ref_x: float | None = None, ref_y: float | None = None,
) -> tuple[float, float]:
"""Return a deterministic snap point on `handle`.
@@ -769,7 +754,7 @@ class AutoCADBackend(ABC):
return {"ok": True, "layer": layer, "deleted": deleted}
async def drawing_apply_iso_layers(
self, standard: "LayerSetId" = "mech",
self, standard: LayerSetId = "mech",
) -> dict:
"""Bootstrap a full ISO-conformant layer set with correct colors
and lineweights. Idempotent."""
@@ -778,7 +763,7 @@ class AutoCADBackend(ABC):
return {"ok": True, "standard": standard, "layers": result}
async def dimension_auto(
self, handles: list[str], style: "DimStyle" = "chain",
self, handles: list[str], style: DimStyle = "chain",
offset: float = 10.0,
) -> list[EntityInfo]:
"""Generate ISO 129 dimensions across `handles` in the chosen style
@@ -828,7 +813,7 @@ class AutoCADBackend(ABC):
dx, dy = ex0 - sx0, ey0 - sy0
L0 = math.hypot(dx, dy) or 1.0
ux, uy = dx / L0, dy / L0
for i, (sx, sy, ex, ey) in enumerate(segs):
for i, (_sx, _sy, ex, ey) in enumerate(segs):
dim = await self.dimension_linear(
sx0, sy0, ex, ey,
sx0 - uy * (off + i * off), sy0 + ux * (off + i * off),
@@ -906,32 +891,6 @@ class AutoCADBackend(ABC):
return [e for e in ents if _ok(e)]
# ── concrete helpers (use existing primitives) ──────────────────────────
async def set_layer_active(self, name: str) -> None:
"""Convenience: alias for layer_set_current. Concrete; backends need not override."""
await self.layer_set_current(name)
async def ensure_linetypes(
self, names: list[str], file: str | None = None,
) -> dict[str, str]:
"""Idempotently load each linetype in `names`. Returns {name: status}.
Status is one of: 'already_loaded', 'loaded', 'failed: <msg>'.
"""
try:
current = {ln.lower() for ln in await self.linetype_list()}
except Exception:
current = set()
results: dict[str, str] = {}
for n in names:
if n.lower() in current:
results[n] = "already_loaded"
continue
try:
await self.linetype_load(n, file=file)
results[n] = "loaded"
except Exception as exc:
results[n] = f"failed: {exc}"
return results
# ---------------------------------------------------------------------------
+2 -7
View File
@@ -360,7 +360,6 @@ class ComBackend(AutoCADBackend):
def __init__(self):
self._executor: ThreadPoolExecutor | None = None
self._batch_mode = False
self._connected = False
self._transaction_active = False
@@ -413,7 +412,7 @@ class ComBackend(AutoCADBackend):
if timeout > 0:
return await asyncio.wait_for(future, timeout=timeout)
return await future
except asyncio.TimeoutError as e:
except TimeoutError as e:
log.error("COM call timed out after %.1fs; rebuilding executor", timeout)
# The worker thread is still blocked inside SendCommand and cannot be
# cancelled. Abandon it (shutdown(wait=False)) and start a fresh
@@ -656,7 +655,7 @@ class ComBackend(AutoCADBackend):
def _sync():
mspace = _msp()
# acPatternTypePreDefined = 0
hatch = mspace.AddHatch(0, pattern, True)
hatch = mspace.AddHatch(0, pattern, False)
hatch.PatternScale = float(scale)
hatch.PatternAngle = deg2rad(angle)
# Build outer loop as a temporary lwpolyline
@@ -886,7 +885,6 @@ class ComBackend(AutoCADBackend):
self, handle, rows, cols, row_spacing, col_spacing,
) -> list[EntityInfo]:
def _sync():
self._batch_mode = True
_COM_STATE["batch_mode"] = True
try:
doc = _acad_doc()
@@ -904,7 +902,6 @@ class ComBackend(AutoCADBackend):
entities.append(_entity_info(result))
return entities
finally:
self._batch_mode = False
_COM_STATE["batch_mode"] = False
_regen()
return await self._run(_sync)
@@ -913,7 +910,6 @@ class ComBackend(AutoCADBackend):
self, handle, count, fill_angle, center_x, center_y,
) -> list[EntityInfo]:
def _sync():
self._batch_mode = True
_COM_STATE["batch_mode"] = True
try:
doc = _acad_doc()
@@ -930,7 +926,6 @@ class ComBackend(AutoCADBackend):
entities.append(_entity_info(result))
return entities
finally:
self._batch_mode = False
_COM_STATE["batch_mode"] = False
_regen()
return await self._run(_sync)
+38 -19
View File
@@ -51,11 +51,6 @@ except ImportError:
# Helper utilities
# ---------------------------------------------------------------------------
_ACI_NAMES = {
1: "Red", 2: "Yellow", 3: "Green", 4: "Cyan",
5: "Blue", 6: "Magenta", 7: "White", 256: "ByLayer", 0: "ByBlock",
}
_BUILTIN_LINETYPES = {"continuous", "bylayer", "byblock"}
# AutoCAD-shipped linetypes that ezdxf.tools.standards does not include.
@@ -344,12 +339,17 @@ class EzdxfBackend(AutoCADBackend):
return await self._async(_sync)
async def drawing_save_as(self, path: str, fmt: str = "dxf") -> dict:
if fmt.lower() == "dwg":
raise RuntimeError(
"ezdxf backend cannot write true DWG binary. "
"Use save_path with a .dxf extension, or switch to the COM backend."
)
def _sync():
doc = self._require_doc()
doc.saveas(path)
self._doc_path = path
self._dirty = False
return {"ok": True, "path": path, "format": fmt}
return {"ok": True, "path": path, "format": "dxf"}
return await self._async(_sync)
async def drawing_export_dxf(self, path: str) -> dict:
@@ -693,7 +693,7 @@ class EzdxfBackend(AutoCADBackend):
dim = msp.add_aligned_dim(
p1=(float(x1), float(y1)),
p2=(float(x2), float(y2)),
dist=math.sqrt((float(dim_x) - float(x1)) ** 2 + (float(dim_y) - float(y1)) ** 2),
distance=math.sqrt((float(dim_x) - float(x1)) ** 2 + (float(dim_y) - float(y1)) ** 2),
)
dim.render()
ent = dim.dimension
@@ -708,11 +708,12 @@ class EzdxfBackend(AutoCADBackend):
) -> EntityInfo:
def _sync():
msp = self._msp()
vxf, vyf = float(vx), float(vy)
dim = msp.add_angular_dim_2l(
center=(float(vx), float(vy)),
p1=(float(x1), float(y1)),
p2=(float(x2), float(y2)),
distance=10.0,
base=(float(tx), float(ty)),
line1=((vxf, vyf), (float(x1), float(y1))),
line2=((vxf, vyf), (float(x2), float(y2))),
location=(float(tx), float(ty)),
)
dim.render()
ent = dim.dimension
@@ -875,11 +876,19 @@ class EzdxfBackend(AutoCADBackend):
d = float(distance)
if ent_type == "LINE":
# Offset a line: move parallel
# Offset a line: move parallel; side_x/side_y selects which side
start = Vec2(ent.dxf.start.x, ent.dxf.start.y)
end = Vec2(ent.dxf.end.x, ent.dxf.end.y)
direction = (end - start).normalize()
normal = Vec2(-direction.y, direction.x) * d
left_normal = Vec2(-direction.y, direction.x)
# Determine sign: positive = left side (default); flip if side point is on right
sign = 1.0
if side_x is not None and side_y is not None:
mid = (start + end) * 0.5
side_vec = Vec2(float(side_x), float(side_y)) - mid
if left_normal.dot(side_vec) < 0:
sign = -1.0
normal = left_normal * (d * sign)
new_start = start + normal
new_end = end + normal
msp = self._msp()
@@ -893,7 +902,13 @@ class EzdxfBackend(AutoCADBackend):
elif ent_type == "CIRCLE":
cx, cy = ent.dxf.center.x, ent.dxf.center.y
new_r = ent.dxf.radius + d
# side_x/side_y selects inside (shrink) or outside (grow); default = outside
sign = 1.0
if side_x is not None and side_y is not None:
dist_to_center = math.sqrt((float(side_x) - cx) ** 2 + (float(side_y) - cy) ** 2)
if dist_to_center < ent.dxf.radius:
sign = -1.0
new_r = ent.dxf.radius + d * sign
if new_r <= 0:
raise ValueError("Offset distance too large for circle")
msp = self._msp()
@@ -944,8 +959,12 @@ class EzdxfBackend(AutoCADBackend):
from ezdxf.math import Matrix44
results = []
cx, cy = float(center_x), float(center_y)
step = math.radians(float(fill_angle)) / max(int(count) - 1, 1)
for i in range(1, int(count)):
fa = float(fill_angle)
n = int(count)
# 360° full circle: n copies evenly spaced, no duplicate at 0°
divisor = n if abs(fa % 360.0) < 1e-6 else max(n - 1, 1)
step = math.radians(fa) / divisor
for i in range(1, n):
copy = ent.copy()
msp.add_entity(copy)
angle = step * i
@@ -1535,7 +1554,7 @@ class EzdxfBackend(AutoCADBackend):
async def entity_trim(self, target_handle, cutter_handle, keep_x, keep_y) -> EntityInfo:
def _sync():
from ezdxf.math import intersection_line_line_2d, Vec2
from ezdxf.math import Vec2, intersection_line_line_2d
target = self._get_entity(target_handle)
cutter = self._get_entity(cutter_handle)
if target.dxftype() != "LINE" or cutter.dxftype() != "LINE":
@@ -1574,7 +1593,7 @@ class EzdxfBackend(AutoCADBackend):
self, target_handle, boundary_handle, end_x=None, end_y=None,
) -> EntityInfo:
def _sync():
from ezdxf.math import intersection_line_line_2d, Vec2
from ezdxf.math import Vec2, intersection_line_line_2d
target = self._get_entity(target_handle)
boundary = self._get_entity(boundary_handle)
if target.dxftype() != "LINE" or boundary.dxftype() != "LINE":
@@ -1616,7 +1635,7 @@ class EzdxfBackend(AutoCADBackend):
"""Shared geometry setup for fillet/chamfer.
Returns (line1, line2, P, d1, d2, A_far, B_far, theta) where d1/d2 are
unit vectors from intersection P toward the far endpoint of each line."""
from ezdxf.math import intersection_line_line_2d, Vec2
from ezdxf.math import Vec2, intersection_line_line_2d
line1 = self._get_entity(handle1)
line2 = self._get_entity(handle2)
if line1.dxftype() != "LINE" or line2.dxftype() != "LINE":
+209
View File
@@ -0,0 +1,209 @@
The lineweight truncation bug is confirmed devastating: ENGINEERING_LAYERS uses floats like 0.50, 0.25, 0.18, 0.13, 0.05 mm, and `layer_create` does `int(lineweight)` (ezdxf_backend.py:1034), so every one floors to 0. Counts confirmed: 109 tools, 6 resources, 5 prompts; LICENSE/benchmarks/ci.yml all absent. All load-bearing claims verified against source. I have everything needed to synthesize.
# AutoCAD MCP Pro — Understanding & Research Dossier
## 1. Architecture at a Glance
AutoCAD MCP Pro is a **single-file FastMCP 3.0 server** (`server.py`, 2881 lines) exposing **109 `@mcp.tool` registrations, 6 `@mcp.resource` endpoints, 5 `@mcp.prompt` templates** (verified by grep, not the docs' "87"). It uses a **strategy-pattern dual backend** behind one ABC (`backends/base.py`, ~73 abstract methods + 5 dataclasses): a **live COM backend** (`com_backend.py`, pywin32, single STA-thread `ThreadPoolExecutor`) and a **headless ezdxf backend** (`ezdxf_backend.py`, file-based DXF, all sync work via `asyncio.to_thread` under one `asyncio.Lock`). Backend selection (`server.py::_make_backend`, server.py:69): env `AUTOCAD_MCP_BACKEND`; Windows `auto`/`com` tries COM then falls back to ezdxf; non-Windows always ezdxf.
Request path: `ErrorHandlingMiddleware → AuditMiddleware → TimingMiddleware → LoggingMiddleware` (server.py:197-200) → tool fn → `_backend(ctx)` (server.py:207, raises `ToolError` if backend None) → identically-named backend method → `_dc()` dataclass→dict serialization (server.py:216). Lifespan (`autocad_lifespan`, server.py:107) builds the backend singleton into `ctx.lifespan_context["backend"]`.
A **deterministic engineering layer** (`engineering/` package) backs the "premium" workflow: parametric gear/keyway/titleblock primitives, a closed-enum `drawing_critique`, an 8-step `DrawingValidator`, ISO layer/linetype scaffolds, and a `PlanSpec` plan-before-draw contract. **The central architectural fact: the premium workflow is fully implemented only on ezdxf; the COM backend stubs all 8 premium meta-tools with `NotImplementedError` (com_backend.py:1637-1660, verified) — yet COM is the default Windows backend.**
---
## 2. Per-Subsystem Summary
**`server.py` (transport + tool registry).** Thin async wrappers over the backend singleton, grouped into 13 sections (1–12 + sub-sections 8b/8c/8d batch/template/validation + 13 premium). Owns server-side composition tools (`entity_delete_many` server.py:1065, `layer_isolate` :1216, `block_find_references` :1362, `analysis_layer_stats` :1506, `entity_batch_*` :1538/:1590, `template_*` :1695/:1730, `validation_check` :1749). Entry point manually parses `--transport/--port/--host` (server.py:2866) and refuses non-loopback HTTP unless `ALLOW_REMOTE_HTTP` + `MCP_AUTH_TOKEN` (`_validate_http_bind`).
**`backends/base.py` (the contract).** `AutoCADBackend` ABC: ~73 `@abstractmethod`, 2 concrete (`set_layer_active`, `ensure_linetypes`), 5 dataclasses (`EntityInfo/LayerInfo/BlockInfo/DrawingInfo/CommandResult`), 3 helpers (`shoelace_area`, `deg2rad`, `rad2deg`). Both backends define a body for every abstract method (no structural gaps); divergences are behavioral.
**`backends/ezdxf_backend.py` (headless).** Full create/modify/query/analysis/dimension/block/layer + premium meta-tools + corner ops. Transactions = full DXF snapshot to tempfile on `_undo_stack` (trimmed to `max_undo_stack=5`, O(drawing-size) per `transaction_begin`). View ops and `system_run_command/run_lisp` are documented no-ops. Screenshot/PDF via matplotlib.
**`backends/com_backend.py` (live AutoCAD).** All COM through one STA worker with per-call timeout (`com_call_timeout`, default 60s) and executor-rebuild on timeout/fatal HRESULT. Corner ops via `_safe_send_command` (CMDACTIVE-polling). Win32 `PrintWindow` screenshots. **8 premium meta-tools + `block_create_from_entities` are stubs.**
**`engineering/` (deterministic CAD).** `gear.py` (involute spur/helical outlines), `keyway.py` (DIN 6885), `titleblock.py` (ISO 7200 A3), `layers.py` (mech/pid/iso13567 sets), `critique.py` (6 focuses), `validator.py` (8-step gate), `plan_spec.py` (PlanSpec/Issue + ISO-128 lineweight set). `section.py` (`cutting_plane_line`, `apply_section_hatch`) and `gear.generate_tooth_profile` are **dead code** (exported, zero callers/tests).
**`tests/` (184 functions, 14 files).** One ezdxf-only `backend` fixture (`conftest.py`). COM backend, server→sanitizer wiring, and `section.py` have **zero coverage**.
---
## 3. Complete MCP Tool Inventory (grouped; ✅ real / ⚠️ partial / ❌ stub-or-missing)
> All 109 tools are **registered** in `server.py` and have **real ezdxf implementations**. "Status" below reflects the **COM backend** (the Windows default) plus correctness caveats. Nothing is purely aspirational/unregistered.
| § | Tools | ezdxf | COM | Notes |
|---|---|---|---|---|
| 1 Drawing (12, doc says 11) | info, new, open, save, save_as, export_dxf, export_pdf, purge, audit, close, undo, redo | ✅ (redo ❌ no-op ezdxf_backend.py:487) | ✅ | `drawing_redo` inert on ezdxf; header says 11 but 12 registered (drawing_redo server.py:426) |
| 2 Entity Create (13) | line, circle, arc, polyline, rectangle(→polyline), text, mtext, hatch, spline, ellipse, point, block_ref | ✅ | ✅ | mtext has no rotation param on ezdxf (:578); hatch pattern-only single loop |
| 3 Dimensions (5) | linear, aligned, angular, radius, diameter | ⚠️ aligned/angular **xfail** (tests/test_dimensions.py:16); angular ignores tx/ty + fixed dist=10 (ezdxf:705) | ✅ | No dimstyle/tolerance support either backend |
| 4 Modify (14) | move, copy, rotate, scale, mirror, offset, trim, extend, fillet, chamfer, delete, array_rectangular, array_polar, set_properties | ⚠️ offset ignores side (:868); corner ops LINE+LINE only (:1535-1750); polar 360° dup (:946) | ⚠️ offset ignores side (:860); corner ops via SendCommand | |
| 5 Query (3) | entity_get, entity_list, entity_delete_many | ✅ | ✅ | COM entity_list = O(n) per-item COM reads (:967) |
| 6 Layer (14) | list, create, delete, set_current, modify, freeze, thaw, lock, unlock, hide, show, isolate, linetype_list, linetype_load | ⚠️ **lineweight int() truncation** (:1034) | ✅ | See RISK-2 |
| 7 Block (7) | list, insert, explode, get_attributes, set_attributes, create_from_entities, find_references | ✅ | ⚠️ create_from_entities ❌ stub (:1253) | BlockInfo.description never populated |
| 8 Analysis (8) | stats, find_in_region, measure_distance, measure_area, bounding_box, select_by_layer, select_by_type, layer_stats | ⚠️ region fallback insert/start only (:1330) | ✅ | hand-rolled loops vs ezdxf query/groupby |
| 8b Batch (2) | entity_batch_create, entity_batch_modify | ✅ | ✅ | |
| 8c Template (2) | template_apply_layers, template_list | ✅ | ✅ | Duplicates `_LAYER_TEMPLATES`/prompt layer sets |
| 8d Validation (1) | validation_check | ✅ | ✅ | |
| 9 View (4, doc says 5) | zoom_extents, zoom_window, screenshot, zoom_and_screenshot | ⚠️ zoom = no-op (:1394) | ✅ | Header says 5 |
| 10 Transactions (3) | begin, commit, rollback | ✅ (commit file-I/O outside lock :1452) | ⚠️ flag-leak on error (:1402) | |
| 11 System (6) | status, get_variable, set_variable, run_command, run_lisp, about | ⚠️ run_command/lisp no-op | ⚠️ run_lisp result always 'nil' (:1487); about catalog drifted | |
| 12 Engineering (7) | gear_draw_helical/spur/section_aa, keyway_draw_keyed_bore/section, titleblock_apply_iso_a3, drawing_finalize | ✅ | ✅ (use generic entity_create_*) | Validator/titleblock import-guarded → None risk |
| 13 Premium (8) | drawing_plan, drawing_critique, point_from_snap, construction_xline, construction_clear, drawing_apply_iso_layers, dimension_auto, entity_select_smart | ✅ | ❌ **all NotImplementedError** (:1637-1660) | **The documented-mandatory workflow is dead on COM** |
**Documented-but-missing entirely:** none. **Documented-as-working but stubbed on default backend:** the 8 premium tools (§13) + `block_create_from_entities` on COM.
---
## 4. Cross-Cutting RISK Register (merged, deduped, severity-sorted)
| # | Sev | Risk | Location | Evidence |
|---|---|---|---|---|
| R1 | **CRITICAL** | All 8 premium meta-tools `raise NotImplementedError` on COM (the Windows default). The "non-negotiable for production" workflow (`drawing_plan→apply_iso_layers→critique→finalize`) 500s on the primary platform. | com_backend.py:1637-1660 | **Verified verbatim**; ezdxf impls real (ezdxf_backend.py:1753-2050) |
| R2 | **CRITICAL** | `layer_create` does `int(lineweight)` so every sub-1.0 ISO mm value (0.50/0.25/0.18/0.13/0.05) floors to **0**. The entire ISO-128 lineweight discipline is silently nulled; `drawing_apply_iso_layers`/`drawing_new` bootstrap produce flat 0-weight layers. | ezdxf_backend.py:1034; values engineering/layers.py:16-26 | **Both verified.** ezdxf wants integer hundredths (50=0.50mm), not raw mm |
| R3 | **HIGH** | `_check_iso128` is a permanent no-op: `lw <= 0: continue` (critique.py:50) skips every layer made 0 by R2. The advertised ISO gate can never fire. Comment "the layer dataclass already normalises to mm" (critique.py:53) is **false** (ezdxf_backend.py:189 stores raw `float(lw)`). | engineering/critique.py:37-64 | **Verified verbatim** |
| R4 | **HIGH** | `_check_dim_overlap` reads `properties['text_position'/'defpoint'/'insertion_point']`, but the ezdxf DIM extractor sets only `dim_type` (ezdxf_backend.py:166-168) → `dim_points` always empty → check is a silent no-op. | engineering/critique.py:194-197 | — |
| R5 | **HIGH** | COM timeout path (`stuck.shutdown(wait=False)`) cannot kill the STA worker still blocked in SendCommand → **permanently leaks one CoInitialize'd thread + COM proxy per timeout**; no `CoUninitialize` anywhere. | com_backend.py:415-433, 68-71 | — |
| R6 | **HIGH** | Zero COM-backend test coverage (~80 methods incl. UndoMark transactions, SendCommand corner ops, CMDACTIVE guard). Passing ezdxf transaction tests give **false confidence** for the semantically different COM model. | tests/ (no ComBackend import); conftest.py:8-15 | — |
| R7 | **HIGH** | Server→sanitizer wiring untested: `test_security.py` tests `sanitize_command/lisp/validate_path` in isolation; no integration test proves `system_run_command/lisp` actually call them. A refactor dropping the call (server.py:2035/2052) passes all security tests while raw commands reach SendCommand. | server.py:2035,2052; tests/test_security.py | — |
| R8 | **HIGH** | LISP-aliasing bypass (`(setq f command)(f "ERASE")`) is claimed-defended in a comment (security.py:138-140) but has **no adversarial test**; UNC/`~`/symlink path escapes in `validate_path` also untested. | security.py:138-179; test_security.py:177-231 | — |
| R9 | **MED** | `entity_array_polar` full-360° fill double-places: `step=radians(360)/(count-1)` puts the last copy at 0°==original → duplicate. Hits the common bolt-circle/gear pattern the engineering layer relies on. | ezdxf_backend.py:946-947 | — |
| R10 | **MED** | `drawing_finalize` calls `drawing_save_as(str(validated))` with no `fmt`. ezdxf default `fmt='dxf'` (ezdxf:345) vs COM `fmt='dwg'` (com:515) vs save_as tool default `'dwg'` (server:336) → 3-way inconsistency; format backend-dependent, not path-derived. | server.py:2299 | **Verified** (omits fmt) |
| R11 | **MED** | `drawing_save_as(fmt='dwg')` on ezdxf writes DXF bytes to a `.dwg` path and returns `{'format':'dwg'}` — silent wrong-format file. ezdxf cannot write DWG; should error or use `odafc`. | ezdxf_backend.py:345-352 | — |
| R12 | **MED** | `dimension_angular` ignores tx/ty, fixed `distance=10.0` → arc radius wrong for large/small parts; breaks COM parity. | ezdxf_backend.py:705-722 | — |
| R13 | **MED** | `entity_offset` ignores `side_x/side_y` (both backends); only `distance` sign flips side; ARC/LWPOLYLINE/ELLIPSE raise "not supported". | ezdxf:868-908; com:860-874 | — |
| R14 | **MED** | Engineering titleblock/validator symbols import-guarded to `None` (engineering/__init__.py:40-51); if module truly fails to import, tools raise opaque "NoneType not callable" instead of clear ToolError. | server.py:2266,2294 | — |
| R15 | **MED** | `system_about` static `tool_groups` catalog drifted: omits all 15 engineering+premium tools, misfiles `entity_delete_many`. Agents inspecting capabilities can't see the CLAUDE.md-mandated tools. | server.py:2070-2106 | — |
| R16 | **MED** | COM `transaction_active` left stale-True if commit/rollback `_run` raises; `rollback` issues `EndUndoMark`+`UNDO B` SendCommand without the CMDACTIVE guard used elsewhere → can interleave mid-command. | com_backend.py:1402,1411-1421 | — |
| R17 | **MED** | `_safe_send_command` busy-polls CMDACTIVE with `time.sleep(0.05)` on the single STA thread → monopolizes the executor up to deadline; `deadline_s`(8s) decoupled from `_run` timeout(60s). | com_backend.py:1521-1536 | — |
| R18 | **MED** | `drawing_new` swallows bootstrap failures into `result['bootstrap']={'error':...}` and reports success → un-bootstrapped drawing looks normal though CLAUDE.md treats the scaffold as a precondition for gear_*/titleblock_*. | server.py:280-294 | — |
| R19 | **MED** | `dimension_aligned`/`dimension_angular` permanently `xfail(strict=False)` — advertised tools shipped broken; an accidental fix wouldn't alert. | tests/test_dimensions.py:16-27 | — |
| R20 | LOW | `_registered_tool_count` reaches into FastMCP privates (`mcp._local_provider._components`, `'tool:'` prefix); a minor upgrade → tool_count=-1 surfaced to clients. | server.py:231-234 | — |
| R21 | LOW | Involute flank prepends a straight radial root→base segment (no trochoid fillet) → geometrically wrong dedendum for `root_r<base_r`; root-arc gap when `root_r>=base_r`; degenerate arc for teeth≤2. | engineering/gear.py:40-50,163-190 | — |
| R22 | LOW | `drawing_undo`/`transaction_rollback` share one `_undo_stack` with no transaction-id/depth separation; reload doesn't reset `_doc_path`/`_current_layer`. | ezdxf_backend.py:470-484,1462-1476 | — |
| R23 | LOW | COM `system_run_lisp` always reports `result='nil'` (SendCommand returns None); no CMDACTIVE guard → a prompting LISP form deadlocks next call. | com_backend.py:1487-1492 | — |
| R24 | LOW | `_apply_attrs` sets `entity.dxf.linetype` without `_ensure_linetype_loaded` (unlike set_properties/layer_modify) → `linetype='HIDDEN'` on create may render Continuous or raise on save. | ezdxf_backend.py:491-501 | — |
| R25 | LOW | `block_find_references` calls `entity_list(type_filter='INSERT')` with no limit (bypasses `max_list_limit`) → unbounded INSERTs into memory; assumes every backend populates `properties['block_name']`. | server.py:1362-1370 | — |
| R26 | LOW | View zoom ops unconditionally registered but no-op on ezdxf → success response implies the view changed when it didn't; `view_zoom_and_screenshot` may not frame the requested region on ezdxf. | server.py:1814-1831,1870 | — |
| R27 | LOW | `validator.py` title check substring-matches 'SPUR'/'HELICAL SPUR' on TEXT layer → false-positive blocks `drawing_finalize` if any note contains 'SPUR'. | engineering/validator.py:166-181 | — |
| R28 | LOW | COM `_find_autocad_hwnd` grabs first visible window whose title contains 'AutoCAD' → may capture palette/splash/About dialog; not tied to `AcadApplication.HWND`. | com_backend.py:283-300 | — |
| R29 | LOW | Broad `except Exception: pass` swallows COM faults (disconnected/busy) at debug level in ~20 sites → bypasses the HRESULT reconnect logic in `_run`. | com_backend.py:199,244,868,901,1216,… | — |
---
## 5. Doc-vs-Code Drift Table
| Claim (source) | Reality | Verdict |
|---|---|---|
| "87 tools" (CLAUDE.md, README:6) | **109** `@mcp.tool` (verified) | STALE (soft — both add "count is dynamic" caveat) |
| "104 tests" (README:83) | **184** test functions across 14 files (verified) | STALE (~80 under) |
| Section 1 "(11 tools)" (CLAUDE.md) | 12 (`drawing_redo` server.py:426) | WRONG |
| Section 9 "(5 tools)" (CLAUDE.md) | 4 view tools registered | WRONG |
| "12-section taxonomy" (CLAUDE.md) | 1–12 + 8b/8c/8d + 13 = 6 extra groups | INCOMPLETE |
| Premium workflow "non-negotiable for production" (CLAUDE.md) | 8/8 premium tools dead on COM (default Windows backend) | OVERCLAIM |
| "MIT — see [LICENSE](LICENSE)" (README:409) | No LICENSE file (verified absent) | **DEAD LINK / legal drift** |
| `benchmarks/` referenced (README:39,302,367) | Absent (verified) | DEAD LINK (forward-looking) |
| ".github/workflows/ci.yml" + "GitHub Actions CI" (README:84,358) | git status `D`; absent on disk (verified) | **CI REMOVED while docs claim it** |
| "Premium discipline lives in `.claude/skills/autocad-mcp-premium/`" (CLAUDE.md) | Entire tree git-deleted (28 deletions) + all `.cursor/` deleted | DANGLING POINTER |
| README tests/ tree (README:347-355) | Omits 6 of 14 test files + engineering/ package | STALE |
| `system_about` tool_groups | Omits 15 engineering+premium tools | DRIFT |
| `BlockInfo.description` field (base.py:71) | Never populated by either backend | DEAD FIELD |
| critique.py:53 "normalises to mm" comment | Stores raw DXF lineweight (ezdxf:189) | FALSE COMMENT |
| `engineering/__init__.py:37-39` "Agent B … in flight"; com_backend "Task #7/#8/#9" | Multi-agent build seams shipped unreconciled | TODO-IN-PROD |
| Dockerfile, .env.example (README) | Present (verified) | OK |
---
## 6. Test Coverage Gaps (ranked)
1. **COM backend: 0% coverage** (com_backend.py entire file) — transactions (UndoMark vs ezdxf snapshot are *different* semantics), SendCommand corner ops, CMDACTIVE guard, timeout/HRESULT recovery, all 8 premium stubs. Highest-risk untested surface; it's the production path. (R6)
2. **Server→sanitizer integration** — no test proves tools invoke `sanitize_command/lisp`/`validate_path`. (R7)
3. **Sanitizer adversarial cases** — LISP aliasing, command word-boundary evasion (`.ERASE`), UNC/`~`/symlink path escapes — the comment claims defenses with no tests. (R8)
4. **Critique detection paths** — `iso128`, `layer_color`, `dim_overlap` only exercised on a *clean* drawing (assert they *don't* fire); their actual detection logic and the unknown-focus branch (critique.py:241) are never proven. (R3, R4)
5. **`dimension_auto` ordinate** (ezdxf:1988) untested; chain/baseline covered.
6. **`dimension_aligned`/`angular`** permanently xfail — shipped broken. (R19)
7. **Uncovered backend methods (both backends):** `system_get/set_variable`, `system_status`, `drawing_close`, `drawing_export_pdf`, `entity_create_block_ref`, `block_set_attributes`, `linetype_list/load` (load-bearing for bootstrap), `view_zoom_*`, `view_screenshot`.
8. **`engineering/section.py`** (dead code) — 0 coverage.
9. **Transaction nesting/error semantics** — only single begin→commit/rollback tested; nested, rollback-without-begin, layer/block restoration untested.
10. **Infrastructure:** CI workflow deleted (no PR gate); `pytest-cov` declared but no `--cov`/`fail-under`; several "template/validation" tests assert nothing about the named feature (vacuous `if start:` guards, test_batch_template.py:30-34).
---
## 7. Research Synthesis
### 7a. Academic techniques (2025–2026) that validate or upgrade this design
- **GeoGramBench (ICLR 2026, arxiv.org/abs/2505.17653):** frontier LLMs score **<50%** at high-abstraction Program-to-Geometry. → Empirically justifies the repo's no-coordinate-guessing rule and `point_from_snap`/`entity_select_smart`. **Action:** expand the deterministic geometry surface (`point_intersection`, `point_offset`, `point_along`) so the LLM *never* computes coordinates.
- **Self-Improving CAD w/ FEA feedback (arxiv.org/html/2605.17448v2):** deterministic controller owns validation, LLM owns design; **+13.4pp** per feedback round; frontier models get **zero** first-attempt strict passes — *iteration beats more tokens*. → This is the repo's architecture, but `drawing_critique` is one-shot. **Action:** make it a routed repair loop; `Issue` (plan_spec.py:64-81) already carries severity/handles/detail — add per-finding repair hints.
- **CADSmith (arxiv.org/abs/2603.26512):** 5-agent Planner/Coder/Executor/Validator/**Refiner**, dual loop, exact-kernel measurements + **independent VLM judge** → Chamfer 28.37mm→0.74mm (**38×**), IoU 0.81→0.96; *removing the rendered image alone made hard cases 35× worse.* → The repo renders a screenshot at finalize (server.py:2301-2309) but only checks bytes exist (validator step 7). **Highest-leverage single upgrade: feed that render to a VLM judge + add an automated Refiner.**
- **CADFusion (ICML 2025, arxiv.org/abs/2501.19054)** & **Seek-CAD (arxiv.org/abs/2505.17702):** render is a first-class signal; step-wise visual feedback catches errors mid-build. → Add a `visual` critique focus; run critique per construction step using the existing transaction model.
- **ProCAD "Clarify Before You Draw" (arxiv.org/html/2602.03045v1):** one-round clarification → invalidity 14.6%→0.9%, Chamfer ~80% lower. → `drawing_plan` records intent passively; add a pre-plan ambiguity/conflict detector that returns targeted questions before any `entity_create_*`.
- **FutureCAD (arxiv.org/abs/2603.11831):** NL → exact B-Rep primitive grounding fixes the fillet/chamfer "which primitive" problem. → Directly targets the repo's hardest API: trim/extend/fillet/chamfer need exact handles **plus** keep_x/keep_y. NL grounding ("fillet the top-left corner") removes the burden GeoGramBench shows LLMs fail at.
- **Text-to-CadQuery (2505.06507), CAD-Coder (2505.14646), EvoCAD (2510.11631):** field converged on Python/CadQuery macros; open local VLMs read CAD images reliably; candidate-generate + critique-as-fitness raises first-try quality. → Add more parametric meta-tools (the gear/keyway pattern) and/or a sandboxed parametric-script tool; an open VLM (CAD-Coder, **Florence-2 for GD&T**, arxiv.org/abs/2411.03707) keeps the visual tier off frontier APIs.
- **MUSE (arxiv.org/html/2605.28579) + CadBench (2605.10873):** grade manufacturability/functionality, not just shape; standard metrics = volumetric/surface IoU, Chamfer, **Invalidity Ratio**. → Replace `validator.py`'s regex/title heuristics with a staged code→geometric→rubric-judge protocol; emit a single scalar "drawing score" for regression tests (the repo has *no* quantitative quality metric).
### 7b. Competitive landscape
| Product | 3D | Render-critique loop | Validation gate | HTTP auth | Code escape hatch |
|---|---|---|---|---|---|
| **AutoCAD MCP Pro** | ❌ 2D only | ⚠️ finalize-only, symbolic | ✅ **unique** (critique+validator+ISO) | ❌ none | ⚠️ COM-only run_command/lisp |
| build123d-mcp/agentcad (github.com/pzfreo/build123d-mcp) | ✅ | ✅ in-loop | ❌ | — | ✅ |
| FreeCAD Robust MCP (150+ tools) | ✅ + FEM | ⚠️ | ❌ | — | ✅ |
| Blender MCP (~22k★) | ✅ | ✅ | ❌ | — | ✅ Python exec |
| RhinoMCP | ✅ | ✅ viewport | ❌ | — | ✅ |
| Fusion360/Onshape MCP | ✅ parametric | — | ❌ | ✅ OAuth/keys | — |
Snyk's 9-CAD-MCP roundup (snyk.io/articles/9-mcp-servers-for-computer-aided-drafting-cad-with-ai/): **vision only in FreeCAD/Blender/Rhino; design validation in none; HTTP auth only in Onshape/Fusion.** → **Our moat is the design-rule validation gate; the field beat us on 3D, in-loop render-critique, and auth.** Autodesk's own in-AutoCAD assistant demo (aecmag.com/features/autodesk-shows-its-ai-hand/) flagship = **automated compliance checking** — exactly our niche, so we must make standards compliance the sharpest, most-cited capability. Adam/Zoo (techcrunch.com/2025/10/31/...) set the UX bar: **parametric editability + post-gen dimension sliders + entity-level conversational editing**.
### 7c. Under-used COM / ezdxf API + ISO standards
**COM (ActiveX) gaps (every create call hits ModelSpace, com_backend.py:125-127):**
- **PaperSpace/Layouts + Plot config** — `drawing_export_pdf` (com_backend.py:528-534) calls bare `Plot.PlotToFile(path,'DWG To PDF.pc3')` with no page setup → ignores the A3/scale `drawing_plan` promised. Add `layout_*`/`viewport_*` (AddPViewport, StandardScale, CanonicalMediaName). (help.autodesk.com ActiveX ref)
- **AddTable** (BOM/revision tables), **AddMLeader** (ISO leader notes — CLAUDE.md *reserves* "leader notes" but no tool creates them), **Fields** (auto-updating title block), **GetDynamicBlockProperties** (parametric symbol libraries — `block_insert` only does attribute text, com:1195-1251), **DimStyles.Add+CopyFrom** (no ISO-129 dimstyle is ever set → undercuts the "Dimensions are ISO 129" rule), **named UCS** (per-view frames for `view_count`), **AttachExternalReference** (shared borders), **gradient/island hatch**.
- **3D solids/regions/booleans** — `system_status` advertises `all_entity_types` (com:1443) which is **false**; either implement `solid_*` or correct the claim.
- **Push selection into AutoCAD:** `entity_list` (com:967) and `analysis_select_by_type` (com:1352) loop ModelSpace.Item(i) in Python (O(n) COM round-trips — the #1 perf killer); `analysis_select_by_layer` (com:1339) already shows the right `SelectionSet.SelectAll` group-code-filter pattern to copy.
**ezdxf gaps (Grep confirms zero use of `set_gradient/set_solid_fill/paperspace/add_viewport/odafc/export_dwg/groupby/dimstyles/MTextEditor/add_ordinate/qsave`):**
- **`addons.odafc.export_dwg`** = real DWG (R12–R2018) → fixes R11/R10 (drawing_save_as fake DWG). (ezdxf.mozman.at/docs/addons/odafc.html)
- **drawing-addon `Configuration`** (lineweight_policy ABSOLUTE, background_policy, SVGBackend/PyMuPdfBackend) → true ISO lineweights + vector output, replacing raw matplotlib (ezdxf:357-379,1400-1424).
- **`query()` mini-language + `groupby()`** → replace hand-rolled loops in analysis_stats/select/entity_select_smart (which pulls 5000 entities then filters in memory, ezdxf:2008-2050).
- **`recover.readfile` + `Auditor.fix`** → real repair; `drawing_audit` (ezdxf:448) reports but never fixes; `drawing_open` (ezdxf:309) has no recover fallback.
- **`MTextEditor`** (stacked tolerance fractions), **Hatch gradients/islands/edge-paths**, **DimStyle overrides** (no ISO dimstyle today, ezdxf:668-774), **path-module offset** (generalizes LINE/CIRCLE-only offset + LINE-only corner ops).
**ISO standards conformance:**
- ✅ **ISO 128-2** lineweight set encoded (plan_spec.py:85, verified) — but **nulled by R2** and group-vs-element-class not checked.
- ⚠️ **ISO 7200** title block — A3-only hardcoded (titleblock.py:16-17), informal labels ("DWG NO" vs ISO field names), len×0.3 centering heuristic (titleblock.py:171), missing "Document type"/"Date of issue".
- ❌ **ISO 129-1 dimensioning** — biggest functional gap: `dimension_*` (base.py:226-257) have **no** param for ±/limit/fit (H7/g6) tolerances or text override → toleranced production dims impossible. (iso.org/obp/ui/#iso:std:iso:129)
- ❌ **ISO 1101 / ASME Y14.5 GD&T** — entirely absent (grep-confirmed): no feature control frame, datum, or ISO 2768 general-tolerance note. (gdandtbasics.com/iso-vs-asme-standards/)
- ⚠️ **ISO 13567** — static mech-only starter list (layers.py:51-62), no field-grammar parser/validator.
---
## 8. "What Would Make This the BEST AutoCAD MCP" — Prioritized Thesis
The evidence converges: **the repo is architecturally ahead on dual-backend + standards-validation but behind on the closed-loop, the production-output plumbing, and the very workflow it markets.** Priorities, evidence-backed:
**P0 — Make the marketed product actually work on its default platform.**
1. **Implement the 8 premium meta-tools on COM** (R1, com_backend.py:1637-1660). Today the CLAUDE.md-mandatory workflow 500s on every Windows install. Nothing else matters until this ships.
2. **Fix the lineweight `int()` truncation** (R2, ezdxf_backend.py:1034 → multiply mm by 100 / map to ACAD enum). This single bug nullifies the ISO-128 system *and* makes the iso128 critique a permanent no-op (R3). The standards-validation gate is the **competitive moat** (Snyk roundup; Autodesk's own flagship demo) — it must be real.
3. **Add CI + a coverage gate + the first COM smoke tests** (R6, ci.yml deleted; pytest-cov unconfigured). Restore the LICENSE file (legal drift).
**P1 — Own the moat by closing the loop (the #1 accuracy lever in every 2025–26 paper).**
4. **Turn `drawing_critique` into a routed iterative Refiner loop** with per-`Issue` repair hints and a bounded budget (CADSmith Refiner; FEA-agent +13.4pp/round; Physics-in-the-Loop 3.3–6.0 iters converge). The `Issue` dataclass already carries handles+detail.
5. **Promote the finalize screenshot to a first-class `visual` critique focus fed to a VLM judge** (CADSmith: dropping the render → 35× worse; CADFusion). Use an **open local VLM** (CAD-Coder / Florence-2 for GD&T) behind a `[vision]` extra so the headless path is unaffected. Add a **dimension round-trip check** (VLM-read dims vs created `dimension_*` entities) to catch what proximity-only `dim_overlap` (R4) cannot.
6. **Add a ProCAD-style pre-plan clarification pass to `drawing_plan`** (invalidity 14.6%→0.9%): detect missing/conflicting dims, material, tolerance, view_count-vs-intent before any geometry.
7. **Emit a scalar "drawing score" / Invalidity-Ratio** (MUSE/CadBench) so the premium workflow is regression-testable — the repo has no quantitative accuracy metric.
**P2 — Reach production-drawing parity (what un-toleranced ISO 128 can't deliver).**
8. **ISO 129 tolerances + ISO 1101/Y14.5 GD&T**: add ±/limit/fit params to `dimension_*` and a `gd_frame(symbol, tol, datums, modifiers)` + datum/ISO-2768-note primitive. This is the categorical gap blocking real part drawings; no surveyed text-to-CAD product covers 2D GD&T — a differentiator.
9. **Real plotted sheets**: COM Layout/PaperSpace/Plot-config so `drawing_plan(sheet_size, scale)` and `titleblock_apply_iso_a3` produce a correct A-series plot; on ezdxf wire `odafc.export_dwg` (kills the fake-DWG R11) and the drawing-addon `Configuration` for true-lineweight SVG/PDF.
10. **An ISO-129 `DimStyle`** created once and applied to all dims (COM `DimStyles.Add+CopyFrom`; ezdxf dimstyle overrides) so "Dimensions are ISO 129" becomes true, and **MLEADER** for the leader notes the docs already reserve.
**P3 — Catch the field on breadth (table-stakes elsewhere, absent here).**
11. **A 3D solids tier** (extrude/revolve from 2D profiles + booleans on COM; STEP/STL on ezdxf) — *or* correct the false `all_entity_types` claim (com:1443). Every competitor has 3D.
12. **Parametric editability + entity-level conversational editing** (Adam/Zoo): capture driving dims in PlanSpec, expose a re-drive tool; "select this circle → make it the 20mm bore" via existing handle model + `entity_select_smart`/`point_from_snap`.
13. **Authenticated remote HTTP** (API-key middleware) given `dangerous_commands_enabled` exists (server.py:111); a **standard-parts/symbol library** (`library_insert` over `block_insert`, dynamic-block props); **NL→B-Rep grounding** (FutureCAD) over the brittle trim/fillet/chamfer keep-point API.
**P4 — Hygiene that compounds with the above.** Generalize LINE-only corner ops + LINE/CIRCLE-only offset via the ezdxf path module; replace hand-rolled selection with `query()`/`groupby()` (ezdxf) and `SelectionSet.SelectAll` filters (COM); fix `entity_array_polar` 360° dup (R9), `dimension_angular` tx/ty (R12), the COM thread-leak `CoUninitialize` (R5); delete dead `section.py`/`generate_tooth_profile`; reconcile `system_about` catalog (R15) and the stale README/CLAUDE.md drift (§5).
**Files central to every priority:** `backends/com_backend.py:1637-1660` (P0.1), `backends/ezdxf_backend.py:1034` + `engineering/layers.py:14-26` (P0.2), `engineering/critique.py:37-64,194-197` (P0.2/P1.5), `engineering/plan_spec.py:64-87` (P1.4/P1.6/P2.8), `backends/base.py:226-257` (P2.8), `server.py:528-534/2294-2309` (P2.9).
+309
View File
@@ -0,0 +1,309 @@
<!-- Generated by the DIAGNOSTICS workflow (wf_c3e5770d-2cf), 2026-06-18.
Verified: 31 | Confirmed: 28 | Refuted/fixed: 2 | New from sweep: 17.
NOTE: R7/R8 (sanitizer wiring + LISP-aliasing/path-traversal defensive tests) were
NOT auto-verified — the security subagents hit a provider cyber-safety filter.
They are tracked for manual defensive review during the hardening work item. -->
# DIAGNOSTICS Report — AutoCAD MCP Pro
Synthesized from the verified recon risk register + fresh-sweep findings. REFUTED / ALREADY-FIXED items are excluded from the ranked body and listed in the appendix. Duplicates between the two sources are merged. Line numbers reflect **current** code (verified against the working tree).
---
## CRITICAL
_None._
---
## HIGH
### N1 — COM `entity_create_hatch` builds an ASSOCIATIVE hatch then deletes its boundary polyline
- **Severity:** HIGH
- **Location:** `backends/com_backend.py:659-673`
- **Description:** `mspace.AddHatch(0, pattern, True)` requests an *associative* hatch linked to the appended boundary loop, but the code then calls `bnd_pline.Delete()` (`com_backend.py:673`) on that very polyline. In live AutoCAD this either errors on `Delete`, drops the boundary, or leaves a hatch whose boundary tracking breaks on the next regen/edit.
- **Fix:** Change the 3rd arg to non-associative — `AddHatch(0, pattern, False)` — so the computed fill survives deleting the throwaway boundary. If associativity is truly wanted, keep the boundary polyline instead of deleting it.
### N2 — `drawing_save_as` default format diverges (ezdxf `"dxf"` vs COM/ABC `"dwg"`); `drawing_finalize` relies on the default
- **Severity:** HIGH (merges register **R10**/**R11** with the fresh-sweep duplicate)
- **Location:** ABC default `backends/base.py:158` (`"dwg"`); ezdxf `backends/ezdxf_backend.py:346` (`"dxf"`); COM `backends/com_backend.py:516` (`"dwg"`); consumed with no fmt at `server.py:2299` (`drawing_finalize` → `backend.drawing_save_as(str(validated))`).
- **Description:** `drawing_finalize` passes no `fmt`, so each backend's default decides the output. Two distinct real defects layer here:
1. **ezdxf silently mislabels DWG (R11):** `drawing_save_as` *ignores* `fmt` entirely — `doc.saveas(path)` derives format from the extension — yet still returns `{"format": fmt}`. For `fmt="dwg"`/`"dwt"` it writes DXF tag-stream bytes (ezdxf has no DWG/DWT writer) while echoing `format:"dwg"`. (`ezdxf_backend.py:346-353`)
2. **COM writes DWG content under a `.dxf` name (R10):** COM's `SaveAs(path, fmt_map[fmt])` honors `fmt`, *not* the extension, so a `.dxf` save_path under COM yields DWG bytes in a `.dxf`-named file.
- Net: identical `drawing_finalize(save_path=...)` produces different on-disk formats per backend, decoupled from the extension the user supplied.
- **Fix:** In `drawing_finalize`, derive `fmt` from the `save_path` extension and pass it explicitly. Unify the ABC + both impl signatures to one default. In ezdxf, reject `fmt not in {dxf, dwt}` (raise `ToolError` for `dwg`) instead of writing DXF and echoing `format=fmt`.
---
## MED
### R4 — `dim_overlap` critique focus is a guaranteed silent no-op (no backend populates the keys it reads)
- **Severity:** MED (claimed HIGH; downgraded — silently returns `[]`, no incorrect output)
- **Location:** reads `engineering/critique.py:199-201`; extractors `backends/ezdxf_backend.py:167-169` and `backends/com_backend.py:204-244` (verified at `critique.py:199-201`).
- **Description:** `_check_dim_overlap` reads `text_position`/`defpoint`/`insertion_point`, but the ezdxf DIM branch sets only `props["dim_type"]` and COM dims get only a generic `bounding_box`. None of the three keys is produced anywhere, so `ip` is always falsy, `dim_points` stays empty, and the focus always returns `[]`.
- **Fix:** Populate a dim reference point (text midpoint / defpoint) in both `_entity_info` extractors for DIM types, OR fall back to `properties["bounding_box"]` center in `_check_dim_overlap`.
### R9 — ezdxf `entity_array_polar` overlaps the original on full-circle fill and diverges from COM
- **Severity:** MED
- **Location:** `backends/ezdxf_backend.py:947-948`
- **Description:** `step = radians(fill_angle) / max(count-1, 1)` with loop `range(1, count)` places the last copy at `step*(count-1) = 360°` for a full ring — coincident with the original at 0°. Also diverges from COM (`com_backend.py:921` uses AutoCAD `ArrayPolar`, spacing `360/count`): `count=4, fill=360` → COM 90/180/270 vs ezdxf 120/240/360. `count-1` is only correct for partial-arc fills.
- **Fix:** `divisor = count if abs(fill_angle % 360) < eps else max(count-1, 1)`.
### R12 — ezdxf `dimension_angular` ignores `tx/ty` and hardcodes `distance=10.0`
- **Severity:** MED
- **Location:** `backends/ezdxf_backend.py:706-723` (hardcode at `:715`)
- **Description:** Accepts `tx/ty` (advertised as "text position X/Y" at `server.py:721-722`) but calls `add_angular_dim_2l(..., distance=10.0)`, ignoring them. COM honors `tx/ty` via `AddDimAngular` (`com_backend.py:770-784`), so identical inputs place the arc differently per backend; the fixed 10.0 mislocates the arc for very large/small parts.
- **Fix:** Pass `location=(tx,ty)` to `add_angular_dim_2l`, or compute `distance=hypot(tx-vx, ty-vy)`.
### R13 / N2-offset — `entity_offset` ignores `side_x/side_y` in BOTH backends; side semantics diverge; ezdxf supports only LINE/CIRCLE
- **Severity:** MED (merges register **R13** with the two fresh-sweep offset findings)
- **Location:** ezdxf `backends/ezdxf_backend.py:869-909`; COM `backends/com_backend.py:861-875`; ABC sig `backends/base.py:315-319`; tool `server.py:859-868`.
- **Description:** Both backends accept `side_x/side_y` and never read them (dead params; the tool advertises them as "a point on the offset side"). ezdxf hard-fixes the LINE normal `Vec2(-dir.y, dir.x)*d` (`:882`) and CIRCLE `radius+d` (`:896`); every other type raises `RuntimeError("Offset not supported …")` (`:908`) — ARC/LWPOLYLINE/POLYLINE/ELLIPSE/SPLINE unsupported, contradicting the tool's "line, circle, or polyline" claim. COM delegates to `ent.Offset(distance)`, whose sign/side convention is not guaranteed to match ezdxf's fixed left-normal — same `(handle, distance)` can offset to opposite sides per backend.
- **Fix:** Honor `side_x/side_y` in both backends (pick the copy whose ref point is nearest the side point, or sign `distance` from the cross product against the offset direction). In ezdxf, support ARC/LWPOLYLINE/POLYLINE/ELLIPSE or drop the documented polyline/side claims. Reconcile so positive distance means the same side in both engines.
### R15 — `system_about` `tool_groups` static dict omits ~20 registered tools and misfiles one
- **Severity:** MED
- **Location:** `server.py:2070-2106`
- **Description:** The hand-maintained `tool_groups` dict omits `drawing_close`, `entity_trim/extend/fillet/chamfer`, all 7 engineering tools, and all 8 premium tools (~15+ shown), and lists `entity_delete_many` under `entity_creation` though it's tagged `{entity, modify}` with `destructiveHint=True`. Live `total_tools` stays correct; only the per-group breakdown drifts.
- **Fix:** Derive `tool_groups` dynamically from registered tools' tags, or at minimum add the engineering/premium groups, move `entity_delete_many`, and add the 5 missing modify/drawing tools.
### R18 — `drawing_new` bootstrap failures return as success
- **Severity:** MED
- **Location:** `server.py:282-293`; per-item swallow at `engineering/layers.py:90-91, 128-129`
- **Description:** Bootstrap is wrapped in `try/except` that stores `{"error": …}` and still `return result` representing a created drawing; per-item failures are caught internally as `"failed: {exc}"` strings. A fully- or partially-failed bootstrap looks like success, so geometry can land on missing engineering layers — a documented precondition violation.
- **Fix:** Surface `bootstrap_ok=False` / `status="degraded"` and/or raise `ToolError` when `bootstrap=True` and any linetype/layer reports `failed` or the catch-all fires.
### R19 — ezdxf `dimension_aligned` / `dimension_angular` ship broken (xfail strict=False masks fix/regression)
- **Severity:** MED
- **Location:** markers `tests/test_dimensions.py:16, 23`; root cause `backends/ezdxf_backend.py:696` (`dist=`) and `:711-715` (`center=/p1=/p2=/distance=`)
- **Description:** ezdxf 1.4.3 `add_aligned_dim()` is `(p1, p2, distance, …)` but code passes `dist=`; `add_angular_dim_2l()` is `(base, line1, line2, …)` but code passes `center=/p1=/p2=/distance=` → `TypeError`. Both are advertised MCP tools (`server.py:695, :714`) and recommended in prompt text (`:2639`). `xfail(strict=False)` means an accidental fix XPASSes silently.
- **Fix:** `dist→distance`; `base=(vx,vy)`, `line1=((vx,vy),(x1,y1))`, `line2=((vx,vy),(x2,y2))`. Change markers to `strict=True` (or remove) so the suite alerts.
### R22 — ezdxf transaction snapshots share the single `_undo_stack` with undo history; reload leaves `_current_layer` stale
- **Severity:** MED
- **Location:** `backends/ezdxf_backend.py:213` (single stack), `drawing_undo` `471-485`, `transaction_begin/commit/rollback` `1429-1477`, `system_status` `1489`
- **Description:** One `_undo_stack` is pushed/popped by both undo and transactions, with no txn id/marker/depth isolation; `system_status` reports `len(_undo_stack)` *as* `transaction_depth`. A `drawing_undo` silently consumes an open transaction's snapshot. On reload, undo/rollback re-read the doc but never reset `_current_layer`, so `is_current` reporting goes stale vs the snapshot's `$CLAYER`.
- **Fix:** Use a dedicated transaction list (or begin-markers/ids) distinct from undo history; on reload re-read `$CLAYER` into `self._current_layer`.
### R5 — COM thread/apartment leak on `SendCommand` timeout (no `CoUninitialize`)
- **Severity:** MED
- **Location:** `backends/com_backend.py:416-434` (timeout path), `69-72` (`_com_init`), `387-391` (disconnect)
- **Description:** `_com_init` calls `CoInitialize()` as the executor initializer; `CoUninitialize` appears nowhere in source. On `TimeoutError`, `stuck.shutdown(wait=False)` cannot cancel a worker still blocked in `SendCommand` (the inline comment admits "Abandon it"), and a fresh executor is built — each timeout permanently leaks one `CoInitialize`'d thread + the COM apartment it pinned. `disconnect()` has the same pattern.
- **Fix:** Bound the leak (track abandoned executors, avoid unbounded re-creation); add per-thread `CoUninitialize` via worker-side `try/finally`/thread-local teardown so apartments release when a timed-out thread eventually unblocks.
### R6 — COM backend (1651 lines, 85 async methods) has zero test coverage
- **Severity:** MED
- **Location:** `tests/conftest.py:5` (only `EzdxfBackend` imported); untested logic incl. CMDACTIVE guard `com_backend.py:1474`, transactions `1397-1419`
- **Description:** No test references `ComBackend`/`win32`/`SendCommand`/`CMDACTIVE`. The `backend` fixture instantiates only ezdxf. SendCommand-driven commands, the CMDACTIVE busy-guard + poll loop, and transaction begin/commit/rollback are unexercised, so COM logic can regress silently on non-Windows CI.
- **Fix:** Add `tests/test_com_backend.py` unit-testing the mockable logic (CMDACTIVE guard, `normalize_lineweight` usage, `_safe_send_command` polling, value coercion) with a mocked COM `app`/`doc`, gated by `@pytest.mark.skipif` when `win32com` is unavailable.
### N3 — `entity_select_smart` `length_range` always rejects ARCs (no backend populates ARC `"length"`)
- **Severity:** MED
- **Location:** `backends/base.py:897-904`
- **Description:** The gate fires for `("LINE","ARC")` and reads `e.properties.get("length")`, but the ARC extractor branches (`ezdxf_backend.py:135-139`, `com_backend.py:217-222`) set only center/radius/angles — never `length`. So every ARC has `length is None → return False`, silently excluding all arcs despite the advertised ARC support.
- **Fix:** Populate `props["length"]` for ARC (= `radius * |Δangle|` in radians, with wraparound) in both extractors, OR compute arc length on the fly in `entity_select_smart`, OR drop "ARC" from the gate and docstring.
### N4 — `dimension_auto` hardcodes `layer="DIM"`, which does not exist in the `iso13567` layer set
- **Severity:** MED
- **Location:** `backends/base.py:822, 836, 845, 852`
- **Description:** Every `dimension_linear` inside `dimension_auto` passes `layer="DIM"`, but the iso13567 set's dim layer is `"M-DIMEN-T-N"` (`engineering/layers.py:57`). After `drawing_apply_iso_layers("iso13567")`, dims land on an auto-created `"DIM"` (default color 7 / lw -3), bypassing the ISO layer. `_check_layer_color` won't flag it because `_EXPECTED_LAYER_COLOR` is built only from `ENGINEERING_LAYERS`. (The hardcoded `"CONSTRUCTION"` in `construction_xline/clear`, `base.py:743/:754`, has the same iso13567 mismatch — its construction layer is `"M-CONST-E-N"`.)
- **Fix:** Resolve the dimension (and construction) layer from the active layer set via a registry lookup keyed on `PlanSpec.layer_set_id` (mech→DIM, pid→DIM, iso13567→M-DIMEN-T-N) instead of literals.
### N5 — `EntityInfo.properties` shape diverges by backend (`bounding_box`, TEXT `rotation`, MTEXT `char_height`)
- **Severity:** MED (merges three fresh-sweep property-parity findings)
- **Location:** `bounding_box`: `com_backend.py:192-199` (every entity) vs `ezdxf_backend.py:122` (never); TEXT/MTEXT: `ezdxf_backend.py:144-152` vs `com_backend.py:229-237`.
- **Description:** COM seeds `properties["bounding_box"]` for *every* entity; ezdxf never adds it. ezdxf TEXT sets `rotation` and MTEXT sets `char_height`; COM TEXT/MTEXT set neither (COM MTEXT sets no height at all). Any client reading these keys works on one backend and `KeyError`s on the other.
- **Fix:** Pick a contract and enforce it both ways: add `bounding_box` to ezdxf via `ezdxf.bbox.extents` (or drop it from COM and expose extents only via `analysis_bounding_box`); add `rotation`/`char_height` to the COM TEXT/MTEXT branches (`rad2deg(entity.Rotation)`, `entity.Height`). Document the props contract.
### N6 — COM `system_run_lisp` always reports `result:"nil"` (SendCommand is void)
- **Severity:** MED (duplicates register **R23**'s value-capture half; R23's no-CMDACTIVE-guard half tracked at LOW below)
- **Location:** `backends/com_backend.py:1488-1493`
- **Description:** `result = doc.SendCommand("(progn …)")` then `str(result) if result else "nil"`. AutoCAD COM `SendCommand` is void-returning → `result` is always `None` → the tool unconditionally returns `"nil"` regardless of the actual LISP value.
- **Fix:** Drop the bogus `result` field, or capture out-of-band: `(setvar "USERS1" (vl-princ-to-string <expr>))` then `GetVariable("USERS1")` after completion.
---
## LOW
### R10/R11 residual — _covered under N2 (HIGH)._ No separate LOW entry; the format-default divergence is fully captured there.
### R16 — COM `transaction_commit/rollback` leave `_transaction_active` stale on error; rollback's `UNDO B` bypasses the CMDACTIVE guard
- **Severity:** LOW
- **Location:** `backends/com_backend.py:1406-1423` (commit `1406-1413`, rollback `1415-1423`; begin mirror at `1402-1403`)
- **Description:** `_transaction_active` is set/cleared *after* `await self._run(_sync)` with no `try/finally`; if `_run` raises, the flag stays stale. Rollback's `_sync` issues raw `doc.SendCommand("_UNDO B\n")` (`:1419`) without the CMDACTIVE pre-check/poll that `system_run_command`/`_safe_send_command` use — the documented deadlock hazard.
- **Fix:** Clear `_transaction_active` in a `finally` around `_run`; route rollback's `UNDO B` through `_safe_send_command`.
### R20 — `_registered_tool_count()` reaches through FastMCP private attrs; fails to `-1` and surfaces it
- **Severity:** LOW
- **Location:** `server.py:225-234` (private access at `:231-232`)
- **Description:** `mcp._local_provider._components` + the undocumented `"tool:"` key prefix, with `except Exception: return -1`. `-1` is surfaced unguarded as `tool_count` (`server.py:1969/1976/1983`) and `total_tools` (`:2107`). A minor `fastmcp>=3.0` restructuring silently degrades the count to `-1`.
- **Fix:** Use the public API (e.g. `len(await mcp.get_tools())`), making the helper async.
### R23 — COM `system_run_lisp` has no CMDACTIVE guard (deadlock on prompting LISP)
- **Severity:** LOW (value-capture half merged into N6)
- **Location:** `backends/com_backend.py:1488-1493`
- **Description:** Unlike `system_run_command` (guards at `1473-1482`) and `_safe_send_command` (polls at `1521-1537`), `system_run_lisp` sends raw with no CMDACTIVE pre-check/poll. A prompting form (`(command "_LINE")`, `(getpoint)`) leaves CMDACTIVE non-zero and deadlocks the next COM call.
- **Fix:** Route through `_safe_send_command` (or add the same pre-send guard + post-send poll).
### R24 — ezdxf `_apply_attrs` assigns linetype without `_ensure_linetype_loaded` → undefined names silently render Continuous
- **Severity:** LOW
- **Location:** `backends/ezdxf_backend.py:501-502`
- **Description:** `_apply_attrs` does `entity.dxf.linetype = linetype` with no load check, unlike `entity_set_properties:981`, `layer_create:1030`, `layer_modify:1075`. `entity_create_line` forwards a documented `linetype` param (`server.py:446`; a prompt even says "use entity_create_line with CENTER linetype"), but `_BUILTIN_LINETYPES` is only `{continuous, bylayer, byblock}`, so HIDDEN/CENTER fall through to Continuous.
- **Fix:** Call `_ensure_linetype_loaded(self._require_doc(), linetype)` before the assignment.
### R26 — ezdxf `view_zoom_window` is a no-op but reports success; `view_zoom_and_screenshot` ignores the window region
- **Severity:** LOW
- **Location:** `backends/ezdxf_backend.py:1395-1399` (zoom no-ops), `1401-1425` (screenshot ignores window); `server.py:1887-1893`
- **Description:** ezdxf zoom ops return `{"ok": True, "message": …}` while doing nothing. `view_zoom_and_screenshot` passes window coords to the no-op, then `view_screenshot` renders the entire modelspace via `draw_layout(msp)` with `bbox_inches="tight"`, ignoring `x1/y1/x2/y2` — so the image frames the whole drawing despite the docstring promising a window.
- **Fix:** Store a pending crop region in `view_zoom_window` and apply via `ax.set_xlim/set_ylim` (drop `bbox_inches="tight"` when cropping); or return `ok:False` for ezdxf window zoom so success no longer implies framing.
### R28 — COM screenshot HWND via title-substring `EnumWindows` instead of `AcadApplication.HWND`
- **Severity:** LOW
- **Location:** `backends/com_backend.py:284-301` (`:292`)
- **Description:** `_find_autocad_hwnd` picks the first visible top-level window whose caption contains `"AutoCAD"` — a palette, About dialog, or Start tab can win (EnumWindows ~ top-Z first). The COM app object's `.HWND` (the real main frame) is unused.
- **Fix:** Use `int(_acad_app().HWND)`; keep EnumWindows only as a fallback.
### R21 — Gear flank/root junction is a straight radial segment, not a fillet; radial discontinuity when `root_r >= base_r`
- **Severity:** LOW
- **Location:** `engineering/gear.py:40-50` (flank), `163-190` (root-arc stitch)
- **Description:** `generate_involute_flank` prepends `(root_r, 0.0)` while the involute starts at `(base_r, 0.0)` — both on y=0, so root→base is a straight radial segment, not a trochoid. When `root_r >= base_r` (z≥42 @20°, z≥27 @25°, all within the teeth≥6 API limit) no root point is added but the root arc is drawn at `root_r`, giving a radial jump in the closed polyline (verified m=2,z=60: 1.118 mm). (The teeth≤2 degenerate case is unreachable — tools enforce teeth≥6.)
- **Fix:** Anchor the flank/root junction at `root_r` (radially extend the first involute point, or use a true trochoid below the base circle); skip the root arc when `root_r >= base_r`.
### N7 — `point_from_snap` reads ARC angles via bracket indexing → opaque `KeyError` instead of the function's standard `RuntimeError`
- **Severity:** LOW
- **Location:** `backends/base.py:629-630, 646-647`
- **Description:** ARC `"end"`/`"mid"` branches use `p["start_angle"]`/`p["end_angle"]` direct subscripting, while every other geometry access uses `p.get(...)` + descriptive `RuntimeError`. If a backend's extraction failed (broad try/except swallows at `ezdxf:170`/`com:245`), this raises an opaque `KeyError` rather than the consistent error.
- **Fix:** Use `p.get(...)` with the same None-check `RuntimeError` pattern as `_line_pts`/`_circle_geom`.
### N8 — PlanSpec stored in `_plan_spec` is never read; `drawing_critique` does not "replay" it as documented
- **Severity:** LOW
- **Location:** write `backends/base.py:574`; never read in `engineering/critique.py` `run_critique` (`234-251`); docstrings `base.py:563`, `server.py:2351`
- **Description:** `drawing_plan` stores `self._plan_spec = plan`; docstrings claim it's "held by the backend and replayed by drawing_critique." But `run_critique` and all `_check_*` never reference `_plan_spec`/`.intent`/`.scale`/`.view_count`/`.dim_style` (grep: written once, read nowhere). The committed plan is dead state and the "replay" contract is unmet.
- **Fix:** Implement plan-vs-drawing checks that read `backend._plan_spec` (e.g. view_count vs detected views, dim_style vs dims present), or correct the docstrings to stop claiming replay.
### N9 — `view_zoom_extents`/`view_zoom_window` return different payload shapes (ezdxf adds `message`, COM omits it)
- **Severity:** LOW
- **Location:** `backends/ezdxf_backend.py:1395-1399` vs `backends/com_backend.py:1372-1385`
- **Description:** ezdxf returns `{"ok": True, "message": …}`; COM returns `{"ok": True}` with no `message`. Same MCP tool, two shapes — a client keying on `result["message"]` works on ezdxf and `KeyError`s/None on COM.
- **Fix:** Return identical keys from both backends (always include `message` + an `applied` bool).
---
## SMELL
### R17 — COM `_safe_send_command` polling holds the lone STA thread; inner `deadline_s` decoupled from outer `com_call_timeout`
- **Severity:** SMELL (downgraded from MED — functionally correct; inner 8s deadline is tighter than the 60s outer net, 50 ms sleep is a yielding wait, and holding the STA thread is unavoidable under STA)
- **Location:** `backends/com_backend.py:1498-1542`; `config.py:38`
- **Fix (optional):** Tie `deadline_s = min(8, com_call_timeout*0.8)` so the two stay consistent if the env var changes.
### R29 — Broad `except Exception` guards around optional COM reads
- **Severity:** SMELL (downgraded — adjacent unguarded calls re-raise genuine disconnects into `_run`; most sites log rather than silently pass; reconnect is not bypassed)
- **Location:** `backends/com_backend.py:200, 245, 251, 470, 1447, 1512-1548` (~30 sites)
- **Fix:** Narrow to `except (pywintypes.com_error, AttributeError)` so genuine logic bugs surface.
### S5 — Two parallel, conflicting layer systems
- **Severity:** SMELL
- **Location:** `server.py:1647-1688` (`_LAYER_TEMPLATES`) vs `engineering/layers.py:14-69` (`ENGINEERING_LAYERS`/`LAYER_SET_REGISTRY`)
- **Description:** `_LAYER_TEMPLATES` (architectural/mechanical/…) uses hundredths-int lineweights, all-Continuous linetypes, no CONSTRUCTION layer; `engineering/layers.py` (mech/pid/iso13567) uses mm-floats, real HIDDEN/CENTER, ISO-128 lineweights, CONSTRUCTION. The overlapping mechanical/mech sets *conflict* (HIDDEN/CENTER colors swapped), and `_LAYER_TEMPLATES` violates the ISO-128 discipline `drawing_critique` enforces.
- **Fix:** Retire `_LAYER_TEMPLATES`/`template_apply_layers` in favor of `LAYER_SET_REGISTRY` (or map legacy keys onto it) for one source of truth.
### S3 — `BlockInfo.description` always empty
- **Severity:** SMELL
- **Location:** declared `backends/base.py:71`; never set in `ezdxf_backend.py:1196-1202` / `com_backend.py:1186-1192`
- **Fix:** Populate `description=blk.block.dxf.get("description", "")` (ezdxf) + COM equivalent, or drop the field.
### S1 — `engineering/section.py` helpers (`cutting_plane_line`, `apply_section_hatch`) are dead
- **Severity:** SMELL
- **Location:** `engineering/section.py:12, 65`; only re-exported (`__init__.py:35, 65-66`), never called (`gear_draw_section_aa` builds its hatch inline).
- **Fix:** Wire a `section_*` MCP tool around them, or delete the module + exports.
### S2 — `generate_tooth_profile` is dead (superseded by `generate_full_gear_outline`)
- **Severity:** SMELL
- **Location:** `engineering/gear.py:74`; exported `__init__.py:9, 59`; duplicates ~80% of the outline math (unused local `t_pitch` at `:94`).
- **Fix:** Delete and remove import/`__all__` entries, or wire into a draw path.
### S4 — Stale "Agent B still in flight" build-seam comment + now-vestigial import guards
- **Severity:** SMELL
- **Location:** `engineering/__init__.py:37-39` (comment), `40-51` (try/except guards)
- **Description:** Both `titleblock.py` and `validator.py` now exist and import cleanly, so the guard's rationale is stale. (Related: **R14** — when a guarded symbol *is* None the tools call it and raise an opaque `TypeError`; currently unreachable since modules exist.)
- **Fix:** Delete the build-seam comment; either drop the guards (import directly) or have the tools raise `ToolError("engineering module unavailable: …")` when a symbol is None instead of calling it.
### N10 — `CommandResult` dataclass + `to_dict` never instantiated
- **Severity:** SMELL
- **Location:** `backends/base.py:26-38`; exported `backends/__init__.py:2, 8`
- **Description:** Defined and re-exported, but no constructor call exists outside docs; every command return uses a plain dict. CLAUDE.md/README still list it as a shared dataclass.
- **Fix:** Delete it + its export + doc refs, or actually adopt it as the command return type.
### N11 — `_ACI_NAMES` color-name table defined but never referenced
- **Severity:** SMELL
- **Location:** `backends/ezdxf_backend.py:54-57`
- **Fix:** Delete, or wire into `_entity_info_dxf`/`_layer_info_dxf` as a `color_name` field.
### N12 — `AutoCADBackend.set_layer_active` alias has zero callers
- **Severity:** SMELL
- **Location:** `backends/base.py:910-912`
- **Fix:** Remove; callers use `layer_set_current` directly.
### N13 — `AutoCADBackend.ensure_linetypes` concrete helper is dead (superseded by `engineering.layers.ensure_standard_linetypes`)
- **Severity:** SMELL
- **Location:** `backends/base.py:914-934`
- **Description:** Concrete ABC method with no callers; the real bootstrap path (`server.py:288`, `layers.py:110`) calls the module-level `ensure_standard_linetypes`, which reimplements the same already_loaded/loaded/failed loop.
- **Fix:** Delete it, or refactor `ensure_standard_linetypes` to delegate to it (single owner).
### N14 — COM `self._batch_mode` instance attr written but never read (`_regen` consults only `_COM_STATE`)
- **Severity:** SMELL
- **Location:** `backends/com_backend.py:363, 889, 907, 916, 933` vs `_regen` `180-187`
- **Description:** `_regen` guards on `_COM_STATE.get("batch_mode")` only. The array methods set *both* `self._batch_mode` and `_COM_STATE['batch_mode']`; the instance attr has no readers and can desync.
- **Fix:** Remove the redundant `self._batch_mode` assignments (keep the dict flag), or make `_regen` read the instance attr and drop the dict — one source of truth.
### R27 — Validator step 5 uses substring matching on all TEXT-layer text
- **Severity:** SMELL (downgraded from LOW — gated by `if helix:`, only runs when caller declares `helix_angle`; flagging a SPUR title on a declared-helical part is intended; only the over-broad substring scan is a smell)
- **Location:** `engineering/validator.py:159-181` (`:168`, `:175`)
- **Fix:** Restrict the scan to title-block text and use word-boundary regex (`\bSPUR\b`/`\bHELICAL\b`) instead of `in`.
### R25-residual — `block_find_references` INSERT cap
- **Severity:** SMELL (memory-bug claim REFUTED — `entity_list` defaults to `limit=200`, hard-capped; see appendix)
- **Location:** `server.py:1368-1369`
- **Fix (optional):** Warn when INSERT count hits the 200 cap to avoid silently under-reporting references in large drawings.
---
## (a) Count Summary by Severity
| Severity | Count | IDs |
|---|---|---|
| CRITICAL | 0 | — |
| HIGH | 2 | N1, N2 |
| MED | 13 | R4, R5, R6, R9, R12, R13, R15, R18, R19, R22, N3, N4, N5, N6 *(14 listed — see note)* |
| LOW | 9 | R16, R20, R23, R24, R26, R28, R21, N7, N8, N9 *(10 listed — see note)* |
| SMELL | 13 | R17, R29, R25, R27, S1, S2, S3, S4, S5, N10, N11, N12, N13, N14 *(14 — see note)* |
> Note: counts reflect merges (R10+R11 → N2; R23 split across N6+R23; R13 absorbs two fresh-sweep offset findings; R10 residual folded into N2). Exact kept-finding totals: **HIGH 2, MED 14, LOW 10, SMELL 14 = 40 kept**; 3 dropped (refuted/already-fixed appendix below).
---
## (b) Confirmed P1 Shortlist — highest-leverage fixes to tackle next
1. **N1 — COM hatch associativity/delete bug** (`com_backend.py:659-673`). Flip `AddHatch(…, True)` → `False`. One-line fix, prevents corrupt/invalid hatches in live AutoCAD.
2. **N2 — unify `drawing_save_as` format + derive from extension in `drawing_finalize`** (`base.py:158`, `ezdxf_backend.py:346`, `com_backend.py:516`, `server.py:2299`). Fixes silent DXF-as-DWG mislabeling and cross-backend format divergence on the primary output path.
3. **R19 — fix the two ezdxf dimension calls + make xfail strict** (`ezdxf_backend.py:696, 711-715`; `tests/test_dimensions.py:16,23`). Two advertised tools ship broken; cheap, high-visibility.
4. **R4 — make `dim_overlap` actually run** (`critique.py:199-201` + extractors). Restores a premium-workflow safety check that is currently a guaranteed no-op.
5. **R18 — surface bootstrap failure in `drawing_new`** (`server.py:282-293`). Prevents silent production-precondition violations (geometry on missing layers).
6. **N4 — parameterize `dimension_auto` / construction layers by active layer set** (`base.py:822/836/845/852, 743, 754`). Fixes ISO-13567 dims/construction landing on wrong auto-created layers.
7. **R9 — fix full-circle polar array overlap + COM divergence** (`ezdxf_backend.py:947`). Eliminates duplicate-entity output and a backend behavior split.
8. **R6 — add a mocked COM unit-test suite** (`tests/test_com_backend.py`). Unlocks regression safety for 1651 lines / 85 methods that currently can't be caught on CI — force-multiplier for every other COM fix above.
9. **R5 — bound the COM timeout thread/apartment leak** (`com_backend.py:416-434`). Prevents long-session resource exhaustion in live AutoCAD.
10. **R13/N5 — `entity_offset` side params + property-shape parity** (`ezdxf_backend.py:869-909`, `com_backend.py:861-875`; props extractors). Removes two dead documented params and the cross-backend `properties` contract drift that breaks downstream clients.
---
## (c) Refuted / Already-Fixed Appendix (checked, not actioned)
- **R1 — premium tools missing on COM:** ALREADY FIXED in `cd6329a`. The 8 premium meta-tools are now concrete methods on `AutoCADBackend` (`backends/base.py`); only `_create_xline` is backend-specific.
- **R2 — `int(lineweight)` truncation:** ALREADY FIXED in `cd6329a` via `normalize_lineweight()` in `base.py` (reconciles mm-float vs hundredths-int).
- **R3 — `iso128` critique no-op:** ALREADY FIXED in `cd6329a`.
- **R8 — _(no verification payload supplied; not assessed)._** Listed only for register completeness.
- **R14 — engineering import-guard `TypeError`:** PARTIAL / **not reachable**. Both `titleblock.py` and `validator.py` exist and import cleanly, so the `None`-fallback branch is never taken in the shipped repo. The vestigial guards/comment are carried as **S4** (SMELL).
- **R25 — `block_find_references` unbounded INSERTs + KeyError:** REFUTED. `entity_list` defaults to `limit=200` (hard-capped at all three layers), the opposite of "unbounded"; `e.properties.get("block_name")` cannot `KeyError`. Optional cap-warning carried as a SMELL above.
- **R29 — COM broad `except Exception` masks disconnects:** REFUTED as stated (reconnect not bypassed; most sites log). Retained as SMELL (narrow the guard types).
- **R17 — `_safe_send_command` deadline decoupling as a defect:** the defect framing is REFUTED (inner deadline is the tighter, primary one); retained as SMELL.
- **S4 (`Task #7/#8` phrasing):** the specific cited phrase is REFUTED (the comment says "Agent B … still in flight," not "Task #7/#8"); the substantive build-seam-comment smell STANDS and is kept as S4.
+146
View File
@@ -0,0 +1,146 @@
<!-- Generated by the IDEAS/roadmap workflow (wf_02a3262e-ddd), 2026-06-18. 19 ideas scored, 9 stress-tested. Pairs with docs/analysis/01-DIAGNOSTICS.md. -->
# AutoCAD MCP Pro — Evidence-Based Roadmap
> Mission: become the best AutoCAD MCP for LLM-driven drawing. The durable moat is the **standards-validation gate** (no competing CAD MCP — build123d / FreeCAD / Blender / Rhino / Onshape — ships any design-rule validation). Everything below is sequenced to widen that moat or to remove the silent no-ops that make it lie.
Priority signal = `(impact + differentiation + evidence_strength) / effort`, computed on **post-challenge adjusted** scores, then adjusted by judgment (quick wins and dependency order). DEFER → Backlog; RESCOPE → adjusted scope used; KEEP → as scored.
---
## Scoring table (all ideas, adjusted, sorted by priority signal)
| ID | Title | Imp | Eff | Diff | Ev | Signal | Verdict | Area |
|----|-------|-----|-----|------|----|--------|---------|------|
| **I9** | Expand deterministic geometry surface | 3 | 2 | 3 | 5 | **5.5** | KEEP→rescoped* | determinism |
| **I3** | ProCAD pre-plan clarification pass | 3 | 2 | 4 | 4 | **5.5** | RESCOPE | closed-loop |
| **I16** | Correctness + hygiene bundle (DIAGNOSTICS) | 3 | 2 | 2 | 5 | **5.0** | KEEP | hygiene |
| **I6** | GD&T primitive (ISO 1101 / Y14.5) | 5 | 3 | 5 | 3 | **4.3** | KEEP (impact↑) | iso-production |
| **I14** | NL→primitive grounding (trim/fillet/chamfer) | 3 | 3 | 3 | 4 | **3.3** | RESCOPE | determinism |
| **I15** | Staged validator (code→geometric→rubric) | 5 | 3 | 5 | 5 | **5.0** | RESCOPE | metrics |
| **I12** | Authenticated remote HTTP | 2 | 1 | 2 | 2 | **6.0** | KEEP (quick win) | infra |
| **I4** | Scalar drawing-score / Invalidity-Ratio | 3 | 2 | 3 | 4 | **5.0** | KEEP | metrics |
| **I2** | Visual critique / render as ImageContent | 3 | 2 | 4 | 5 | **6.0** | RESCOPE (cheap half) | closed-loop |
| **I18** | Real drawing repair (recover + Auditor.fix) | 2 | 2 | 1 | 3 | **3.0** | KEEP | robustness |
| **I1** | Routed iterative Refiner loop | 4 | 4 | 5 | 5 | **3.5** | RESCOPE | closed-loop |
| **I5** | ISO 129 tolerances on dimensions | 4 | 4 | 3 | 5 | **3.0** | RESCOPE | iso-production |
| **I8** | ISO-129 DimStyle + MLEADER | 3 | 3 | 2 | 3 | **2.7** | as scored | iso-production |
| **I19** | COM test harness (mocked) + cov gate | 2 | 2 | 1 | 3 | **3.0** | KEEP | hygiene |
| **I7** | Real plotted sheets + real DWG | 3 | 3 | 3 | 3 | **3.0** | RESCOPE (cheap half) | iso-production |
| **I17** | Fast selection via native query | 2 | 3 | 1 | 2 | **1.7** | ezdxf half only | perf |
| **I13** | Standard-parts / symbol library | 3 | 4 | 3 | 2 | **2.0** | RESCOPE | breadth |
| **I11** | Parametric editability / conv. edit | 3 | 4 | 3 | 2 | **2.0** | as scored | breadth |
| **I10** | 3D solids tier (or fix false claim) | 2 | 4 | 2 | 2 | **1.5** | split | breadth |
\* I9/I3 share the top signal; I9 edges first because it is zero-risk and directly converts the GeoGramBench failure mode (<50% program-to-geometry) into deterministic tool calls.
---
## Sprint 1 — Quick wins & correctness
Foundation: the closed-loop moat only pays off if the validator/critique signals are not silent no-ops. Land the cheap, CI-verifiable correctness fixes and the highest-signal determinism wins first.
### I16 — Correctness + hygiene bundle (DIAGNOSTICS fixes) · imp 3 / eff 2 / diff 2
**Why now:** Every premium-workflow signal that the moat depends on is currently degraded by confirmed defects (fake DWG, broken aligned/angular dims, polar-array dup, silent bootstrap). Iteration evidence (Self-Improving CAD +13.4pp/round) only pays off if signals aren't dead. **Depends on:** land alongside I19 (COM-only N1/R5 need a harness).
**First step:** One PR for the CI-verifiable wins: fix R19 (`ezdxf_backend.py:696` `dist`→positional `distance`; `:711` `add_angular_dim_2l(base=…, line1=…, line2=…, location=(tx,ty))`), flip `tests/test_dimensions.py:16,23` to `xfail(strict=True)`, fix R9 divisor (`:947`), R12 `location=(tx,ty)` (`:715`), R13 offset side via cross-product sign; then `ruff check --fix .` (41 auto) + 6 manual. Verified: ezdxf `drawing_save_as` ignores `fmt` and returns `{"format": fmt}` while writing DXF bytes (`ezdxf_backend.py:346-353`) — make it derive fmt from extension and refuse to mislabel.
### I9 — Expand deterministic geometry surface · imp 3 / eff 2 / diff 3
**Why now:** Highest priority signal. `point_from_snap` is a **concrete** base method sourcing from the shared `EntityInfo.properties` contract (verified at `base.py:585`), so new snaps are pure math in one place, zero per-backend code, free on COM+ezdxf. Finishes the pre-committed `int` stub (`plan_spec.py:24`). **Depends on:** none.
**First step:** Implement the deferred `point_intersection` (line-line / line-circle / circle-circle, ref-point disambiguation) and `point_tangent` (external-point-to-circle — highest leverage). Demote/drop `point_offset` (trivial vector add). Either cover LWPOLYLINE or document the gap explicitly. Dual-backend tests in `tests/test_meta_tools.py`.
### I12 — Authenticated remote HTTP · imp 2 / eff 1 / diff 2 — **QUICK WIN / latent security bug**
**Why now:** Confirmed: no `auth=` arg and no `TokenVerifier` anywhere (grep returned nothing). `_validate_http_bind` promises bearer-token auth but it is **never enforced** — a remote operator who sets `ALLOW_REMOTE_HTTP`+`MCP_AUTH_TOKEN` gets an unauthenticated server that believes it is authenticated. FastMCP 3.0.1 ships `StaticTokenVerifier`. ~10 lines.
**First step:** Build `_auth = StaticTokenVerifier(tokens={settings.mcp_auth_token: {...}}) if settings.mcp_auth_token else None`, pass `auth=_auth` into `FastMCP(...)` at `server.py:153`; add `tests/test_http_auth.py` (401 on bad/missing token, pass on correct).
### I12-adjacent quick win — I2 cheap half (render as ImageContent) · imp 3 / eff ~1 / diff 4
**Why now:** `fastmcp Image` is already imported/returned by `view_screenshot` (`server.py:1846+`); `drawing_finalize` writes screenshot bytes to disk and discards them. Returning the existing PNG bytes as `ImageContent` lets the already-multimodal driving agent (Claude) see the render — captures the CADSmith "render is a first-class signal" win with **no model bundling, no torch, no [vision] extra**.
**First step:** Add `return_image`/`include_render` to `drawing_finalize` (or a thin `visual_inspect` tool) that wraps existing bytes in `Image`. Keep the deterministic critique gate pure — the visual signal is advisory, **not** a `CritiqueFocus` member. Defer the local-VLM judge and OCR dim round-trip.
---
## Phase A — Own the moat (closed-loop validation)
This is the differentiation core. Sequence: scalar metric → staged unified validator → clarification gate → refiner loop. The metric makes the loop measurable; the unified validator fixes a real disconnect; the refiner acts on it.
### I4 — Scalar drawing-score / Invalidity-Ratio · imp 3 / eff 2 / diff 3
**Why now:** No quantitative metric exists today; `drawing_critique` returns a list, `validator.run` returns severity counts only. MUSE/CadBench grade Invalidity-Ratio, not shape. A scalar is the objective function that makes the repair loop measurable and the smoke test regression-trackable. **Depends on:** sequence after I16 (so the score isn't averaged over dead checks like R4 dim_overlap).
**First step:** Add `engineering/scoring.py::score_findings(severity_counts)` → `score = 100 − (errors·Wₑ + warnings·Wᵥᵥ + info·Wᵢ)` clamped, plus `invalidity_ratio = errors/max(1, findings)`. Consume from both `validator.py` and `critique.py`; surface in `drawing_finalize` payload. Document weights as a convention, not ground truth.
### I15 — Staged validator (code→geometric→rubric) · imp 5 / eff 3 / diff 5 — RESCOPE
**Why now:** Strongest hidden find: `drawing_finalize` (`server.py:2311`) calls **only** `DrawingValidator.run()` and **never** `run_critique` — the 6 premium critique focuses are not part of the finalize gate. Unifying validator's 8 steps + critique's 6 focuses into one staged protocol fixes a real architectural disconnect, not just cosmetics. **Depends on:** the no-op critique fixes (R4/R12/R19 in I16) must land first or the staged scalar lies. Drop R27 (separate SMELL); defer the VLM rubric stage to I2's deferred half.
**First step:** Define `StagedReport` (stages: code/geometric) with `{rule_id, stage, severity, passed, handles, detail}` + the I4 scalar; refactor `DrawingValidator.run` to tag findings with stage and reuse `_FOCUS_DISPATCH` for the geometric stage; wire **both** into `drawing_finalize`. Assert the scalar in `scripts/smoke_premium.py`.
### I3 — ProCAD pre-plan clarification pass · imp 3 / eff 2 / diff 4 — RESCOPE
**Why now:** ProCAD's one clarification round cut invalidity 14.6%→0.9% — attacks "garbage in" before any geometry, which no downstream critique recovers. `drawing_plan` is a thin concrete method building a `PlanSpec` (`base.py:553`). **Depends on:** none, but deep material/tolerance NL-extraction should wait for the I5 tolerance model. **Rescope:** ship cheap robust checks only — PlanSpec has no structured tolerance/material fields, so deep NL-parsing balloons effort.
**First step:** `engineering/plan_spec.py::detect_ambiguities(plan)` mirroring `_FOCUS_DISPATCH`: rule 1 = `view_count` vs intent keywords (section/orthographic/3-view); rule 2 = scale-vs-sheet sanity; rule 3 = ordinate dim_style needs a datum note. Surface advisory `clarifications` in the server wrapper return.
### I1 — Routed iterative Refiner loop · imp 4 / eff 4 / diff 5 — RESCOPE
**Why now:** The validation-gate-as-repair-loop is the moat no competitor has, directly operationalizing CADSmith Refiner (38x) and FEA-agent (+13.4pp/round). All repair primitives exist on both backends with transaction rollback. **Depends on:** I15 (unified staged report) and I16 (R4/R19). **Rescope to honest v1:** auto-repair only the 3 clean deterministic focuses + iso128-snap; route `untrimmed_corner` and `dim_overlap` to the LLM as **hints**, not auto-fixes.
**First step:** `engineering/refine.py` with a `REPAIR_HINTS` registry mapping each `CritiqueFocus` to (a) a deterministic repair coroutine and (b) a fallback hint. Implement the 4 trivial focuses (`construction_left`→`construction_clear`, `duplicate_entities`→`entity_delete(handles[1])`, `layer_color`→`layer_modify(color=detail.expected)`, `iso128`→snap-to-nearest-ISO). Then `drawing_refine(max_rounds=4)` looping critique→repair→re-critique. **Critical blocker (R22):** the ezdxf `_undo_stack` is shared between user undo and transactions with no txn-id isolation (`system_status` even reports `len(_undo_stack)` as transaction_depth) — add a dedicated txn list/guard before the rollback cycle, or it corrupts undo state.
---
## Phase B — Production ISO parity
Make output actually manufacturable. ISO 129 tolerances are the single biggest functional gap; GD&T is the highest-differentiation breadth item.
### I6 — GD&T primitive (ISO 1101 / Y14.5) · imp 5 / eff 3 / diff 5 — KEEP (impact↑)
**Why now:** Maximal differentiation — 2D GD&T is absent from every surveyed text-to-CAD product and every competing MCP. Pure composition over existing primitives (the `titleblock_apply_iso_a3` pattern, `server.py:2248`) — no ABC/dual-backend additions. Impact raised to 5: bundle a thin tolerance-text option and one GD&T critique check (datum letter in an FCF must have a matching datum feature) to convert it from "draws a symbol" to "validates a standard." **Depends on:** none (composes cleanly).
**First step:** `engineering/gdt.py` with the 14 ISO 1101 characteristic glyphs + Ⓜ/Ⓛ/Ⓢ modifiers (unicode/MTEXT codes), then `draw_feature_control_frame(...)` composing the frame from `entity_create_line` + `entity_create_text` on DIM layer. Register one `gd_frame` `@mcp.tool`. **Design constraint:** avoid ezdxf's native Tolerance/FCF entity (renders blank through matplotlib) — compose from lines+text. Smoke-test glyph rendering via screenshot.
### I5 — ISO 129 tolerances on dimensions · imp 4 / eff 4 / diff 3 — RESCOPE
**Why now:** A drawing without ±/limit/fit callouts is not manufacturable — the dossier's "biggest functional gap." Empirically verified ezdxf 1.4.3 renders sym/limit/text overrides via override dict. **Depends on:** R19 fix (I16) is a soft prerequisite — aligned/angular dims are currently broken, so don't bolt tolerances onto them yet. **Rescope:** ezdxf-first, CI-testable subset; defer aligned/angular tolerances behind R19; COM tolerance rendering best-effort/untested until I19.
**First step:** Add `tol_upper/tol_lower/tol_mode/fit/text_override` to `dimension_linear/radius/diameter` in `base.py`; implement in ezdxf via override dict (sym/limit → DIMTOL/DIMLIM + DIMTP/DIMTM; fit → `engineering/iso286_fits.py` nominal-band×grade table — the main authored-data cost). Test asserts the DIMENSION carries DIMTP/DIMTM and reads e.g. `20 +0.021/0`.
### I8 — ISO-129 DimStyle + MLEADER · imp 3 / eff 3 / diff 2 — as scored
**Why now:** Every dim today inherits the default "Standard" style (no DIMTXT/DIMASZ/precision). Natural sibling to `drawing_apply_iso_layers`. Low differentiation (table-stakes) — sequence after I5/I6. **Depends on:** I5 (share the dimstyle plumbing).
**First step:** `engineering/layers.py::apply_iso_dimstyle(backend)` → `doc.dimstyles.new("ISO-25", …)`; thread `dimstyle="ISO-25"` through ezdxf dim methods; wire into `drawing_apply_iso_layers`. Defer COM `DimStyles.Add` and the MLEADER tool to follow-ups.
### I7 — Cheap half: fmt-from-extension + true ISO-128 lineweights · imp 3 / eff 3 / diff 3 — RESCOPE
**Why now:** Two isolated wins. (1) Fake-DWG is ezdxf-only (COM already writes real DWG via `SaveAs(path,12)`); (2) ISO-128 true lineweights via ezdxf `Configuration` (`lineweight_policy`) on the render path is the genuinely differentiating piece and feeds the render/critique signal. **Defer** the from-scratch paperspace/viewport/Plot pipeline (multi-day, CI-unverifiable, `odafc` needs the ODA binary — confirmed not on PATH) to the backlog. **Depends on:** none.
**First step:** Wire `Configuration(lineweight_policy=…)` into the PDF/screenshot Frontend; unify the save_as fmt default and fail loud when ezdxf can't write real DWG (use `odafc.export_dwg` if present, else raise — never mislabel).
---
## Phase C — Breadth & determinism extras
Catch-up capability and convenience. Lower priority signals; do not displace the moat work.
### I14 — NL→primitive grounding for trim/fillet/chamfer · imp 3 / eff 3 / diff 3 — RESCOPE
**Why now:** Repair-side companion to the `untrimmed_corner` detector; attacks FutureCAD primitive-grounding + GeoGramBench. **Rescope:** "which corner" is net-new work — `analysis_bounding_box` returns only a global bbox, so corner→handle mapping re-runs O(n²) endpoint clustering. Ship ezdxf path with bbox-quadrant corner naming that **errors with candidates when ambiguous** instead of guessing; defer COM until I19 lands COM tests. **Depends on:** I9 (intersection math), I19 (COM verification).
### I18 — Real drawing repair (recover + Auditor.fix) · imp 2 / eff 2 / diff 1 — KEEP
**Why now:** Confirmed honesty bug: `drawing_audit` returns only `auditor.errors`, ignores `auditor.fixes`, while the docstring claims it "fixes errors"; `drawing_open` uses bare `readfile` with no `recover.readfile` fallback. Near-free robustness win.
**First step:** Return `errors`+`fixes`+`fixed_count` and `_mark_dirty()` when fixes applied; wrap `readfile` with a `recover.readfile` fallback.
### I19 — COM test harness (mocked) + scoped coverage gate · imp 2 / eff 2 / diff 1 — KEEP
**Why now:** R6 confirmed — zero tests touch `ComBackend`; CI is ubuntu-only so 1651 lines never run. `normalize_lineweight` (the P0 anti-truncation fix, pure/OS-agnostic) has no regression guard. Unblocks COM verification for I14/I5/I16. **Depends on:** none; should land **before/with** I16's COM-only items (N1/R5).
**First step:** `tests/test_com_backend.py` with a `sys.modules` fake-COM shim; parametrized `normalize_lineweight` table tests + `_safe_send_command` CMDACTIVE/timeout/var-restore tests. Scope `--cov-fail-under` to `backends.base` + tested modules, not repo-wide.
### Backlog / Later (!)
- **I17** — Fast selection: do the **ezdxf-only** half (`query()`/`groupby()` one-liners, zero-risk, headless-testable); defer the COM `SelectAll` rewrite (CI-unverifiable, finicky compound filters). Not a moat-widener.
- **I13** — Standard-parts library: ship the cheap `library_insert` wrapper + a few parametric ISO bolt block defs; the real cost (ISO-correct fastener + P&ID symbol authoring) is large ongoing work. Defer dynamic-block driving (COM-only, breaks dual-backend parity).
- **I11** — Parametric editability: ship single-entity `entity_edit_geometry` (set radius/endpoints) first; true re-drive needs a constraint graph the architecture has no scaffold for. Catch-up, not moat.
- **I10** — 3D solids: **do the near-zero-effort honesty fix now** (correct the false `"all_entity_types"` capability at `com_backend.py:1444`). Defer the COM-only solids tier; ezdxf has no ACIS kernel so it breaks dual-backend parity, and all cited research targets 2D fidelity.
- **I7 expensive half** — paperspace/viewport/Plot pipeline + bundled-VLM judge.
---
## Recommended next 5 work items
1. **I16 (Correctness bundle) + I19 (COM harness) together.** Highest evidence strength; everything in Phase A is built on signals that are currently silent no-ops (R4 dim_overlap, R19 broken dims, fake DWG). The COM harness lands with it so the COM-only fixes (N1, R5) and `normalize_lineweight` are actually guarded. Foundation for the entire moat.
2. **I9 (Deterministic geometry surface).** Top priority signal, zero risk, one-place pure math. Directly converts the GeoGramBench <50% program-to-geometry failure into deterministic tool calls — the cheapest moat-adjacent win available.
3. **I12 (HTTP auth) + I2-cheap (render as ImageContent).** Two quick wins. I12 closes a real false-security gap (~10 lines, verified no auth wiring exists). I2-cheap captures the CADSmith "render is a first-class signal" win for ~1 day with no model bundling, by returning the already-produced PNG to the multimodal driver.
4. **I4 → I15 (Scalar metric → unified staged validator).** I4 gives the objective function; I15 fixes the confirmed disconnect where `drawing_finalize` never runs the premium critique focuses. Together they make the moat measurable, regression-testable, and actually enforced at finalize — the prerequisite for the refiner.
5. **I6 (GD&T primitive).** Highest-differentiation feature in the dossier, KEEP at effort 3, impact raised to 5 by bundling a tolerance-text option and one datum-consistency critique. Pure composition, no backend surface change — the single biggest "no competitor has this" capability.
Sequencing rationale: items 1–4 are explicitly ordered by `depends_on` (signals must be honest before the scalar averages them; the scalar must exist before the refiner can optimize it; R22 txn-isolation gates the refiner's rollback cycle). Item 5 is parallelizable — it composes cleanly and shares no dependency with the closed-loop track, so it can run alongside Phase A.
---
## Security note — unverified DIAGNOSTICS items (R7/R8)
The diagnostics flagged two security items that the security-review subagents **could not verify**: **R7** (input-sanitizer wiring — whether `system_run_command` / `system_run_lisp` user strings are actually routed through a sanitizer before reaching `SendCommand`) and **R8** (absence of adversarial sanitizer tests). These sit on the live-COM command path, which is the highest-blast-radius surface in the repo (arbitrary AutoLISP/command execution) and is exactly the path with **zero COM test coverage** (R6).
**Recommendation — add a defensive hardening work item (slot in Sprint 1, alongside I19):** (a) trace `system_run_command`/`system_run_lisp` end-to-end and confirm every user-supplied string passes through an explicit allow/deny sanitizer before `_safe_send_command`; if no sanitizer is wired, add one (reject embedded newlines, `\x03`/ESC injection, nested `(command …)` escapes, and `SCRIPT`/`SHELL`/`START` command words). (b) Add adversarial unit tests on the mocked-COM harness from I19 asserting that known injection payloads are rejected before reaching `SendCommand`. This is cheap insurance on the one surface where a prompt-injected agent could execute arbitrary OS-level actions via `SHELL`/`START`, and it composes directly with the I19 fake-COM fixture.
File diff suppressed because one or more lines are too long
+2 -21
View File
@@ -6,7 +6,6 @@ from .gear import (
draw_spur_gear_front_view,
generate_full_gear_outline,
generate_involute_flank,
generate_tooth_profile,
involute_xy,
)
from .keyway import (
@@ -32,23 +31,8 @@ from .plan_spec import (
SheetSize,
SnapType,
)
from .section import apply_section_hatch, cutting_plane_line
# Agent B contributes titleblock + validator. Imports are guarded so this
# package remains usable while B is still in flight; once those modules land
# the symbols flow through transparently.
try:
from .titleblock import TitleBlockMetadata, apply_iso_a3_titleblock
except ImportError:
TitleBlockMetadata = None # type: ignore[assignment]
apply_iso_a3_titleblock = None # type: ignore[assignment]
try:
from .validator import DrawingValidator, ValidationFinding, ValidationResult
except ImportError:
DrawingValidator = None # type: ignore[assignment]
ValidationFinding = None # type: ignore[assignment]
ValidationResult = None # type: ignore[assignment]
from .titleblock import TitleBlockMetadata, apply_iso_a3_titleblock
from .validator import DrawingValidator, ValidationFinding, ValidationResult
__all__ = [
"draw_helical_gear_front_view",
@@ -56,14 +40,11 @@ __all__ = [
"draw_gear_section_aa",
"generate_full_gear_outline",
"generate_involute_flank",
"generate_tooth_profile",
"involute_xy",
"draw_keyed_bore",
"draw_keyway_section",
"keyway_dimensions",
"DIN6885_TABLE",
"cutting_plane_line",
"apply_section_hatch",
"ENGINEERING_LAYERS",
"STANDARD_LINETYPES",
"ensure_engineering_layers",
+7 -7
View File
@@ -34,7 +34,7 @@ _DIM_TYPES = {
}
async def _check_iso128(backend: "AutoCADBackend") -> list[Issue]:
async def _check_iso128(backend: AutoCADBackend) -> list[Issue]:
"""Layer lineweights must be in ISO_128_LINEWEIGHTS_MM (or -3=ByLayer
sentinel meaning 'inherit')."""
issues: list[Issue] = []
@@ -69,7 +69,7 @@ async def _check_iso128(backend: "AutoCADBackend") -> list[Issue]:
return issues
async def _check_layer_color(backend: "AutoCADBackend") -> list[Issue]:
async def _check_layer_color(backend: AutoCADBackend) -> list[Issue]:
"""Engineering-standard layers must keep their canonical ACI color."""
issues: list[Issue] = []
try:
@@ -93,7 +93,7 @@ async def _check_layer_color(backend: "AutoCADBackend") -> list[Issue]:
return issues
async def _check_construction_left(backend: "AutoCADBackend") -> list[Issue]:
async def _check_construction_left(backend: AutoCADBackend) -> list[Issue]:
"""CONSTRUCTION layer must be empty before drawing_finalize."""
try:
ents = await backend.entity_list(layer_filter="CONSTRUCTION", limit=5000)
@@ -113,7 +113,7 @@ async def _check_construction_left(backend: "AutoCADBackend") -> list[Issue]:
)]
async def _check_untrimmed_corner(backend: "AutoCADBackend") -> list[Issue]:
async def _check_untrimmed_corner(backend: AutoCADBackend) -> list[Issue]:
"""Two LINE endpoints within `tol` but not exactly equal usually means a
corner where one line overshoots — TRIM/EXTEND/FILLET was forgotten."""
tol = 0.5 # mm — generous; tighten via PlanSpec.notes later
@@ -156,7 +156,7 @@ async def _check_untrimmed_corner(backend: "AutoCADBackend") -> list[Issue]:
return issues
async def _check_duplicate_entities(backend: "AutoCADBackend") -> list[Issue]:
async def _check_duplicate_entities(backend: AutoCADBackend) -> list[Issue]:
"""Two LINEs with identical endpoints (in either direction) are duplicates."""
try:
lines = await backend.entity_list(type_filter="LINE", limit=5000)
@@ -185,7 +185,7 @@ async def _check_duplicate_entities(backend: "AutoCADBackend") -> list[Issue]:
return issues
async def _check_dim_overlap(backend: "AutoCADBackend") -> list[Issue]:
async def _check_dim_overlap(backend: AutoCADBackend) -> list[Issue]:
"""Two dimensions whose insertion points are within `tol` likely overlap."""
tol = 5.0 # mm
try:
@@ -232,7 +232,7 @@ _FOCUS_DISPATCH = {
async def run_critique(
backend: "AutoCADBackend",
backend: AutoCADBackend,
focus: list[CritiqueFocus] | None = None,
) -> list[Issue]:
"""Dispatch each requested focus check and return aggregated issues."""
+4 -56
View File
@@ -71,58 +71,6 @@ def _arc_points(
return pts
def generate_tooth_profile(
module: float,
teeth: int,
pressure_angle: float = 20.0,
n_points_per_flank: int = 40,
) -> list[tuple[float, float]]:
"""Single tooth profile: right flank, tip arc, mirrored left flank."""
pa = math.radians(pressure_angle)
pitch_r = module * teeth / 2.0
base_r = pitch_r * math.cos(pa)
outer_r = pitch_r + module
root_r = pitch_r - 1.25 * module
# Tooth half-thickness angle on the pitch circle.
s = math.pi * module / 2.0
theta_pitch = s / pitch_r / 2.0
# Find involute parameter at pitch circle so we can shift the flank to be
# symmetric about the tooth centerline (y=0 axis after rotation).
if outer_r > base_r:
t_pitch = math.sqrt((pitch_r / base_r) ** 2 - 1.0) if pitch_r > base_r else 0.0
else:
t_pitch = 0.0
inv_pitch = math.tan(pa) - pa # involute function at pressure angle
# The flank generated by `generate_involute_flank` starts at theta=0 on the
# base circle. The involute point at the pitch radius makes angle inv_pitch
# (approx). We rotate the flank by -inv_pitch + theta_pitch so the pitch
# crossing sits at +theta_pitch above the tooth-center axis.
rot = -inv_pitch + theta_pitch
right_flank = generate_involute_flank(base_r, root_r, outer_r, n_points_per_flank)
right_flank = _rotate_points(right_flank, rot)
# Left flank by mirroring across the X axis (tooth centerline).
left_flank = [(x, -y) for x, y in right_flank]
# Tip arc connects the outermost right-flank point to the outermost left-flank point.
tip_right = right_flank[-1]
tip_left = left_flank[-1]
a_right = math.atan2(tip_right[1], tip_right[0])
a_left = math.atan2(tip_left[1], tip_left[0])
if a_left > a_right:
a_left -= 2 * math.pi
tip_arc = _arc_points(0.0, 0.0, outer_r, a_right, a_left, n=5)
profile: list[tuple[float, float]] = []
profile.extend(right_flank)
profile.extend(tip_arc[1:-1])
profile.extend(reversed(left_flank))
return profile
def generate_full_gear_outline(
module: float,
teeth: int,
@@ -200,7 +148,7 @@ def generate_full_gear_outline(
async def _draw_helix_symbol(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
cx: float,
cy: float,
@@ -237,7 +185,7 @@ async def _draw_helix_symbol(
async def draw_helical_gear_front_view(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
module: float,
teeth: int,
@@ -330,7 +278,7 @@ async def draw_helical_gear_front_view(
async def draw_spur_gear_front_view(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
module: float,
teeth: int,
@@ -356,7 +304,7 @@ async def draw_spur_gear_front_view(
async def draw_gear_section_aa(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
gear_metadata: dict,
x_offset: float,
+2 -2
View File
@@ -56,7 +56,7 @@ def keyway_dimensions(bore_diameter: float) -> dict:
async def draw_keyed_bore(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
center: tuple[float, float],
bore_diameter: float,
@@ -110,7 +110,7 @@ async def draw_keyed_bore(
async def draw_keyway_section(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
center: tuple[float, float],
bore_diameter: float,
+3 -3
View File
@@ -71,7 +71,7 @@ LAYER_SET_REGISTRY: dict[str, list[tuple[str, int, str, float, str]]] = {
STANDARD_LINETYPES: list[str] = ["CENTER", "HIDDEN", "PHANTOM", "DASHED", "DASHDOT"]
async def ensure_standard_linetypes(backend: "AutoCADBackend") -> dict[str, str]:
async def ensure_standard_linetypes(backend: AutoCADBackend) -> dict[str, str]:
"""Idempotently load STANDARD_LINETYPES via backend.linetype_load."""
try:
existing = {ln.lower() for ln in await backend.linetype_list()}
@@ -92,13 +92,13 @@ async def ensure_standard_linetypes(backend: "AutoCADBackend") -> dict[str, str]
return results
async def ensure_engineering_layers(backend: "AutoCADBackend") -> dict[str, str]:
async def ensure_engineering_layers(backend: AutoCADBackend) -> dict[str, str]:
"""Idempotently create every layer in ENGINEERING_LAYERS via backend.layer_create."""
return await apply_layer_set(backend, "mech")
async def apply_layer_set(
backend: "AutoCADBackend", standard: str = "mech",
backend: AutoCADBackend, standard: str = "mech",
) -> dict[str, str]:
"""Idempotently apply a named layer set ('mech', 'pid', 'iso13567')."""
layer_set = LAYER_SET_REGISTRY.get(standard)
-82
View File
@@ -1,82 +0,0 @@
"""Generic cutting-plane and section-hatch helpers."""
from __future__ import annotations
import math
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from backends.base import AutoCADBackend
async def cutting_plane_line(
backend: "AutoCADBackend",
*,
p1: tuple[float, float],
p2: tuple[float, float],
label: str = "A",
layer: str = "PHANTOM",
) -> dict:
"""Draw a section line, arrowheads at each end, and 'X-X' labels."""
x1, y1 = float(p1[0]), float(p1[1])
x2, y2 = float(p2[0]), float(p2[1])
line = await backend.entity_create_line(x1, y1, x2, y2, layer=layer)
dx, dy = x2 - x1, y2 - y1
length = math.hypot(dx, dy) or 1.0
ux, uy = dx / length, dy / length
nx, ny = -uy, ux
arrow_len = max(length * 0.05, 4.0)
arrow_w = arrow_len * 0.4
arrows: list[str] = []
for tip_x, tip_y, dir_x, dir_y in (
(x1, y1, -ux, -uy),
(x2, y2, ux, uy),
):
bx = tip_x + dir_x * arrow_len
by = tip_y + dir_y * arrow_len
wing1_x = bx + nx * arrow_w
wing1_y = by + ny * arrow_w
wing2_x = bx - nx * arrow_w
wing2_y = by - ny * arrow_w
a1 = await backend.entity_create_line(tip_x, tip_y, wing1_x, wing1_y, layer=layer)
a2 = await backend.entity_create_line(tip_x, tip_y, wing2_x, wing2_y, layer=layer)
arrows.extend([a1.handle, a2.handle])
text = f"{label}-{label}"
text_h = max(arrow_len, 3.5)
offset = arrow_len * 1.5
labels: list[str] = []
for tip_x, tip_y, dir_x, dir_y in (
(x1, y1, -ux, -uy),
(x2, y2, ux, uy),
):
tx = tip_x + dir_x * offset + nx * offset * 0.5
ty = tip_y + dir_y * offset + ny * offset * 0.5
t = await backend.entity_create_text(text, tx, ty, height=text_h, layer="TEXT")
labels.append(t.handle)
return {"line": line.handle, "arrows": arrows, "labels": labels}
async def apply_section_hatch(
backend: "AutoCADBackend",
*,
boundary_points: list[list[float]],
pattern: str = "ANSI31",
angle: float = 45.0,
scale: float = 1.0,
layer: str = "HATCH",
) -> str:
"""Wrapper around backend.entity_create_hatch returning the new handle."""
info = await backend.entity_create_hatch(
pattern=pattern,
boundary_points=boundary_points,
scale=scale,
angle=angle,
layer=layer,
)
return info.handle
+1 -1
View File
@@ -55,7 +55,7 @@ class TitleBlockMetadata:
async def apply_iso_a3_titleblock(
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
metadata: TitleBlockMetadata,
origin: tuple[float, float] = (0.0, 0.0),
+1 -1
View File
@@ -62,7 +62,7 @@ class DrawingValidator:
async def run(
self,
backend: "AutoCADBackend",
backend: AutoCADBackend,
*,
expected: dict | None = None,
) -> ValidationResult:
+223
View File
@@ -0,0 +1,223 @@
# AutoCAD MCP Pro — Oturum Notları
Tarih: 2026-06-18
Kullanıcı: Umutcan Edizsalan (umutcon12@gmail.com)
Hedef: Projeye tam hakim ol → bug/sorun tespit et → geliştirme yol haritası üret → onay sonrası uygula.
---
## YAPILDI ✅ — RECON Workflow (Task #1)
### Ne yapıldı
- 16 subagent çalıştırıldı: 8 kod-haritalama + 7 web araştırması + 1 sentez.
- Tüm kod tabanı `file:line` hassasiyetinde haritalandı.
- Web araştırması (arXiv, rakip MCP'ler, ezdxf/COM API, ISO standartları) tamamlandı.
- Sentez dosyası: `docs/analysis/00-RECON-dossier.md` (31KB, tam rapor)
- Ham veri: `docs/analysis/_recon_raw.json`
### Temel Bulgular
**Gerçek sayılar (dökümanda ne yazıyor vs gerçek):**
| Konu | Dökümanda | Gerçek |
|---|---|---|
| Tool sayısı | "87 tools" | **109 @mcp.tool** |
| Test sayısı | "104 tests" | **184 test fonksiyonu** |
| Section 1 | "(11 tools)" | **12 araç** (drawing_redo fazla) |
| Section 9 | "(5 tools)" | **4 araç** |
| LICENSE dosyası | "MIT — see LICENSE" | **YOK** (legal açık) |
| CI (.github/workflows/ci.yml) | "GitHub Actions CI" | **SİLİNMİŞ** |
| benchmarks/ klasörü | referans var | **YOK** |
| .claude/skills/ ve .cursor/ | referans var | **GIT'TEN SİLİNMİŞ** |
---
## YAPILDI ✅ — FAZ 1: P0 FIX (commit cd6329a, branch fix/p0-com-premium-lineweight)
- **R1 FİXLENDİ:** 8 premium meta-tool base sınıfına (AutoCADBackend) concrete + backend-agnostik olarak taşındı → COM artık miras alıyor, çökme bitti. Tek backend-özel parça yeni `_create_xline` primitifi. ezdxf'ten ~285 satır tekrar silindi.
- **R2 FİXLENDİ:** `normalize_lineweight()` eklendi (mm-float ↔ hundredths-int konvansiyon çakışması çözüldü); 6 çağrı sitesine bağlandı. ISO-128 lineweight'ler artık 0'a yuvarlanmıyor.
- **R3 FİXLENDİ:** iso128 kritiği artık ateşleniyor (R2 sayesinde); CONSTRUCTION katmanları muaf tutuldu (yanlış-pozitif engeli); yanıltıcı yorum düzeltildi.
- **LICENSE** eklendi (MIT). **ci.yml** geri yüklendi (lint geçici non-blocking; test job gerçek gate).
- **Doğrulama:** 182 passed / 2 xfailed + fonksiyonel test (GEOMETRY lw=50, kritik 0.40mm'yi yakalıyor).
---
## YAPILDI ✅ — FAZ 2: DIAGNOSTICS + IDEAS (workflow'lar)
- **DIAGNOSTICS** (`docs/analysis/01-DIAGNOSTICS.md`): 31 doğrulandı, 28 onaylandı, 3 reddedildi (R25/R29/R17 adversarial olarak çürütüldü), taramadan 17 yeni bulgu. → 0 CRITICAL (R1-R3 fixli), 2 HIGH (N1 COM hatch, N2 save-format), 14 MED, 10 LOW, 14 SMELL.
- **IDEAS/ROADMAP** (`docs/analysis/02-ROADMAP.md`): 19 fikir Impact×Effort×Diff×Evidence ile skorlandı, 9'u challenge edildi. Sprint 1 (correctness+quick wins) → Phase A (closed-loop moat) → Phase B (ISO production) → Phase C (breadth).
- **İki kritik yeni bulgu:** (1) HTTP auth hiç zorlanmıyor (latent güvenlik), (2) `drawing_finalize` `run_critique`'i HİÇ çağırmıyor — moat finalize'da devrede değil.
- **R7/R8 güvenlik:** otomatik doğrulanamadı (cyber-filter) → savunmacı hardening kalemi olarak Sprint 1'e eklendi.
**Önerilen sonraki 5:** ① I16 correctness bundle + I19 COM test harness · ② I9 deterministik geometri · ③ I12 HTTP auth + I2 render-as-image · ④ I4 scalar score + I15 birleşik validator · ⑤ I6 GD&T.
**Durum: Task #4 (konsolide rapor) HAZIR → kullanıcı onayı bekleniyor (FAZ 3 = uygulama).**
---
## KRİTİK BUGLAR (R1-R3 ARTIK FİXLİ — kalanlar FAZ 2 DIAGNOSTICS'te doğrulandı)
### R1 — KRİTİK: Premium araçlar COM'da çöküyor
- **Konum:** `backends/com_backend.py:1637-1660`
- **Sorun:** 8 premium meta-tool (`drawing_plan`, `drawing_critique`, `point_from_snap`, `construction_xline`, `construction_clear`, `drawing_apply_iso_layers`, `dimension_auto`, `entity_select_smart`) COM backend'inde `NotImplementedError` fırlatıyor.
- **Sonuç:** CLAUDE.md'nin "üretim için olmazsa olmaz" dediği akış, **Windows'taki varsayılan backend'de (COM) çöküyor**. Yani kullanıcı AutoCAD açıp kullandığında bu araçlar çalışmıyor.
### R2 — KRİTİK: Lineweight sıfırlanıyor
- **Konum:** `backends/ezdxf_backend.py:1034`
- **Sorun:** `int(lineweight)` → 0.50, 0.25, 0.18, 0.13 mm gibi tüm ISO-128 değerleri **0'a yuvarlanıyor**.
- **Sonuç:** Tüm ISO-128 çizgi kalınlığı disiplini sessizce sıfırlanıyor. `drawing_apply_iso_layers` ve `drawing_new` bootstrap'ı da etkileniyor.
### R3 — YÜKSEK: ISO-128 kritiği kalıcı no-op
- **Konum:** `engineering/critique.py:50`
- **Sorun:** `_check_iso128` içinde `lw <= 0: continue` → R2 yüzünden zaten hepsi 0 → kritik hiçbir zaman ateşlenmiyor.
---
## RİSK KAYITÇISI (Tüm Bulgular, Öncelik Sırasına Göre)
### KRİTİK (2 adet)
| # | Konu | Konum |
|---|---|---|
| R1 | 8 premium araç COM'da NotImplementedError | com_backend.py:1637-1660 |
| R2 | int(lineweight) → 0, tüm ISO-128 sıfırlandı | ezdxf_backend.py:1034 |
### YÜKSEK (8 adet)
| # | Konu | Konum |
|---|---|---|
| R3 | iso128 kritiği kalıcı no-op | critique.py:50 |
| R4 | dim_overlap kritiği da no-op (properties yok) | critique.py:194-197 |
| R5 | COM timeout → thread leak + CoUninitialize yok | com_backend.py:415-433 |
| R6 | COM backend'de SIFIR test | tests/ (ComBackend import yok) |
| R7 | Server→sanitizer wiring testsiz | server.py:2035,2052 |
| R8 | LISP aliasing bypass testi yok | security.py:138-179 |
### ORTA (21 adet)
| # | Konu | Konum |
|---|---|---|
| R9 | entity_array_polar 360° → son kopya 0°=orijinal çakışır | ezdxf_backend.py:946-947 |
| R10 | drawing_finalize fmt belirtmiyor → 3 farklı davranış | server.py:2299 |
| R11 | drawing_save_as(fmt='dwg') ezdxf'de DXF bayt yazar, .dwg ismiyle | ezdxf_backend.py:345-352 |
| R12 | dimension_angular tx/ty yok sayıyor, fixed distance=10 | ezdxf_backend.py:705-722 |
| R13 | entity_offset side_x/side_y her iki backend'de de yok sayılıyor | ezdxf:868-908 / com:860-874 |
| R14 | engineering import guard → None → opaque "NoneType not callable" | server.py:2266,2294 |
| R15 | system_about tool_groups → 15 tool eksik, drifted | server.py:2070-2106 |
| R16 | COM transaction_active sızıntısı hata durumunda | com_backend.py:1402 |
| R17 | _safe_send_command busy-poll STA thread'i bloke ediyor | com_backend.py:1521-1536 |
| R18 | drawing_new bootstrap hatalarını yutuyor, başarı döndürüyor | server.py:280-294 |
| R19 | dimension_aligned/angular kalıcı xfail, kırık araçlar gönderildi | tests/test_dimensions.py:16-27 |
| ... | (+ 10 daha orta öncelikli risk) | |
### DÜŞÜK (22 adet)
| # | Konu | Konum |
|---|---|---|
| R20 | _registered_tool_count FastMCP iç yapısına bağımlı | server.py:231-234 |
| R21 | Involute dişi geometrisi matematiksel hatalı (trochoid fillet yok) | engineering/gear.py:40-50 |
| R22 | undo/transaction stack paylaşıyor, doc_path sıfırlanmıyor | ezdxf_backend.py:470-484 |
| R23 | COM run_lisp her zaman 'nil' döndürüyor | com_backend.py:1487-1492 |
| R24 | _apply_attrs linetype'ı yüklemeden set ediyor | ezdxf_backend.py:491-501 |
| R25 | block_find_references max_list_limit bypass | server.py:1362-1370 |
| R26 | view zoom ezdxf'de no-op ama success döndürüyor | server.py:1814-1831 |
| R27 | validator title check false-positive ('SPUR' içeren notta) | engineering/validator.py:166-181 |
| R28 | COM HWND palette/splash dialog'u yakalayabilir | com_backend.py:283-300 |
| R29 | ~20 sitede except Exception: pass COM hatalarını yutuyor | com_backend.py:199,244,… |
### KOD KOKUSU / ANLAMSIZ KOD (53 adet)
- `engineering/section.py` — tamamen dead code, sıfır caller, sıfır test
- `gear.generate_tooth_profile` — dead code (export edilmiş ama hiç çağrılmıyor)
- `BlockInfo.description` field — hiç doldurulmuyor (her iki backend'de)
- `critique.py:53` "normalises to mm" yorumu — FALSE COMMENT
- `engineering/__init__.py:37-39` "Agent B … in flight" / "Task #7/#8" — çok-agent build seam üretime gönderilmiş
- `_LAYER_TEMPLATES` (server.py) ve prompt layer set'leri çakışıyor/kopyalanmış
- `template_list` / `validation_check` saf in-memory, yeterli test yok
- `system_about` static catalog 15 araç eksik
- ... (+ 40 daha smell)
---
## TEST KAPSAMASI (Eksikler)
1. **COM backend: %0 coverage** — en riskli yer, sıfır test
2. **Server→sanitizer entegrasyon** — araçlar sanitizer'ı gerçekten çağırıyor mu? Test yok
3. **LISP adversarial bypass** — aliasing, UNC path vs test yok
4. **Critique detection paths** — sadece temiz çizimde test edilmiş (hata bulmuyor mu diye değil)
5. `dimension_auto ordinate` — testsiz
6. `dimension_aligned/angular` — kalıcı xfail (kırık gönderilmiş)
7. Pek çok backend metodu hiç test edilmemiyor (system_get/set_variable, drawing_close, view_screenshot, ...)
---
## ARAŞTIRMA ÖZETİ (Ne bulduk)
### Akademik (arXiv 2025-2026)
- **GeoGramBench (ICLR 2026):** LLM'ler koordinat hesaplamada <%50 başarı → `point_from_snap` yaklaşımımızı doğruluyor, genişletmek lazım
- **CADSmith:** 5-agent döngüsü (Planner/Coder/Executor/Validator/Refiner) + VLM render → Chamfer 38× iyileştirme. **Render → VLM judge döngüsü en yüksek kaldıraç**
- **Self-Improving CAD + FEA:** İterasyon per token'dan çok daha değerli; drawing_critique tek seferlik değil döngü olmalı
- **ProCAD:** Çizmeden önce belirsizlik tespiti → hata oranı %14.6 → %0.9
### Rakip MCP'ler
| Rakip | 3D | Render-Critique | Validation Gate |
|---|---|---|---|
| **Bizim (AutoCAD MCP Pro)** | ❌ | ⚠️ finalize-only | ✅ **UNIQUE** |
| build123d-mcp | ✅ | ✅ | ❌ |
| FreeCAD MCP (150+ tool) | ✅ + FEM | ⚠️ | ❌ |
| Blender MCP (22k★) | ✅ | ✅ | ❌ |
| RhinoMCP | ✅ | ✅ | ❌ |
**Moatımız: Standards-validation gate — kimse yapmıyor.**
**Gerideyiz: 3D, in-loop render-critique, HTTP auth.**
---
## KALAN GÖREVLER ⏳
### Task #2 — DIAGNOSTICS Workflow
- Bug'ları `file:line` doğruluğuyla teyit et
- Ölü kodu + anlamsız kodlamaları listele
- Çekişmeli doğrulama (refute) sonrası "onaylanmış bulgu listesi" üret
- **Durum: Bekliyor (RECON tamamlandı, hazır)**
### Task #3 — IDEAS Workflow
- RECON araştırmasından türetilen fikir adayları oluştur
- Impact × Effort × Differentiation skoru
- Önceliklendirilmiş, kanıta dayalı yol haritası
- **Durum: DIAGNOSTICS bitmesini bekliyor**
### Task #4 — Konsolide Rapor + Onay
- RECON + DIAGNOSTICS + IDEAS birleştir
- Kullanıcıya sun
- **Onay sonrası** kodlamaya geç (kullanıcı bu tercihi seçti)
---
## DOSYALAR
| Dosya | İçerik |
|---|---|
| `docs/analysis/00-RECON-dossier.md` | Tam sentez raporu (31KB) |
| `docs/analysis/_recon_raw.json` | Ham yapılandırılmış veri |
| `not.md` | Bu dosya — oturum notları |
---
## ÖNERİLEN FIX ÖNCELİKLERİ (Onay Bekleniyor)
**P0 — Hemen Çalışmaz Olanlar (Onay Sonrası Fix)**
1. R1: `com_backend.py:1637-1660` → 8 premium aracı COM'a port et
2. R2: `ezdxf_backend.py:1034` → `int(lineweight * 100)` veya enum map ile düzelt
3. LICENSE dosyası ekle (legal açık)
4. CI (ci.yml) geri yükle veya yeniden kur
**P1 — Kritik Kalite (Onay Sonrası)**
5. `drawing_critique` → iteratif refiner döngüsü
6. Finalize screenshot'ı VLM judge'a besle
7. `drawing_plan`'a pre-flight ambiguity detector ekle
8. COM testleri: mock veya live fixture
**P2 — Üretim Çizim Kalitesi**
9. ISO-129 toleranslar + GD&T çerçevesi
10. PaperSpace/Layout/Plot desteği (COM)
11. odafc ile gerçek DWG export (ezdxf)
**P3 — Kapsam Genişletme**
12. 3D solid tier (COM extrude/revolve/boolean)
13. Parametrik yeniden sürüş (driving dims → re-drive tool)
14. HTTP API-key auth middleware
+8 -5
View File
@@ -287,10 +287,11 @@ async def drawing_new(
)
lt_status = await ensure_standard_linetypes(backend)
layer_status = await ensure_engineering_layers(backend)
result["bootstrap"] = {"linetypes": lt_status, "layers": layer_status}
result["bootstrap"] = {"ok": True, "linetypes": lt_status, "layers": layer_status}
except Exception as exc:
await ctx.warning(f"Engineering bootstrap skipped: {exc}")
result["bootstrap"] = {"error": str(exc)}
await ctx.warning(f"Engineering bootstrap failed: {exc}")
result["bootstrap"] = {"ok": False, "error": str(exc)}
result["status"] = "degraded"
return result
@@ -2263,7 +2264,7 @@ async def titleblock_apply_iso_a3(
ctx: Context = None,
) -> dict:
"""ISO 7200 / A3 (420x297 mm) title block. Title text is used verbatim."""
from engineering import apply_iso_a3_titleblock, TitleBlockMetadata
from engineering import TitleBlockMetadata, apply_iso_a3_titleblock
backend = _backend(ctx)
metadata = TitleBlockMetadata(
title=title, drawing_no=drawing_no, part_no=part_no, material=material,
@@ -2296,7 +2297,9 @@ async def drawing_finalize(
if save_path:
validated = validate_path(save_path, allow_write=True)
await backend.drawing_save_as(str(validated))
from pathlib import Path as _P
_fmt = _P(str(validated)).suffix.lstrip(".").lower() or "dxf"
await backend.drawing_save_as(str(validated), fmt=_fmt)
if screenshot_path:
validated_shot = validate_path(screenshot_path, allow_write=True)
-2
View File
@@ -13,14 +13,12 @@ async def test_dimension_linear(backend):
assert info.type == "DIMENSION"
@pytest.mark.xfail(reason="ezdxf API parameter mismatch in add_aligned_dim", strict=False)
async def test_dimension_aligned(backend):
info = await backend.dimension_aligned(0, 0, 100, 50, 50, 60)
assert info.handle
assert info.type == "DIMENSION"
@pytest.mark.xfail(reason="ezdxf API parameter mismatch in add_angular_dim_2l", strict=False)
async def test_dimension_angular(backend):
info = await backend.dimension_angular(0, 0, 100, 0, 0, 100, 50, 50)
assert info.handle
-1
View File
@@ -18,7 +18,6 @@ from engineering.layers import (
ensure_standard_linetypes,
)
# ---------------------------------------------------------------------------
# Pure math (sync)
# ---------------------------------------------------------------------------
+2 -3
View File
@@ -4,7 +4,6 @@ Backend coverage: ezdxf only. COM tests need a live AutoCAD instance and are
skipped unless explicitly enabled.
"""
import math
import pytest
pytestmark = pytest.mark.asyncio
@@ -151,7 +150,7 @@ class TestChamfer:
# Tangent points: (96,0) and (100,4) in some order.
pts = sorted([tuple(s[:2]), tuple(e[:2])])
expected = sorted([(96.0, 0.0), (100.0, 4.0)])
for got, exp in zip(pts, expected):
for got, exp in zip(pts, expected, strict=True):
assert _close(got[0], exp[0]) and _close(got[1], exp[1])
async def test_chamfer_asymmetric(self, backend):
@@ -162,7 +161,7 @@ class TestChamfer:
e = cl.properties["end"]
pts = sorted([tuple(s[:2]), tuple(e[:2])])
expected = sorted([(96.0, 0.0), (100.0, 6.0)])
for got, exp in zip(pts, expected):
for got, exp in zip(pts, expected, strict=True):
assert _close(got[0], exp[0]) and _close(got[1], exp[1])
async def test_chamfer_trims_source_lines(self, backend):
+2 -2
View File
@@ -187,9 +187,9 @@ class TestDimensionAuto:
assert len(dims) == 2
async def test_unknown_style_raises(self, backend):
l = await backend.entity_create_line(0, 0, 50, 0)
ln = await backend.entity_create_line(0, 0, 50, 0)
with pytest.raises(RuntimeError, match="unknown style"):
await backend.dimension_auto([l.handle], style="diagonal")
await backend.dimension_auto([ln.handle], style="diagonal")
async def test_empty_handles_returns_empty(self, backend):
result = await backend.dimension_auto([], style="chain")