`add_diameter_dim` / `add_radius_dim` measure to the point they are handed;
`leader_length` is a text placement. The ezdxf backend passed
`centre + (radius + leader)` as that point, so the leader became part of the
measurement: a true 40 mm bore was dimensioned 60 at the default
`leader_length=10`, and 108 at 34. Radius callouts were `leader_length` too
large the same way.
This is the worst class of defect for this project -- a wrong number, at
default settings, on the one part of a drawing that exists to carry numbers --
and it shipped in v1.4.0 and earlier. The size now comes from `radius` and
`location` only places the text.
The live COM backend was never affected: ActiveX's AddDimDiametric takes the
two chord points and the leader length as separate arguments. The two engines
had been disagreeing by 2 x leader_length on the same call.
Found while building the drawing for the new README, whose own bore callout was
wrong. Two checks added to correctness_suite.py; both report fail -> pass
against v1.4.0, not miss -> pass, which is what separates a repaired defect
from new capability in that lane. A/B is now 21/26 -> 26/26, five fixed, zero
regressed.
README rewritten from scratch
- Hero is a real ISO A3 sheet the server drew: m6 z24 involute spur gear, DIN
6885 keyed bore, section A-A, ISO 129 dimensions, ISO 286 H7 fit, ISO 7200
title block, 56 entities, drawing_critique clean. New
scripts/render_readme_showcase.py rebuilds it, and the tool-call sequence
that produces it is printed in the page.
- Star history chart, badge row, capability matrix, five evidence lanes, and a
"what this release is bad at" section carrying the 28% throughput regression.
- Every image and repo link is now an absolute URL: README.md is the PyPI long
description, where relative paths 404.
Corrections this work forced, all measured
- The df=0 example was wrong. FILLET is already in entity_fillet's own text;
the commands that genuinely scored zero are WBLOCK, PEDIT, OVERKILL,
LAYTRANS and MATCHPROP.
- 40,305 tokens is the tools/list wire payload; ~25,143 is the part forwarded
to the model. The old wording said "of schema", which is neither.
- `--tokenizer anthropic` needs a package and credentials this repo does not
lock, and refuses rather than falling back. Said so.
- Discovery ranks over the 149-tool advertised catalog, not the 154 registered.
- Five middlewares, not four; the fifth is what makes refusals machine-readable.
- Five of the 27 capability keys are environment-dependent, so the matrix
carries footnotes instead of bare ticks.
- "Every coordinate is WCS" has one deliberate exception: TEXT rotation.
- render_live_chart's axis still said "fixed 10-task matrix" while our bar came
from v3's fifteen. Each bar now carries its own matrix.
- .env.example said MAX_UNDO_STACK=50 (code: 5) and omitted four real
variables, two of them the remote-HTTP controls.
- server.json described an empty ALLOWED_PATHS as blocking access. It disables
the allowlist.
- server.py called the lean profile ~46 tools; it is 47.
- backends/capability.py cited a test file that does not exist.
1249 passed, 1 xfailed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Python evaluates an `except` clause on the way out of every failed call,
not only the ones it is meant to catch. `pywintypes` is bound only when
`sys.platform == "win32"` and the import succeeded, so on any host
without pywin32 -- all of Linux, and Windows installed without the
`[com]` extra -- an ordinary exception leaving `ComBackend._run` hit that
clause and became `NameError: name 'pywintypes' is not defined`, burying
the real error.
Every Linux CI leg failed on exactly this, in
test_ordinary_errors_are_unchanged. The timeout tests survived only
because they matched an earlier `except`; nothing else in the suite sends
a non-timeout exception through `_run`.
Fixed with a module-level `_COM_ERROR` tuple: `(pywintypes.com_error,)`
where the import worked, `()` where it did not. An empty tuple catches
nothing, which is correct when no com_error type exists to be raised -- a
placeholder Exception subclass would be worse than the bug, silently
swallowing unrelated errors.
Reproduced on Windows before fixing by deleting the module attribute,
which resolves the name the same way the absent import does; that is now
a test, alongside one pinning `_COM_ERROR` as a tuple. Verified through
the locked environment CI uses: 1244 passed, 1 xfailed.
README test-count snapshot 1243 -> 1245.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The v1.5.0 lock was generated before the version bump, so its own
[[package]] entry said autocad-mcp-pro 1.4.0 while pyproject said 1.5.0.
No dependency differed -- the transitive graph is byte-identical -- but
`uv sync --locked` will not re-resolve, so lint, test-linux 3.11,
test-linux 3.12 and test-windows all failed at install before running a
single test.
`uv lock` fixes it in one line. The gate that should have caught it did
not exist: test_release_consistency chains pyproject -> version.py ->
CHANGELOG -> README -> server.json, and the lockfile was the one release
artifact carrying a version string nothing checked. Added
test_uv_lock_records_the_pyproject_version, verified RED against the
broken lock and green against the fixed one.
Reproduced the CI lanes locally into a throwaway environment before
pushing this time: `uv sync --locked --only-group dev`, then
`--locked --extra pdf`, then ruff and pytest under `uv run --no-sync`.
1242 passed, 1 xfailed.
README test-count snapshot 1242 -> 1243 for the new gate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tool count 131 -> 154; 474 -> 1242 collected tests.
The release exists for two measured reasons. A 149-tool catalog costs a client
~40,300 tokens before it has asked for anything, and a drafter searching the way
drafters think -- FILLET, BPOLY, QSELECT, WBLOCK -- scored df=0 against the stock
index, because those words were simply not in the data.
Discovery and batching
- DISCOVERY_MODE=search replaces the catalog with search_tools + call_tool,
ranked over an AutoCAD command and synonym corpus (discovery/aliases.py) on
top of fastmcp's BM25 index. Measured: 40,305 -> 356 tokens advertised.
- cad_batch runs a step list in one round trip; fields= projects result-heavy
responses. The `core` profile is gone -- discovery replaced it.
Honesty debt (Tier 0)
- OCS/ECS: `extrusion` appeared nowhere in this codebase, so mirrored and
rotated geometry reported coordinates that were quietly wrong. backends/ocs.py
translates at the backend boundary; every tool coordinate is WCS on both
engines, and a tilted plane refuses rather than answering with a number that
looks fine.
- drawing_save_as(fmt="dwg") wrote DXF bytes to a .dwg path. drawing_audit never
repaired. analysis_measure_area shoelaced points the caller typed in.
- ezdxf calls gained a per-call timeout, so one hung call can no longer wedge a
server whose document lock is a single asyncio.Lock.
Architecture
- The 1854-line backend ABC is 19 contract modules; @capability(key, reason=)
makes optional methods declarative and a gate fails on undeclared holes.
- 8 layout/viewport tools, CHSPACE, CAD_PROGID escape hatch.
- Wave A: selection filters, boundary tracing, hatch depth, annotation objects,
measurement -- 13 tools of a planned 39. The REGION and 2D-boolean family was
cut on evidence: add_region() yields zero ACIS bytes and the greiner_hormann
substitute loses 28.2% of the area on a square with one semicircular edge.
Verified against a live AutoCAD 2026, which found four COM defects no amount of
reading would have caught -- AcadPViewport has no ViewCenter, and the line that
set it had been swallowed by a bare except since v1.4.
Benchmark v3 (15 tasks) and what it caught
- v2 scored this server 10/10, which carried no information: every task in it
exercised something the server was built around. Five tasks were added because
they can fail, and three did while being written -- a hatch could not be
measured though the refusal listed HATCH as measurable, the boundary tools and
the measure tool disagreed about the same shape (139.2177 vs 139.2699), and a
circle stored as two bulged vertices measured 0.0.
- Competitor reports are NOT re-scored. They are pinned v1.4 runs against v2 and
were never asked these five questions; the chart shows those rows `not run`.
- Correctness A/B: 21/24 -> 24/24, zero regressions.
Performance, stated plainly
The published v1.4 perf report was recorded on CPython 3.14 and the new one on
3.11.15, so the apparent 2-6x speedup is the interpreter, not this release.
Re-measured on one interpreter, v1.5.0 is 28% slower creating entities and 22%
slower on the 10k roundtrip. Fully attributed: EZDXF_CALL_TIMEOUT=0 restores
v1.4.0's exact numbers. That is the deadlock fix being paid for, the knob is
documented, and winning it back is on the 1.6 roadmap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
source_review.json extended with three projects reviewed 2026-07-23 (LokmenoWer/best-cad-mcp B/74, daobataotie/CAD-MCP B/35, zh19980811/Easy-MCP-AutoCad C/30) and our entry updated to the released v1.4.0 (95: CI matrix, PyPI packaging, A/B gate, paper space, ISO 286, opt-in solids). Chart regenerated via render_chart; README benchmark section now opens with the leaderboard + 9-row table, source-review lane merged into it.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ran benchmarks/compare_versions.py against the v1.3.0 tag: 21/21 on both versions, 0 fixed / 0 regressed - the v1.4.0 feature+infra work introduced no correctness regressions. Main README gains lane 4 (version A/B, with the historical v1.0.0 -> v1.1.0 contrast); benchmarks/README records the release-gate result and drops the stale 'performance coming' stub.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds production-grade engineering features and tools: 2D GD&T (feature control frames + datum features) via engineering/gdt.py, ISO‑129 dimension tolerance support (engineering/tolerances.py) and an ezdxf/COM plumbing for toleranced dims, a scalar drawing score + invalidity ratio (engineering/scoring.py) wired into drawing_finalize, in-place entity editors (entity_edit_text/entity_edit_geometry), and a friendly drawing_settings facade. Includes server tool wiring, COM/ezdxf backend implementations, datum-consistency critique, many unit tests, README/CLAUDE/CHANGELOG updates, and a version bump to 1.2.0. Also corrects COM backend capability label (entities_2d).
New COM-only Entity Query tool that reads AutoCAD's implied ("pickfirst")
selection set and returns the selected entity handles + EntityInfo, so the AI
can scope work (e.g. dimension_auto) to exactly what the user picked in the
viewport instead of the whole drawing. Resolves#1 — the layer-juggling
workaround is no longer needed.
- backends/base.py: abstract selection_get() returning a status dict
- backends/com_backend.py: reads ActiveDocument.PickfirstSelectionSet, surfaces
the PICKFIRST sysvar, handles the empty-selection case
- backends/ezdxf_backend.py: headless stub (ok=False, empty handles, same shape)
- server.py: @mcp.tool registration (readOnlyHint), SECTION 5 3 -> 4 tools
- README + CHANGELOG + pyproject: 110 -> 111 tools, v1.1.0 -> v1.1.1
Verified live on AutoCAD 2027 (COM): selection_get correctly returned the
user's grip selection across two distinct picks; 318 tests pass, ruff-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- benchmarks/correctness_suite.py: 21 deterministic headless (ezdxf) checks
across core ops, dimensions, arrays, geometry, query/parity, render, the
quality gate, and engineering.
- benchmarks/compare_versions.py: one-command A/B runner — checks out an older
ref into a throwaway worktree and runs the same suite against both, each check
in its own subprocess so a crash is a miss, not a run-killer.
- benchmarks/README.md: methodology, results table, honesty caveats.
- README: Benchmarks section now reports the measured result —
v1.0.0 8/21 (38.1%) -> v1.1.0 21/21 (100%); 13 fixed, 0 regressions.
- Delete .github/workflows/ci.yml and not.md.
- Drop the now-inaccurate "GitHub Actions CI" claims from README/CHANGELOG so
the v1.1.0 docs match reality (the ezdxf-in-your-CI capability notes remain).