Skip to content

solidworks_mcp.setup.cli

solidworks_mcp.setup.cli

solidworks-mcp setup: connect MCP clients to this server, step by step.

This module is only the conversation with the user: it asks, reports and decides the order. Finding clients (:mod:.clients), editing their configs (:mod:.registration) and checking the server (:mod:.handshake) live in their own modules.

Attributes

PACKAGE module-attribute

PACKAGE = 'solidworks-mcp-python'

SERVER_NAME module-attribute

SERVER_NAME = 'solidworks'

app module-attribute

app = typer.Typer(add_completion=False)

console module-attribute

console = Console()

Classes

Client dataclass

Client(key: str, name: str, found: bool, config_path: Path | None, section: str = 'mcpServers', stdio_type: bool = False, restart_hint: str = '')

One MCP client and how to register a server with it.

Attributes:

Name Type Description
key str

The name used with --client (e.g. claude-desktop).

name str

The name shown to the user.

found bool

Whether the client looks installed.

config_path Path | None

Its JSON config file; None for a client configured through its own CLI (Claude Code).

section str

The object in the config that holds the servers.

stdio_type bool

Whether entries need "type": "stdio" (VS Code).

restart_hint str

What to do after setup so the client loads the server.

Methods:
entry
entry(command: str, args: list[str]) -> dict[str, Any]

Build the server entry in the shape this client expects.

Parameters:

Name Type Description Default
command str

The absolute launcher path.

required
args list[str]

The launcher's arguments.

required

Returns:

Type Description
dict[str, Any]

The JSON entry for the config file.

Source code in src/solidworks_mcp/setup/clients.py
def entry(self, command: str, args: list[str]) -> dict[str, Any]:
    """Build the server entry in the shape this client expects.

    Args:
        command: The absolute launcher path.
        args: The launcher's arguments.

    Returns:
        The JSON entry for the config file.
    """
    entry: dict[str, Any] = {"command": command, "args": args}
    return {"type": "stdio", **entry} if self.stdio_type else entry

ConfigFileError

Bases: Exception

A client config file can't be read or safely changed.

EntryChange dataclass

EntryChange(location: str, action: str, backup: Path | None = None, previous: str | None = None)

What happened to the server entry in one client's config.

Attributes:

Name Type Description
location str

Where the entry lives (a file path, or a description).

action str

added, updated, removed or unchanged.

backup Path | None

The copy made before writing, if a file was overwritten.

previous str | None

For an updated entry, the command it ran before, so the user can see which copy it pointed at. Only the command is kept, never the rest of the entry.

HandshakeResult dataclass

HandshakeResult(ok: bool, tools: int, message: str)

The outcome of the start-up check.

Attributes:

Name Type Description
ok bool

Whether the server answered and listed at least one tool.

tools int

How many tools it listed.

message str

One line for the user.

OldEntryChoice

Bases: Enum

What to do with older entries that start this same server.

Methods:
from_flags classmethod
from_flags(*, replace_old: bool, yes: bool) -> OldEntryChoice

Pick from the command-line flags.

--replace-old removes them. --yes alone keeps them, since removing someone's entries is never done without being asked.

Parameters:

Name Type Description Default
replace_old bool

--replace-old was given.

required
yes bool

--yes was given.

required

Returns:

Type Description
OldEntryChoice

The choice.

Source code in src/solidworks_mcp/setup/cli.py
@classmethod
def from_flags(cls, *, replace_old: bool, yes: bool) -> OldEntryChoice:
    """Pick from the command-line flags.

    ``--replace-old`` removes them. ``--yes`` alone keeps them, since
    removing someone's entries is never done without being asked.

    Args:
        replace_old: ``--replace-old`` was given.
        yes: ``--yes`` was given.

    Returns:
        The choice.
    """
    if replace_old:
        return cls.REMOVE
    return cls.KEEP if yes else cls.ASK

OldEntryCleaner

Bases: Protocol

Finds and removes older entries that start this same server.

Only registrars that can read their client's entries implement this. Claude Code's are held by its CLI, so it doesn't.

Methods:
old_entries
old_entries() -> list[tuple[str, str]]

List the older entries for this server.

Returns:

Type Description
list[tuple[str, str]]

(name, launcher file name) for each, safe to print.

Source code in src/solidworks_mcp/setup/registration.py
def old_entries(self) -> list[tuple[str, str]]:
    """List the older entries for this server.

    Returns:
        ``(name, launcher file name)`` for each, safe to print.
    """
    ...
remove_entry
remove_entry(name: str, *, dry_run: bool) -> EntryChange

Remove one entry by name.

Parameters:

Name Type Description Default
name str

The entry's key.

required
dry_run bool

Report what would change, and change nothing.

required

Returns:

Type Description
EntryChange

What happened.

Source code in src/solidworks_mcp/setup/registration.py
def remove_entry(self, name: str, *, dry_run: bool) -> EntryChange:
    """Remove one entry by name.

    Args:
        name: The entry's key.
        dry_run: Report what would change, and change nothing.

    Returns:
        What happened.
    """
    ...

Functions:

_choose_clients

_choose_clients(clients: list[Client], keys: list[str] | None, yes: bool, command: str) -> list[Client]

Pick the clients to configure: by --client, --yes or a prompt.

Parameters:

Name Type Description Default
clients list[Client]

Every supported client.

required
keys list[str] | None

Keys from --client, if any.

required
yes bool

Take every client found, without asking.

required
command str

The launcher this run will write, for the status column.

required

Returns:

Type Description
list[Client]

The chosen clients, in the order offered.

Raises:

Type Description
BadParameter

An unknown key, or an answer that isn't a client number.

Source code in src/solidworks_mcp/setup/cli.py
def _choose_clients(
    clients: list[Client], keys: list[str] | None, yes: bool, command: str
) -> list[Client]:
    """Pick the clients to configure: by ``--client``, ``--yes`` or a prompt.

    Args:
        clients: Every supported client.
        keys: Keys from ``--client``, if any.
        yes: Take every client found, without asking.
        command: The launcher this run will write, for the status column.

    Returns:
        The chosen clients, in the order offered.

    Raises:
        typer.BadParameter: An unknown key, or an answer that isn't a client number.
    """
    if keys:
        by_key = {c.key: c for c in clients}
        unknown = [k for k in keys if k not in by_key]
        if unknown:
            raise typer.BadParameter(
                f"Unknown client {', '.join(unknown)}; choose from {', '.join(by_key)}.",
                param_hint="--client",
            )
        return [by_key[k] for k in keys]

    table = Table(show_header=True, header_style="bold")
    for column in ("#", "Client", "Status"):
        table.add_column(column)
    for number, target in enumerate(clients, 1):
        table.add_row(str(number), target.name, _status(target, command))
    console.print(table)

    if yes:
        return [c for c in clients if c.found]
    found = [str(n) for n, c in enumerate(clients, 1) if c.found]
    answer = Prompt.ask(
        "Which clients? Numbers separated by commas, or 'all'",
        default=",".join(found) or "all",
    )
    return _parse_choice(answer, clients)

_choose_mode

_choose_mode(mock: bool, yes: bool) -> bool

Decide between a real and a mock-mode entry.

Parameters:

Name Type Description Default
mock bool

--mock was given.

required
yes bool

Don't ask; with no SolidWorks found, still write a real entry.

required

Returns:

Type Description
bool

Whether to write a mock-mode entry.

Source code in src/solidworks_mcp/setup/cli.py
def _choose_mode(mock: bool, yes: bool) -> bool:
    """Decide between a real and a mock-mode entry.

    Args:
        mock: ``--mock`` was given.
        yes: Don't ask; with no SolidWorks found, still write a real entry.

    Returns:
        Whether to write a mock-mode entry.
    """
    if mock:
        console.print("Mock mode: the server answers without SolidWorks.")
        return True
    years = installed_versions()
    if years:
        found = ", ".join(f"SOLIDWORKS {year}" for year in years)
        console.print(f"Found {found}. The server connects to the running SolidWorks.")
        return False
    console.print("[yellow]No SolidWorks install was found in the registry.[/yellow]")
    if yes:
        return False
    return Confirm.ask(
        "Set up mock mode instead (for trying the tools without SolidWorks)?"
    )

_choose_year

_choose_year(requested: int | None) -> int | None

Decide the SolidWorks year a real entry starts the server with.

Parameters:

Name Type Description Default
requested int | None

--year, if given.

required

Returns:

Type Description
int | None

The requested year, else the newest installed one; None when no

int | None

install is found and none was requested (the server then warns and

int | None

uses the newest it finds).

Raises:

Type Description
Exit

With code 1, before anything is written, when the requested year isn't installed.

Source code in src/solidworks_mcp/setup/cli.py
def _choose_year(requested: int | None) -> int | None:
    """Decide the SolidWorks year a real entry starts the server with.

    Args:
        requested: ``--year``, if given.

    Returns:
        The requested year, else the newest installed one; ``None`` when no
        install is found and none was requested (the server then warns and
        uses the newest it finds).

    Raises:
        typer.Exit: With code 1, before anything is written, when the
            requested year isn't installed.
    """
    years = installed_versions()
    if requested is None:
        if years:
            console.print(
                f"The server will work with SOLIDWORKS {years[0]} (--year {years[0]})."
            )
            return years[0]
        return None
    if years and requested not in years:
        installed = ", ".join(str(found) for found in years)
        console.print(
            f"[red]SOLIDWORKS {requested} isn't installed here; installed: "
            f"{installed}.[/red] No config was changed."
        )
        raise typer.Exit(1)
    return requested

_clean_old_entries

_clean_old_entries(target: Client, cleaner: OldEntryCleaner, choice: OldEntryChoice, dry_run: bool) -> bool

Offer to remove older entries that start this same server.

Only each entry's name and launcher file name are shown; nothing else from the file is.

Parameters:

Name Type Description Default
target Client

The client.

required
cleaner OldEntryCleaner

Its registrar, which can read and remove entries.

required
choice OldEntryChoice

Whether to ask, remove without asking, or only report.

required
dry_run bool

Report what would change, and change nothing.

required

Returns:

Type Description
bool

Whether every removal that was attempted succeeded.

Source code in src/solidworks_mcp/setup/cli.py
def _clean_old_entries(
    target: Client, cleaner: OldEntryCleaner, choice: OldEntryChoice, dry_run: bool
) -> bool:
    """Offer to remove older entries that start this same server.

    Only each entry's name and launcher file name are shown; nothing else
    from the file is.

    Args:
        target: The client.
        cleaner: Its registrar, which can read and remove entries.
        choice: Whether to ask, remove without asking, or only report.
        dry_run: Report what would change, and change nothing.

    Returns:
        Whether every removal that was attempted succeeded.
    """
    old = cleaner.old_entries()
    if not old:
        return True
    listed = ", ".join(f"{name} ({launcher})" for name, launcher in old)
    console.print(
        f"  {target.name} also has older entries that start this server: {listed}. "
        "With both, every tool loads twice."
    )
    if choice is OldEntryChoice.KEEP:
        console.print(
            "  Kept. Run solidworks-mcp setup again with --replace-old to remove them."
        )
        return True
    if choice is OldEntryChoice.ASK and not Confirm.ask("  Remove them?", default=True):
        return True
    for name, _ in old:
        try:
            change = cleaner.remove_entry(name, dry_run=dry_run)
        except ConfigFileError as exc:
            console.print(f"[red]  {target.name}: {exc}[/red]")
            return False
        console.print(f"  {_describe(change, dry_run, label=name)}")
    return True

_configure

_configure(target: Client, command: str, args: list[str], remove: bool, dry_run: bool, choice: OldEntryChoice) -> bool

Add or remove the entry for one client, then deal with older entries.

Parameters:

Name Type Description Default
target Client

The client.

required
command str

The launcher.

required
args list[str]

Its arguments.

required
remove bool

Remove the entry instead of adding it.

required
dry_run bool

Report what would change, and change nothing.

required
choice OldEntryChoice

What to do about older entries for this server.

required

Returns:

Type Description
bool

Whether it succeeded.

Source code in src/solidworks_mcp/setup/cli.py
def _configure(
    target: Client,
    command: str,
    args: list[str],
    remove: bool,
    dry_run: bool,
    choice: OldEntryChoice,
) -> bool:
    """Add or remove the entry for one client, then deal with older entries.

    Args:
        target: The client.
        command: The launcher.
        args: Its arguments.
        remove: Remove the entry instead of adding it.
        dry_run: Report what would change, and change nothing.
        choice: What to do about older entries for this server.

    Returns:
        Whether it succeeded.
    """
    entry: dict[str, Any] | None = None if remove else target.entry(command, args)
    registrar = registrar_for(target)
    try:
        change = registrar.apply(entry, dry_run=dry_run)
    except ConfigFileError as exc:
        console.print(f"[red]{target.name}: {exc}[/red]")
        return False
    console.print(f"{target.name}: {_describe(change, dry_run)}")
    if isinstance(registrar, OldEntryCleaner):
        return _clean_old_entries(target, registrar, choice, dry_run)
    return True

_describe

_describe(change: EntryChange, dry_run: bool, label: str = SERVER_NAME) -> str

Say what happened to the entry, in one line.

Parameters:

Name Type Description Default
change EntryChange

The registrar's result.

required
dry_run bool

Whether nothing was actually changed.

required
label str

The entry's name.

SERVER_NAME

Returns:

Type Description
str

The line, naming the backup if one was made.

Source code in src/solidworks_mcp/setup/cli.py
def _describe(change: EntryChange, dry_run: bool, label: str = SERVER_NAME) -> str:
    """Say what happened to the entry, in one line.

    Args:
        change: The registrar's result.
        dry_run: Whether nothing was actually changed.
        label: The entry's name.

    Returns:
        The line, naming the backup if one was made.
    """
    if change.action == "not run":
        return f"'claude' is not on the PATH; run: {change.location}"
    verb = change.action
    if dry_run and verb != "unchanged":
        verb = f"would be {verb}"
    line = f"{label} entry {verb} ({escape(change.location)})"
    if change.previous:
        line += f"; it ran {escape(change.previous)} before"
    if change.backup:
        line += f"; backup: {change.backup.name}"
    return line

_parse_choice

_parse_choice(answer: str, clients: list[Client]) -> list[Client]

Turn an answer like 1, 3 or all into clients.

Parameters:

Name Type Description Default
answer str

What the user typed.

required
clients list[Client]

The numbered clients (numbered from 1).

required

Returns:

Type Description
list[Client]

The chosen clients, each once, in the order typed.

Raises:

Type Description
BadParameter

A part that isn't a listed number.

Source code in src/solidworks_mcp/setup/cli.py
def _parse_choice(answer: str, clients: list[Client]) -> list[Client]:
    """Turn an answer like ``1, 3`` or ``all`` into clients.

    Args:
        answer: What the user typed.
        clients: The numbered clients (numbered from 1).

    Returns:
        The chosen clients, each once, in the order typed.

    Raises:
        typer.BadParameter: A part that isn't a listed number.
    """
    if answer.strip().lower() == "all":
        return list(clients)
    chosen: list[Client] = []
    for part in answer.replace(" ", "").split(","):
        if not part:
            continue
        if not (part.isdigit() and 1 <= int(part) <= len(clients)):
            raise typer.BadParameter(f"'{part}' is not a client number.")
        target = clients[int(part) - 1]
        if target not in chosen:
            chosen.append(target)
    return chosen

_run_check

_run_check(command: str, args: list[str]) -> HandshakeResult

Run the start-up check in mock mode, whatever mode is being configured.

Mock mode proves the launcher and the stdio handshake without starting or touching SolidWorks.

Parameters:

Name Type Description Default
command str

The launcher.

required
args list[str]

The arguments being written to the configs.

required

Returns:

Type Description
HandshakeResult

The check's outcome.

Source code in src/solidworks_mcp/setup/cli.py
def _run_check(command: str, args: list[str]) -> HandshakeResult:
    """Run the start-up check in mock mode, whatever mode is being configured.

    Mock mode proves the launcher and the stdio handshake without starting
    or touching SolidWorks.

    Args:
        command: The launcher.
        args: The arguments being written to the configs.

    Returns:
        The check's outcome.
    """
    check_args = args if "--mock" in args else [*args, "--mock"]
    return check_server(command, check_args)

_status

_status(target: Client, command: str) -> str

Describe a client for the table: found or not, and its config state.

The only value shown from the file is the solidworks entry's own command, and only when it runs a different copy than command.

Parameters:

Name Type Description Default
target Client

The client.

required
command str

The launcher this setup run will write.

required

Returns:

Type Description
str

One rich-formatted line.

Source code in src/solidworks_mcp/setup/cli.py
def _status(target: Client, command: str) -> str:
    """Describe a client for the table: found or not, and its config state.

    The only value shown from the file is the ``solidworks`` entry's own
    command, and only when it runs a different copy than ``command``.

    Args:
        target: The client.
        command: The launcher this setup run will write.

    Returns:
        One rich-formatted line.
    """
    if not target.found:
        return "[dim]not found[/dim]"
    if target.config_path is None:
        return "found (uses the claude command)"
    where = escape(str(target.config_path))
    if not target.config_path.exists():
        return f"found; will create {where}"
    try:
        entry = current_entry(target.config_path, target.section, SERVER_NAME)
    except ConfigFileError:
        return f"[red]config is not valid JSON[/red]: {where}"
    if entry is None:
        return f"config found: {where}"
    current = entry.get("command") if isinstance(entry, dict) else None
    if isinstance(current, str) and current != command:
        # e.g. a test copy's .venv: deleting that folder would break the client.
        return (
            f"has a solidworks entry: {where}\n"
            f"[yellow]it runs another copy: {escape(current)}[/yellow]"
        )
    return f"has a solidworks entry: {where}"

check_server

check_server(command: str, args: list[str], timeout: float = 90.0) -> HandshakeResult

Start command args and run the MCP handshake over its stdio.

The process is always killed afterwards. Its stderr goes to a temporary file: an unread pipe can fill up and stall the server.

Parameters:

Name Type Description Default
command str

The launcher to start.

required
args list[str]

Its arguments (setup passes --mock).

required
timeout float

Seconds to wait for both replies, in total.

90.0

Returns:

Type Description
HandshakeResult

Whether it worked, the tool count and a message.

Source code in src/solidworks_mcp/setup/handshake.py
def check_server(
    command: str, args: list[str], timeout: float = 90.0
) -> HandshakeResult:
    """Start ``command args`` and run the MCP handshake over its stdio.

    The process is always killed afterwards. Its stderr goes to a temporary
    file: an unread pipe can fill up and stall the server.

    Args:
        command: The launcher to start.
        args: Its arguments (setup passes ``--mock``).
        timeout: Seconds to wait for both replies, in total.

    Returns:
        Whether it worked, the tool count and a message.
    """
    with tempfile.TemporaryFile() as stderr:
        try:
            process = subprocess.Popen(
                [command, *args],
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=stderr,
            )
        except OSError as exc:
            return HandshakeResult(
                False, 0, f"Could not start {command}: {exc.strerror or exc}."
            )
        try:
            return _converse(process, time.monotonic() + timeout)
        finally:
            process.kill()
            process.wait()

current_entry

current_entry(path: Path, section: str, name: str) -> Any

Read one server entry.

Parameters:

Name Type Description Default
path Path

The JSON config file.

required
section str

The object that holds the servers (mcpServers/servers).

required
name str

The server's key in that object.

required

Returns:

Type Description
Any

The entry, or None when the file, section or key is absent.

Source code in src/solidworks_mcp/setup/config_file.py
def current_entry(path: Path, section: str, name: str) -> Any:
    """Read one server entry.

    Args:
        path: The JSON config file.
        section: The object that holds the servers (``mcpServers``/``servers``).
        name: The server's key in that object.

    Returns:
        The entry, or ``None`` when the file, section or key is absent.
    """
    return server_entries(path, section).get(name)

find_clients

find_clients(env: Mapping[str, str] | None = None, which: Callable[[str], str | None] = shutil.which) -> list[Client]

List every supported client, found or not, in the order they are offered.

Parameters:

Name Type Description Default
env Mapping[str, str] | None

The environment; defaults to os.environ.

None
which Callable[[str], str | None]

How to look up a command on the PATH (for claude).

which

Returns:

Type Description
list[Client]

The clients, with found and config_path filled in.

Source code in src/solidworks_mcp/setup/clients.py
def find_clients(
    env: Mapping[str, str] | None = None,
    which: Callable[[str], str | None] = shutil.which,
) -> list[Client]:
    """List every supported client, found or not, in the order they are offered.

    Args:
        env: The environment; defaults to ``os.environ``.
        which: How to look up a command on the PATH (for ``claude``).

    Returns:
        The clients, with ``found`` and ``config_path`` filled in.
    """
    env = os.environ if env is None else env
    home = Path(env.get("USERPROFILE") or env.get("HOME") or Path.home())
    roaming = Path(env.get("APPDATA", ""))

    desktop_file, desktop_found = claude_desktop_config(env)
    vscode_file = roaming / "Code/User/mcp.json"
    cursor_file = home / ".cursor/mcp.json"
    lmstudio_file = home / ".lmstudio/mcp.json"
    return [
        Client(
            "claude-desktop",
            "Claude Desktop",
            desktop_found,
            desktop_file,
            # Claude Desktop also saves its preferences to this file, so a copy
            # still running in the tray may write over the entry; quitting it
            # fully before rerunning setup avoids that.
            restart_hint="Quit Claude Desktop from the system tray, then reopen it; "
            "Settings > Developer then shows solidworks as running. If the entry "
            "is gone, quit Claude Desktop fully and run solidworks-mcp setup again.",
        ),
        Client(
            "claude-code",
            "Claude Code",
            which("claude") is not None,
            None,
            restart_hint="Run /mcp in a new Claude Code session.",
        ),
        Client(
            "vscode",
            "VS Code",
            vscode_file.parent.is_dir(),
            vscode_file,
            section="servers",
            stdio_type=True,
            restart_hint="Run 'MCP: List Servers' from the Command Palette and start solidworks.",
        ),
        Client(
            "cursor",
            "Cursor",
            cursor_file.parent.is_dir(),
            cursor_file,
            restart_hint="Restart Cursor and check solidworks is enabled in its MCP settings.",
        ),
        Client(
            "lmstudio",
            "LM Studio",
            lmstudio_file.parent.is_dir(),
            lmstudio_file,
            restart_hint="Restart LM Studio and enable solidworks in the Program tab.",
        ),
    ]

in_uv_cache

in_uv_cache(python: str) -> bool

Whether an interpreter lives in a temporary environment in uv's cache.

uv tool install environments (under uv\tools) and project .venv folders are persistent and don't count.

Parameters:

Name Type Description Default
python str

The interpreter's path.

required

Returns:

Type Description
bool

True when a cache folder holds an archive-v<n> folder on the

bool

path to it.

Source code in src/solidworks_mcp/setup/launcher.py
def in_uv_cache(python: str) -> bool:
    """Whether an interpreter lives in a temporary environment in uv's cache.

    ``uv tool install`` environments (under ``uv\\tools``) and project
    ``.venv`` folders are persistent and don't count.

    Args:
        python: The interpreter's path.

    Returns:
        True when a ``cache`` folder holds an ``archive-v<n>`` folder on the
        path to it.
    """
    parts = [part.lower() for part in Path(python).parts]
    return any(
        _UV_ARCHIVE.fullmatch(part) and "cache" in parts[:index]
        for index, part in enumerate(parts)
    )

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)

registrar_for

registrar_for(client: Client) -> Registrar

The registrar that knows how to configure client.

Parameters:

Name Type Description Default
client Client

A client from :func:~.clients.find_clients.

required

Returns:

Type Description
Registrar

A file registrar for file-configured clients, else the Claude CLI one.

Source code in src/solidworks_mcp/setup/registration.py
def registrar_for(client: Client) -> Registrar:
    """The registrar that knows how to configure ``client``.

    Args:
        client: A client from :func:`~.clients.find_clients`.

    Returns:
        A file registrar for file-configured clients, else the Claude CLI one.
    """
    if client.config_path is None:
        return ClaudeCliRegistrar(client)
    return JsonFileRegistrar(client)

server_command

server_command(mock: bool = False, year: int | None = None, python: str | None = None, lasting: bool = True, uvx_finder: Callable[[], str | None] = find_uvx) -> tuple[str, list[str]]

The command and arguments that start this server.

Parameters:

Name Type Description Default
mock bool

Add --mock, so the server answers without SolidWorks.

False
year int | None

Add --year <year>, the SolidWorks release the server works with; ignored with mock.

None
python str | None

The interpreter to look beside; defaults to sys.executable.

None
lasting bool

Return a command that outlives a temporary uv environment. False gives the launcher that runs right now, which is what the start-up check needs.

True
uvx_finder Callable[[], str | None]

Finds uvx, replaceable in tests.

find_uvx

Returns:

Type Description
str

(command, args): uvx running this package when lasting and

list[str]

the interpreter is in uv's cache; otherwise the solidworks-mcp

tuple[str, list[str]]

console script in the environment's scripts folder or, without one,

tuple[str, list[str]]

this Python running the server module.

Source code in src/solidworks_mcp/setup/launcher.py
def server_command(
    mock: bool = False,
    year: int | None = None,
    python: str | None = None,
    lasting: bool = True,
    uvx_finder: Callable[[], str | None] = find_uvx,
) -> tuple[str, list[str]]:
    """The command and arguments that start this server.

    Args:
        mock: Add ``--mock``, so the server answers without SolidWorks.
        year: Add ``--year <year>``, the SolidWorks release the server works
            with; ignored with ``mock``.
        python: The interpreter to look beside; defaults to ``sys.executable``.
        lasting: Return a command that outlives a temporary uv environment.
            ``False`` gives the launcher that runs right now, which is what
            the start-up check needs.
        uvx_finder: Finds ``uvx``, replaceable in tests.

    Returns:
        ``(command, args)``: ``uvx`` running this package when ``lasting`` and
        the interpreter is in uv's cache; otherwise the ``solidworks-mcp``
        console script in the environment's scripts folder or, without one,
        this Python running the server module.
    """
    python = python or sys.executable
    if mock:
        flags = ["--mock"]
    else:
        flags = ["--year", str(year)] if year else []
    if lasting and in_uv_cache(python):
        uvx = uvx_finder()
        if uvx:
            return uvx, [PACKAGE, *flags]
    scripts = Path(python).parent
    # `.exe` on Windows; the bare name elsewhere.
    for name in ("solidworks-mcp.exe", "solidworks-mcp"):
        launcher = scripts / name
        if launcher.is_file():
            return str(launcher), flags
    return python, ["-m", "solidworks_mcp.server", *flags]

setup

setup(client: list[str] | None = typer.Option(None, '--client', help='Client to configure: claude-desktop, claude-code, vscode, cursor, lmstudio. Repeat for several. Default: choose from a list.'), yes: bool = typer.Option(False, '--yes', '-y', help="Don't ask; configure every client found."), dry_run: bool = typer.Option(False, '--dry-run', help='Show what would change, and change nothing.'), remove: bool = typer.Option(False, '--remove', help='Remove the solidworks entry instead of adding it.'), mock: bool = typer.Option(False, '--mock', help='Configure mock mode, which needs no SolidWorks.'), skip_check: bool = typer.Option(False, '--skip-check', help="Don't start the server to check it first."), replace_old: bool = typer.Option(False, '--replace-old', help='Remove older hand-made entries that start this server (run-mcp.ps1 and the like) without asking.'), year: int | None = typer.Option(None, '--year', min=1995, max=2100, help='SolidWorks year the server works with, e.g. 2026. Default: the newest installed.')) -> None

Find your SolidWorks and MCP clients, and add this server to them.

Only the solidworks entry of each client's config is changed. The file is backed up first, and no other entry is ever printed.

Parameters:

Name Type Description Default
client list[str] | None

Client keys to configure; None to choose from a list.

Option(None, '--client', help='Client to configure: claude-desktop, claude-code, vscode, cursor, lmstudio. Repeat for several. Default: choose from a list.')
yes bool

Configure every client found, without asking.

Option(False, '--yes', '-y', help="Don't ask; configure every client found.")
dry_run bool

Report what would change, and change nothing.

Option(False, '--dry-run', help='Show what would change, and change nothing.')
remove bool

Remove the entry instead of adding it.

Option(False, '--remove', help='Remove the solidworks entry instead of adding it.')
mock bool

Write a mock-mode entry.

Option(False, '--mock', help='Configure mock mode, which needs no SolidWorks.')
skip_check bool

Skip the start-up check.

Option(False, '--skip-check', help="Don't start the server to check it first.")
replace_old bool

Remove older entries for this server without asking.

Option(False, '--replace-old', help='Remove older hand-made entries that start this server (run-mcp.ps1 and the like) without asking.')
year int | None

The SolidWorks year to start the server with; the newest installed when None.

Option(None, '--year', min=1995, max=2100, help='SolidWorks year the server works with, e.g. 2026. Default: the newest installed.')

Raises:

Type Description
Exit

With code 1 if the check failed or a client couldn't be configured.

Source code in src/solidworks_mcp/setup/cli.py
@app.command()
def setup(
    client: list[str] | None = typer.Option(
        None,
        "--client",
        help="Client to configure: claude-desktop, claude-code, vscode, cursor, "
        "lmstudio. Repeat for several. Default: choose from a list.",
    ),
    yes: bool = typer.Option(
        False, "--yes", "-y", help="Don't ask; configure every client found."
    ),
    dry_run: bool = typer.Option(
        False, "--dry-run", help="Show what would change, and change nothing."
    ),
    remove: bool = typer.Option(
        False, "--remove", help="Remove the solidworks entry instead of adding it."
    ),
    mock: bool = typer.Option(
        False, "--mock", help="Configure mock mode, which needs no SolidWorks."
    ),
    skip_check: bool = typer.Option(
        False, "--skip-check", help="Don't start the server to check it first."
    ),
    replace_old: bool = typer.Option(
        False,
        "--replace-old",
        help="Remove older hand-made entries that start this server "
        "(run-mcp.ps1 and the like) without asking.",
    ),
    year: int | None = typer.Option(
        None,
        "--year",
        min=1995,
        max=2100,
        help="SolidWorks year the server works with, e.g. 2026. "
        "Default: the newest installed.",
    ),
) -> None:
    """Find your SolidWorks and MCP clients, and add this server to them.

    Only the solidworks entry of each client's config is changed. The file is
    backed up first, and no other entry is ever printed.

    Args:
        client: Client keys to configure; ``None`` to choose from a list.
        yes: Configure every client found, without asking.
        dry_run: Report what would change, and change nothing.
        remove: Remove the entry instead of adding it.
        mock: Write a mock-mode entry.
        skip_check: Skip the start-up check.
        replace_old: Remove older entries for this server without asking.
        year: The SolidWorks year to start the server with; the newest
            installed when ``None``.

    Raises:
        typer.Exit: With code 1 if the check failed or a client couldn't be
            configured.
    """
    console.print("[bold]SolidWorks MCP setup[/bold]\n")
    if not remove:
        mock = _choose_mode(mock, yes)
        year = None if mock else _choose_year(year)
    command, args = server_command(mock=mock, year=year)
    if in_uv_cache(sys.executable) and args[:1] != [PACKAGE]:
        console.print(
            "[yellow]Running from uv's cache, and uvx was not found: the entry "
            "will stop working when uv cleans its cache. Install uv so uvx is on "
            "the PATH, then run setup again.[/yellow]"
        )

    chosen = _choose_clients(find_clients(), client, yes, command)
    if not chosen:
        console.print("No clients chosen; nothing to do.")
        return

    # Check before writing, so a broken launcher never reaches a config.
    if not (remove or skip_check):
        console.print(
            "\nChecking that the server starts (mock mode, SolidWorks is not touched)..."
        )
        # The launcher running now: a `uvx` entry would fetch from PyPI instead.
        result = _run_check(*server_command(mock=mock, year=year, lasting=False))
        if not result.ok:
            console.print(f"[red]{result.message}[/red] No config was changed.")
            raise typer.Exit(1)
        console.print(f"[green]{result.message}[/green]")

    console.print()
    # One client's failure doesn't stop the others; it sets the exit code.
    choice = OldEntryChoice.from_flags(replace_old=replace_old, yes=yes)
    failures = sum(
        not _configure(target, command, args, remove, dry_run, choice)
        for target in chosen
    )

    if dry_run:
        console.print("\nDry run: nothing was changed.")
    elif not remove:
        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)