Skip to content

solidworks_mcp.adapters.solidworks.io

solidworks_mcp.adapters.solidworks.io

Model I/O mixin for PyWin32 SolidWorks operations.

Attributes

_DIMENSION_ANGULAR module-attribute

_DIMENSION_ANGULAR = 1

_DIMENSION_LINEAR module-attribute

_DIMENSION_LINEAR = 0

_DOC_TYPE_NAMES module-attribute

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

_EXTREME_DIRECTIONS module-attribute

_EXTREME_DIRECTIONS = ((0, False, (-1.0, 0.0, 0.0)), (0, True, (1.0, 0.0, 0.0)), (1, False, (0.0, -1.0, 0.0)), (1, True, (0.0, 1.0, 0.0)), (2, False, (0.0, 0.0, -1.0)), (2, True, (0.0, 0.0, 1.0)))

_FEATURE_WALK_MEMBERS module-attribute

_FEATURE_WALK_MEMBERS = ('GetTypeName2', 'GetNextFeature', 'GetFirstSubFeature', 'GetNextSubFeature', 'IsSuppressed')

_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

_SAME_TITLE_ALREADY_OPEN module-attribute

_SAME_TITLE_ALREADY_OPEN = 65536

_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_DEFAULT_TEMPLATES module-attribute

_SW_DEFAULT_TEMPLATES = {'part': 8, 'assembly': 9, 'drawing': 10}

_SW_DWG_PAPERS_USER_DEFINED module-attribute

_SW_DWG_PAPERS_USER_DEFINED = 12

_SW_DWG_PAPER_A_SIZE module-attribute

_SW_DWG_PAPER_A_SIZE = 0

_SW_DWG_TEMPLATE_NONE module-attribute

_SW_DWG_TEMPLATE_NONE = 13

_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_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

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)

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

        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, face_a: list[float] | None = None, face_b: list[float] | None = None) -> AdapterResult[dict[str, Any]]

Mate two components together.

Wraps IAssemblyDoc::AddMate5. A named entity (a reference plane or axis) is selected via IComponent2::FeatureByName + IFeature::Select2. A face is given as a point on it in the component's own part coordinates: the face is found among that component's faces only (IComponent2::GetBodies2, which report part coordinates; measured on SW 2026) and selected with IEntity::Select4. Searching one component means a pin and the hole it sits in, whose surfaces share points, can't be confused.

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
face_a list[float] | None

[x, y, z] mm on a face of the first component, in its part coordinates; used instead of entity_a.

None
face_b list[float] | None

The same for the second component.

None

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,
    face_a: list[float] | None = None,
    face_b: list[float] | None = None,
) -> AdapterResult[dict[str, Any]]:
    """Mate two components together.

    Wraps ``IAssemblyDoc::AddMate5``. A named entity (a reference plane or
    axis) is selected via ``IComponent2::FeatureByName`` +
    ``IFeature::Select2``. A face is given as a point on it in the
    component's own part coordinates: the face is found among *that
    component's* faces only (``IComponent2::GetBodies2``, which report
    part coordinates; measured on SW 2026) and selected with
    ``IEntity::Select4``. Searching one component means a pin and the
    hole it sits in, whose surfaces share points, can't be confused.

    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.
        face_a (list[float] | None): ``[x, y, z]`` mm on a face of the
            first component, in its part coordinates; used instead of
            ``entity_a``.
        face_b (list[float] | None): The same for the second component.

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

        wanted = {component_a: entity_a, component_b: entity_b}
        faces = {component_a: face_a, component_b: face_b}
        found = _find_components(adapter, assembly, [component_a, component_b])

        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]
            point = faces[component_name]
            if point is not None:
                _select_component_face(
                    adapter, model, wrapped, component_name, point, index > 0
                )
                continue
            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": [
                mate_entity_label(entity_a, face_a),
                mate_entity_label(entity_b, face_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)
    # After a close, SolidWorks activates another open document; close that
    # one, as the tool promises, rather than the document already closed.
    active = getattr(adapter.swApp, "ActiveDoc", None) if adapter.swApp else None
    if active is not None:
        adapter.currentModel = active
    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, template: 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
template str | None

Optional .asmdot path. When given the assembly is made from it with NewDocument; otherwise NewAssembly and then the user-preference default templates are tried.

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, template: str | None = None
) -> AdapterResult[SolidWorksModel]:
    """Create a new assembly document and set it as active.

    Args:
        name: Reserved for future naming policy.
        template: Optional ``.asmdot`` path. When given the assembly is
            made from it with ``NewDocument``; otherwise ``NewAssembly``
            and then the user-preference default templates are tried.

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

        explicit = self._explicit_template_path(template, ".asmdot")
        if explicit:
            model = app.NewDocument(explicit, 0, 0, 0)
        else:
            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; "
                        "pass template with the path of a .asmdot file"
                    )
                model = app.NewDocument(asm_template, 0, 0, 0)

        if not model:
            if explicit:
                raise Exception(f"Failed to create new assembly from {explicit!r}")
            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, template: 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
template str | None

Optional .drwdot path, used in place of the user-preference default. An explicit template that fails is an error; it does not fall back to a blank drawing.

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, template: str | None = None
) -> AdapterResult[SolidWorksModel]:
    """Create a new drawing document and set it as active.

    Args:
        name: Reserved for future naming policy.
        template: Optional ``.drwdot`` path, used in place of the
            user-preference default. An explicit template that fails is
            an error; it does not fall back to a blank drawing.

    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.
        explicit = self._explicit_template_path(template, ".drwdot")
        drw_template = explicit or self._resolve_template_path(
            [10, 1, 0, 2, 3], ".drwdot"
        )
        model = None
        if drw_template:
            model = app.NewDocument(
                drw_template, _SW_DWG_PAPERS_USER_DEFINED, 0.2794, 0.2159
            )
        if not model and explicit:
            raise Exception(f"Failed to create a drawing from {explicit!r}")
        if not model:
            # No usable template (a settings reset blanks the slot, or it
            # names a deleted file). Like create_part's NewPart fallback,
            # let SolidWorks make a blank A-size drawing with no sheet
            # format; views can still be placed on it.
            model = adapter._attempt(
                lambda: app.NewDrawing2(
                    _SW_DWG_TEMPLATE_NONE, "", _SW_DWG_PAPER_A_SIZE, 0.2794, 0.2159
                )
            )
        if not model:
            if drw_template:
                tried = f"template {drw_template!r}"
            else:
                tried = "no drawing template is configured"
            raise Exception(
                f"Failed to create a drawing: {tried}, and a blank drawing "
                "without a template (NewDrawing2) also failed"
            )

        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, template: 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
template str | None

Optional .prtdot path. When given the part is made from it with NewDocument; otherwise NewPart and then the user-preference default templates are tried.

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,
    template: 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.
        template: Optional ``.prtdot`` path. When given the part is made
            from it with ``NewDocument``; otherwise ``NewPart`` and then
            the user-preference default templates are tried.

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

        explicit = self._explicit_template_path(template, ".prtdot")
        if explicit:
            model = app.NewDocument(explicit, 0, 0, 0)
        else:
            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; "
                        "pass template with the path of a .prtdot file"
                    )
                model = app.NewDocument(part_template, 0, 0, 0)

        if not model:
            if explicit:
                raise Exception(f"Failed to create new part from {explicit!r}")
            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_application_info async
get_application_info() -> AdapterResult[dict[str, Any]]

Read SolidWorks' revision, language and default templates.

Each value is read on its own, so one unreadable value is None and the rest still come back. Templates are reported by file name only, so no path under the user's profile leaves the adapter.

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: See

AdapterResult[dict[str, Any]]

SolidWorksAdapter.get_application_info.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def get_application_info(self) -> AdapterResult[dict[str, Any]]:
    """Read SolidWorks' revision, language and default templates.

    Each value is read on its own, so one unreadable value is ``None``
    and the rest still come back. Templates are reported by file name
    only, so no path under the user's profile leaves the adapter.

    Returns:
        AdapterResult[dict[str, Any]]: See
        ``SolidWorksAdapter.get_application_info``.
    """
    adapter = self._adapter(self)
    app = adapter.swApp
    if app is None:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
        )

    def _text(read: Any) -> str | None:
        value = adapter._attempt(read, default=None)
        return str(value) if value else None

    def _template(preference: int) -> dict[str, Any]:
        path = _text(lambda: app.GetUserPreferenceStringValue(preference))
        return {
            "file_name": os.path.basename(path) if path else None,
            "exists": bool(path) and os.path.isfile(str(path)),
        }

    def _read() -> dict[str, Any]:
        return {
            "revision": _text(
                lambda: adapter._get_attr_or_call(app, "RevisionNumber")
            ),
            "language": _text(lambda: app.GetCurrentLanguage()),
            "templates": {
                kind: _template(preference)
                for kind, preference in _SW_DEFAULT_TEMPLATES.items()
            },
        }

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("get_application_info", _read),
    )
get_dimension async
get_dimension(name: str) -> AdapterResult[dict[str, Any]]

Read a named model dimension in the unit its type gives it.

IDimension.SystemValue is metres for a length, radians for an angle and the number itself for a count; IDimension.GetType says which (:func:_dimension_reading). GetValue3 is not used: it is in the document's units, which this can't convert without reading the document's unit system.

Parameters:

Name Type Description Default
name str

Fully-qualified dimension name, e.g. "D1@Boss-Extrude1".

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: value and units ("mm",

AdapterResult[dict[str, Any]]

"deg", or None for a count or a dimension of unknown type).

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def get_dimension(self, name: str) -> AdapterResult[dict[str, Any]]:
    """Read a named model dimension in the unit its type gives it.

    ``IDimension.SystemValue`` is metres for a length, radians for an
    angle and the number itself for a count; ``IDimension.GetType`` says
    which (:func:`_dimension_reading`). ``GetValue3`` is not used: it is
    in the document's units, which this can't convert without reading
    the document's unit system.

    Args:
        name: Fully-qualified dimension name, e.g. ``"D1@Boss-Extrude1"``.

    Returns:
        AdapterResult[dict[str, Any]]: ``value`` and ``units`` ("mm",
        "deg", or ``None`` for a count or a dimension of unknown type).
    """
    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _get() -> dict[str, Any]:
        """Get the dimension value and its unit."""
        dimension = adapter.currentModel.Parameter(name)
        if not dimension:
            raise Exception(
                f"Dimension '{name}' not found; list the names with "
                "list_model_dimensions"
            )
        dimension = _sw_type_info.flagged(dimension, "IDimension")
        value = adapter._attempt(lambda: dimension.SystemValue, default=None)
        if value is None:
            raise Exception(f"Failed to read dimension '{name}'")
        kind = adapter._attempt(lambda: dimension.GetType(), default=None)
        return _dimension_reading(kind, float(value))

    return cast(
        AdapterResult[dict[str, Any]],
        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]

            # GetMassProperties gives Lxx, Lyy, Lzz, Lxy, Lzx, Lyz at
            # 6-11 (API docs); moi is the row-major 3 x 3 tensor that
            # IMassProperty.GetMomentOfInertia returns.
            moi = [0.0] * 9
            if len(raw) >= 12:
                moi[0] = raw[6]
                moi[4] = raw[7]
                moi[8] = raw[8]
                moi[1] = raw[9]  # Lxy
                moi[2] = raw[10]  # Lzx = Ixz
                moi[5] = raw[11]  # Lyz

        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_geometry_summary async
get_model_geometry_summary() -> AdapterResult[dict[str, Any]]

Summarise the active part or assembly without rebuilding it.

Unlike get_mass_properties this never calls ForceRebuild3, so it leaves the live selection intact (COM pitfall #23) and can be read right before a feature call. A value that cannot be read is None.

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: See

AdapterResult[dict[str, Any]]

SolidWorksAdapter.get_model_geometry_summary.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def get_model_geometry_summary(self) -> AdapterResult[dict[str, Any]]:
    """Summarise the active part or assembly without rebuilding it.

    Unlike ``get_mass_properties`` this never calls ``ForceRebuild3``, so
    it leaves the live selection intact (COM pitfall #23) and can be read
    right before a feature call. A value that cannot be read is ``None``.

    Returns:
        AdapterResult[dict[str, Any]]: See
        ``SolidWorksAdapter.get_model_geometry_summary``.
    """
    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 not in (1, 2):
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                "get_model_geometry_summary needs an active part or assembly "
                f"(active document type is {doc_type!r})"
            ),
        )

    def _summarise() -> dict[str, Any]:
        model = adapter.currentModel
        summary: dict[str, Any] = {}
        if doc_type == 1:
            part = _sw_type_info.flagged(model, "IPartDoc")
            bodies = _solid_bodies(adapter, part)
            volume = _bodies_volume_mm3(adapter, bodies)
            summary["volume_mm3"] = (
                volume if volume is not None else _model_volume_mm3(adapter, model)
            )
            box = _extreme_point_box(adapter, bodies or [])
            if box is None:
                box = _box_from_metres(
                    adapter._attempt(lambda: part.GetPartBox(True), default=None),
                    "part_box",
                )
            summary["document_type"] = "part"
            summary["solid_body_count"] = None if bodies is None else len(bodies)
        else:
            assembly = _sw_type_info.flagged(model, "IAssemblyDoc")
            summary["volume_mm3"] = _model_volume_mm3(adapter, model)
            box = _box_from_metres(
                adapter._attempt(lambda: assembly.GetBox(0), default=None),
                "assembly_box",
            )
            summary["document_type"] = "assembly"
            summary["solid_body_count"] = None
            summary["component_count"] = len(_component_names(adapter, assembly))
            summary["mate_count"] = _assembly_mate_count(adapter, model)
        summary["bounding_box_method"] = box.pop("method") if box else None
        summary["bounding_box_mm"] = box
        return summary

    return cast(
        AdapterResult[dict[str, Any]],
        adapter._handle_com_operation("get_model_geometry_summary", _summarise),
    )
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
        )
        # IConfiguration.Name is a property; it has no GetName method.
        active_name = adapter._attempt(
            lambda: adapter._get_attr_or_call(active_config, "Name"), 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

        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}")
        if not os.path.isfile(resolved_path):
            raise FileNotFoundError(f"File not found: {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
        # The configuration SolidWorks opened, or None: never an assumed
        # "Default". IConfiguration.Name is a property, not a method.
        active_config = adapter._attempt(lambda: model.GetActiveConfiguration())
        config = (
            adapter._attempt(
                lambda: adapter._get_attr_or_call(active_config, "Name"),
                default=None,
            )
            if active_config
            else None
        )

        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 every file it uses into one folder.

SolidWorks lists the files (IModelDoc2.GetDependencies2) and each is copied, as saved on disk, into target_dir under its own name. The copy opens with its own parts: SolidWorks looks for a referenced file in the assembly's folder before the stored path. (SolidWorks' own Pack and Go, IPackAndGo, listed only the assembly on SolidWorks 2026.) No document is closed or changed.

Parameters:

Name Type Description Default
source_path str

The .sldasm file.

required
target_dir str

The folder to copy into; created if needed. A file already there with a copied file's name is replaced.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict]: source_assembly, target_dir,

AdapterResult[dict[str, Any]]

copied_files, source_files and unsaved_changes (the

AdapterResult[dict[str, Any]]

files open with changes the copy doesn't have).

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def pack_and_go_assembly(
    self,
    source_path: str,
    target_dir: str,
) -> AdapterResult[dict[str, Any]]:
    """Copy an assembly and every file it uses into one folder.

    SolidWorks lists the files (``IModelDoc2.GetDependencies2``) and each
    is copied, as saved on disk, into ``target_dir`` under its own name.
    The copy opens with its own parts: SolidWorks looks for a referenced
    file in the assembly's folder before the stored path. (SolidWorks'
    own Pack and Go, ``IPackAndGo``, listed only the assembly on
    SolidWorks 2026.) No document is closed or changed.

    Args:
        source_path: The ``.sldasm`` file.
        target_dir: The folder to copy into; created if needed. A file
            already there with a copied file's name is replaced.

    Returns:
        AdapterResult[dict]: ``source_assembly``, ``target_dir``,
        ``copied_files``, ``source_files`` and ``unsaved_changes`` (the
        files open with changes the copy doesn't have).
    """
    adapter = self._adapter(self)
    source = Path(source_path)
    out_dir = Path(target_dir)

    def _pack() -> dict[str, Any]:
        model = _open_or_find_assembly(adapter, source)
        _sw_type_info.flag_doc(model, 2)
        adapter.currentModel = model
        listed = dependency_paths(model.GetDependencies2(True, True, False))
        files = _files_to_pack(source, listed, out_dir)
        out_dir.mkdir(parents=True, exist_ok=True)
        copied = [str(shutil.copy2(f, out_dir / f.name)) for f in files]
        unsaved = [
            f.name
            for f in files
            if adapter._attempt(
                lambda f=f: bool(
                    adapter._get_attr_or_call(
                        adapter.swApp.GetOpenDocumentByName(str(f)),
                        "GetSaveFlag",
                    )
                ),
                default=False,
            )
        ]
        return {
            "source_assembly": str(source),
            "target_dir": str(out_dir),
            "copied_files": sorted(copied),
            "source_files": [str(f) for f in files],
            "unsaved_changes": unsaved,
        }

    result = adapter._handle_com_operation("pack_and_go_assembly", _pack)
    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(top_level_only: bool = False) -> AdapterResult[dict[str, Any]]

Force a rebuild and report exactly which features fail and why.

Parameters:

Name Type Description Default
top_level_only bool

Rebuild only the top level of an assembly. Defaults to False (full rebuild).

False

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: rebuilt, top_level_only,

AdapterResult[dict[str, Any]]

errors (see rebuild_diagnostics.explain), error_count,

AdapterResult[dict[str, Any]]

warning_count and a one-line summary. ERROR - still

AdapterResult[dict[str, Any]]

carrying that data - when any error-severity entry remains or

AdapterResult[dict[str, Any]]

SolidWorks reports the rebuild itself failed.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def rebuild_model(
    self, top_level_only: bool = False
) -> AdapterResult[dict[str, Any]]:
    """Force a rebuild and report exactly which features fail and why.

    Args:
        top_level_only: Rebuild only the top level of an assembly.
            Defaults to False (full rebuild).

    Returns:
        AdapterResult[dict[str, Any]]: ``rebuilt``, ``top_level_only``,
        ``errors`` (see ``rebuild_diagnostics.explain``), ``error_count``,
        ``warning_count`` and a one-line ``summary``. ERROR - still
        carrying that ``data`` - when any error-severity entry remains or
        SolidWorks reports the rebuild itself failed.
    """
    from .com_utils import strip_op_prefix
    from .rebuild_diagnostics import explain, summarize
    from .rebuild_inspector import collect_rebuild_issues

    adapter = self._adapter(self)
    if not adapter.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _rebuild() -> dict[str, Any]:
        model = adapter.currentModel
        # ForceRebuild3 returns False whenever a feature fails to rebuild
        # (confirmed live 2026-10-13), so False is not "the rebuild call
        # broke" - collect the per-feature issues either way and only fall
        # back to a generic failure when SolidWorks names none.
        rebuilt = bool(model.ForceRebuild3(top_level_only))
        entries = [explain(i) for i in collect_rebuild_issues(adapter, model)]
        error_count, warning_count, summary = summarize(entries)
        if not rebuilt and not error_count:
            # Keep any warnings in data; only the cause is unnamed.
            summary = (
                "SolidWorks reported that the rebuild failed (ForceRebuild3 "
                "returned False) but listed no failing feature."
                + (f" Warnings: {summary}" if warning_count else "")
            )
        return {
            "rebuilt": rebuilt,
            "top_level_only": top_level_only,
            "errors": entries,
            "error_count": error_count,
            "warning_count": warning_count,
            "summary": summary,
        }

    result = cast(
        "AdapterResult[dict[str, Any]]",
        adapter._handle_com_operation("rebuild_model", _rebuild),
    )
    if not result.is_success:
        result.error = strip_op_prefix(result.error, "rebuild_model")
    elif result.data and (result.data["error_count"] or not result.data["rebuilt"]):
        result.status = AdapterResultStatus.ERROR
        result.error = result.data["summary"]
    return result
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_component_state async
set_component_state(component: str, fixed: bool | None = None, flexible: bool | None = None) -> AdapterResult[dict[str, Any]]

Fix or float a component, and make a sub-assembly rigid or flexible.

Fixing selects the component and calls IAssemblyDoc::FixComponent or UnfixComponent. Solving goes through IAssemblyDoc::CompConfigProperties5, which also sets suppression, visibility, configuration, BOM and envelope for the selected component: each is read from the component first and passed back, so only solving changes.

Parameters:

Name Type Description Default
component str

Instance name, as list_components reports it.

required
fixed bool | None

True fixes it, False floats it.

None
flexible bool | None

True flexible, False rigid (sub-assemblies).

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The state before and after.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
async def set_component_state(
    self,
    component: str,
    fixed: bool | None = None,
    flexible: bool | None = None,
) -> AdapterResult[dict[str, Any]]:
    """Fix or float a component, and make a sub-assembly rigid or flexible.

    Fixing selects the component and calls ``IAssemblyDoc::FixComponent``
    or ``UnfixComponent``. Solving goes through
    ``IAssemblyDoc::CompConfigProperties5``, which also sets suppression,
    visibility, configuration, BOM and envelope for the selected
    component: each is read from the component first and passed back, so
    only solving changes.

    Args:
        component (str): Instance name, as ``list_components`` reports it.
        fixed (bool | None): True fixes it, False floats it.
        flexible (bool | None): True flexible, False rigid (sub-assemblies).

    Returns:
        AdapterResult[dict[str, Any]]: The state before and after.
    """
    adapter = self._adapter(self)
    if _doc_type(adapter) != 2:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="set_component_state requires an assembly document",
        )
    if fixed is None and flexible is None:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="Nothing to change: give fixed, flexible or both.",
        )

    def _state() -> dict[str, Any]:
        model = adapter.currentModel
        assembly = _sw_type_info.flagged(model, "IAssemblyDoc")
        target = _find_components(adapter, assembly, [component])[component]
        before = _component_state(adapter, target)
        sel_mgr = _sw_type_info.flagged(model.SelectionManager, "ISelectionMgr")

        def select() -> None:
            adapter._attempt(lambda: model.ClearSelection2(True), default=None)
            data = adapter._attempt(
                lambda: sel_mgr.CreateSelectData(), default=None
            )
            if not adapter._attempt(
                lambda: target.Select4(False, data, False), default=False
            ):
                raise Exception(f"Failed to select {component}")

        if fixed is not None:
            select()
            call = assembly.FixComponent if fixed else assembly.UnfixComponent
            adapter._attempt(call, default=None)
        if flexible is not None:
            select()
            config = before["referenced_configuration"] or ""
            applied = adapter._attempt(
                lambda: assembly.CompConfigProperties5(
                    before["suppression"],
                    1 if flexible else 0,  # swComponentSolvingOption_e
                    bool(before["visible"]),
                    bool(config),
                    config,
                    bool(before["exclude_from_bom"]),
                    bool(before["envelope"]),
                ),
                default=False,
            )
            if not applied:
                raise Exception(
                    f"SolidWorks refused to make {component} "
                    f"{'flexible' if flexible else 'rigid'}; only a "
                    "sub-assembly can be flexible."
                )
        adapter._attempt(lambda: model.ClearSelection2(True), default=None)
        adapter._attempt(lambda: model.EditRebuild3(), default=None)
        after = _component_state(adapter, target)
        if fixed is not None and after["fixed"] is not fixed:
            raise Exception(
                f"{component} is still {'floating' if fixed else 'fixed'} "
                "after the change."
            )
        return {"component": component, "before": before, "after": after}

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

_assembly_mate_count

_assembly_mate_count(adapter: Any, model: Any) -> int | None

Find the MateGroup feature and count its mates.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
model Any

The active assembly IModelDoc2.

required

Returns:

Type Description
int | None

int | None: The mate count (0 when there is no mate group), or

int | None

None when the feature tree could not be read.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _assembly_mate_count(adapter: Any, model: Any) -> int | None:
    """Find the ``MateGroup`` feature and count its mates.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        model: The active assembly ``IModelDoc2``.

    Returns:
        int | None: The mate count (``0`` when there is no mate group), or
        ``None`` when the feature tree could not be read.
    """
    feature = adapter._attempt(lambda: model.FirstFeature(), default=None)
    if feature is None:
        return None
    guard = 0
    while feature is not None and guard < 10000:
        guard += 1
        adapter._attempt(
            lambda f=feature: _sw_type_info.flag_members(f, *_FEATURE_WALK_MEMBERS),
            default=0,
        )
        if adapter._attempt(lambda f=feature: f.GetTypeName2(), default="") == (
            "MateGroup"
        ):
            return _count_mates(adapter, feature)
        feature = adapter._attempt(lambda f=feature: f.GetNextFeature(), default=None)
    return 0

_bodies_volume_mm3

_bodies_volume_mm3(adapter: Any, bodies: list[Any] | None) -> float | None

Sum the solid bodies' volumes, independent of the selection.

IModelDocExtension.CreateMassProperty computes for the current selection and returns no volume while a sketch or plane is selected, which is the state a feature call runs in. IBody2.GetMassProperties reads each body directly; element 3 of its array is the volume in m³. The density argument only affects mass, which is not used here.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
bodies list[Any] | None

Wrapped IBody2 dispatches, [] for an empty part.

required

Returns:

Type Description
float | None

float | None: Volume in mm³ (0.0 for no bodies), or None when

float | None

the bodies or any body's volume could not be read.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _bodies_volume_mm3(adapter: Any, bodies: list[Any] | None) -> float | None:
    """Sum the solid bodies' volumes, independent of the selection.

    ``IModelDocExtension.CreateMassProperty`` computes for the current
    selection and returns no volume while a sketch or plane is selected,
    which is the state a feature call runs in. ``IBody2.GetMassProperties``
    reads each body directly; element 3 of its array is the volume in m³.
    The density argument only affects mass, which is not used here.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        bodies: Wrapped ``IBody2`` dispatches, ``[]`` for an empty part.

    Returns:
        float | None: Volume in mm³ (``0.0`` for no bodies), or ``None`` when
        the bodies or any body's volume could not be read.
    """
    if bodies is None:
        return None
    total = 0.0
    for body in bodies:
        props = adapter._attempt(lambda b=body: b.GetMassProperties(1.0), default=None)
        if not isinstance(props, (list, tuple)) or len(props) < 4:
            return None
        volume = props[3]
        if not isinstance(volume, (int, float)):
            return None
        total += float(volume)
    return round(total * 1e9, 4)

_box_from_metres

_box_from_metres(raw: Any, method: str) -> dict[str, Any] | None

Turn a SolidWorks [x1, y1, z1, x2, y2, z2] box in metres into mm.

Parameters:

Name Type Description Default
raw Any

The six-value box, or anything else.

required
method str

How the box was measured, reported back to the caller.

required

Returns:

Type Description
dict[str, Any] | None

dict[str, Any] | None: min, max, size (mm) and method,

dict[str, Any] | None

or None when raw is not a six-number box.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _box_from_metres(raw: Any, method: str) -> dict[str, Any] | None:
    """Turn a SolidWorks ``[x1, y1, z1, x2, y2, z2]`` box in metres into mm.

    Args:
        raw: The six-value box, or anything else.
        method: How the box was measured, reported back to the caller.

    Returns:
        dict[str, Any] | None: ``min``, ``max``, ``size`` (mm) and ``method``,
        or ``None`` when ``raw`` is not a six-number box.
    """
    if not isinstance(raw, (list, tuple)) or len(raw) < 6:
        return None
    if not all(isinstance(v, (int, float)) for v in raw[:6]):
        return None
    values = [float(v) * 1000.0 for v in raw[:6]]
    low = [min(values[i], values[i + 3]) for i in range(3)]
    high = [max(values[i], values[i + 3]) for i in range(3)]
    return {
        "min": [round(v, 4) for v in low],
        "max": [round(v, 4) for v in high],
        "size": [round(high[i] - low[i], 4) for i in range(3)],
        "method": method,
    }

_byref_bool

_byref_bool() -> Any

Return a byref bool VARIANT (e.g. IFeature::GetErrorCode2(IsWarning)).

Returns:

Name Type Description
Any Any

The byref VARIANT, or a _ByrefFallback(False) without pywin32.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _byref_bool() -> Any:
    """Return a byref bool VARIANT (e.g. ``IFeature::GetErrorCode2(IsWarning)``).

    Returns:
        Any: The byref VARIANT, or a ``_ByrefFallback(False)`` without pywin32.
    """
    variant_ctor = getattr(getattr(win32com, "client", None), "VARIANT", None)
    if not callable(variant_ctor):
        return _ByrefFallback(False)
    return variant_ctor(
        int(getattr(pythoncom, "VT_BYREF", 0)) | int(getattr(pythoncom, "VT_BOOL", 0)),
        False,
    )

_byref_bstr

_byref_bstr() -> Any

Return a byref string VARIANT for a SolidWorks String out-parameter.

Under forced late binding an omitted out-parameter raises "Parameter not optional", and pythoncom.Missing cannot be converted to a COM VARIANT, so pass VARIANT(VT_BYREF | VT_BSTR, "") and read .value afterwards (e.g. IPartDoc::GetMaterialPropertyName2's Database; confirmed live on R2026x).

Returns:

Name Type Description
Any Any

The byref VARIANT, or a _ByrefFallback("") without pywin32.

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

    Under forced late binding an omitted out-parameter raises "Parameter not
    optional", and ``pythoncom.Missing`` cannot be converted to a COM VARIANT,
    so pass ``VARIANT(VT_BYREF | VT_BSTR, "")`` and read ``.value`` afterwards
    (e.g. ``IPartDoc::GetMaterialPropertyName2``'s ``Database``; confirmed live
    on R2026x).

    Returns:
        Any: The byref VARIANT, or a ``_ByrefFallback("")`` without pywin32.
    """
    variant_ctor = getattr(getattr(win32com, "client", None), "VARIANT", None)
    if not callable(variant_ctor):
        return _ByrefFallback("")
    return variant_ctor(
        int(getattr(pythoncom, "VT_BYREF", 0)) | int(getattr(pythoncom, "VT_BSTR", 0)),
        "",
    )

_byref_double

_byref_double() -> Any

Return a byref double VARIANT for a SolidWorks Double out-parameter.

IBody2::GetExtremePoint reports its point through three of these. See :func:_byref_bstr for why pythoncom.Missing does not work.

Returns:

Name Type Description
Any Any

A VARIANT(VT_BYREF | VT_R8, 0.0), or a _ByrefFallback(0.0)

Any

without pywin32.

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

    ``IBody2::GetExtremePoint`` reports its point through three of these. See
    :func:`_byref_bstr` for why ``pythoncom.Missing`` does not work.

    Returns:
        Any: A ``VARIANT(VT_BYREF | VT_R8, 0.0)``, or a ``_ByrefFallback(0.0)``
        without pywin32.
    """
    variant_ctor = getattr(getattr(win32com, "client", None), "VARIANT", None)
    if not callable(variant_ctor):
        return _ByrefFallback(0.0)
    return variant_ctor(
        int(getattr(pythoncom, "VT_BYREF", 0)) | int(getattr(pythoncom, "VT_R8", 0)),
        0.0,
    )

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

_byref_variant

_byref_variant() -> Any

Return a byref VARIANT for an array/variant out-parameter.

IModelDocExtension::GetWhatsWrong fills three of these (features, error codes, warning flags); confirmed live on R2026x that late binding accepts VARIANT(VT_BYREF | VT_VARIANT, None) and exposes the result on .value. pythoncom.Missing is rejected ("can not be converted to a COM VARIANT").

Returns:

Name Type Description
Any Any

The byref VARIANT, or a _ByrefFallback(None) without pywin32.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _byref_variant() -> Any:
    """Return a byref VARIANT for an array/variant out-parameter.

    ``IModelDocExtension::GetWhatsWrong`` fills three of these (features,
    error codes, warning flags); confirmed live on R2026x that late binding
    accepts ``VARIANT(VT_BYREF | VT_VARIANT, None)`` and exposes the result on
    ``.value``. ``pythoncom.Missing`` is rejected ("can not be converted to a
    COM VARIANT").

    Returns:
        Any: The byref VARIANT, or a ``_ByrefFallback(None)`` without pywin32.
    """
    variant_ctor = getattr(getattr(win32com, "client", None), "VARIANT", None)
    if not callable(variant_ctor):
        return _ByrefFallback(None)
    return variant_ctor(
        int(getattr(pythoncom, "VT_BYREF", 0))
        | int(getattr(pythoncom, "VT_VARIANT", 0)),
        None,
    )

_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_state

_component_state(adapter: Any, component: Any) -> dict[str, Any]

Read what CompConfigProperties5 sets, plus whether it is fixed.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _component_state(adapter: Any, component: Any) -> dict[str, Any]:
    """Read what ``CompConfigProperties5`` sets, plus whether it is fixed."""
    visible = adapter._attempt(lambda: component.Visible, default=None)
    return {
        "fixed": adapter._attempt(lambda: bool(component.IsFixed()), default=None),
        "flexible": adapter._attempt(lambda: component.Solving == 1, default=None),
        "suppression": adapter._attempt(
            lambda: component.GetSuppression2(), default=None
        ),
        "visible": None if visible is None else visible == 1,
        "referenced_configuration": adapter._attempt(
            lambda: component.ReferencedConfiguration, default=None
        ),
        "exclude_from_bom": adapter._attempt(
            lambda: bool(component.ExcludeFromBOM), default=None
        ),
        "envelope": adapter._attempt(
            lambda: bool(component.IsEnvelope()), default=None
        ),
    }

_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

_count_mates

_count_mates(adapter: Any, mate_group: Any) -> int

Count the unsuppressed mates under the assembly's mate group.

Mates are sub-features whose type name starts with Mate; a mate folder (any ...Folder type) is entered one level deep.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
mate_group Any

The MateGroup feature.

required

Returns:

Name Type Description
int int

The number of unsuppressed mates.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _count_mates(adapter: Any, mate_group: Any) -> int:
    """Count the unsuppressed mates under the assembly's mate group.

    Mates are sub-features whose type name starts with ``Mate``; a mate
    folder (any ``...Folder`` type) is entered one level deep.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        mate_group: The ``MateGroup`` feature.

    Returns:
        int: The number of unsuppressed mates.
    """
    count = 0
    for child in _sub_features(adapter, mate_group):
        kind = str(adapter._attempt(lambda c=child: c.GetTypeName2(), default="") or "")
        members = _sub_features(adapter, child) if kind.endswith("Folder") else [child]
        for mate in members:
            mate_kind = str(
                adapter._attempt(lambda m=mate: m.GetTypeName2(), default="") or ""
            )
            suppressed = adapter._attempt(
                lambda m=mate: m.IsSuppressed(), default=False
            )
            if mate_kind.startswith("Mate") and not suppressed:
                count += 1
    return count

_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,
    }

_dimension_reading

_dimension_reading(kind: Any, system_value: float) -> dict[str, Any]

A dimension's value in the unit its type gives it.

Parameters:

Name Type Description Default
kind Any

IDimension.GetType's value, or None when unreadable.

required
system_value float

IDimension.SystemValue: metres, radians or a count.

required

Returns:

Type Description
dict[str, Any]

dict[str, Any]: value and units: mm for a length, degrees for

dict[str, Any]

an angle, and the number with units: None for anything else.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _dimension_reading(kind: Any, system_value: float) -> dict[str, Any]:
    """A dimension's value in the unit its type gives it.

    Args:
        kind: ``IDimension.GetType``'s value, or ``None`` when unreadable.
        system_value: ``IDimension.SystemValue``: metres, radians or a count.

    Returns:
        dict[str, Any]: ``value`` and ``units``: mm for a length, degrees for
        an angle, and the number with ``units: None`` for anything else.
    """
    if kind == _DIMENSION_LINEAR:
        return {"value": system_value * 1000.0, "units": "mm"}
    if kind == _DIMENSION_ANGULAR:
        return {"value": math.degrees(system_value), "units": "deg"}
    return {"value": system_value, "units": None}

_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

_extreme_point_box

_extreme_point_box(adapter: Any, bodies: list[Any]) -> dict[str, Any] | None

Tight bounding box from each body's six extreme points.

GetPartBox can be loose on curved faces; GetExtremePoint returns the actual farthest point of the body in a direction.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
bodies list[Any]

Wrapped IBody2 dispatches.

required

Returns:

Type Description
dict[str, Any] | None

dict[str, Any] | None: The box, or None when there are no bodies

dict[str, Any] | None

or any extreme point could not be read.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _extreme_point_box(adapter: Any, bodies: list[Any]) -> dict[str, Any] | None:
    """Tight bounding box from each body's six extreme points.

    ``GetPartBox`` can be loose on curved faces; ``GetExtremePoint`` returns
    the actual farthest point of the body in a direction.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        bodies: Wrapped ``IBody2`` dispatches.

    Returns:
        dict[str, Any] | None: The box, or ``None`` when there are no bodies
        or any extreme point could not be read.
    """
    if not bodies:
        return None
    low = [float("inf")] * 3
    high = [float("-inf")] * 3
    for body in bodies:
        for axis, is_max, direction in _EXTREME_DIRECTIONS:
            out = (_byref_double(), _byref_double(), _byref_double())
            found = adapter._attempt(
                lambda b=body, d=direction, o=out: b.GetExtremePoint(*d, *o),
                default=None,
            )
            value = out[axis].value
            if not found or not isinstance(value, (int, float)):
                return None
            if is_max:
                high[axis] = max(high[axis], float(value))
            else:
                low[axis] = min(low[axis], float(value))
    return _box_from_metres([*low, *high], "extreme_points")

_files_to_pack

_files_to_pack(source: Path, dependencies: list[Path], out_dir: Path) -> list[Path]

The assembly and each file it uses, checked before anything is copied.

Parameters:

Name Type Description Default
source Path

The assembly.

required
dependencies list[Path]

The files GetDependencies2 listed.

required
out_dir Path

The folder they are copied into, under their own names.

required

Returns:

Type Description
list[Path]

list[Path]: The assembly first, then each dependency once.

Raises:

Type Description
FileNotFoundError

A file is not on disk.

ValueError

Two files share a name (renaming one would break the reference to it), or out_dir is a folder they are in.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _files_to_pack(source: Path, dependencies: list[Path], out_dir: Path) -> list[Path]:
    """The assembly and each file it uses, checked before anything is copied.

    Args:
        source: The assembly.
        dependencies: The files ``GetDependencies2`` listed.
        out_dir: The folder they are copied into, under their own names.

    Returns:
        list[Path]: The assembly first, then each dependency once.

    Raises:
        FileNotFoundError: A file is not on disk.
        ValueError: Two files share a name (renaming one would break the
            reference to it), or ``out_dir`` is a folder they are in.
    """
    files = [source]
    keys = {os.path.normcase(str(source))}
    for path in dependencies:
        if os.path.normcase(str(path)) not in keys:
            keys.add(os.path.normcase(str(path)))
            files.append(path)
    missing = [str(f) for f in files if not f.is_file()]
    if missing:
        raise FileNotFoundError(
            f"Files the assembly uses were not found on disk: {', '.join(missing)}. "
            "Nothing was copied."
        )
    by_name: dict[str, list[str]] = {}
    for f in files:
        by_name.setdefault(f.name.casefold(), []).append(str(f))
    clashes = [path for group in by_name.values() if len(group) > 1 for path in group]
    if clashes:
        raise ValueError(
            f"These files share a name and can't go in one folder: {', '.join(clashes)}. "
            "Nothing was copied."
        )
    target = os.path.normcase(str(out_dir.resolve()))
    if any(os.path.normcase(str(f.parent.resolve())) == target for f in files):
        raise ValueError(
            "target_dir holds the files being packed; choose another folder. "
            "Nothing was copied."
        )
    return files

_find_components

_find_components(adapter: Any, assembly: Any, names: list[str]) -> dict[str, Any]

The named top-level components of assembly, as IComponent2.

Parameters:

Name Type Description Default
adapter Any

The connected adapter.

required
assembly Any

The IAssemblyDoc dispatch.

required
names list[str]

Instance names, e.g. ["bracket-1", "pin-1"].

required

Returns:

Type Description
dict[str, Any]

dict[str, Any]: Name to wrapped component, one per name.

Raises:

Type Description
Exception

When the components can't be read, or a name isn't there (the message lists what the assembly holds).

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _find_components(adapter: Any, assembly: Any, names: list[str]) -> dict[str, Any]:
    """The named top-level components of ``assembly``, as ``IComponent2``.

    Args:
        adapter: The connected adapter.
        assembly: The ``IAssemblyDoc`` dispatch.
        names: Instance names, e.g. ``["bracket-1", "pin-1"]``.

    Returns:
        dict[str, Any]: Name to wrapped component, one per name.

    Raises:
        Exception: When the components can't be read, or a name isn't there
            (the message lists what the assembly holds).
    """
    components = adapter._attempt(lambda: assembly.GetComponents(True), default=None)
    if not isinstance(components, (list, tuple)):
        raise Exception("Could not read the assembly's components")

    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 names:
            found[str(name)] = wrapped

    missing = [n for n in names 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)}."
        )
    return found

_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

_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

_model_volume_mm3

_model_volume_mm3(adapter: Any, model: Any) -> float | None

Read the model's volume without forcing a rebuild.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
model Any

The active IModelDoc2.

required

Returns:

Type Description
float | None

float | None: Volume in mm³, or None when it cannot be read.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _model_volume_mm3(adapter: Any, model: Any) -> float | None:
    """Read the model's volume without forcing a rebuild.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        model: The active ``IModelDoc2``.

    Returns:
        float | None: Volume in mm³, or ``None`` when it cannot be read.
    """
    mass_props = adapter._attempt(
        lambda: model.Extension.CreateMassProperty(), default=None
    )
    if mass_props is None:
        return None
    volume = adapter._attempt(lambda: mass_props.Volume, default=None)
    if not isinstance(volume, (int, float)):
        return None
    return round(float(volume) * 1e9, 4)

_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())

_open_or_find_assembly

_open_or_find_assembly(adapter: Any, source: Path) -> Any

Open the assembly, or use it when it is already open; never close anything.

OpenDoc6 returns nothing for a file that can't be opened, including when a document with the same file name is already open. The assembly itself, if open, is found by its path (ISldWorks.GetOpenDocumentByName) and used as it is, unsaved changes included.

Parameters:

Name Type Description Default
adapter Any

A connected adapter.

required
source Path

The assembly file.

required

Returns:

Name Type Description
Any Any

The assembly's IModelDoc2.

Raises:

Type Description
RuntimeError

When it can neither be opened nor found open.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _open_or_find_assembly(adapter: Any, source: Path) -> Any:
    """Open the assembly, or use it when it is already open; never close anything.

    ``OpenDoc6`` returns nothing for a file that can't be opened, including
    when a document with the same file name is already open. The assembly
    itself, if open, is found by its path (``ISldWorks.GetOpenDocumentByName``)
    and used as it is, unsaved changes included.

    Args:
        adapter: A connected adapter.
        source: The assembly file.

    Returns:
        Any: The assembly's ``IModelDoc2``.

    Raises:
        RuntimeError: When it can neither be opened nor found open.
    """
    err, warn = _byref_int(), _byref_int()
    model = adapter.swApp.OpenDoc6(str(source), 2, 1, "", err, warn)
    if model is not None:
        return model
    model = adapter._attempt(
        lambda: adapter.swApp.GetOpenDocumentByName(str(source)), default=None
    )
    if model is not None:
        return model
    if err.value == _SAME_TITLE_ALREADY_OPEN:
        raise RuntimeError(
            f"another document named {source.name} is already open; close it, "
            "then pack again (nothing was closed)"
        )
    raise RuntimeError(f"OpenDoc6 failed err={err.value} warn={warn.value}")

_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

_select_component_face

_select_component_face(adapter: Any, model: Any, component: Any, component_name: str, point: list[float], append: bool) -> None

Select the face of component at point (its part coordinates, mm).

Only that component's faces are searched, so coincident surfaces of two parts (a pin in its hole) can't be confused.

Parameters:

Name Type Description Default
adapter Any

The connected adapter.

required
model Any

The active assembly document.

required
component Any

The IComponent2 dispatch.

required
component_name str

Its instance name, for messages.

required
point list[float]

[x, y, z] mm on the face, in the component's part coordinates.

required
append bool

Add to the current selection.

required

Raises:

Type Description
Exception

When no face of the component lies at the point, or it won't select.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _select_component_face(
    adapter: Any,
    model: Any,
    component: Any,
    component_name: str,
    point: list[float],
    append: bool,
) -> None:
    """Select the face of ``component`` at ``point`` (its part coordinates, mm).

    Only that component's faces are searched, so coincident surfaces of two
    parts (a pin in its hole) can't be confused.

    Args:
        adapter: The connected adapter.
        model: The active assembly document.
        component: The ``IComponent2`` dispatch.
        component_name: Its instance name, for messages.
        point: ``[x, y, z]`` mm on the face, in the component's part coordinates.
        append: Add to the current selection.

    Raises:
        Exception: When no face of the component lies at the point, or it
            won't select.
    """
    from .face_selection import FaceEdgeSelector

    selector = FaceEdgeSelector(adapter)
    try:
        face = selector.face_at_point(selector.component_faces(component), point)
    except LookupError as exc:
        raise Exception(
            f"Face not found on {component_name} at {list(point)} mm (part "
            f"coordinates): {exc}"
        ) from exc
    sel_mgr = _sw_type_info.flagged(model.SelectionManager, "ISelectionMgr")
    select_data = adapter._attempt(lambda: sel_mgr.CreateSelectData(), default=None)
    handle = _sw_type_info.flagged(face.handle, "IEntity")
    if not adapter._attempt(lambda: handle.Select4(append, select_data), default=False):
        raise Exception(
            f"Failed to select the face of {component_name} at {list(point)} mm"
        )

_solid_bodies

_solid_bodies(adapter: Any, part: Any) -> list[Any] | None

Return the part's solid bodies as wrapped IBody2 dispatches.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
part Any

The part document, flagged for IPartDoc.

required

Returns:

Type Description
list[Any] | None

list[Any] | None: The bodies (possibly empty), or None when

list[Any] | None

GetBodies2 could not be read at all. A part with no bodies yet

list[Any] | None

answers null rather than an empty array, so null without an error

list[Any] | None

is an empty list.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _solid_bodies(adapter: Any, part: Any) -> list[Any] | None:
    """Return the part's solid bodies as wrapped ``IBody2`` dispatches.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        part: The part document, flagged for ``IPartDoc``.

    Returns:
        list[Any] | None: The bodies (possibly empty), or ``None`` when
        ``GetBodies2`` could not be read at all. A part with no bodies yet
        answers null rather than an empty array, so null without an error
        is an empty list.
    """
    raw, error = adapter._attempt_with_error(
        lambda: part.GetBodies2(_SW_SOLID_BODY, False)
    )
    if error is not None:
        return None
    if raw is None:
        return []
    items = list(raw) if isinstance(raw, (list, tuple)) else [raw]
    return [_as_com(adapter, body, "IBody2") or body for body in items if body]

_sub_features

_sub_features(adapter: Any, parent: Any) -> list[Any]

Return a feature's sub-features, each flagged for the tree walk.

Parameters:

Name Type Description Default
adapter Any

A connected PyWin32Adapter.

required
parent Any

The parent IFeature.

required

Returns:

Type Description
list[Any]

list[Any]: The sub-features in tree order.

Source code in src/solidworks_mcp/adapters/solidworks/io.py
def _sub_features(adapter: Any, parent: Any) -> list[Any]:
    """Return a feature's sub-features, each flagged for the tree walk.

    Args:
        adapter: A connected ``PyWin32Adapter``.
        parent: The parent ``IFeature``.

    Returns:
        list[Any]: The sub-features in tree order.
    """
    children: list[Any] = []
    child = adapter._attempt(lambda: parent.GetFirstSubFeature(), default=None)
    while child is not None and len(children) < 10000:
        adapter._attempt(
            lambda c=child: _sw_type_info.flag_members(c, *_FEATURE_WALK_MEMBERS),
            default=0,
        )
        children.append(child)
        child = adapter._attempt(lambda c=child: c.GetNextSubFeature(), default=None)
    return children

_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

dependency_paths

dependency_paths(raw_dependencies: Any) -> list[Path]

The model files in a GetDependencies2 reply, each once, in order.

IModelDoc2.GetDependencies2 returns a flat array that alternates a document's name and its full path. Only full paths to SolidWorks model files are kept; a repeated path (compared as Windows does, ignoring case) is kept once.

Parameters:

Name Type Description Default
raw_dependencies Any

The reply; anything other than a list or tuple gives no paths.

required

Returns:

Type Description
list[Path]

list[Path]: The absolute model-file paths.

Source code in src/solidworks_mcp/utils/sw_dependencies.py
def dependency_paths(raw_dependencies: Any) -> list[Path]:
    """The model files in a ``GetDependencies2`` reply, each once, in order.

    ``IModelDoc2.GetDependencies2`` returns a flat array that alternates a
    document's name and its full path. Only full paths to SolidWorks model
    files are kept; a repeated path (compared as Windows does, ignoring case)
    is kept once.

    Args:
        raw_dependencies: The reply; anything other than a list or tuple
            gives no paths.

    Returns:
        list[Path]: The absolute model-file paths.
    """
    if not isinstance(raw_dependencies, (list, tuple)):
        return []
    paths: list[Path] = []
    seen: set[str] = set()
    for item in raw_dependencies:
        if not isinstance(item, str) or not item.lower().endswith(_MODEL_SUFFIXES):
            continue
        path = Path(item)
        key = os.path.normcase(os.path.normpath(item))
        absolute = path.is_absolute() or PureWindowsPath(item).is_absolute()
        if absolute and key not in seen:
            seen.add(key)
            paths.append(path)
    return paths

mate_entity_label

mate_entity_label(entity: str, face: list[float] | None) -> str

How a mate's reply names one side: the entity, or the face point.

Shared by the live and mock adapters.

Parameters:

Name Type Description Default
entity str

The named entity (used when no face is given).

required
face list[float] | None

A point on a face, in part coordinates (mm).

required

Returns:

Name Type Description
str str

e.g. "Front Plane" or "face at [0.0, 5.0, 0.0] mm".

Source code in src/solidworks_mcp/adapters/solidworks/face_selection.py
def mate_entity_label(entity: str, face: list[float] | None) -> str:
    """How a mate's reply names one side: the entity, or the face point.

    Shared by the live and mock adapters.

    Args:
        entity: The named entity (used when no face is given).
        face: A point on a face, in part coordinates (mm).

    Returns:
        str: e.g. ``"Front Plane"`` or ``"face at [0.0, 5.0, 0.0] mm"``.
    """
    if face is None:
        return entity
    return f"face at {[float(c) for c in face]} mm"