From b4d8b86cde0d29171e5ecdb28072c680a5fb5d33 Mon Sep 17 00:00:00 2001 From: Andrew Bartels Date: Wed, 4 Nov 2026 21:47:08 -0800 Subject: [PATCH 1/4] chore(openspec): archive the tool-descriptions change (#82) --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/tools/tool-descriptions/spec.md | 0 .../tasks.md | 0 .../specs/tools/tool-descriptions/spec.md | 73 +++++++++++++++++++ 6 files changed, 73 insertions(+) rename openspec/changes/{tool-descriptions-for-agents => archive/2026-11-04-tool-descriptions-for-agents}/.openspec.yaml (100%) rename openspec/changes/{tool-descriptions-for-agents => archive/2026-11-04-tool-descriptions-for-agents}/design.md (100%) rename openspec/changes/{tool-descriptions-for-agents => archive/2026-11-04-tool-descriptions-for-agents}/proposal.md (100%) rename openspec/changes/{tool-descriptions-for-agents => archive/2026-11-04-tool-descriptions-for-agents}/specs/tools/tool-descriptions/spec.md (100%) rename openspec/changes/{tool-descriptions-for-agents => archive/2026-11-04-tool-descriptions-for-agents}/tasks.md (100%) create mode 100644 openspec/specs/tools/tool-descriptions/spec.md diff --git a/openspec/changes/tool-descriptions-for-agents/.openspec.yaml b/openspec/changes/archive/2026-11-04-tool-descriptions-for-agents/.openspec.yaml similarity index 100% rename from openspec/changes/tool-descriptions-for-agents/.openspec.yaml rename to openspec/changes/archive/2026-11-04-tool-descriptions-for-agents/.openspec.yaml diff --git a/openspec/changes/tool-descriptions-for-agents/design.md b/openspec/changes/archive/2026-11-04-tool-descriptions-for-agents/design.md similarity index 100% rename from openspec/changes/tool-descriptions-for-agents/design.md rename to openspec/changes/archive/2026-11-04-tool-descriptions-for-agents/design.md diff --git a/openspec/changes/tool-descriptions-for-agents/proposal.md b/openspec/changes/archive/2026-11-04-tool-descriptions-for-agents/proposal.md similarity index 100% rename from openspec/changes/tool-descriptions-for-agents/proposal.md rename to openspec/changes/archive/2026-11-04-tool-descriptions-for-agents/proposal.md diff --git a/openspec/changes/tool-descriptions-for-agents/specs/tools/tool-descriptions/spec.md b/openspec/changes/archive/2026-11-04-tool-descriptions-for-agents/specs/tools/tool-descriptions/spec.md similarity index 100% rename from openspec/changes/tool-descriptions-for-agents/specs/tools/tool-descriptions/spec.md rename to openspec/changes/archive/2026-11-04-tool-descriptions-for-agents/specs/tools/tool-descriptions/spec.md diff --git a/openspec/changes/tool-descriptions-for-agents/tasks.md b/openspec/changes/archive/2026-11-04-tool-descriptions-for-agents/tasks.md similarity index 100% rename from openspec/changes/tool-descriptions-for-agents/tasks.md rename to openspec/changes/archive/2026-11-04-tool-descriptions-for-agents/tasks.md diff --git a/openspec/specs/tools/tool-descriptions/spec.md b/openspec/specs/tools/tool-descriptions/spec.md new file mode 100644 index 00000000..d373c715 --- /dev/null +++ b/openspec/specs/tools/tool-descriptions/spec.md @@ -0,0 +1,73 @@ +# tools/tool-descriptions Specification + +## Purpose +Every MCP tool tells an agent, before it is called, what it does, when to +use it over its siblings, what it needs, what it changes and what it +returns. +## Requirements +### Requirement: Descriptions follow one standard + +Every registered tool's description SHALL say: + +- what the tool does, in SolidWorks terms; +- when to use it, and which other tool to use instead, where tools overlap; +- what it needs (document type, selection, SolidWorks running); +- what it changes, or that it is read-only; +- what its success payload carries. + +It SHALL NOT be a restatement of the tool's name, and SHALL fit in 1200 +characters. + +#### Scenario: A tool that overlaps another + +- **WHEN** an agent reads the description of `get_mass_properties` +- **THEN** it says which of `get_mass_properties` and + `calculate_mass_properties` to use + +#### Scenario: A tool that only reads + +- **WHEN** an agent reads the description of a tool that changes nothing +- **THEN** it says the tool is read-only + +### Requirement: The input object is described + +A tool that takes one `input_data` object SHALL describe that object +specifically: what it holds, and which fields are required. It SHALL NOT use +a generic description such as "The input data value." Every field inside +the input object SHALL have its own description. + +#### Scenario: Reading a tool's schema + +- **WHEN** a client lists the tools +- **THEN** no tool's `input_data` description is generic +- **AND** every field of every input object has a description + +### Requirement: Behaviour hints on every tool + +Every registered tool SHALL carry the MCP annotations `title`, +`readOnlyHint`, `destructiveHint` and `idempotentHint`. A tool that changes +nothing SHALL be read-only. A tool that deletes, overwrites or closes without +saving SHALL be destructive. Every tool SHALL also carry one family tag, +the name of the module that defines it. + +#### Scenario: Listing the tools + +- **WHEN** a client lists the tools +- **THEN** each tool has a title and the three hints +- **AND** `list_features` is read-only, and `delete_feature` is destructive +- **AND** `create_extrusion` is tagged `modeling` + +### Requirement: The standard is checked + +The test suite SHALL fail when a registered tool: + +- has a description that breaks the standard; +- has a generic `input_data` description, or an input field without one; +- has no hints or no family tag. + +#### Scenario: A new tool with a placeholder docstring + +- **WHEN** a contributor registers a tool whose description is + "Handle new thing." +- **THEN** the description check fails and names the tool + From 47f9ed706d66a48d323dbd47a423f47ceee3add0 Mon Sep 17 00:00:00 2001 From: Andrew Bartels Date: Wed, 4 Nov 2026 21:49:24 -0800 Subject: [PATCH 2/4] docs(openspec): plan the agent skill and server instructions (#175) --- .../.openspec.yaml | 2 + .../agent-skill-and-instructions/design.md | 125 ++++++++++++++++++ .../agent-skill-and-instructions/proposal.md | 65 +++++++++ .../specs/agents/server-instructions/spec.md | 29 ++++ .../specs/agents/usage-skill/spec.md | 96 ++++++++++++++ .../agent-skill-and-instructions/tasks.md | 55 ++++++++ openspec/queue.md | 6 +- 7 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 openspec/changes/agent-skill-and-instructions/.openspec.yaml create mode 100644 openspec/changes/agent-skill-and-instructions/design.md create mode 100644 openspec/changes/agent-skill-and-instructions/proposal.md create mode 100644 openspec/changes/agent-skill-and-instructions/specs/agents/server-instructions/spec.md create mode 100644 openspec/changes/agent-skill-and-instructions/specs/agents/usage-skill/spec.md create mode 100644 openspec/changes/agent-skill-and-instructions/tasks.md diff --git a/openspec/changes/agent-skill-and-instructions/.openspec.yaml b/openspec/changes/agent-skill-and-instructions/.openspec.yaml new file mode 100644 index 00000000..4ccb2b41 --- /dev/null +++ b/openspec/changes/agent-skill-and-instructions/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-04 diff --git a/openspec/changes/agent-skill-and-instructions/design.md b/openspec/changes/agent-skill-and-instructions/design.md new file mode 100644 index 00000000..04808d26 --- /dev/null +++ b/openspec/changes/agent-skill-and-instructions/design.md @@ -0,0 +1,125 @@ +## Context + +See proposal.md for why. Current state: + +- `server.py` builds `FastMCP("SolidWorks MCP Server", version=...)` with no + `instructions` and registers no resources; tools come from + `tools.register_tools`. +- `cli_main` starts the server, except `solidworks-mcp setup ...`, which hands + off to the Typer app in `setup/cli.py`. +- FastMCP 3.4.9 (installed) has + `fastmcp.server.providers.skills.SkillProvider(skill_path)`, which serves + one skill folder as `skill:///SKILL.md`, `skill:///_manifest` + and its other files, added with `mcp.add_provider(...)`. The + `SkillCatalogProvider` and `Skill` classes the user's notes mention are + FastMCP 4.x and are not used. +- The wheel packages `src/solidworks_mcp`, so files placed under it ship. +- Tool descriptions (#82) already say what each tool does; the skill must + not repeat them. + +## Goals / Non-Goals + +**Goals:** one source of guidance, in the package, reachable three ways +(server instructions, `skill://` resources, an installed skill), and a test +that fails when it names a tool that is gone or recommends one that can't +run. + +**Non-Goals:** + +- Fixing the faults the skill warns about (queue item 10). +- Per-client install logic beyond a skills folder (no editing of client + configs; `solidworks-mcp setup` stays as it is). +- Contributor guidance: `com-api-pitfalls.md` stays the COM reference. + +## Decisions + +### The skill lives in the package, at `src/solidworks_mcp/skills/solidworks-mcp/` + +Rather than `skills/solidworks-mcp/` at the repo root (the issue's +suggestion). Inside the package it ships in the wheel, so `uvx` and pip +installs can serve and install it without the repo; at the root it would need +extra packaging config and would be missing from installs. The +`skills/` folder name keeps the layout the issue describes. + +### Layout: `SKILL.md` plus four reference files + +- `SKILL.md`: when to use it, the build order, the unit rule, the + read-back rule, and a map to the references (target: about 150 lines). +- `references/workflows.md`: part, assembly and drawing walk-throughs as + tool sequences, with the checks between steps. +- `references/selection-and-units.md`: planes, faces, edges; every input + that takes metres (`measure_entities`, the `edge_names` point specs of + `add_fillet`/`add_chamfer`) against the rest in mm. +- `references/errors-and-limits.md`: reading an error (`error_type`, + `hint`), `rebuild_model` and `undo`, the tools that can't run yet, and + the known faults from queue item 10 with their workarounds. +- `references/vba-fallback.md`: when to use the VBA generation tools and + `execute_macro`, and when not to. + +This keeps `SKILL.md` short (it is read whole), with detail loaded only when +needed, which is how both Claude Code and the `skill://` manifest are meant +to work. + +### Serve it with `SkillProvider`, supporting files as a template + +`mcp.add_provider(SkillProvider(path))` in `server.setup()`, with the +default `supporting_files="template"`: `list_resources` shows `SKILL.md` and +the manifest, and the reference files are read through the manifest. This +keeps the resource list short. Alternative, `"resources"` (every file +listed): rejected, it lengthens the list every client loads. + +### Instructions: a constant next to the skill, at most 1500 characters + +A module constant (`skills.SERVER_INSTRUCTIONS`) passed to `FastMCP(..., +instructions=...)`. It holds only the rules a session can't do without (see +the spec) and points to the skill. Alternative, generating it from +`SKILL.md`: rejected; the two have different jobs and lengths. + +### `solidworks-mcp skill install` as its own small Typer app + +`cli_main` routes `skill` the way it routes `setup`. The command takes +`--project`, `--dir PATH` and `--dry-run`, and copies the packaged files with +`importlib.resources`, overwriting only files it ships. It never deletes, +never follows the folder outside the target, and reads nothing but the +packaged skill, so it can't touch other users' or tools' files. It needs no +network or token. + +Alternatives: an option on `setup` (rejected: `setup` is an interactive flow +about client configs; mixing the two would make `--yes` also write skills), +or only documenting a manual copy (rejected: the issue asks for one +command). `setup` prints a one-line hint about `skill install` at the end; +that is the only change to it. + +### A test keeps the skill true + +`tests/integration/test_skill_content.py` (it spans the skill, the tools +and the instructions, so it is cross-module) parses tool names from +the skill files and the instructions (backticked names that match the +registered-tool pattern) and checks each is registered, and that no tool the +skill's build steps recommend is one that can't run (the "Not available yet" +sentence in its description). A fault fixed later must also be removed from +`errors-and-limits.md`: the queue's item-10 entry says so. + +## Risks / Trade-offs + +- [The skill drifts from the tools as they change] → the name test above, + and the queue note that fixing a fault edits the skill. +- [Clients that ignore `instructions` and resources] → the installed skill + covers them; the README says how. +- [Codex's skills folder may differ by version] → `--dir` takes any folder; + the README names Claude Code's folder and shows `--dir` for others rather + than guessing a path. +- [The measured run depends on live SolidWorks and an agent run] → it is the + last task, run by the user with `live_demo.ps1`-style permission, and does + not block merging the skill. + +## Migration Plan + +Additive. Rollback is removing the provider and instructions line; installed +skills are plain files users can delete. + +## Open Questions + +- Which demo prompt the measurement uses: the bracket demo (#147, queue item + 6, may be narrowed) or the monitor-stand demo already in `docs/demos/`. + Either fits the task; it is chosen when the live run is scheduled. diff --git a/openspec/changes/agent-skill-and-instructions/proposal.md b/openspec/changes/agent-skill-and-instructions/proposal.md new file mode 100644 index 00000000..66e5242e --- /dev/null +++ b/openspec/changes/agent-skill-and-instructions/proposal.md @@ -0,0 +1,65 @@ +## Why + +Agents call this server's tools without knowing how SolidWorks work goes: +that a feature needs a sketch closed first, which tools take millimetres and +which take metres, that a modelling tool's success does not prove the +geometry is right, or which tools can't run yet. The guidance that exists +(`docs/agents/com-api-pitfalls.md`, `.claude/skills/`) is written for +contributors changing the COM code, not for people using the server, and an +agent only sees it if someone pastes it in. Issue #175 asks for a skill users +install into their agent; the tool descriptions (#82, now done) cover each +tool, so what is missing is the guidance that spans tools. + +## What Changes + +- **A usage skill** for agents that drive the server: `SKILL.md` plus a few + reference files, shipped inside the Python package so every install has + it. It covers the build order (part, sketch, `exit_sketch`, feature, + `verify_model`), units (mm in, with the named exceptions that take metres), + planes and face/edge selection, reading back after every change, recovering + from errors (`rebuild_model`, `undo`), the tools that can't run yet and the + known faults, and when to fall back to VBA generation. Sourced from the + tool code, the description standard, `com-api-pitfalls.md` and + `openspec/queue.md`. +- **The server serves the skill** as MCP resources (`skill://solidworks-mcp/SKILL.md` + and its files), so a client that reads resources gets it with no install. +- **Server instructions**: a short `instructions` text sent at connection + with the few rules every session needs, pointing to the skill for the rest. +- **One-command install**: `solidworks-mcp skill install` copies the skill + into an agent's skills folder (Claude Code's user or project folder by + default, or any folder given). It writes only its own folder and reads no + other file. +- **README and docs**: the install command and what the skill covers. +- **Measured**: the demo prompt run with and without the skill on real + SolidWorks, and the difference recorded (live run, with the user's + permission). + +No adapter or COM code changes. Nothing is removed. + +## Capabilities + +### New Capabilities + +- `agents/usage-skill`: the skill's content and the rules that keep it true + (every tool it names exists and is described as the tool behaves), how the + server serves it, and how `solidworks-mcp skill install` places it. +- `agents/server-instructions`: the `instructions` text the server sends at + connection. + +### Modified Capabilities + +None. + +## Impact + +- New: `src/solidworks_mcp/skills/solidworks-mcp/` (the skill files), + `src/solidworks_mcp/skills/` code to serve and install it, a `skill` + subcommand in `cli_main` (`server.py`). +- Changed: `server.py` passes `instructions` to `FastMCP` and adds the + skill provider; README and `docs/` gain the install section. +- Packaging: the skill files ship in the wheel (they live under + `src/solidworks_mcp`, which the wheel already includes). +- Depends on FastMCP 3.4.9's `SkillProvider`, already installed; no new + dependency. +- No change to `pywin32_adapter.py`, `base.py` or `mock_adapter.py`. +- GitHub issue: #175. diff --git a/openspec/changes/agent-skill-and-instructions/specs/agents/server-instructions/spec.md b/openspec/changes/agent-skill-and-instructions/specs/agents/server-instructions/spec.md new file mode 100644 index 00000000..7d538bd9 --- /dev/null +++ b/openspec/changes/agent-skill-and-instructions/specs/agents/server-instructions/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: The server sends short usage instructions at connection + +The server SHALL send an `instructions` text in its MCP initialize response. +It SHALL be at most 1500 characters and SHALL say: + +- to call `get_model_info` (or `list_open_documents`) first, to see what is + open; +- the build order: `create_sketch`, draw, `exit_sketch`, then the feature; +- that inputs are millimetres and degrees unless a field says otherwise; +- to finish a build with `verify_model`, since a tool's success does not + prove the geometry; +- that tools whose description starts "Not available yet" return + `not_supported`; +- where the full guidance is: the `skill://solidworks-mcp/SKILL.md` resource. + +Every tool it names SHALL be a registered tool. + +#### Scenario: A client connects + +- **WHEN** an MCP client initializes a session with the server +- **THEN** the response carries the instructions text +- **AND** it names `skill://solidworks-mcp/SKILL.md` + +#### Scenario: The instructions name a removed tool + +- **WHEN** a tool named in the instructions is no longer registered +- **THEN** the test suite fails, naming the tool diff --git a/openspec/changes/agent-skill-and-instructions/specs/agents/usage-skill/spec.md b/openspec/changes/agent-skill-and-instructions/specs/agents/usage-skill/spec.md new file mode 100644 index 00000000..f30e40cb --- /dev/null +++ b/openspec/changes/agent-skill-and-instructions/specs/agents/usage-skill/spec.md @@ -0,0 +1,96 @@ +## ADDED Requirements + +### Requirement: The skill teaches cross-tool use of the server + +The package SHALL include a skill named `solidworks-mcp`: a `SKILL.md` with +a name and a description in its frontmatter, plus reference files. It SHALL +cover: + +- the build order: create or open a document, `create_sketch`, draw, + `exit_sketch`, then the feature tool; and `verify_model` as the last step; +- units: inputs in millimetres and degrees, naming each input that takes + metres instead; +- choosing planes, faces and edges, and preferring a whole face over a point + on an edge; +- reading back after every change, and what a success does and does not + prove; +- recovering from a failure (`rebuild_model`, `undo`, `get_model_info`); +- the tools that can't run yet and the known faults agents must work around; +- when to use the VBA generation tools instead. + +`SKILL.md` SHALL stay under 500 lines and put detail in the reference files. + +#### Scenario: An agent reads the skill + +- **WHEN** an agent loads `SKILL.md` +- **THEN** its frontmatter has `name: solidworks-mcp` and a description that + says when to use it +- **AND** its body lists the build order and the unit rule + +### Requirement: The skill names only real tools, correctly + +Every tool name the skill mentions SHALL be a tool the server registers. A +tool the skill tells agents to call SHALL NOT be one that can't run yet +(every call returns `not_supported`). + +#### Scenario: A tool is renamed or removed + +- **WHEN** a tool the skill names is no longer registered +- **THEN** the test suite fails, naming the tool and the skill file + +#### Scenario: The skill recommends a tool that can't run + +- **WHEN** the skill's build steps name a tool that returns `not_supported` + for every call +- **THEN** the test suite fails + +### Requirement: The server serves the skill + +The server SHALL expose the skill as MCP resources: `SKILL.md` at +`skill://solidworks-mcp/SKILL.md`, a manifest listing its files, and each +reference file. Reading them SHALL need no SolidWorks connection. + +#### Scenario: A client lists the server's resources + +- **WHEN** a client lists resources on a running server, in mock mode or real +- **THEN** `skill://solidworks-mcp/SKILL.md` is listed +- **AND** reading it returns the same text as the packaged `SKILL.md` + +### Requirement: One command installs the skill into an agent + +`solidworks-mcp skill install` SHALL copy the packaged skill into +`/solidworks-mcp/`. The skills folder SHALL be Claude Code's +user folder (`~/.claude/skills`) by default, the current project's +`.claude/skills` with `--project`, or any folder given with `--dir`. It +SHALL report the path it wrote. + +It SHALL write only inside `/solidworks-mcp/`, SHALL NOT read +or change any other file (no client config, no other skill), and SHALL work +offline with no account or token. When the folder already holds the skill, +it SHALL replace the packaged files and leave any other file in that folder +alone. `--dry-run` SHALL report what it would write without writing. + +#### Scenario: First install for Claude Code + +- **WHEN** a user runs `solidworks-mcp skill install` +- **THEN** `~/.claude/skills/solidworks-mcp/SKILL.md` exists, identical to + the packaged one +- **AND** the command prints that path + +#### Scenario: Upgrading after a new release + +- **WHEN** the skill folder already holds an older copy and a file the user + added +- **THEN** the packaged files are replaced +- **AND** the user's file is unchanged + +#### Scenario: Another agent + +- **WHEN** a user runs `solidworks-mcp skill install --dir ` +- **THEN** the skill is written to `/solidworks-mcp/` + +#### Scenario: The target can't be written + +- **WHEN** the skills folder can't be created or written +- **THEN** the command exits with an error that names the path +- **AND** nothing outside that path has changed diff --git a/openspec/changes/agent-skill-and-instructions/tasks.md b/openspec/changes/agent-skill-and-instructions/tasks.md new file mode 100644 index 00000000..61ff740c --- /dev/null +++ b/openspec/changes/agent-skill-and-instructions/tasks.md @@ -0,0 +1,55 @@ +## 1. The skill package + +- [ ] 1.1 `src/solidworks_mcp/skills/__init__.py`: `SKILL_DIR` (the packaged + skill folder, via `importlib.resources`) and `SERVER_INSTRUCTIONS`; + tests in `tests/solidworks_mcp/skills/test_init.py` +- [ ] 1.2 Write `SKILL.md` (frontmatter `name: solidworks-mcp` and a + description; build order, units, read-back, map to the references), + under 500 lines +- [ ] 1.3 Write `references/workflows.md`, `references/selection-and-units.md`, + `references/errors-and-limits.md` and `references/vba-fallback.md`, + each fact checked against the tool code and descriptions +- [ ] 1.4 `tests/integration/test_skill_content.py`: every backticked tool + name in the skill and the instructions is registered; no recommended + step names a "Not available yet" tool; `SKILL.md` frontmatter and + length; instructions at most 1500 characters + +## 2. Served by the server + +- [ ] 2.1 `server.py`: pass `instructions=SERVER_INSTRUCTIONS` to `FastMCP` + and add `SkillProvider(SKILL_DIR)` in `setup()` +- [ ] 2.2 Tests: the initialize result carries the instructions; + `skill://solidworks-mcp/SKILL.md` and the manifest are listed and read + back the packaged text, in mock mode + +## 3. `solidworks-mcp skill install` + +- [ ] 3.1 `src/solidworks_mcp/skills/install.py`: a Typer app with + `install` (`--project`, `--dir`, `--dry-run`); copies only the + packaged files into `/solidworks-mcp/`, replaces them on + upgrade, leaves other files, reads nothing else, reports the path +- [ ] 3.2 `cli_main` routes `skill ...` to it, as it does `setup`; `setup` + ends with a one-line hint about `skill install` +- [ ] 3.3 Tests in `tests/solidworks_mcp/skills/test_install.py`: default, + `--project` and `--dir` targets (home and cwd redirected to a temp + folder), upgrade keeps a user file, `--dry-run` writes nothing, an + unwritable target errors naming the path; and the `cli_main` routing + +## 4. Docs + +- [ ] 4.1 README: a short "Agent skill" section with the install command and + how other agents use `--dir` +- [ ] 4.2 `docs/agents/`: a page on the skill and the instructions, linked + from mkdocs nav; link the skill from `com-api-pitfalls.md` as the + user-facing counterpart +- [ ] 4.3 `openspec/queue.md`: item 10 notes that fixing a fault also edits + `references/errors-and-limits.md`; CHANGELOG entry + +## 5. Ship + +- [ ] 5.1 Full mock suite, ruff, 100 % coverage, `mkdocs build --strict` +- [ ] 5.2 Ask the user, then run the measurement on real SolidWorks: the + chosen demo prompt in a fresh agent session without the skill, then + with it installed; record tool calls, errors and whether + `verify_model` passed, in the PR and in `docs/agents/` (paced runs, + `SW_TEST_PACE_SECONDS=4`) diff --git a/openspec/queue.md b/openspec/queue.md index 93482641..417403c1 100644 --- a/openspec/queue.md +++ b/openspec/queue.md @@ -6,12 +6,12 @@ reviewed, then implemented, signed off live where it touches SolidWorks, and archived after its PR merges. Update this file when an item is done or the order changes. -Last updated 2026-11-04. +Last updated 2026-11-05. | # | Item | Issue | OpenSpec change | State | | --- | --- | --- | --- | --- | -| 1 | Tool descriptions agents can act on: one description standard, MCP hints and family tags, and a check that keeps them | #82 (docstring half) | `tool-descriptions-for-agents` | Plan approved 2026-11-04; three PRs: (1) standard, check and hints (merged, #202), (2) the eight worst-scoring modules (merged, #203), (3) the other six (built, in review) | -| 2 | Guidance through MCP and an installable skill: the server's `instructions`, the skill served as `skill://` resources with FastMCP 3.4.9's `SkillProvider`, and the same skill installable into an agent | #175 | not yet written | Next | +| 1 | Tool descriptions agents can act on: one description standard, MCP hints and family tags, and a check that keeps them | #82 (docstring half) | `tool-descriptions-for-agents` (archived) | Done 2026-11-05 (#202, #203, #204). Left: after the next release, ask Glama for a rescan and record the score in the internal assessment | +| 2 | Guidance through MCP and an installable skill: the server's `instructions`, the skill served as `skill://` resources with FastMCP 3.4.9's `SkillProvider`, and the same skill installable into an agent | #175 | `agent-skill-and-instructions` | Plan written 2026-11-05, in review | | 3 | `--selftest` command and a compatibility-report issue template | #179 | not yet written | | | 4 | README: COM API coverage audit and licence-tier tags | #79 | not yet written | | | 5 | Remove the stale `validate_coverage.py`; decide whether mypy gates CI | #157 | not yet written | | From 637ab26d293cd691484311fa886e56246809bbc5 Mon Sep 17 00:00:00 2001 From: Andrew Bartels Date: Wed, 4 Nov 2026 22:16:38 -0800 Subject: [PATCH 3/4] feat(skills): an agent skill served as skill:// resources, server instructions, and skill install (#175) --- .../agent-skill-and-instructions/design.md | 3 +- .../agent-skill-and-instructions/tasks.md | 20 +-- src/solidworks_mcp/server.py | 18 +- src/solidworks_mcp/setup/cli.py | 3 + src/solidworks_mcp/skills/__init__.py | 36 ++++ src/solidworks_mcp/skills/install.py | 107 ++++++++++++ .../skills/solidworks-mcp/SKILL.md | 106 ++++++++++++ .../references/errors-and-limits.md | 58 +++++++ .../references/selection-and-units.md | 67 ++++++++ .../solidworks-mcp/references/vba-fallback.md | 30 ++++ .../solidworks-mcp/references/workflows.md | 96 +++++++++++ src/solidworks_mcp/tools/automation.py | 4 +- src/solidworks_mcp/tools/vba_generation.py | 2 +- tests/integration/test_skill_content.py | 157 ++++++++++++++++++ tests/solidworks_mcp/setup/test_cli.py | 7 + tests/solidworks_mcp/skills/__init__.py | 0 tests/solidworks_mcp/skills/test_init.py | 21 +++ tests/solidworks_mcp/skills/test_install.py | 138 +++++++++++++++ tests/solidworks_mcp/test_server.py | 20 +++ tests/solidworks_mcp/test_server_skill.py | 61 +++++++ 20 files changed, 938 insertions(+), 16 deletions(-) create mode 100644 src/solidworks_mcp/skills/__init__.py create mode 100644 src/solidworks_mcp/skills/install.py create mode 100644 src/solidworks_mcp/skills/solidworks-mcp/SKILL.md create mode 100644 src/solidworks_mcp/skills/solidworks-mcp/references/errors-and-limits.md create mode 100644 src/solidworks_mcp/skills/solidworks-mcp/references/selection-and-units.md create mode 100644 src/solidworks_mcp/skills/solidworks-mcp/references/vba-fallback.md create mode 100644 src/solidworks_mcp/skills/solidworks-mcp/references/workflows.md create mode 100644 tests/integration/test_skill_content.py create mode 100644 tests/solidworks_mcp/skills/__init__.py create mode 100644 tests/solidworks_mcp/skills/test_init.py create mode 100644 tests/solidworks_mcp/skills/test_install.py create mode 100644 tests/solidworks_mcp/test_server_skill.py diff --git a/openspec/changes/agent-skill-and-instructions/design.md b/openspec/changes/agent-skill-and-instructions/design.md index 04808d26..27a8b268 100644 --- a/openspec/changes/agent-skill-and-instructions/design.md +++ b/openspec/changes/agent-skill-and-instructions/design.md @@ -62,7 +62,8 @@ to work. ### Serve it with `SkillProvider`, supporting files as a template -`mcp.add_provider(SkillProvider(path))` in `server.setup()`, with the +`mcp.add_provider(SkillProvider(path))` where the server builds its +`FastMCP` instance, not in `setup()` (which connects to SolidWorks first), with the default `supporting_files="template"`: `list_resources` shows `SKILL.md` and the manifest, and the reference files are read through the manifest. This keeps the resource list short. Alternative, `"resources"` (every file diff --git a/openspec/changes/agent-skill-and-instructions/tasks.md b/openspec/changes/agent-skill-and-instructions/tasks.md index 61ff740c..79b60d8a 100644 --- a/openspec/changes/agent-skill-and-instructions/tasks.md +++ b/openspec/changes/agent-skill-and-instructions/tasks.md @@ -1,36 +1,36 @@ ## 1. The skill package -- [ ] 1.1 `src/solidworks_mcp/skills/__init__.py`: `SKILL_DIR` (the packaged +- [x] 1.1 `src/solidworks_mcp/skills/__init__.py`: `SKILL_DIR` (the packaged skill folder, via `importlib.resources`) and `SERVER_INSTRUCTIONS`; tests in `tests/solidworks_mcp/skills/test_init.py` -- [ ] 1.2 Write `SKILL.md` (frontmatter `name: solidworks-mcp` and a +- [x] 1.2 Write `SKILL.md` (frontmatter `name: solidworks-mcp` and a description; build order, units, read-back, map to the references), under 500 lines -- [ ] 1.3 Write `references/workflows.md`, `references/selection-and-units.md`, +- [x] 1.3 Write `references/workflows.md`, `references/selection-and-units.md`, `references/errors-and-limits.md` and `references/vba-fallback.md`, each fact checked against the tool code and descriptions -- [ ] 1.4 `tests/integration/test_skill_content.py`: every backticked tool +- [x] 1.4 `tests/integration/test_skill_content.py`: every backticked tool name in the skill and the instructions is registered; no recommended step names a "Not available yet" tool; `SKILL.md` frontmatter and length; instructions at most 1500 characters ## 2. Served by the server -- [ ] 2.1 `server.py`: pass `instructions=SERVER_INSTRUCTIONS` to `FastMCP` - and add `SkillProvider(SKILL_DIR)` in `setup()` -- [ ] 2.2 Tests: the initialize result carries the instructions; +- [x] 2.1 `server.py`: pass `instructions=SERVER_INSTRUCTIONS` to `FastMCP` + and add `SkillProvider(SKILL_DIR)` when the `FastMCP` instance is built +- [x] 2.2 Tests: the initialize result carries the instructions; `skill://solidworks-mcp/SKILL.md` and the manifest are listed and read back the packaged text, in mock mode ## 3. `solidworks-mcp skill install` -- [ ] 3.1 `src/solidworks_mcp/skills/install.py`: a Typer app with +- [x] 3.1 `src/solidworks_mcp/skills/install.py`: a Typer app with `install` (`--project`, `--dir`, `--dry-run`); copies only the packaged files into `/solidworks-mcp/`, replaces them on upgrade, leaves other files, reads nothing else, reports the path -- [ ] 3.2 `cli_main` routes `skill ...` to it, as it does `setup`; `setup` +- [x] 3.2 `cli_main` routes `skill ...` to it, as it does `setup`; `setup` ends with a one-line hint about `skill install` -- [ ] 3.3 Tests in `tests/solidworks_mcp/skills/test_install.py`: default, +- [x] 3.3 Tests in `tests/solidworks_mcp/skills/test_install.py`: default, `--project` and `--dir` targets (home and cwd redirected to a temp folder), upgrade keeps a user file, `--dry-run` writes nothing, an unwritable target errors naming the path; and the `cli_main` routing diff --git a/src/solidworks_mcp/server.py b/src/solidworks_mcp/server.py index f9b3de4b..29157b51 100644 --- a/src/solidworks_mcp/server.py +++ b/src/solidworks_mcp/server.py @@ -18,6 +18,7 @@ import typer from fastmcp import FastMCP +from fastmcp.server.providers.skills import SkillProvider from loguru import logger from pydantic import BaseModel from pydantic_ai import Agent @@ -31,6 +32,7 @@ from .cache.response_cache import CachePolicy, ResponseCache from .config import DeploymentMode, SolidWorksMCPConfig, load_config from .exceptions import SolidWorksMCPError +from .skills import SERVER_INSTRUCTIONS, SKILL_DIR from .version import __version__ AGENT_SYSTEM_PROMPT = ( @@ -86,7 +88,13 @@ def __init__(self, config: SolidWorksMCPConfig): """ self.config = config self.state = MCPServerState(config=config) - self.mcp = FastMCP("SolidWorks MCP Server", version=__version__) + self.mcp = FastMCP( + "SolidWorks MCP Server", + version=__version__, + instructions=SERVER_INSTRUCTIONS, + ) + # The usage skill, as skill:// resources; needs no SolidWorks connection. + self.mcp.add_provider(SkillProvider(SKILL_DIR)) self.server: FastMCP | None = None self._setup_complete = False self._db_logging_enabled = self._env_truthy( @@ -785,7 +793,8 @@ async def main() -> None: def cli_main() -> None: """Console script entry point using Typer CLI. - ``solidworks-mcp setup ...`` runs the guided client setup; anything else + ``solidworks-mcp setup ...`` runs the guided client setup, + ``solidworks-mcp skill ...`` installs the usage skill; anything else starts the server. Returns: @@ -796,6 +805,11 @@ def cli_main() -> None: app(args=sys.argv[2:], prog_name="solidworks-mcp setup") return + if sys.argv[1:2] == ["skill"]: + from .skills.install import app as skill_app + + skill_app(args=sys.argv[2:], prog_name="solidworks-mcp skill") + return typer.run(cli) diff --git a/src/solidworks_mcp/setup/cli.py b/src/solidworks_mcp/setup/cli.py index 57ef26b0..991064c0 100644 --- a/src/solidworks_mcp/setup/cli.py +++ b/src/solidworks_mcp/setup/cli.py @@ -156,6 +156,9 @@ def setup( console.print("\n[bold]Next[/bold]") for target in chosen: console.print(f" {target.name}: {target.restart_hint}") + console.print( + " To teach your agent how to use these tools: solidworks-mcp skill install" + ) if failures: raise typer.Exit(1) diff --git a/src/solidworks_mcp/skills/__init__.py b/src/solidworks_mcp/skills/__init__.py new file mode 100644 index 00000000..cc93e1ce --- /dev/null +++ b/src/solidworks_mcp/skills/__init__.py @@ -0,0 +1,36 @@ +"""The agent skill shipped with the server, and the instructions sent at connection. + +The skill folder (``solidworks-mcp/``) is plain files inside the package, so +every install has it: the server serves it as ``skill://`` resources, and +``solidworks-mcp skill install`` copies it into an agent's skills folder. +""" + +from __future__ import annotations + +from pathlib import Path + +#: The skill's name, as in its ``SKILL.md`` frontmatter and its folder name. +SKILL_NAME = "solidworks-mcp" + +#: The packaged skill folder. +SKILL_DIR = Path(__file__).parent / SKILL_NAME + +#: Sent in the MCP initialize response: only what every session needs. +SERVER_INSTRUCTIONS = """\ +SolidWorks MCP server: drive SolidWorks parts, assemblies and drawings. + +- Start with `get_model_info` (or `list_open_documents`) to see what is open. +- Build in this order: `create_part`, `create_sketch`, draw (`add_rectangle`, \ +`add_circle`, ...), `exit_sketch`, then the feature (`create_extrusion`, \ +`create_cut_extrude`, `create_revolve`). Features use the sketch you closed last. +- Inputs are millimetres and degrees unless a field says metres. +- A tool's success does not prove the geometry is right: finish a build with \ +`verify_model`, giving the size, volume or counts you expect. +- A tool whose description starts "Not available yet" returns `not_supported`; \ +use the tool it names instead. +- On an error, read `error_type` and `hint`; `rebuild_model` names failing \ +features and `undo` steps back. + +Full guidance (workflows, selection, units, known limits): read the resource \ +skill://solidworks-mcp/SKILL.md. +""" diff --git a/src/solidworks_mcp/skills/install.py b/src/solidworks_mcp/skills/install.py new file mode 100644 index 00000000..4523f951 --- /dev/null +++ b/src/solidworks_mcp/skills/install.py @@ -0,0 +1,107 @@ +"""``solidworks-mcp skill install``: copy the usage skill into an agent's skills folder. + +It writes only ``/solidworks-mcp/``, overwriting the files the +package ships and leaving any other file there alone. It reads nothing but the +packaged skill (no client config, no other skill) and needs no network. +""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +import typer + +from . import SKILL_DIR, SKILL_NAME + +app = typer.Typer( + add_completion=False, + help="Install the SolidWorks MCP usage skill into an agent.", +) + + +def skills_folder(project: bool, directory: Path | None, home: Path, cwd: Path) -> Path: + """Choose the skills folder the skill goes into. + + Args: + project: Use the current project's ``.claude/skills``. + directory: Any other skills folder; wins over ``project``. + home: The user's home folder. + cwd: The current working folder. + + Returns: + Path: The skills folder (the skill goes in a ``solidworks-mcp`` + folder inside it). + """ + if directory is not None: + return directory + return (cwd if project else home) / ".claude" / "skills" + + +def packaged_files() -> list[Path]: + """List the skill's files, relative to the packaged skill folder. + + Returns: + list[Path]: Every file of the skill, sorted. + """ + return sorted(p.relative_to(SKILL_DIR) for p in SKILL_DIR.rglob("*") if p.is_file()) + + +def install_skill(folder: Path, dry_run: bool = False) -> list[Path]: + """Copy the packaged skill into ``folder/solidworks-mcp/``. + + Args: + folder: The skills folder. + dry_run: Report the files without writing them. + + Returns: + list[Path]: The files written (or that would be written). + + Raises: + OSError: When a folder can't be created or a file can't be written. + ValueError: When a target would land outside the skill's folder. + """ + target = folder / SKILL_NAME + root = target.resolve() + written: list[Path] = [] + for relative in packaged_files(): + destination = target / relative + if not destination.resolve().is_relative_to(root): + raise ValueError(f"Refusing to write outside {target}: {destination}") + if not dry_run: + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(SKILL_DIR / relative, destination) + written.append(destination) + return written + + +@app.callback() +def main() -> None: + """Install the SolidWorks MCP usage skill into an agent.""" + + +@app.command() +def install( + project: bool = typer.Option( + False, "--project", help="Install into this project's .claude/skills." + ), + directory: Path | None = typer.Option( + None, + "--dir", + help="Install into this skills folder (for agents other than Claude Code).", + ), + dry_run: bool = typer.Option( + False, "--dry-run", help="Show what would be written, write nothing." + ), +) -> None: + """Copy the skill into ~/.claude/skills (default), a project, or a folder.""" + folder = skills_folder(project, directory, Path.home(), Path.cwd()) + try: + written = install_skill(folder, dry_run=dry_run) + except (OSError, ValueError) as exc: + typer.echo(f"Could not install the skill into {folder / SKILL_NAME}: {exc}") + raise typer.Exit(code=1) from exc + verb = "Would write" if dry_run else "Installed" + typer.echo(f"{verb} the {SKILL_NAME} skill in {folder / SKILL_NAME}:") + for path in written: + typer.echo(f" {path}") diff --git a/src/solidworks_mcp/skills/solidworks-mcp/SKILL.md b/src/solidworks_mcp/skills/solidworks-mcp/SKILL.md new file mode 100644 index 00000000..2f3a15a8 --- /dev/null +++ b/src/solidworks_mcp/skills/solidworks-mcp/SKILL.md @@ -0,0 +1,106 @@ +--- +name: solidworks-mcp +description: How to build and check SolidWorks parts, assemblies and drawings with the SolidWorks MCP server's tools. Use whenever you call its tools (create_part, create_sketch, create_extrusion, verify_model, generate_drawing_package and the rest), plan a CAD model, or recover from one of its errors. +--- + +# Using the SolidWorks MCP server + +These tools drive a real SolidWorks session through its API. Each tool's own +description says what it does and what it needs; this skill covers what +spans tools: the order to call them in, units, choosing faces and edges, +checking the result, and what doesn't work yet. + +## Before anything + +- Call `get_model_info` first. It says which document is active (it follows + a switch the user made in the SolidWorks window). `list_open_documents` + lists every open document; `activate_document` switches. +- Call tools one at a time and wait for each result. The server pauses + after every SolidWorks call on purpose: driving SolidWorks back-to-back + crashes it. + +## The build order + +A solid feature always comes from a closed sketch: + +1. `create_part` (or `open_model` for an existing file). +2. `create_sketch` on a plane: "Front", "Top", "Right", or a plane from + `create_reference_plane`. Don't pass `sketch_name`; the reply gives + SolidWorks' own name for the sketch. +3. Draw: `add_rectangle`, `add_circle`, `add_line`, `add_arc`, `add_polygon`, + and so on. For a revolve, add the axis with `add_centerline` in this + sketch. +4. `exit_sketch`. +5. The feature: `create_extrusion` (add material), `create_cut_extrude` + (remove it), `create_revolve`, `create_sweep`, `create_loft`. + `create_extrusion` and `create_revolve` use the sketch you just closed, + whatever name you pass, so call them straight after `exit_sketch`. + `create_cut_extrude` takes `sketch_name`; pass it. +6. Then edge treatments: `select_face_edges`, then `add_fillet` or + `add_chamfer` with empty `edge_names`. +7. Finish with `verify_model` (see "Check the result"). +8. `set_material`, then `save_part` with a `file_path`. + +Repeat 2-5 for each feature. For holes in a ring or a row, draw every +circle in one sketch (`sketch_circular_pattern`, `sketch_linear_pattern`) +and cut once: `pattern_circular` and `mirror_feature` can't repeat a cut yet. + +Worked sequences for parts, assemblies and drawings: +[references/workflows.md](references/workflows.md). + +## Units + +Inputs are millimetres and degrees, with two exceptions that take metres: +the `"x,y,z"` and `"face:x,y,z"` point specs in `add_fillet` and +`add_chamfer` `edge_names`, and every coordinate and result of +`measure_entities`. When a field's description says metres, believe it. +Details and the model axes: [references/selection-and-units.md](references/selection-and-units.md). + +## Choosing faces and edges + +- To treat the edges of one face, use `select_face_edges` (by `side`, a + `point` in mm, or a `plane`), then `add_fillet` or `add_chamfer` with empty + `edge_names`. Do it right before the fillet: anything that rebuilds or + selects in between (including `calculate_mass_properties`) clears the + selection. +- If the user says "this face" or "these edges", call + `get_selected_entities` to read what they clicked rather than guessing. +- A point on an edge where two faces meet can pick the wrong edge; give a + point inside a face (`"face:x,y,z"`) instead. + +## Check the result + +A tool returning `status: success` means SolidWorks accepted the call, not +that the geometry is what you meant. After a feature: + +- `create_extrusion`, `create_revolve` and `create_cut_extrude` return + `measured_change` (volume and bounding box before and after). A cut whose + volume didn't drop, or an extrusion whose box didn't grow as planned, is + wrong even though it succeeded. +- At the end, call `verify_model` with what you expect: `bounding_box_mm`, + `volume_mm3`, `mass_kg`, `feature_count`, `solid_body_count`, and for an + assembly `component_count`, `mate_count`, `no_interference`. Work out the + expected values before building. A `fail` verdict still has + `status: success`; read `verdict`. +- For one size, `measure_entities` or `get_dimension`. + +## When something fails + +Every error carries `error_type` and a `hint`; follow the hint. The common +ones, and `rebuild_model`, `undo` and what to tell the user: +[references/errors-and-limits.md](references/errors-and-limits.md). + +## What doesn't work yet + +- 39 tools are registered but no adapter implements them: their description + starts "Not available yet" and every call returns `not_supported`. Use the + tool their description names instead. +- Some tools ignore some of their inputs, and a few behave in ways you must + work around. The list, with the workaround for each: + [references/errors-and-limits.md](references/errors-and-limits.md). + +## When to write VBA instead + +Only when no tool does the job (a shell, an option a tool doesn't expose): +[references/vba-fallback.md](references/vba-fallback.md). The `generate_vba_*` +tools return macro text for the user to run; they don't change the model. diff --git a/src/solidworks_mcp/skills/solidworks-mcp/references/errors-and-limits.md b/src/solidworks_mcp/skills/solidworks-mcp/references/errors-and-limits.md new file mode 100644 index 00000000..89b8be5f --- /dev/null +++ b/src/solidworks_mcp/skills/solidworks-mcp/references/errors-and-limits.md @@ -0,0 +1,58 @@ +# Errors and limits + +## Reading an error + +Every failed call returns `status: "error"`, a `message`, an `error_type` +and a `hint`; some carry `data` with details. Act on `error_type`: + +| `error_type` | What to do | +| --- | --- | +| `no_active_document` | Open or create one (`open_model`, `create_part`), or `activate_document`. | +| `no_active_sketch` | `create_sketch` first. | +| `wrong_document_type` | The active document is the wrong kind (a drawing tool on a part); switch with `activate_document`. | +| `not_found` | A name or path is wrong. List what exists (`list_features`, `list_components`, `list_drawing_views`, `list_open_documents`) and retry with the exact name. | +| `feature_not_built` | SolidWorks accepted the call but built nothing: the sketch isn't closed, the cut misses the body, the depth is wrong. Change the input, don't repeat the call. | +| `not_supported` | The tool can't run yet. Use the one its description names. | +| `circuit_open` | Calls are paused after repeated failures. Wait about a minute, fix the cause, then retry. | +| `solidworks_unavailable` | SolidWorks closed or crashed. Tell the user; they restart it. | +| `com_member_not_found`, `com_type_mismatch`, `com_wrong_thread` | A server problem, not a modelling one. Stop and tell the user the tool, the arguments and the message. | + +Don't retry the same call unchanged: if it failed once with the same input, +it will again. + +## Recovering + +- `rebuild_model` rebuilds and, when features fail, returns an error whose + `data.errors` names each failing feature, what it depends on and a `hint`. +- `undo` steps back one or more operations; its reply says whether the + feature tree changed. +- `suppress_feature` turns off a feature without deleting it; + `delete_feature` removes it and its dependents. +- `get_model_info` and `list_features` show where the model stands now. + +## Tools that can't run yet + +39 tools are registered but no adapter implements them. Their description +starts "Not available yet"; every call returns `not_supported`. They are +mostly batch, template, macro-recording and drawing-analysis tools, plus +`add_dimension`, `create_section_view`, `create_detail_view`, +`update_title_block`, `analyze_geometry`, `get_material_properties` and +`batch_export`. Each description names what to use today. + +## Known faults and how to work around them + +| Fault | Workaround | +| --- | --- | +| `create_extrusion` and `create_revolve` ignore `sketch_name` (and the revolve `axis_entity`): they use the sketch closed last. | Call them straight after `exit_sketch` on the right sketch; put the revolve axis in that sketch with `add_centerline`. | +| `create_sketch` echoes a `sketch_name` you pass instead of SolidWorks' name. | Don't pass `sketch_name`; use the name in the reply. | +| `pattern_circular` and `mirror_feature` report an error unless the volume grows, so a pattern or mirror of a cut fails. | Pattern or mirror the sketch entities (`sketch_circular_pattern`, `sketch_linear_pattern`, `sketch_mirror`), then cut once. | +| `create_part` ignores `material`; `create_assembly` ignores `components`; `create_drawing` ignores `model_path` and `sheet_format`. | `set_material` after the first feature; `insert_component` for each part; `create_drawing_view` or `generate_drawing_package` for views. | +| `create_technical_drawing` ignores its template, sheet size, title and output path. | `generate_drawing_package` for a complete drawing. | +| `delete_feature`'s message may not mention the dependents it also removed. | `list_features` afterwards. | +| `save_as` replaces an existing file whatever `overwrite` says. | `save_part` or `save_assembly`, which refuse an existing file unless `overwrite` is on. | +| The `export_*` tools take the format from the file extension, not `format_type`. | Name the file with the right extension. | +| `calculate_mass_properties` labels moments of inertia kg·mm², but they are kg·m². | Read them as kg·m². | +| `check_interference` ignores `include_hidden`. | Show the components you want checked. | +| `execute_macro` runs a macro once, whatever `repeat_count` says, and runs only binary `.swp` files. | Call it again for a repeat. | +| `tutorial_simple_hole` fails at its cut step on real SolidWorks. | Use `create_sketch`, `add_circle`, `exit_sketch` and `create_cut_extrude`. | +| `add_mate` can mate only reference planes and axes, by name. | Model parts so their planes line up with the mates you need. | diff --git a/src/solidworks_mcp/skills/solidworks-mcp/references/selection-and-units.md b/src/solidworks_mcp/skills/solidworks-mcp/references/selection-and-units.md new file mode 100644 index 00000000..ed9c0189 --- /dev/null +++ b/src/solidworks_mcp/skills/solidworks-mcp/references/selection-and-units.md @@ -0,0 +1,67 @@ +# Selection and units + +## Units, tool by tool + +Millimetres and degrees everywhere, except: + +| Takes or returns metres | What | +| --- | --- | +| `add_fillet`, `add_chamfer` | the `"x,y,z"` and `"face:x,y,z"` specs in `edge_names` | +| `measure_entities` | the entity coordinates you pass, and `distance_m`, `length_m`, `area_m2` and the other results | +| `list_model_dimensions` | `system_value` (metres or radians); `value` is in the document's units | + +Points in mm, for contrast: `select_face_edges` `point`, +`create_reference_point` `x`/`y`/`z`, `insert_component` `x`/`y`/`z`, and +the sketch tools. `get_selected_entities` reports in mm (`center_mm`, +`length_mm`). + +`set_dimension` takes millimetres and is for lengths; don't use it on an +angle dimension. `set_units` changes what the document shows, not what the +tools take. + +Mass is in kg, volume in mm³. `calculate_mass_properties` returns moments +of inertia in kg·m², whatever their `units` label says. + +## Planes and model axes + +| Sketch plane | Extrudes along | `select_face_edges` side facing that way | +| --- | --- | --- | +| Front | z | front (+Z) / back (-Z) | +| Top | y | top (+Y) / bottom (-Y) | +| Right | x | right (+X) / left (-X) | + +`verify_model`'s `bounding_box_mm` is [x, y, z] in these axes, so a +100 x 60 rectangle on the Top plane extruded 10 mm is [100, 10, 60]. + +Any plane name `create_reference_plane` returned works in `create_sketch`. + +## Picking faces and edges + +In order of reliability: + +1. **The user's selection.** When they point at something, call + `get_selected_entities` and use what it reports. `add_fillet` and + `add_chamfer` with empty `edge_names` act on the current selection. +2. **`select_face_edges`.** Every edge of one face, by `side` (the furthest + flat face that way), a `point` on the face in mm, or a `plane` the face + lies on. `edge_type: "circular"` picks hole rims, `"linear"` the straight + edges. Then fillet or chamfer with empty `edge_names`. +3. **A face spec.** `"face:x,y,z"` (metres) in `edge_names` treats every edge + of the face containing that point. +4. **An edge point.** `"x,y,z"` (metres) picks the nearest edge. Avoid it + where two faces meet at the point (a hole rim on a flat face): it can + pick a different edge. + +Selection is fragile: anything that selects or rebuilds in between clears +it, including `calculate_mass_properties`, `rebuild_model`, +`measure_entities`, and `verify_model` when it checks `mass_kg`. Select immediately before the tool that uses it. + +`select_feature` selects a feature, sketch, plane or component by name, for +the tools that act on a selection. + +## Sketch entities + +Each sketch tool returns an `id` (e.g. "Line_1"). `add_sketch_constraint`, +`add_sketch_dimension`, `sketch_mirror`, `sketch_offset` and the sketch +patterns take these ids. `check_sketch_fully_defined` says whether the +sketch still has free geometry. diff --git a/src/solidworks_mcp/skills/solidworks-mcp/references/vba-fallback.md b/src/solidworks_mcp/skills/solidworks-mcp/references/vba-fallback.md new file mode 100644 index 00000000..ccbe87f8 --- /dev/null +++ b/src/solidworks_mcp/skills/solidworks-mcp/references/vba-fallback.md @@ -0,0 +1,30 @@ +# When to write VBA instead + +Use a direct tool whenever one exists: it reports what it did, checks the +result, and can be undone. Fall back to VBA only when: + +- no tool makes the feature: a shell (`generate_vba_part_modeling`), or a + mate between faces or edges (`generate_vba_assembly_mates`); +- a tool doesn't expose an option you need: extrusion end conditions beyond + the ones `create_extrusion` takes (`generate_vba_extrusion`), or revolve + options (`generate_vba_revolve`); +- the user wants a macro to keep and run themselves. + +## What the VBA tools do and don't do + +The `generate_vba_*` tools only write macro text. They read nothing from +SolidWorks and change nothing. The macro comes back in `vba_code` with +`usage_instructions`; the user pastes it into the SolidWorks VBA editor +(Alt+F11) and runs it. + +`execute_macro` runs a binary `.swp` macro file that already exists. It +refuses VBA source text, so it can't run what the `generate_vba_*` tools +return. A macro it runs must not wait for the user (no message boxes or +forms), or the call hangs, and its changes can't be undone from here. + +After the user runs a macro, read the model back (`get_model_info`, +`list_features`, `verify_model`) before going on: nothing reports what the +macro did. + +To look up a SolidWorks API call while writing VBA, use +`lookup_api_method`, `lookup_api_interface` or `search_solidworks_api_help`. diff --git a/src/solidworks_mcp/skills/solidworks-mcp/references/workflows.md b/src/solidworks_mcp/skills/solidworks-mcp/references/workflows.md new file mode 100644 index 00000000..01833070 --- /dev/null +++ b/src/solidworks_mcp/skills/solidworks-mcp/references/workflows.md @@ -0,0 +1,96 @@ +# Workflows + +Tool sequences for the common jobs, with the check to make after each +step. Values are examples; sizes are millimetres. + +## A plate with a hole and rounded top edges + +1. `create_part` with `name: "plate"`. +2. `create_sketch` with `plane: "Top"`. +3. `add_rectangle` from (-50, -30) to (50, 30). +4. `exit_sketch`. +5. `create_extrusion` with `depth: 10`. Check `measured_change`: the box + should grow to 100 x 10 x 60 (a Top-plane sketch extrudes along y). +6. `create_sketch` with `plane: "Top"`, then `add_circle` at (0, 0) with + `radius: 5`, then `exit_sketch`. The reply names the sketch, e.g. + "Sketch2". +7. `create_cut_extrude` with `sketch_name: "Sketch2"`, + `end_condition: "ThroughAll"`, `depth: 10`. A sketch under the body is + cut the other way automatically (`direction_flipped: true`). Check that + the volume dropped by about π·5²·10 ≈ 785 mm³. +8. `select_face_edges` with `side: "top"` and `edge_type: "linear"`, then + `add_fillet` with `radius: 2` and `edge_names: []`. +9. `verify_model` with `bounding_box_mm: [100, 10, 60]`, + `solid_body_count: 1`, `feature_count: 3`, and a `volume_mm3` with a + `relative_tolerance` wide enough for the fillets. +10. `set_material` (find the exact name with `list_available_materials`), + then `save_part` with a `file_path` ending in `.sldprt`. + +## Sketching somewhere other than the three planes + +`create_reference_plane` with `reference: "Top Plane"` and `offset: 10` makes +a plane 10 mm up; its name (e.g. "Plane1") goes into `create_sketch`. Angled +planes aren't supported yet. + +## A revolved part (a knob or a shaft) + +1. `create_sketch` on "Front". +2. Draw half the profile with `add_line` (closed), and the axis with + `add_centerline` along it. +3. `exit_sketch`, then `create_revolve` with `angle: 360`. It uses the sketch + you just closed and the centreline in it. + +## A ring of holes + +1. One sketch on the face's plane: `add_circle` for the first hole at its + radius from the origin. +2. `sketch_circular_pattern` with that circle's `id` and `count: 6`. The + centre is always the sketch origin. +3. `exit_sketch`, then one `create_cut_extrude`. + +Don't use `pattern_circular` on a cut: it reports an error unless the +volume grows. + +## Changing an existing model + +1. `open_model`, then `list_features` and `list_model_dimensions`. +2. `set_dimension` with the full name (e.g. "D1@Boss-Extrude1") and the new + value in mm. +3. `rebuild_model`: an error names each failing feature and how to fix it. +4. `verify_model` with the new expected size. + +To turn a feature off without deleting it, `suppress_feature`. After +`delete_feature`, call `list_features`: dependents may have gone too. + +## An assembly + +1. Save each part first (`save_part`): `insert_component` takes a file path. +2. `create_assembly`, then `insert_component` for each part, with a + position in mm. +3. `list_components` for the instance names (e.g. "plate-1"). +4. `add_mate` between components, by reference plane or axis name + ("Front Plane", "Axis1"). Faces and edges can't be mated by these tools. +5. `check_interference`, then `verify_model` with `component_count`, + `mate_count` and `no_interference: true`. +6. `save_assembly`, or `pack_and_go_assembly` for a self-contained folder. + +## A drawing + +The whole drawing in one call: `generate_drawing_package` with the saved +model's `model_path`. It makes an A3 drawing with views, dimensions, title +block and notes, checks nothing overlaps, and saves it with a PDF. + +Step by step instead: + +1. `create_drawing`, then `create_drawing_view` for each view. +2. `insert_model_dimensions`, then `arrange_dimensions` (it reports any + overlap it couldn't clear; `set_sheet_scale` gives more room). +3. `auto_center_marks`, `add_note`, and for an assembly `insert_bom_table`. +4. `save_file` with a `.slddrw` path, and `export_pdf`. + +## Exporting + +`export_step`, `export_stl` and the other `export_*` tools write the active +document; the file extension decides the format, so name it to match. +`export_image` saves a picture in a chosen view: useful to show the user the +result. diff --git a/src/solidworks_mcp/tools/automation.py b/src/solidworks_mcp/tools/automation.py index febf4b2e..0317c4b1 100644 --- a/src/solidworks_mcp/tools/automation.py +++ b/src/solidworks_mcp/tools/automation.py @@ -430,8 +430,8 @@ async def batch_process_files(input_data: BatchProcessInput) -> dict[str, Any]: Not available yet: no adapter implements it, so every call returns an error with `error_type: "not_supported"`. It is planned for a future release. To export - several files today, use `batch_export`. Changes: once available, opens, changes - and saves each file. Returns: once available, a per-file result under `data`. + several files today, open each with `open_model` and use `export_step`. + Changes: once available, opens, changes and saves each file. Returns: once available, a per-file result under `data`. Args: input_data: The batch. `source_directory` is required; `operation_type`, diff --git a/src/solidworks_mcp/tools/vba_generation.py b/src/solidworks_mcp/tools/vba_generation.py index 369754ef..e460a37f 100644 --- a/src/solidworks_mcp/tools/vba_generation.py +++ b/src/solidworks_mcp/tools/vba_generation.py @@ -600,7 +600,7 @@ async def generate_vba_drawing_views(input_data: VBADrawingInput) -> dict[str, A async def generate_vba_batch_export(input_data: VBABatchInput) -> dict[str, Any]: """Write a VBA macro that opens every matching file in a folder and saves a STEP copy of each to another folder. - To export files directly, use `batch_export`. The macro comes back as text for + To export one file directly, use `export_step`. The macro comes back as text for the SolidWorks VBA editor (Alt+F11); `execute_macro` runs only binary .swp files, so it can't run this directly. Read-only. Nothing runs in SolidWorks. Returns: `vba_code`, the `parameters` used and `usage_instructions`. diff --git a/tests/integration/test_skill_content.py b/tests/integration/test_skill_content.py new file mode 100644 index 00000000..4026b31d --- /dev/null +++ b/tests/integration/test_skill_content.py @@ -0,0 +1,157 @@ +"""The agent skill and the server instructions name only real tools (spec: agents/usage-skill). + +Every backticked snake_case name in the skill files and the instructions must +be a registered tool, an input field of one, an ``error_type``, or one of the +response keys listed below. A tool renamed or removed therefore fails here, +naming the file. A tool that can't run yet (its description starts "Not +available yet") may be named only in ``references/errors-and-limits.md``, +which lists them as such; nowhere else may the skill send an agent to one. +""" + +from __future__ import annotations + +import re +from typing import Any + +import pytest +from fastmcp import FastMCP + +from solidworks_mcp.skills import SERVER_INSTRUCTIONS, SKILL_DIR, SKILL_NAME +from solidworks_mcp.tools import register_tools +from solidworks_mcp.tools.error_classifier import ( + DEFAULT_RULES, + INTERNAL, + INVALID_INPUT, + NOT_SUPPORTED, + OPERATION_FAILED, +) + +_LIMITS_FILE = "references/errors-and-limits.md" +_NAME = re.compile(r"`([a-z][a-z0-9]*(?:_[a-z0-9]+)+)`") +#: Response keys the skill quotes; they are not input fields or tools. +_RESPONSE_KEYS = { + "area_m2", + "center_mm", + "direction_flipped", + "distance_m", + "error_type", + "length_m", + "length_mm", + "measured_change", + "system_value", + "usage_instructions", + "vba_code", +} + +_SKILL_FILES = sorted( + p.relative_to(SKILL_DIR).as_posix() for p in SKILL_DIR.rglob("*.md") +) + + +def _field_names(schema: dict[str, Any]) -> set[str]: + names: set[str] = set() + + def walk(node: Any) -> None: + if isinstance(node, dict): + names.update(node.get("properties", {})) + for value in node.values(): + walk(value) + elif isinstance(node, list): + for value in node: + walk(value) + + walk(schema) + return names + + +@pytest.fixture(scope="module") +def tools_by_name() -> dict[str, Any]: + import asyncio + + from solidworks_mcp.adapters.mock_adapter import MockSolidWorksAdapter + + async def collect() -> dict[str, Any]: + mcp = FastMCP("skill-content") + await register_tools(mcp, MockSolidWorksAdapter({}), {}) + return {t.name: t for t in await mcp.list_tools()} + + return asyncio.run(collect()) + + +@pytest.fixture(scope="module") +def known_names(tools_by_name: dict[str, Any]) -> set[str]: + fields: set[str] = set() + for tool in tools_by_name.values(): + fields |= _field_names(tool.parameters or {}) + error_types = {rule.error_type for rule in DEFAULT_RULES} | { + INTERNAL, + INVALID_INPUT, + NOT_SUPPORTED, + OPERATION_FAILED, + } + return set(tools_by_name) | fields | error_types | _RESPONSE_KEYS + + +def _unknown(text: str, known: set[str]) -> list[str]: + return sorted({n for n in _NAME.findall(text) if n not in known}) + + +def test_the_skill_files_are_found() -> None: + assert "SKILL.md" in _SKILL_FILES + assert _LIMITS_FILE in _SKILL_FILES + + +@pytest.mark.parametrize("name", _SKILL_FILES) +def test_every_name_in_the_skill_is_real(name: str, known_names: set[str]) -> None: + text = (SKILL_DIR / name).read_text(encoding="utf-8") + + assert _unknown(text, known_names) == [], name + + +def test_every_name_in_the_instructions_is_real(known_names: set[str]) -> None: + assert _unknown(SERVER_INSTRUCTIONS, known_names) == [] + + +@pytest.mark.parametrize("name", [n for n in _SKILL_FILES if n != _LIMITS_FILE]) +def test_the_skill_sends_agents_only_to_tools_that_run( + name: str, tools_by_name: dict[str, Any] +) -> None: + unavailable = { + tool_name + for tool_name, tool in tools_by_name.items() + if "Not available yet" in (tool.description or "") + } + text = (SKILL_DIR / name).read_text(encoding="utf-8") + + assert sorted(set(_NAME.findall(text)) & unavailable) == [], name + + +def test_the_instructions_send_agents_only_to_tools_that_run( + tools_by_name: dict[str, Any], +) -> None: + named = set(_NAME.findall(SERVER_INSTRUCTIONS)) + + assert all( + "Not available yet" not in (tools_by_name[n].description or "") + for n in named & set(tools_by_name) + ) + + +def test_skill_md_has_frontmatter_and_stays_short() -> None: + text = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8") + front = re.match(r"---\n(.*?)\n---\n", text, re.DOTALL) + + assert front is not None + assert f"name: {SKILL_NAME}" in front.group(1) + assert re.search(r"^description: \S.{40,}", front.group(1), re.MULTILINE) + assert len(text.splitlines()) < 500 + + +def test_the_instructions_stay_short() -> None: + assert len(SERVER_INSTRUCTIONS) <= 1500 + + +def test_the_unknown_name_check_reports_a_missing_tool() -> None: + assert _unknown("Call `create_widget` then `create_part`.", {"create_part"}) == [ + "create_widget" + ] diff --git a/tests/solidworks_mcp/setup/test_cli.py b/tests/solidworks_mcp/setup/test_cli.py index 0b1356f6..0ccbc1dd 100644 --- a/tests/solidworks_mcp/setup/test_cli.py +++ b/tests/solidworks_mcp/setup/test_cli.py @@ -198,6 +198,13 @@ def test_a_failed_check_writes_nothing( assert "solidworks" not in _servers(machine["desktop"]) +def test_setup_ends_with_the_skill_install_hint(machine: dict[str, Any]) -> None: + result = _run("--yes") + + assert result.exit_code == 0, result.output + assert "solidworks-mcp skill install" in result.output + + def test_skip_check(machine: dict[str, Any]) -> None: result = _run("--yes", "--skip-check") diff --git a/tests/solidworks_mcp/skills/__init__.py b/tests/solidworks_mcp/skills/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/solidworks_mcp/skills/test_init.py b/tests/solidworks_mcp/skills/test_init.py new file mode 100644 index 00000000..49df6357 --- /dev/null +++ b/tests/solidworks_mcp/skills/test_init.py @@ -0,0 +1,21 @@ +"""The packaged skill folder and the server instructions (``solidworks_mcp.skills``).""" + +from __future__ import annotations + +from solidworks_mcp.skills import SERVER_INSTRUCTIONS, SKILL_DIR, SKILL_NAME + + +def test_the_skill_folder_ships_with_the_package() -> None: + assert SKILL_DIR.name == SKILL_NAME == "solidworks-mcp" + assert (SKILL_DIR / "SKILL.md").is_file() + references = sorted(p.name for p in (SKILL_DIR / "references").glob("*.md")) + assert references == [ + "errors-and-limits.md", + "selection-and-units.md", + "vba-fallback.md", + "workflows.md", + ] + + +def test_the_instructions_point_to_the_skill_resource() -> None: + assert f"skill://{SKILL_NAME}/SKILL.md" in SERVER_INSTRUCTIONS diff --git a/tests/solidworks_mcp/skills/test_install.py b/tests/solidworks_mcp/skills/test_install.py new file mode 100644 index 00000000..d367bf8b --- /dev/null +++ b/tests/solidworks_mcp/skills/test_install.py @@ -0,0 +1,138 @@ +"""``solidworks-mcp skill install`` (spec: agents/usage-skill, the install requirement).""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from solidworks_mcp.skills import SKILL_DIR, SKILL_NAME +from solidworks_mcp.skills.install import ( + app, + install_skill, + packaged_files, + skills_folder, +) + +runner = CliRunner() + + +@pytest.fixture +def places(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Path]: + """A fake home and project folder, so nothing real is written.""" + home, project = tmp_path / "home", tmp_path / "project" + home.mkdir() + project.mkdir() + monkeypatch.setattr(Path, "home", classmethod(lambda cls: home)) + monkeypatch.chdir(project) + return {"home": home, "project": project} + + +def _same_as_packaged(target: Path) -> bool: + return all( + (target / rel).read_bytes() == (SKILL_DIR / rel).read_bytes() + for rel in packaged_files() + ) + + +def test_the_default_target_is_claude_codes_user_folder( + places: dict[str, Path], +) -> None: + result = runner.invoke(app, ["install"]) + + target = places["home"] / ".claude" / "skills" / SKILL_NAME + assert result.exit_code == 0, result.output + assert _same_as_packaged(target) + assert str(target) in result.output.replace("\n", "") + + +def test_project_installs_into_this_projects_folder(places: dict[str, Path]) -> None: + result = runner.invoke(app, ["install", "--project"]) + + assert result.exit_code == 0, result.output + assert _same_as_packaged(places["project"] / ".claude" / "skills" / SKILL_NAME) + assert not (places["home"] / ".claude").exists() + + +def test_dir_installs_into_any_skills_folder( + places: dict[str, Path], tmp_path: Path +) -> None: + other = tmp_path / "codex" / "skills" + + result = runner.invoke(app, ["install", "--dir", str(other)]) + + assert result.exit_code == 0, result.output + assert _same_as_packaged(other / SKILL_NAME) + + +def test_an_upgrade_replaces_the_skill_and_keeps_the_users_files( + places: dict[str, Path], +) -> None: + target = places["home"] / ".claude" / "skills" / SKILL_NAME + (target / "references").mkdir(parents=True) + (target / "SKILL.md").write_text("old", encoding="utf-8") + (target / "my-notes.md").write_text("mine", encoding="utf-8") + + result = runner.invoke(app, ["install"]) + + assert result.exit_code == 0, result.output + assert _same_as_packaged(target) + assert (target / "my-notes.md").read_text(encoding="utf-8") == "mine" + + +def test_dry_run_writes_nothing(places: dict[str, Path]) -> None: + result = runner.invoke(app, ["install", "--dry-run"]) + + assert result.exit_code == 0, result.output + assert "Would write" in result.output + assert not (places["home"] / ".claude").exists() + + +def test_an_unwritable_target_is_an_error_naming_the_path( + places: dict[str, Path], tmp_path: Path +) -> None: + blocker = tmp_path / "not-a-folder" + blocker.write_text("a file where the skills folder should be", encoding="utf-8") + + result = runner.invoke(app, ["install", "--dir", str(blocker)]) + + assert result.exit_code == 1 + assert "Could not install the skill" in result.output + assert str(blocker) in result.output.replace("\n", "") + + +def test_a_target_outside_the_skill_folder_is_refused( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + escape = tmp_path / "elsewhere" + escape.mkdir() + folder = tmp_path / "skills" + (folder / SKILL_NAME).mkdir(parents=True) + real_resolve = Path.resolve + + def resolve(self: Path, strict: bool = False) -> Path: + if self.name == "SKILL.md": + return escape / "SKILL.md" + return real_resolve(self, strict) + + monkeypatch.setattr(Path, "resolve", resolve) + + with pytest.raises(ValueError, match="Refusing to write outside"): + install_skill(folder) + assert not (escape / "SKILL.md").exists() + + +def test_skills_folder_prefers_dir_then_project_then_home(tmp_path: Path) -> None: + home, cwd, other = tmp_path / "h", tmp_path / "c", tmp_path / "o" + + assert skills_folder(False, None, home, cwd) == home / ".claude" / "skills" + assert skills_folder(True, None, home, cwd) == cwd / ".claude" / "skills" + assert skills_folder(True, other, home, cwd) == other + + +def test_the_packaged_files_are_the_skill_and_its_references() -> None: + names = {p.as_posix() for p in packaged_files()} + + assert "SKILL.md" in names + assert "references/workflows.md" in names diff --git a/tests/solidworks_mcp/test_server.py b/tests/solidworks_mcp/test_server.py index 92f13c19..790a1f4b 100644 --- a/tests/solidworks_mcp/test_server.py +++ b/tests/solidworks_mcp/test_server.py @@ -1042,6 +1042,26 @@ def test_cli_main_routes_setup_to_the_setup_app(): mock_run.assert_not_called() +def test_cli_main_routes_skill_to_the_skill_app(): + """`solidworks-mcp skill ...` installs the skill, not the server.""" + from solidworks_mcp.server import cli_main + + with ( + patch( + "solidworks_mcp.server.sys.argv", + ["solidworks-mcp", "skill", "install", "--dry-run"], + ), + patch("solidworks_mcp.skills.install.app") as skill_app, + patch("solidworks_mcp.server.typer.run") as mock_run, + ): + cli_main() + + skill_app.assert_called_once_with( + args=["install", "--dry-run"], prog_name="solidworks-mcp skill" + ) + mock_run.assert_not_called() + + def test_cli_help_lists_each_option_without_internal_text(): """`--help` shows the summary and one line per option, not the Args section.""" import click diff --git a/tests/solidworks_mcp/test_server_skill.py b/tests/solidworks_mcp/test_server_skill.py new file mode 100644 index 00000000..781728f9 --- /dev/null +++ b/tests/solidworks_mcp/test_server_skill.py @@ -0,0 +1,61 @@ +"""The server sends its instructions and serves the usage skill (specs: agents/*).""" + +from __future__ import annotations + +import json + +import pytest +from fastmcp import Client + +from solidworks_mcp.config import SolidWorksMCPConfig +from solidworks_mcp.server import SolidWorksMCPServer +from solidworks_mcp.skills import SERVER_INSTRUCTIONS, SKILL_DIR + +_SKILL = "skill://solidworks-mcp" + + +@pytest.mark.asyncio +async def test_a_client_gets_the_instructions_at_connection( + mock_config: SolidWorksMCPConfig, +) -> None: + server = SolidWorksMCPServer(mock_config) + + async with Client(server.mcp) as client: + result = client.initialize_result + + assert result is not None + assert result.instructions == SERVER_INSTRUCTIONS + + +@pytest.mark.asyncio +async def test_the_skill_is_listed_and_reads_back_the_packaged_text( + mock_config: SolidWorksMCPConfig, +) -> None: + server = SolidWorksMCPServer(mock_config) + + async with Client(server.mcp) as client: + uris = {str(r.uri) for r in await client.list_resources()} + skill = await client.read_resource(f"{_SKILL}/SKILL.md") + manifest = await client.read_resource(f"{_SKILL}/_manifest") + + assert {f"{_SKILL}/SKILL.md", f"{_SKILL}/_manifest"} <= uris + assert skill[0].text == (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8") + listed = {entry["path"] for entry in json.loads(manifest[0].text)["files"]} + assert "references/workflows.md" in listed + + +@pytest.mark.asyncio +async def test_a_reference_file_is_read_through_the_skill_template( + mock_config: SolidWorksMCPConfig, +) -> None: + server = SolidWorksMCPServer(mock_config) + expected = (SKILL_DIR / "references" / "errors-and-limits.md").read_text( + encoding="utf-8" + ) + + async with Client(server.mcp) as client: + content = await client.read_resource( + f"{_SKILL}/references/errors-and-limits.md" + ) + + assert content[0].text == expected From 4319567549ce4e96d3228ed633d4f0e2be997053 Mon Sep 17 00:00:00 2001 From: Andrew Bartels Date: Wed, 4 Nov 2026 22:22:33 -0800 Subject: [PATCH 4/4] docs(skills): README and docs page for the agent skill; queue note and changelog (#175) --- CHANGELOG.md | 9 ++++ README.md | 12 +++++ docs/agents/agent-skill.md | 51 +++++++++++++++++++ docs/agents/com-api-pitfalls.md | 5 ++ mkdocs.yml | 1 + .../agent-skill-and-instructions/design.md | 4 +- .../agent-skill-and-instructions/tasks.md | 10 ++-- openspec/queue.md | 4 ++ 8 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 docs/agents/agent-skill.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e407fa..2a7a0a72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **An agent skill for using the server** (#175). `solidworks-mcp skill + install` puts the `solidworks-mcp` skill in Claude Code's skills folder + (`--project` or `--dir` for others); it covers the build order, units + (and the inputs that take metres), choosing faces and edges, checking + results with `verify_model`, recovering from errors, the tools that don't + work yet and the known faults. The server also serves it as + `skill://solidworks-mcp/SKILL.md` and sends short usage instructions when + a client connects. A test fails if the skill names a tool that is gone or + recommends one that can't run. - **The server takes `--year`**, the installed SolidWorks release it works with (`uvx solidworks-mcp-python --year 2026`). It is required for real SolidWorks and `setup` writes it into every client entry, from the newest diff --git a/README.md b/README.md index 4bccbc7a..eb4bbf9c 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,18 @@ It works only with the circuit-breaker adapter wrapper (the default for real Sol +## Teach your agent the workflow + +Each tool's description says what that tool does. The `solidworks-mcp` skill covers what spans tools: the order to build in (sketch, close it, then the feature), which inputs take millimetres and which take metres, how to pick faces and edges, checking the result with `verify_model`, recovering from errors, and the tools that don't work yet. Install it into Claude Code with: + +```powershell +uvx solidworks-mcp-python skill install +``` + +That writes `~/.claude/skills/solidworks-mcp/`. Add `--project` for the current project's `.claude/skills`, or `--dir ` for another agent's skills folder. It writes only that folder and needs no login. Re-run it after an upgrade. + +Clients that read MCP resources don't need the install: the server serves the same skill at `skill://solidworks-mcp/SKILL.md`, and points to it in the instructions it sends when a client connects. More: [Agent skill](docs/agents/agent-skill.md). + ## Contributing `install.ps1` gives you an editable install, so code changes take effect the next time a client starts the server. For the development tools, install the extras: diff --git a/docs/agents/agent-skill.md b/docs/agents/agent-skill.md new file mode 100644 index 00000000..08149e53 --- /dev/null +++ b/docs/agents/agent-skill.md @@ -0,0 +1,51 @@ +# Agent Skill + +The server ships a skill, `solidworks-mcp`, for agents that use its tools. +Each tool's own description says what that tool does +([Tool Description Standard](tool-description-standard.md)); the skill +covers what spans tools: + +- the order to build in: create or open a document, sketch, close the + sketch, then the feature, and `verify_model` at the end; +- units: inputs are millimetres and degrees, except the few that take metres; +- choosing planes, faces and edges, and reading what the user selected; +- checking results, since a tool's success doesn't prove the geometry; +- reading errors and recovering with `rebuild_model` and `undo`; +- the tools that can't run yet and the known faults, with workarounds; +- when to write VBA instead. + +The files are `SKILL.md` and four references in +`src/solidworks_mcp/skills/solidworks-mcp/`, installed with the package. + +## Three ways an agent gets it + +| Way | What the agent sees | Needs | +| --- | --- | --- | +| Server instructions | A short text in the MCP initialize response: start with `get_model_info`, the build order, the unit rule, `verify_model`, and where the skill is | Nothing; every client that shows instructions gets it | +| MCP resources | `skill://solidworks-mcp/SKILL.md`, a `skill://solidworks-mcp/_manifest` listing its files, and each reference file | A client that reads resources | +| Installed skill | The skill in the agent's skills folder, loaded when relevant | `solidworks-mcp skill install` | + +## Installing it + +```powershell +uvx solidworks-mcp-python skill install # ~/.claude/skills/solidworks-mcp +uvx solidworks-mcp-python skill install --project # .claude/skills in this folder +uvx solidworks-mcp-python skill install --dir C:\path\to\skills # another agent +uvx solidworks-mcp-python skill install --dry-run # show, write nothing +``` + +The command copies the packaged files into `/solidworks-mcp/`, +replacing older copies of them and leaving any other file there alone. It +reads nothing but the packaged skill (no client config, no other skill) and +needs no network or login. Re-run it after upgrading the server. + +## Keeping it true + +`tests/integration/test_skill_content.py` fails when the skill or the +instructions name a tool that isn't registered, or send an agent to a tool +that can't run yet (those may be named only in the limits reference, which +lists them). When a fault in `openspec/queue.md` item 10 is fixed, remove +it from `references/errors-and-limits.md` in the same change. + +For changing the server's COM code, the contributor reference is +[COM API Pitfalls](com-api-pitfalls.md). diff --git a/docs/agents/com-api-pitfalls.md b/docs/agents/com-api-pitfalls.md index 68c6124b..f7211849 100644 --- a/docs/agents/com-api-pitfalls.md +++ b/docs/agents/com-api-pitfalls.md @@ -10,6 +10,11 @@ Every entry below caused a real runtime failure that required hours to diagnose. They are the delta between "what the documentation says" and "what actually works" under pywin32 late-binding on SW 2025/2026. +!!! note "Using the server, not changing it?" + This page is for changing the COM code. For how an agent should *use* the + tools (build order, units, selection, known limits), see the + [Agent Skill](agent-skill.md). + --- ## 1. `SelectByID2` — `Callout` must be `VT_DISPATCH` null, not `None` diff --git a/mkdocs.yml b/mkdocs.yml index 2c3974ca..a360be71 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -68,6 +68,7 @@ nav: - Worked Examples: agents/worked-examples.md - Prompt-Driven Design: agents/prompt-driven-design.md - Tool Description Standard: agents/tool-description-standard.md + - Agent Skill: agents/agent-skill.md - Prompting Tutorials: agents/prompting-tutorials.md - COM API Pitfalls (LLM Agent Guide): agents/com-api-pitfalls.md - Planning: diff --git a/openspec/changes/agent-skill-and-instructions/design.md b/openspec/changes/agent-skill-and-instructions/design.md index 27a8b268..a8a2c489 100644 --- a/openspec/changes/agent-skill-and-instructions/design.md +++ b/openspec/changes/agent-skill-and-instructions/design.md @@ -79,8 +79,8 @@ the spec) and points to the skill. Alternative, generating it from ### `solidworks-mcp skill install` as its own small Typer app `cli_main` routes `skill` the way it routes `setup`. The command takes -`--project`, `--dir PATH` and `--dry-run`, and copies the packaged files with -`importlib.resources`, overwriting only files it ships. It never deletes, +`--project`, `--dir PATH` and `--dry-run`, and copies the packaged files from +the package folder, overwriting only files it ships. It never deletes, never follows the folder outside the target, and reads nothing but the packaged skill, so it can't touch other users' or tools' files. It needs no network or token. diff --git a/openspec/changes/agent-skill-and-instructions/tasks.md b/openspec/changes/agent-skill-and-instructions/tasks.md index 79b60d8a..cb0a4d67 100644 --- a/openspec/changes/agent-skill-and-instructions/tasks.md +++ b/openspec/changes/agent-skill-and-instructions/tasks.md @@ -1,7 +1,7 @@ ## 1. The skill package - [x] 1.1 `src/solidworks_mcp/skills/__init__.py`: `SKILL_DIR` (the packaged - skill folder, via `importlib.resources`) and `SERVER_INSTRUCTIONS`; + skill folder, beside the module) and `SERVER_INSTRUCTIONS`; tests in `tests/solidworks_mcp/skills/test_init.py` - [x] 1.2 Write `SKILL.md` (frontmatter `name: solidworks-mcp` and a description; build order, units, read-back, map to the references), @@ -37,17 +37,17 @@ ## 4. Docs -- [ ] 4.1 README: a short "Agent skill" section with the install command and +- [x] 4.1 README: a short "Agent skill" section with the install command and how other agents use `--dir` -- [ ] 4.2 `docs/agents/`: a page on the skill and the instructions, linked +- [x] 4.2 `docs/agents/`: a page on the skill and the instructions, linked from mkdocs nav; link the skill from `com-api-pitfalls.md` as the user-facing counterpart -- [ ] 4.3 `openspec/queue.md`: item 10 notes that fixing a fault also edits +- [x] 4.3 `openspec/queue.md`: item 10 notes that fixing a fault also edits `references/errors-and-limits.md`; CHANGELOG entry ## 5. Ship -- [ ] 5.1 Full mock suite, ruff, 100 % coverage, `mkdocs build --strict` +- [x] 5.1 Full mock suite, ruff, 100 % coverage, `mkdocs build --strict` - [ ] 5.2 Ask the user, then run the measurement on real SolidWorks: the chosen demo prompt in a fresh agent session without the skill, then with it installed; record tool calls, errors and whether diff --git a/openspec/queue.md b/openspec/queue.md index 417403c1..5ba26e4c 100644 --- a/openspec/queue.md +++ b/openspec/queue.md @@ -51,6 +51,10 @@ Found while rewriting the descriptions (39 in all). ## Small faults (item 10) +When one is fixed, remove it from the agent skill's +`src/solidworks_mcp/skills/solidworks-mcp/references/errors-and-limits.md` +in the same change. + - `execute_macro` ignores `repeat_count`: it runs the macro once, but its message says "executed N times". - `create_technical_drawing` ignores `template`, `sheet_size`, `title` and