Skip to content

solidworks_mcp.selftest.cli

solidworks_mcp.selftest.cli

solidworks-mcp-python selftest: options, the precheck and exit codes.

Exit 0 when every area passes, 1 when any fails or is skipped, 2 when the selftest can't run at all.

Attributes

AREAS module-attribute

AREAS = (PartArea(), AssemblyArea(), DrawingArea(), ExportArea())

NOT_RUNNING module-attribute

NOT_RUNNING = "SolidWorks isn't running. Start SolidWorks, wait for it to finish loading, then run this again."

app module-attribute

app = typer.Typer(add_completion=False, no_args_is_help=False)

Classes

Context dataclass

Context(call: ToolCaller, folder: Path, files: dict[str, str] = dict())

What the areas share: the tool caller, the folder, and saved files.

DocumentGuard

DocumentGuard(call: ToolCaller)

An async context manager owning the selftest's folder and documents.

Use call to list, activate and close documents.

Parameters:

Name Type Description Default
call ToolCaller

The selftest's tool caller.

required
Source code in src/solidworks_mcp/selftest/documents.py
def __init__(self, call: ToolCaller) -> None:
    """Use ``call`` to list, activate and close documents.

    Args:
        call: The selftest's tool caller.
    """
    self._call = call
    self._baseline: set[str] = set()
    self._folder: Path | None = None
    self.problems: list[str] = []
Attributes
folder property
folder: Path

The selftest's temporary folder; only inside async with.

Raises:

Type Description
RuntimeError

The guard has not been entered.

Methods:
__aenter__ async
__aenter__() -> DocumentGuard

Note the open documents and make the folder.

Returns:

Name Type Description
DocumentGuard DocumentGuard

This guard, with folder set.

Source code in src/solidworks_mcp/selftest/documents.py
async def __aenter__(self) -> DocumentGuard:
    """Note the open documents and make the folder.

    Returns:
        DocumentGuard: This guard, with ``folder`` set.
    """
    self._baseline = {d["title"] for d in await self._documents() if d["title"]}
    self._folder = Path(tempfile.mkdtemp(prefix="solidworks-mcp-selftest-"))
    return self
__aexit__ async
__aexit__(exc_type: type[BaseException] | None, exc: BaseException | None, traceback: TracebackType | None) -> None

Close the selftest's documents and delete its folder.

Parameters:

Name Type Description Default
exc_type type[BaseException] | None

The exception's type, if one is propagating.

required
exc BaseException | None

The exception, if one is propagating.

required
traceback TracebackType | None

Its traceback.

required
Source code in src/solidworks_mcp/selftest/documents.py
async def __aexit__(
    self,
    exc_type: type[BaseException] | None,
    exc: BaseException | None,
    traceback: TracebackType | None,
) -> None:
    """Close the selftest's documents and delete its folder.

    Args:
        exc_type: The exception's type, if one is propagating.
        exc: The exception, if one is propagating.
        traceback: Its traceback.
    """
    await self.cleanup()
cleanup async
cleanup() -> None

Close the selftest's documents, unsaved, then delete its folder.

A failure is recorded in problems, never raised, so it can't hide the results it follows.

Source code in src/solidworks_mcp/selftest/documents.py
async def cleanup(self) -> None:
    """Close the selftest's documents, unsaved, then delete its folder.

    A failure is recorded in ``problems``, never raised, so it can't hide
    the results it follows.
    """
    try:
        documents = await self._documents()
    except ToolFailed as exc:
        self.problems.append(f"could not list the open documents: {exc.message}")
        documents = []
    ours = sorted(
        (d for d in documents if self.is_ours(d)),
        key=lambda d: _CLOSE_ORDER.get(str(d.get("type")), 2),
    )
    for document in ours:
        try:
            await self._call(
                "activate_document",
                {"title_or_path": document["title"]},
                record=False,
            )
            await self._call("close_model", {"save": False}, record=False)
        except ToolFailed as exc:
            self.problems.append(
                f"could not close {document['title']}: {exc.message}"
            )
    shutil.rmtree(self.folder, ignore_errors=True)
    if self.folder.exists():
        self.problems.append(f"could not delete {self.folder}")
    self._folder = None
is_ours
is_ours(document: dict[str, Any]) -> bool

Whether the selftest made document.

Parameters:

Name Type Description Default
document dict[str, Any]

An entry from list_open_documents.

required

Returns:

Name Type Description
bool bool

True when it is new since the start and was never saved,

bool

or was saved inside the selftest's folder.

Source code in src/solidworks_mcp/selftest/documents.py
def is_ours(self, document: dict[str, Any]) -> bool:
    """Whether the selftest made ``document``.

    Args:
        document: An entry from ``list_open_documents``.

    Returns:
        bool: True when it is new since the start and was never saved,
        or was saved inside the selftest's folder.
    """
    if not document.get("title") or document["title"] in self._baseline:
        return False
    path = document.get("path") or ""
    if not path:
        return True
    folder = os.path.normcase(str(self.folder.resolve()))
    return os.path.normcase(str(Path(path).resolve())).startswith(folder + os.sep)

Outcome

Bases: StrEnum

An area's result.

SolidWorksMCPConfig

Bases: BaseModel

Main configuration for SolidWorks MCP Server.

Attributes:

Name Type Description
adapter_type AdapterType

The adapter type value.

allowed_hosts list[str]

The allowed hosts value.

allowed_origins list[str]

The allowed origins value.

api_key SecretStr | None

The api key value.

api_key_required bool

The api key required value.

api_keys list[str]

The api keys value.

cache_dir Path | None

The cache dir value.

circuit_breaker_enabled bool

The circuit breaker enabled value.

circuit_breaker_threshold int

The circuit breaker threshold value.

circuit_breaker_timeout int

The circuit breaker timeout value.

complexity_parameter_threshold int

The complexity parameter threshold value.

complexity_score_threshold float

The complexity score threshold value.

connection_pool_size int

The connection pool size value.

connection_pooling bool

The connection pooling value.

cors_origins list[str]

The cors origins value.

data_dir Path

The data dir value.

database_url str

The database url value.

debug bool

The debug value.

deployment_mode DeploymentMode

The deployment mode value.

enable_analysis_tools bool

The enable analysis tools value.

enable_audit_logging bool

The enable audit logging value.

enable_circuit_breaker bool

The enable circuit breaker value.

enable_connection_pooling bool

The enable connection pooling value.

enable_cors bool

The enable cors value.

enable_design_tables bool

The enable design tables value.

enable_intelligent_routing bool

The enable intelligent routing value.

enable_macro_recording bool

The enable macro recording value.

enable_pdm bool

The enable pdm value.

enable_rate_limiting bool

The enable rate limiting value.

enable_response_cache bool

The enable response cache value.

enable_sql_integration bool

The enable sql integration value.

enable_windows_validation bool

The enable windows validation value.

host str

The host value.

log_file Path | None

The log file value.

log_level str

The log level value.

max_connections int

The max connections value.

max_retries int

The max retries value.

mock_solidworks bool

The mock solidworks value.

model_config Any

The model config value.

pdm_server str | None

The pdm server value.

pdm_vault str | None

The pdm vault value.

port int

The port value.

rate_limit_enabled bool

The rate limit enabled value.

rate_limit_per_minute int

The rate limit per minute value.

response_cache_max_entries int

The response cache max entries value.

response_cache_ttl_seconds int

The response cache ttl seconds value.

security_level SecurityLevel

The security level value.

solidworks_path str | None

The solidworks path value.

solidworks_year int | None

The solidworks year value.

sql_connection str | None

The sql connection value.

state_file str | None

The state file value.

testing bool

The testing value.

timeout_seconds float

The timeout seconds value.

worker_processes int

The worker processes value.

Attributes
can_use_solidworks property
can_use_solidworks: bool

Check if SolidWorks integration is possible.

Returns:

Name Type Description
bool bool

True if use solidworks, otherwise False.

is_windows property
is_windows: bool

Check if running on Windows.

Returns:

Name Type Description
bool bool

True if windows, otherwise False.

Methods:
from_env classmethod
from_env(env_file: str | None = None) -> SolidWorksMCPConfig

Build configuration from environment variables.

Parameters:

Name Type Description Default
env_file str | None

The env file value. Defaults to None.

None

Returns:

Name Type Description
SolidWorksMCPConfig SolidWorksMCPConfig

The result produced by the operation.

Source code in src/solidworks_mcp/config.py
@classmethod
def from_env(cls, env_file: str | None = None) -> SolidWorksMCPConfig:
    """Build configuration from environment variables.

    Args:
        env_file (str | None): The env file value. Defaults to None.

    Returns:
        SolidWorksMCPConfig: The result produced by the operation.
    """
    import json

    env_prefix = "SOLIDWORKS_MCP_"
    raw_values: dict[str, Any] = {}

    list_like_fields = {"cors_origins", "allowed_hosts", "api_keys"}

    def _coerce_env_value(key: str, value: Any) -> Any:
        """Build internal coerce env value.

        Args:
            key (str): The key value.
            value (Any): The value value.

        Returns:
            Any: The result produced by the operation.
        """
        if not isinstance(value, str):
            return value
        if key in list_like_fields:
            stripped = value.strip()
            if stripped.startswith("[") and stripped.endswith("]"):
                try:
                    parsed = json.loads(stripped)
                    if isinstance(parsed, list):
                        return parsed
                except Exception:
                    # Leave original value for Pydantic to validate/report.
                    return value
        return value

    if env_file and Path(env_file).exists():
        for key, value in dotenv_values(env_file).items():
            if key and key.startswith(env_prefix) and value is not None:
                field_name = key[len(env_prefix) :].lower()
                raw_values[field_name] = _coerce_env_value(field_name, value)

    for key, value in os.environ.items():
        if key.startswith(env_prefix):
            field_name = key[len(env_prefix) :].lower()
            raw_values[field_name] = _coerce_env_value(field_name, value)

    return cls(**raw_values)
get_database_config
get_database_config() -> dict[str, Any]

Get database configuration.

Returns:

Type Description
dict[str, Any]

dict[str, Any]: A dictionary containing the resulting values.

Source code in src/solidworks_mcp/config.py
def get_database_config(self) -> dict[str, Any]:
    """Get database configuration.

    Returns:
        dict[str, Any]: A dictionary containing the resulting values.
    """
    return {
        "url": self.database_url,
        "echo": self.debug,
    }
get_security_config
get_security_config() -> dict[str, Any]

Get security configuration.

Returns:

Type Description
dict[str, Any]

dict[str, Any]: A dictionary containing the resulting values.

Source code in src/solidworks_mcp/config.py
def get_security_config(self) -> dict[str, Any]:
    """Get security configuration.

    Returns:
        dict[str, Any]: A dictionary containing the resulting values.
    """
    return {
        "api_key": self.api_key.get_secret_value() if self.api_key else None,
        "allowed_hosts": self.allowed_hosts,
        "enable_cors": self.enable_cors,
        "cors_origins": self.cors_origins,
        "enable_rate_limiting": self.enable_rate_limiting,
        "rate_limit_per_minute": self.rate_limit_per_minute,
        "security_level": self.security_level,
    }
model_post_init
model_post_init(__context: Any) -> None

Post-initialization setup.

Parameters:

Name Type Description Default
__context Any

The context value.

required

Returns:

Name Type Description
None None

None.

Source code in src/solidworks_mcp/config.py
def model_post_init(self, __context: Any) -> None:
    """Post-initialization setup.

    Args:
        __context (Any): The context value.

    Returns:
        None: None.
    """
    if self.cache_dir is None:
        self.cache_dir = self.data_dir / "cache"
    if self.log_file is None:
        self.log_file = self.data_dir / "logs" / "server.log"

    # Ensure data directories exist
    self.data_dir.mkdir(parents=True, exist_ok=True)
    self.cache_dir.mkdir(parents=True, exist_ok=True)
    self.log_file.parent.mkdir(parents=True, exist_ok=True)

    # Set testing defaults
    if self.testing:
        self.mock_solidworks = True
        self.adapter_type = AdapterType.MOCK

    # Give SoC logging a session id to write under when it's enabled
    # without one being named explicitly.
    if self.soc_logging_enabled and not self.soc_session_id:
        self.soc_session_id = f"soc-{datetime.now(UTC).strftime('%Y%m%d-%H%M%S')}"
set_cache_dir classmethod
set_cache_dir(v: Path | None, info: ValidationInfo) -> Path

Set default cache directory.

Parameters:

Name Type Description Default
v Path | None

The v value.

required
info ValidationInfo

The info value.

required

Returns:

Name Type Description
Path Path

The result produced by the operation.

Source code in src/solidworks_mcp/config.py
@field_validator("cache_dir")
@classmethod
def set_cache_dir(cls, v: Path | None, info: ValidationInfo) -> Path:
    """Set default cache directory.

    Args:
        v (Path | None): The v value.
        info (ValidationInfo): The info value.

    Returns:
        Path: The result produced by the operation.
    """
    if v is None:
        data_dir = cast(
            Path,
            info.data.get("data_dir", Path.home() / ".solidworks_mcp"),
        )
        return data_dir / "cache"
    return v
set_log_file classmethod
set_log_file(v: Path | None, info: ValidationInfo) -> Path

Set default log file path.

Parameters:

Name Type Description Default
v Path | None

The v value.

required
info ValidationInfo

The info value.

required

Returns:

Name Type Description
Path Path

The result produced by the operation.

Source code in src/solidworks_mcp/config.py
@field_validator("log_file")
@classmethod
def set_log_file(cls, v: Path | None, info: ValidationInfo) -> Path:
    """Set default log file path.

    Args:
        v (Path | None): The v value.
        info (ValidationInfo): The info value.

    Returns:
        Path: The result produced by the operation.
    """
    if v is None:
        data_dir = cast(
            Path,
            info.data.get("data_dir", Path.home() / ".solidworks_mcp"),
        )
        return data_dir / "logs" / "server.log"
    return v
sync_legacy_alias_fields
sync_legacy_alias_fields() -> SolidWorksMCPConfig

Sync test/developer alias fields into canonical runtime fields.

Several fixtures and scripts still populate compatibility fields such as rate_limit_enabled and connection_pooling. Runtime code reads the canonical fields, so normalize them here after validation.

Returns:

Name Type Description
SolidWorksMCPConfig SolidWorksMCPConfig

The result produced by the operation.

Source code in src/solidworks_mcp/config.py
@model_validator(mode="after")
def sync_legacy_alias_fields(self) -> SolidWorksMCPConfig:
    """Sync test/developer alias fields into canonical runtime fields.

    Several fixtures and scripts still populate compatibility fields such as
    ``rate_limit_enabled`` and ``connection_pooling``. Runtime code reads the canonical
    fields, so normalize them here after validation.

    Returns:
        SolidWorksMCPConfig: The result produced by the operation.
    """
    self.enable_circuit_breaker = self.circuit_breaker_enabled
    self.enable_connection_pooling = self.connection_pooling
    self.connection_pool_size = self.max_connections
    self.enable_rate_limiting = self.rate_limit_enabled
    if self.allowed_origins and not self.cors_origins:
        self.cors_origins = list(self.allowed_origins)
    return self
validate_adapter_type classmethod
validate_adapter_type(v: AdapterType, info: ValidationInfo) -> AdapterType

Validate adapter type based on platform.

Parameters:

Name Type Description Default
v AdapterType

The v value.

required
info ValidationInfo

The info value.

required

Returns:

Name Type Description
AdapterType AdapterType

The result produced by the operation.

Source code in src/solidworks_mcp/config.py
@field_validator("adapter_type")
@classmethod
def validate_adapter_type(cls, v: AdapterType, info: ValidationInfo) -> AdapterType:
    """Validate adapter type based on platform.

    Args:
        v (AdapterType): The v value.
        info (ValidationInfo): The info value.

    Returns:
        AdapterType: The result produced by the operation.
    """
    return v
validate_complexity_parameter_threshold classmethod
validate_complexity_parameter_threshold(v: int) -> int

Validate the complexity parameter threshold.

Parameters:

Name Type Description Default
v int

The v value.

required

Returns:

Name Type Description
int int

The computed numeric result.

Raises:

Type Description
ValueError

Complexity_parameter_threshold must be >= 1.

Source code in src/solidworks_mcp/config.py
@field_validator("complexity_parameter_threshold")
@classmethod
def validate_complexity_parameter_threshold(cls, v: int) -> int:
    """Validate the complexity parameter threshold.

    Args:
        v (int): The v value.

    Returns:
        int: The computed numeric result.

    Raises:
        ValueError: Complexity_parameter_threshold must be >= 1.
    """
    if v < 1:
        raise ValueError("complexity_parameter_threshold must be >= 1")
    return v
validate_complexity_score_threshold classmethod
validate_complexity_score_threshold(v: float) -> float

Validate the complexity score threshold.

Parameters:

Name Type Description Default
v float

The v value.

required

Returns:

Name Type Description
float float

The computed numeric result.

Raises:

Type Description
ValueError

Complexity_score_threshold must be in (0, 1].

Source code in src/solidworks_mcp/config.py
@field_validator("complexity_score_threshold")
@classmethod
def validate_complexity_score_threshold(cls, v: float) -> float:
    """Validate the complexity score threshold.

    Args:
        v (float): The v value.

    Returns:
        float: The computed numeric result.

    Raises:
        ValueError: Complexity_score_threshold must be in (0, 1].
    """
    if v <= 0 or v > 1:
        raise ValueError("complexity_score_threshold must be in (0, 1]")
    return v
validate_port classmethod
validate_port(v: int) -> int

Validate the port.

Parameters:

Name Type Description Default
v int

The v value.

required

Returns:

Name Type Description
int int

The computed numeric result.

Raises:

Type Description
ValueError

Port must be between 1 and 65535.

Source code in src/solidworks_mcp/config.py
@field_validator("port")
@classmethod
def validate_port(cls, v: int) -> int:
    """Validate the port.

    Args:
        v (int): The v value.

    Returns:
        int: The computed numeric result.

    Raises:
        ValueError: Port must be between 1 and 65535.
    """
    if v < 1 or v > 65535:
        raise ValueError("Port must be between 1 and 65535")
    return v
validate_response_cache_max_entries classmethod
validate_response_cache_max_entries(v: int) -> int

Validate response cache size.

Parameters:

Name Type Description Default
v int

The v value.

required

Returns:

Name Type Description
int int

The computed numeric result.

Raises:

Type Description
ValueError

Response_cache_max_entries must be >= 1.

Source code in src/solidworks_mcp/config.py
@field_validator("response_cache_max_entries")
@classmethod
def validate_response_cache_max_entries(cls, v: int) -> int:
    """Validate response cache size.

    Args:
        v (int): The v value.

    Returns:
        int: The computed numeric result.

    Raises:
        ValueError: Response_cache_max_entries must be >= 1.
    """
    if v < 1:
        raise ValueError("response_cache_max_entries must be >= 1")
    return v
validate_response_cache_ttl_seconds classmethod
validate_response_cache_ttl_seconds(v: int) -> int

Validate default response cache TTL.

Parameters:

Name Type Description Default
v int

The v value.

required

Returns:

Name Type Description
int int

The computed numeric result.

Raises:

Type Description
ValueError

Response_cache_ttl_seconds must be >= 1.

Source code in src/solidworks_mcp/config.py
@field_validator("response_cache_ttl_seconds")
@classmethod
def validate_response_cache_ttl_seconds(cls, v: int) -> int:
    """Validate default response cache TTL.

    Args:
        v (int): The v value.

    Returns:
        int: The computed numeric result.

    Raises:
        ValueError: Response_cache_ttl_seconds must be >= 1.
    """
    if v < 1:
        raise ValueError("response_cache_ttl_seconds must be >= 1")
    return v
validate_timeout classmethod
validate_timeout(v: float) -> float

Validate the timeout.

Parameters:

Name Type Description Default
v float

The v value.

required

Returns:

Name Type Description
float float

The computed numeric result.

Raises:

Type Description
ValueError

Timeout_seconds must be > 0.

Source code in src/solidworks_mcp/config.py
@field_validator("timeout_seconds")
@classmethod
def validate_timeout(cls, v: float) -> float:
    """Validate the timeout.

    Args:
        v (float): The v value.

    Returns:
        float: The computed numeric result.

    Raises:
        ValueError: Timeout_seconds must be > 0.
    """
    if v <= 0:
        raise ValueError("timeout_seconds must be > 0")
    return v

ToolCaller

ToolCaller(client: Any)

Call MCP tools by name with dict input, as a client does; log each call.

Wrap a connected fastmcp.Client.

Parameters:

Name Type Description Default
client Any

The client, already entered.

required
Source code in src/solidworks_mcp/selftest/results.py
def __init__(self, client: Any) -> None:
    """Wrap a connected ``fastmcp.Client``.

    Args:
        client: The client, already entered.
    """
    self._client = client
    self.calls: list[str] = []
Methods:
__call__ async
__call__(name: str, args: dict[str, Any] | None = None, record: bool = True) -> dict[str, Any]

Call name and return its reply; raise when it reports an error.

Parameters:

Name Type Description Default
name str

The tool's name.

required
args dict[str, Any] | None

Its input, sent as input_data; None for a tool without input.

None
record bool

Whether the call is listed in the report.

True

Returns:

Type Description
dict[str, Any]

dict[str, Any]: The tool's reply.

Raises:

Type Description
ToolFailed

The tool replied with status: error.

Source code in src/solidworks_mcp/selftest/results.py
async def __call__(
    self, name: str, args: dict[str, Any] | None = None, record: bool = True
) -> dict[str, Any]:
    """Call ``name`` and return its reply; raise when it reports an error.

    Args:
        name: The tool's name.
        args: Its input, sent as ``input_data``; ``None`` for a tool
            without input.
        record: Whether the call is listed in the report.

    Returns:
        dict[str, Any]: The tool's reply.

    Raises:
        ToolFailed: The tool replied with ``status: error``.
    """
    if record:
        self.calls.append(name)
    result = await self._client.call_tool(
        name, {} if args is None else {"input_data": args}, raise_on_error=False
    )
    reply = tool_reply(result)
    if reply.get("status") != "success":
        raise ToolFailed(name, str(reply.get("message") or "no message"))
    return reply

Functions:

_start_server async

_start_server(config: SolidWorksMCPConfig) -> Any

Build and connect the same server the CLI runs.

Parameters:

Name Type Description Default
config SolidWorksMCPConfig

The selftest's configuration.

required

Returns:

Name Type Description
Any Any

The set-up SolidWorksMCPServer, its adapter connected.

Source code in src/solidworks_mcp/selftest/cli.py
async def _start_server(config: SolidWorksMCPConfig) -> Any:
    """Build and connect the same server the CLI runs.

    Args:
        config: The selftest's configuration.

    Returns:
        Any: The set-up ``SolidWorksMCPServer``, its adapter connected.
    """
    from ..server import SolidWorksMCPServer

    server = SolidWorksMCPServer(config)
    await server.setup()
    await server.adapter.connect()
    return server

installed_versions

installed_versions(registry: ModuleType | None = None) -> list[int]

List the installed SolidWorks years.

Parameters:

Name Type Description Default
registry ModuleType | None

A winreg-like module; defaults to winreg (tests pass a fake).

None

Returns:

Type Description
list[int]

The years, newest first; empty off Windows or with no install.

Source code in src/solidworks_mcp/setup/solidworks.py
def installed_versions(registry: ModuleType | None = None) -> list[int]:
    """List the installed SolidWorks years.

    Args:
        registry: A ``winreg``-like module; defaults to ``winreg`` (tests
            pass a fake).

    Returns:
        The years, newest first; empty off Windows or with no install.
    """
    if registry is None:
        try:
            import winreg
        except ImportError:  # not Windows
            return []
        registry = winreg
    reg: Any = registry  # winreg and the test fakes share this shape
    try:
        root = reg.OpenKey(reg.HKEY_LOCAL_MACHINE, r"SOFTWARE\SolidWorks")
    except OSError:  # no SolidWorks key at all
        return []
    years: set[int] = set()
    with root:
        index = 0
        while True:
            # EnumKey raises OSError past the last subkey; that ends the walk.
            try:
                name = reg.EnumKey(root, index)
            except OSError:
                break
            if match := _VERSION_KEY.match(name):
                years.add(int(match.group(1)))
            index += 1
    return sorted(years, reverse=True)

load_config

load_config(config_file: str | None = None) -> SolidWorksMCPConfig

Load configuration from file and environment variables.

Parameters:

Name Type Description Default
config_file str | None

The config file value. Defaults to None.

None

Returns:

Name Type Description
SolidWorksMCPConfig SolidWorksMCPConfig

The result produced by the operation.

Source code in src/solidworks_mcp/config.py
def load_config(config_file: str | None = None) -> SolidWorksMCPConfig:
    """Load configuration from file and environment variables.

    Args:
        config_file (str | None): The config file value. Defaults to None.

    Returns:
        SolidWorksMCPConfig: The result produced by the operation.
    """
    if config_file:
        config_path = Path(config_file)
        if config_path.exists() and config_path.suffix.lower() == ".json":
            import json

            with config_path.open("r", encoding="utf-8") as f:
                data = json.load(f)
            return SolidWorksMCPConfig(**data)
        return SolidWorksMCPConfig.from_env(str(config_path))

    return SolidWorksMCPConfig.from_env()

precheck

precheck(system: Callable[[], str] = platform.system, versions: Callable[[], list[int]] = installed_versions, running: Callable[[], bool] = solidworks_running) -> str | None

Why the selftest can't run here, or None when it can.

Parameters:

Name Type Description Default
system Callable[[], str]

Returns the OS name.

system
versions Callable[[], list[int]]

Returns the installed SolidWorks years.

installed_versions
running Callable[[], bool]

Whether SolidWorks is running.

solidworks_running

Returns:

Type Description
str | None

str | None: What to do next, or None.

Source code in src/solidworks_mcp/selftest/cli.py
def precheck(
    system: Callable[[], str] = platform.system,
    versions: Callable[[], list[int]] = installed_versions,
    running: Callable[[], bool] = solidworks_running,
) -> str | None:
    """Why the selftest can't run here, or ``None`` when it can.

    Args:
        system: Returns the OS name.
        versions: Returns the installed SolidWorks years.
        running: Whether SolidWorks is running.

    Returns:
        str | None: What to do next, or ``None``.
    """
    if system() != "Windows":
        return "The selftest needs Windows with SolidWorks installed."
    if not versions():
        return (
            "No SolidWorks install was found. Install SolidWorks, then run this again."
        )
    if not running():
        return NOT_RUNNING
    return None

read_environment async

read_environment(adapter: Any, year: int | None) -> dict[str, Any]

Describe the setup the selftest ran on.

A value that can't be read is None; nothing here stops the run.

Parameters:

Name Type Description Default
adapter Any

The connected adapter.

required
year int | None

The SolidWorks year the server was started with.

required

Returns:

Type Description
dict[str, Any]

dict[str, Any]: solidworks (revision, language,

dict[str, Any]

templates, year), os, python, pywin32 and

dict[str, Any]

solidworks_mcp versions.

Source code in src/solidworks_mcp/selftest/environment.py
async def read_environment(adapter: Any, year: int | None) -> dict[str, Any]:
    """Describe the setup the selftest ran on.

    A value that can't be read is ``None``; nothing here stops the run.

    Args:
        adapter: The connected adapter.
        year: The SolidWorks year the server was started with.

    Returns:
        dict[str, Any]: ``solidworks`` (``revision``, ``language``,
        ``templates``, ``year``), ``os``, ``python``, ``pywin32`` and
        ``solidworks_mcp`` versions.
    """
    info = await adapter.get_application_info()
    data = info.data if info.is_success and info.data else {}
    return {
        "solidworks": {
            "revision": data.get("revision"),
            "language": data.get("language"),
            "year": year,
            "templates": data.get("templates") or {},
        },
        "os": platform.platform(),
        "python": platform.python_version(),
        "pywin32": _package_version("pywin32"),
        "solidworks_mcp": __version__,
    }

render

render(environment: dict[str, Any], results: list[AreaResult], problems: list[str]) -> str

Everything the command prints after the run.

Parameters:

Name Type Description Default
environment dict[str, Any]

From read_environment.

required
results list[AreaResult]

The areas' results.

required
problems list[str]

Cleanup problems, printed but kept out of the summary.

required

Returns:

Name Type Description
str str

The area lines, any cleanup problems, the summary and the link.

Source code in src/solidworks_mcp/selftest/report.py
def render(
    environment: dict[str, Any], results: list[AreaResult], problems: list[str]
) -> str:
    """Everything the command prints after the run.

    Args:
        environment: From ``read_environment``.
        results: The areas' results.
        problems: Cleanup problems, printed but kept out of the summary.

    Returns:
        str: The area lines, any cleanup problems, the summary and the link.
    """
    summary = summary_markdown(environment, results)
    parts = ["\n".join(area_line(r) for r in results)]
    if problems:
        parts.append("Cleanup:\n" + "\n".join(f"  {p}" for p in problems))
    parts.append("Summary to paste into a compatibility report:\n\n" + summary)
    link = issue_link(summary)
    if link:
        parts.append(f"Open the report form with this summary filled in:\n{link}")
    else:
        parts.append(
            "Open the report form and paste the summary above:\n" + ISSUE_FORM_URL
        )
    return "\n\n".join(parts) + "\n"

run_areas async

run_areas(areas: Iterable[Area], ctx: Context) -> list[AreaResult]

Run each area, skipping one whose files an earlier area didn't save.

A tool error ends its area as FAIL with the tool's message; the checks made before it are kept. Any other exception propagates, so a bug in the selftest is never reported as a SolidWorks failure.

Parameters:

Name Type Description Default
areas Iterable[Area]

The areas, in order.

required
ctx Context

The selftest context.

required

Returns:

Type Description
list[AreaResult]

list[AreaResult]: One result per area, in order.

Source code in src/solidworks_mcp/selftest/runner.py
async def run_areas(areas: Iterable[Area], ctx: Context) -> list[AreaResult]:
    """Run each area, skipping one whose files an earlier area didn't save.

    A tool error ends its area as FAIL with the tool's message; the checks
    made before it are kept. Any other exception propagates, so a bug in
    the selftest is never reported as a SolidWorks failure.

    Args:
        areas: The areas, in order.
        ctx: The selftest context.

    Returns:
        list[AreaResult]: One result per area, in order.
    """
    results = []
    for area in areas:
        missing = [need for need in area.needs if need not in ctx.files]
        if missing:
            results.append(
                AreaResult(
                    area.name,
                    Outcome.SKIP,
                    note=f"needs the saved {' and '.join(missing)}",
                )
            )
            continue
        first_call = len(ctx.call.calls)
        checks: list[Check] = []
        note = None
        try:
            await area.run(ctx, checks)
        except ToolFailed as exc:
            note = str(exc)
        passed = note is None and bool(checks) and all(c.passed for c in checks)
        results.append(
            AreaResult(
                area.name,
                Outcome.PASS if passed else Outcome.FAIL,
                checks,
                ctx.call.calls[first_call:],
                note,
            )
        )
    return results

run_selftest async

run_selftest(config: SolidWorksMCPConfig, report_path: Path | None, start_server: Callable[[SolidWorksMCPConfig], Any] = _start_server) -> int

Run every area against SolidWorks, print the report, return the exit code.

Parameters:

Name Type Description Default
config SolidWorksMCPConfig

The configuration, its solidworks_year set.

required
report_path Path | None

Where to also write the Markdown summary, if anywhere.

required
start_server Callable[[SolidWorksMCPConfig], Any]

Builds and connects the server (tests pass a stub).

_start_server

Returns:

Name Type Description
int int

EXIT_PASS, EXIT_FAIL or EXIT_CANNOT_RUN.

Source code in src/solidworks_mcp/selftest/cli.py
async def run_selftest(
    config: SolidWorksMCPConfig,
    report_path: Path | None,
    start_server: Callable[[SolidWorksMCPConfig], Any] = _start_server,
) -> int:
    """Run every area against SolidWorks, print the report, return the exit code.

    Args:
        config: The configuration, its ``solidworks_year`` set.
        report_path: Where to also write the Markdown summary, if anywhere.
        start_server: Builds and connects the server (tests pass a stub).

    Returns:
        int: ``EXIT_PASS``, ``EXIT_FAIL`` or ``EXIT_CANNOT_RUN``.
    """
    from fastmcp import Client

    try:
        server = await start_server(config)
    except Exception as exc:  # noqa: BLE001 - any connect failure means "can't run"
        typer.echo(f"Could not connect to SolidWorks: {exc}")
        return EXIT_CANNOT_RUN
    try:
        async with Client(server.mcp) as client:
            call = ToolCaller(client)
            environment = await read_environment(server.adapter, config.solidworks_year)
            guard = DocumentGuard(call)
            async with guard:
                results = await run_areas(AREAS, Context(call, guard.folder))
    finally:
        await server.adapter.disconnect()
    typer.echo(render(environment, results, guard.problems))
    if report_path is not None:
        report_path.write_text(summary_markdown(environment, results), encoding="utf-8")
        typer.echo(f"Summary written to {report_path}")
    passed = all(r.outcome is Outcome.PASS for r in results)
    return EXIT_PASS if passed else EXIT_FAIL

selftest

selftest(year: int | None = typer.Option(None, '--year', min=1995, max=2100, help='The installed SolidWorks release to test, e.g. 2026. Default: the newest installed.'), report: Path | None = typer.Option(None, '--report', help='Also write the Markdown summary to this file.'), config: str | None = typer.Option(None, '--config', help='Configuration file path.')) -> None

Check this SolidWorks setup through the MCP tools and print a report.

With SolidWorks running, it builds a small part, an assembly, a drawing view and a STEP export, checks each against a hand calculation, and prints a summary to send with a compatibility report. It opens and closes a few documents of its own, and never saves, closes or changes one you had open.

Parameters:

Name Type Description Default
year int | None

The SolidWorks release year.

Option(None, '--year', min=1995, max=2100, help='The installed SolidWorks release to test, e.g. 2026. Default: the newest installed.')
report Path | None

A file to write the Markdown summary to.

Option(None, '--report', help='Also write the Markdown summary to this file.')
config str | None

Path to a configuration file.

Option(None, '--config', help='Configuration file path.')

Raises:

Type Description
Exit

Always, with the exit code.

Source code in src/solidworks_mcp/selftest/cli.py
@app.command()
def selftest(
    year: int | None = typer.Option(
        None,
        "--year",
        min=1995,
        max=2100,
        help="The installed SolidWorks release to test, e.g. 2026. "
        "Default: the newest installed.",
    ),
    report: Path | None = typer.Option(
        None, "--report", help="Also write the Markdown summary to this file."
    ),
    config: str | None = typer.Option(
        None, "--config", help="Configuration file path."
    ),
) -> None:
    """Check this SolidWorks setup through the MCP tools and print a report.

    With SolidWorks running, it builds a small part, an assembly, a drawing
    view and a STEP export, checks each against a hand calculation, and
    prints a summary to send with a compatibility report. It opens and
    closes a few documents of its own, and never saves, closes or changes
    one you had open.

    \f
    Args:
        year: The SolidWorks release year.
        report: A file to write the Markdown summary to.
        config: Path to a configuration file.

    Raises:
        typer.Exit: Always, with the exit code.
    """
    problem = precheck()
    if problem:
        typer.echo(problem)
        raise typer.Exit(EXIT_CANNOT_RUN)
    loaded = load_config(config)
    loaded.mock_solidworks = False
    loaded.log_level = "WARNING"
    loaded.solidworks_year = year or installed_versions()[0]
    if not loaded.can_use_solidworks:
        typer.echo(
            "The configuration asks for the mock adapter; the selftest needs SolidWorks."
        )
        raise typer.Exit(EXIT_CANNOT_RUN)
    from .. import utils

    utils.setup_logging(loaded)
    typer.echo(
        f"Testing SolidWorks {loaded.solidworks_year}: this opens and closes a "
        "few documents of its own and leaves yours alone. About three minutes."
    )
    raise typer.Exit(asyncio.run(run_selftest(loaded, report)))

solidworks_running

solidworks_running() -> bool

Whether an SLDWORKS.exe process is running, without touching COM.

Returns:

Name Type Description
bool bool

True when tasklist lists one.

Source code in src/solidworks_mcp/selftest/cli.py
def solidworks_running() -> bool:
    """Whether an ``SLDWORKS.exe`` process is running, without touching COM.

    Returns:
        bool: True when ``tasklist`` lists one.
    """
    listed = subprocess.run(
        ["tasklist", "/FI", "IMAGENAME eq SLDWORKS.exe", "/NH"],
        capture_output=True,
        text=True,
        check=False,
    )
    return "sldworks.exe" in listed.stdout.lower()

summary_markdown

summary_markdown(environment: dict[str, Any], results: list[AreaResult]) -> str

The summary to paste into a compatibility issue.

Parameters:

Name Type Description Default
environment dict[str, Any]

From read_environment.

required
results list[AreaResult]

The areas' results.

required

Returns:

Name Type Description
str str

Markdown: the environment, then each area's checks and tools.

Source code in src/solidworks_mcp/selftest/report.py
def summary_markdown(environment: dict[str, Any], results: list[AreaResult]) -> str:
    """The summary to paste into a compatibility issue.

    Args:
        environment: From ``read_environment``.
        results: The areas' results.

    Returns:
        str: Markdown: the environment, then each area's checks and tools.
    """
    sw = environment.get("solidworks") or {}
    passed = sum(1 for r in results if r.outcome is Outcome.PASS)
    lines = [
        f"### Selftest: {passed} of {len(results)} areas passed",
        "",
        "| | |",
        "| --- | --- |",
        f"| SolidWorks | {_value(sw.get('revision'))} "
        f"(year {_value(sw.get('year'))}), {_value(sw.get('language'))} |",
        f"| Templates | {_templates(sw.get('templates') or {})} |",
        f"| OS | {_value(environment.get('os'))} |",
        f"| Python | {_value(environment.get('python'))}, "
        f"pywin32 {_value(environment.get('pywin32'))} |",
        f"| solidworks-mcp-python | {_value(environment.get('solidworks_mcp'))} |",
    ]
    for result in results:
        lines += ["", f"#### {result.name}: {result.outcome.value}"]
        if result.note:
            lines += ["", result.note]
        if result.checks:
            lines += [
                "",
                "| Check | Expected | Measured | Tolerance | Result |",
                "| --- | --- | --- | --- | --- |",
            ]
            lines += [
                f"| {c.name} | {_expected(c)} | "
                f"{_value(c.measured)} | {_tolerance(c)} | "
                f"{'pass' if c.passed else 'FAIL'} |"
                for c in result.checks
            ]
        if result.calls:
            lines += ["", f"Tools: {_tools(result.calls)}"]
    return "\n".join(lines) + "\n"