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/.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..a8a2c489 --- /dev/null +++ b/openspec/changes/agent-skill-and-instructions/design.md @@ -0,0 +1,126 @@ +## 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))` 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 +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 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. + +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..cb0a4d67 --- /dev/null +++ b/openspec/changes/agent-skill-and-instructions/tasks.md @@ -0,0 +1,55 @@ +## 1. The skill package + +- [x] 1.1 `src/solidworks_mcp/skills/__init__.py`: `SKILL_DIR` (the packaged + 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), + under 500 lines +- [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 +- [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 + +- [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` + +- [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 +- [x] 3.2 `cli_main` routes `skill ...` to it, as it does `setup`; `setup` + ends with a one-line hint about `skill install` +- [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 + +## 4. Docs + +- [x] 4.1 README: a short "Agent skill" section with the install command and + how other agents use `--dir` +- [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 +- [x] 4.3 `openspec/queue.md`: item 10 notes that fixing a fault also edits + `references/errors-and-limits.md`; CHANGELOG entry + +## 5. Ship + +- [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 + `verify_model` passed, in the PR and in `docs/agents/` (paced runs, + `SW_TEST_PACE_SECONDS=4`) 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/queue.md b/openspec/queue.md index 93482641..5ba26e4c 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 | | @@ -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 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 + 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