Add AutoCAD MCP premium skill and backend APIs

Introduce the autocad-mcp-premium skill with workflow docs, examples, and references (.claude/skills/autocad-mcp-premium/*). Add PlanSpec/critique plumbing and engineering helpers (engineering/plan_spec.py, engineering/critique.py) plus tests and a smoke script. Extend backend API (backends/base.py) with corner operations and premium meta-tools (trim/extend/fillet/chamfer, drawing_plan, drawing_critique, point_from_snap, construction_xline/clear, drawing_apply_iso_layers, dimension_auto, entity_select_smart) and implement COM backend corner ops (backends/com_backend.py) with placeholders for meta-tools. Update CLAUDE.md to document the premium drawing rules and standard workflow. Misc: add examples, references, and tests for premium features, and minor server/backend adjustments.
This commit is contained in:
U-C4N
2026-05-15 19:17:14 +03:00
parent 88a9005a6a
commit 9258da429f
19 changed files with 2847 additions and 2 deletions
+240
View File
@@ -868,6 +868,83 @@ async def entity_offset(
return _dc(result)
# ── corner operations (trim/extend/fillet/chamfer) ──────────────────────────
@mcp.tool(
annotations={"title": "Trim Entity", "readOnlyHint": False, "destructiveHint": False},
tags={"entity", "modify", "corner"},
)
async def entity_trim(
target_handle: Annotated[str, "Handle of the line being trimmed"],
cutter_handle: Annotated[str, "Handle of the cutting line"],
keep_x: Annotated[float, "X of a point on the side of the target to KEEP"],
keep_y: Annotated[float, "Y of a point on the side of the target to KEEP"],
ctx: Context = None,
) -> dict:
"""Trim `target` against `cutter`, keeping the segment containing (keep_x, keep_y).
V1 supports LINE+LINE only. Cutter is treated as an infinite ray (AutoCAD's
default 'implied extend' trim mode). Raises if the lines are parallel.
"""
result = await _backend(ctx).entity_trim(target_handle, cutter_handle, keep_x, keep_y)
return _dc(result)
@mcp.tool(
annotations={"title": "Extend Entity", "readOnlyHint": False, "destructiveHint": False},
tags={"entity", "modify", "corner"},
)
async def entity_extend(
target_handle: Annotated[str, "Handle of the line being extended"],
boundary_handle: Annotated[str, "Handle of the boundary line"],
end_x: Annotated[float | None, "X of a point near the endpoint to extend (None = auto)"] = None,
end_y: Annotated[float | None, "Y of a point near the endpoint to extend (None = auto)"] = None,
ctx: Context = None,
) -> dict:
"""Extend `target` to meet `boundary`. If end_x/end_y is None, the target
endpoint nearest the boundary is auto-selected.
V1 supports LINE+LINE only. Raises if the lines are parallel.
"""
result = await _backend(ctx).entity_extend(target_handle, boundary_handle, end_x, end_y)
return _dc(result)
@mcp.tool(
annotations={"title": "Fillet Two Entities", "readOnlyHint": False, "destructiveHint": False},
tags={"entity", "modify", "corner"},
)
async def entity_fillet(
handle1: Annotated[str, "First entity handle"],
handle2: Annotated[str, "Second entity handle"],
radius: Annotated[float, Field(description="Fillet radius (>= 0; 0 = sharp corner / corner-merge)", ge=0.0)],
trim: Annotated[bool, "If true, trim source entities to the tangent points (AutoCAD default)"] = True,
ctx: Context = None,
) -> dict:
"""Round a corner with a tangent arc. Returns info on the new ARC entity
(or the first source line for radius=0). V1 supports LINE+LINE only."""
result = await _backend(ctx).entity_fillet(handle1, handle2, radius, trim)
return _dc(result)
@mcp.tool(
annotations={"title": "Chamfer Two Entities", "readOnlyHint": False, "destructiveHint": False},
tags={"entity", "modify", "corner"},
)
async def entity_chamfer(
handle1: Annotated[str, "First entity handle"],
handle2: Annotated[str, "Second entity handle"],
dist1: Annotated[float, Field(description="Chamfer distance along first line", gt=0.0)],
dist2: Annotated[float | None, "Chamfer distance along second line (None = symmetric, dist2=dist1)"] = None,
trim: Annotated[bool, "If true, trim source entities to the tangent points (AutoCAD default)"] = True,
ctx: Context = None,
) -> dict:
"""Bevel a corner with a chamfer line. Returns info on the new chamfer LINE.
V1 supports LINE+LINE only."""
result = await _backend(ctx).entity_chamfer(handle1, handle2, dist1, dist2, trim)
return _dc(result)
@mcp.tool(
annotations={"title": "Delete Entity", "readOnlyHint": False, "destructiveHint": True},
tags={"entity", "modify"},
@@ -2243,6 +2320,169 @@ async def drawing_finalize(
return payload
# ---------------------------------------------------------------------------
# ── SECTION 13: Premium Drafting Meta-Tools (5x quality multiplier) ─────────
# ---------------------------------------------------------------------------
# These tools wrap the entity primitives in a quality-first workflow:
# plan → snap-aware geometry → critique → finalize.
# See `.claude/skills/autocad-mcp-premium/` for the full discipline.
@mcp.tool(
annotations={"title": "Drawing: Plan (commit intent before drawing)",
"destructiveHint": False},
tags={"premium", "planning"},
)
async def drawing_plan(
intent: Annotated[str, "One-line description of what this drawing represents."],
sheet_size: Annotated[str, Field(default="A3",
description="Paper size: A4 / A3 / A2 / A1 / A0.")] = "A3",
scale: Annotated[float, Field(default=1.0, gt=0,
description="Drawing scale (1.0 = 1:1, 0.1 = 1:10, etc).")] = 1.0,
layer_set_id: Annotated[str, Field(default="mech",
description="Layer set to bootstrap: mech / pid / iso13567.")] = "mech",
view_count: Annotated[int, Field(default=1, ge=1)] = 1,
dim_style: Annotated[str, Field(default="chain",
description="Default dimensioning style: chain / baseline / ordinate / mixed.")] = "chain",
notes: Annotated[list[str] | None, "Free-form constraint notes."] = None,
ctx: Context = None,
) -> dict:
"""Commit a PlanSpec before any geometry is created.
The PlanSpec is held by the backend and replayed by `drawing_critique`
at finalize time to verify the drawing matches the original intent.
Always call this FIRST in a premium workflow.
"""
plan = await _backend(ctx).drawing_plan(
intent, sheet_size, scale, layer_set_id, view_count, dim_style, notes,
)
return _dc(plan)
@mcp.tool(
annotations={"title": "Drawing: Critique (premium quality checks)",
"destructiveHint": False, "readOnlyHint": True},
tags={"premium", "validation"},
)
async def drawing_critique(
focus: Annotated[list[str] | None, Field(default=None,
description="Subset of: iso128, layer_color, dim_overlap, untrimmed_corner, "
"duplicate_entities, construction_left. None = run all.")] = None,
ctx: Context = None,
) -> list[dict]:
"""Run premium-quality checks. Returns zero issues for a clean drawing.
Standard production gate: must return [] before `drawing_finalize`.
"""
issues = await _backend(ctx).drawing_critique(focus)
return [_dc(i) for i in issues]
@mcp.tool(
annotations={"title": "Point: Snap (deterministic OSNAP)", "readOnlyHint": True},
tags={"premium", "snap"},
)
async def point_from_snap(
handle: Annotated[str, "Entity handle to snap onto"],
snap: Annotated[str, Field(description="Snap type: end | mid | center | quad | perp | near")],
ref_x: Annotated[float | None, "Reference X (required for perp/near; disambiguates end/quad)"] = None,
ref_y: Annotated[float | None, "Reference Y"] = None,
ctx: Context = None,
) -> dict:
"""Compute a deterministic snap point on an entity. Use this INSTEAD OF
guessing coordinates — eliminates the most common LLM drawing error.
"""
pt = await _backend(ctx).point_from_snap(handle, snap, ref_x, ref_y)
return {"x": float(pt[0]), "y": float(pt[1])}
@mcp.tool(
annotations={"title": "Construction: XLine (infinite reference)",
"destructiveHint": False},
tags={"premium", "construction"},
)
async def construction_xline(
x: Annotated[float, "Base point X"],
y: Annotated[float, "Base point Y"],
angle_deg: Annotated[float, "Angle in degrees (0=horizontal, 90=vertical)"],
layer: Annotated[str, "Layer for the construction line"] = "CONSTRUCTION",
ctx: Context = None,
) -> dict:
"""Create an infinite construction line on the CONSTRUCTION layer.
Use as scaffolding; call `construction_clear()` before finalize.
"""
result = await _backend(ctx).construction_xline(x, y, angle_deg, layer)
return _dc(result)
@mcp.tool(
annotations={"title": "Construction: Clear (delete scaffold)",
"destructiveHint": True},
tags={"premium", "construction"},
)
async def construction_clear(
layer: Annotated[str, "Layer to clear"] = "CONSTRUCTION",
ctx: Context = None,
) -> dict:
"""Delete every entity on the CONSTRUCTION layer. Idempotent.
Must be called before `drawing_finalize` to satisfy `construction_left` critique.
"""
return await _backend(ctx).construction_clear(layer)
@mcp.tool(
annotations={"title": "Drawing: Apply ISO Layer Set (bootstrap)",
"destructiveHint": False},
tags={"premium", "layers"},
)
async def drawing_apply_iso_layers(
standard: Annotated[str, Field(default="mech",
description="Layer set: mech (DIN/ISO mechanical), pid (P&ID), iso13567 (CAD layer naming).")] = "mech",
ctx: Context = None,
) -> dict:
"""Bootstrap a full ISO-conformant layer set with correct colors and lineweights.
Idempotent — existing layers are not modified.
"""
return await _backend(ctx).drawing_apply_iso_layers(standard)
@mcp.tool(
annotations={"title": "Dimension: Auto (chain / baseline / ordinate)",
"destructiveHint": False},
tags={"premium", "dimension"},
)
async def dimension_auto(
handles: Annotated[list[str], "List of entity handles to dimension"],
style: Annotated[str, Field(default="chain",
description="chain | baseline | ordinate")] = "chain",
offset: Annotated[float, Field(default=10.0, gt=0,
description="Dimension-line offset from the geometry (mm)")] = 10.0,
ctx: Context = None,
) -> list[dict]:
"""Generate ISO 129 dimensions across the listed entities in the chosen style.
V1 supports LINE entities only.
"""
result = await _backend(ctx).dimension_auto(handles, style, offset)
return [_dc(e) for e in result]
@mcp.tool(
annotations={"title": "Entity: Smart Select (semantic predicate)",
"readOnlyHint": True},
tags={"premium", "select"},
)
async def entity_select_smart(
predicate: Annotated[dict, Field(description=(
"Predicate dict (all keys optional, AND-ed): "
"type (e.g. 'LINE'), layer (name), near ([x,y,radius]), "
"length_range ([min,max], LINE/ARC only), color (ACI int)."
))],
ctx: Context = None,
) -> list[dict]:
"""Select entities by semantic predicate instead of memorising handles."""
result = await _backend(ctx).entity_select_smart(predicate)
return [_dc(e) for e in result]
# ---------------------------------------------------------------------------
# ── RESOURCES ───────────────────────────────────────────────────────────────
# ---------------------------------------------------------------------------