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.

Methods:
create_reference_point async
create_reference_point(mode: str, x: float, y: float, z: float, distance: float | None = None, percent: float | None = None) -> AdapterResult[dict[str, Any]]

Create a reference point on the active part.

Parameters:

Name Type Description Default
mode str

"along_curve" or "face_center".

required
x float

X of a point on the target edge/face, in millimetres.

required
y float

Y of a point on the target edge/face, in millimetres.

required
z float

Z of a point on the target edge/face, in millimetres.

required
distance float | None

For along_curve, offset from the edge start in mm.

None
percent float | None

For along_curve, position as 0-100 of edge length.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Delegates to

AdapterResult[dict[str, Any]]

func:_create_reference_point_impl; see it for the payload shape.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
async def create_reference_point(
    self,
    mode: str,
    x: float,
    y: float,
    z: float,
    distance: float | None = None,
    percent: float | None = None,
) -> AdapterResult[dict[str, Any]]:
    """Create a reference point on the active part.

    Args:
        mode: ``"along_curve"`` or ``"face_center"``.
        x: X of a point on the target edge/face, in millimetres.
        y: Y of a point on the target edge/face, in millimetres.
        z: Z of a point on the target edge/face, in millimetres.
        distance: For ``along_curve``, offset from the edge start in mm.
        percent: For ``along_curve``, position as 0-100 of edge length.

    Returns:
        AdapterResult[dict[str, Any]]: Delegates to
        :func:`_create_reference_point_impl`; see it for the payload shape.
    """
    return _create_reference_point_impl(
        self, mode, x, y, z, distance, percent
    )
mirror_feature async
mirror_feature(features: list[str], mirror_plane: str, merge: bool = True, mirror_bodies: bool = True) -> AdapterResult[dict[str, Any]]

Mirror solid bodies or features about a plane.

InsertMirrorFeature returns a Feature object even when it mirrored nothing, so the model's volume is measured before and after and the call is reported as failed if the volume did not grow. That check runs here rather than inside the COM closure because get_mass_properties is the read path known to work - reading CreateMassProperty().Volume inline returns None when a plane is still in the selection set, which silently disables the check.

Parameters:

Name Type Description Default
features list[str]

Names of the bodies (or features) to mirror. For a body mirror these are the names as they appear under Solid Bodies, which for a lofted wing is the loft's own name.

required
mirror_plane str

Plane to mirror about, e.g. "Right Plane".

required
merge bool

Merge the mirrored result with the original.

True
mirror_bodies bool

Mirror whole solid bodies rather than features. Defaults to True, which is what works for anything built from a loft or sweep: SolidWorks only resolves a feature mirror when the feature's own sketch sits on the mirror plane, so a wing lofted between stations offset from the centreline cannot be feature-mirrored. Measured on SW 2025: body mirroring a 1819569 mm^3 wing about the Right Plane gave 3636460 mm^3.

True

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: What was mirrored, and the volume

AdapterResult[dict[str, Any]]

before and after. ERROR when nothing was selected, the call

AdapterResult[dict[str, Any]]

failed, or the volume did not grow.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
async def mirror_feature(
    self,
    features: list[str],
    mirror_plane: str,
    merge: bool = True,
    mirror_bodies: bool = True,
) -> AdapterResult[dict[str, Any]]:
    """Mirror solid bodies or features about a plane.

    ``InsertMirrorFeature`` returns a Feature object even when it mirrored
    nothing, so the model's volume is measured before and after and the
    call is reported as failed if the volume did not grow. That check runs
    here rather than inside the COM closure because ``get_mass_properties``
    is the read path known to work - reading ``CreateMassProperty().Volume``
    inline returns ``None`` when a plane is still in the selection set,
    which silently disables the check.

    Args:
        features: Names of the bodies (or features) to mirror. For a body
            mirror these are the names as they appear under Solid Bodies,
            which for a lofted wing is the loft's own name.
        mirror_plane: Plane to mirror about, e.g. ``"Right Plane"``.
        merge: Merge the mirrored result with the original.
        mirror_bodies: Mirror whole solid bodies rather than features.
            Defaults to ``True``, which is what works for anything built
            from a loft or sweep: SolidWorks only resolves a *feature*
            mirror when the feature's own sketch sits on the mirror plane,
            so a wing lofted between stations offset from the centreline
            cannot be feature-mirrored. Measured on SW 2025: body mirroring
            a 1819569 mm^3 wing about the Right Plane gave 3636460 mm^3.

    Returns:
        AdapterResult[dict[str, Any]]: What was mirrored, and the volume
        before and after. ``ERROR`` when nothing was selected, the call
        failed, or the volume did not grow.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    before = await self.get_mass_properties()
    volume_before = before.data.volume if before.is_success and before.data else None

    result = _mirror_feature_impl(
        self, features, mirror_plane, merge, mirror_bodies
    )
    if not result.is_success:
        return result

    after = await self.get_mass_properties()
    volume_after = after.data.volume if after.is_success and after.data else None

    if volume_before is None or volume_after is None:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                "The mirror call returned a feature but the volume could "
                "not be read before and after, so whether any geometry was "
                "produced is unknown."
            ),
        )
    if volume_after <= volume_before * 1.001:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                f"The mirror produced no new geometry (volume "
                f"{volume_before:.1f} -> {volume_after:.1f} mm3). "
                "SolidWorks returned a feature but mirrored nothing. With "
                "mirror_bodies=False this happens whenever the feature's "
                "own sketch does not sit on the mirror plane; try "
                "mirror_bodies=True to mirror the solid body instead."
            ),
        )

    data = dict(result.data or {})
    data["volume_before"] = volume_before
    data["volume_after"] = volume_after
    data["volume_ratio"] = round(volume_after / volume_before, 6)
    return AdapterResult(
        status=AdapterResultStatus.SUCCESS,
        data=data,
        execution_time=result.execution_time,
    )
pattern_circular async
pattern_circular(features: list[str], axis: str, count: int, angle: float = 360.0, equal_spacing: bool = True) -> AdapterResult[dict[str, Any]]

Pattern features around an axis.

FeatureCircularPattern5 returns a Feature object whether or not it produced anything, so the model volume is measured before and after here - in the async layer, using get_mass_properties, which is the read path that works. Reading volume inline inside the COM closure returns None while a selection is active.

Note that instances landing off the parent body are silently dropped by SolidWorks, so the volume rises by less than count - 1 times the seed. That is correct behaviour, not a failure - only no growth at all means the pattern did nothing.

Parameters:

Name Type Description Default
features list[str]

Names of the features to pattern.

required
axis str

Name of the axis to rotate about, e.g. "Axis1" from create_axis.

required
count int

Total number of instances including the original.

required
angle float

Total angle in degrees to spread them over. Defaults to a full 360.

360.0
equal_spacing bool

Space instances evenly across angle.

True

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: What was patterned and the volume

AdapterResult[dict[str, Any]]

before and after. ERROR when nothing could be selected or the

AdapterResult[dict[str, Any]]

volume did not grow.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
async def pattern_circular(
    self,
    features: list[str],
    axis: str,
    count: int,
    angle: float = 360.0,
    equal_spacing: bool = True,
) -> AdapterResult[dict[str, Any]]:
    """Pattern features around an axis.

    ``FeatureCircularPattern5`` returns a Feature object whether or not it
    produced anything, so the model volume is measured before and after
    here - in the async layer, using ``get_mass_properties``, which is the
    read path that works. Reading volume inline inside the COM closure
    returns ``None`` while a selection is active.

    Note that instances landing off the parent body are silently dropped
    by SolidWorks, so the volume rises by less than ``count - 1`` times the
    seed. That is correct behaviour, not a failure - only *no* growth at
    all means the pattern did nothing.

    Args:
        features: Names of the features to pattern.
        axis: Name of the axis to rotate about, e.g. ``"Axis1"`` from
            ``create_axis``.
        count: Total number of instances including the original.
        angle: Total angle in degrees to spread them over. Defaults to a
            full 360.
        equal_spacing: Space instances evenly across ``angle``.

    Returns:
        AdapterResult[dict[str, Any]]: What was patterned and the volume
        before and after. ``ERROR`` when nothing could be selected or the
        volume did not grow.
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    before = await self.get_mass_properties()
    volume_before = (
        before.data.volume if before.is_success and before.data else None
    )

    result = _pattern_circular_impl(self, features, axis, count, angle, equal_spacing)
    if not result.is_success:
        return result

    after = await self.get_mass_properties()
    volume_after = after.data.volume if after.is_success and after.data else None

    if volume_before is None or volume_after is None:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                "The pattern call returned a feature but the volume could "
                "not be read before and after, so whether any instances "
                "were created is unknown."
            ),
        )
    if volume_after <= volume_before * 1.0001:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                f"The circular pattern produced no geometry (volume "
                f"{volume_before:.1f} -> {volume_after:.1f} mm3). "
                f"SolidWorks returned a feature but patterned nothing. "
                f"Check that '{axis}' is a real axis in the tree and that "
                f"the instances land on the parent body."
            ),
        )

    data = dict(result.data or {})
    data["volume_before"] = volume_before
    data["volume_after"] = volume_after
    data["volume_ratio"] = round(volume_after / volume_before, 6)
    return AdapterResult(
        status=AdapterResultStatus.SUCCESS,
        data=data,
        execution_time=result.execution_time,
    )
rename_feature async
rename_feature(old_name: str, new_name: str) -> AdapterResult[dict[str, Any]]

Rename a feature on the active model's tree.

Parameters:

Name Type Description Default
old_name str

The feature's current name.

required
new_name str

The name to give it.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Delegates to

AdapterResult[dict[str, Any]]

func:_rename_feature_impl; see it for the payload shape.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
async def rename_feature(
    self, old_name: str, new_name: str
) -> AdapterResult[dict[str, Any]]:
    """Rename a feature on the active model's tree.

    Args:
        old_name: The feature's current name.
        new_name: The name to give it.

    Returns:
        AdapterResult[dict[str, Any]]: Delegates to
        :func:`_rename_feature_impl`; see it for the payload shape.
    """
    return _rename_feature_impl(self, old_name, new_name)

SolidWorksIOMixin

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

Methods:
activate_document async
activate_document(title_or_path: str) -> AdapterResult[dict[str, Any]]

Make an already-open document the active one.

Matches title_or_path against the open documents by exact title, by full path, or by file name (all case-insensitive), then calls ISldWorks::ActivateDoc3 and updates the adapter's tracked currentModel. The resulting ActiveDoc is read back and a mismatch is reported as an error.

Parameters:

Name Type Description Default
title_or_path str

A window title ("bracket.SLDPRT"), a full path, or a bare file name of a document that is already open.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: activated (the title switched

AdapterResult[dict[str, Any]]

to) and verified (bool, or None when the readback could

AdapterResult[dict[str, Any]]

not be performed). ERROR when not connected, the argument is

AdapterResult[dict[str, Any]]

blank, or nothing open matches.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def activate_document(
    self, title_or_path: str
) -> AdapterResult[dict[str, Any]]:
    """Make an already-open document the active one.

    Matches ``title_or_path`` against the open documents by exact title,
    by full path, or by file name (all case-insensitive), then calls
    ``ISldWorks::ActivateDoc3`` and updates the adapter's tracked
    ``currentModel``. The resulting ``ActiveDoc`` is read back and a
    mismatch is reported as an error.

    Args:
        title_or_path: A window title (``"bracket.SLDPRT"``), a full path,
            or a bare file name of a document that is already open.

    Returns:
        AdapterResult[dict[str, Any]]: ``activated`` (the title switched
        to) and ``verified`` (bool, or ``None`` when the readback could
        not be performed). ``ERROR`` when not connected, the argument is
        blank, or nothing open matches.
    """
    adapter = self._adapter(self)
    if not adapter.is_connected():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
        )
    target = (title_or_path or "").strip()
    if not target:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="title_or_path is required"
        )

    def _activate() -> dict[str, Any]:
        app = adapter.swApp
        if app is None:
            raise Exception("SolidWorks application is not connected")

        docs = _coerce_dispatch_sequence(
            adapter._attempt(lambda: app.GetDocuments(), default=None)
        )
        needle = target.lower()
        match: Any = None
        match_title: str | None = None
        open_titles: list[str] = []
        for doc in docs:
            title = adapter._attempt(
                lambda d=doc: adapter._get_attr_or_call(d, "GetTitle"),
                default=None,
            )
            path = adapter._attempt(
                lambda d=doc: adapter._get_attr_or_call(d, "GetPathName"),
                default=None,
            )
            if title:
                open_titles.append(str(title))
            candidates = {
                str(title).lower() if title else "",
                str(path).lower() if path else "",
                Path(str(path)).name.lower() if path else "",
            }
            candidates.discard("")
            if needle in candidates:
                match = doc
                match_title = str(title) if title else None
                break

        if match is None:
            raise Exception(
                f"No open document matches {target!r}. Open: "
                + (", ".join(open_titles) or "none")
            )
        if not match_title:
            match_title = adapter._attempt(
                lambda: adapter._get_attr_or_call(match, "GetTitle"), default=None
            )
        if not match_title:
            raise Exception(
                "Matched an open document but could not read its title to "
                "activate it"
            )

        activated = adapter._attempt(
            lambda: app.ActivateDoc3(match_title, False, 0, _byref_int()),
            default=None,
        )
        model = activated or adapter._attempt(
            lambda: getattr(app, "ActiveDoc", None), default=None
        )
        if model is not None:
            type_raw = adapter._attempt(
                lambda: adapter._get_attr_or_call(model, "GetType"), default=1
            )
            adapter._attempt(
                lambda: _sw_type_info.flag_doc(
                    model, int(type_raw) if isinstance(type_raw, (int, float)) else 1
                ),
                default=0,
            )
            adapter.currentModel = model

        active_after = adapter._attempt(
            lambda: getattr(app, "ActiveDoc", None), default=None
        )
        now_title = (
            adapter._attempt(
                lambda: adapter._get_attr_or_call(active_after, "GetTitle"),
                default=None,
            )
            if active_after is not None
            else None
        )
        if now_title is None:
            verified: bool | None = None
        else:
            verified = str(now_title) == str(match_title)
            if not verified:
                raise Exception(
                    f"Requested {match_title!r} but ActiveDoc is "
                    f"{now_title!r} after ActivateDoc3; activation did not take."
                )
        return {"activated": str(match_title), "verified": verified}

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("activate_document", _activate),
    )
add_drawing_view async
add_drawing_view(payload: Any = None) -> AdapterResult[dict[str, Any]]

Add a view of a model to the active drawing sheet.

The same operation as create_drawing_view; both tool entry points exist upstream and reach this one implementation.

Parameters:

Name Type Description Default
payload Any

Tool payload; see _place_view.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The new view's name and position.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def add_drawing_view(
    self, payload: Any = None
) -> AdapterResult[dict[str, Any]]:
    """Add a view of a model to the active drawing sheet.

    The same operation as ``create_drawing_view``; both tool entry points
    exist upstream and reach this one implementation.

    Args:
        payload: Tool payload; see ``_place_view``.

    Returns:
        AdapterResult[dict[str, Any]]: The new view's name and position.
    """
    return self._place_view(payload)
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),
        )
add_note async
add_note(payload: Any = None) -> AdapterResult[dict[str, Any]]

Place a text note on the active drawing sheet.

Wraps IModelDoc2::InsertNote(Text) and then positions the returned annotation: InsertNote places the note wherever SolidWorks likes, so the position is applied afterwards via IAnnotation::SetPosition.

Parameters:

Name Type Description Default
payload Any

Tool payload. Reads text, position_x/position_y (or a two-element position) in millimetres, and font_size in points, matching the tool schema.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The note text and where it landed.

Example::

await adapter.add_note(
    {"text": "MATERIAL: AISI 1018", "position_x": 200.0,
     "position_y": 50.0}
)
Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def add_note(self, payload: Any = None) -> AdapterResult[dict[str, Any]]:
    """Place a text note on the active drawing sheet.

    Wraps ``IModelDoc2::InsertNote(Text)`` and then positions the returned
    annotation: ``InsertNote`` places the note wherever SolidWorks likes,
    so the position is applied afterwards via ``IAnnotation::SetPosition``.

    Args:
        payload: Tool payload. Reads ``text``, ``position_x``/``position_y``
            (or a two-element ``position``) in millimetres, and
            ``font_size`` in **points**, matching the tool schema.

    Returns:
        AdapterResult[dict[str, Any]]: The note text and where it landed.

    Example::

        await adapter.add_note(
            {"text": "MATERIAL: AISI 1018", "position_x": 200.0,
             "position_y": 50.0}
        )
    """
    adapter = self._adapter(self)
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)

    data = _payload(payload)
    text = _first(data, "text", "note", default="")
    if not text:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="add_note requires text",
        )

    position = data.get("position")
    if isinstance(position, (list, tuple)) and len(position) >= 2:
        x, y = float(position[0]), float(position[1])
    else:
        x = float(_first(data, "position_x", "x", default=100.0))
        y = float(_first(data, "position_y", "y", default=50.0))

    # The schema expresses font size in points, not millimetres.
    font_points = float(_first(data, "font_size", default=0.0) or 0.0)
    font_mm = font_points * _POINTS_TO_MM

    def _add_note() -> dict[str, Any]:
        model = adapter.currentModel
        adapter._attempt(lambda: model.ClearSelection2(True), default=None)

        note = adapter._attempt(
            lambda: model.InsertNote(str(text)), default=None
        )
        if note is None:
            raise Exception(
                "InsertNote returned nothing - the note was not created."
            )

        positioned = False
        annotation = adapter._attempt(
            lambda: _sw_type_info.flagged(note, "INote").GetAnnotation(),
            default=None,
        )
        if annotation is not None:
            positioned = bool(
                adapter._attempt(
                    lambda: _sw_type_info.flagged(
                        annotation, "IAnnotation"
                    ).SetPosition(x / 1000.0, y / 1000.0, 0.0),
                    default=False,
                )
            )

        if font_mm:
            adapter._attempt(
                lambda: _sw_type_info.flagged(note, "INote").SetTextFormat(
                    0, False, font_mm / 1000.0
                ),
                default=None,
            )

        adapter._attempt(lambda: model.EditRebuild3(), default=None)
        return {
            "text": text,
            "position": {"x": x, "y": y},
            "positioned": positioned,
            "font_size_points": font_points or None,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("add_note", _add_note),
    )
auto_center_marks async
auto_center_marks(view_name: str, mark_holes: bool = True, mark_fillets: bool = False, mark_slots: bool = True) -> AdapterResult[dict[str, Any]]

Auto-insert centre marks on circular features in a drawing view.

Wraps IView::AutoInsertCenterMarks2 (falling back to AutoInsertCenterMarks on older builds) using the document's default size/gap/font. IView::GetCenterMarkCount() returns 0 regardless of marks actually present on SW 3DEXPERIENCE R2026x, so the count is derived instead from IView::GetAnnotations(): the view's annotations are enumerated and those flagged as swCenterMarkSym (swAnnotationType_e) are counted. center_marks_before == center_marks_after == 0 is reported when the annotation enumeration cannot be performed (COM unavailable or older builds).

Adding nothing is still a success - the view may simply have no un-marked circular features.

Parameters:

Name Type Description Default
view_name str

Name of a view on the active drawing.

required
mark_holes bool

Mark holes / bores. Defaults to True.

True
mark_fillets bool

Mark fillets. Defaults to False.

False
mark_slots bool

Mark slots. Defaults to True.

True

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The view, the feature types acted

AdapterResult[dict[str, Any]]

on, and center_marks_before / center_marks_after /

AdapterResult[dict[str, Any]]

center_marks_added. ERROR when the active document is not

AdapterResult[dict[str, Any]]

a drawing, no feature type is selected, or the view is not found.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def auto_center_marks(
    self,
    view_name: str,
    mark_holes: bool = True,
    mark_fillets: bool = False,
    mark_slots: bool = True,
) -> AdapterResult[dict[str, Any]]:
    """Auto-insert centre marks on circular features in a drawing view.

    Wraps ``IView::AutoInsertCenterMarks2`` (falling back to
    ``AutoInsertCenterMarks`` on older builds) using the document's
    default size/gap/font. ``IView::GetCenterMarkCount()`` returns 0
    regardless of marks actually present on SW 3DEXPERIENCE R2026x, so
    the count is derived instead from ``IView::GetAnnotations()``: the
    view's annotations are enumerated and those flagged as
    ``swCenterMarkSym`` (``swAnnotationType_e``) are counted.
    ``center_marks_before == center_marks_after == 0`` is reported when
    the annotation enumeration cannot be performed (COM unavailable or
    older builds).

    Adding nothing is still a success - the view may simply have no
    un-marked circular features.

    Args:
        view_name: Name of a view on the active drawing.
        mark_holes: Mark holes / bores. Defaults to ``True``.
        mark_fillets: Mark fillets. Defaults to ``False``.
        mark_slots: Mark slots. Defaults to ``True``.

    Returns:
        AdapterResult[dict[str, Any]]: The view, the feature types acted
        on, and ``center_marks_before`` / ``center_marks_after`` /
        ``center_marks_added``. ``ERROR`` when the active document is not
        a drawing, no feature type is selected, or the view is not found.
    """
    adapter = self._adapter(self)
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)

    insert_type = (
        (_SW_CM_TYPE_HOLE if mark_holes else 0)
        | (_SW_CM_TYPE_FILLET if mark_fillets else 0)
        | (_SW_CM_TYPE_SLOT if mark_slots else 0)
    )
    if insert_type == 0:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                "Select at least one feature type: mark_holes, "
                "mark_fillets or mark_slots."
            ),
        )

    def _auto_marks() -> dict[str, Any]:
        drawing = _sw_type_info.flagged(adapter.currentModel, "IDrawingDoc")
        view = _resolve_drawing_view(adapter, drawing, view_name)
        if view is None:
            raise Exception(
                f"No view named {view_name!r} on the active drawing. "
                f"Views: {', '.join(_view_names(adapter, drawing)) or 'none'}"
            )

        def _center_mark_count() -> int:
            """Count annotations on the view flagged as swCenterMarkSym.

            Enumerates ``IView::GetAnnotations()`` and inspects each
            annotation's type via ``IAnnotation::GetType()``. Returns 0 when
            the enumeration cannot be performed (COM unavailable, older
            builds, or the annotation type is unknown). This is the count
            source for both ``center_marks_before`` and ``center_marks_after``.
            """
            raw_annos = adapter._attempt(
                lambda: view.GetAnnotations(), default=None
            )
            if not isinstance(raw_annos, (list, tuple)):
                return 0
            count = 0
            for raw in raw_annos:
                anno = _as_com(adapter, raw, "IAnnotation")
                if anno is None:
                    continue
                anno_type = adapter._attempt(
                    lambda anno=anno: anno.GetType(), default=None
                )
                if (
                    isinstance(anno_type, (int, float))
                    and anno_type == _SW_ANNOTATION_CENTER_MARK
                ):
                    count += 1
            return count

        before = _center_mark_count()

        ran = adapter._attempt(
            lambda: view.AutoInsertCenterMarks2(
                insert_type,
                _SW_CM_STYLE_SINGLE,
                True,  # LinearSlotCenter
                True,  # ArcSlotCenter
                True,  # UseDocumentDefaults
                0.0,   # Size (ignored when UseDocumentDefaults)
                0.0,   # Gap
                True,  # ExtendedLines
                False,  # CenterLineFont
                0.0,   # Angle
            ),
            default=None,
        )
        if ran is None:
            ran = adapter._attempt(
                lambda: view.AutoInsertCenterMarks(
                    insert_type,
                    _SW_CM_STYLE_SINGLE,
                    True,
                    True,
                    True,
                    0.0,
                    True,
                    False,
                    0.0,
                ),
                default=None,
            )

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

        after = _center_mark_count()

        return {
            "view": view_name,
            "feature_types": [
                label
                for label, on in (
                    ("holes", mark_holes),
                    ("fillets", mark_fillets),
                    ("slots", mark_slots),
                )
                if on
            ],
            "center_marks_before": before,
            "center_marks_after": after,
            "center_marks_added": after - before,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("auto_center_marks", _auto_marks),
    )
check_interference async
check_interference(params: Any = None) -> AdapterResult[dict[str, Any]]

Run SolidWorks' interference detection on the active assembly.

Uses IAssemblyDoc::InterferenceDetectionManager (IInterferenceDetectionMgr), not the older ToolsCheckInterference2. That call is declared Sub with two ByRef out-parameters, and on SW 2025 through pywin32 late binding it could not be made to report anything: pythoncom.Missing raises PyOleMissing can not be converted to a COM VARIANT, a plain None or a typed array VARIANT raises Type mismatch, passing a component array throws server-side, and a byref VT_VARIANT pair is accepted but leaves both out-parameters None for an assembly that demonstrably interferes. An inspection tool that always answers "no interference" is worse than one that refuses.

GetInterferenceCount is an ordinary return value, so 0 is a real measurement rather than a swallowed failure, and each IInterference carries the overlap Volume.

Parameters:

Name Type Description Default
params Any

Optional settings. coincident (bool, default False) treats touching faces as interference; include_multibody (bool, default True); ignore_hidden (bool, default False). A components list filters which interferences are reported. tolerance is accepted and ignored - SolidWorks' interference detection has no tolerance setting - and the payload says so.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: interference_found,

AdapterResult[dict[str, Any]]

interference_count, and per-interference component pairs with

AdapterResult[dict[str, Any]]

overlap volumes in mm^3. ERROR when there is no active model

AdapterResult[dict[str, Any]]

or the active document is not an assembly.

Raises:

Type Description
Exception

Propagated through _handle_com_operation.

Example::

await adapter.check_interference({"coincident": False})
Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def check_interference(
    self, params: Any = None
) -> AdapterResult[dict[str, Any]]:
    """Run SolidWorks' interference detection on the active assembly.

    Uses ``IAssemblyDoc::InterferenceDetectionManager``
    (``IInterferenceDetectionMgr``), **not** the older
    ``ToolsCheckInterference2``. That call is declared ``Sub`` with two
    ``ByRef`` out-parameters, and on SW 2025 through pywin32 late binding
    it could not be made to report anything: ``pythoncom.Missing`` raises
    ``PyOleMissing can not be converted to a COM VARIANT``, a plain
    ``None`` or a typed array VARIANT raises ``Type mismatch``, passing a
    component array throws server-side, and a byref ``VT_VARIANT`` pair is
    accepted but leaves both out-parameters ``None`` for an assembly that
    demonstrably interferes. An inspection tool that always answers "no
    interference" is worse than one that refuses.

    ``GetInterferenceCount`` is an ordinary return value, so ``0`` is a
    real measurement rather than a swallowed failure, and each
    ``IInterference`` carries the overlap ``Volume``.

    Args:
        params (Any): Optional settings. ``coincident`` (bool, default
            ``False``) treats touching faces as interference;
            ``include_multibody`` (bool, default ``True``);
            ``ignore_hidden`` (bool, default ``False``). A ``components``
            list filters which interferences are reported. ``tolerance``
            is accepted and ignored - SolidWorks' interference detection
            has no tolerance setting - and the payload says so.

    Returns:
        AdapterResult[dict[str, Any]]: ``interference_found``,
        ``interference_count``, and per-interference component pairs with
        overlap volumes in mm^3. ``ERROR`` when there is no active model
        or the active document is not an assembly.

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

    Example::

        await adapter.check_interference({"coincident": False})
    """
    adapter = self._adapter(self)
    options = _payload_dict(params)

    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=(
                "Interference detection requires an assembly document "
                f"(active document type is {doc_type!r}, expected 2)"
            ),
        )

    coincident = bool(options.get("coincident", False))
    include_multibody = bool(options.get("include_multibody", True))
    ignore_hidden = bool(options.get("ignore_hidden", False))
    wanted = options.get("components") or []
    wanted_names = {str(name) for name in wanted} if wanted else set()

    def _check() -> dict[str, Any]:
        assembly = _sw_type_info.flagged(adapter.currentModel, "IAssemblyDoc")
        manager = adapter._attempt(
            lambda: assembly.InterferenceDetectionManager, default=None
        )
        if manager is None:
            raise Exception(
                "InterferenceDetectionManager is unavailable on this "
                "assembly, so no interference check was performed."
            )
        manager = _sw_type_info.flagged(manager, "IInterferenceDetectionMgr")

        for name, value in (
            ("TreatCoincidenceAsInterference", coincident),
            ("IncludeMultibodyPartInterferences", include_multibody),
            ("IgnoreHiddenBodies", ignore_hidden),
            ("MakeInterferingPartsTransparent", False),
        ):
            adapter._attempt(
                lambda n=name, v=value: setattr(manager, n, v), default=None
            )

        try:
            # A real return value: 0 means SolidWorks found nothing, and a
            # COM failure raises instead of flattening into a false clean
            # result.
            count = int(manager.GetInterferenceCount() or 0)
            interferences = (
                adapter._attempt(lambda: manager.GetInterferences(), default=None)
                if count
                else None
            )
            details = _interference_details(adapter, interferences)
        finally:
            # Leaves the assembly out of interference-display mode even
            # when the read above fails.
            adapter._attempt(lambda: manager.Done(), default=None)

        if wanted_names:
            details = [
                item
                for item in details
                if wanted_names & set(item.get("components", []))
            ]

        return {
            "interference_found": bool(details) if wanted_names else count > 0,
            "interference_count": len(details) if wanted_names else count,
            "interferences": details,
            "coincident_treated_as_interference": coincident,
            "scope": sorted(wanted_names) if wanted_names else "whole assembly",
            # Said plainly rather than silently dropped: a caller passing a
            # tolerance would otherwise assume it was applied.
            "tolerance_applied": None,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("check_interference", _check),
    )
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.

        ``currentModel`` may be a raw ``swApp.ActiveDoc`` dispatch that was
        never run through ``flag_doc`` (``get_model_info`` assigns it
        straight from ``ActiveDoc``). On such a dispatch, late binding
        returns zero-arg accessors property-style, so ``model.GetTitle()``
        tries to *call* the returned string and raises
        ``'str' object is not callable`` (issue #91, COM pitfall #5).
        Read them through ``_get_attr_or_call`` which works either way.
        """
        if save:
            adapter._get_attr_or_call(model, "Save")
        title = adapter._get_attr_or_call(model, "GetTitle")
        app.CloseDoc(title)
        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")

        # Slot 10 is swDefaultTemplateDrawing. Slot 1 was read here
        # before and comes back empty on SW 2025, after which the
        # fallback did GetUserPreferenceStringValue(0).replace("Part",
        # "Drawing") on another empty string - so NewDocument got "" and
        # every drawing operation was unreachable. Measured live on
        # SW 2025: 8=Part.prtdot, 9=Assembly.asmdot, 10=Drawing.drwdot,
        # 0-3 all empty.
        drw_template = self._resolve_template_path([10, 1, 0, 2, 3], ".drwdot")
        if not drw_template:
            raise Exception("No drawing template configured in SolidWorks")

        model = app.NewDocument(drw_template, 12, 0.2794, 0.2159)
        if not model:
            raise Exception(
                f"Failed to create new drawing from template "
                f"'{drw_template}'"
            )

        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_drawing_view async
create_drawing_view(payload: Any = None) -> AdapterResult[dict[str, Any]]

Place a view of a model on the active drawing sheet.

Parameters:

Name Type Description Default
payload Any

Tool payload; see _place_view.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The new view's name and position.

Example::

await adapter.create_drawing_view(
    {"model_path": r"C:\parts\bracket.sldprt", "orientation": "front"}
)
Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def create_drawing_view(
    self, payload: Any = None
) -> AdapterResult[dict[str, Any]]:
    """Place a view of a model on the active drawing sheet.

    Args:
        payload: Tool payload; see ``_place_view``.

    Returns:
        AdapterResult[dict[str, Any]]: The new view's name and position.

    Example::

        await adapter.create_drawing_view(
            {"model_path": r"C:\\parts\\bracket.sldprt", "orientation": "front"}
        )
    """
    return self._place_view(payload)
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

Optional linear unit system for the new part - mm, cm, m, in or ft (aliases such as inch accepted). Applied after creation; the outcome is recorded under properties["units"] (or properties["units_error"] if it could not be applied). An unrecognised token is an error.

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: Optional linear unit system for the new part - ``mm``,
            ``cm``, ``m``, ``in`` or ``ft`` (aliases such as ``inch``
            accepted). Applied after creation; the outcome is recorded
            under ``properties["units"]`` (or ``properties["units_error"]``
            if it could not be applied). An unrecognised token is an error.

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

    unit_token: str | None = None
    if units is not None and str(units).strip():
        unit_token = _normalise_unit_system(str(units))
        if unit_token is None:
            return AdapterResult(
                status=AdapterResultStatus.ERROR,
                error=(
                    f"Unrecognised units {units!r}. "
                    "Expected one of: mm, cm, m, in, ft."
                ),
            )

    def _create() -> SolidWorksModel:
        """Create a new part."""
        _ = name
        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)
        properties: dict[str, Any] = {"created": datetime.now().isoformat()}
        if unit_token is not None:
            # A units failure must not lose the part that was just made;
            # record it and carry on.
            try:
                properties["units"] = _apply_unit_system(
                    adapter, model, unit_token
                )
            except Exception as exc:  # noqa: BLE001 - surfaced, not raised
                properties["units_error"] = str(exc)
        return SolidWorksModel(
            path="",
            name=title,
            type="Part",
            is_active=True,
            configuration="Default",
            properties=properties,
        )

    return cast(
        AdapterResult[SolidWorksModel],
        adapter._handle_com_operation("create_part", _create),
    )
create_technical_drawing async
create_technical_drawing(payload: Any = None) -> AdapterResult[dict[str, Any]]

Lay out the three standard views of a model on the active sheet.

Wraps IDrawingDoc::Create3rdAngleViews2 (or Create1stAngleViews2), which places front, top and side in one call. Both return a bare boolean, so the view list before and after is what actually confirms the views exist.

Parameters:

Name Type Description Default
payload Any

Tool payload. Reads model_path/model_file and third_angle (default True; projection set to "first_angle" selects the other).

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The view names that appeared.

Example::

await adapter.create_technical_drawing(
    {"model_file": r"C:\parts\bracket.sldprt"}
)
Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def create_technical_drawing(
    self, payload: Any = None
) -> AdapterResult[dict[str, Any]]:
    """Lay out the three standard views of a model on the active sheet.

    Wraps ``IDrawingDoc::Create3rdAngleViews2`` (or
    ``Create1stAngleViews2``), which places front, top and side in one
    call. Both return a bare boolean, so the view list before and after is
    what actually confirms the views exist.

    Args:
        payload: Tool payload. Reads ``model_path``/``model_file`` and
            ``third_angle`` (default ``True``; ``projection`` set to
            ``"first_angle"`` selects the other).

    Returns:
        AdapterResult[dict[str, Any]]: The view names that appeared.

    Example::

        await adapter.create_technical_drawing(
            {"model_file": r"C:\\parts\\bracket.sldprt"}
        )
    """
    adapter = self._adapter(self)
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)

    data = _payload(payload)
    model_path = _first(data, "model_path", "model_file", "path")
    if not model_path:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="A model path is required (model_path or model_file)",
        )

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

    third_angle = bool(data.get("third_angle", True))
    if str(data.get("projection", "")).lower() in {"first", "first_angle"}:
        third_angle = False

    def _standard() -> dict[str, Any]:
        drawing = _sw_type_info.flagged(adapter.currentModel, "IDrawingDoc")
        before = _view_names(adapter, drawing)

        created = adapter._attempt(
            lambda: (
                drawing.Create3rdAngleViews2(path)
                if third_angle
                else drawing.Create1stAngleViews2(path)
            ),
            default=False,
        )

        after = _view_names(adapter, drawing)
        added = [n for n in after if n not in before]
        if not added:
            raise Exception(
                f"No views were created from '{model_path}' (the call "
                f"returned {created!r}). Check the model has solid "
                "geometry and opens on its own."
            )

        return {
            "views": added,
            "model_path": path,
            "projection": "third_angle" if third_angle else "first_angle",
            "views_before": len(before),
            "views_after": len(after),
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("create_technical_drawing", _standard),
    )
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)
    # Resync from ActiveDoc: the user may have opened or switched
    # documents in the SolidWorks UI since the last tool call (issue #91).
    self._sync_current_model_from_active()
    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),
    )
list_drawing_views async
list_drawing_views() -> AdapterResult[list[str]]

List the views on the active drawing.

Returns:

Type Description
AdapterResult[list[str]]

AdapterResult[list[str]]: View names, sheet formats excluded.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def list_drawing_views(self) -> AdapterResult[list[str]]:
    """List the views on the active drawing.

    Returns:
        AdapterResult[list[str]]: View names, sheet formats excluded.
    """
    adapter = self._adapter(self)
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[list[str]]", guard)

    def _list_views() -> list[str]:
        drawing = _sw_type_info.flagged(adapter.currentModel, "IDrawingDoc")
        return _view_names(adapter, drawing)

    return cast(
        AdapterResult[list[str]],
        adapter._handle_com_operation("list_drawing_views", _list_views),
    )
list_open_documents async
list_open_documents() -> AdapterResult[list[dict[str, Any]]]

Enumerate every document currently open in SolidWorks.

Read-only: uses ISldWorks::GetDocuments and does not touch the active-document selection. Each entry carries title, path, type (Part / Assembly / Drawing / Unknown) and is_active.

Returns:

Type Description
AdapterResult[list[dict[str, Any]]]

AdapterResult[list[dict[str, Any]]]: One entry per open document

AdapterResult[list[dict[str, Any]]]

(possibly empty). ERROR only when not connected.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def list_open_documents(self) -> AdapterResult[list[dict[str, Any]]]:
    """Enumerate every document currently open in SolidWorks.

    Read-only: uses ``ISldWorks::GetDocuments`` and does not touch the
    active-document selection. Each entry carries ``title``, ``path``,
    ``type`` (``Part`` / ``Assembly`` / ``Drawing`` / ``Unknown``) and
    ``is_active``.

    Returns:
        AdapterResult[list[dict[str, Any]]]: One entry per open document
        (possibly empty). ``ERROR`` only when not connected.
    """
    adapter = self._adapter(self)
    if not adapter.is_connected():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
        )

    def _list() -> list[dict[str, Any]]:
        app = adapter.swApp
        if app is None:
            raise Exception("SolidWorks application is not connected")

        docs = _coerce_dispatch_sequence(
            adapter._attempt(lambda: app.GetDocuments(), default=None)
        )
        active = (
            adapter._attempt(lambda: getattr(app, "ActiveDoc", None), default=None)
        )
        active_title = (
            adapter._attempt(
                lambda: adapter._get_attr_or_call(active, "GetTitle"), default=None
            )
            if active is not None
            else None
        )
        active_path = (
            adapter._attempt(
                lambda: adapter._get_attr_or_call(active, "GetPathName"),
                default=None,
            )
            if active is not None
            else None
        )
        return [
            _describe_open_document(adapter, d, active_title, active_path)
            for d in docs
        ]

    return cast(
        AdapterResult[list[dict[str, Any]]],
        adapter._handle_com_operation("list_open_documents", _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)

        # OpenDoc6 with swOpenDocOptions_Silent opens the file but does not
        # make it the active document - SolidWorks keeps the previously
        # focused window active. Without this, get_model_info and every
        # later tool call (exports especially) target the wrong document
        # (issue #91). ActivateDoc3 takes the titlebar name, not the path.
        if title:
            activated = adapter._attempt(
                lambda: app.ActivateDoc3(title, False, 0, _byref_int())
            )
            if activated is not None:
                adapter._attempt(
                    lambda: _sw_type_info.flag_doc(activated, int(doc_type)),
                    default=0,
                )
                adapter.currentModel = activated
        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_body_as_part async
save_body_as_part(body_name: str, file_path: str) -> AdapterResult[dict[str, Any]]

Extract one solid body from the active multibody part to a new file.

Wraps IFeatureManager::CreateSaveBodyFeature - the API behind Insert > Features > Save Bodies. body_name is matched against IPartDoc::GetBodies2(swSolidBody) by IBody2::Name; a Save Bodies feature is added and the standalone part is written to file_path. CreateSaveBodyFeature returns the feature (or None) but no write status, so success is confirmed by the file existing afterwards.

Parameters:

Name Type Description Default
body_name str

Name of a solid body in the active part.

required
file_path str

Absolute path for the new .sldprt; its parent directory must already exist.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The body, the written path, the new

AdapterResult[dict[str, Any]]

feature's name, and every solid-body name found. ERROR when

AdapterResult[dict[str, Any]]

the active document is not a part, the body is absent, the parent

AdapterResult[dict[str, Any]]

directory is missing, or no file was written.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def save_body_as_part(
    self, body_name: str, file_path: str
) -> AdapterResult[dict[str, Any]]:
    """Extract one solid body from the active multibody part to a new file.

    Wraps ``IFeatureManager::CreateSaveBodyFeature`` - the API behind
    Insert > Features > Save Bodies. ``body_name`` is matched against
    ``IPartDoc::GetBodies2(swSolidBody)`` by ``IBody2::Name``; a Save
    Bodies feature is added and the standalone part is written to
    ``file_path``. ``CreateSaveBodyFeature`` returns the feature (or
    ``None``) but no write status, so success is confirmed by the file
    existing afterwards.

    Args:
        body_name: Name of a solid body in the active part.
        file_path: Absolute path for the new ``.sldprt``; its parent
            directory must already exist.

    Returns:
        AdapterResult[dict[str, Any]]: The body, the written path, the new
        feature's name, and every solid-body name found. ``ERROR`` when
        the active document is not a part, the body is absent, the parent
        directory is missing, or no file was written.
    """
    adapter = self._adapter(self)
    self._sync_current_model_from_active()
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )
    if _doc_type(adapter) != 1:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="save_body_as_part requires an active part document",
        )
    if not str(body_name or "").strip():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="body_name is required"
        )
    target = str(file_path or "").strip()
    if not target:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="file_path is required"
        )
    target = os.path.abspath(target)
    parent = os.path.dirname(target)
    if not os.path.isdir(parent):
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=f"Parent directory does not exist: {parent}",
        )

    def _save_body() -> dict[str, Any]:
        model = adapter.currentModel
        part = _sw_type_info.flagged(model, "IPartDoc")
        raw_bodies = adapter._attempt(
            lambda: part.GetBodies2(_SW_SOLID_BODY, False), default=None
        )
        bodies = (
            list(raw_bodies)
            if isinstance(raw_bodies, (list, tuple))
            else ([raw_bodies] if raw_bodies else [])
        )
        if not bodies:
            raise Exception("The active part has no solid bodies")

        names: list[str] = []
        match = None
        for body in bodies:
            flagged = _sw_type_info.flagged(body, "IBody2")
            name = adapter._attempt(
                lambda f=flagged: adapter._get_attr_or_call(f, "Name"),
                default=None,
            )
            if name is not None:
                names.append(str(name))
                if str(name) == body_name:
                    match = flagged
        if match is None:
            raise Exception(
                f"Body {body_name!r} not found. Solid bodies: "
                + (", ".join(names) or "none")
            )

        manager = adapter._attempt(lambda: model.FeatureManager, default=None)
        if manager is None:
            raise Exception("Part has no FeatureManager")
        manager = _sw_type_info.flagged(manager, "IFeatureManager")

        if os.path.exists(target):
            os.remove(target)

        feature = adapter._attempt(
            lambda: manager.CreateSaveBodyFeature(
                _variant_array(pythoncom.VT_DISPATCH, [match]),
                _variant_array(pythoncom.VT_BSTR, [target]),
                "",
                False,
                True,
            ),
            default=None,
        )
        adapter._attempt(lambda: model.EditRebuild3(), default=None)

        if not os.path.exists(target):
            raise Exception(
                f"CreateSaveBodyFeature did not write a file to {target}; "
                "the Save Bodies feature was rejected."
            )

        feat_name = (
            adapter._attempt(
                lambda: adapter._get_attr_or_call(feature, "Name"),
                default=None,
            )
            if feature is not None
            else None
        )
        return {
            "body": body_name,
            "file_path": target,
            "feature": str(feat_name) if feat_name else None,
            "solid_bodies": names,
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("save_body_as_part", _save_body),
    )
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),
    )
set_units async
set_units(unit_system: str) -> AdapterResult[dict[str, Any]]

Set the active document's linear unit system.

Accepts mm, cm, m, in or ft (plus common aliases such as inch / millimeters). The choice is applied by setting both swUnitSystem and the exact swUnitsLinear preference, then reading the linear unit back - a mismatch is reported as an error.

Parameters:

Name Type Description Default
unit_system str

The target unit token.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: What was applied and whether the

AdapterResult[dict[str, Any]]

readback confirmed it. ERROR when there is no active model or

AdapterResult[dict[str, Any]]

the token is not recognised.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def set_units(self, unit_system: str) -> AdapterResult[dict[str, Any]]:
    """Set the active document's linear unit system.

    Accepts ``mm``, ``cm``, ``m``, ``in`` or ``ft`` (plus common aliases
    such as ``inch`` / ``millimeters``). The choice is applied by setting
    both ``swUnitSystem`` and the exact ``swUnitsLinear`` preference, then
    reading the linear unit back - a mismatch is reported as an error.

    Args:
        unit_system: The target unit token.

    Returns:
        AdapterResult[dict[str, Any]]: What was applied and whether the
        readback confirmed it. ``ERROR`` when there is no active model or
        the token is not recognised.
    """
    adapter = self._adapter(self)
    # Resync from ActiveDoc in case the user switched documents in the UI.
    self._sync_current_model_from_active()
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    token = _normalise_unit_system(unit_system)
    if token is None:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                f"Unrecognised unit system {unit_system!r}. "
                "Expected one of: mm, cm, m, in, ft."
            ),
        )

    def _apply() -> dict[str, Any]:
        return _apply_unit_system(adapter, adapter.currentModel, token)

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("set_units", _apply),
    )

SolidWorksSelectionMixin

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

SolidWorksSketchMixin

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