The evidence for track H, in the shape tracks A, E, B + G and F used.
- benchmarks/tasks_v7.py: 21 tasks, DEFAULT_MATRIX = "v7"; v6 to v2 stay
addressable. Both new tasks read the synthetic plant pair and nothing
else. takeoff_roundtrip: the P&ID called schematic, every routable run's
layout length equal to the rectilinear MST of its tags, sizes and services
as drawn (a return drawn on the supply layer included), the cable metres
rounded up after the allowance, an unstated power left empty.
understand_foreign: inches over millimetres flagged, the stray entity found
by handle, two plan copies separated, five layers classified. A test breaks
each input and watches its gate fail by name.
- Five correctness checks, every number worked out by hand in its docstring;
the published A/B report is re-run over 48 checks: 26 / 48 -> 48 / 48,
zero regressed.
- scripts/smoke_understand_com.py reads the pair through the live engine -
snapshots, the DWG route, both takeoffs and a diff - and closes every
document it opens; the CHANGELOG records what it returned.
scripts/field_test_takeoff.py runs the takeoffs on a user's own files,
refuses to write inside the repository and prints no quantity by default.
- The README token table is remeasured; check_doc_numbers derives the task
count from the v7 matrix; CLAUDE.md, both READMEs, the CHANGELOG and the
spec status line say what shipped.
One reader defect found on the way: describe.tag_occurrences read a wiring
callout ("Wiring to CP-2") as a place its panel is written, so the scale
check called CP-2 ambiguous and read "Wiring to CP1" as a phantom tag CP1 -
10 tags and 45 pairs (13.3 % within 10 %) where the generator placed 11 and
55 (21.8 %). A callout is no longer a tag occurrence, the rule
network.tag_occurrences already kept; two tests failed before the fix.
The smoke works only in documents it opened from its own temporary folder,
known by full path; it attaches with GetActiveObject only (the backend's
Dispatch fallback would launch AutoCAD), checks in the same COM-thread call
that the active document is its own before each snapshot, the DWG save and
the move, closes its documents with Close(False) and re-activates the
document that was active when it started. No new ActiveX member: Documents,
Item, FullName, ActiveDocument, Activate and Close are the ones the backend
already calls. Measured on AutoCAD 2026 (25.1s): exit 0, model-space types
equal file / live / DWG, export 0.104 s, no document of the run left open.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
33 KiB
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]"
# XLSX workbooks (takeoffs, data_extract) via openpyxl
pip install -e ".[office]"
# 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;@abstractmethodstays the default. Every key used must be declared in both capability maps —tests/test_capability_contract.pyfails 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 theAutoCADBackendcomposition of every contract. Import surface is unchanged:from backends.base import AutoCADBackend, UnsupportedCapabilityErrorstill works.backends/ezdxf_backend.py—EzdxfBackend: file-based DXF operations using theezdxflibrary. All sync ezdxf calls are wrapped withasyncio.to_threadvia_async(). Transactions are implemented as full DXF snapshots on an_undo_stack.backends/com_backend.py—ComBackend: live AutoCAD control viapywin32COM. All COM calls are routed through aThreadPoolExecutorwith a single thread to satisfy AutoCAD's STA (Single-Threaded Apartment) COM requirement.
Backend selection at startup (in server.py::_make_backend):
- Check
AUTOCAD_MCP_BACKENDenv var - On Windows with
auto/com: try COM first, fall back to ezdxf - 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 inctx.lifespan_context["backend"] - Middleware stack:
ErrorHandlingMiddleware→AuditMiddleware(custom timing/audit log) →TimingMiddleware→LoggingMiddleware _backend(ctx)helper: retrieves the backend from lifespan context, raisesToolErrorif not ready
Tools are organized into sections (counts are indicative — system_about is authoritative):
- Drawing Management (12 tools):
drawing_*(includesdrawing_redo) - Entity Creation (14 tools):
entity_create_*(includesentity_create_table,leader_create_mleader) - Dimensions (5 tools):
dimension_* - Entity Modification (16 tools):
entity_move/copy/rotate/scale/mirror/offset/delete/array_*, corner ops (entity_trim/extend/fillet/chamfer), plus in-placeentity_edit_text(TEXT/MTEXT content/height/rotation) andentity_edit_geometry(CIRCLE/LINE/ARC — center/radius/endpoints/angles), both handle-preserving - Entity Query (9 tools):
entity_get,entity_list,entity_delete_many,selection_get,selection_window/polygon/filter, andentity_get_xdata/entity_set_xdata(typed by group code) - Layer Management (14 tools incl. 2 linetype_*):
layer_*,linetype_list,linetype_load - Block Operations (8 tools):
block_*— includesblock_define(a block from typed primitive specs + ATTDEFs, no source entities needed) - Analysis & Query (8 tools):
analysis_*— plus Batch (2), Templates (2), Validation (1) - View & Screenshot (4 tools — includes
view_zoom_and_screenshot):view_* - Transactions (3 tools):
transaction_begin/commit/rollback - System (8 tools):
system_status/get_variable/set_variable/run_command/run_lisp/about/capabilities, plusdrawing_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) - Engineering / Deterministic CAD (8 tools):
gear_draw_*,keyway_draw_*,titleblock_apply_iso_a3,drawing_finalize - 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 - GD&T (ISO 1101 / ASME Y14.5):
gd_frame(feature control frames),datum_feature— enforced by thegdtcritique focus - Layouts & Paper Space (12 tools): tab lifecycle
layout_list/create/set_current/delete/rename/copy, viewportsviewport_create/list/set_scale/lock/delete, andentity_change_space(CHSPACE — taggedlayoutandmodify, andlayoutwins the group priority, so it files here).drawing_export_pdftakes an optionallayoutparam 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_spaceis the mirror case — ezdxf-only (chspace), because ActiveX exposes no change-space member and the COM route is unverified. - 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 reportssolid_3das unsupported (no headless ACIS). - 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 readerpid_graphand its deliverablespid_instrument_index/line_list/equipment_list, andpid_from_spec(whole sheet, one transaction). Sixpid_*critique focuses (pid_dangling_line,pid_duplicate_tag,pid_incompatible_connection,pid_untagged_instrument,pid_illegal_tag,pid_unconnected_equipment) run underfocus=Noneand insidedrawing_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 byscripts/smoke_pid_com.py(AutoCAD 2026). - Styles (10 tools, pack
core):dimstyle_list/create/modify/set_current(authored ISO-25 / ANSI presets fromengineering/standards/dimstyles.py;dimstyle_modifyaccepts onlyDIM_VARIABLE_WHITELISTnames (33) and refuses out-of-range values by name before writing;changedlists only variables that moved, andrerender_required: truesays a headless DIMENSION keeps its rendered block until redrawn),textstyle_list/create/set_current(font_resolved: falseis reported, never refused — DXF stores only the name),mleaderstyle_list/create(both engines: ezdxf ondoc.mleader_styles, live through theACAD_MLEADERSTYLEdictionary — ActiveX has no MLeaderStyle collection, which is not an engine boundary, so nomleaderstylecapability key exists), anddrawing_apply_standard("iso"|"ansi")— dimstyle + text style + units + themechlayer set in one call, each item reportingcreatedor already present. - Page setup & templates (6 tools, pack
core):page_setup_list/apply(ISO 216 / ANSI Y14.1 sizes fromPAPER_SIZES; an unknown ctb is written and reportedplot_style_known: false),plot_style_list(catalog + the files actually installed on a live seat),batch_plot(every paper layout throughdrawing_export_pdf(layout=);mediabox_mmis 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,.dwttwin on COM,scripts/build_templates.py --checkproves the DXFs reproducible),drawing_template_save(a.dwtrequest headlessly is refused with capabilitydwt_writenaming the DXF route).drawing_new(template=)resolves a catalog name to the bundled file and reportstemplate: {name, source}. - Environment (22 tools, pack
settings): documentsdocument_list/activate/close(both engines — the headless backend keeps a document registry;document_closerefuses to discard unsaved work unlessdiscard=True), layer stateslayer_state_save/restore/list/delete(portable: JSON chunks in anACADMCP_LAYERSTATESXRECORD that travels with the file and does not appear in AutoCAD's Layer States Manager —system_capabilities.layer_statessays so), named viewsview_named_save/restore/list(headless restore writes$VIEWCTR/$VIEWSIZEand reportsapplied: "header_only"), UCSucs_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-entrySYSVAR_CATALOG; an unknown name answersknown: falseplus nearest names, never an error;system_set_variablenow 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, capabilitydwgprops), and the live-only groupsystem_launch(live_application),system_preferences_get/set(preferences; a whitelist ofPreferences.*keys with ranges, read-only keys refused by name, writes reportold/new),user_pick_point/select,system_prompt_message(interactive_prompt; a cancelled pick returns{cancelled: true}, aCOM_CALL_TIMEOUToverrun reportstimed_out). The COM paths were executed live once byscripts/smoke_settings_com.py(AutoCAD 2026), which also built the.dwttwins. - Mechanical parts (6 tools, pack
mech):mech_part_draw,mech_view_add,mech_dimension_part,mech_hole_pattern,mech_part_from_spec,mech_part_inspect. One part model — a profile (RevolvedPartsegments or aPrismaticPartoutline) plus typed features — and one view engine that turns it into front / side / top / section / detail views with hidden lines, cut faces and ISO 129 dimensions. The model is written onto the drawing asACADMCP_MECHXDATA, somech_view_addcan add a view months later without the caller re-describing the part. A feature that cannot be projected into a view is listed inomitted: [{feature, reason}]— never dropped. - Standard parts (3 tools, pack
mech):std_part_list,std_part_insert,std_feature_draw. ISO 4014 / 4017 / 4032 / 7089 / 4762 fasteners, ISO 15 bearings with ISO 8826-1 representation, DIN 471/472 retaining-ring grooves, DIN 509 undercuts, DIN 332 centre holes and ISO 3601-2 O-ring grooves — authored tables, inserted as real blocks with attributes andACADMCP_MECHXDATA, which is what makes the parts list a read of the drawing. A size outside a table's transcribed coverage is refused by name, never interpolated. - Mechanical annotation (5 tools, pack
mech):surface_texture(ISO 21920-1, ISO 1302's older Ra form accepted),weld_symbol(ISO 2553),centre_marks(ISO 128-23),section_line(ISO 128-40 — it returns the planemech_view_add(kind="section")consumes, so the label and the view cannot disagree),hatch_material(the ISO 128-50 map; an unknown material is refused with the list, never hatched as steel). - Sheet and delivery (11 tools, pack
core):sheet_frame(ISO 5457 A4-A0 — 20 mm filing margin, 10 mm elsewhere on A3, zone grid, centring and trimming marks),titleblock_apply(ISO 7200 for every size;titleblock_apply_iso_a3is now a thin alias),revision_add,bom_extract(never modifies the drawing),bom_table(ISO 7573),balloon_add(ISO 6433, linked by XDATA so a rerun renumbers instead of duplicating),data_extract(CSV always; XLSX refused withxlsx_writewhenopenpyxlis absent),xref_attach,xref_manage,image_attach,drawing_export_dwg(COMSaveAsnatively; headless only with the ODA File Converter, otherwisedwg_writerefuses and names the install route). The COM paths of SECTIONS 21-24 were executed live once byscripts/smoke_mech_com.py(AutoCAD 2026): part, section, dimensions, an ISO 4014 block with its XDATA, the parts list read off the drawing, a linked balloon, an xref attached and detached, a real.dwg, anddrawing_critique(focus=None)clean. - Architecture (12 tools, pack
arch):arch_wall,arch_opening,arch_stair,arch_room,arch_rooms_detect,arch_schedule,arch_grid,arch_symbol,arch_catalogue_list,arch_catalogue_insert,arch_dimension_chains,arch_plan_from_spec. One plan model — a wall network (axis polyline, thickness, justification, material) with hosted doors and windows — and one engine: L / T / X junctions resolved, openings cut, cut faces hatched by material (engineering/arch/materials.py: ISO 128-50 general hatching for brick, the JIS A 0150 RC / stone / wood symbols, AutoCAD's AR-CONC / AR-SAND, AAC as a declared convention), and anything it cannot place listed inomitted: [{element, reason}], never dropped. Rooms are computed: a label's area is the planar face its point lies in, andarch_rooms_detectreads rooms from any plan (confidence 1.0 for our walls, 0.6 for plain lines) without modifying it. Schedules are a read of theACADMCP_ARCHrecords, drawn as real TABLEs. Labels and schedules speak English, or Turkish withlang="tr"(decimal comma). The COM paths were executed live once byscripts/smoke_arch_com.py(AutoCAD 2026). - Understanding & QA (4 tools, pack
core):drawing_understand(one read-only report of a drawing somebody else made — declared vs inferred units with the evidence, declared vs robust extents and the outlier handles behind them, clusters (sheets, plan copies), every layer's likely discipline and service from an EN / TR / RU / NL / DE vocabulary with a confidence and the deciding keyword, equipment tags, rooms, text languages, blocks, layouts, title-block attributes),drawing_scale_check(a drawing against a reference: tags unique within a cluster are matched — a tag seen twice isambiguous, never picked — pairs at least 2 m apart, the median ratio, IQR and the share within ±10 %:to_scaleat 80 % or more, with its factor;schematicat 50 % or less;partlybetween),drawing_diff(two revisions matched by signature, then handle, then type + layer + nearest position;markup=Truedraws revision clouds on the newer one — its only write) anddrawing_topology_check(dangling ends, near misses, interior crossings; the threetopo_*critique focuses run the same engine underfocus=Noneon layers no semantic focus covers). Every one reads one snapshot per drawing (engineering/understand/snapshot.py:ezdxfdirectly, or on a live seatDocument.Export(<tmp>/snap, "DXF", ...), which never renames the document and carries model space, paper space and block definitions — measured). - Plant takeoffs (2 tools, pack
plant):pipe_takeoff(topology from the P&ID — segments merged at their ends, split at T-junctions, continued through inline fittings, a double-drawn segment counted once; service from the layer overridden by supply / return words; sizes kept as drawn —Ø51,SMS51,DN20are never equated — direct, by continuity orunassigned; lengths from the layout: the rectilinear MST of the tags a run connects, labelled an estimate, the P&ID length kept beside it as a comparison; statuses A / B / C) andcable_takeoff(one row per load: power from the P&ID's electrical texts, never invented; the panel from the layout'sWiring to CPncallouts; the Manhattan length plus the allowance rounded up to the metre; a section only from a caller'ssection_rules). Both write a workbook (Metraj / Özet / Kontrol / Metodoloji,langtr / en / ru, XLSX with theofficeextra, CSV always) and never the drawing.scripts/field_test_takeoff.pyruns both on a user's own pair and writes next to it, never into this repository. The COM paths were executed live once byscripts/smoke_understand_com.py(AutoCAD 2026).
Tool profiles: TOOL_PROFILE=lean|full (default full) controls the advertised surface — lean = 65 curated drafting tools (55 with TOOL_PACKS=core, which drops the three pid_*, the three mech_* and the four arch_* lean members, and keeps drawing_understand; 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,mech,arch,plant (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, mech the 14 SECTION 21-23 mechanical tools, arch the 12 SECTION 25 architectural tools, plant the two SECTION 27 takeoffs (track D's Plant 3D readers join it); SECTION 18/19/24/26 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_v7.py is the current set (21 tasks; --matrix v6 / v5 / v4 / v3 / 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, the v4 pid_roundtrip / page_setup_truth, the v5 mech_assembly (a flange-coupling sheet gated on a zero-issue drawing_critique(focus=None) and a finalize score of at least 90) or the v6 arch_roundtrip (a two-room plan drawn by arch_plan_from_spec, its rooms read back by arch_rooms_detect within 0.1 % of the label and of the hand-computed net floor, the schedules listing the drawn tags, gated like mech_assembly) or the v7 takeoff_roundtrip / understand_foreign (the synthetic plant pair of tests/fixtures/plant_pair.py read without its truth: the P&ID called schematic, every routable run's layout length equal to the rectilinear MST of its tags, the cable metres rounded up after the allowance; units, the outlier, both plan copies and the service layers found by one describe) — the chart shows them not run, not zero. benchmarks/correctness_suite.py (48 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
- Add the method to the matching module in
backends/contracts/—@abstractmethodif both engines must have it,@capability("key", reason=...)only if it is a real engine boundary (and then declarekeyin both capability maps, or the gate fails) - Implement it in
EzdxfBackend(backends/ezdxf_backend.py) using the_async(func)wrapper - Implement it in
ComBackend(backends/com_backend.py) usingself._com(func) - Register the tool in
server.pywith@mcp.tool(...)calling_backend(ctx).your_method(...) - 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.pytranslates at the backend boundary, so OCS never reaches a tool. An entity in a plane tilted out of WCS XY reportsplane_normaland omits the fields xy cannot express; writing an xy into such an entity is refused withcapability: "ocs_tilted_plane"— useentity_moveinstead. - ACI colors: 1–255 for specific colors, 256=ByLayer, 0=ByBlock.
- COM backend only:
system_run_command,system_run_lisp, andview_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_newauto-bootstraps standard linetypes (CENTER, HIDDEN, PHANTOM) and engineering layers (GEOMETRY, DIM, CENTER, HIDDEN, PHANTOM, HATCH, TEXT, TITLEBLOCK). Passbootstrap=Falseto opt out. - Generic contracts added in 1.6:
block_define(name, entities, attdefs)(typed primitives + ATTDEFs, both engines;overwritereplaces contents so INSERTs keep the name) andentity_get/set_xdata(typed by group code; 255-char strings, 16 KB per entity, enforced before writing). - P&ID reader rules:
pid_graphnever modifies the drawing, reads geometry from the drawing (an INSERT's real rotation/scale) rather than the catalogue, and reportssource/confidenceper node (catalog 1.0, xdata 0.95, heuristic 0.6, inferred 0.3) — never raised by agreement between heuristics. Readstats.confidence_minbefore trusting a foreign drawing. TOOL_PACKS=all|core,pid,settings,mech,arch,plantadvertises only the packs a client uses;coreis always on;system_aboutreportstool_packs.- Layer states are ours, not AutoCAD's.
layer_state_*stores a portable snapshot in anACADMCP_LAYERSTATESXRECORD; 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, insystem_capabilities.layer_states(mode: "xrecord", reasonportable_acadmcp_xrecord;not_listed_in_autocad_layer_states_manager) and in the README. - Page-setup evidence is the PDF.
batch_plotreportsmediabox_mmparsed 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 reportschanged— re-setting a value to itself is not a change. - Nothing interprets coordinates in UCS.
ucs_set/ucs_restorestore and activate a UCS for the operator's benefit; every tool input and output stays WCS (the repository rule above). - The part model, not primitives: a machine part is
mech_part_drawwith a segment list or an outline plus typed features — never hand-drawn lines. The same rule as gears and keyways, for the same reason. - Standards tables are transcribed, never derived: every row under
engineering/mech/standards/carries aSOURCEnaming standard, edition and table, and a size outside the coverage is refused by name.engineering/mech/standards/materials.pystates which of its rows are ISO 128-50, which are ASME Y14.2M viaacadiso.pat, and which (concrete, wood) have no mechanical standard at all. - Six
mech_*critique focuses (mech_missing_centreline,mech_unhatched_section,mech_view_misaligned,mech_duplicate_dimension,mech_thread_unrepresented,mech_bom_balloon_mismatch) run underfocus=Noneand insidedrawing_finalize; they return nothing on a drawing with noACADMCP_MECHpayload.mech_view_misalignedchecks the projection axis and not the quadrant, andmech_duplicate_dimensioncatches two dimensions in the same place rather than a semantic restatement — both narrowings are in the module docstring, because the payload carries no projection angle and no engine exposes what a dimension measured. - Live ActiveX will not create a layer on assignment (
entity.Layer = "X"on a missing layer raisesKey not found; ezdxf creates it silently). Every mechanical/annotation draw path ensures its target layers before the first entity — a new path must do the same, or it passes every headless test and dies live. - A diameter's text stands beyond the FIRST chord point on both engines (the ActiveX
AddDimDiametricrule, measured);dimension_diameter(x1, y1, x2, y2)annotates on the(x1, y1)side. - The plan model, not lines: a wall is
arch_wall(an axis, a thickness, a justification, a material) and a door or window is hosted in it byarch_opening— never hand-drawn lines. The engine resolves the junctions and cuts the openings; what it cannot place is inomitted. - Areas are measured, never typed: a room's area is the planar face its label point lies in, written into the label and the
roomrecord byarch_room; no tool accepts an area from its caller, andarch_plan_from_specrefuses a typedareaby path. - Plan text follows the plot scale:
scaleis a denominator (50 = 1:50) and a paper heighthis drawn ath × scaledrawing units — no annotative objects.lang="tr"switches words and the decimal separator; any other language is refused with the list. - Three
arch_*critique focuses (arch_room_unlabelled,arch_wall_gap— two wall ends within 50 mm that do not join,arch_opening_clash) run underfocus=Noneand insidedrawing_finalize; they return nothing on a drawing with noACADMCP_ARCHrecord. - Nominal, not standard: the furniture and sanitary blocks carry nominal catalogue dimensions and say so; the stair tool reports the Blondel relation (2R + G within 600–650 mm) as a design rule, never as a building code; the
archlayer colours are a repository convention (ISO 13567 fixes names, not colours). - Read-only by default (track H):
drawing_understand,drawing_scale_check,drawing_topology_checkand both takeoffs never modify the drawing;drawing_diffwrites only withmarkup=True, and a takeoff writes a workbook file, never the drawing. - Lengths come from the inferred unit: a reader measures in the unit the geometry implies (text heights, wall and door sizes, overall size) and reports metres; a disagreement with
INSUNITSis a warning with the numbers — a layout declaring inches over millimetre geometry is measured in millimetres, never ×25.4. - Scale check before a P&ID length: a P&ID is a length source only when
drawing_scale_checkcalls itto_scale; otherwise a run's length is the orthogonal estimate on the layout and the drawn length stays a comparison column (length_source="pid"on a schematic P&ID is refused unlessforce=True, quoting the statistics). - Nothing is invented (track H): a power the P&ID does not state stays empty and becomes an open item, an unreadable size stays
unassigned, and no cable-section table is authored — a section comes only from the caller'ssection_rules. - Client drawings never enter the repository: every track-H fixture is the synthetic plant pair (
tests/fixtures/plant_pair.py), whose truth is computed from the positions it placed;scripts/field_test_takeoff.pyrefuses an output folder inside the repository and prints no quantity unless--showis given. - Production drawings: For real engineering output, use the
engineering/package primitives via thegear_*/keyway_*/titleblock_*MCP tools — do NOT hand-draw teeth/keyways/sections with rawentity_create_*calls. Always end withdrawing_finalizefor the 8-step validator.
Premium Drawing Rules
These rules are non-negotiable for production engineering output.
- Plan before draw: Call
drawing_plan(intent, scale, sheet)before anyentity_create_*. The returned PlanSpec is held by the backend and replayed bydrawing_critiqueat finalize time. - 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). - Layer discipline: All geometry on engineering layers (GEOMETRY, HIDDEN,
CENTER, ...). Construction geometry on
CONSTRUCTIONlayer (color 250, lightest weight); wipe withconstruction_clear()before finalize. - 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. - Corners must be explicit: Two intersecting lines that should meet at a
sharp corner must use
entity_trim(withkeep_x/keep_y) — leaving overshoot is reported asuntrimmed_cornerbydrawing_critique. Rounded/beveled corners useentity_fillet/entity_chamfer. - Dimensions via auto: Prefer
dimension_auto(handles, "chain"|"baseline"| "ordinate")over individualdimension_linearcalls. Manual dimensions only for special cases (leader notes, ordinate origins). - Critique-then-finalize:
drawing_critique(focus=[...])must return zero issues beforedrawing_finalize. Available focuses (closed enum):iso128,layer_color,dim_overlap,untrimmed_corner,duplicate_entities,construction_left. Passfocus=Nonefor all. - 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=...)