Skip to content

solidworks_mcp.setup.registration

solidworks_mcp.setup.registration

Add or remove the server entry for one client, by that client's own means.

Most clients read a JSON file (:class:JsonFileRegistrar); Claude Code is configured through its claude mcp command (:class:ClaudeCliRegistrar). :func:registrar_for picks one, so the setup wizard never needs to know which kind of client it is talking to.

Attributes

Runner module-attribute

Runner = Callable[..., subprocess.CompletedProcess[str]]

SERVER_NAME module-attribute

SERVER_NAME = 'solidworks'

Classes

ClaudeCliRegistrar

ClaudeCliRegistrar(client: Client, run: Runner = subprocess.run)

Registers the server with claude mcp add --scope user.

Bind to Claude Code.

Parameters:

Name Type Description Default
client Client

The Claude Code client; found says if claude is on the PATH.

required
run Runner

How to run a command (subprocess.run, or a fake in tests).

run
Source code in src/solidworks_mcp/setup/registration.py
def __init__(self, client: Client, run: Runner = subprocess.run) -> None:
    """Bind to Claude Code.

    Args:
        client: The Claude Code client; ``found`` says if ``claude`` is on the PATH.
        run: How to run a command (``subprocess.run``, or a fake in tests).
    """
    self._found = client.found
    self._run = run
Methods:
apply
apply(entry: dict[str, Any] | None, *, dry_run: bool) -> EntryChange

Run claude mcp to add or remove the server.

Without claude on the PATH, or with dry_run, nothing runs and the change reports the command for the user to run.

Parameters:

Name Type Description Default
entry dict[str, Any] | None

The entry to register, or None to remove it.

required
dry_run bool

Report the command, and run nothing.

required

Returns:

Type Description
EntryChange

What happened; location is the command line.

Raises:

Type Description
ConfigFileError

claude mcp add failed.

Source code in src/solidworks_mcp/setup/registration.py
def apply(self, entry: dict[str, Any] | None, *, dry_run: bool) -> EntryChange:
    """Run ``claude mcp`` to add or remove the server.

    Without ``claude`` on the PATH, or with ``dry_run``, nothing runs and
    the change reports the command for the user to run.

    Args:
        entry: The entry to register, or ``None`` to remove it.
        dry_run: Report the command, and run nothing.

    Returns:
        What happened; ``location`` is the command line.

    Raises:
        ConfigFileError: ``claude mcp add`` failed.
    """
    remove = ["claude", "mcp", "remove", "--scope", "user", SERVER_NAME]
    command = remove if entry is None else self._add_command(entry)
    shown = subprocess.list2cmdline(command)
    action = "removed" if entry is None else "added"
    if dry_run or not self._found:
        return EntryChange(shown, "not run" if not self._found else action)

    # `add` fails if the name exists, so clear any old entry first; a
    # failed remove only means there was nothing to remove.
    self._run(remove, capture_output=True, text=True)
    if entry is None:
        return EntryChange(shown, action)
    result = self._run(command, capture_output=True, text=True)
    if result.returncode != 0:
        raise ConfigFileError(
            f"'claude mcp add' failed (exit {result.returncode}); run it yourself: {shown}"
        )
    return EntryChange(shown, action)

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.

JsonFileRegistrar

JsonFileRegistrar(client: Client)

Edits the solidworks entry in the client's JSON config file.

It also implements :class:OldEntryCleaner, since it can read the file.

Bind to a file-configured client.

Parameters:

Name Type Description Default
client Client

A client whose config_path is set.

required
Source code in src/solidworks_mcp/setup/registration.py
def __init__(self, client: Client) -> None:
    """Bind to a file-configured client.

    Args:
        client: A client whose ``config_path`` is set.
    """
    assert client.config_path is not None
    self._path = client.config_path
    self._section = client.section
Methods:
apply
apply(entry: dict[str, Any] | None, *, dry_run: bool) -> EntryChange

Set or remove the entry in the file (see :func:set_entry).

Parameters:

Name Type Description Default
entry dict[str, Any] | None

The entry to write, or None to remove it.

required
dry_run bool

Report what would change, and change nothing.

required

Returns:

Type Description
EntryChange

What happened, with the backup made before writing.

Source code in src/solidworks_mcp/setup/registration.py
def apply(self, entry: dict[str, Any] | None, *, dry_run: bool) -> EntryChange:
    """Set or remove the entry in the file (see :func:`set_entry`).

    Args:
        entry: The entry to write, or ``None`` to remove it.
        dry_run: Report what would change, and change nothing.

    Returns:
        What happened, with the backup made before writing.
    """
    return set_entry(self._path, self._section, SERVER_NAME, entry, dry_run=dry_run)
old_entries
old_entries() -> list[tuple[str, str]]

List the file's older entries that start this server.

Returns:

Type Description
list[tuple[str, str]]

(name, launcher file name) for each (see :mod:.legacy).

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

    Returns:
        ``(name, launcher file name)`` for each (see :mod:`.legacy`).
    """
    servers = server_entries(self._path, self._section)
    return [(name, launcher_name(servers[name])) for name in old_entries(servers)]
remove_entry
remove_entry(name: str, *, dry_run: bool) -> EntryChange

Remove one entry from the file, with the same care as :meth:apply.

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, with the backup made before writing.

Source code in src/solidworks_mcp/setup/registration.py
def remove_entry(self, name: str, *, dry_run: bool) -> EntryChange:
    """Remove one entry from the file, with the same care as :meth:`apply`.

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

    Returns:
        What happened, with the backup made before writing.
    """
    return set_entry(self._path, self._section, name, None, dry_run=dry_run)

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.
    """
    ...

Registrar

Bases: Protocol

Changes a client's solidworks entry.

Methods:
apply
apply(entry: dict[str, Any] | None, *, dry_run: bool) -> EntryChange

Add or update entry, or remove the entry when it is None.

Parameters:

Name Type Description Default
entry dict[str, Any] | None

The entry to write, or None to remove it.

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 apply(self, entry: dict[str, Any] | None, *, dry_run: bool) -> EntryChange:
    """Add or update ``entry``, or remove the entry when it is None.

    Args:
        entry: The entry to write, or ``None`` to remove it.
        dry_run: Report what would change, and change nothing.

    Returns:
        What happened.
    """
    ...

Functions:

launcher_name

launcher_name(entry: Any) -> str

The program file an entry starts, for showing to the user.

Parameters:

Name Type Description Default
entry Any

A server entry from a client config.

required

Returns:

Type Description
str

The file name of its command (e.g. powershell.exe), without the

str

folder; ? if it has none.

Source code in src/solidworks_mcp/setup/legacy.py
def launcher_name(entry: Any) -> str:
    """The program file an entry starts, for showing to the user.

    Args:
        entry: A server entry from a client config.

    Returns:
        The file name of its ``command`` (e.g. ``powershell.exe``), without the
        folder; ``?`` if it has none.
    """
    command = entry.get("command") if isinstance(entry, dict) else None
    return (
        PureWindowsPath(command).name if isinstance(command, str) and command else "?"
    )

old_entries

old_entries(servers: dict[str, Any]) -> list[str]

Name every entry, besides solidworks, that starts this server.

Parameters:

Name Type Description Default
servers dict[str, Any]

A config's server entries (mcpServers or servers).

required

Returns:

Type Description
list[str]

Their names, in the config's order.

Source code in src/solidworks_mcp/setup/legacy.py
def old_entries(servers: dict[str, Any]) -> list[str]:
    """Name every entry, besides ``solidworks``, that starts this server.

    Args:
        servers: A config's server entries (``mcpServers`` or ``servers``).

    Returns:
        Their names, in the config's order.
    """
    return [
        name
        for name, entry in servers.items()
        if name != SERVER_NAME and starts_this_server(entry)
    ]

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_entries

server_entries(path: Path, section: str) -> dict[str, Any]

Read every server entry in a config, for inspection in code only.

Callers must not print the entries: they may hold other servers' secrets.

Parameters:

Name Type Description Default
path Path

The JSON config file.

required
section str

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

required

Returns:

Type Description
dict[str, Any]

The entries by name; {} when the file or section is absent or the

dict[str, Any]

section isn't an object.

Source code in src/solidworks_mcp/setup/config_file.py
def server_entries(path: Path, section: str) -> dict[str, Any]:
    """Read every server entry in a config, for inspection in code only.

    Callers must not print the entries: they may hold other servers' secrets.

    Args:
        path: The JSON config file.
        section: The object that holds the servers (``mcpServers``/``servers``).

    Returns:
        The entries by name; ``{}`` when the file or section is absent or the
        section isn't an object.
    """
    servers = read_config(path).get(section)
    return servers if isinstance(servers, dict) else {}

set_entry

set_entry(path: Path, section: str, name: str, entry: dict[str, Any] | None, *, dry_run: bool = False) -> EntryChange

Set section.name to entry, or delete it when entry is None.

Every other key in the file keeps its value and order. Before writing, the file is backed up; it is then written atomically as UTF-8 without a BOM, and read back to confirm it parses and holds the new entry.

Parameters:

Name Type Description Default
path Path

The JSON config file; created, with its folder, if missing.

required
section str

The object that holds the servers.

required
name str

The server's key in that object.

required
entry dict[str, Any] | None

The new entry, or None to remove it.

required
dry_run bool

Work out the action but write nothing.

False

Returns:

Type Description
EntryChange

What happened (or, with dry_run, what would happen).

Raises:

Type Description
ConfigFileError

The file can't be parsed, the section isn't an object, or the written file doesn't hold the entry.

Source code in src/solidworks_mcp/setup/config_file.py
def set_entry(
    path: Path,
    section: str,
    name: str,
    entry: dict[str, Any] | None,
    *,
    dry_run: bool = False,
) -> EntryChange:
    """Set ``section.name`` to ``entry``, or delete it when ``entry`` is None.

    Every other key in the file keeps its value and order. Before writing,
    the file is backed up; it is then written atomically as UTF-8 without a
    BOM, and read back to confirm it parses and holds the new entry.

    Args:
        path: The JSON config file; created, with its folder, if missing.
        section: The object that holds the servers.
        name: The server's key in that object.
        entry: The new entry, or ``None`` to remove it.
        dry_run: Work out the action but write nothing.

    Returns:
        What happened (or, with ``dry_run``, what would happen).

    Raises:
        ConfigFileError: The file can't be parsed, the section isn't an
            object, or the written file doesn't hold the entry.
    """
    data = read_config(path)
    servers = data.get(section, {})
    if not isinstance(servers, dict):
        raise ConfigFileError(
            f'"{section}" in {path} is not an object; left untouched.'
        )

    action = _action(servers, name, entry)
    previous = _command_of(servers.get(name)) if action == "updated" else None
    if action == "unchanged" or dry_run:
        return EntryChange(str(path), action, previous=previous)

    if entry is None:
        del servers[name]
    else:
        servers[name] = entry
    data[section] = servers

    backup = _backup(path)
    _write_atomically(path, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
    if current_entry(path, section, name) != entry:
        raise ConfigFileError(
            f"{path} did not hold the new entry after writing; "
            f"restore it from {backup}."
        )
    return EntryChange(str(path), action, backup, previous)