feat(server): settings pack — 22 environment tools behind TOOL_PACKS, lean grows to 55, three groups, golden set +10

Task 24 (merge sanity) on main after the five track E groups merged.

- TOOL_PACK_NAMES = ("core", "pid", "settings"); PACK_TOOL_NAMES["settings"]
  is exactly the 22 SECTION 20 tools. Styles and page setup stay in core.
- LEAN_TOOL_NAMES += page_setup_apply, batch_plot (53 -> 55).
- _GROUP_TAG_PRIORITY / _GROUP_TAG_LABELS: "style" moved up next to "pid",
  "pagesetup" -> page_setup and "environment" -> environment added; the 28
  SECTION 19/20 decorators gain their section tag, so _tool_groups() reports
  styles 10 / page_setup 6 / environment 22 (23 groups, 204 tools) and
  pack == group == section for the settings pack.
- SECTION headers, backends/base.py composition, contracts/__init__.py
  exports and the layer_states feature entry were already correct after the
  merge (read and confirmed, nothing edited).
- Pack text: config.py, .env.example, README config row; README <sub> line
  204 tools in 23 groups; release snapshot 3259 collected tests (measured).
- docs/tool-inventory.json regenerated; registry snapshot re-derived.
- tests: test_tool_packs.py replaced (11 tests), test_capability_contract.py
  +3 (composition, the agreed capability flags, layer_states honesty),
  golden set +10 tuning cases (81 = 66 tuning + 15 holdout); all ten route
  in the top 3 without alias edits.

Deviation from the plan text: TRACK_E_KEYS carries five keys, not six. The
plan's "mleaderstyle" (ezdxf True / com False) contradicts spec §8.1, which
retracted that key because the ActiveX route exists (§4.2); measured on the
tree: capability_keys() == {boundary_trace, chspace, interactive_prompt,
live_application, preferences, revcloud, wipeout}, and
ComBackend.mleaderstyle_create is a native implementation
(backends/com_backend.py:6752, is_capability_default -> False). The test now
also asserts the key is absent.

No new ActiveX call was written in this task, so no live probe was needed.

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-22 11:35:00 +03:00
co-authored by Claude Opus 5
parent 90c1194f7f
commit 70afd57831
9 changed files with 356 additions and 126 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ DANGEROUS_COMMANDS_ENABLED=false
# still boots, falling back to full with a warning. # still boots, falling back to full with a warning.
TOOL_PROFILE=full TOOL_PROFILE=full
# Tool packs: all (default) | comma list of core,pid — advertise only the # Tool packs: all (default) | comma list of core,pid,settings — advertise only the
# vertical packs this client uses. core is always on. # vertical packs this client uses. core is always on.
TOOL_PACKS=all TOOL_PACKS=all
+3 -3
View File
@@ -27,7 +27,7 @@ Live through COM on Windows, or headless through ezdxf anywhere — one typed co
</div> </div>
> **v1.5 release snapshot:** 204 tools · 8 resources · 5 prompt templates · 2423 collected tests. > **v1.5 release snapshot:** 204 tools · 8 resources · 5 prompt templates · 3259 collected tests.
> 204 is the **registered** count; a default install advertises 199 over `tools/list`, > 204 is the **registered** count; a default install advertises 199 over `tools/list`,
> because `ENABLE_3D` is unset. `system_about` is the runtime authority. > because `ENABLE_3D` is unset. `system_about` is the runtime authority.
@@ -115,7 +115,7 @@ Claude Desktop, Cursor, or any stdio MCP host. For HTTP: `autocad-mcp --transpor
| Quality loop | `drawing_preflight` → `drawing_plan` → `drawing_critique` → `drawing_refine` → `drawing_finalize` (0–100 score) | | Quality loop | `drawing_preflight` → `drawing_plan` → `drawing_critique` → `drawing_refine` → `drawing_finalize` (0–100 score) |
| Delivery | `drawing_deliver`: DXF/PDF/PNG + SHA-256 manifest + reopen-parity checks | | Delivery | `drawing_deliver`: DXF/PDF/PNG + SHA-256 manifest + reopen-parity checks |
<sub>172 tools in 20 groups; <code>TOOL_PACKS=core</code> hides the nine <code>pid_*</code> tools from a client that never draws a P&ID. Plus 8 resources that cost nothing in the tool budget (<code>autocad://drawing/info</code>, <code>layers</code>, <code>blocks</code>, <code>entities/stats</code>, <code>entities/{layer_name}</code>, <code>system/status</code>, <code>pid/symbols</code>, <code>standards/isa51</code>) and 5 prompt templates.</sub> <sub>204 tools in 23 groups; <code>TOOL_PACKS=core</code> hides the nine <code>pid_*</code> tools and the 22 environment tools (<code>TOOL_PACKS=core,settings</code> keeps the latter) from a client that needs neither. Plus 8 resources that cost nothing in the tool budget (<code>autocad://drawing/info</code>, <code>layers</code>, <code>blocks</code>, <code>entities/stats</code>, <code>entities/{layer_name}</code>, <code>system/status</code>, <code>pid/symbols</code>, <code>standards/isa51</code>) and 5 prompt templates.</sub>
**Two rules worth knowing.** Every coordinate in and out of a tool is WCS on both engines — the one exception is TEXT `rotation`, which stays in the entity frame because a mirrored TEXT is mirror-imaged and no scalar angle expresses that. And 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 states its own accuracy. **Two rules worth knowing.** Every coordinate in and out of a tool is WCS on both engines — the one exception is TEXT `rotation`, which stays in the entity frame because a mirrored TEXT is mirror-imaged and no scalar angle expresses that. And 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 states its own accuracy.
@@ -217,7 +217,7 @@ Nothing loads a `.env` file — export these, or set them in your MCP client's `
| `AUTOCAD_MCP_BACKEND` | `auto` | `auto`, `com`, or `ezdxf` | | `AUTOCAD_MCP_BACKEND` | `auto` | `auto`, `com`, or `ezdxf` |
| `CAD_PROGID` | `AutoCAD.Application` | COM ProgID the live backend attaches to | | `CAD_PROGID` | `AutoCAD.Application` | COM ProgID the live backend attaches to |
| `TOOL_PROFILE` | `full` | `lean` (50 curated tools) or `full` | | `TOOL_PROFILE` | `full` | `lean` (50 curated tools) or `full` |
| `TOOL_PACKS` | `all` | Vertical packs to advertise: `core,pid` (`core` always on) | | `TOOL_PACKS` | `all` | Vertical packs to advertise: `core,pid,settings` (`core` always on) |
| `DISCOVERY_MODE` | `off` | `search` replaces the catalog with `search_tools` + `call_tool` | | `DISCOVERY_MODE` | `off` | `search` replaces the catalog with `search_tools` + `call_tool` |
| `ENABLE_3D` | `false` | Expose the opt-in `solid_*` tools (COM) | | `ENABLE_3D` | `false` | Expose the opt-in `solid_*` tools (COM) |
| `LOG_LEVEL` | `INFO` | Python logging level | | `LOG_LEVEL` | `INFO` | Python logging level |
+1 -1
View File
@@ -103,7 +103,7 @@ class Settings:
self.tool_profile: str = os.environ.get("TOOL_PROFILE", "full").lower().strip() self.tool_profile: str = os.environ.get("TOOL_PROFILE", "full").lower().strip()
# Tool packs: "all" (default) advertises every vertical pack; a comma # Tool packs: "all" (default) advertises every vertical pack; a comma
# list ("core,pid") advertises only those. core cannot be disabled. # list ("core,pid,settings") advertises only those. core cannot be disabled.
self.tool_packs: str = os.environ.get("TOOL_PACKS", "all").lower().strip() self.tool_packs: str = os.environ.get("TOOL_PACKS", "all").lower().strip()
# Tool discovery: "off" (default) advertises the full catalog; "search" # Tool discovery: "off" (default) advertises the full catalog; "search"
+68 -38
View File
@@ -6,7 +6,7 @@
"tools": 204, "tools": 204,
"resources": 8, "resources": 8,
"prompts": 5, "prompts": 5,
"groups": 21 "groups": 23
}, },
"groups": { "groups": {
"analysis": 12, "analysis": 12,
@@ -14,22 +14,24 @@
"blocks": 9, "blocks": 9,
"corner_ops": 4, "corner_ops": 4,
"dimensions": 5, "dimensions": 5,
"drawing": 16, "drawing": 11,
"engineering": 10, "engineering": 10,
"entity_creation": 18, "entity_creation": 18,
"entity_modification": 16, "entity_modification": 16,
"entity_query": 9, "entity_query": 9,
"layers": 18, "environment": 22,
"layouts": 16, "layers": 14,
"layouts": 12,
"page_setup": 6,
"pid": 9, "pid": 9,
"premium": 12, "premium": 12,
"solids": 5, "solids": 5,
"styles": 10, "styles": 10,
"system": 14, "system": 7,
"templates": 4, "templates": 2,
"transactions": 3, "transactions": 3,
"validation": 1, "validation": 1,
"view": 10 "view": 4
}, },
"tools": [ "tools": [
{ {
@@ -178,12 +180,13 @@
}, },
{ {
"name": "batch_plot", "name": "batch_plot",
"group": "layouts", "group": "page_setup",
"cost": "mutate", "cost": "mutate",
"summary": "Plot every sheet (or the named ones) to PDF; each sheet size is read back from its PDF.", "summary": "Plot every sheet (or the named ones) to PDF; each sheet size is read back from its PDF.",
"tags": [ "tags": [
"export", "export",
"layout", "layout",
"pagesetup",
"plot" "plot"
], ],
"annotations": { "annotations": {
@@ -524,11 +527,12 @@
}, },
{ {
"name": "document_activate", "name": "document_activate",
"group": "drawing", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Switch which open document every later tool call targets.", "summary": "Switch which open document every later tool call targets.",
"tags": [ "tags": [
"drawing" "drawing",
"environment"
], ],
"annotations": { "annotations": {
"title": "Activate Document", "title": "Activate Document",
@@ -538,11 +542,12 @@
}, },
{ {
"name": "document_close", "name": "document_close",
"group": "drawing", "group": "environment",
"cost": "destructive", "cost": "destructive",
"summary": "Close one open document by name; never drops unsaved work unless told to.", "summary": "Close one open document by name; never drops unsaved work unless told to.",
"tags": [ "tags": [
"drawing" "drawing",
"environment"
], ],
"annotations": { "annotations": {
"title": "Close Document", "title": "Close Document",
@@ -551,11 +556,12 @@
}, },
{ {
"name": "document_list", "name": "document_list",
"group": "drawing", "group": "environment",
"cost": "read", "cost": "read",
"summary": "List the open documents: which is active, which have unsaved changes.", "summary": "List the open documents: which is active, which have unsaved changes.",
"tags": [ "tags": [
"drawing" "drawing",
"environment"
], ],
"annotations": { "annotations": {
"title": "List Documents", "title": "List Documents",
@@ -759,11 +765,12 @@
}, },
{ {
"name": "drawing_properties_get", "name": "drawing_properties_get",
"group": "drawing", "group": "environment",
"cost": "read", "cost": "read",
"summary": "Read the drawing's title, subject, author, keywords, comments and custom properties.", "summary": "Read the drawing's title, subject, author, keywords, comments and custom properties.",
"tags": [ "tags": [
"drawing", "drawing",
"environment",
"settings" "settings"
], ],
"annotations": { "annotations": {
@@ -773,11 +780,12 @@
}, },
{ {
"name": "drawing_properties_set", "name": "drawing_properties_set",
"group": "drawing", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Set the drawing's summary fields and add, change or delete custom properties.", "summary": "Set the drawing's summary fields and add, change or delete custom properties.",
"tags": [ "tags": [
"drawing", "drawing",
"environment",
"settings" "settings"
], ],
"annotations": { "annotations": {
@@ -867,11 +875,12 @@
}, },
{ {
"name": "drawing_template_list", "name": "drawing_template_list",
"group": "templates", "group": "page_setup",
"cost": "read", "cost": "read",
"summary": "List the five bundled drawing templates: standard, sheet, layers, styles, page setup.", "summary": "List the five bundled drawing templates: standard, sheet, layers, styles, page setup.",
"tags": [ "tags": [
"drawing", "drawing",
"pagesetup",
"query", "query",
"template" "template"
], ],
@@ -882,11 +891,12 @@
}, },
{ {
"name": "drawing_template_save", "name": "drawing_template_save",
"group": "templates", "group": "page_setup",
"cost": "mutate", "cost": "mutate",
"summary": "Save the current drawing as a template: .dwt on live AutoCAD, .dxf headlessly.", "summary": "Save the current drawing as a template: .dwt on live AutoCAD, .dxf headlessly.",
"tags": [ "tags": [
"drawing", "drawing",
"pagesetup",
"template" "template"
], ],
"annotations": { "annotations": {
@@ -1756,10 +1766,11 @@
}, },
{ {
"name": "layer_state_delete", "name": "layer_state_delete",
"group": "layers", "group": "environment",
"cost": "destructive", "cost": "destructive",
"summary": "Delete one saved layer state from the drawing.", "summary": "Delete one saved layer state from the drawing.",
"tags": [ "tags": [
"environment",
"layer" "layer"
], ],
"annotations": { "annotations": {
@@ -1769,10 +1780,11 @@
}, },
{ {
"name": "layer_state_list", "name": "layer_state_list",
"group": "layers", "group": "environment",
"cost": "read", "cost": "read",
"summary": "List the layer states saved in this drawing.", "summary": "List the layer states saved in this drawing.",
"tags": [ "tags": [
"environment",
"layer" "layer"
], ],
"annotations": { "annotations": {
@@ -1782,10 +1794,11 @@
}, },
{ {
"name": "layer_state_restore", "name": "layer_state_restore",
"group": "layers", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Restore a saved layer state, all properties or a chosen subset.", "summary": "Restore a saved layer state, all properties or a chosen subset.",
"tags": [ "tags": [
"environment",
"layer" "layer"
], ],
"annotations": { "annotations": {
@@ -1796,10 +1809,11 @@
}, },
{ {
"name": "layer_state_save", "name": "layer_state_save",
"group": "layers", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Save the layer table (on/frozen/locked/colour/linetype/lineweight/plot + current) under a name, in the file.", "summary": "Save the layer table (on/frozen/locked/colour/linetype/lineweight/plot + current) under a name, in the file.",
"tags": [ "tags": [
"environment",
"layer" "layer"
], ],
"annotations": { "annotations": {
@@ -1984,11 +1998,12 @@
}, },
{ {
"name": "page_setup_apply", "name": "page_setup_apply",
"group": "layouts", "group": "page_setup",
"cost": "safe", "cost": "safe",
"summary": "Set a sheet's paper, orientation, ctb, plot scale and device (PAGESETUP).", "summary": "Set a sheet's paper, orientation, ctb, plot scale and device (PAGESETUP).",
"tags": [ "tags": [
"layout", "layout",
"pagesetup",
"plot" "plot"
], ],
"annotations": { "annotations": {
@@ -1998,11 +2013,12 @@
}, },
{ {
"name": "page_setup_list", "name": "page_setup_list",
"group": "layouts", "group": "page_setup",
"cost": "read", "cost": "read",
"summary": "Read each sheet's page setup: paper, orientation, ctb, scale, device.", "summary": "Read each sheet's page setup: paper, orientation, ctb, scale, device.",
"tags": [ "tags": [
"layout", "layout",
"pagesetup",
"plot", "plot",
"query" "query"
], ],
@@ -2140,11 +2156,12 @@
}, },
{ {
"name": "plot_style_list", "name": "plot_style_list",
"group": "layouts", "group": "page_setup",
"cost": "read", "cost": "read",
"summary": "List the plot style tables (ctb): the AutoCAD catalogue, plus what is installed.", "summary": "List the plot style tables (ctb): the AutoCAD catalogue, plus what is installed.",
"tags": [ "tags": [
"layout", "layout",
"pagesetup",
"plot", "plot",
"query" "query"
], ],
@@ -2358,10 +2375,11 @@
}, },
{ {
"name": "system_launch", "name": "system_launch",
"group": "system", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Attach to the running AutoCAD or start it, optionally opening a file (live only).", "summary": "Attach to the running AutoCAD or start it, optionally opening a file (live only).",
"tags": [ "tags": [
"environment",
"system" "system"
], ],
"annotations": { "annotations": {
@@ -2372,10 +2390,11 @@
}, },
{ {
"name": "system_preferences_get", "name": "system_preferences_get",
"group": "system", "group": "environment",
"cost": "read", "cost": "read",
"summary": "Read AutoCAD preferences from the whitelist (OPTIONS dialog values; live only).", "summary": "Read AutoCAD preferences from the whitelist (OPTIONS dialog values; live only).",
"tags": [ "tags": [
"environment",
"system" "system"
], ],
"annotations": { "annotations": {
@@ -2385,10 +2404,11 @@
}, },
{ {
"name": "system_preferences_set", "name": "system_preferences_set",
"group": "system", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Change one whitelisted AutoCAD preference; reports old and new (live only).", "summary": "Change one whitelisted AutoCAD preference; reports old and new (live only).",
"tags": [ "tags": [
"environment",
"system" "system"
], ],
"annotations": { "annotations": {
@@ -2399,10 +2419,11 @@
}, },
{ {
"name": "system_prompt_message", "name": "system_prompt_message",
"group": "system", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Print a message on AutoCAD's command line (live only).", "summary": "Print a message on AutoCAD's command line (live only).",
"tags": [ "tags": [
"environment",
"system" "system"
], ],
"annotations": { "annotations": {
@@ -2465,10 +2486,11 @@
}, },
{ {
"name": "system_variable_describe", "name": "system_variable_describe",
"group": "system", "group": "environment",
"cost": "read", "cost": "read",
"summary": "Explain a system variable: type, range, default, where it is saved, which engine honours it.", "summary": "Explain a system variable: type, range, default, where it is saved, which engine honours it.",
"tags": [ "tags": [
"environment",
"settings", "settings",
"system" "system"
], ],
@@ -2633,10 +2655,11 @@
}, },
{ {
"name": "ucs_list", "name": "ucs_list",
"group": "view", "group": "environment",
"cost": "read", "cost": "read",
"summary": "List the user coordinate systems, with the implicit world one.", "summary": "List the user coordinate systems, with the implicit world one.",
"tags": [ "tags": [
"environment",
"view" "view"
], ],
"annotations": { "annotations": {
@@ -2646,10 +2669,11 @@
}, },
{ {
"name": "ucs_restore", "name": "ucs_restore",
"group": "view", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Make a saved UCS current, or `world` to reset to WCS.", "summary": "Make a saved UCS current, or `world` to reset to WCS.",
"tags": [ "tags": [
"environment",
"view" "view"
], ],
"annotations": { "annotations": {
@@ -2660,10 +2684,11 @@
}, },
{ {
"name": "ucs_set", "name": "ucs_set",
"group": "view", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Define (or replace) a named UCS from an origin and two perpendicular axes, and make it current.", "summary": "Define (or replace) a named UCS from an origin and two perpendicular axes, and make it current.",
"tags": [ "tags": [
"environment",
"view" "view"
], ],
"annotations": { "annotations": {
@@ -2674,10 +2699,11 @@
}, },
{ {
"name": "user_pick_point", "name": "user_pick_point",
"group": "system", "group": "environment",
"cost": "read", "cost": "read",
"summary": "Ask the operator to pick a point on screen (live only; blocks until they do).", "summary": "Ask the operator to pick a point on screen (live only; blocks until they do).",
"tags": [ "tags": [
"environment",
"system" "system"
], ],
"annotations": { "annotations": {
@@ -2687,10 +2713,11 @@
}, },
{ {
"name": "user_select", "name": "user_select",
"group": "system", "group": "environment",
"cost": "read", "cost": "read",
"summary": "Ask the operator to select one entity or a set on screen (live only).", "summary": "Ask the operator to select one entity or a set on screen (live only).",
"tags": [ "tags": [
"environment",
"system" "system"
], ],
"annotations": { "annotations": {
@@ -2714,10 +2741,11 @@
}, },
{ {
"name": "view_named_list", "name": "view_named_list",
"group": "view", "group": "environment",
"cost": "read", "cost": "read",
"summary": "List the named views saved in this drawing.", "summary": "List the named views saved in this drawing.",
"tags": [ "tags": [
"environment",
"view" "view"
], ],
"annotations": { "annotations": {
@@ -2727,10 +2755,11 @@
}, },
{ {
"name": "view_named_restore", "name": "view_named_restore",
"group": "view", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Restore a named view.", "summary": "Restore a named view.",
"tags": [ "tags": [
"environment",
"view" "view"
], ],
"annotations": { "annotations": {
@@ -2741,10 +2770,11 @@
}, },
{ {
"name": "view_named_save", "name": "view_named_save",
"group": "view", "group": "environment",
"cost": "safe", "cost": "safe",
"summary": "Save a named view: a centre and height (and width) to come back to.", "summary": "Save a named view: a centre and height (and width) to come back to.",
"tags": [ "tags": [
"environment",
"view" "view"
], ],
"annotations": { "annotations": {
+70 -32
View File
@@ -690,6 +690,9 @@ async def _registered_tool_count() -> int | None:
# entity/drawing tags. Tools whose tags match none fall into "other". # entity/drawing tags. Tools whose tags match none fall into "other".
_GROUP_TAG_PRIORITY = ( _GROUP_TAG_PRIORITY = (
"pid", "pid",
"style",
"pagesetup",
"environment",
"engineering", "engineering",
"premium", "premium",
"corner", "corner",
@@ -703,7 +706,6 @@ _GROUP_TAG_PRIORITY = (
"validation", "validation",
"analysis", "analysis",
"block", "block",
"style",
"layer", "layer",
"linetype", "linetype",
"create", "create",
@@ -716,6 +718,9 @@ _GROUP_TAG_PRIORITY = (
# Map the winning tag to a human-readable group label for the breakdown. # Map the winning tag to a human-readable group label for the breakdown.
_GROUP_TAG_LABELS = { _GROUP_TAG_LABELS = {
"pid": "pid", "pid": "pid",
"style": "styles",
"pagesetup": "page_setup",
"environment": "environment",
"engineering": "engineering", "engineering": "engineering",
"premium": "premium", "premium": "premium",
"corner": "corner_ops", "corner": "corner_ops",
@@ -729,7 +734,6 @@ _GROUP_TAG_LABELS = {
"analysis": "analysis", "analysis": "analysis",
"validation": "validation", "validation": "validation",
"block": "blocks", "block": "blocks",
"style": "styles",
"layer": "layers", "layer": "layers",
"linetype": "layers", "linetype": "layers",
"create": "entity_creation", "create": "entity_creation",
@@ -938,6 +942,11 @@ LEAN_TOOL_NAMES = frozenset(
"dimstyle_set_current", "dimstyle_set_current",
"textstyle_set_current", "textstyle_set_current",
"drawing_apply_standard", "drawing_apply_standard",
# Page setup (track E) — the two a lean client needs to set the
# sheet and plot it. Core-pack tools, so TOOL_PACKS=core leaves
# them on the lean surface; with S's three above, lean is 55.
"page_setup_apply",
"batch_plot",
} }
) )
@@ -948,8 +957,10 @@ SOLID_TOOL_NAMES = frozenset(
# Tool packs: vertical domains a client can opt out of advertising. `core` is # Tool packs: vertical domains a client can opt out of advertising. `core` is
# everything not claimed by another pack and is always on. ~300 tools would # everything not claimed by another pack and is always on. ~300 tools would
# push the full catalog to ~80k idle tokens; a client that only drafts # push the full catalog to ~80k idle tokens; a client that only drafts
# mechanically should not pay for the P&ID surface. # mechanically should not pay for the P&ID surface, and a client that never
TOOL_PACK_NAMES = ("core", "pid") # talks to a live seat should not pay for the environment surface. Styles and
# page setup (SECTION 18/19) are drafting essentials and stay in core.
TOOL_PACK_NAMES = ("core", "pid", "settings")
PACK_TOOL_NAMES: dict[str, frozenset[str]] = { PACK_TOOL_NAMES: dict[str, frozenset[str]] = {
"pid": frozenset( "pid": frozenset(
{ {
@@ -964,6 +975,33 @@ PACK_TOOL_NAMES: dict[str, frozenset[str]] = {
"pid_from_spec", "pid_from_spec",
} }
), ),
# SECTION 20 — the 22 environment tools (spec §7, §8.2).
"settings": frozenset(
{
"document_list",
"document_activate",
"document_close",
"layer_state_save",
"layer_state_restore",
"layer_state_list",
"layer_state_delete",
"view_named_save",
"view_named_restore",
"view_named_list",
"ucs_list",
"ucs_set",
"ucs_restore",
"system_launch",
"system_preferences_get",
"system_preferences_set",
"drawing_properties_get",
"drawing_properties_set",
"system_variable_describe",
"user_pick_point",
"user_select",
"system_prompt_message",
}
),
} }
_active_tool_profile: dict | None = None _active_tool_profile: dict | None = None
@@ -7283,7 +7321,7 @@ async def drawing_apply_standard(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "List Page Setups", "readOnlyHint": True}, annotations={"title": "List Page Setups", "readOnlyHint": True},
tags={"layout", "plot", "query"}, tags={"pagesetup", "layout", "plot", "query"},
) )
async def page_setup_list( async def page_setup_list(
layout: Annotated[ layout: Annotated[
@@ -7314,7 +7352,7 @@ async def page_setup_list(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Apply Page Setup", "destructiveHint": False}, annotations={"title": "Apply Page Setup", "destructiveHint": False},
tags={"layout", "plot"}, tags={"pagesetup", "layout", "plot"},
) )
async def page_setup_apply( async def page_setup_apply(
layout: Annotated[str, "Paper-space layout to set up (never 'Model')."], layout: Annotated[str, "Paper-space layout to set up (never 'Model')."],
@@ -7369,7 +7407,7 @@ async def page_setup_apply(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "List Plot Styles", "readOnlyHint": True}, annotations={"title": "List Plot Styles", "readOnlyHint": True},
tags={"layout", "plot", "query"}, tags={"pagesetup", "layout", "plot", "query"},
) )
async def plot_style_list(ctx: Context = None) -> dict: async def plot_style_list(ctx: Context = None) -> dict:
"""The ctb files AutoCAD ships (`monochrome.ctb`, `acad.ctb`, `Grayscale.ctb`, """The ctb files AutoCAD ships (`monochrome.ctb`, `acad.ctb`, `Grayscale.ctb`,
@@ -7390,7 +7428,7 @@ async def plot_style_list(ctx: Context = None) -> dict:
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Batch Plot", "destructiveHint": False}, annotations={"title": "Batch Plot", "destructiveHint": False},
tags={"layout", "plot", "export"}, tags={"pagesetup", "layout", "plot", "export"},
) )
async def batch_plot( async def batch_plot(
output_dir: Annotated[str, "Folder for the PDFs (created if missing)."], output_dir: Annotated[str, "Folder for the PDFs (created if missing)."],
@@ -7437,7 +7475,7 @@ async def batch_plot(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "List Drawing Templates", "readOnlyHint": True}, annotations={"title": "List Drawing Templates", "readOnlyHint": True},
tags={"template", "drawing", "query"}, tags={"pagesetup", "template", "drawing", "query"},
) )
async def drawing_template_list(ctx: Context = None) -> dict: async def drawing_template_list(ctx: Context = None) -> dict:
"""The templates `drawing_new(template=<name>)` can start from. """The templates `drawing_new(template=<name>)` can start from.
@@ -7468,7 +7506,7 @@ async def drawing_template_list(ctx: Context = None) -> dict:
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Save As Template", "destructiveHint": False}, annotations={"title": "Save As Template", "destructiveHint": False},
tags={"template", "drawing"}, tags={"pagesetup", "template", "drawing"},
) )
async def drawing_template_save( async def drawing_template_save(
path: Annotated[str, "Destination .dwt (live AutoCAD) or .dxf (either engine)."], path: Annotated[str, "Destination .dwt (live AutoCAD) or .dxf (either engine)."],
@@ -7511,7 +7549,7 @@ async def drawing_template_save(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "List Documents", "readOnlyHint": True}, annotations={"title": "List Documents", "readOnlyHint": True},
tags={"drawing"}, tags={"environment", "drawing"},
) )
async def document_list(ctx: Context = None) -> dict: async def document_list(ctx: Context = None) -> dict:
"""Every open document with `name`, `path`, `active`, `saved` and """Every open document with `name`, `path`, `active`, `saved` and
@@ -7534,7 +7572,7 @@ async def document_list(ctx: Context = None) -> dict:
@cad_tool(summary="Switch which open document every later tool call targets.", cost="safe") @cad_tool(summary="Switch which open document every later tool call targets.", cost="safe")
@mcp.tool( @mcp.tool(
annotations={"title": "Activate Document", "readOnlyHint": False, "destructiveHint": False}, annotations={"title": "Activate Document", "readOnlyHint": False, "destructiveHint": False},
tags={"drawing"}, tags={"environment", "drawing"},
) )
async def document_activate( async def document_activate(
name_or_path: Annotated[ name_or_path: Annotated[
@@ -7560,7 +7598,7 @@ async def document_activate(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Close Document", "destructiveHint": True}, annotations={"title": "Close Document", "destructiveHint": True},
tags={"drawing"}, tags={"environment", "drawing"},
) )
async def document_close( async def document_close(
name_or_path: Annotated[ name_or_path: Annotated[
@@ -7592,7 +7630,7 @@ async def document_close(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Save Layer State", "readOnlyHint": False, "destructiveHint": False}, annotations={"title": "Save Layer State", "readOnlyHint": False, "destructiveHint": False},
tags={"layer"}, tags={"environment", "layer"},
) )
async def layer_state_save( async def layer_state_save(
name: Annotated[str, "State name, e.g. PLOT-SET or DESIGN"], name: Annotated[str, "State name, e.g. PLOT-SET or DESIGN"],
@@ -7616,7 +7654,7 @@ async def layer_state_save(
@cad_tool(summary="Restore a saved layer state, all properties or a chosen subset.", cost="safe") @cad_tool(summary="Restore a saved layer state, all properties or a chosen subset.", cost="safe")
@mcp.tool( @mcp.tool(
annotations={"title": "Restore Layer State", "readOnlyHint": False, "destructiveHint": False}, annotations={"title": "Restore Layer State", "readOnlyHint": False, "destructiveHint": False},
tags={"layer"}, tags={"environment", "layer"},
) )
async def layer_state_restore( async def layer_state_restore(
name: Annotated[str, "A name from layer_state_list"], name: Annotated[str, "A name from layer_state_list"],
@@ -7646,7 +7684,7 @@ async def layer_state_restore(
@cad_tool(summary="List the layer states saved in this drawing.", cost="read") @cad_tool(summary="List the layer states saved in this drawing.", cost="read")
@mcp.tool( @mcp.tool(
annotations={"title": "List Layer States", "readOnlyHint": True}, annotations={"title": "List Layer States", "readOnlyHint": True},
tags={"layer"}, tags={"environment", "layer"},
) )
async def layer_state_list(ctx: Context = None) -> dict: async def layer_state_list(ctx: Context = None) -> dict:
"""Every state under `ACADMCP_LAYERSTATES` with its description and layer """Every state under `ACADMCP_LAYERSTATES` with its description and layer
@@ -7659,7 +7697,7 @@ async def layer_state_list(ctx: Context = None) -> dict:
@cad_tool(summary="Delete one saved layer state from the drawing.", cost="destructive") @cad_tool(summary="Delete one saved layer state from the drawing.", cost="destructive")
@mcp.tool( @mcp.tool(
annotations={"title": "Delete Layer State", "destructiveHint": True}, annotations={"title": "Delete Layer State", "destructiveHint": True},
tags={"layer"}, tags={"environment", "layer"},
) )
async def layer_state_delete( async def layer_state_delete(
name: Annotated[str, "A name from layer_state_list"], name: Annotated[str, "A name from layer_state_list"],
@@ -7676,7 +7714,7 @@ async def layer_state_delete(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Save Named View", "readOnlyHint": False, "destructiveHint": False}, annotations={"title": "Save Named View", "readOnlyHint": False, "destructiveHint": False},
tags={"view"}, tags={"environment", "view"},
) )
async def view_named_save( async def view_named_save(
name: Annotated[str, "View name, e.g. DETAIL-A"], name: Annotated[str, "View name, e.g. DETAIL-A"],
@@ -7701,7 +7739,7 @@ async def view_named_save(
@cad_tool(summary="Restore a named view.", cost="safe") @cad_tool(summary="Restore a named view.", cost="safe")
@mcp.tool( @mcp.tool(
annotations={"title": "Restore Named View", "readOnlyHint": False, "destructiveHint": False}, annotations={"title": "Restore Named View", "readOnlyHint": False, "destructiveHint": False},
tags={"view"}, tags={"environment", "view"},
) )
async def view_named_restore( async def view_named_restore(
name: Annotated[str, "A name from view_named_list"], name: Annotated[str, "A name from view_named_list"],
@@ -7725,7 +7763,7 @@ async def view_named_restore(
@cad_tool(summary="List the named views saved in this drawing.", cost="read") @cad_tool(summary="List the named views saved in this drawing.", cost="read")
@mcp.tool( @mcp.tool(
annotations={"title": "List Named Views", "readOnlyHint": True}, annotations={"title": "List Named Views", "readOnlyHint": True},
tags={"view"}, tags={"environment", "view"},
) )
async def view_named_list(ctx: Context = None) -> dict: async def view_named_list(ctx: Context = None) -> dict:
"""Every VIEW table entry with centre, height and width. No refusals. """Every VIEW table entry with centre, height and width. No refusals.
@@ -7737,7 +7775,7 @@ async def view_named_list(ctx: Context = None) -> dict:
@cad_tool(summary="List the user coordinate systems, with the implicit world one.", cost="read") @cad_tool(summary="List the user coordinate systems, with the implicit world one.", cost="read")
@mcp.tool( @mcp.tool(
annotations={"title": "List UCS", "readOnlyHint": True}, annotations={"title": "List UCS", "readOnlyHint": True},
tags={"view"}, tags={"environment", "view"},
) )
async def ucs_list(ctx: Context = None) -> dict: async def ucs_list(ctx: Context = None) -> dict:
"""The `world` row first, then every UCS table entry with origin and unit """The `world` row first, then every UCS table entry with origin and unit
@@ -7763,7 +7801,7 @@ async def ucs_list(ctx: Context = None) -> dict:
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Set UCS", "readOnlyHint": False, "destructiveHint": False}, annotations={"title": "Set UCS", "readOnlyHint": False, "destructiveHint": False},
tags={"view"}, tags={"environment", "view"},
) )
async def ucs_set( async def ucs_set(
name: Annotated[str, "UCS name"], name: Annotated[str, "UCS name"],
@@ -7788,7 +7826,7 @@ async def ucs_set(
@cad_tool(summary="Make a saved UCS current, or `world` to reset to WCS.", cost="safe") @cad_tool(summary="Make a saved UCS current, or `world` to reset to WCS.", cost="safe")
@mcp.tool( @mcp.tool(
annotations={"title": "Restore UCS", "readOnlyHint": False, "destructiveHint": False}, annotations={"title": "Restore UCS", "readOnlyHint": False, "destructiveHint": False},
tags={"view"}, tags={"environment", "view"},
) )
async def ucs_restore( async def ucs_restore(
name: Annotated[str, "A name from ucs_list, or `world`"], name: Annotated[str, "A name from ucs_list, or `world`"],
@@ -7812,7 +7850,7 @@ async def ucs_restore(
"readOnlyHint": False, "readOnlyHint": False,
"destructiveHint": False, "destructiveHint": False,
}, },
tags={"system"}, tags={"environment", "system"},
) )
async def system_launch( async def system_launch(
visible: Annotated[bool, "Show the application window"] = True, visible: Annotated[bool, "Show the application window"] = True,
@@ -7837,7 +7875,7 @@ async def system_launch(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Get Preferences", "readOnlyHint": True}, annotations={"title": "Get Preferences", "readOnlyHint": True},
tags={"system"}, tags={"environment", "system"},
) )
async def system_preferences_get( async def system_preferences_get(
keys: Annotated[ keys: Annotated[
@@ -7865,7 +7903,7 @@ async def system_preferences_get(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Set Preference", "readOnlyHint": False, "destructiveHint": False}, annotations={"title": "Set Preference", "readOnlyHint": False, "destructiveHint": False},
tags={"system"}, tags={"environment", "system"},
) )
async def system_preferences_set( async def system_preferences_set(
key: Annotated[str, "A writable key from system_preferences_get"], key: Annotated[str, "A writable key from system_preferences_get"],
@@ -7890,7 +7928,7 @@ async def system_preferences_set(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Ask Operator: Pick Point", "readOnlyHint": True}, annotations={"title": "Ask Operator: Pick Point", "readOnlyHint": True},
tags={"system"}, tags={"environment", "system"},
) )
async def user_pick_point( async def user_pick_point(
prompt: Annotated[str, "Shown on the command line, e.g. 'Pick the base point'"], prompt: Annotated[str, "Shown on the command line, e.g. 'Pick the base point'"],
@@ -7914,7 +7952,7 @@ async def user_pick_point(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Ask Operator: Select", "readOnlyHint": True}, annotations={"title": "Ask Operator: Select", "readOnlyHint": True},
tags={"system"}, tags={"environment", "system"},
) )
async def user_select( async def user_select(
prompt: Annotated[str, "Shown on the command line"], prompt: Annotated[str, "Shown on the command line"],
@@ -7937,7 +7975,7 @@ async def user_select(
@cad_tool(summary="Print a message on AutoCAD's command line (live only).", cost="safe") @cad_tool(summary="Print a message on AutoCAD's command line (live only).", cost="safe")
@mcp.tool( @mcp.tool(
annotations={"title": "Command-Line Message", "readOnlyHint": False, "destructiveHint": False}, annotations={"title": "Command-Line Message", "readOnlyHint": False, "destructiveHint": False},
tags={"system"}, tags={"environment", "system"},
) )
async def system_prompt_message( async def system_prompt_message(
text: Annotated[str, "The message; one line is best"], text: Annotated[str, "The message; one line is best"],
@@ -7955,7 +7993,7 @@ async def system_prompt_message(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Describe System Variable", "readOnlyHint": True}, annotations={"title": "Describe System Variable", "readOnlyHint": True},
tags={"system", "settings"}, tags={"environment", "system", "settings"},
) )
async def system_variable_describe( async def system_variable_describe(
name: Annotated[ name: Annotated[
@@ -8006,7 +8044,7 @@ async def system_variable_describe(
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Drawing Properties (read)", "readOnlyHint": True}, annotations={"title": "Drawing Properties (read)", "readOnlyHint": True},
tags={"drawing", "settings"}, tags={"environment", "drawing", "settings"},
) )
async def drawing_properties_get(ctx: Context = None) -> dict: async def drawing_properties_get(ctx: Context = None) -> dict:
"""The DWGPROPS dialog as data: `summary` (title, subject, author, keywords, """The DWGPROPS dialog as data: `summary` (title, subject, author, keywords,
@@ -8027,7 +8065,7 @@ async def drawing_properties_get(ctx: Context = None) -> dict:
) )
@mcp.tool( @mcp.tool(
annotations={"title": "Drawing Properties (write)", "readOnlyHint": False}, annotations={"title": "Drawing Properties (write)", "readOnlyHint": False},
tags={"drawing", "settings"}, tags={"environment", "drawing", "settings"},
) )
async def drawing_properties_set( async def drawing_properties_set(
title: Annotated[str | None, "SummaryInfo Title"] = None, title: Annotated[str | None, "SummaryInfo Title"] = None,
+20 -1
View File
@@ -1,6 +1,6 @@
"""Golden query set for tool discovery: English in, one right tool out. """Golden query set for tool discovery: English in, one right tool out.
Seventy-one cases (56 tuning + 15 holdout) split into a **tuning** set and a Eighty-one cases (66 tuning + 15 holdout) split into a **tuning** set and a
**holdout** set. The split is **holdout** set. The split is
the point of the file. Ranking work is measured against the point of the file. Ranking work is measured against
:data:`TUNING_CASES` only; :data:`HOLDOUT_CASES` were written at the same time, :data:`TUNING_CASES` only; :data:`HOLDOUT_CASES` were written at the same time,
@@ -155,6 +155,25 @@ TUNING_CASES: tuple[GoldenCase, ...] = (
), ),
GoldenCase("create a text style that uses the isocp font", "textstyle_create", "paraphrase"), GoldenCase("create a text style that uses the isocp font", "textstyle_create", "paraphrase"),
GoldenCase("which dimension styles are defined", "dimstyle_list", "counting", risk="read"), GoldenCase("which dimension styles are defined", "dimstyle_list", "counting", risk="read"),
# v1.6 track E — styles, page setup, templates, environment. Written
# before the alias records were measured against them; a miss here is
# fixed by adding vocabulary to the tool's record in discovery/aliases.py,
# never by moving the expectation.
GoldenCase("PAGESETUP", "page_setup_apply", "command"),
GoldenCase("PUBLISH", "batch_plot", "command"),
GoldenCase("MLEADERSTYLE", "mleaderstyle_create", "command"),
GoldenCase("STYLE", "textstyle_create", "command"),
GoldenCase("iso 25 dimension style", "dimstyle_create", "synonym"),
GoldenCase("layer snapshot", "layer_state_save", "synonym"),
GoldenCase("save this drawing as a template", "drawing_template_save", "paraphrase"),
GoldenCase("switch to the other open drawing", "document_activate", "paraphrase"),
GoldenCase("ask the operator to click a point on screen", "user_pick_point", "paraphrase"),
GoldenCase(
"what does the LTSCALE variable mean",
"system_variable_describe",
"counting",
risk="read",
),
) )
+50
View File
@@ -32,6 +32,9 @@ import pytest
from backends.base import AutoCADBackend, UnsupportedCapabilityError from backends.base import AutoCADBackend, UnsupportedCapabilityError
from backends.capability import capability_keys, is_capability_default from backends.capability import capability_keys, is_capability_default
from backends.com_backend import ComBackend from backends.com_backend import ComBackend
from backends.contracts.environment import EnvironmentContract
from backends.contracts.pagesetup import PageSetupContract
from backends.contracts.styles import StylesContract
from backends.ezdxf_backend import EzdxfBackend from backends.ezdxf_backend import EzdxfBackend
# Marked per-test rather than module-wide: half of these are plain registry # Marked per-test rather than module-wide: half of these are plain registry
@@ -116,3 +119,50 @@ async def test_an_implementing_backend_is_unaffected(backend):
result = await backend.entity_change_space([], "nope") result = await backend.entity_change_space([], "nope")
assert result["ok"] is False assert result["ok"] is False
assert "error" in result, "the real implementation still does its own validation" assert "error" in result, "the real implementation still does its own validation"
# ── track E: composition and the five agreed keys ───────────────────────────
#: spec §8.1 — the flag each engine must report. `mleaderstyle` is deliberately
#: absent: the spec retracted it (the ActiveX route through the
#: ``ACAD_MLEADERSTYLE`` dictionary exists, §4.2), and both engines implement
#: ``mleaderstyle_create`` natively — `capability_keys()` never registers it.
TRACK_E_KEYS = {
"dwgprops": {"ezdxf": False, "com": True},
"dwt_write": {"ezdxf": False, "com": True},
"live_application": {"ezdxf": False, "com": True},
"preferences": {"ezdxf": False, "com": True},
"interactive_prompt": {"ezdxf": False, "com": True},
}
def _feature_keys_full(backend) -> dict:
return backend.capabilities().to_dict()["features"]
def test_track_e_contracts_are_composed_into_the_backend():
for contract in (StylesContract, PageSetupContract, EnvironmentContract):
assert issubclass(AutoCADBackend, contract), f"{contract.__name__} not in AutoCADBackend"
assert issubclass(EzdxfBackend, contract) and issubclass(ComBackend, contract)
def test_track_e_capability_keys_carry_the_agreed_flags():
assert "mleaderstyle" not in capability_keys(), "spec §8.1: no such engine boundary"
ezdxf_features = _feature_keys_full(EzdxfBackend())
com_features = _feature_keys_full(ComBackend())
for key, flags in TRACK_E_KEYS.items():
assert key in ezdxf_features, f"{key} undeclared on ezdxf"
assert key in com_features, f"{key} undeclared on com"
assert ezdxf_features[key]["supported"] is flags["ezdxf"], key
assert com_features[key]["supported"] is flags["com"], key
def test_layer_states_are_declared_as_ours_not_autocads():
"""Spec §7.2 / §8.3: the states live in the file and travel with it, and do
not appear in AutoCAD's Layer States Manager. Said in the map, not only in
a docstring."""
for backend in (EzdxfBackend(), ComBackend()):
feature = _feature_keys_full(backend)["layer_states"]
assert feature["supported"] is True
assert feature["mode"] == "xrecord"
assert "layer_states_manager" in (feature["reason"] or ""), backend.name
+122 -14
View File
@@ -1,4 +1,11 @@
"""TOOL_PACKS: advertise only the vertical packs a client uses; core is always on.""" # tests/test_tool_packs.py
"""TOOL_PACKS: advertise only the vertical packs a client uses; core is always on.
v1.6 track E adds the `settings` pack: the 22 SECTION 20 environment tools.
Styles (SECTION 18) and page setup / templates (SECTION 19) are drafting
essentials and stay in `core`, so `TOOL_PACKS=core` hides the environment
surface and nothing else.
"""
from __future__ import annotations from __future__ import annotations
@@ -24,6 +31,63 @@ PID_TOOLS = {
"pid_from_spec", "pid_from_spec",
} }
STYLE_TOOLS = {
"dimstyle_list",
"dimstyle_create",
"dimstyle_modify",
"dimstyle_set_current",
"textstyle_list",
"textstyle_create",
"textstyle_set_current",
"mleaderstyle_list",
"mleaderstyle_create",
"drawing_apply_standard",
}
PAGE_SETUP_TOOLS = {
"page_setup_list",
"page_setup_apply",
"plot_style_list",
"batch_plot",
"drawing_template_list",
"drawing_template_save",
}
SETTINGS_TOOLS = {
"document_list",
"document_activate",
"document_close",
"layer_state_save",
"layer_state_restore",
"layer_state_list",
"layer_state_delete",
"view_named_save",
"view_named_restore",
"view_named_list",
"ucs_list",
"ucs_set",
"ucs_restore",
"system_launch",
"system_preferences_get",
"system_preferences_set",
"drawing_properties_get",
"drawing_properties_set",
"system_variable_describe",
"user_pick_point",
"user_select",
"system_prompt_message",
}
LEAN_SETTINGS_ESSENTIALS = {
"drawing_apply_standard",
"page_setup_apply",
"batch_plot",
"dimstyle_set_current",
"textstyle_set_current",
}
ALL_PACKS = ["core", "pid", "settings"]
@pytest_asyncio.fixture(autouse=True) @pytest_asyncio.fixture(autouse=True)
async def _restore(monkeypatch): async def _restore(monkeypatch):
@@ -32,29 +96,48 @@ async def _restore(monkeypatch):
await server._apply_tool_profile("full") await server._apply_tool_profile("full")
def test_pack_registry_names_real_tools_and_only_pid_ones(): def test_pack_registry_names_real_tools_and_only_them():
assert server.TOOL_PACK_NAMES == ("core", "pid") assert server.TOOL_PACK_NAMES == ("core", "pid", "settings")
assert server.PACK_TOOL_NAMES["pid"] == frozenset(PID_TOOLS) assert server.PACK_TOOL_NAMES["pid"] == frozenset(PID_TOOLS)
assert server.PACK_TOOL_NAMES["settings"] == frozenset(SETTINGS_TOOLS)
assert len(SETTINGS_TOOLS) == 22
assert not (server.PACK_TOOL_NAMES["settings"] & server.PACK_TOOL_NAMES["pid"])
async def test_core_only_hides_every_pid_tool_but_keeps_block_define(monkeypatch): async def test_every_pack_member_is_a_registered_tool():
registered = {tool.name for tool in await server._registered_tools()}
for pack, names in server.PACK_TOOL_NAMES.items():
missing = names - registered
assert not missing, f"pack {pack!r} names unregistered tools: {sorted(missing)}"
async def test_core_only_hides_pid_and_settings_but_keeps_styles_and_page_setup(monkeypatch):
monkeypatch.setattr(config.settings, "tool_packs", "core") monkeypatch.setattr(config.settings, "tool_packs", "core")
info = await server._apply_tool_profile("full") info = await server._apply_tool_profile("full")
disabled = set(info["disabled_tools"]) disabled = set(info["disabled_tools"])
assert PID_TOOLS <= disabled assert PID_TOOLS <= disabled
assert SETTINGS_TOOLS <= disabled
assert not (STYLE_TOOLS & disabled), "styles are core drafting essentials"
assert not (PAGE_SETUP_TOOLS & disabled), "page setup and templates are core"
assert "block_define" not in disabled and "entity_set_xdata" not in disabled assert "block_define" not in disabled and "entity_set_xdata" not in disabled
assert info["tool_packs"] == { assert info["tool_packs"] == {"enabled": ["core"], "available": ALL_PACKS, "ignored": []}
"enabled": ["core"],
"available": ["core", "pid"],
"ignored": [],
}
async def test_all_is_the_default_and_enables_pid(monkeypatch): async def test_settings_pack_alone_keeps_pid_hidden(monkeypatch):
monkeypatch.setattr(config.settings, "tool_packs", "settings")
info = await server._apply_tool_profile("full")
disabled = set(info["disabled_tools"])
assert PID_TOOLS <= disabled
assert not (SETTINGS_TOOLS & disabled)
assert info["tool_packs"]["enabled"] == ["core", "settings"]
async def test_all_is_the_default_and_enables_every_pack(monkeypatch):
monkeypatch.setattr(config.settings, "tool_packs", "all") monkeypatch.setattr(config.settings, "tool_packs", "all")
info = await server._apply_tool_profile("full") info = await server._apply_tool_profile("full")
assert not (PID_TOOLS & set(info["disabled_tools"])) disabled = set(info["disabled_tools"])
assert info["tool_packs"]["enabled"] == ["core", "pid"] assert not ((PID_TOOLS | SETTINGS_TOOLS) & disabled)
assert info["tool_packs"]["enabled"] == ALL_PACKS
async def test_unknown_pack_is_ignored_with_a_warning_and_core_cannot_be_dropped( async def test_unknown_pack_is_ignored_with_a_warning_and_core_cannot_be_dropped(
@@ -65,7 +148,7 @@ async def test_unknown_pack_is_ignored_with_a_warning_and_core_cannot_be_dropped
info = await server._apply_tool_profile("full") info = await server._apply_tool_profile("full")
assert info["tool_packs"] == { assert info["tool_packs"] == {
"enabled": ["core", "pid"], "enabled": ["core", "pid"],
"available": ["core", "pid"], "available": ALL_PACKS,
"ignored": ["mech3000"], "ignored": ["mech3000"],
} }
assert "mech3000" in caplog.text assert "mech3000" in caplog.text
@@ -79,9 +162,34 @@ async def test_lean_intersects_with_packs(monkeypatch):
assert "entity_create_line" not in info["disabled_tools"] assert "entity_create_line" not in info["disabled_tools"]
async def test_lean_carries_the_five_settings_essentials_and_nothing_from_the_settings_pack(
monkeypatch,
):
"""Spec §8.2: lean = 55. The five are core-pack tools, so `TOOL_PACKS=core`
leaves them on a lean surface; no environment tool is lean."""
assert LEAN_SETTINGS_ESSENTIALS <= server.LEAN_TOOL_NAMES
assert not (SETTINGS_TOOLS & server.LEAN_TOOL_NAMES)
assert len(server.LEAN_TOOL_NAMES) == 55
monkeypatch.setattr(config.settings, "tool_packs", "core")
info = await server._apply_tool_profile("lean")
disabled = set(info["disabled_tools"])
assert not (LEAN_SETTINGS_ESSENTIALS & disabled)
assert info["enabled_count"] == 55 - 3, "lean minus the three pid tools"
async def test_the_three_track_e_sections_file_under_their_own_groups():
"""Pack, group and SECTION are the same list, so `system_about` and
`docs/tool-inventory.json` describe the surface the packs gate."""
groups = await server._tool_groups()
assert set(groups["styles"]) == STYLE_TOOLS
assert set(groups["page_setup"]) == PAGE_SETUP_TOOLS
assert set(groups["environment"]) == SETTINGS_TOOLS
assert len(groups) == 23
async def test_system_about_reports_packs(): async def test_system_about_reports_packs():
from fastmcp import Client from fastmcp import Client
async with Client(server.mcp) as client: async with Client(server.mcp) as client:
about = (await client.call_tool("system_about", {})).structured_content about = (await client.call_tool("system_about", {})).structured_content
assert about["tool_packs"]["available"] == ["core", "pid"] assert about["tool_packs"]["available"] == ALL_PACKS
+21 -36
View File
@@ -495,7 +495,7 @@ async def test_tool_groups_is_byte_identical_to_the_source_declared_grouping():
async def test_tool_group_sizes_are_unchanged(): async def test_tool_group_sizes_are_unchanged():
"""Frozen snapshot of the surface (172 tools, 20 groups). """Frozen snapshot of the surface (204 tools, 23 groups).
Taken before @cad_tool landed at 131 tools; `batch` moved 2 -> 3 when Taken before @cad_tool landed at 131 tools; `batch` moved 2 -> 3 when
v1.5.0's `cad_batch` joined `entity_batch_create`/`entity_batch_modify`, and v1.5.0's `cad_batch` joined `entity_batch_create`/`entity_batch_modify`, and
@@ -517,35 +517,18 @@ async def test_tool_group_sizes_are_unchanged():
when `pid_tag_parse` (the ISA-5.1 tag grammar as a read-only tool) when `pid_tag_parse` (the ISA-5.1 tag grammar as a read-only tool)
joined; the `pid` joined; the `pid`
tag is ranked first in `_GROUP_TAG_PRIORITY` so those tools file under tag is ranked first in `_GROUP_TAG_PRIORITY` so those tools file under
`pid` rather than under their secondary `query` / `create` tags. `styles` `pid` rather than under their secondary `query` / `create` tags.
appeared (0 -> 9) when track E's SECTION 18 opened with the dimension, Three groups appeared with v1.6's track E — `styles` (10, SECTION 18),
text and multileader style tools, then 9 -> 10 when `page_setup` (6, SECTION 19) and `environment` (22, SECTION 20) — each
`drawing_apply_standard` (ISO or ANSI styles, units and layers in one tagged with its section name, ranked directly after `pid` in
call) joined; the `style` tag sits ahead of `layer` and `drawing` in `_GROUP_TAG_PRIORITY` so a style tool that also carries `dimension` or an
`_GROUP_TAG_PRIORITY` so they never file under their secondary environment tool that also carries `layer` files under its section; the
`query` / `create` / `modify` / `drawing` tags. `system` `environment` group is exactly `PACK_TOOL_NAMES["settings"]`. (Until the
moved 7 -> 8 when v1.6's `system_variable_describe` (the sysvar catalogue merge task tagged them, the four groups had filed those 28 tools under
as a read-only tool) opened SECTION 20. `drawing` moved 11 -> 13 when `layouts` / `templates` / `drawing` / `layers` / `view` / `system` by
v1.6's `drawing_properties_get` / `drawing_properties_set` (DWGPROPS on their secondary tags; those counts returned to their pre-track-E values
both engines) joined SECTION 20 — tagged `drawing` and `settings`, and when the section tags landed.) Every other number here has been unchanged
`settings` is not a group tag, so they file under drawing. since the snapshot was taken.
`layouts` moved 12 -> 16 when v1.6's SECTION 19 opened with
`page_setup_list` / `page_setup_apply` / `plot_style_list` / `batch_plot`
— all tagged `layout`, which `_GROUP_TAG_PRIORITY` ranks above
`query`/`export`, so they file with the sheets they set up. `templates`
moved 2 -> 3 when v1.6's `drawing_template_list` joined SECTION 19
(tagged `template`, which ranks above `drawing`), then 3 -> 4 when
`drawing_template_save` joined. `drawing`
moved 13 -> 16 when v1.6's `document_list` / `document_activate` /
`document_close` (multi-document on both engines) opened SECTION 20;
`layers` moved 14 -> 18 with the four `layer_state_*` tools and `view`
4 -> 10 with `view_named_*` and `ucs_*` (UCS files under `view`, where
AutoCAD's own ribbon keeps it) when v1.6's SECTION 20 grew. `system`
moved 8 -> 14 with v1.6's live-only environment tools (`system_launch`,
`system_preferences_get/set`, `user_pick_point`, `user_select`,
`system_prompt_message`), which refuse headlessly with declared
capability keys. Every other number here has been unchanged since the
snapshot was taken.
""" """
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()} sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
assert sizes == { assert sizes == {
@@ -554,21 +537,23 @@ async def test_tool_group_sizes_are_unchanged():
"blocks": 9, "blocks": 9,
"corner_ops": 4, "corner_ops": 4,
"dimensions": 5, "dimensions": 5,
"drawing": 16, "drawing": 11,
"engineering": 10, "engineering": 10,
"entity_creation": 18, "entity_creation": 18,
"entity_modification": 16, "entity_modification": 16,
"entity_query": 9, "entity_query": 9,
"layers": 18, "environment": 22,
"layouts": 16, "layers": 14,
"layouts": 12,
"page_setup": 6,
"pid": 9, "pid": 9,
"premium": 12, "premium": 12,
"solids": 5, "solids": 5,
"styles": 10, "styles": 10,
"system": 14, "system": 7,
"templates": 4, "templates": 2,
"transactions": 3, "transactions": 3,
"validation": 1, "validation": 1,
"view": 10, "view": 4,
} }
assert sum(sizes.values()) == 204 assert sum(sizes.values()) == 204