Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **`reset_circuit_breaker`** (#208). Re-opens operations the server paused
after repeated failures, all of them or one by name; the `circuit_open`
hint now names it.

- **The U-joint demo** (#175, #207). SolidWorks' U-Joint sample, written up
as a prompt and built by Claude Code (Opus 5.5, medium effort) in 294
tool calls, is the top demo in the README and on the Demos page, beside
Expand Down Expand Up @@ -58,6 +62,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **One failed call could pause a tool for good** (#208). The server's router
fell back to the VBA adapter, which called back into the router, so one
failed `create_extrusion` counted as many failures and opened the circuit
breaker at once; every retry did the same. The fallback is gone (it only
re-ran the same COM call).
- **`check_interference` repeated an old result** (#209). Its result was
cached for 60 seconds while components moved; it is no longer cached.

- **The live chamfer and fillet tests treat the faces they name** (#199).
They assumed the test block's height ran along Z; it runs along Y, so they
chamfered a side face where they meant the face with the hole, and their
Expand Down
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,13 @@ updated 2026-04-24.

### 4. `Circuit breaker is open for <tool>`

- **Cause:** server-side resilience library trips after N failures in a
window. Subsequent calls fail fast even when the underlying issue is fixed.
- **Fix:** wait for breaker timeout (~30–60s) or restart the server.
- **Cause:** the server's circuit breaker pauses an operation after 5
failures. Subsequent calls fail fast even when the underlying issue is
fixed. Before #208 a single failure could count many times: the server's
router fell back to the VBA adapter, which re-entered the router.
- **Fix:** call the `reset_circuit_breaker` tool (all operations, or one by
name), or wait for the timeout (`SOLIDWORKS_MCP_CIRCUIT_BREAKER_TIMEOUT`,
default 60 s), or restart the server.

### 5. COM apartment / threading mismatch — FastMCP async workers

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
[![SolidWorks](https://img.shields.io/badge/tested%20on-SolidWorks%202026-red)](https://www.solidworks.com/)
[![Coverage](https://codecov.io/gh/andrewbartels1/SolidworksMCP-python/branch/main/graph/badge.svg)](https://codecov.io/gh/andrewbartels1/SolidworksMCP-python)

Python MCP server for SolidWorks automation with 154 tools, plus an optional agent/prompt-testing layer for AI-assisted workflows.
Python MCP server for SolidWorks automation with 155 tools, plus an optional agent/prompt-testing layer for AI-assisted workflows.

## See it work

Expand Down Expand Up @@ -276,7 +276,7 @@ Add this to `%APPDATA%\Code\User\mcp.json`:
}
```

Run **MCP: List Servers** from the Command Palette and start `solidworks`. Its output should end with `Discovered 154 tools` (the count grows as tools are added).
Run **MCP: List Servers** from the Command Palette and start `solidworks`. Its output should end with `Discovered 155 tools` (the count grows as tools are added).

</details>

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Nothing in the install or setup signs in to anything. The only download is the P
Open SolidWorks, restart your client as setup told you, and ask *"What model is open in SolidWorks?"*. The server's log (VS Code shows it under the server's output; Claude Desktop writes `%LOCALAPPDATA%\Claude\Logs\mcp-server-solidworks.log`) includes:

- `SolidWorks COM interface is available`
- `Registered 154 SolidWorks tools` (the count grows as tools are added)
- `Registered 155 SolidWorks tools` (the count grows as tools are added)
- `Connected to SolidWorks`

VS Code labels every line the server writes to stderr as `[warning]`. That is where its normal log goes, since stdout carries the MCP messages, so those labels aren't problems.
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Run `uvx solidworks-mcp-python setup` again any time to add a client. To work on
Open SolidWorks, restart your client as setup told you, and ask *"What model is open in SolidWorks?"*. The server's log includes:

- `SolidWorks COM interface is available`
- `Registered 154 SolidWorks tools` (the count grows as tools are added)
- `Registered 155 SolidWorks tools` (the count grows as tools are added)
- `Connected to SolidWorks`

If it says `Could not connect to SolidWorks`, SolidWorks wasn't open when the server started. Open it and restart the server from your client.
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/vscode-mcp-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Open SolidWorks, then run **MCP: List Servers** and start (or restart) `solidwor

- `SolidWorks COM interface is available`
- `Connected to SolidWorks`
- `Discovered 154 tools` (the count grows as tools are added)
- `Discovered 155 tools` (the count grows as tools are added)

VS Code labels every line the server writes to stderr as `[warning]`. That is the server's normal log, so those labels aren't problems.

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ cd SolidworksMCP-python
Open SolidWorks, restart your client, and ask *"What model is open in SolidWorks?"*. Healthy server logs include:

- `SolidWorks COM interface is available`
- `Registered 154 SolidWorks tools` (the count grows as tools are added)
- `Registered 155 SolidWorks tools` (the count grows as tools are added)
- `Connected to SolidWorks`

### Linux / WSL only
Expand Down
4 changes: 2 additions & 2 deletions docs/user-guide/tools-overview.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tools Overview

The SolidWorks MCP Server provides 154 specialized tools for CAD automation. This page is a navigation map, not a tutorial.
The SolidWorks MCP Server provides 155 specialized tools for CAD automation. This page is a navigation map, not a tutorial.

## Read this first

Expand Down Expand Up @@ -148,7 +148,7 @@ The SolidWorks MCP Server provides 154 specialized tools for CAD automation. Thi
| **Macro Recording** | 7 | High | Workflow capture and optimization |
| **Docs Discovery** | 2 | Medium | API and COM discovery workflows |

**Total: 154 Tools**
**Total: 155 Tools**

## Tool Design Principles

Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/circuit-breaker-recovery/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-11-06
28 changes: 28 additions & 0 deletions openspec/changes/circuit-breaker-recovery/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Context

`factory.create_adapter` wraps the COM adapter in `CircuitBreakerAdapter` (per-operation buckets, threshold 5, timeout 60 s from `SOLIDWORKS_MCP_CIRCUIT_BREAKER_TIMEOUT`), then in `ConnectionPoolAdapter`, whose `adapter_factory` returns that same wrapped instance, so every pool slot shares one breaker. On an exception the pool retries up to `max_retries`, disconnects the "failed" adapter and asks the factory for a replacement (the same object). No server log survived the failing session.

## Goals / Non-Goals

**Goals:** find and fix the lock-out with a test that reproduces it; a reset tool; clearer `circuit_open` errors.
**Non-Goals:** redesigning the pool (a later change may remove it, since one COM apartment can't use several connections).

## Decisions

- **Reproduce first.** Build the production stack around a fake adapter that fails in the ways `create_extrusion` can (exception, error result, slow call) and drive the session's sequence. Candidate causes to test: a pool retry recording several failures for one call; the pool disconnecting the shared adapter; a refused call resetting `last_failure_time`; `half_open_calls` never reset. The fix follows what the test shows; the spec's scenarios are the acceptance tests.
- **The reset tool lives beside the other server tools**, uses `require_capability(adapter, "reset_circuit_breakers")`, and is forwarded by the pool. The breaker gains `reset(operation=None) -> list[str]`.
- **Hint text** comes from the error classifier's `circuit_open` rule: it names `reset_circuit_breaker` and `SOLIDWORKS_MCP_CIRCUIT_BREAKER_TIMEOUT` (the seconds left aren't known to the classifier).

## Risks / Trade-offs

- [The reset tool lets an agent hammer a failing operation] → the result says what was open and the agent skill says to fix the cause first.

## Cause (found 2026-11-06)

The server log of the failing session shows one `create_extrusion` failure, then "Circuit breaker opened after 5 failures" in the same second, with a traceback cycling `server._routed_call` → `IntelligentRouter.execute` → `VbaGeneratorAdapter.create_extrusion` → `server._routed_call`. `_instrument_adapter_methods` replaces methods on the adapter instance, and the server's `VbaGeneratorAdapter` was built on that same instance, so the router's "VBA fallback" re-entered the routed method: each COM failure fell back, failed again and recursed, counting one failure many times. Every later attempt (the sketch was already used, so it really failed) did the same, which is why the breaker looked like it never recovered. Fix: the server no longer passes a VBA route (it only re-ran the same COM call). The pool is not involved, so the open question below is closed: no.

The same session's stale interference result (#209) was the router's 60-second cache of `check_interference`; it is removed from the cacheable set.

## Open Questions

- Whether the lock-out also needs the pool turned off by default; decided after the reproduction.
23 changes: 23 additions & 0 deletions openspec/changes/circuit-breaker-recovery/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Why

In a measured agent session (2026-11-06, the U-joint demo) `create_extrusion` succeeded once, was refused 12 seconds later with "Circuit breaker is open for create_extrusion", and stayed refused for more than 8 minutes, although the breaker's timeout is 60 seconds. The agent had to rebuild every later part by revolving and cutting, and asked for a reset tool that doesn't exist (#208).

## What Changes

- Fix the lock-out: the server's router fell back to a VBA adapter built on the same routed adapter, so one failure re-entered the router and was counted many times (design.md, Cause). The server no longer passes a VBA route.
- `check_interference` is no longer cached by the router (its stale result is part of #209).
- A new tool, `reset_circuit_breaker`: closes every breaker, or one operation's; reports what was open. Read-only towards SolidWorks.
- An open breaker's `hint` names the reset tool and the timeout setting.
- The runbook documents `SOLIDWORKS_MCP_CIRCUIT_BREAKER_TIMEOUT` and the reset tool.

## Capabilities

### New Capabilities
- `adapters/circuit-breaker`: when a breaker opens, how it recovers, and how it is reset.

### Modified Capabilities

## Impact

- `adapters/circuit_breaker.py`, `adapters/connection_pool.py`, possibly `adapters/factory.py` (wrapping order); a tool in `tools/analysis.py` or a new `tools/server_admin.py`; the agent skill's errors reference; `CLAUDE.md` runbook item 4.
- Touches adapter wrappers but no COM calls. Tests beside each module, plus a cross-module test of the production stack in `tests/integration/`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## ADDED Requirements

### Requirement: A success never leaves a breaker open

A successful call SHALL close its operation's breaker, and the next call SHALL be allowed.

#### Scenario: Success then a second call
- **WHEN** `create_extrusion` succeeds and is called again at once
- **THEN** the second call reaches the adapter

### Requirement: An open breaker recovers after its timeout

An open breaker SHALL let one trial call through once its timeout has passed since the last failure, and close when that call succeeds.

#### Scenario: Waiting out the timeout
- **WHEN** a breaker opened after repeated failures and the timeout has passed
- **THEN** the next call reaches the adapter, and on success the breaker is closed

### Requirement: Breakers can be reset by a tool

A `reset_circuit_breaker` tool SHALL close all breakers, or the one named, and report which were open.

#### Scenario: Reset one operation
- **WHEN** an agent calls `reset_circuit_breaker` with `operation: create_extrusion` while it is open
- **THEN** the result lists it as reset and the next `create_extrusion` call reaches the adapter

#### Scenario: No breaker in use
- **WHEN** the adapter has no circuit breaker (mock mode)
- **THEN** the tool succeeds and reports that nothing was open

### Requirement: An open breaker explains itself

The error for a refused call SHALL name the operation, and its `hint` SHALL name the reset tool and the timeout setting.

#### Scenario: Refused call
- **WHEN** a call is refused by an open breaker
- **THEN** `error_type` is `circuit_open` and `hint` names `reset_circuit_breaker` and `SOLIDWORKS_MCP_CIRCUIT_BREAKER_TIMEOUT`
15 changes: 15 additions & 0 deletions openspec/changes/circuit-breaker-recovery/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## 1. Reproduce

- [x] 1.1 `tests/integration/test_circuit_breaker_stack.py`: the factory's real wrapping around a fake adapter; reproduce "success, then open" and "never half-opens"; record the cause in design.md

## 2. Fix and reset

- [x] 2.1 Fix the cause; the spec's first two requirements pass
- [x] 2.2 `CircuitBreakerAdapter.reset(operation=None)`, forwarded by the pool; tests beside each
- [x] 2.3 `reset_circuit_breaker` tool (description to the standard, Read-only towards SolidWorks); tests
- [x] 2.4 `circuit_open` hint with the wait and the tool name

## 3. Docs and ship

- [x] 3.1 `CLAUDE.md` runbook item 4, the agent skill's errors reference, CHANGELOG, queue
- [x] 3.2 Full mock suite, ruff, 100 % coverage; then a paced live check with the user's permission
2 changes: 1 addition & 1 deletion openspec/queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Last updated 2026-11-06.
| --- | --- | --- | --- | --- |
| 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` (archived) | Done 2026-11-06 (#205, #211; #147 closed as superseded) |
| 3 | The circuit breaker locks a tool out after a success and never reopens; a reset tool | #208 | not yet written | Next |
| 3 | The circuit breaker locks a tool out after a success and never reopens; a reset tool | #208 | `circuit-breaker-recovery` | Implemented 2026-11-06, in review |
| 4 | Support levels for every tool, and COM coverage progress bars at the top of the README with an interactive docs page | #79 (coverage half) | `tool-support-levels-and-progress` | Plan written 2026-11-06, in review |
| 5 | Tool gaps found building the U-joint: face mates, fixed parts and flexible sub-assemblies, angled planes, angled chamfers, `both_directions`, a stale interference result, thin extrudes | #209 | not yet written | |
| 6 | `--selftest` command and a compatibility-report issue template | #179 | not yet written | |
Expand Down
2 changes: 1 addition & 1 deletion src/solidworks_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""SolidWorks MCP Server - Python Implementation with FastMCP and PydanticAI.

This is a comprehensive Python implementation of the SolidWorks MCP server, providing
154 tools for SolidWorks automation with enhanced security, configurability, and modern
155 tools for SolidWorks automation with enhanced security, configurability, and modern
Python architectures.

Original TypeScript implementation rights and IP remain with the original author. This
Expand Down
16 changes: 16 additions & 0 deletions src/solidworks_mcp/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,22 @@ async def list_components(self) -> AdapterResult[list[str]]:
error="list_components is not implemented by this adapter",
)

async def reset_circuit_breakers(
self, operation: str | None = None
) -> AdapterResult[dict[str, list[str]]]:
"""Re-open operations a circuit breaker paused.

An adapter without a circuit breaker has nothing paused; the breaker
and the connection pool override this.

Args:
operation: The operation to reset; ``None`` resets every one.

Returns:
AdapterResult[dict[str, list[str]]]: ``reset``, the operations that were paused.
"""
return AdapterResult(status=AdapterResultStatus.SUCCESS, data={"reset": []})

async def get_model_geometry_summary(self) -> AdapterResult[dict[str, Any]]:
"""Summarise the active part or assembly without changing it.

Expand Down
47 changes: 47 additions & 0 deletions src/solidworks_mcp/adapters/circuit_breaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,53 @@ def _soc_log(

# Adapter interface implementation

def reset(self, operation: str | None = None) -> list[str]:
"""Close the breaker for one operation, or for all of them.

Args:
operation: The operation to reset; ``None`` resets every one.

Returns:
list[str]: The operations that were open or half-open.
"""
if operation is None:
buckets = dict(self._buckets)
else:
buckets = (
{operation: self._buckets[operation]}
if operation in self._buckets
else {}
)
was_open = [
name
for name, bucket in buckets.items()
if bucket.state != CircuitState.CLOSED
]
for bucket in buckets.values():
bucket.state = CircuitState.CLOSED
bucket.failure_count = 0
bucket.half_open_calls = 0
if operation is None:
self.state = CircuitState.CLOSED
self.failure_count = 0
self.half_open_calls = 0
return sorted(was_open)

async def reset_circuit_breakers(
self, operation: str | None = None
) -> AdapterResult[dict[str, list[str]]]:
"""Reset breakers through the adapter interface (see :meth:`reset`).

Args:
operation: The operation to reset; ``None`` resets every one.

Returns:
AdapterResult[dict[str, list[str]]]: ``reset``, the operations that were open.
"""
return AdapterResult(
status=AdapterResultStatus.SUCCESS, data={"reset": self.reset(operation)}
)

async def connect(self) -> None:
"""Connect through circuit breaker.

Expand Down
25 changes: 25 additions & 0 deletions src/solidworks_mcp/adapters/connection_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,31 @@ async def _execute_with_pool(
error=f"Operation {operation_name} failed after {self.max_retries} retries: {last_error}",
)

async def reset_circuit_breakers(
self, operation: str | None = None
) -> AdapterResult[dict[str, list[str]]]:
"""Reset the circuit breakers of the pooled adapters.

Every slot may hold the same breaker-wrapped adapter, so each one is
reset once.

Args:
operation: The operation to reset; ``None`` resets every one.

Returns:
AdapterResult[dict[str, list[str]]]: ``reset``, the operations that were open.
"""
was_open: set[str] = set()
unique = {id(adapter): adapter for adapter in self.pool}
for adapter in unique.values():
reset = getattr(adapter, "reset_circuit_breakers", None)
if reset is not None:
result = await reset(operation)
was_open.update((result.data or {}).get("reset", []))
return AdapterResult(
status=AdapterResultStatus.SUCCESS, data={"reset": sorted(was_open)}
)

# Adapter interface implementation

async def connect(self) -> None:
Expand Down
3 changes: 2 additions & 1 deletion src/solidworks_mcp/adapters/intelligent_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ def __init__(
# "calculate_mass_properties",
"get_material_properties",
"analyze_geometry",
"check_interference",
# "check_interference": changes as components are mated or moved;
# a cached answer ignored the changes (#209).
# Drawing analysis operations
"analyze_drawing_comprehensive",
"analyze_drawing_dimensions",
Expand Down
Loading
Loading