feat(environment): system_launch, preference whitelist and operator prompts on COM; declared refusals headlessly

user_pick_point / user_select answer {cancelled: true} on ESC (DISP_E_EXCEPTION)
and {timed_out: true} past COM_CALL_TIMEOUT; preferences_set refuses read-only,
unknown and out-of-range keys before any ActiveX call.

The read-only key is Files.PrinterStyleSheetPath, not the spec's
PrintStyleSheetPath: IAcadPreferencesFiles has no member of that name
(measured live on AutoCAD 2026 / R25.1), and a misspelt key would make
preferences_get() raise on every live call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-21 18:34:09 +03:00
co-authored by Claude Opus 5
parent c7dd72410a
commit b83a74e969
9 changed files with 1153 additions and 11 deletions
+146 -1
View File
@@ -6819,7 +6819,7 @@ async def pid_tag_parse(
# ---------------------------------------------------------------------------
# ── SECTION 20: Environment (13 tools) ──────────────────────────────────────
# ── SECTION 20: Environment (19 tools) ──────────────────────────────────────
# ---------------------------------------------------------------------------
@@ -7120,6 +7120,151 @@ async def ucs_restore(
return await _backend(ctx).ucs_restore(name)
@cad_tool(
summary="Attach to the running AutoCAD or start it, optionally opening a file (live only).",
cost="safe",
)
@mcp.tool(
annotations={
"title": "Launch / Attach AutoCAD",
"readOnlyHint": False,
"destructiveHint": False,
},
tags={"system"},
)
async def system_launch(
visible: Annotated[bool, "Show the application window"] = True,
open_path: Annotated[str | None, "A .dwg/.dxf to open after attaching"] = None,
ctx: Context = None,
) -> dict:
"""Connect to the application named by `CAD_PROGID` (attach if running,
launch otherwise) and report `launched`, `attached`, `version` and the
active `document`. Refusals: headless, `capability: "live_application"`
(there is no application to launch); an `open_path` that fails path
validation. Pack: settings · lean: no.
"""
if open_path is not None:
open_path = str(validate_path(open_path, allow_write=False))
await ctx.info(f"Launching or attaching to {config.settings.cad_progid}")
return await _backend(ctx).system_launch(visible, open_path)
@cad_tool(
summary="Read AutoCAD preferences from the whitelist (OPTIONS dialog values; live only).",
cost="read",
)
@mcp.tool(
annotations={"title": "Get Preferences", "readOnlyHint": True},
tags={"system"},
)
async def system_preferences_get(
keys: Annotated[
list[str] | None,
"Subset of OpenSave.SaveAsType, OpenSave.AutoSaveInterval, OpenSave.CreateBackup, "
"OpenSave.IncrementalSavePercent, Display.CursorSize, Drafting.AutoSnapMarkerSize, "
"Drafting.AutoSnapTooltip, Selection.PickBoxSize, Output.DefaultPlotStyleTable, "
"Output.DefaultOutputDevice, Files.SupportPath, Files.TemplateDwgPath, "
"Files.PrinterStyleSheetPath, Files.PrinterConfigPath; omit for all",
] = None,
ctx: Context = None,
) -> dict:
"""The whitelisted `Preferences.*` values, enum values as names
(`SaveAsType: "ac2018_dwg"`), with `read_only` naming the four `Files.*`
paths. Refusals: headless, `capability: "preferences"` (they live in the
running application, not in a file); a key outside the whitelist.
Pack: settings · lean: no.
"""
return await _backend(ctx).preferences_get(keys)
@cad_tool(
summary="Change one whitelisted AutoCAD preference; reports old and new (live only).",
cost="safe",
)
@mcp.tool(
annotations={"title": "Set Preference", "readOnlyHint": False, "destructiveHint": False},
tags={"system"},
)
async def system_preferences_set(
key: Annotated[str, "A writable key from system_preferences_get"],
value: Annotated[Any, "New value: bool, int within the key's range, string, or an enum name"],
ctx: Context = None,
) -> dict:
"""Write one preference and report `old`, `new` and `changed`.
Refusals, all before any write: a read-only key (`Files.*`), an unknown
key, a value of the wrong type, an int outside the authored range
(`AutoSaveInterval` 0–600 min, `CursorSize` 1–100, `PickBoxSize` 0–50, …),
an unknown `SaveAsType` name; headless, `capability: "preferences"`.
Pack: settings · lean: no.
"""
await ctx.info(f"Setting preference {key} = {value!r}")
return await _backend(ctx).preferences_set(key, value)
@cad_tool(
summary="Ask the operator to pick a point on screen (live only; blocks until they do).",
cost="read",
)
@mcp.tool(
annotations={"title": "Ask Operator: Pick Point", "readOnlyHint": True},
tags={"system"},
)
async def user_pick_point(
prompt: Annotated[str, "Shown on the command line, e.g. 'Pick the base point'"],
ctx: Context = None,
) -> dict:
"""`Utility.GetPoint`: returns the WCS `x`, `y` (`z`) the operator clicks.
ESC is an answer, not an error: `cancelled: true` with AutoCAD's reason.
Waiting longer than `COM_CALL_TIMEOUT` returns `timed_out: true` (the
prompt is abandoned on AutoCAD's side; press ESC there). Refusals: an
empty prompt; headless, `capability: "interactive_prompt"` (no operator).
Pack: settings · lean: no.
"""
await ctx.info(f"Asking the operator to pick a point: {prompt}")
return await _backend(ctx).user_pick_point(prompt)
@cad_tool(
summary="Ask the operator to select one entity or a set on screen (live only).",
cost="read",
)
@mcp.tool(
annotations={"title": "Ask Operator: Select", "readOnlyHint": True},
tags={"system"},
)
async def user_select(
prompt: Annotated[str, "Shown on the command line"],
mode: Annotated[str, "single (GetEntity) | multiple (SelectOnScreen)"] = "single",
ctx: Context = None,
) -> dict:
"""Returns `handles` the operator picked — one with `mode="single"` (plus
the `picked` point), any number with `mode="multiple"` (Enter with nothing
selected is `count: 0`, not a cancel). ESC → `cancelled: true`; a wait
longer than `COM_CALL_TIMEOUT` → `timed_out: true`. Refusals: a mode
outside the two, an empty prompt; headless, `capability:
"interactive_prompt"`. Pack: settings · lean: no.
"""
await ctx.info(f"Asking the operator to select ({mode}): {prompt}")
return await _backend(ctx).user_select(prompt, mode)
@cad_tool(summary="Print a message on AutoCAD's command line (live only).", cost="safe")
@mcp.tool(
annotations={"title": "Command-Line Message", "readOnlyHint": False, "destructiveHint": False},
tags={"system"},
)
async def system_prompt_message(
text: Annotated[str, "The message; one line is best"],
ctx: Context = None,
) -> dict:
"""`Utility.Prompt`: tell the operator something where they are looking.
Nothing in the drawing changes. Refusals: an empty message; headless,
`capability: "interactive_prompt"`. Pack: settings · lean: no."""
return await _backend(ctx).system_prompt_message(text)
# ---------------------------------------------------------------------------
# ── RESOURCES ───────────────────────────────────────────────────────────────
# ---------------------------------------------------------------------------