Skip to content

solidworks_mcp.adapters.solidworks

solidworks_mcp.adapters.solidworks

Grouped SolidWorks mixins for the PyWin32 adapter.

Attributes

__all__ module-attribute

__all__ = ['SolidWorksFeaturesMixin', 'SolidWorksIOMixin', 'SolidWorksSelectionMixin', 'SolidWorksSketchMixin']

Classes

SolidWorksFeaturesMixin

Expose SolidWorks feature methods via mixin-local implementation helpers.

SolidWorksIOMixin

Expose model open/save/create/configuration methods through a mixin.

Methods:
add_mate async
add_mate(component_a: str, component_b: str, entity_a: str = 'Front Plane', entity_b: str = 'Front Plane', mate_type: str = 'coincident', alignment: str = 'aligned', distance: float = 0.0, angle: float = 0.0) -> AdapterResult[dict[str, Any]]

Mate two components together.

Wraps IAssemblyDoc::AddMate5. The two entities are selected via IComponent2::FeatureByName + IFeature::Select2 rather than SelectByID2, which raises Type mismatch on this build.

That restricts the entities to named tree features — the reference planes and axes of each component. Plane-to-plane mating covers alignment and stacking, which is the common case; mating to a specific face or edge needs entity names this adapter cannot enumerate.

Parameters:

Name Type Description Default
component_a str

First component instance name, as reported by :meth:list_components.

required
component_b str

Second component instance name.

required
entity_a str

Named feature on the first component.

'Front Plane'
entity_b str

Named feature on the second component.

'Front Plane'
mate_type str

coincident, concentric, perpendicular, parallel, tangent, distance or angle.

'coincident'
alignment str

aligned, anti_aligned or closest.

'aligned'
distance float

Distance in millimetres, for a distance mate.

0.0
angle float

Angle in degrees, for an angle mate.

0.0

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The mate created, plus the bounding

AdapterResult[dict[str, Any]]

box before and after so the caller can see what moved. ERROR

AdapterResult[dict[str, Any]]

when the entities cannot be selected or SolidWorks rejects the

AdapterResult[dict[str, Any]]

mate.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Example::

await adapter.add_mate("plate-1", "plate-2")
Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def add_mate(
        self,
        component_a: str,
        component_b: str,
        entity_a: str = "Front Plane",
        entity_b: str = "Front Plane",
        mate_type: str = "coincident",
        alignment: str = "aligned",
        distance: float = 0.0,
        angle: float = 0.0,
    ) -> AdapterResult[dict[str, Any]]:
        """Mate two components together.

        Wraps ``IAssemblyDoc::AddMate5``.  The two entities are selected via
        ``IComponent2::FeatureByName`` + ``IFeature::Select2`` rather than
        ``SelectByID2``, which raises ``Type mismatch`` on this build.

        That restricts the entities to *named tree features* — the reference
        planes and axes of each component.  Plane-to-plane mating covers
        alignment and stacking, which is the common case; mating to a specific
        face or edge needs entity names this adapter cannot enumerate.

        Args:
            component_a (str): First component instance name, as reported by
                :meth:`list_components`.
            component_b (str): Second component instance name.
            entity_a (str): Named feature on the first component.
            entity_b (str): Named feature on the second component.
            mate_type (str): ``coincident``, ``concentric``, ``perpendicular``,
                ``parallel``, ``tangent``, ``distance`` or ``angle``.
            alignment (str): ``aligned``, ``anti_aligned`` or ``closest``.
            distance (float): Distance in millimetres, for a distance mate.
            angle (float): Angle in degrees, for an angle mate.

        Returns:
            AdapterResult[dict[str, Any]]: The mate created, plus the bounding
            box before and after so the caller can see what moved.  ``ERROR``
            when the entities cannot be selected or SolidWorks rejects the
            mate.

        Raises:
            Exception: Propagated through ``_handle_com_operation``.

        Example::

            await adapter.add_mate("plate-1", "plate-2")
        """
        adapter = self._adapter(self)
        if _doc_type(adapter) != 2:
            return AdapterResult(
                status=AdapterResultStatus.ERROR,
                error="add_mate requires an assembly document",
            )

        mate_key = str(mate_type).strip().lower()
        if mate_key not in _MATE_TYPES:
            return AdapterResult(
                status=AdapterResultStatus.ERROR,
                error=(
                    f"Unknown mate type '{mate_type}'. "
                    f"Use one of: {', '.join(sorted(_MATE_TYPES))}."
                ),
            )
        align_key = str(alignment).strip().lower()
        if align_key not in _MATE_ALIGNMENTS:
            return AdapterResult(
                status=AdapterResultStatus.ERROR,
                error=(
                    f"Unknown alignment '{alignment}'. "
                    f"Use one of: {', '.join(sorted(_MATE_ALIGNMENTS))}."
                ),
            )

        def _mate() -> dict[str, Any]:
            import math

            model = adapter.currentModel
            assembly = _sw_type_info.flagged(model, "IAssemblyDoc")

            components = adapter._attempt(
                lambda: assembly.GetComponents(True), default=None
            )
            if not isinstance(components, (list, tuple)):
                raise Exception("Could not read the assembly's components")

            wanted = {component_a: entity_a, component_b: entity_b}
            found: dict[str, Any] = {}
            for component in components:
                wrapped = _as_com(adapter, component, "IComponent2")
                if wrapped is None:
                    continue
                name = adapter._attempt(lambda w=wrapped: w.Name2, default=None)
                if name and str(name) in wanted:
                    found[str(name)] = wrapped

            missing = [n for n in (component_a, component_b) if n not in found]
            if missing:
                available = [
                    str(adapter._attempt(lambda c=c: _as_com(adapter, c, "IComponent2").Name2, default="?"))
                    for c in components
                ]
                raise Exception(
                    f"Component(s) not found: {', '.join(missing)}. "
                    f"The assembly holds: {', '.join(available)}."
                )

            adapter._attempt(lambda: model.ClearSelection2(True), default=None)
            for index, component_name in enumerate((component_a, component_b)):
                wrapped = found[component_name]
                entity_name = wanted[component_name]
                feature = adapter._attempt(
                    lambda w=wrapped, e=entity_name: w.FeatureByName(e), default=None
                )
                if feature is None:
                    raise Exception(
                        f"'{entity_name}' not found on {component_name}. "
                        "Only named tree features (reference planes and axes) "
                        "can be selected here."
                    )
                flagged = _as_com(adapter, feature, "IFeature")
                if flagged is None or not adapter._attempt(
                    lambda f=flagged, a=index > 0: f.Select2(a, 0), default=False
                ):
                    raise Exception(
                        f"Failed to select '{entity_name}' on {component_name}"
                    )

            selected = adapter._attempt(
                lambda: model.SelectionManager.GetSelectedObjectCount2(-1), default=0
            )
            if selected != 2:
                raise Exception(
                    f"Expected 2 selected entities for the mate, got {selected}"
                )

            transforms_before = _component_transforms(adapter, assembly)
            status = _byref_int()
            mate = adapter._attempt(
                lambda: assembly.AddMate5(
                    _MATE_TYPES[mate_key],
                    _MATE_ALIGNMENTS[align_key],
                    False,  # Flip
                    distance / 1000.0,  # Distance (m)
                    distance / 1000.0,  # upper limit
                    distance / 1000.0,  # lower limit
                    0.0,  # gear ratio numerator
                    0.0,  # gear ratio denominator
                    math.radians(float(angle)),
                    math.radians(float(angle)),
                    math.radians(float(angle)),
                    False,  # ForPositioningOnly
                    False,  # LockRotation
                    0,  # WidthMateOption
                    status,
                ),
                default=None,
            )
            adapter._attempt(lambda: model.EditRebuild3(), default=None)

            error_status = getattr(status, "value", None)
            # swAddMateError_e reports 1 for success on this build (measured:
            # a mate that demonstrably moved a component returned 1).
            if mate is None or (error_status not in (None, 1)):
                raise Exception(
                    f"SolidWorks rejected the {mate_key} mate "
                    f"(error status {error_status!r}). Check the two entities "
                    "can actually satisfy this mate type."
                )

            transforms_after = _component_transforms(adapter, assembly)
            moved = sorted(
                name
                for name, matrix in transforms_after.items()
                if name in transforms_before and transforms_before[name] != matrix
            )
            # An empty snapshot means no transform could be read, which is
            # not the same as "nothing moved" - report it as unknown rather
            # than as a negative result the caller would read as fact.
            comparable = bool(transforms_before and transforms_after)
            return {
                "mate_type": mate_key,
                "alignment": align_key,
                "components": [component_a, component_b],
                "entities": [entity_a, entity_b],
                "distance": distance or None,
                "angle": angle or None,
                "moved_components": moved,
                "geometry_moved": bool(moved) if comparable else None,
            }

        return cast(
            AdapterResult[dict[str, Any]],
            adapter._handle_com_operation("add_mate", _mate),
        )
close_model async
close_model(save: bool = False) -> AdapterResult[None]

Close the current SolidWorks model and optionally save first.

Parameters:

Name Type Description Default
save bool

When True, calls Save before closing.

False

Returns:

Type Description
AdapterResult[None]

AdapterResult[None]: Result of the close operation.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def close_model(self, save: bool = False) -> AdapterResult[None]:
    """Close the current SolidWorks model and optionally save first.

    Args:
        save: When ``True``, calls ``Save`` before closing.

    Returns:
        AdapterResult[None]: Result of the close operation.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.WARNING, error="No active model to close"
        )
    model = adapter.currentModel
    app = adapter.swApp
    if model is None or app is None:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="SolidWorks application is not connected",
        )

    def _close() -> None:
        """Close the model document."""
        if save:
            model.Save()
        app.CloseDoc(model.GetTitle())
        adapter.currentModel = None

    return cast(
        AdapterResult[None],
        adapter._handle_com_operation("close_model", _close),
    )
create_assembly async
create_assembly(name: str | None = None) -> AdapterResult[SolidWorksModel]

Create a new assembly document and set it as active.

Parameters:

Name Type Description Default
name str | None

Reserved for future naming policy.

None

Returns:

Type Description
AdapterResult[SolidWorksModel]

AdapterResult[SolidWorksModel]: Metadata for the new assembly document.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def create_assembly(
    self, name: str | None = None
) -> AdapterResult[SolidWorksModel]:
    """Create a new assembly document and set it as active.

    Args:
        name: Reserved for future naming policy.

    Returns:
        AdapterResult[SolidWorksModel]: Metadata for the new assembly document.
    """
    adapter = self._adapter(self)
    if not adapter.is_connected():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
        )

    def _create() -> SolidWorksModel:
        """Create a new assembly."""
        _ = name
        model = None
        app = adapter.swApp
        if app is None:
            raise Exception("SolidWorks application is not connected")

        new_assembly = getattr(app, "NewAssembly", None)
        if callable(new_assembly):
            model = adapter._attempt(new_assembly)

        if not model:
            asm_template = self._resolve_template_path([9, 2, 3, 1, 0], ".asmdot")
            if not asm_template:
                raise Exception("No assembly template configured in SolidWorks")
            model = app.NewDocument(asm_template, 0, 0, 0)

        if not model:
            raise Exception("Failed to create new assembly")

        adapter._attempt(lambda: _sw_type_info.flag_doc(model, 2), default=0)
        adapter.currentModel = model
        title = self._read_model_title(model)
        return SolidWorksModel(
            path="",
            name=title,
            type="Assembly",
            is_active=True,
            configuration="Default",
            properties={"created": datetime.now().isoformat()},
        )

    return cast(
        AdapterResult[SolidWorksModel],
        adapter._handle_com_operation("create_assembly", _create),
    )
create_drawing async
create_drawing(name: str | None = None) -> AdapterResult[SolidWorksModel]

Create a new drawing document and set it as active.

Parameters:

Name Type Description Default
name str | None

Reserved for future naming policy.

None

Returns:

Type Description
AdapterResult[SolidWorksModel]

AdapterResult[SolidWorksModel]: Metadata for the new drawing document.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def create_drawing(
    self, name: str | None = None
) -> AdapterResult[SolidWorksModel]:
    """Create a new drawing document and set it as active.

    Args:
        name: Reserved for future naming policy.

    Returns:
        AdapterResult[SolidWorksModel]: Metadata for the new drawing document.
    """
    adapter = self._adapter(self)
    if not adapter.is_connected():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
        )

    def _create() -> SolidWorksModel:
        """Create a new drawing."""
        _ = name
        app = adapter.swApp
        if app is None:
            raise Exception("SolidWorks application is not connected")

        drw_template = app.GetUserPreferenceStringValue(1)
        if not drw_template:
            drw_template = app.GetUserPreferenceStringValue(0).replace(
                "Part", "Drawing"
            )

        model = app.NewDocument(drw_template, 12, 0.2794, 0.2159)
        if not model:
            raise Exception("Failed to create new drawing")

        adapter._attempt(lambda: _sw_type_info.flag_doc(model, 3), default=0)
        adapter.currentModel = model
        title = self._read_model_title(model)
        return SolidWorksModel(
            path="",
            name=title,
            type="Drawing",
            is_active=True,
            configuration="Default",
            properties={"created": datetime.now().isoformat()},
        )

    return cast(
        AdapterResult[SolidWorksModel],
        adapter._handle_com_operation("create_drawing", _create),
    )
create_part async
create_part(name: str | None = None, units: str | None = None) -> AdapterResult[SolidWorksModel]

Create a new part document and set it as active.

Parameters:

Name Type Description Default
name str | None

Reserved for future naming policy.

None
units str | None

Reserved for future units policy.

None

Returns:

Type Description
AdapterResult[SolidWorksModel]

AdapterResult[SolidWorksModel]: Metadata for the new part document.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def create_part(
    self, name: str | None = None, units: str | None = None
) -> AdapterResult[SolidWorksModel]:
    """Create a new part document and set it as active.

    Args:
        name: Reserved for future naming policy.
        units: Reserved for future units policy.

    Returns:
        AdapterResult[SolidWorksModel]: Metadata for the new part document.
    """
    adapter = self._adapter(self)
    if not adapter.is_connected():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
        )

    def _create() -> SolidWorksModel:
        """Create a new part."""
        _ = name, units
        model = None
        app = adapter.swApp
        if app is None:
            raise Exception("SolidWorks application is not connected")

        new_part = getattr(app, "NewPart", None)
        if callable(new_part):
            model = adapter._attempt(new_part)

        if not model:
            part_template = self._resolve_template_path([8, 0, 1, 2, 3], ".prtdot")
            if not part_template:
                raise Exception("No part template configured in SolidWorks")
            model = app.NewDocument(part_template, 0, 0, 0)

        if not model:
            raise Exception("Failed to create new part")

        adapter._attempt(lambda: _sw_type_info.flag_doc(model, 1), default=0)
        adapter.currentModel = model
        title = self._read_model_title(model)
        return SolidWorksModel(
            path="",
            name=title,
            type="Part",
            is_active=True,
            configuration="Default",
            properties={"created": datetime.now().isoformat()},
        )

    return cast(
        AdapterResult[SolidWorksModel],
        adapter._handle_com_operation("create_part", _create),
    )
get_dimension async
get_dimension(name: str) -> AdapterResult[float]

Read a named model dimension in millimetres.

Parameters:

Name Type Description Default
name str

Fully-qualified dimension name.

required

Returns:

Type Description
AdapterResult[float]

AdapterResult[float]: Dimension value in millimetres.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def get_dimension(self, name: str) -> AdapterResult[float]:
    """Read a named model dimension in millimetres.

    Args:
        name: Fully-qualified dimension name.

    Returns:
        AdapterResult[float]: Dimension value in millimetres.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _get() -> float:  # pragma: no cover
        """Get the dimension value."""
        dimension = adapter.currentModel.Parameter(name)
        if not dimension:
            raise Exception(f"Dimension '{name}' not found")
        # SystemValue is reliable on SW 2025 (in meters, convert to mm)
        value = adapter._attempt(lambda: dimension.SystemValue, default=None)
        if value is None:
            # Fall back to GetValue3 for older SW versions
            value = adapter._attempt(
                lambda: dimension.GetValue3(0, 0), default=None
            )
        if value is None:
            raise Exception(f"Failed to read dimension '{name}'")
        return float(value) * 1000

    return cast(
        AdapterResult[float],
        adapter._handle_com_operation("get_dimension", _get),
    )
get_mass_properties async
get_mass_properties() -> AdapterResult[MassProperties]

Get mass properties for the active model.

Returns:

Type Description
AdapterResult[MassProperties]

AdapterResult[MassProperties]: Computed mass, volume, area, COM, and inertia.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def get_mass_properties(self) -> AdapterResult[MassProperties]:
    """Get mass properties for the active model.

    Returns:
        AdapterResult[MassProperties]: Computed mass, volume, area, COM, and inertia.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _get() -> MassProperties:
        """Get mass properties."""
        adapter._attempt(
            lambda: adapter.currentModel.ForceRebuild3(False), default=None
        )

        # Primary: Extension.CreateMassProperty() object API (most detailed)
        mass_props = adapter._attempt(
            lambda: adapter.currentModel.Extension.CreateMassProperty(),
            default=None,
        )

        if mass_props:
            volume = mass_props.Volume * 1e9
            surface_area = mass_props.SurfaceArea * 1e6
            mass = mass_props.Mass

            center_of_mass = [0.0, 0.0, 0.0]
            com = adapter._attempt(lambda: mass_props.CenterOfMass, default=None)
            if isinstance(com, (list, tuple)) and len(com) >= 3:
                center_of_mass = [com[0] * 1000, com[1] * 1000, com[2] * 1000]

            moi = adapter._attempt(
                lambda: mass_props.GetMomentOfInertia(0), default=None
            )
            if not isinstance(moi, (list, tuple)) or len(moi) < 9:
                moi = [0.0] * 9
        else:
            # Fallback: GetMassProperties as attribute (tuple) or callable (SW 2022)
            gmp = getattr(adapter.currentModel, "GetMassProperties", None)
            if callable(gmp):
                raw = adapter._attempt(gmp, default=None)
            elif isinstance(gmp, (list, tuple)):
                raw = gmp
            else:
                raw = None

            if not isinstance(raw, (list, tuple)) or len(raw) < 6:
                raise Exception("Failed to get mass properties")

            center_of_mass = [
                raw[0] * 1000.0,
                raw[1] * 1000.0,
                raw[2] * 1000.0,
            ]
            volume = raw[3] * 1e9
            surface_area = raw[4] * 1e6
            mass = raw[5]

            moi = [0.0] * 9
            if len(raw) >= 12:
                moi[0] = raw[6]
                moi[4] = raw[7]
                moi[8] = raw[8]
                moi[1] = raw[9]
                moi[5] = raw[10]
                moi[2] = raw[11]

        return MassProperties(
            volume=volume,
            surface_area=surface_area,
            mass=mass,
            center_of_mass=center_of_mass,
            moments_of_inertia={
                "Ixx": moi[0],
                "Iyy": moi[4],
                "Izz": moi[8],
                "Ixy": moi[1],
                "Ixz": moi[2],
                "Iyz": moi[5],
            },
        )

    return cast(
        AdapterResult[MassProperties],
        adapter._handle_com_operation("get_mass_properties", _get),
    )
get_model_info async
get_model_info() -> AdapterResult[dict[str, Any]]

Collect summary metadata about the active model.

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Model information payload.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def get_model_info(self) -> AdapterResult[dict[str, Any]]:
    """Collect summary metadata about the active model.

    Returns:
        AdapterResult[dict[str, Any]]: Model information payload.
    """
    adapter = self._adapter(self)
    active_model = (
        getattr(adapter.swApp, "ActiveDoc", None) if adapter.swApp else None
    )
    if active_model is not None:
        adapter.currentModel = active_model
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _get_info() -> dict[str, Any]:
        """Get model information."""
        # With late-bound SolidWorks COM, GetActiveConfiguration is
        # exposed as an object-valued property even though the API names
        # it like a method. Calling that COM object raises "member not
        # found", so read it directly.
        active_config = getattr(
            adapter.currentModel, "GetActiveConfiguration", None
        )
        # 'Name' on Configuration is a property, not a method.
        config_name = (
            getattr(active_config, "Name", "Default")
            if active_config
            else "Default"
        )
        # Try GetSaveFlag (method) first, fallback to property
        is_dirty_raw = adapter._attempt(
            lambda: adapter._get_attr_or_call(adapter.currentModel, "GetSaveFlag"),
            default=None,
        )
        is_dirty = bool(is_dirty_raw) if is_dirty_raw is not None else None
        feature_count = adapter._attempt(
            lambda: int(
                adapter.currentModel.FeatureManager.GetFeatureCount(True) or 0
            ),
            default=0,
        )
        rebuild_status_raw = adapter._attempt(
            lambda: adapter.currentModel.GetRebuildStatus(), default=None
        )
        # GetRebuildStatus returns 0=ok, 1=needs rebuild, or None=failed
        rebuild_status = (
            rebuild_status_raw if rebuild_status_raw is not None else None
        )
        return {
            "title": adapter._get_attr_or_call(adapter.currentModel, "GetTitle"),
            "path": adapter._get_attr_or_call(adapter.currentModel, "GetPathName"),
            "type": adapter._get_document_type(),
            "configuration": config_name,
            "is_dirty": is_dirty,
            "feature_count": feature_count,
            "rebuild_status": rebuild_status,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("get_model_info", _get_info),
    )
insert_component async
insert_component(file_path: str, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> AdapterResult[dict[str, Any]]

Insert a part or sub-assembly into the active assembly.

Wraps IAssemblyDoc::AddComponent4(CompName, ConfigName, X, Y, Z), falling back to AddComponent5. Position is in millimetres.

The component file must contain solid geometry. SolidWorks silently refuses to insert an empty part — every overload returns None and the component count stays put. That behaviour is what made this look unimplementable until the save_file bug that was writing empty parts got fixed.

Success is confirmed by the assembly's component count going up, since AddComponent* gives no usable failure signal.

Parameters:

Name Type Description Default
file_path str

Absolute path to the .sldprt or .sldasm.

required
x float

X position in millimetres.

0.0
y float

Y position in millimetres.

0.0
z float

Z position in millimetres.

0.0

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Component name and before/after

AdapterResult[dict[str, Any]]

counts. ERROR when the active document is not an assembly, the

AdapterResult[dict[str, Any]]

file is missing, or nothing was inserted.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Example::

await adapter.insert_component(r"C:\parts\bracket.sldprt", 0, 0, 0)
Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def insert_component(
        self, file_path: str, x: float = 0.0, y: float = 0.0, z: float = 0.0
    ) -> AdapterResult[dict[str, Any]]:
        """Insert a part or sub-assembly into the active assembly.

        Wraps ``IAssemblyDoc::AddComponent4(CompName, ConfigName, X, Y, Z)``,
        falling back to ``AddComponent5``.  Position is in **millimetres**.

        **The component file must contain solid geometry.**  SolidWorks
        silently refuses to insert an empty part — every overload returns
        ``None`` and the component count stays put.  That behaviour is what
        made this look unimplementable until the ``save_file`` bug that was
        writing empty parts got fixed.

        Success is confirmed by the assembly's component count going up, since
        ``AddComponent*`` gives no usable failure signal.

        Args:
            file_path (str): Absolute path to the ``.sldprt`` or ``.sldasm``.
            x (float): X position in millimetres.
            y (float): Y position in millimetres.
            z (float): Z position in millimetres.

        Returns:
            AdapterResult[dict[str, Any]]: Component name and before/after
            counts.  ``ERROR`` when the active document is not an assembly, the
            file is missing, or nothing was inserted.

        Raises:
            Exception: Propagated through ``_handle_com_operation``.

        Example::

            await adapter.insert_component(r"C:\\parts\\bracket.sldprt", 0, 0, 0)
        """
        adapter = self._adapter(self)
        if not adapter.currentModel:
            return AdapterResult(
                status=AdapterResultStatus.ERROR, error="No active model"
            )

        path = os.path.abspath(file_path)
        if not os.path.exists(path):
            return AdapterResult(
                status=AdapterResultStatus.ERROR,
                error=f"Component file not found: {file_path}",
            )

        doc_type = _doc_type(adapter)
        if doc_type != 2:
            return AdapterResult(
                status=AdapterResultStatus.ERROR,
                error=(
                    "insert_component requires an assembly document "
                    f"(active document type is {doc_type!r}, expected 2). "
                    "Call create_assembly first."
                ),
            )

        def _insert() -> dict[str, Any]:
            assembly = _sw_type_info.flagged(adapter.currentModel, "IAssemblyDoc")
            before = _component_names(adapter, assembly)

            # The document has to be loaded before it can be inserted, and the
            # errors/warnings out-parameters must be byref VARIANTs: with
            # pythoncom.Missing OpenDoc6 returns None and the part stays
            # unloaded, after which every AddComponent overload does nothing.
            app = adapter.swApp
            opened = adapter._attempt(
                lambda: app.OpenDoc6(
                    path,
                    2 if path.lower().endswith(".sldasm") else 1,
                    1,
                    "",
                    _byref_int(),
                    _byref_int(),
                ),
                default=None,
            )
            if not opened:
                raise Exception(
                    f"Could not load '{file_path}' - OpenDoc6 returned nothing."
                )

            title = adapter._attempt(
                lambda: _sw_type_info.flagged(
                    adapter.currentModel, "IModelDoc2"
                ).GetTitle(),
                default=None,
            )
            if title:
                adapter._attempt(
                    lambda: app.ActivateDoc3(title, False, 0, _byref_int()),
                    default=None,
                )

            component = adapter._attempt(
                lambda: assembly.AddComponent4(
                    path, "", x / 1000.0, y / 1000.0, z / 1000.0
                ),
                default=None,
            )
            if component is None:
                component = adapter._attempt(
                    lambda: assembly.AddComponent5(
                        path, 0, "", False, "",
                        x / 1000.0, y / 1000.0, z / 1000.0,
                    ),
                    default=None,
                )

            adapter._attempt(lambda: assembly.EditRebuild3(), default=None)

            after = _component_names(adapter, assembly)
            if len(after) <= len(before):
                raise Exception(
                    f"Component was not inserted - the assembly still has "
                    f"{len(after)} component(s). The most common cause is a "
                    f"part with no solid geometry: SolidWorks refuses those "
                    f"silently. Check '{file_path}' opens with a body."
                )

            added = [n for n in after if n not in before]
            return {
                "component": added[-1] if added else after[-1],
                "file_path": path,
                "position": {"x": x, "y": y, "z": z},
                "components_before": len(before),
                "components_after": len(after),
            }

        return cast(
            AdapterResult[dict[str, Any]],
            adapter._handle_com_operation("insert_component", _insert),
        )
list_components async
list_components() -> AdapterResult[list[str]]

List the top-level components of the active assembly.

Returns:

Type Description
AdapterResult[list[str]]

AdapterResult[list[str]]: Component names, or an error when the

AdapterResult[list[str]]

active document is not an assembly.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def list_components(self) -> AdapterResult[list[str]]:
        """List the top-level components of the active assembly.

        Returns:
            AdapterResult[list[str]]: Component names, or an error when the
            active document is not an assembly.
        """
        adapter = self._adapter(self)
        if not adapter.currentModel:
            return AdapterResult(
                status=AdapterResultStatus.ERROR, error="No active model"
            )
        doc_type = _doc_type(adapter)
        if doc_type != 2:
            return AdapterResult(
                status=AdapterResultStatus.ERROR,
                error=(
                    "list_components requires an assembly document "
                    f"(active document type is {doc_type!r}, expected 2)"
                ),
            )

        def _list() -> list[str]:
            assembly = _sw_type_info.flagged(adapter.currentModel, "IAssemblyDoc")
            return _component_names(adapter, assembly)

        return cast(
            AdapterResult[list[str]],
            adapter._handle_com_operation("list_components", _list),
        )
list_configurations async
list_configurations() -> AdapterResult[list[str]]

List all configuration names on the active model.

Returns:

Type Description
AdapterResult[list[str]]

AdapterResult[list[str]]: Configuration names, or empty list when unavailable.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def list_configurations(self) -> AdapterResult[list[str]]:
    """List all configuration names on the active model.

    Returns:
        AdapterResult[list[str]]: Configuration names, or empty list when unavailable.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="No active model",
        )

    def _list() -> list[str]:
        """List configurations."""
        raw_names = getattr(adapter.currentModel, "GetConfigurationNames", None)
        names = raw_names() if callable(raw_names) else raw_names
        if names is None:
            names = []
        if isinstance(names, str):
            return [names]

        normalized_names = [str(name) for name in names]
        if normalized_names:
            return normalized_names

        active_config = adapter._attempt(
            lambda: adapter.currentModel.GetActiveConfiguration(), default=None
        )
        active_name = adapter._attempt(
            lambda: active_config.GetName(), default=None
        )
        if active_name:
            return [str(active_name)]
        return []

    return cast(
        AdapterResult[list[str]],
        adapter._handle_com_operation("list_configurations", _list),
    )
open_model async
open_model(file_path: str) -> AdapterResult[SolidWorksModel]

Open a SolidWorks model file and set it as active on the adapter.

Parameters:

Name Type Description Default
file_path str

Path to a .sldprt, .sldasm, or .slddrw file.

required

Returns:

Type Description
AdapterResult[SolidWorksModel]

AdapterResult[SolidWorksModel]: Model metadata for the opened document.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def open_model(self, file_path: str) -> AdapterResult[SolidWorksModel]:
    """Open a SolidWorks model file and set it as active on the adapter.

    Args:
        file_path: Path to a ``.sldprt``, ``.sldasm``, or ``.slddrw`` file.

    Returns:
        AdapterResult[SolidWorksModel]: Model metadata for the opened document.
    """
    adapter = self._adapter(self)
    if not adapter.is_connected():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
        )

    def _open() -> SolidWorksModel:
        """Open the model document."""
        resolved_path = os.path.abspath(file_path)
        file_path_lower = resolved_path.lower()
        if file_path_lower.endswith(".sldprt"):
            doc_type = adapter.constants["swDocPART"]
            model_type = "Part"
        elif file_path_lower.endswith(".sldasm"):
            doc_type = adapter.constants["swDocASSEMBLY"]
            model_type = "Assembly"
        elif file_path_lower.endswith(".slddrw"):
            doc_type = adapter.constants["swDocDRAWING"]
            model_type = "Drawing"
        else:
            raise ValueError(f"Unsupported file type: {resolved_path}")

        app = adapter.swApp
        variant_ctor = getattr(getattr(win32com, "client", None), "VARIANT", None)
        vt_byref = int(getattr(pythoncom, "VT_BYREF", 0))
        vt_i4 = int(getattr(pythoncom, "VT_I4", 0))
        if callable(variant_ctor):
            errors = variant_ctor(vt_byref | vt_i4, 0)
            warnings = variant_ctor(vt_byref | vt_i4, 0)
        else:
            errors = 0
            warnings = 0
        model = app.OpenDoc6(resolved_path, doc_type, 1, "", errors, warnings)
        if not model:
            raise Exception(f"Failed to open model: {resolved_path}")

        adapter._attempt(
            lambda: _sw_type_info.flag_doc(model, int(doc_type)), default=0
        )

        adapter.currentModel = model
        title = self._read_model_title(model)
        active_config = adapter._attempt(lambda: model.GetActiveConfiguration())
        config = (
            adapter._attempt(lambda: active_config.GetName(), default="Default")
            if active_config
            else "Default"
        )

        return SolidWorksModel(
            path=resolved_path,
            name=title,
            type=model_type,
            is_active=True,
            configuration=config,
            properties={
                "last_modified": (
                    model.GetSaveTime()
                    if callable(getattr(model, "GetSaveTime", None))
                    else None
                ),
            },
        )

    return cast(
        AdapterResult[SolidWorksModel],
        adapter._handle_com_operation("open_model", _open),
    )
pack_and_go_assembly async
pack_and_go_assembly(source_path: str, target_dir: str) -> AdapterResult[dict[str, Any]]

Copy an assembly and all its referenced components to a self-contained folder.

Uses IModelDocExtension.GetPackAndGo()IPackAndGo via the comtypes vtable interface (bypassing the broken IDispatch path present in SolidWorks 2026's late-binding layer), then calls IModelDocExtension.SavePackAndGo() to execute the copy. All file paths inside the copied assembly are automatically updated by SolidWorks — this is the native Pack-and-Go mechanism.

Parameters:

Name Type Description Default
source_path str

Absolute path to the source .sldasm file.

required
target_dir str

Directory where the assembly and parts will be copied. Created if it does not exist.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict]: On success, data is a dict with keys:

AdapterResult[dict[str, Any]]

source_assembly, target_dir, copied_files,

AdapterResult[dict[str, Any]]

source_files, save_statuses, and all_files_saved.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def pack_and_go_assembly(  # pragma: no cover
    self,
    source_path: str,
    target_dir: str,
) -> AdapterResult[dict[str, Any]]:
    """Copy an assembly and all its referenced components to a self-contained folder.

    Uses ``IModelDocExtension.GetPackAndGo()`` → ``IPackAndGo`` via the
    comtypes vtable interface (bypassing the broken IDispatch path present
    in SolidWorks 2026's late-binding layer), then calls
    ``IModelDocExtension.SavePackAndGo()`` to execute the copy.  All file
    paths inside the copied assembly are automatically updated by
    SolidWorks — this is the native Pack-and-Go mechanism.

    Args:
        source_path: Absolute path to the source ``.sldasm`` file.
        target_dir: Directory where the assembly and parts will be copied.
                    Created if it does not exist.

    Returns:
        AdapterResult[dict]: On success, ``data`` is a dict with keys:
        ``source_assembly``, ``target_dir``, ``copied_files``,
        ``source_files``, ``save_statuses``, and ``all_files_saved``.
    """
    adapter = self._adapter(self)
    source = Path(source_path)
    out_dir = Path(target_dir)

    def _do_pack_and_go() -> dict[str, Any]:  # pragma: no cover
        # Load comtypes TLB (cached after first call)
        sw_lib = _get_sw_comtypes_lib()
        if sw_lib is None:
            raise RuntimeError(
                "comtypes SolidWorks type library not available. "
                "Ensure comtypes is installed and SolidWorks is registered."
            )

        # Prepare a clean target directory. SW holds file locks on previously
        # opened assemblies so rmtree raises WinError 32. We rename the old
        # dir aside (Windows allows rename with open handles) and delete the
        # backup afterwards; if rename also fails we just proceed and let SW
        # overwrite existing files.
        if out_dir.exists():
            backup = out_dir.parent / f"{out_dir.name}_bak_{uuid.uuid4().hex[:8]}"
            try:
                os.rename(out_dir, backup)
                try:
                    shutil.rmtree(backup)
                except Exception:
                    pass  # best-effort cleanup; stale backup is harmless
            except OSError:
                pass  # rename also failed — proceed; SW will overwrite files
        out_dir.mkdir(parents=True, exist_ok=True)

        # Open the source assembly
        vt = pythoncom.VT_BYREF | pythoncom.VT_I4
        from win32com.client import VARIANT  # noqa: PLC0415

        err = VARIANT(vt, 0)
        warn = VARIANT(vt, 0)
        model = adapter.swApp.OpenDoc6(str(source), 2, 1, "", err, warn)
        if model is None and err.value == 65536:
            adapter.swApp.CloseAllDocuments(False)
            err = VARIANT(vt, 0)
            warn = VARIANT(vt, 0)
            model = adapter.swApp.OpenDoc6(str(source), 2, 1, "", err, warn)
        if model is None:
            raise RuntimeError(f"OpenDoc6 failed err={err.value} warn={warn.value}")
        _sw_type_info.flag_doc(model, 2)
        adapter.currentModel = model

        # Bridge model.Extension → IModelDocExtension via comtypes vtable
        ext_ct = _bridge_com_to_comtypes(model.Extension, sw_lib.IModelDocExtension)

        # GetPackAndGo() via vtable (IDispatch path broken in SW 2026)
        pg = ext_ct.GetPackAndGo()

        # Configure: flatten all files to root of target directory
        pg.FlattenToSingleFolder = True
        pg.SetSaveToName(True, str(out_dir) + "\\")

        # Record what files will be packed
        names_result = pg.GetDocumentNames()
        source_files: list[str] = list(names_result[0]) if names_result[0] else []

        # Execute Pack and Go — SavePackAndGo returns a tuple of per-file status codes
        ext_ct2 = _bridge_com_to_comtypes(
            model.Extension, sw_lib.IModelDocExtension
        )
        status_arr = ext_ct2.SavePackAndGo(pg)
        save_statuses: list[int] = list(status_arr) if status_arr else []

        copied_files = sorted(
            str(p)
            for p in out_dir.rglob("*")
            if p.is_file() and p.suffix.lower() in {".sldasm", ".sldprt", ".slddrw"}
        )
        return {
            "source_assembly": str(source),
            "target_dir": str(out_dir),
            "copied_files": copied_files,
            "source_files": source_files,
            "save_statuses": save_statuses,
            "all_files_saved": all(s == 0 for s in save_statuses),
        }

    result = adapter._handle_com_operation("pack_and_go_assembly", _do_pack_and_go)
    if not result.is_success:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=f"Pack and Go failed: {result.error}",
        )
    return AdapterResult(
        status=AdapterResultStatus.SUCCESS,
        data=result.data,
        execution_time=result.execution_time,
    )
rebuild_model async
rebuild_model() -> AdapterResult[None]

Force a model rebuild.

Returns:

Type Description
AdapterResult[None]

AdapterResult[None]: Result of the rebuild operation.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def rebuild_model(self) -> AdapterResult[None]:
    """Force a model rebuild.

    Returns:
        AdapterResult[None]: Result of the rebuild operation.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _rebuild() -> None:
        """Rebuild the model."""
        success = adapter.currentModel.ForceRebuild3(False)
        if not success:
            raise Exception("Failed to rebuild model")

    return cast(
        AdapterResult[None],
        adapter._handle_com_operation("rebuild_model", _rebuild),
    )
save_file async
save_file(file_path: str | None = None) -> AdapterResult[None]

Save the active model to its current path or to a new file path.

Parameters:

Name Type Description Default
file_path str | None

Optional target path for Save As.

None

Returns:

Type Description
AdapterResult[None]

AdapterResult[None]: Result of the save operation.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def save_file(self, file_path: str | None = None) -> AdapterResult[None]:
    """Save the active model to its current path or to a new file path.

    Args:
        file_path: Optional target path for Save As.

    Returns:
        AdapterResult[None]: Result of the save operation.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _save() -> None:
        """Save the model."""
        if file_path:
            resolved_path = os.path.abspath(file_path)
            directory = os.path.dirname(resolved_path)
            if directory:
                os.makedirs(directory, exist_ok=True)

            current_path = adapter._attempt(
                lambda: adapter._get_attr_or_call(
                    adapter.currentModel, "GetPathName"
                ),
                default="",
            )
            same_file = bool(current_path) and os.path.normcase(
                os.path.abspath(str(current_path))
            ) == os.path.normcase(resolved_path)

            if same_file:
                # Saving a document over its own path is a plain Save.
                # It used to fall through to the Save-As branch below, which
                # closed the document and deleted the file before calling
                # SaveAs3 on the now-closed doc. That wrote an empty part and
                # lost the geometry.
                save_result = adapter._attempt(
                    lambda: adapter.currentModel.Save3(1, None, None)
                )
                if save_result is None:
                    save_fn = getattr(adapter.currentModel, "Save", None)
                    if callable(save_fn):
                        save_fn()
                if not os.path.exists(resolved_path):
                    raise Exception(
                        f"File not written after save: {resolved_path}"
                    )
                return

            # A *different* document may be holding the target path open.
            # Close that one by name only - never the document being saved.
            if adapter.swApp:
                adapter._attempt(
                    lambda: adapter.swApp.CloseDoc(
                        os.path.basename(resolved_path)
                    )
                )

            # Deliberately no os.remove here: SaveAs3 overwrites, and
            # deleting first meant a failed save destroyed the old file too.
            save_as3_result = adapter.currentModel.SaveAs3(resolved_path, 0, 0)
            if not self._is_success(save_as3_result):
                save_as = getattr(adapter.currentModel, "SaveAs", None)
                if callable(save_as):
                    fallback_result = save_as(resolved_path)
                    if not self._is_success(fallback_result):
                        raise Exception(f"Failed to save as: {resolved_path}")
                else:
                    raise Exception(f"Failed to save as: {resolved_path}")

            if not os.path.exists(resolved_path):
                raise Exception(f"File not written after save: {resolved_path}")
            return

        save_result = adapter._attempt(
            lambda: adapter.currentModel.Save3(1, None, None)
        )
        if save_result is None:
            save_fn = getattr(adapter.currentModel, "Save", None)
            if callable(save_fn):
                save_result = save_fn()
            else:
                raise Exception("Failed to save file")

        if self._is_success(save_result):
            return

        path_attr = getattr(adapter.currentModel, "GetPathName", "")
        model_path = path_attr() if callable(path_attr) else path_attr
        if model_path and os.path.exists(model_path):
            return
        raise Exception("Failed to save file")

    return cast(
        AdapterResult[None],
        adapter._handle_com_operation("save_file", _save),
    )
set_dimension async
set_dimension(name: str, value: float) -> AdapterResult[None]

Set a named model dimension in millimetres and rebuild.

Parameters:

Name Type Description Default
name str

Fully-qualified dimension name.

required
value float

New value in millimetres.

required

Returns:

Type Description
AdapterResult[None]

AdapterResult[None]: Result of the set operation.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def set_dimension(self, name: str, value: float) -> AdapterResult[None]:
    """Set a named model dimension in millimetres and rebuild.

    Args:
        name: Fully-qualified dimension name.
        value: New value in millimetres.

    Returns:
        AdapterResult[None]: Result of the set operation.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _set() -> None:
        """Set the dimension value."""
        dimension = adapter.currentModel.Parameter(name)
        if not dimension:
            raise Exception(f"Dimension '{name}' not found")

        # SetValue3 has gen_py parameter mapping issues on SW 2025.
        # SystemValue (in meters) is reliable.
        value_m = value / 1000.0
        adapter._attempt(
            lambda: setattr(dimension, "SystemValue", value_m),
            default=None,
        )

        # Rebuild: try EditRebuild3 first, fall back to ForceRebuild3
        rebuilt = adapter._attempt(
            lambda: adapter.currentModel.EditRebuild3(), default=None
        )
        if rebuilt is None:
            rebuilt = adapter._attempt(
                lambda: adapter.currentModel.ForceRebuild3(True), default=None
            )
        if rebuilt is None:
            raise Exception("Failed to set dimension")

    return cast(
        AdapterResult[None],
        adapter._handle_com_operation("set_dimension", _set),
    )

SolidWorksSelectionMixin

Expose feature-selection and feature-list methods through a mixin.

SolidWorksSketchMixin

Expose sketch creation and editing methods via mixin-local implementation.