Skip to content

solidworks_mcp.adapters.solidworks.io

solidworks_mcp.adapters.solidworks.io

Model I/O mixin for PyWin32 SolidWorks operations.

Attributes

_COMTYPES_AVAILABLE module-attribute

_COMTYPES_AVAILABLE = True

_DOC_TYPE_NAMES module-attribute

_DOC_TYPE_NAMES: dict[int, str] = {1: 'Part', 2: 'Assembly', 3: 'Drawing'}

_MATE_ALIGNMENTS module-attribute

_MATE_ALIGNMENTS: dict[str, int] = {'aligned': 0, 'anti_aligned': 1, 'closest': 2}

_MATE_TYPES module-attribute

_MATE_TYPES: dict[str, int] = {'coincident': 0, 'concentric': 1, 'perpendicular': 2, 'parallel': 3, 'tangent': 4, 'distance': 5, 'angle': 6}

_NAMED_VIEWS module-attribute

_NAMED_VIEWS: dict[str, str] = {'front': '*Front', 'back': '*Back', 'left': '*Left', 'right': '*Right', 'top': '*Top', 'bottom': '*Bottom', 'isometric': '*Isometric', 'iso': '*Isometric', 'trimetric': '*Trimetric', 'dimetric': '*Dimetric', 'current': '*Current'}

_POINTS_TO_MM module-attribute

_POINTS_TO_MM = 25.4 / 72.0

_SW_ANNOTATION_CENTER_MARK module-attribute

_SW_ANNOTATION_CENTER_MARK = 13

_SW_CM_STYLE_SINGLE module-attribute

_SW_CM_STYLE_SINGLE = 2

_SW_CM_TYPE_FILLET module-attribute

_SW_CM_TYPE_FILLET = 2

_SW_CM_TYPE_HOLE module-attribute

_SW_CM_TYPE_HOLE = 1

_SW_CM_TYPE_SLOT module-attribute

_SW_CM_TYPE_SLOT = 4

_SW_PREF_OPTION_NONE module-attribute

_SW_PREF_OPTION_NONE = 0

_SW_PREF_UNITS_LINEAR module-attribute

_SW_PREF_UNITS_LINEAR = 47

_SW_PREF_UNITS_LINEAR_DECIMALS module-attribute

_SW_PREF_UNITS_LINEAR_DECIMALS = 49

_SW_PREF_UNIT_SYSTEM module-attribute

_SW_PREF_UNIT_SYSTEM = 263

_SW_SOLID_BODY module-attribute

_SW_SOLID_BODY = 0

_SW_TLB_GUID module-attribute

_SW_TLB_GUID = '{83A33D31-27C5-11CE-BFD4-00400513BB57}'

_SW_UNIT_ALIASES module-attribute

_SW_UNIT_ALIASES: dict[str, str] = {'mm': 'mm', 'millimeter': 'mm', 'millimeters': 'mm', 'millimetre': 'mm', 'millimetres': 'mm', 'cm': 'cm', 'centimeter': 'cm', 'centimeters': 'cm', 'm': 'm', 'meter': 'm', 'meters': 'm', 'metre': 'm', 'metres': 'm', 'in': 'in', 'inch': 'in', 'inches': 'in', '"': 'in', 'ft': 'ft', 'foot': 'ft', 'feet': 'ft', "'": 'ft'}

_SW_UNIT_SYSTEMS module-attribute

_SW_UNIT_SYSTEMS: dict[str, tuple[int, int]] = {'mm': (5, 0), 'cm': (1, 1), 'm': (2, 2), 'in': (3, 3), 'ft': (4, 4)}

_SW_UNIT_SYSTEM_CUSTOM module-attribute

_SW_UNIT_SYSTEM_CUSTOM = 4

_sw_comtypes_lib module-attribute

_sw_comtypes_lib: Any = None

Classes

AdapterResult dataclass

AdapterResult(status: AdapterResultStatus, data: T | None = None, error: str | None = None, execution_time: float | None = None, metadata: dict[str, Any] | None = None)

Bases: Generic[T]

Result wrapper for adapter operations.

Attributes:

Name Type Description
data T | None

The data value.

error str | None

The error value.

execution_time float | None

The execution time value.

metadata dict[str, Any] | None

The metadata value.

status AdapterResultStatus

The status value.

Attributes
is_error property
is_error: bool

Check if operation had an error.

Returns:

Name Type Description
bool bool

True if error, otherwise False.

is_success property
is_success: bool

Check if operation was successful.

Returns:

Name Type Description
bool bool

True if success, otherwise False.

AdapterResultStatus

Bases: StrEnum

Result status for adapter operations.

Attributes:

Name Type Description
ERROR Any

The error value.

SUCCESS Any

The success value.

TIMEOUT Any

The timeout value.

WARNING Any

The warning value.

MassProperties

Bases: BaseModel

Mass properties information.

Attributes:

Name Type Description
center_of_mass list[float]

The center of mass value.

mass float

The mass value.

moments_of_inertia dict[str, float]

The moments of inertia value.

principal_axes dict[str, list[float]] | None

The principal axes value.

surface_area float

The surface area value.

volume float

The volume value.

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

SolidWorksModel

Bases: BaseModel

SolidWorks model information.

Attributes:

Name Type Description
configuration str | None

The configuration value.

is_active bool

The is active value.

name str

The name value.

path str

The path value.

properties dict[str, Any] | None

The properties value.

type str

The type value.

Methods:
__getitem__
__getitem__(key: str) -> Any

Build internal getitem.

Parameters:

Name Type Description Default
key str

The key value.

required

Returns:

Name Type Description
Any Any

The result produced by the operation.

Source code in src/solidworks_mcp/adapters/base.py
def __getitem__(self, key: str) -> Any:
    """Build internal getitem.

    Args:
        key (str): The key value.

    Returns:
        Any: The result produced by the operation.
    """
    if key == "title":
        return self.name
    if key == "units":
        return (self.properties or {}).get("units")
    return self.model_dump().get(key)

_ByrefFallback

_ByrefFallback(value: Any)

Stand-in for a byref VARIANT when pywin32 is unavailable.

The callers' contract for a byref holder is "an object whose .value the COM call fills in". Returning a bare 0 or "" broke that: on any machine without pywin32 (Linux CI, mock runs) _read_material_name could never read its database out-parameter back, so it silently reported None. This keeps the contract on both platforms.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def __init__(self, value: Any) -> None:
    self.value = value
Methods:
__bool__
__bool__() -> bool

Truthiness follows the seeded value.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def __bool__(self) -> bool:
    """Truthiness follows the seeded value."""
    return bool(self.value)
__eq__
__eq__(other: Any) -> bool

Compare equal to the seeded value, so callers can treat it as one.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def __eq__(self, other: Any) -> bool:
    """Compare equal to the seeded value, so callers can treat it as one."""
    if isinstance(other, _ByrefFallback):
        return bool(self.value == other.value)
    return bool(self.value == other)
__hash__
__hash__() -> int

Hash as the seeded value.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def __hash__(self) -> int:
    """Hash as the seeded value."""
    return hash(self.value)
__repr__
__repr__() -> str

Show the seeded value for readable assertion output.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def __repr__(self) -> str:
    """Show the seeded value for readable assertion output."""
    return f"_ByrefFallback({self.value!r})"

Functions:

_apply_unit_system

_apply_unit_system(adapter: Any, model: Any, unit_system: str) -> dict[str, Any]

Set a document's linear unit system and confirm it actually took.

IModelDocExtension::SetUserPreferenceInteger accepts the swUnitSystem write and returns quietly even on builds where the document's unit system does not move (observed on SW 2026 / 3DEXPERIENCE against an assembly). The change is therefore verified by reading the unit-system preference itself (swUnitSystem = slot 263) back after a rebuild - not the swUnitsLinear slot, which echoes whatever was written regardless of whether the document honoured it.

For the mm / cm / m / in presets only swUnitSystem is set and the preset drives the linear unit. ft has no preset, so it is applied as swUnitSystem_Custom plus an explicit swUnitsLinear.

Parameters:

Name Type Description Default
adapter Any

A connected adapter.

required
model Any

The IModelDoc2 to modify.

required
unit_system str

Already-normalised token (key of _SW_UNIT_SYSTEMS).

required

Returns:

Type Description
dict[str, Any]

dict[str, Any]: unit_system; applied_unit_system /

dict[str, Any]

applied_length_unit (the swUnitSystem_e / swLengthUnit_e targets);

dict[str, Any]

observed_unit_system / observed_length_unit (what the document

dict[str, Any]

reports afterwards); set_call_ok (bool the COM call returned);

dict[str, Any]

and verified (True only when the document's own unit-system

dict[str, Any]

preference now equals the target; None when it could not be read).

Raises:

Type Description
Exception

When the extension is unavailable, or the readback shows the document did not adopt the requested unit system.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _apply_unit_system(adapter: Any, model: Any, unit_system: str) -> dict[str, Any]:
    """Set a document's linear unit system and confirm it actually took.

    ``IModelDocExtension::SetUserPreferenceInteger`` accepts the
    ``swUnitSystem`` write and returns quietly even on builds where the
    document's unit system does not move (observed on SW 2026 / 3DEXPERIENCE
    against an assembly). The change is therefore verified by reading the
    *unit-system preference itself* (``swUnitSystem`` = slot 263) back after
    a rebuild - not the ``swUnitsLinear`` slot, which echoes whatever was
    written regardless of whether the document honoured it.

    For the ``mm`` / ``cm`` / ``m`` / ``in`` presets only ``swUnitSystem`` is
    set and the preset drives the linear unit. ``ft`` has no preset, so it is
    applied as ``swUnitSystem_Custom`` plus an explicit ``swUnitsLinear``.

    Args:
        adapter: A connected adapter.
        model: The ``IModelDoc2`` to modify.
        unit_system: Already-normalised token (key of ``_SW_UNIT_SYSTEMS``).

    Returns:
        dict[str, Any]: ``unit_system``; ``applied_unit_system`` /
        ``applied_length_unit`` (the swUnitSystem_e / swLengthUnit_e targets);
        ``observed_unit_system`` / ``observed_length_unit`` (what the document
        reports afterwards); ``set_call_ok`` (bool the COM call returned);
        and ``verified`` (``True`` only when the document's own unit-system
        preference now equals the target; ``None`` when it could not be read).

    Raises:
        Exception: When the extension is unavailable, or the readback shows
            the document did not adopt the requested unit system.
    """
    system_value, length_value = _SW_UNIT_SYSTEMS[unit_system]
    is_custom = system_value == _SW_UNIT_SYSTEM_CUSTOM
    ext = adapter._attempt(lambda: model.Extension, default=None)
    if ext is None:
        raise Exception("Document has no Extension; cannot set units")

    before_system = _pref_int(adapter, ext, _SW_PREF_UNIT_SYSTEM)
    before_linear = _pref_int(adapter, ext, _SW_PREF_UNITS_LINEAR)

    set_system_ok = adapter._attempt(
        lambda: ext.SetUserPreferenceInteger(
            _SW_PREF_UNIT_SYSTEM, _SW_PREF_OPTION_NONE, system_value
        ),
        default=None,
    )
    set_linear_ok: Any = None
    if is_custom:
        set_linear_ok = adapter._attempt(
            lambda: ext.SetUserPreferenceInteger(
                _SW_PREF_UNITS_LINEAR, _SW_PREF_OPTION_NONE, length_value
            ),
            default=None,
        )

    # Nudge SolidWorks to apply and to flag the document dirty.
    adapter._attempt(lambda: model.EditRebuild3(), default=None)
    adapter._attempt(lambda: model.GraphicsRedraw2(), default=None)
    adapter._attempt(lambda: model.SetSaveFlag(), default=None)

    after_system = _pref_int(adapter, ext, _SW_PREF_UNIT_SYSTEM)
    after_linear = _pref_int(adapter, ext, _SW_PREF_UNITS_LINEAR)

    if after_system is None:
        verified: bool | None = None
    else:
        verified = after_system == system_value
        if is_custom and verified:
            verified = after_linear == length_value

    result = {
        "unit_system": unit_system,
        "applied_unit_system": system_value,
        "applied_length_unit": length_value,
        "observed_unit_system": after_system,
        "observed_length_unit": after_linear,
        "unit_system_before": before_system,
        "length_unit_before": before_linear,
        "set_call_ok": bool(set_system_ok) if set_system_ok is not None else None,
        "set_linear_call_ok": (
            bool(set_linear_ok) if set_linear_ok is not None else None
        ),
        "verified": verified,
    }

    if verified is False:
        raise Exception(
            f"Unit system '{unit_system}' (swUnitSystem={system_value}) was not "
            f"adopted: SetUserPreferenceInteger returned {result['set_call_ok']!r}, "
            f"and the document still reports swUnitSystem={after_system}, "
            f"swUnitsLinear={after_linear} (was {before_system}/{before_linear})."
        )

    return result

_as_com

_as_com(adapter: Any, obj: Any, interface: str) -> Any

Wrap a raw dispatch and flag its methods for an interface.

Objects handed back inside arrays (GetViews and friends) arrive as raw PyIDispatch. Method flagging is a no-op on those, so every call against them raises until they are wrapped through dynamic.Dispatch.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
obj Any

The raw dispatch.

required
interface str

Interface name, e.g. "IView".

required

Returns:

Name Type Description
Any Any

The wrapped, flagged object, or None.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _as_com(adapter: Any, obj: Any, interface: str) -> Any:
    """Wrap a raw dispatch and flag its methods for an interface.

    Objects handed back inside arrays (``GetViews`` and friends) arrive as raw
    ``PyIDispatch``.  Method flagging is a no-op on those, so every call
    against them raises until they are wrapped through ``dynamic.Dispatch``.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        obj: The raw dispatch.
        interface: Interface name, e.g. ``"IView"``.

    Returns:
        Any: The wrapped, flagged object, or ``None``.
    """
    wrapped = adapter._attempt(lambda: _dynamic.Dispatch(obj), default=None)
    if wrapped is None:
        return None
    adapter._attempt(
        lambda: _sw_type_info.flag_methods(wrapped, interface), default=None
    )
    return wrapped

_bridge_com_to_comtypes

_bridge_com_to_comtypes(pywin32_obj: Any, iface: Any) -> Any

Bridge a pywin32 CDispatch/COM object to a comtypes interface pointer.

Extracts the raw IUnknown pointer from pywin32's repr string, then QueryInterface-es for iface via comtypes. The AddRef keeps the object alive through the Python reference.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _bridge_com_to_comtypes(pywin32_obj: Any, iface: Any) -> Any:  # pragma: no cover
    """Bridge a pywin32 CDispatch/COM object to a comtypes interface pointer.

    Extracts the raw IUnknown pointer from pywin32's repr string, then
    QueryInterface-es for *iface* via comtypes.  The AddRef keeps the object
    alive through the Python reference.
    """
    unk = pywin32_obj._oleobj_.QueryInterface(pythoncom.IID_IUnknown)
    m = re.search(r"obj at (0x[0-9a-fA-F]+)", repr(unk))
    if not m:
        raise RuntimeError(f"Could not extract COM pointer from {repr(unk)!r}")
    ptr_int = int(m.group(1), 16)
    ct_unk = ctypes.cast(ptr_int, ctypes.POINTER(comtypes.IUnknown))
    ct_unk.AddRef()
    return ct_unk.QueryInterface(iface)

_byref_int

_byref_int() -> Any

Return a byref long VARIANT for a SolidWorks out-parameter.

See :func:_byref_bstr for why pythoncom.Missing does not work.

Returns:

Name Type Description
Any Any

A VARIANT(VT_BYREF | VT_I4, 0), or 0 when pywin32 is

Any

unavailable (test/mock environments).

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _byref_int() -> Any:
    """Return a byref long VARIANT for a SolidWorks out-parameter.

    See :func:`_byref_bstr` for why ``pythoncom.Missing`` does not work.

    Returns:
        Any: A ``VARIANT(VT_BYREF | VT_I4, 0)``, or ``0`` when pywin32 is
        unavailable (test/mock environments).
    """
    variant_ctor = getattr(getattr(win32com, "client", None), "VARIANT", None)
    if not callable(variant_ctor):
        return _ByrefFallback(0)
    return variant_ctor(
        int(getattr(pythoncom, "VT_BYREF", 0)) | int(getattr(pythoncom, "VT_I4", 0)), 0
    )

_coerce_dispatch_sequence

_coerce_dispatch_sequence(raw: Any) -> list[Any]

Normalise ISldWorks::GetDocuments output into a list of dispatches.

Depending on the SolidWorks build and how many documents are open, GetDocuments returns None, a single IModelDoc2, or a tuple/list of them.

Parameters:

Name Type Description Default
raw Any

Whatever GetDocuments() returned.

required

Returns:

Type Description
list[Any]

list[Any]: Zero or more non-None dispatches.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _coerce_dispatch_sequence(raw: Any) -> list[Any]:
    """Normalise ``ISldWorks::GetDocuments`` output into a list of dispatches.

    Depending on the SolidWorks build and how many documents are open,
    ``GetDocuments`` returns ``None``, a single ``IModelDoc2``, or a
    tuple/list of them.

    Args:
        raw: Whatever ``GetDocuments()`` returned.

    Returns:
        list[Any]: Zero or more non-``None`` dispatches.
    """
    if raw is None:
        return []
    if isinstance(raw, (list, tuple)):
        return [d for d in raw if d is not None]
    return [raw]

_component_names

_component_names(adapter: Any, assembly: Any) -> list[str]

Return the names of an assembly's top-level components.

AddComponent* can return an object having added nothing, so the component list is the ground truth for whether an insert worked.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
assembly Any

The assembly document, flagged for IAssemblyDoc.

required

Returns:

Type Description
list[str]

list[str]: Component names, empty when the assembly holds none.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _component_names(adapter: Any, assembly: Any) -> list[str]:
    """Return the names of an assembly's top-level components.

    ``AddComponent*`` can return an object having added nothing, so the
    component list is the ground truth for whether an insert worked.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        assembly: The assembly document, flagged for ``IAssemblyDoc``.

    Returns:
        list[str]: Component names, empty when the assembly holds none.
    """
    return [name for name, _ in _component_pairs(adapter, assembly)]

_component_pairs

_component_pairs(adapter: Any, assembly: Any) -> list[tuple[str, Any]]

Return an assembly's top-level components as (name, dispatch).

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
assembly Any

The assembly document, flagged for IAssemblyDoc.

required

Returns:

Type Description
list[tuple[str, Any]]

list[tuple[str, Any]]: Component name and its IComponent2

list[tuple[str, Any]]

dispatch. A component whose dispatch cannot be wrapped is reported

list[tuple[str, Any]]

as ("<unnamed>", None) so callers still see the correct count.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _component_pairs(adapter: Any, assembly: Any) -> list[tuple[str, Any]]:
    """Return an assembly's top-level components as ``(name, dispatch)``.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        assembly: The assembly document, flagged for ``IAssemblyDoc``.

    Returns:
        list[tuple[str, Any]]: Component name and its ``IComponent2``
        dispatch. A component whose dispatch cannot be wrapped is reported
        as ``("<unnamed>", None)`` so callers still see the correct count.
    """
    components = adapter._attempt(lambda: assembly.GetComponents(True), default=None)
    if not isinstance(components, (list, tuple)):
        return []

    pairs: list[tuple[str, Any]] = []
    for component in components:
        wrapped = _as_com(adapter, component, "IComponent2")
        if wrapped is None:
            pairs.append(("<unnamed>", None))
            continue
        name = adapter._attempt(lambda c=wrapped: c.Name2, default=None)
        if not name:
            name = adapter._attempt(lambda c=wrapped: c.GetPathName(), default=None)
        pairs.append((str(name) if name else "<unnamed>", wrapped))
    return pairs

_component_transforms

_component_transforms(adapter: Any, assembly: Any) -> dict[str, tuple[float, ...]]

Snapshot every component's placement, for before/after comparison.

IComponent2.Transform2 is the component's position relative to the assembly root, as a MathTransform whose ArrayData is 16 doubles (a 3x3 rotation, a translation, and a scale). Comparing the snapshot before and after a mate is what distinguishes a mate that actually positioned geometry from one that SolidWorks accepted and ignored.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
assembly Any

The IAssemblyDoc dispatch to snapshot.

required

Returns:

Type Description
dict[str, tuple[float, ...]]

dict[str, tuple[float, ...]]: Component name to transform matrix.

dict[str, tuple[float, ...]]

Components whose transform cannot be read are omitted rather than

dict[str, tuple[float, ...]]

recorded as unchanged — an unreadable transform is not evidence of

dict[str, tuple[float, ...]]

anything, and treating it as "same" would fake a passing comparison.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _component_transforms(adapter: Any, assembly: Any) -> dict[str, tuple[float, ...]]:
    """Snapshot every component's placement, for before/after comparison.

    ``IComponent2.Transform2`` is the component's position relative to the
    assembly root, as a ``MathTransform`` whose ``ArrayData`` is 16 doubles
    (a 3x3 rotation, a translation, and a scale). Comparing the snapshot
    before and after a mate is what distinguishes a mate that actually
    positioned geometry from one that SolidWorks accepted and ignored.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        assembly: The ``IAssemblyDoc`` dispatch to snapshot.

    Returns:
        dict[str, tuple[float, ...]]: Component name to transform matrix.
        Components whose transform cannot be read are omitted rather than
        recorded as unchanged — an unreadable transform is not evidence of
        anything, and treating it as "same" would fake a passing comparison.
    """
    snapshot: dict[str, tuple[float, ...]] = {}
    for name, component in _component_pairs(adapter, assembly):
        if component is None:
            continue
        transform = adapter._attempt(lambda c=component: c.Transform2, default=None)
        if transform is None:
            continue
        data = adapter._attempt(lambda t=transform: t.ArrayData, default=None)
        if not isinstance(data, (list, tuple)):
            continue
        try:
            snapshot[name] = tuple(round(float(value), 9) for value in data)
        except (TypeError, ValueError):
            continue
    return snapshot

_describe_open_document

_describe_open_document(adapter: Any, doc: Any, active_title: str | None, active_path: str | None) -> dict[str, Any]

Summarise one open document: title, path, type and whether it is active.

Parameters:

Name Type Description Default
adapter Any

A connected adapter (for _get_attr_or_call).

required
doc Any

An IModelDoc2 dispatch.

required
active_title str | None

Title of ISldWorks::ActiveDoc, if known.

required
active_path str | None

Path of ISldWorks::ActiveDoc, if known.

required

Returns:

Type Description
dict[str, Any]

dict[str, Any]: title, path, type and is_active.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _describe_open_document(
    adapter: Any,
    doc: Any,
    active_title: str | None,
    active_path: str | None,
) -> dict[str, Any]:
    """Summarise one open document: title, path, type and whether it is active.

    Args:
        adapter: A connected adapter (for ``_get_attr_or_call``).
        doc: An ``IModelDoc2`` dispatch.
        active_title: Title of ``ISldWorks::ActiveDoc``, if known.
        active_path: Path of ``ISldWorks::ActiveDoc``, if known.

    Returns:
        dict[str, Any]: ``title``, ``path``, ``type`` and ``is_active``.
    """
    title = adapter._attempt(
        lambda: adapter._get_attr_or_call(doc, "GetTitle"), default=None
    )
    path = adapter._attempt(
        lambda: adapter._get_attr_or_call(doc, "GetPathName"), default=None
    )
    type_raw = adapter._attempt(
        lambda: adapter._get_attr_or_call(doc, "GetType"), default=None
    )
    type_name = _DOC_TYPE_NAMES.get(
        int(type_raw) if isinstance(type_raw, (int, float)) else -1, "Unknown"
    )
    is_active = False
    if title and active_title:
        is_active = str(title) == str(active_title)
    elif path and active_path:
        is_active = str(path).lower() == str(active_path).lower()
    return {
        "title": str(title) if title else None,
        "path": str(path) if path else "",
        "type": type_name,
        "is_active": is_active,
    }

_doc_type

_doc_type(adapter: Any) -> int | None

Return the active document's type: 1 part, 2 assembly, 3 drawing.

GetType is one of the members pywin32 late binding may expose as either a bound method or a plain value, so calling it directly returns None on some documents. _get_attr_or_call handles both shapes.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required

Returns:

Type Description
int | None

int | None: The document type, or None when it cannot be read.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _doc_type(adapter: Any) -> int | None:
    """Return the active document's type: 1 part, 2 assembly, 3 drawing.

    ``GetType`` is one of the members pywin32 late binding may expose as either
    a bound method or a plain value, so calling it directly returns ``None`` on
    some documents.  ``_get_attr_or_call`` handles both shapes.

    Args:
        adapter: A connected ``PyWin32Adapter``.

    Returns:
        int | None: The document type, or ``None`` when it cannot be read.
    """
    value = adapter._attempt(
        lambda: adapter._get_attr_or_call(adapter.currentModel, "GetType"),
        default=None,
    )
    return int(value) if isinstance(value, (int, float)) else None

_first

_first(payload: dict[str, Any], *keys: str, default: Any = None) -> Any

Return the first key present and not None.

The drawing tools disagree on field names for the same thing — a model is model_path on one schema and model_file on another — so each adapter method accepts every spelling its callers use.

Parameters:

Name Type Description Default
payload dict[str, Any]

The tool payload.

required
*keys str

Candidate key names, in priority order.

()
default Any

Returned when no key is present.

None

Returns:

Name Type Description
Any Any

The first value found, otherwise default.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _first(payload: dict[str, Any], *keys: str, default: Any = None) -> Any:
    """Return the first key present and not None.

    The drawing tools disagree on field names for the same thing — a model is
    ``model_path`` on one schema and ``model_file`` on another — so each
    adapter method accepts every spelling its callers use.

    Args:
        payload: The tool payload.
        *keys: Candidate key names, in priority order.
        default: Returned when no key is present.

    Returns:
        Any: The first value found, otherwise ``default``.
    """
    for key in keys:
        value = payload.get(key)
        if value is not None:
            return value
    return default

_get_sw_comtypes_lib

_get_sw_comtypes_lib() -> Any

Return the comtypes wrapper for the SolidWorks type library.

Tries SW version numbers 35..30 (newest to oldest). Returns None when comtypes is unavailable or the TLB cannot be found in the registry.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _get_sw_comtypes_lib() -> Any:
    """Return the comtypes wrapper for the SolidWorks type library.

    Tries SW version numbers 35..30 (newest to oldest). Returns ``None`` when
    comtypes is unavailable or the TLB cannot be found in the registry.
    """
    global _sw_comtypes_lib
    if _sw_comtypes_lib is not None:
        return _sw_comtypes_lib
    if not _COMTYPES_AVAILABLE:
        return None
    for major in (35, 34, 33, 32, 31, 30):  # pragma: no cover
        try:
            _sw_comtypes_lib = _comtypes_client.GetModule(
                (comtypes.GUID(_SW_TLB_GUID), major, 0)
            )
            return _sw_comtypes_lib
        except Exception:
            continue
    return None  # pragma: no cover

_interference_details

_interference_details(adapter: Any, interferences: Any) -> list[dict[str, Any]]

Describe each interference: the components involved and the overlap.

IInterference::Volume is in cubic metres and is converted to mm^3 to match every other volume this adapter reports.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
interferences Any

The sequence returned by GetInterferences.

required

Returns:

Type Description
list[dict[str, Any]]

list[dict[str, Any]]: One entry per interference. Empty when the

list[dict[str, Any]]

sequence is absent, which is what SolidWorks returns for a clean

list[dict[str, Any]]

assembly.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _interference_details(adapter: Any, interferences: Any) -> list[dict[str, Any]]:
    """Describe each interference: the components involved and the overlap.

    ``IInterference::Volume`` is in cubic metres and is converted to mm^3 to
    match every other volume this adapter reports.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        interferences: The sequence returned by ``GetInterferences``.

    Returns:
        list[dict[str, Any]]: One entry per interference. Empty when the
        sequence is absent, which is what SolidWorks returns for a clean
        assembly.
    """
    if not isinstance(interferences, (list, tuple)):
        return []

    details: list[dict[str, Any]] = []
    for item in interferences:
        wrapped = _as_com(adapter, item, "IInterference")
        if wrapped is None:
            continue

        volume_m3 = adapter._attempt(lambda w=wrapped: w.Volume, default=None)
        try:
            volume_mm3 = (
                round(float(volume_m3) * 1e9, 6) if volume_m3 is not None else None
            )
        except (TypeError, ValueError):
            volume_mm3 = None

        component_count = adapter._attempt(
            lambda w=wrapped: w.GetComponentCount(), default=None
        )
        # ``Components`` is a property, not a method. ``GetComponents()``
        # does not exist on IInterference and ``IGetComponents(n)`` rejects
        # its own documented argument with "Invalid number of parameters".
        raw_components = adapter._attempt(
            lambda w=wrapped: w.Components, default=None
        )

        names: list[str] = []
        if isinstance(raw_components, (list, tuple)):
            for component in raw_components:
                comp = _as_com(adapter, component, "IComponent2")
                if comp is None:
                    continue
                name = adapter._attempt(lambda c=comp: c.Name2, default=None)
                names.append(str(name) if name else "<unnamed>")

        details.append(
            {
                "components": names,
                "component_count": int(component_count) if component_count else len(names),
                "volume_mm3": volume_mm3,
            }
        )
    return details

_normalise_unit_system

_normalise_unit_system(unit_system: str) -> str | None

Map a user-supplied unit token onto a canonical key of _SW_UNIT_SYSTEMS.

Parameters:

Name Type Description Default
unit_system str

e.g. "mm", "inch", "Millimeters".

required

Returns:

Type Description
str | None

str | None: "mm" | "cm" | "m" | "in" | "ft", or None when

str | None

the token is not recognised.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _normalise_unit_system(unit_system: str) -> str | None:
    """Map a user-supplied unit token onto a canonical key of ``_SW_UNIT_SYSTEMS``.

    Args:
        unit_system: e.g. ``"mm"``, ``"inch"``, ``"Millimeters"``.

    Returns:
        str | None: ``"mm" | "cm" | "m" | "in" | "ft"``, or ``None`` when
        the token is not recognised.
    """
    return _SW_UNIT_ALIASES.get((unit_system or "").strip().lower())

_payload

_payload(data: Any) -> dict[str, Any]

Coerce a tool payload into a plain dict.

The drawing tools hand the adapter either a model_dump() result or a raw dict, depending on the tool. Accept a Pydantic model too, so a direct adapter call from a test or script behaves the same as one through a tool.

Parameters:

Name Type Description Default
data Any

A dict, a Pydantic model, or an object with attributes.

required

Returns:

Type Description
dict[str, Any]

dict[str, Any]: The payload as a dict; empty when nothing was given.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _payload(data: Any) -> dict[str, Any]:
    """Coerce a tool payload into a plain dict.

    The drawing tools hand the adapter either a ``model_dump()`` result or a
    raw dict, depending on the tool. Accept a Pydantic model too, so a direct
    adapter call from a test or script behaves the same as one through a tool.

    Args:
        data: A dict, a Pydantic model, or an object with attributes.

    Returns:
        dict[str, Any]: The payload as a dict; empty when nothing was given.
    """
    if data is None:
        return {}
    if isinstance(data, dict):
        return data
    dump = getattr(data, "model_dump", None)
    if callable(dump):
        return cast("dict[str, Any]", dump())
    return {
        key: value for key, value in vars(data).items() if not key.startswith("_")
    }

_payload_dict

_payload_dict(data: Any) -> dict[str, Any]

Coerce a tool payload into a plain dict.

tools/analysis.py hands the adapter input_data.model_dump(), but a direct call from a test or script may pass the model itself or nothing.

Parameters:

Name Type Description Default
data Any

A dict, a Pydantic model, or None.

required

Returns:

Type Description
dict[str, Any]

dict[str, Any]: The payload as a dict; empty when nothing was given.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _payload_dict(data: Any) -> dict[str, Any]:
    """Coerce a tool payload into a plain dict.

    ``tools/analysis.py`` hands the adapter ``input_data.model_dump()``, but a
    direct call from a test or script may pass the model itself or nothing.

    Args:
        data: A dict, a Pydantic model, or ``None``.

    Returns:
        dict[str, Any]: The payload as a dict; empty when nothing was given.
    """
    if data is None:
        return {}
    if isinstance(data, dict):
        return data
    dump = getattr(data, "model_dump", None)
    if callable(dump):
        return cast("dict[str, Any]", dump())
    return {key: value for key, value in vars(data).items() if not key.startswith("_")}

_pref_int

_pref_int(adapter: Any, ext: Any, pref: int) -> int | None

Read one IModelDocExtension integer user preference.

Parameters:

Name Type Description Default
adapter Any

A connected adapter (for _attempt).

required
ext Any

The document's IModelDocExtension dispatch.

required
pref int

A swUserPreferenceIntegerValue_e value.

required

Returns:

Type Description
int | None

int | None: The preference value, or None when the COM call

int | None

fails or returns a non-numeric result.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _pref_int(adapter: Any, ext: Any, pref: int) -> int | None:
    """Read one ``IModelDocExtension`` integer user preference.

    Args:
        adapter: A connected adapter (for ``_attempt``).
        ext: The document's ``IModelDocExtension`` dispatch.
        pref: A ``swUserPreferenceIntegerValue_e`` value.

    Returns:
        int | None: The preference value, or ``None`` when the COM call
        fails or returns a non-numeric result.
    """
    value = adapter._attempt(
        lambda: ext.GetUserPreferenceInteger(pref, _SW_PREF_OPTION_NONE),
        default=None,
    )
    return int(value) if isinstance(value, (int, float)) else None

_resolve_drawing_view

_resolve_drawing_view(adapter: Any, drawing: Any, view_name: str) -> Any

Return the IView on the active drawing whose name matches.

Walks IDrawingDoc::GetViews (a list of sheets, each shaped (sheet, view, view, ...)) the same way :func:_view_names does. The match is case-insensitive.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
drawing Any

The drawing document, flagged for IDrawingDoc.

required
view_name str

The drawing-view name to look for.

required

Returns:

Name Type Description
Any Any

The flagged IView dispatch, or None when nothing matches.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _resolve_drawing_view(adapter: Any, drawing: Any, view_name: str) -> Any:
    """Return the ``IView`` on the active drawing whose name matches.

    Walks ``IDrawingDoc::GetViews`` (a list of sheets, each shaped
    ``(sheet, view, view, ...)``) the same way :func:`_view_names` does. The
    match is case-insensitive.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        drawing: The drawing document, flagged for ``IDrawingDoc``.
        view_name: The drawing-view name to look for.

    Returns:
        Any: The flagged ``IView`` dispatch, or ``None`` when nothing matches.
    """
    target = str(view_name or "").strip().lower()
    if not target:
        return None
    sheets = adapter._attempt(lambda: drawing.GetViews(), default=None)
    if not isinstance(sheets, (list, tuple)):
        return None
    for sheet in sheets:
        views = sheet if isinstance(sheet, (list, tuple)) else [sheet]
        for index, view in enumerate(views):
            if index == 0 and isinstance(sheet, (list, tuple)):
                continue
            wrapped = _as_com(adapter, view, "IView")
            if wrapped is None:
                continue
            name = adapter._attempt(lambda w=wrapped: w.GetName2(), default=None)
            if name and str(name).lower() == target:
                return wrapped
    return None

_variant_array

_variant_array(element_vt: int, values: Any) -> Any

Wrap a Python sequence as a SAFEARRAY VARIANT for a SolidWorks call.

pywin32 late binding otherwise unpacks a bare list into positional arguments, so SolidWorks array parameters (CreateSaveBodyFeature's bodies/paths, sketch entity lists, ...) need a single VT_ARRAY VARIANT.

Parameters:

Name Type Description Default
element_vt int

The element type flag, e.g. pythoncom.VT_DISPATCH or pythoncom.VT_BSTR.

required
values Any

The sequence of elements.

required

Returns:

Name Type Description
Any Any

VARIANT(VT_ARRAY | element_vt, list(values)), or the plain

Any

list when pywin32 is unavailable (test/mock environments).

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _variant_array(element_vt: int, values: Any) -> Any:
    """Wrap a Python sequence as a SAFEARRAY VARIANT for a SolidWorks call.

    pywin32 late binding otherwise unpacks a bare list into positional
    arguments, so SolidWorks array parameters (``CreateSaveBodyFeature``'s
    bodies/paths, sketch entity lists, ...) need a single ``VT_ARRAY``
    VARIANT.

    Args:
        element_vt: The element type flag, e.g. ``pythoncom.VT_DISPATCH`` or
            ``pythoncom.VT_BSTR``.
        values: The sequence of elements.

    Returns:
        Any: ``VARIANT(VT_ARRAY | element_vt, list(values))``, or the plain
        list when pywin32 is unavailable (test/mock environments).
    """
    variant_ctor = getattr(getattr(win32com, "client", None), "VARIANT", None)
    array_flag = getattr(pythoncom, "VT_ARRAY", 0)
    if not callable(variant_ctor) or not array_flag:
        return list(values)
    return variant_ctor(int(array_flag) | int(element_vt), list(values))

_view_names

_view_names(adapter: Any, drawing: Any) -> list[str]

Return the drawing's view names, excluding sheet formats.

CreateDrawViewFromModelView3 returns None for a model SolidWorks could not resolve, and Create3rdAngleViews2 returns a bare boolean, so the view list is the ground truth for whether views were really added.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
drawing Any

The drawing document, flagged for IDrawingDoc.

required

Returns:

Type Description
list[str]

list[str]: View names in sheet order.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _view_names(adapter: Any, drawing: Any) -> list[str]:
    """Return the drawing's view names, excluding sheet formats.

    ``CreateDrawViewFromModelView3`` returns ``None`` for a model SolidWorks
    could not resolve, and ``Create3rdAngleViews2`` returns a bare boolean, so
    the view list is the ground truth for whether views were really added.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        drawing: The drawing document, flagged for ``IDrawingDoc``.

    Returns:
        list[str]: View names in sheet order.
    """
    sheets = adapter._attempt(lambda: drawing.GetViews(), default=None)
    if not isinstance(sheets, (list, tuple)):
        return []

    names: list[str] = []
    for sheet in sheets:
        views = sheet if isinstance(sheet, (list, tuple)) else [sheet]
        for index, view in enumerate(views):
            # GetViews returns (sheet, view, view, ...) per sheet; entry 0 is
            # the sheet itself, not a drawing view.
            if index == 0 and isinstance(sheet, (list, tuple)):
                continue
            wrapped = _as_com(adapter, view, "IView")
            if wrapped is None:
                continue
            name = adapter._attempt(lambda w=wrapped: w.GetName2(), default=None)
            if name:
                names.append(str(name))
    return names