Files
autocad-mcp-pro/CLAUDE.md
T
Umut CANandClaude Opus 5 39e6e2f9e4 docs(settings): track E shipped — CLAUDE.md sections 18-20, README, changelog with the live COM smoke record
CLAUDE.md gains items 18-20 (styles, page setup & templates, environment),
the grown drawing_settings facade on item 11, lean 55 / 52 and
TOOL_PACKS=all|core,pid,settings, and three conventions: layer states are
ours (ACADMCP_LAYERSTATES XRECORD, mode "xrecord"), page-setup evidence is
the PDF's own /MediaBox, nothing interprets coordinates in UCS.

README token table re-measured on this tree with benchmarks/token_suite.py
(ratio/v1 estimates): full 199 tools / 59,315 tokens, core,settings 190 /
55,593, core 168 / 48,825, lean 55 / 16,360, search 2 / 356; corpus
re-derived from discovery.aliases: 186 AutoCAD command names, 933 synonym
phrases, 204 tools. Styles & standards row, page setup & templates row,
final environment wording, and "Known limitations of the settings track".

CHANGELOG Unreleased: the track E intro, Added (styles, page setup &
templates, environment, drawing_settings growth, benchmark evidence),
Fixed (the Track A hardening list), Changed (three groups, packs, measured
idle cost, release snapshot 204 · 8 · 5 · 3284). Spec status: implemented.

Where the plan's text and the tree disagreed, the tree wins:
mleaderstyle_create is implemented on both engines through the
ACAD_MLEADERSTYLE dictionary and no `mleaderstyle` capability key exists
(spec §4, capability maps), so the docs say so instead of calling it
ezdxf-only; the Task 7 fix (tolerant ACADMCP_PID payloads, crossings
tested against the arc) joins the hardening list the plan left it out of.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 13:38:14 +03:00

21 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

AutoCAD MCP Pro is a FastMCP 3.0 server that exposes ~166 tools, 8 resources, and 5 prompt templates for AutoCAD automation. (The exact tool count is reported dynamically by system_status / system_about — never hardcode it.) It runs with a dual-engine architecture: a live COM backend (Windows/AutoCAD required) and a headless ezdxf backend (works anywhere).

Running the Server

# STDIO mode (default, for MCP clients like Claude Desktop)
python server.py

# HTTP mode
fastmcp run server.py:mcp --transport http --port 8000

# Force a specific backend
AUTOCAD_MCP_BACKEND=ezdxf python server.py   # headless file ops
AUTOCAD_MCP_BACKEND=com python server.py     # live AutoCAD (Windows only)
AUTOCAD_MCP_BACKEND=auto python server.py    # auto-detect (default)

Installation

# Core (ezdxf backend only)
pip install -e .

# With COM backend (Windows + AutoCAD)
pip install -e ".[com]"

# With PDF/screenshot support via matplotlib
pip install -e ".[pdf]"

# Everything
pip install -e ".[full]"

Running Tests

pytest
pytest tests/test_specific.py::test_name   # single test
pytest -x                                   # stop on first failure

Uses pytest-asyncio for async test support.

Architecture

Dual-Backend Pattern

The server uses a strategy pattern with an abstract base:

  • backends/contracts/ — the interface, one module per domain (drawing.py, layouts.py, entity_creation.py, …). Split in v1.5.0 out of a single 1854-line ABC; some slices (premium, gdt, settings) also carry shared concrete implementations, so they are mixins rather than pure interfaces.
  • backends/capability.py — the refusal vocabulary (UnsupportedCapabilityError) plus @capability(key, reason=...), which marks a contract method optional and gives it a default that raises that refusal. Use it only for genuine engine boundaries, never for an unfinished method; @abstractmethod stays the default. Every key used must be declared in both capability maps — tests/test_capability_contract.py fails otherwise, which is what stops the decorator becoming a way to ship undeclared holes.
  • backends/base.py — shared dataclasses (EntityInfo, LayerInfo, BlockInfo, DrawingInfo, CapabilityMap) and the AutoCADBackend composition of every contract. Import surface is unchanged: from backends.base import AutoCADBackend, UnsupportedCapabilityError still works.
  • backends/ezdxf_backend.py — EzdxfBackend: file-based DXF operations using the ezdxf library. All sync ezdxf calls are wrapped with asyncio.to_thread via _async(). Transactions are implemented as full DXF snapshots on an _undo_stack.
  • backends/com_backend.py — ComBackend: live AutoCAD control via pywin32 COM. All COM calls are routed through a ThreadPoolExecutor with a single thread to satisfy AutoCAD's STA (Single-Threaded Apartment) COM requirement.

Backend selection at startup (in server.py::_make_backend):

  1. Check AUTOCAD_MCP_BACKEND env var
  2. On Windows with auto/com: try COM first, fall back to ezdxf
  3. On non-Windows: always use ezdxf

FastMCP Server Structure (server.py)

The mcp FastMCP instance is configured with:

  • Lifespan (autocad_lifespan): initializes the backend singleton; stores it in ctx.lifespan_context["backend"]
  • Middleware stack: ErrorHandlingMiddleware → AuditMiddleware (custom timing/audit log) → TimingMiddleware → LoggingMiddleware
  • _backend(ctx) helper: retrieves the backend from lifespan context, raises ToolError if not ready

Tools are organized into sections (counts are indicative — system_about is authoritative):

  1. Drawing Management (12 tools): drawing_* (includes drawing_redo)
  2. Entity Creation (14 tools): entity_create_* (includes entity_create_table, leader_create_mleader)
  3. Dimensions (5 tools): dimension_*
  4. Entity Modification (16 tools): entity_move/copy/rotate/scale/mirror/offset/delete/array_*, corner ops (entity_trim/extend/fillet/chamfer), plus in-place entity_edit_text (TEXT/MTEXT content/height/rotation) and entity_edit_geometry (CIRCLE/LINE/ARC — center/radius/endpoints/angles), both handle-preserving
  5. Entity Query (9 tools): entity_get, entity_list, entity_delete_many, selection_get, selection_window/polygon/filter, and entity_get_xdata / entity_set_xdata (typed by group code)
  6. Layer Management (14 tools incl. 2 linetype_*): layer_*, linetype_list, linetype_load
  7. Block Operations (8 tools): block_* — includes block_define (a block from typed primitive specs + ATTDEFs, no source entities needed)
  8. Analysis & Query (8 tools): analysis_* — plus Batch (2), Templates (2), Validation (1)
  9. View & Screenshot (4 tools — includes view_zoom_and_screenshot): view_*
  10. Transactions (3 tools): transaction_begin/commit/rollback
  11. System (8 tools): system_status/get_variable/set_variable/run_command/run_lisp/about/capabilities, plus drawing_settings (friendly units/scale/precision/osnap facade over system variables; the facade now also covers limits, grid/snap/ortho/polar, PSLTSCALE, annotation scale ("1:50"), linear/angular unit formats and the current dimstyle/textstyle, with unknown keys refused and every key reported on read)
  12. Engineering / Deterministic CAD (8 tools): gear_draw_*, keyway_draw_*, titleblock_apply_iso_a3, drawing_finalize
  13. Premium meta-tools (12): drawing_preflight, drawing_plan, drawing_critique, drawing_refine, drawing_deliver, point_from_snap/intersection/tangent, construction_*, drawing_apply_iso_layers, dimension_auto, entity_select_smart
  14. GD&T (ISO 1101 / ASME Y14.5): gd_frame (feature control frames), datum_feature — enforced by the gdt critique focus
  15. Layouts & Paper Space (12 tools): tab lifecycle layout_list/create/set_current/delete/rename/copy, viewports viewport_create/list/set_scale/lock/delete, and entity_change_space (CHSPACE — tagged layout and modify, and layout wins the group priority, so it files here). drawing_export_pdf takes an optional layout param and does project model content through viewports on both engines (viewport_render — v1.4 declared this COM-only and was wrong; the headless renderer only omits the viewport borders). entity_change_space is the mirror case — ezdxf-only (chspace), because ActiveX exposes no change-space member and the COM route is unverified.
  16. 3D Solids (5 tools, opt-in via ENABLE_3D=true, COM only): solid_box/cylinder/extrude/revolve/boolean — hidden from discovery and rejected while disabled; ezdxf reports solid_3d as unsupported (no headless ACIS).
  17. P&ID (9 tools, pack pid): pid_symbol_list/insert (ISO 10628-2 + ISA-5.1 catalogue authored in code, real blocks with TAG attributes, named ports, ACADMCP_PID XDATA), pid_line_draw (port-to-port orthogonal routing, ISA-5.1 line classes → layer/linetype/marker blocks, line numbers), pid_tag_parse (ISA-5.1 grammar), the reader pid_graph and its deliverables pid_instrument_index/line_list/equipment_list, and pid_from_spec (whole sheet, one transaction). Six pid_* critique focuses (pid_dangling_line, pid_duplicate_tag, pid_incompatible_connection, pid_untagged_instrument, pid_illegal_tag, pid_unconnected_equipment) run under focus=None and inside drawing_finalize; they return nothing on a drawing without P&ID nodes. Two resources (autocad://pid/symbols, autocad://standards/isa51) carry the catalogue and the ISA-5.1 letter tables. Both engines implement every P&ID contract; the COM paths were executed live once by scripts/smoke_pid_com.py (AutoCAD 2026).
  18. Styles (10 tools, pack core): dimstyle_list/create/modify/set_current (authored ISO-25 / ANSI presets from engineering/standards/dimstyles.py; dimstyle_modify accepts only DIM_VARIABLE_WHITELIST names (33) and refuses out-of-range values by name before writing; changed lists only variables that moved, and rerender_required: true says a headless DIMENSION keeps its rendered block until redrawn), textstyle_list/create/set_current (font_resolved: false is reported, never refused — DXF stores only the name), mleaderstyle_list/create (both engines: ezdxf on doc.mleader_styles, live through the ACAD_MLEADERSTYLE dictionary — ActiveX has no MLeaderStyle collection, which is not an engine boundary, so no mleaderstyle capability key exists), and drawing_apply_standard("iso"|"ansi") — dimstyle + text style + units + the mech layer set in one call, each item reporting created or already present.
  19. Page setup & templates (6 tools, pack core): page_setup_list/apply (ISO 216 / ANSI Y14.1 sizes from PAPER_SIZES; an unknown ctb is written and reported plot_style_known: false), plot_style_list (catalog + the files actually installed on a live seat), batch_plot (every paper layout through drawing_export_pdf(layout=); mediabox_mm is read back from the PDF's own /MediaBox, so the sheet size is verified, not assumed), drawing_template_list (the five bundled templates: iso_a3_mech, iso_a1_arch, iso_a3_pid, ansi_b_mech, ansi_d_arch — DXF headless, .dwt twin on COM, scripts/build_templates.py --check proves the DXFs reproducible), drawing_template_save (a .dwt request headlessly is refused with capability dwt_write naming the DXF route). drawing_new(template=) resolves a catalog name to the bundled file and reports template: {name, source}.
  20. Environment (22 tools, pack settings): documents document_list/activate/close (both engines — the headless backend keeps a document registry; document_close refuses to discard unsaved work unless discard=True), layer states layer_state_save/restore/list/delete (portable: JSON chunks in an ACADMCP_LAYERSTATES XRECORD that travels with the file and does not appear in AutoCAD's Layer States Manager — system_capabilities.layer_states says so), named views view_named_save/restore/list (headless restore writes $VIEWCTR/$VIEWSIZE and reports applied: "header_only"), UCS ucs_list/set/restore (non-orthogonal axes refused with the measured angle; tool coordinates stay WCS — a UCS is stored for the operator, no tool interprets input in it), system_variable_describe (the 90-entry SYSVAR_CATALOG; an unknown name answers known: false plus nearest names, never an error; system_set_variable now refuses an out-of-range value for a catalogued name before writing), drawing_properties_get/set (custom properties on both engines through $CUSTOMPROPERTYTAG; the five summary fields are COM-only, capability dwgprops), and the live-only group system_launch (live_application), system_preferences_get/set (preferences; a whitelist of Preferences.* keys with ranges, read-only keys refused by name, writes report old/new), user_pick_point/select, system_prompt_message (interactive_prompt; a cancelled pick returns {cancelled: true}, a COM_CALL_TIMEOUT overrun reports timed_out). The COM paths were executed live once by scripts/smoke_settings_com.py (AutoCAD 2026), which also built the .dwt twins.

Tool profiles: TOOL_PROFILE=lean|full (default full) controls the advertised surface — lean = 55 curated drafting tools (52 with TOOL_PACKS=core; the five settings essentials — drawing_apply_standard, dimstyle_set_current, textstyle_set_current, page_setup_apply, batch_plot — are core-pack tools and stay; including cad_batch, since a client with a tight tool cap is exactly the client paying most per turn). Applied in the lifespan; reported by system_about. The core profile was removed in v1.5.0 (the discovery layer replaced it); TOOL_PROFILE=core falls back to full with a warning.

Tool packs: TOOL_PACKS=all|core,pid,settings (default all) advertises only the vertical packs a client uses; core is always on and is everything no other pack claims; settings is the 22 SECTION 20 environment tools, pid the nine P&ID tools; SECTION 18/19 are core; unknown names are ignored with a warning; a lean profile intersects with the enabled packs. system_about reports tool_packs.

Tool discovery: DISCOVERY_MODE=off|search (default off). search replaces the advertised catalog with search_tools + call_tool, ranking hits over an AutoCAD command / synonym corpus (discovery/aliases.py) layered on fastmcp's BM25 index.

ISO 129 tolerances: dimension_linear / dimension_radius / dimension_diameter take tol_upper / tol_lower / tol_mode (symmetric ± / deviation +a/-b / limit / basic) and text_override (e.g. ⌀20 H7). Prefer these over hand-drawn tolerance text.

ISO 286 fits: the same dimension tools take fit="H7" (mutually exclusive with tol_*) — deviations are resolved from authored ISO 286 tables (engineering/fits.py; shafts d/e/f/g/h/js/k/m/n/p, holes D/E/F/G/H/JS, sizes 1–500 mm) against the measured nominal and the fit code is appended to the dimension text.

Drawing score: drawing_finalize returns payload["score"] — a 0-100 scalar + invalidity_ratio + A-F grade over the validator + critique union. Use it as the objective quality metric.

Measuring: never read vertices back and shoelace them — that loses 28.2% of the area on a semicircular edge, silently. analysis_measure_entity(handle) reads the real geometry, and its payload states its own accuracy: exact, flatten_tolerance, assumed_closed, self_intersecting. A HATCH reports the area it fills (outer loops minus islands) plus hatch_style; on a curved hatch edge flatten_tolerance is not the accuracy knob, because ezdxf hands boundaries over as cubic Beziers whose ~0.028% circle error is already baked in. boundary_trace / boundary_from_entities report the same number analysis_measure_entity gives for the polyline they just drew — if those ever diverge, one of them is approximating and neither says so.

Benchmark matrix: benchmarks/tasks_v4.py is the current set (17 tasks; --matrix v3 / --matrix v2 reproduce the earlier sets). The published competitor reports are pinned v1.4 runs against v2 and are never back-filled for the five v3 tasks or the v4 pid_roundtrip / page_setup_truth — the chart shows them not run, not zero. benchmarks/correctness_suite.py (32 checks) is the release A/B gate: python benchmarks/compare_versions.py v1.5.1 (the published report is benchmarks/results/published/ab-v1.5.1-vs-v1.6.0-dev.json; scripts/check_doc_numbers.py names it in AB_REPORT).

Adding a New Tool

  1. Add the method to the matching module in backends/contracts/ — @abstractmethod if both engines must have it, @capability("key", reason=...) only if it is a real engine boundary (and then declare key in both capability maps, or the gate fails)
  2. Implement it in EzdxfBackend (backends/ezdxf_backend.py) using the _async(func) wrapper
  3. Implement it in ComBackend (backends/com_backend.py) using self._com(func)
  4. Register the tool in server.py with @mcp.tool(...) calling _backend(ctx).your_method(...)
  5. Use _dc(result) to convert dataclass returns to dicts

Key Conventions

  • Entity handles: hex strings (e.g. "1A2B"). Always returned from create/copy operations; required by all modify/query operations.
  • Coordinates: drawing units (mm by default); angles in degrees, counter-clockwise from X axis. Every coordinate in or out of a tool is WCS on both engines. DXF stores CIRCLE/ARC/LWPOLYLINE/TEXT/INSERT geometry in a per-entity frame (extrusion); backends/ocs.py translates at the backend boundary, so OCS never reaches a tool. An entity in a plane tilted out of WCS XY reports plane_normal and omits the fields xy cannot express; writing an xy into such an entity is refused with capability: "ocs_tilted_plane" — use entity_move instead.
  • ACI colors: 1–255 for specific colors, 256=ByLayer, 0=ByBlock.
  • COM backend only: system_run_command, system_run_lisp, and view_zoom_extents/window (view ops are no-ops in ezdxf).
  • Screenshot: COM uses Win32 window capture (Pillow required); ezdxf renders via matplotlib.
  • _dc(obj): converts dataclasses to dicts recursively for JSON serialization.
  • Engineering layer scaffold: drawing_new auto-bootstraps standard linetypes (CENTER, HIDDEN, PHANTOM) and engineering layers (GEOMETRY, DIM, CENTER, HIDDEN, PHANTOM, HATCH, TEXT, TITLEBLOCK). Pass bootstrap=False to opt out.
  • Generic contracts added in 1.6: block_define(name, entities, attdefs) (typed primitives + ATTDEFs, both engines; overwrite replaces contents so INSERTs keep the name) and entity_get/set_xdata (typed by group code; 255-char strings, 16 KB per entity, enforced before writing).
  • P&ID reader rules: pid_graph never modifies the drawing, reads geometry from the drawing (an INSERT's real rotation/scale) rather than the catalogue, and reports source/confidence per node (catalog 1.0, xdata 0.95, heuristic 0.6, inferred 0.3) — never raised by agreement between heuristics. Read stats.confidence_min before trusting a foreign drawing.
  • TOOL_PACKS=all|core,pid,settings advertises only the packs a client uses; core is always on; system_about reports tool_packs.
  • Layer states are ours, not AutoCAD's. layer_state_* stores a portable snapshot in an ACADMCP_LAYERSTATES XRECORD; it survives save/reopen on both engines and travels with the file, and it is not listed in AutoCAD's Layer States Manager. Said in the tool docstring, in system_capabilities.layer_states (mode: "xrecord", reason portable_acadmcp_xrecord;not_listed_in_autocad_layer_states_manager) and in the README.
  • Page-setup evidence is the PDF. batch_plot reports mediabox_mm parsed from the file's /MediaBox (engineering/standards/pdfinfo.py); a page setup is verified by what a printer would get, not by the setter's return value. A style or setting write reports changed — re-setting a value to itself is not a change.
  • Nothing interprets coordinates in UCS. ucs_set / ucs_restore store and activate a UCS for the operator's benefit; every tool input and output stays WCS (the repository rule above).
  • Production drawings: For real engineering output, use the engineering/ package primitives via the gear_* / keyway_* / titleblock_* MCP tools — do NOT hand-draw teeth/keyways/sections with raw entity_create_* calls. Always end with drawing_finalize for the 8-step validator.

Premium Drawing Rules

These rules are non-negotiable for production engineering output.

  1. Plan before draw: Call drawing_plan(intent, scale, sheet) before any entity_create_*. The returned PlanSpec is held by the backend and replayed by drawing_critique at finalize time.
  2. No coordinate guessing: Never compute snap points (endpoints, midpoints, intersections, perpendicular feet) from memory. Use point_from_snap(handle, "end"|"mid"|"center"|"quad"|"perp"|"near", ref_x, ref_y).
  3. Layer discipline: All geometry on engineering layers (GEOMETRY, HIDDEN, CENTER, ...). Construction geometry on CONSTRUCTION layer (color 250, lightest weight); wipe with construction_clear() before finalize.
  4. Lineweights are ISO 128: 0.13/0.18/0.25/0.35/0.50/0.70/1.00/1.40/2.00 mm only. Use drawing_apply_iso_layers("mech"|"pid"|"iso13567") to bootstrap correct lineweights per layer; never set lineweight manually outside this set.
  5. Corners must be explicit: Two intersecting lines that should meet at a sharp corner must use entity_trim (with keep_x/keep_y) — leaving overshoot is reported as untrimmed_corner by drawing_critique. Rounded/beveled corners use entity_fillet / entity_chamfer.
  6. Dimensions via auto: Prefer dimension_auto(handles, "chain"|"baseline"| "ordinate") over individual dimension_linear calls. Manual dimensions only for special cases (leader notes, ordinate origins).
  7. Critique-then-finalize: drawing_critique(focus=[...]) must return zero issues before drawing_finalize. Available focuses (closed enum): iso128, layer_color, dim_overlap, untrimmed_corner, duplicate_entities, construction_left. Pass focus=None for all.
  8. Meta-tools over raw: When a meta-tool exists for a workflow, use it; do not reimplement with low-level primitives. Same rule as the engineering- primitives line above.

Standard Premium Workflow (template)

1. plan = drawing_plan(intent, sheet_size="A3", scale=1.0)
2. drawing_apply_iso_layers("mech")        # or "pid", "iso13567"
3. construction_xline(...)                 # scaffolding (optional)
4. entity_create_line / circle / ...       # main geometry on engineering layers
5. entity_trim / fillet / chamfer          # close corners explicitly
6. handles = entity_select_smart({...})    # avoid handle-memorization
7. dimension_auto(handles, style="chain")  # ISO 129 dims
8. issues = drawing_critique(focus=None)   # must be []
9. construction_clear()                    # wipe scaffolding
10. drawing_finalize(save_path=..., screenshot_path=...)