Skip to content

solidworks_mcp.adapters.solidworks

solidworks_mcp.adapters.solidworks

Grouped SolidWorks mixins for the PyWin32 adapter.

Attributes

__all__ module-attribute

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

Classes

SolidWorksDocumentationMixin

Expose model-data and drawing-documentation methods through a mixin.

Methods:
arrange_dimensions async
arrange_dimensions(view_name: str | None = None) -> AdapterResult[dict[str, Any]]

Move drawing dimensions so none overlaps another, then verify.

Reads every dimension's text box and lines, plans positions outside the views with :func:dimension_layout.plan_layout, applies them with IAnnotation::SetPosition, rebuilds, then re-reads the real geometry and checks the rule with :func:dimension_layout.find_overlaps. The check covers every view on the sheet, even when only one view is rearranged, because neighbouring views' dimensions can collide.

Parameters:

Name Type Description Default
view_name str | None

Only move this view's dimensions; all views when omitted.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: overlaps_before, overlaps_after

AdapterResult[dict[str, Any]]

(each violation listed), moved and unplaced. ERROR when any

AdapterResult[dict[str, Any]]

overlap remains - the rule is not met - with the same data, so the

AdapterResult[dict[str, Any]]

caller can see which dimensions still collide.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def arrange_dimensions(
    self, view_name: str | None = None
) -> AdapterResult[dict[str, Any]]:
    """Move drawing dimensions so none overlaps another, then verify.

    Reads every dimension's text box and lines, plans positions outside
    the views with :func:`dimension_layout.plan_layout`, applies them with
    ``IAnnotation::SetPosition``, rebuilds, then **re-reads the real
    geometry** and checks the rule with :func:`dimension_layout.find_overlaps`.
    The check covers every view on the sheet, even when only one view is
    rearranged, because neighbouring views' dimensions can collide.

    Args:
        view_name: Only move this view's dimensions; all views when omitted.

    Returns:
        AdapterResult[dict[str, Any]]: ``overlaps_before``, ``overlaps_after``
        (each violation listed), ``moved`` and ``unplaced``. ERROR when any
        overlap remains - the rule is not met - with the same data, so the
        caller can see which dimensions still collide.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)

    def _arrange() -> dict[str, Any]:
        return self._arrange_layout(view_name)

    result = cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("arrange_dimensions", _arrange),
    )
    if result.is_success and result.data and result.data["overlaps_after"]:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=_overlap_summary(result.data),
            data=result.data,
            execution_time=result.execution_time,
        )
    return result
export_flat_pattern async
export_flat_pattern(file_path: str) -> AdapterResult[dict[str, Any]]

Export the active sheet-metal part's flat pattern to DXF or DWG.

Wraps IPartDoc::ExportToDWG2 with swExportToDWG_ExportSheetMetal. SheetMetalOptions 1 exports the flat-pattern geometry only (no bend lines or sketches).

Parameters:

Name Type Description Default
file_path str

Output .dxf or .dwg path.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The written path and its size.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def export_flat_pattern(
    self, file_path: str
) -> AdapterResult[dict[str, Any]]:
    """Export the active sheet-metal part's flat pattern to DXF or DWG.

    Wraps ``IPartDoc::ExportToDWG2`` with ``swExportToDWG_ExportSheetMetal``.
    SheetMetalOptions ``1`` exports the flat-pattern geometry only (no bend
    lines or sketches).

    Args:
        file_path: Output ``.dxf`` or ``.dwg`` path.

    Returns:
        AdapterResult[dict[str, Any]]: The written path and its size.
    """
    guard = self._require_doc_type({1}, "export_flat_pattern")
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    out = os.path.abspath(file_path)
    if os.path.splitext(out)[1].lower() not in {".dxf", ".dwg"}:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="file_path must end in .dxf or .dwg",
        )

    def _export() -> dict[str, Any]:
        model = self.currentModel
        part = _sw_type_info.flagged(model, "IPartDoc")
        bodies = _sequence(
            self._attempt(
                lambda: part.GetBodies2(_SW_SOLID_BODY, True), default=None
            )
        )
        if not any(
            self._attempt(
                lambda b=b: _as_com(self, b, "IBody2").IsSheetMetal(), default=False
            )
            for b in bodies
        ):
            raise LookupError(
                "The active part has no sheet-metal body, so it has no flat "
                "pattern to export"
            )
        os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
        model_path = str(
            self._attempt(lambda: model.GetPathName(), default="") or ""
        )
        ok = part.ExportToDWG2(
            out,
            model_path,
            _SW_EXPORT_SHEET_METAL,
            True,
            None,
            False,
            False,
            1,
            None,
        )
        if not ok or not os.path.exists(out):
            raise Exception(f"ExportToDWG2 did not write {out} (returned {ok!r})")
        return {"file_path": out, "size_bytes": os.path.getsize(out)}

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("export_flat_pattern", _export),
    )
get_custom_properties async
get_custom_properties(configuration: str | None = None) -> AdapterResult[dict[str, Any]]

Read the custom properties of the active document.

Wraps IModelDocExtension::CustomPropertyManager(config) with GetNames + Get6. "" reads the file-level properties a drawing title block usually links to.

Parameters:

Name Type Description Default
configuration str | None

Configuration name; file-level when omitted.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: properties mapping name to

AdapterResult[dict[str, Any]]

{"value", "resolved"}.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def get_custom_properties(
    self, configuration: str | None = None
) -> AdapterResult[dict[str, Any]]:
    """Read the custom properties of the active document.

    Wraps ``IModelDocExtension::CustomPropertyManager(config)`` with
    ``GetNames`` + ``Get6``. ``""`` reads the file-level properties a
    drawing title block usually links to.

    Args:
        configuration: Configuration name; file-level when omitted.

    Returns:
        AdapterResult[dict[str, Any]]: ``properties`` mapping name to
        ``{"value", "resolved"}``.
    """
    if not self.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _get() -> dict[str, Any]:
        manager = _sw_type_info.flagged(
            self.currentModel.Extension.CustomPropertyManager(configuration or ""),
            "ICustomPropertyManager",
        )
        props: dict[str, dict[str, str]] = {}
        names = self._attempt(
            lambda: self._get_attr_or_call(manager, "GetNames"), default=None
        )
        for name in _sequence(names):
            value, resolved = _read_custom_property(self, manager, str(name))
            props[str(name)] = {"value": value, "resolved": resolved}
        return {"configuration": configuration or "", "properties": props}

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("get_custom_properties", _get),
    )
get_material async
get_material(configuration: str | None = None) -> AdapterResult[dict[str, Any]]

Read the material assigned to the active part.

Wraps IPartDoc::GetMaterialPropertyName2; see :func:_read_material for its Database out-parameter.

Parameters:

Name Type Description Default
configuration str | None

Configuration name; the active one when omitted.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: material (None when unset),

AdapterResult[dict[str, Any]]

database and density_kg_m3, read from the material's entry

AdapterResult[dict[str, Any]]

in its .sldmat library (None when unset or not found).

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def get_material(
    self, configuration: str | None = None
) -> AdapterResult[dict[str, Any]]:
    """Read the material assigned to the active part.

    Wraps ``IPartDoc::GetMaterialPropertyName2``; see
    :func:`_read_material` for its ``Database`` out-parameter.

    Args:
        configuration: Configuration name; the active one when omitted.

    Returns:
        AdapterResult[dict[str, Any]]: ``material`` (``None`` when unset),
        ``database`` and ``density_kg_m3``, read from the material's entry
        in its ``.sldmat`` library (``None`` when unset or not found).
    """
    guard = self._require_doc_type({1}, "get_material")
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)

    def _get() -> dict[str, Any]:
        part = _sw_type_info.flagged(self.currentModel, "IPartDoc")
        name, database = _read_material(part, configuration)
        app = getattr(self, "swApp", None)
        density = (
            _material_density(
                _library_files(self._material_folders(app)), name, database
            )
            if name and app is not None
            else None
        )
        return {
            "material": name or None,
            "database": database or None,
            "density_kg_m3": density,
            "configuration": configuration,
        }

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("get_material", _get),
    )
insert_model_dimensions async
insert_model_dimensions(view_name: str | None = None, include_hole_wizard: bool = True) -> AdapterResult[dict[str, Any]]

Bring model dimensions into drawing views, each dimension shown once.

Imports the model's "marked for drawing" dimensions view by view with IDrawingDoc::InsertModelAnnotations3 (UsePlacementInSketch off, so SolidWorks places them rather than copying sketch positions). Drafting rules applied:

  • pictorial views (isometric, dimetric, trimetric) are not dimensioned unless named explicitly;
  • a model dimension already shown in an earlier view is deleted from later views, so each dimension appears exactly once on the sheet.

Falls back to IDrawingDoc::AutoDimension only when nothing at all could be imported. Placement is then made overlap-free by :meth:arrange_dimensions.

Parameters:

Name Type Description Default
view_name str | None

One view to dimension; every orthographic view when omitted.

None
include_hole_wizard bool

Also import Hole Wizard location dimensions.

True

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Per-view counts, duplicates removed,

AdapterResult[dict[str, Any]]

skipped pictorial views and the method used.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def insert_model_dimensions(
    self, view_name: str | None = None, include_hole_wizard: bool = True
) -> AdapterResult[dict[str, Any]]:
    """Bring model dimensions into drawing views, each dimension shown once.

    Imports the model's "marked for drawing" dimensions view by view with
    ``IDrawingDoc::InsertModelAnnotations3`` (``UsePlacementInSketch`` off,
    so SolidWorks places them rather than copying sketch positions).
    Drafting rules applied:

    * pictorial views (isometric, dimetric, trimetric) are not dimensioned
      unless named explicitly;
    * a model dimension already shown in an earlier view is deleted from
      later views, so each dimension appears exactly once on the sheet.

    Falls back to ``IDrawingDoc::AutoDimension`` only when nothing at all
    could be imported. Placement is then made overlap-free by
    :meth:`arrange_dimensions`.

    Args:
        view_name: One view to dimension; every orthographic view when omitted.
        include_hole_wizard: Also import Hole Wizard location dimensions.

    Returns:
        AdapterResult[dict[str, Any]]: Per-view counts, duplicates removed,
        skipped pictorial views and the ``method`` used.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)

    def _insert() -> dict[str, Any]:
        return self._import_dimensions(view_name, include_hole_wizard)

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("insert_model_dimensions", _insert),
    )
list_available_materials async
list_available_materials(search: str | None = None) -> AdapterResult[list[dict[str, str]]]

List materials from the configured SolidWorks material libraries.

Reads the custom library folders from swFileLocationsMaterialDatabases (a ;-separated list) plus the install's built-in sldmaterials folder, and parses every .sldmat file in them.

Parameters:

Name Type Description Default
search str | None

Case-insensitive substring filter on name or classification.

None

Returns:

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

AdapterResult[list[dict[str, str]]]: name, classification

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

and database per material. metadata gives the folders

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

read and the total before filtering, so an empty search can be

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

told apart from an unexpected library location.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def list_available_materials(
    self, search: str | None = None
) -> AdapterResult[list[dict[str, str]]]:
    """List materials from the configured SolidWorks material libraries.

    Reads the custom library folders from
    ``swFileLocationsMaterialDatabases`` (a ``;``-separated list) plus the
    install's built-in ``sldmaterials`` folder, and parses every
    ``.sldmat`` file in them.

    Args:
        search: Case-insensitive substring filter on name or classification.

    Returns:
        AdapterResult[list[dict[str, str]]]: ``name``, ``classification``
        and ``database`` per material. ``metadata`` gives the ``folders``
        read and the ``total`` before filtering, so an empty search can be
        told apart from an unexpected library location.
    """
    app = getattr(self, "swApp", None)
    if app is None:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Not connected to SolidWorks"
        )
    needle = (search or "").strip().lower()
    searched: dict[str, Any] = {}

    def _list() -> list[dict[str, str]]:
        folder_list = self._material_folders(app)
        folders = ";".join(folder_list)
        materials: list[dict[str, str]] = []
        for path in _library_files(folder_list):
            materials.extend(_parse_material_library(path))
        searched["folders"] = folder_list
        searched["total"] = len(materials)
        if not materials:
            raise LookupError(f"No .sldmat libraries found in: {folders}")
        if needle:
            materials = [
                m
                for m in materials
                if needle in m["name"].lower()
                or needle in m["classification"].lower()
            ]
        return materials

    result = cast(
        "AdapterResult[list[dict[str, str]]]",
        self._handle_com_operation("list_available_materials", _list),
    )
    if result.is_success:
        result.metadata = dict(searched)
    return result
list_model_dimensions async
list_model_dimensions(feature_name: str | None = None) -> AdapterResult[list[dict[str, Any]]]

List every dimension in the active part or assembly.

Walks IModelDoc2::FirstFeature / IFeature::GetNextFeature and each feature's GetFirstDisplayDimension chain. full_name (e.g. "D1@Sketch1") is the name set_dimension accepts, so the pair gives edit-by-name access to every driving parameter in the model.

Parameters:

Name Type Description Default
feature_name str | None

Only list dimensions of this feature (case-insensitive).

None

Returns:

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

AdapterResult[list[dict[str, Any]]]: full_name, feature,

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

value (document units), system_value (metres / radians)

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

and read_only per dimension.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def list_model_dimensions(
    self, feature_name: str | None = None
) -> AdapterResult[list[dict[str, Any]]]:
    """List every dimension in the active part or assembly.

    Walks ``IModelDoc2::FirstFeature`` / ``IFeature::GetNextFeature`` and
    each feature's ``GetFirstDisplayDimension`` chain. ``full_name`` (e.g.
    ``"D1@Sketch1"``) is the name ``set_dimension`` accepts, so the pair
    gives edit-by-name access to every driving parameter in the model.

    Args:
        feature_name: Only list dimensions of this feature (case-insensitive).

    Returns:
        AdapterResult[list[dict[str, Any]]]: ``full_name``, ``feature``,
        ``value`` (document units), ``system_value`` (metres / radians)
        and ``read_only`` per dimension.
    """
    guard = self._require_doc_type({1, 2}, "list_model_dimensions")
    if guard is not None:
        return cast("AdapterResult[list[dict[str, Any]]]", guard)
    target = (feature_name or "").strip().lower()

    def _list() -> list[dict[str, Any]]:
        model = self.currentModel
        dims: list[dict[str, Any]] = []
        feature = self._attempt(lambda: model.FirstFeature(), default=None)
        guard_count = 0
        while feature is not None and guard_count < 20000:
            guard_count += 1
            feat = _sw_type_info.flagged(feature, "IFeature")
            name = str(self._attempt(lambda f=feat: f.Name, default="") or "")
            if not target or name.lower() == target:
                display = self._attempt(
                    lambda f=feat: f.GetFirstDisplayDimension(), default=None
                )
                while display is not None:
                    disp = _sw_type_info.flagged(display, "IDisplayDimension")
                    dim = self._attempt(
                        lambda d=disp: d.GetDimension2(0), default=None
                    )
                    if dim is not None:
                        dim = _sw_type_info.flagged(dim, "IDimension")
                        dims.append(
                            {
                                "full_name": _strip_document_suffix(
                                    self._attempt(
                                        lambda d=dim: d.FullName, default=None
                                    )
                                ),
                                "feature": name,
                                "value": self._attempt(
                                    lambda d=dim: d.Value, default=None
                                ),
                                "system_value": self._attempt(
                                    lambda d=dim: d.SystemValue, default=None
                                ),
                                "read_only": bool(
                                    self._attempt(
                                        lambda d=dim: d.ReadOnly, default=False
                                    )
                                ),
                            }
                        )
                    display = self._attempt(
                        lambda f=feat, d=display: f.GetNextDisplayDimension(d),
                        default=None,
                    )
            feature = self._attempt(lambda f=feat: f.GetNextFeature(), default=None)
        if target and not dims:
            raise LookupError(
                f"No dimensions found on feature {feature_name!r}; list "
                "the feature tree with list_features for exact names"
            )
        return dims

    return cast(
        "AdapterResult[list[dict[str, Any]]]",
        self._handle_com_operation("list_model_dimensions", _list),
    )
measure_entities async
measure_entities(entities: list[str]) -> AdapterResult[dict[str, Any]]

Measure one or more entities with SolidWorks' Measure tool.

Selects each entity by spec (see :func:_parse_measure_spec), then runs IModelDocExtension::CreateMeasure + IMeasure::Calculate(None), which measures the current selection. Values SolidWorks reports as -1 (not applicable to the selection) are dropped.

Parameters:

Name Type Description Default
entities list[str]

One or two entity specs, e.g. ["face:0,0.01,0"] for a face area, or two edge/face specs for distance and angle.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Measured values in metres, square

AdapterResult[dict[str, Any]]

metres and radians, plus selected; is_parallel only for

AdapterResult[dict[str, Any]]

two entities.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def measure_entities(
    self, entities: list[str]
) -> AdapterResult[dict[str, Any]]:
    """Measure one or more entities with SolidWorks' Measure tool.

    Selects each entity by spec (see :func:`_parse_measure_spec`), then runs
    ``IModelDocExtension::CreateMeasure`` + ``IMeasure::Calculate(None)``,
    which measures the current selection. Values SolidWorks reports as
    ``-1`` (not applicable to the selection) are dropped.

    Args:
        entities: One or two entity specs, e.g. ``["face:0,0.01,0"]`` for a
            face area, or two edge/face specs for distance and angle.

    Returns:
        AdapterResult[dict[str, Any]]: Measured values in metres, square
        metres and radians, plus ``selected``; ``is_parallel`` only for
        two entities.
    """
    guard = self._require_doc_type({1, 2}, "measure_entities")
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    if not entities or len(entities) > 2:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="measure_entities takes one or two entity specs",
        )

    def _measure() -> dict[str, Any]:
        model = self.currentModel
        self._attempt(lambda: model.ClearSelection2(True), default=None)
        for index, spec in enumerate(entities):
            name, x, y, z, kind = _parse_measure_spec(spec)
            append = index > 0
            if name:
                ok = bool(
                    self._attempt(
                        lambda n=name, k=kind, a=append: (
                            model.Extension.SelectByID2(
                                n, k, 0.0, 0.0, 0.0, a, 0, _null_callout(), 0
                            )
                        ),
                        default=False,
                    )
                )
            else:
                ok = _select_edge_by_coord(self, x, y, z, append, 0, kind)
            if not ok:
                raise LookupError(
                    f"No entity found for {spec!r}; check the name or the "
                    "coordinates (metres)"
                )

        # CreateMeasure is a zero-arg method; unflagged, late binding asks
        # for it as a property and SW 2026 answers "Member not found".
        # Not wrapped in _attempt: a COM error surfaces with its message.
        extension = _sw_type_info.flagged(model.Extension, "IModelDocExtension")
        measure = extension.CreateMeasure()
        if measure is None:
            raise Exception("CreateMeasure returned nothing")
        measure = _sw_type_info.flagged(measure, "IMeasure")
        if not measure.Calculate(None):
            raise Exception("IMeasure.Calculate failed for this selection")

        result: dict[str, Any] = {"selected": list(entities)}
        for prop, key in (
            ("Distance", "distance_m"),
            ("CenterDistance", "center_distance_m"),
            ("Angle", "angle_rad"),
            ("Radius", "radius_m"),
            ("Diameter", "diameter_m"),
            ("Length", "length_m"),
            ("ArcLength", "arc_length_m"),
            ("Area", "area_m2"),
            ("Perimeter", "perimeter_m"),
            ("DeltaX", "delta_x_m"),
            ("DeltaY", "delta_y_m"),
            ("DeltaZ", "delta_z_m"),
        ):
            value = self._attempt(lambda p=prop: getattr(measure, p), default=None)
            if isinstance(value, (int, float)) and value != -1:
                result[key] = float(value)
        if len(entities) == 2:
            # Parallelism only means something between two entities.
            parallel = self._attempt(lambda: measure.IsParallel, default=None)
            if isinstance(parallel, bool):
                result["is_parallel"] = parallel
        self._attempt(lambda: model.ClearSelection2(True), default=None)
        return result

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("measure_entities", _measure),
    )
set_custom_property async
set_custom_property(name: str, value: str, configuration: str | None = None) -> AdapterResult[dict[str, Any]]

Create or overwrite one text custom property on the active document.

Wraps ICustomPropertyManager::Add3(name, swCustomInfoText, value, swCustomPropertyReplaceValue) and reads the value back.

Parameters:

Name Type Description Default
name str

Property name, e.g. "PartNo" or "DrawnBy".

required
value str

Text value; may be a $PRP:"..." link.

required
configuration str | None

Configuration name; file-level when omitted.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The stored and resolved value.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def set_custom_property(
    self, name: str, value: str, configuration: str | None = None
) -> AdapterResult[dict[str, Any]]:
    """Create or overwrite one text custom property on the active document.

    Wraps ``ICustomPropertyManager::Add3(name, swCustomInfoText, value,
    swCustomPropertyReplaceValue)`` and reads the value back.

    Args:
        name: Property name, e.g. ``"PartNo"`` or ``"DrawnBy"``.
        value: Text value; may be a ``$PRP:"..."`` link.
        configuration: Configuration name; file-level when omitted.

    Returns:
        AdapterResult[dict[str, Any]]: The stored and resolved value.
    """
    if not self.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )
    if not name.strip():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="name is required"
        )

    def _set() -> dict[str, Any]:
        manager = _sw_type_info.flagged(
            self.currentModel.Extension.CustomPropertyManager(configuration or ""),
            "ICustomPropertyManager",
        )
        code = manager.Add3(
            name,
            _SW_CUSTOM_INFO_TEXT,
            str(value),
            _SW_CUSTOM_PROPERTY_REPLACE_VALUE,
        )
        stored, resolved = _read_custom_property(self, manager, name)
        if stored != str(value):
            raise Exception(
                f"Custom property {name!r} was not stored (Add3 returned {code!r}, "
                f"read back {stored!r})."
            )
        return {
            "name": name,
            "value": stored,
            "resolved": resolved,
            "configuration": configuration or "",
        }

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("set_custom_property", _set),
    )
set_material async
set_material(material_name: str, database: str | None = None, configuration: str | None = None) -> AdapterResult[dict[str, Any]]

Assign a material to the active part.

Wraps IPartDoc::SetMaterialPropertyName2 (a void call), then reads the material back - the read-back is the only proof it applied. A name the library does not hold leaves the part unchanged and is reported as not found.

Parameters:

Name Type Description Default
material_name str

Material name exactly as in the library, e.g. "AISI 1020" or "6061-T6 (SS)".

required
database str | None

Library name; "SOLIDWORKS Materials" when omitted.

None
configuration str | None

Configuration name; "" (all) when omitted.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Requested and applied material.

Source code in src/solidworks_mcp/adapters/solidworks/documentation.py
async def set_material(
    self,
    material_name: str,
    database: str | None = None,
    configuration: str | None = None,
) -> AdapterResult[dict[str, Any]]:
    """Assign a material to the active part.

    Wraps ``IPartDoc::SetMaterialPropertyName2`` (a ``void`` call), then
    reads the material back - the read-back is the only proof it applied.
    A name the library does not hold leaves the part unchanged and is
    reported as not found.

    Args:
        material_name: Material name exactly as in the library, e.g.
            ``"AISI 1020"`` or ``"6061-T6 (SS)"``.
        database: Library name; ``"SOLIDWORKS Materials"`` when omitted.
        configuration: Configuration name; ``""`` (all) when omitted.

    Returns:
        AdapterResult[dict[str, Any]]: Requested and applied material.
    """
    guard = self._require_doc_type({1}, "set_material")
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    if not material_name.strip():
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="material_name is required"
        )
    library = database or _DEFAULT_MATERIAL_DATABASE

    def _set() -> dict[str, Any]:
        part = _sw_type_info.flagged(self.currentModel, "IPartDoc")
        part.SetMaterialPropertyName2(configuration or "", library, material_name)
        self._attempt(lambda: self.currentModel.EditRebuild3(), default=None)
        applied, _library = _read_material(part, configuration)
        if applied.lower() != material_name.lower():
            # SolidWorks ignores a name its library does not hold.
            raise LookupError(
                f"Material {material_name!r} not found in {library!r}; the "
                f"part still reports {applied or 'no material'}. Use "
                "list_available_materials for the exact name and library."
            )
        return {
            "material": applied,
            "database": library,
            "configuration": configuration,
        }

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("set_material", _set),
    )

SolidWorksDrawingStandardMixin

Create complete standard A3 drawings, BOM tables and the sheet scale.

Methods:
add_datum_tag async
add_datum_tag(label: str, attach_at_mm: tuple[float, float], x_mm: float, y_mm: float) -> AdapterResult[dict[str, Any]]

Add a datum feature symbol (a datum tag) on a drawing edge.

IModelDoc2::InsertDatumTag2 attaches the tag to the edge picked at attach_at_mm; IDatumTag::SetLabel names it, and the label is read back with GetLabel.

Parameters:

Name Type Description Default
label str

The datum letter, e.g. A.

required
attach_at_mm tuple[float, float]

A sheet point on the edge or face line it marks.

required
x_mm float

Where the tag goes on the sheet.

required
y_mm float

Where the tag goes on the sheet.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The label as read back.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
async def add_datum_tag(
    self,
    label: str,
    attach_at_mm: tuple[float, float],
    x_mm: float,
    y_mm: float,
) -> AdapterResult[dict[str, Any]]:
    """Add a datum feature symbol (a datum tag) on a drawing edge.

    ``IModelDoc2::InsertDatumTag2`` attaches the tag to the edge picked at
    ``attach_at_mm``; ``IDatumTag::SetLabel`` names it, and the label is
    read back with ``GetLabel``.

    Args:
        label: The datum letter, e.g. ``A``.
        attach_at_mm: A sheet point on the edge or face line it marks.
        x_mm: Where the tag goes on the sheet.
        y_mm: Where the tag goes on the sheet.

    Returns:
        AdapterResult[dict[str, Any]]: The label as read back.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    letter = label.strip().upper()
    if len(letter) != 1 or not letter.isalpha():
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="label must be a single letter, e.g. 'A'",
        )

    def _insert() -> dict[str, Any]:
        model = self.currentModel
        if not self._select_edge_at(attach_at_mm):
            raise LookupError(f"No drawing edge found at {list(attach_at_mm)} mm")
        tag = self._get_attr_or_call(model, "InsertDatumTag2")
        self._attempt(lambda: model.ClearSelection2(True), default=None)
        if tag is None:
            raise Exception("InsertDatumTag2 returned nothing; no tag was added")
        tag = _sw_type_info.flagged(tag, "IDatumTag")
        tag.SetLabel(letter)
        self._place_annotation(tag, x_mm, y_mm)
        read = str(
            self._attempt(lambda: self._get_attr_or_call(tag, "GetLabel")) or ""
        )
        if read != letter:
            raise Exception(f"The datum tag reads {read!r}, not {letter!r}")
        return {"label": letter, "position_mm": [x_mm, y_mm]}

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("add_datum_tag", _insert),
    )
add_gtol async
add_gtol(characteristic: str, tolerance: float, x_mm: float, y_mm: float, diameter_zone: bool = False, material_condition: str = 'none', datums: list[str] | None = None, attach_at_mm: tuple[float, float] | None = None, dimension: tuple[str, str] | None = None) -> AdapterResult[dict[str, Any]]

Add a feature control frame (GD&T) to the active drawing.

IModelDoc2::InsertGtol creates the frame on whatever is selected: a drawing dimension (dimension; the frame sits under its text and moves with it, the usual place for a hole's position tolerance), the edge picked at attach_at_mm (with a leader), or nothing (free-standing); IGtol::SetFrameSymbols2 sets the characteristic, the Ø zone and the material condition, and IGtol::SetFrameValues the tolerance and datums. The values are read back with GetFrameValues, so a frame SolidWorks did not fill in is an error, not a success.

Parameters:

Name Type Description Default
characteristic str

position, flatness, perpendicularity, parallelism, profile_surface or concentricity.

required
tolerance float

The tolerance value, in drawing units.

required
x_mm float

Where the frame goes on the sheet.

required
y_mm float

Where the frame goes on the sheet.

required
diameter_zone bool

Prefix the tolerance with Ø (a cylindrical zone).

False
material_condition str

none, mmc (Ⓜ) or lmc (Ⓛ).

'none'
datums list[str] | None

Up to three datum letters, primary first.

None
attach_at_mm tuple[float, float] | None

A sheet point on the edge the leader points to.

None
dimension tuple[str, str] | None

(view name, model dimension name), e.g. ("Drawing View2", "D3@Sketch1"): attach the frame to that drawing dimension. x_mm/y_mm are then not used.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The frame as read back.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
async def add_gtol(
    self,
    characteristic: str,
    tolerance: float,
    x_mm: float,
    y_mm: float,
    diameter_zone: bool = False,
    material_condition: str = "none",
    datums: list[str] | None = None,
    attach_at_mm: tuple[float, float] | None = None,
    dimension: tuple[str, str] | None = None,
) -> AdapterResult[dict[str, Any]]:
    """Add a feature control frame (GD&T) to the active drawing.

    ``IModelDoc2::InsertGtol`` creates the frame on whatever is selected:
    a drawing dimension (``dimension``; the frame sits under its text and
    moves with it, the usual place for a hole's position tolerance), the
    edge picked at ``attach_at_mm`` (with a leader), or nothing
    (free-standing);
    ``IGtol::SetFrameSymbols2`` sets the characteristic, the Ø zone and
    the material condition, and ``IGtol::SetFrameValues`` the tolerance
    and datums. The values are read back with ``GetFrameValues``, so a
    frame SolidWorks did not fill in is an error, not a success.

    Args:
        characteristic: ``position``, ``flatness``, ``perpendicularity``,
            ``parallelism``, ``profile_surface`` or ``concentricity``.
        tolerance: The tolerance value, in drawing units.
        x_mm: Where the frame goes on the sheet.
        y_mm: Where the frame goes on the sheet.
        diameter_zone: Prefix the tolerance with Ø (a cylindrical zone).
        material_condition: ``none``, ``mmc`` (Ⓜ) or ``lmc`` (Ⓛ).
        datums: Up to three datum letters, primary first.
        attach_at_mm: A sheet point on the edge the leader points to.
        dimension: ``(view name, model dimension name)``, e.g.
            ``("Drawing View2", "D3@Sketch1")``: attach the frame to that
            drawing dimension. ``x_mm``/``y_mm`` are then not used.

    Returns:
        AdapterResult[dict[str, Any]]: The frame as read back.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    problem = gtol_problem(characteristic, tolerance, material_condition, datums)
    if problem is not None:
        return AdapterResult(status=AdapterResultStatus.ERROR, error=problem)
    symbol = _GTOL_SYMBOLS[characteristic]
    modifier = _GTOL_MODIFIERS[material_condition]
    letters = [d.strip().upper() for d in datums or []]

    def _insert() -> dict[str, Any]:
        model = self.currentModel
        target = None
        if dimension is not None:
            target = self._drawing_dimension(*dimension)
            if target is None or not self._select_display_dimension(model, target):
                raise LookupError(
                    f"No dimension {dimension[1]!r} in view {dimension[0]!r}"
                )
            attached: Any = dimension[1]
        elif attach_at_mm is not None:
            if not self._select_edge_at(attach_at_mm):
                raise LookupError(
                    f"No drawing edge found at {list(attach_at_mm)} mm"
                )
            attached = True
        else:
            attached = False
        gtol = self._get_attr_or_call(model, "InsertGtol")
        self._attempt(lambda: model.ClearSelection2(True), default=None)
        if gtol is None:
            raise Exception("InsertGtol returned nothing; no frame was added")
        gtol = _sw_type_info.flagged(gtol, "IGtol")
        padded = [*letters, "", "", ""][:3]
        text = f"{tolerance:g}"
        gtol.SetFrameSymbols2(
            1, symbol, diameter_zone, modifier, False, "", "", "", ""
        )
        gtol.SetFrameValues(1, text, "", *padded)
        if dimension is None:
            self._place_annotation(gtol, x_mm, y_mm)
        self._attempt(lambda: model.GraphicsRedraw2(), default=None)
        values = self._attempt(lambda: gtol.GetFrameValues(1), default=None)
        read = [str(v) for v in values] if isinstance(values, (list, tuple)) else []
        if not read or read[0] != text or read[2:5] != padded:
            raise Exception(
                f"The frame did not take its values: read back {read or None}, "
                f"expected tolerance {text!r} and datums {padded}"
            )
        return {
            "characteristic": characteristic,
            "tolerance": text,
            "diameter_zone": diameter_zone,
            "material_condition": material_condition,
            "datums": letters,
            "attached": attached,
            "position_mm": None if dimension is not None else [x_mm, y_mm],
            "layout": (
                None
                if dimension is None
                else self._clear_attached_frame(dimension[0], target, gtol)
            ),
        }

    result = cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("add_gtol", _insert),
    )
    layout = (result.data or {}).get("layout") if result.is_success else None
    if layout and layout["overlaps_after"]:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                f"The frame was added but {layout['overlaps_after']} overlap(s) "
                "remain on the sheet - the no-overlap rule is not met; see layout"
            ),
            data=result.data,
            execution_time=result.execution_time,
        )
    return result
create_standard_drawing async
create_standard_drawing(model_path: str, output_dir: str | None = None, title: str | None = None, subtitle: str | None = None, drawing_number: str | None = None, author: str = '', revision: str = 'A', company: str = '', drawn_date: str | None = None, notes: list[str] | None = None, include_isometric: bool = True, write_model_properties: bool = True, title_block_text_height_mm: float | None = None, include_bom: bool = False) -> AdapterResult[dict[str, Any]]

Create a complete A3 drawing that follows the drawing standard.

Opens the model (or uses its open copy), writes the title-block properties into it, makes a drawing on the A3 ISO format, builds the sheet in separate COM steps, writes the drawing properties, saves <drawing_number>.SLDDRW and exports a PDF.

Parameters:

Name Type Description Default
model_path str

Part (.SLDPRT) or assembly (.SLDASM) to draw.

required
output_dir str | None

Folder for the .SLDDRW and PDF; the model's folder by default.

None
title str | None

Title block TITLE; the model file name by default.

None
subtitle str | None

Company field when company is empty.

None
drawing_number str | None

DWG NO.; also the drawing file name. Default <MODEL>-Rev<revision>-01.

None
author str

DRAWN name.

''
revision str

Revision letter.

'A'
company str

Company name field.

''
drawn_date str | None

DRAWN date; today (e.g. "17 Oct 2026") by default.

None
notes list[str] | None

Extra note lines (at most seven lines in total are shown).

None
include_isometric bool

Add the isometric view in its cell.

True
write_model_properties bool

Write Description/Material/Weight custom properties into the model (the format reads them from there) and save the model.

True
title_block_text_height_mm float | None

Largest text height for the DWG NO. and TITLE fields, e.g. 3 for smaller text; they are shrunk further when they still do not fit.

None
include_bom bool

Assemblies only: a top-level BOM in the top of the isometric's cell, the isometric fitted below it.

False

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: Files, scale, views, dimension

AdapterResult[dict[str, Any]]

counts, the overlap check, every property written and each step's

AdapterResult[dict[str, Any]]

outcome. ERROR, with that data, when a step fails, the drawing or

AdapterResult[dict[str, Any]]

PDF could not be saved, or any overlap remains.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
async def create_standard_drawing(
    self,
    model_path: str,
    output_dir: str | None = None,
    title: str | None = None,
    subtitle: str | None = None,
    drawing_number: str | None = None,
    author: str = "",
    revision: str = "A",
    company: str = "",
    drawn_date: str | None = None,
    notes: list[str] | None = None,
    include_isometric: bool = True,
    write_model_properties: bool = True,
    title_block_text_height_mm: float | None = None,
    include_bom: bool = False,
) -> AdapterResult[dict[str, Any]]:
    """Create a complete A3 drawing that follows the drawing standard.

    Opens the model (or uses its open copy), writes the title-block
    properties into it, makes a drawing on the A3 ISO format, builds the
    sheet in separate COM steps, writes the drawing properties, saves
    ``<drawing_number>.SLDDRW`` and exports a PDF.

    Args:
        model_path: Part (.SLDPRT) or assembly (.SLDASM) to draw.
        output_dir: Folder for the .SLDDRW and PDF; the model's folder by default.
        title: Title block TITLE; the model file name by default.
        subtitle: Company field when ``company`` is empty.
        drawing_number: DWG NO.; also the drawing file name. Default
            ``<MODEL>-Rev<revision>-01``.
        author: DRAWN name.
        revision: Revision letter.
        company: Company name field.
        drawn_date: DRAWN date; today (e.g. "17 Oct 2026") by default.
        notes: Extra note lines (at most seven lines in total are shown).
        include_isometric: Add the isometric view in its cell.
        write_model_properties: Write Description/Material/Weight custom
            properties into the model (the format reads them from there)
            and save the model.
        title_block_text_height_mm: Largest text height for the DWG NO.
            and TITLE fields, e.g. 3 for smaller text; they are shrunk
            further when they still do not fit.
        include_bom: Assemblies only: a top-level BOM in the top of the
            isometric's cell, the isometric fitted below it.

    Returns:
        AdapterResult[dict[str, Any]]: Files, scale, views, dimension
        counts, the overlap check, every property written and each step's
        outcome. ERROR, with that data, when a step fails, the drawing or
        PDF could not be saved, or any overlap remains.
    """
    path = os.path.abspath(model_path)
    if not os.path.isfile(path):
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error=f"Model file not found: {path}"
        )
    ext = os.path.splitext(path)[1].lower()
    if ext not in {".sldprt", ".sldasm"}:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=f"model_path must be a .SLDPRT or .SLDASM, not {ext or 'no extension'}",
        )
    if include_bom and ext != ".sldasm":
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="include_bom needs an assembly (.SLDASM); a part has no BOM",
        )
    stem = os.path.splitext(os.path.basename(path))[0]
    rev = (revision or "A").strip()
    number = drawing_number or f"{stem.upper().replace(' ', '-')}-Rev{rev}-01"
    out_dir = os.path.abspath(output_dir or os.path.dirname(path))
    drawing_path = os.path.join(out_dir, f"{number}.SLDDRW")
    pdf_path = os.path.join(out_dir, f"{number}.pdf")
    steps: list[dict[str, Any]] = []
    written: dict[str, dict[str, str]] = {}
    data: dict[str, Any] = {"drawing_number": number, "steps": steps}

    def failed(error: str) -> AdapterResult[dict[str, Any]]:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error=error, data=data
        )

    # 1. The model: open it, read its material, write the title-block properties.
    active = self._handle_com_operation(
        "activate_document", lambda: self._use_open_document(path)
    )
    if not (active.is_success and active.data):
        opened = await self.open_model(path)
        if not opened.is_success:
            return failed(
                f"Could not open model: {opened.error}. If a different file "
                "with the same name is open in SolidWorks, close it first."
            )
    material = None
    if ext == ".sldprt":
        mat = await self.get_material()
        material = (mat.data or {}).get("material") if mat.is_success else None
    model_props, drawing_props = title_block_properties(
        model_file=path,
        title=title or stem,
        subtitle=subtitle,
        author=author,
        drawn_date=drawn_date or _date.today().strftime("%d %b %Y"),
        revision=rev,
        company=company,
    )
    if ext == ".sldasm":
        # An assembly has no material of its own: a link to it shows
        # "<not specified>". Its own Material property is left as set.
        model_props.pop("Material")
    if write_model_properties:
        for name, value in model_props.items():
            _step(
                steps,
                f"model_property:{name}",
                await self.set_custom_property(name, value),
            )
        _step(steps, "save_model", await self.save_file())
        written["model"] = model_props

    # 2. A drawing on the A3 ISO format, without the Sheet Format/Size prompt.
    found = self._handle_com_operation(
        "standard_drawing_format", lambda: find_sheet_format(self)
    )
    if not (found.is_success and found.data):
        return failed(
            "Sheet format 'a3 - iso.slddrt' not found in SolidWorks' sheet "
            "format folders or under C:\\ProgramData\\SOLIDWORKS"
        )
    prompt = self._handle_com_operation(
        "standard_drawing_dialog", self._new_drawing_without_dialog
    )
    try:
        created = await self.create_drawing()
    finally:
        self._handle_com_operation(
            "standard_drawing_dialog",
            lambda: self._restore_dialog(
                prompt.data if prompt.is_success else None
            ),
        )
    if not created.is_success:
        return failed(f"Could not create drawing: {created.error}")
    _step(steps, "drawing_units_mm", await self.set_units("mm"))

    # 3. The sheet, one COM step at a time.
    s = _Sheet(
        path=path,
        sheet_format=str(found.data),
        include_isometric=include_isometric,
        include_bom=include_bom,
        material=material,
        extra_notes=list(notes or []),
        field_height=(
            title_block_text_height_mm / 1000.0
            if title_block_text_height_mm
            else None
        ),
    )
    for name, build in (
        ("sheet", self._setup_a3_sheet),
        ("views", self._create_views),
        ("layout", self._fit_views),
        ("bom", self._bom_above_iso),
        ("dimensions", self._dimension_views),
        ("notes", self._annotate),
        ("check", self._check_sheet),
    ):
        done = self._handle_com_operation(
            f"standard_drawing_{name}", lambda build=build: build(s)
        )
        _step(steps, f"build_{name}", done)
        if not done.is_success:
            return failed(f"Drawing step '{name}' failed: {done.error}")
        if name == "dimensions":
            data["dimensions"] = done.data
        if name == "bom":
            data["bom"] = done.data
        if name == "views":
            data["hidden_lines_visible"] = done.data["hidden_lines_visible"]
        if name == "check":
            data["overlap_check"] = done.data
    data.update(
        {
            "scale": scale_label(s.scale),
            "isometric_scale": scale_label(s.iso_scale) if s.iso_scale else None,
            "views": {
                n: {"role": s.role.get(n), "caption": s.captions.get(n)}
                for n in s.views
            },
            "sheet_format": s.sheet_format,
            "notes": s.note_lines,
        }
    )

    # 4. Drawing properties, then the files.
    for name, value in drawing_props.items():
        _step(
            steps,
            f"drawing_property:{name}",
            await self.set_custom_property(name, value),
        )
    written["drawing"] = drawing_props
    data["properties_written"] = written
    os.makedirs(out_dir, exist_ok=True)
    saved = await self.save_file(drawing_path)
    _step(steps, "save_drawing", saved)
    data["drawing_path"] = drawing_path if saved.is_success else None
    if not saved.is_success:
        return failed(
            f"Drawing built but not saved to {drawing_path}: {saved.error}"
        )

    # 5. The title block, now that the DWG NO. shows the saved file name.
    fitted = self._handle_com_operation(
        "standard_drawing_title_block", lambda: self._fit_title_block(s)
    )
    _step(steps, "fit_title_block", fitted)
    data["title_block"] = fitted.data if fitted.is_success else None
    data["format_links"] = s.format_links
    check = data["overlap_check"]
    for name, field_fit in (data["title_block"] or {}).items():
        if field_fit.get("fits") is False:
            check["annotation_overlaps"].append(
                {
                    "dimension": f"title block {name}",
                    "conflicts_with": "the next title-block field",
                    "kind": "field_overflow",
                }
            )
            check["overlaps_after"] = check.get("overlaps_after", 0) + 1
    _step(steps, "save_drawing_again", await self.save_file(drawing_path))
    pdf = await self.export_file(pdf_path, "pdf")
    _step(steps, "export_pdf", pdf)
    data["pdf_path"] = pdf_path if pdf.is_success else None
    if not pdf.is_success:
        return failed(
            f"Drawing saved to {drawing_path} but the PDF export failed: {pdf.error}"
        )

    overlaps = data["overlap_check"].get("overlaps_after", 0)
    overflowing = any(
        f.get("fits") is False for f in (data.get("title_block") or {}).values()
    )
    if overlaps:
        check = data["overlap_check"]
        found = [
            *check.get("remaining_overlaps", []),
            *check.get("annotation_overlaps", []),
        ]
        named = "; ".join(
            f"{o['dimension']} vs {o['conflicts_with']} ({o['kind']})"
            for o in found[:5]
        )
        return failed(
            f"Drawing saved but {overlaps} overlap(s) remain - the no-overlap "
            "rule is not met; see overlap_check"
            + (f": {named}" if named else "")
            + (f". For a title-block field, {FIELD_HINT}" if overflowing else "")
        )
    return AdapterResult(status=AdapterResultStatus.SUCCESS, data=data)
insert_bom_table async
insert_bom_table(view_name: str, template_path: str | None = None, bom_type: str = 'top_level_only', x_mm: float | None = None, y_mm: float | None = None) -> AdapterResult[dict[str, Any]]

Insert a bill-of-materials table anchored to an assembly drawing view.

Wraps IView::InsertBomTable4. With no position the table snaps to the sheet format's BOM anchor, or to the sheet's bottom-left corner when the sheet has no format. Its stationary corner is the bottom-left, so the table grows up onto the sheet; with the top-left it hung below the sheet (measured live on SW 2026). The table's box is read back and must lie on the sheet. An anchor near the top of the sheet (the ISO A3 format's) makes it grow off the top, so a table placed by the anchor is slid back onto the sheet; one placed at a given x_mm/y_mm is never moved. With no template the library default bom-standard.sldbomtbt from the BOM templates folder is used.

Parameters:

Name Type Description Default
view_name str

A view of an assembly on the active drawing.

required
template_path str | None

.sldbomtbt table template.

None
bom_type str

top_level_only, parts_only or indented.

'top_level_only'
x_mm float | None

Optional table position on the sheet.

None
y_mm float | None

Optional table position on the sheet.

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The view, BOM type, template, row

AdapterResult[dict[str, Any]]

count, table_box_mm and moved_onto_sheet; an error naming

AdapterResult[dict[str, Any]]

the box when the table still runs off the sheet.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
async def insert_bom_table(
    self,
    view_name: str,
    template_path: str | None = None,
    bom_type: str = "top_level_only",
    x_mm: float | None = None,
    y_mm: float | None = None,
) -> AdapterResult[dict[str, Any]]:
    """Insert a bill-of-materials table anchored to an assembly drawing view.

    Wraps ``IView::InsertBomTable4``. With no position the table snaps to
    the sheet format's BOM anchor, or to the sheet's bottom-left corner
    when the sheet has no format. Its stationary corner is the
    bottom-left, so the table grows up onto the sheet; with the top-left
    it hung below the sheet (measured live on SW 2026). The table's box is
    read back and must lie on the sheet. An anchor near the top of the
    sheet (the ISO A3 format's) makes it grow off the top, so a table
    placed by the anchor is slid back onto the sheet; one placed at a
    given ``x_mm``/``y_mm`` is never moved. With no template the library
    default ``bom-standard.sldbomtbt`` from the BOM templates folder is
    used.

    Args:
        view_name: A view of an assembly on the active drawing.
        template_path: ``.sldbomtbt`` table template.
        bom_type: ``top_level_only``, ``parts_only`` or ``indented``.
        x_mm: Optional table position on the sheet.
        y_mm: Optional table position on the sheet.

    Returns:
        AdapterResult[dict[str, Any]]: The view, BOM type, template, row
        count, ``table_box_mm`` and ``moved_onto_sheet``; an error naming
        the box when the table still runs off the sheet.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    if bom_type not in _SW_BOM_TYPES:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error="bom_type must be top_level_only, parts_only or indented",
        )

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation(
            "insert_bom_table",
            lambda: self._insert_bom(
                view_name, template_path, bom_type, x_mm, y_mm
            )[1],
        ),
    )
set_sheet_scale async
set_sheet_scale(numerator: float, denominator: float) -> AdapterResult[dict[str, Any]]

Set the active drawing sheet's scale, e.g. 1:2 (ISheet::SetScale).

Parameters:

Name Type Description Default
numerator float

Scale numerator.

required
denominator float

Scale denominator.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The sheet name, the scale read

AdapterResult[dict[str, Any]]

back from the sheet and the requested one.

Source code in src/solidworks_mcp/adapters/solidworks/drawing_standard.py
async def set_sheet_scale(
    self, numerator: float, denominator: float
) -> AdapterResult[dict[str, Any]]:
    """Set the active drawing sheet's scale, e.g. 1:2 (``ISheet::SetScale``).

    Args:
        numerator: Scale numerator.
        denominator: Scale denominator.

    Returns:
        AdapterResult[dict[str, Any]]: The sheet name, the ``scale`` read
        back from the sheet and the ``requested`` one.
    """
    guard = self._require_drawing()
    if guard is not None:
        return cast("AdapterResult[dict[str, Any]]", guard)
    if numerator <= 0 or denominator <= 0:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="Scale values must be positive"
        )

    def _scale() -> dict[str, Any]:
        drawing = _sw_type_info.flagged(self.currentModel, "IDrawingDoc")
        sheet = _sw_type_info.flagged(drawing.GetCurrentSheet(), "ISheet")
        if not sheet.SetScale(float(numerator), float(denominator), True, False):
            raise Exception("ISheet.SetScale returned False")
        self._attempt(lambda: self.currentModel.EditRebuild3(), default=None)
        name = self._attempt(lambda: sheet.GetName(), default=None)
        # GetProperties2: paper size, template, scale num, scale den, ...
        props = self._attempt(lambda: sheet.GetProperties2(), default=None)
        if not (isinstance(props, (list, tuple)) and len(props) >= 4):
            raise Exception("The sheet's scale could not be read back")
        return {
            "sheet": name,
            "scale": f"{float(props[2]):g}:{float(props[3]):g}",
            "requested": f"{numerator:g}:{denominator:g}",
        }

    return cast(
        "AdapterResult[dict[str, Any]]",
        self._handle_com_operation("set_sheet_scale", _scale),
    )

SolidWorksFeaturesMixin

Expose SolidWorks feature methods via mixin-local implementation helpers.

Methods:
add_fillet async
add_fillet(radius: float, edge_names: list[str]) -> AdapterResult[SolidWorksFeature]

Fillet edges, then confirm the model's volume actually changed.

IModelDoc2.FeatureFillet3 can report success without building anything, so besides the feature-count check inside :func:_add_fillet_impl the volume is compared before and after, here in the async layer for the same reason as mirror_feature. An unreadable volume is not treated as failure.

Parameters:

Name Type Description Default
radius float

Fillet radius in millimetres.

required
edge_names list[str]

Edge names, "x,y,z" points or "face:x,y,z" specs; see :func:_add_fillet_impl.

required

Returns:

Type Description
AdapterResult[SolidWorksFeature]

AdapterResult[SolidWorksFeature]: The fillet, with

AdapterResult[SolidWorksFeature]

volume_before / volume_after in its properties when they

AdapterResult[SolidWorksFeature]

could be read. ERROR when the volume did not change.

Source code in src/solidworks_mcp/adapters/solidworks/features.py
async def add_fillet(
    self, radius: float, edge_names: list[str]
) -> AdapterResult[SolidWorksFeature]:
    """Fillet edges, then confirm the model's volume actually changed.

    ``IModelDoc2.FeatureFillet3`` can report success without building
    anything, so besides the feature-count check inside
    :func:`_add_fillet_impl` the volume is compared before and after,
    here in the async layer for the same reason as ``mirror_feature``.
    An unreadable volume is not treated as failure.

    Args:
        radius: Fillet radius in millimetres.
        edge_names: Edge names, ``"x,y,z"`` points or ``"face:x,y,z"``
            specs; see :func:`_add_fillet_impl`.

    Returns:
        AdapterResult[SolidWorksFeature]: The fillet, with
        ``volume_before`` / ``volume_after`` in its properties when they
        could be read. ``ERROR`` when the volume did not change.
    """
    before = await self.get_mass_properties()
    volume_before = (
        before.data.volume if before.is_success and before.data else None
    )

    result = _add_fillet_impl(self, radius, edge_names)
    if not result.is_success or result.data is None or volume_before is None:
        return result

    after = await self.get_mass_properties()
    volume_after = after.data.volume if after.is_success and after.data else None
    if volume_after is None:
        return result
    if abs(volume_after - volume_before) <= 1e-7 * max(volume_before, 1.0):
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                f"The fillet changed nothing (volume stayed at "
                f"{volume_before:.3f} mm3). SolidWorks accepted the call "
                "but filleted no edge; check that the specs name edges or "
                "faces of the body."
            ),
        )
    result.data.properties["volume_before"] = volume_before
    result.data.properties["volume_after"] = volume_after
    return result
create_reference_point async
create_reference_point(mode: str, x: float, y: float, z: float, distance: float | None = None, percent: float | None = None) -> AdapterResult[dict[str, Any]]

Create a reference point on the active part.

Parameters:

Name Type Description Default
mode str

"along_curve" or "face_center".

required
x float

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

required
y float

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

required
z float

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

required
distance float | None

For along_curve, offset from the edge start in mm.

None
percent float | None

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

None

Returns:

Type Description
AdapterResult[dict[str, Any]]

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

AdapterResult[dict[str, Any]]

func:_create_reference_point_impl; see it for the payload shape.

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

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

    Returns:
        AdapterResult[dict[str, Any]]: Delegates to
        :func:`_create_reference_point_impl`; see it for the payload shape.
    """
    return _create_reference_point_impl(self, mode, x, y, z, distance, percent)
delete_feature async
delete_feature(name: str) -> AdapterResult[dict[str, Any]]

Delete a feature and report every feature that went with it.

The tree is listed before and after with list_features, the read path that works on SolidWorks, and the names that disappeared become removed_features: the named feature and its dependents.

Parameters:

Name Type Description Default
name str

The feature or sketch to delete.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The delete's data plus

AdapterResult[dict[str, Any]]

removed_features in tree order (just name when the tree

AdapterResult[dict[str, Any]]

can't be listed).

Source code in src/solidworks_mcp/adapters/solidworks/features.py
async def delete_feature(self, name: str) -> AdapterResult[dict[str, Any]]:
    """Delete a feature and report every feature that went with it.

    The tree is listed before and after with ``list_features``, the read
    path that works on SolidWorks, and the names that disappeared become
    ``removed_features``: the named feature and its dependents.

    Args:
        name: The feature or sketch to delete.

    Returns:
        AdapterResult[dict[str, Any]]: The delete's data plus
        ``removed_features`` in tree order (just ``name`` when the tree
        can't be listed).
    """
    before = await self.list_features(include_suppressed=True)
    result = _delete_feature_impl(self, name)
    if not result.is_success:
        return result
    after = await self.list_features(include_suppressed=True)
    data = dict(result.data or {})
    data["removed_features"] = _removed_names(before, after) or [
        name.split("@", 1)[0]
    ]
    return AdapterResult(
        status=AdapterResultStatus.SUCCESS,
        data=data,
        execution_time=result.execution_time,
    )
mirror_feature async
mirror_feature(features: list[str], mirror_plane: str, merge: bool = True, mirror_bodies: bool = True) -> AdapterResult[dict[str, Any]]

Mirror solid bodies or features about a plane.

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

Parameters:

Name Type Description Default
features list[str]

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

required
mirror_plane str

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

required
merge bool

Merge the mirrored result with the original.

True
mirror_bodies bool

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

True

Returns:

Type Description
AdapterResult[dict[str, Any]]

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

AdapterResult[dict[str, Any]]

before, after and its change. ERROR when nothing was selected,

AdapterResult[dict[str, Any]]

the call failed, or the volume did not change.

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

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

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

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

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

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

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

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

    return _with_volume_change(result, volume_before, volume_after)
pattern_circular async
pattern_circular(features: list[str], axis: str, count: int, angle: float = 360.0, equal_spacing: bool = True) -> AdapterResult[dict[str, Any]]

Pattern features around an axis.

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

Note that instances landing off the parent body are silently dropped by SolidWorks, so the volume changes by less than count - 1 times the seed. That is correct behaviour, not a failure - only no change at all means the pattern did nothing. A pattern of a cut lowers the volume, a pattern of a boss raises it; both count.

Parameters:

Name Type Description Default
features list[str]

Names of the features to pattern.

required
axis str

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

required
count int

Total number of instances including the original.

required
angle float

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

360.0
equal_spacing bool

Space instances evenly across angle.

True

Returns:

Type Description
AdapterResult[dict[str, Any]]

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

AdapterResult[dict[str, Any]]

before, after and its change. ERROR when nothing could be

AdapterResult[dict[str, Any]]

selected or the volume did not change.

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

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

    Note that instances landing off the parent body are silently dropped
    by SolidWorks, so the volume changes by less than ``count - 1`` times
    the seed. That is correct behaviour, not a failure - only *no* change
    at all means the pattern did nothing. A pattern of a cut lowers the
    volume, a pattern of a boss raises it; both count.

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

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

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

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

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

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

    return _with_volume_change(result, volume_before, volume_after)
rename_feature async
rename_feature(old_name: str, new_name: str) -> AdapterResult[dict[str, Any]]

Rename a feature on the active model's tree.

Parameters:

Name Type Description Default
old_name str

The feature's current name.

required
new_name str

The name to give it.

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

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

AdapterResult[dict[str, Any]]

func:_rename_feature_impl; see it for the payload shape.

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

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

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

SolidWorksIOMixin

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

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

Make an already-open document the active one.

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

Parameters:

Name Type Description Default
title_or_path str

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

required

Returns:

Type Description
AdapterResult[dict[str, Any]]

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

AdapterResult[dict[str, Any]]

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

AdapterResult[dict[str, Any]]

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

AdapterResult[dict[str, Any]]

blank, or nothing open matches.

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

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

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

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

    def _activate() -> dict[str, Any]:
        app = adapter.swApp

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

SolidWorksSelectionMixin

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

Methods:
get_selected_entities async
get_selected_entities() -> AdapterResult[list[dict[str, Any]]]

Report the faces/edges/vertices currently highlighted in the live SolidWorks UI, so an agent can see exactly what the user clicked on instead of guessing from a screenshot (see com-api-pitfalls.md #21 for why a screenshot alone is not reliable proof of document/view state).

Refreshes currentModel from swApp.ActiveDoc first, the same way list_features does, since the user's selection lives on whatever document is actually active in the UI right now -- which may not be the document this adapter last created or touched.

Returns:

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

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

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

entity, each with at least index and type

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

("FACE"/"EDGE"/"VERTEX"/"UNKNOWN(<n>)"). Faces

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

additionally carry bounding_box_mm, center_mm,

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

area_mm2 and normal (when planar). Edges carry

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

start_mm/end_mm and a straight-line length_mm

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

(the true length of a curved edge will be longer). Vertices

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

carry point_mm. Any field that fails to resolve is simply

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

omitted rather than failing the whole call -- a partial

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

description is more useful than none.

Source code in src/solidworks_mcp/adapters/solidworks/selection.py
async def get_selected_entities(self) -> AdapterResult[list[dict[str, Any]]]:
    """Report the faces/edges/vertices currently highlighted in the live
    SolidWorks UI, so an agent can see exactly what the user clicked on
    instead of guessing from a screenshot (see com-api-pitfalls.md #21 for
    why a screenshot alone is not reliable proof of document/view state).

    Refreshes ``currentModel`` from ``swApp.ActiveDoc`` first, the same
    way ``list_features`` does, since the user's selection lives on
    whatever document is actually active in the UI right now -- which
    may not be the document this adapter last created or touched.

    Returns:
        AdapterResult[list[dict[str, Any]]]: One entry per selected
        entity, each with at least ``index`` and ``type``
        (``"FACE"``/``"EDGE"``/``"VERTEX"``/``"UNKNOWN(<n>)"``). Faces
        additionally carry ``bounding_box_mm``, ``center_mm``,
        ``area_mm2`` and ``normal`` (when planar). Edges carry
        ``start_mm``/``end_mm`` and a straight-line ``length_mm``
        (the true length of a curved edge will be longer). Vertices
        carry ``point_mm``. Any field that fails to resolve is simply
        omitted rather than failing the whole call -- a partial
        description is more useful than none.
    """
    sw_app = getattr(self, "swApp", None)
    active_model = getattr(sw_app, "ActiveDoc", None) if sw_app else None
    if active_model is not None:
        self.currentModel = active_model
    if not self.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _describe() -> list[dict[str, Any]]:
        model = self.currentModel
        sel_mgr = model.SelectionManager
        self._attempt(
            lambda: _sw_type_info.flag_methods(sel_mgr, "ISelectionMgr"),
            default=0,
        )
        count = (
            self._attempt(lambda: sel_mgr.GetSelectedObjectCount2(-1), default=0)
            or 0
        )

        results: list[dict[str, Any]] = []
        for i in range(1, int(count) + 1):
            raw_type = self._attempt(
                lambda i=i: sel_mgr.GetSelectedObjectType3(i, -1), default=None
            )
            obj = self._attempt(
                lambda i=i: sel_mgr.GetSelectedObject6(i, -1), default=None
            )
            if obj is None:
                continue
            self._attempt(
                lambda o=obj: _sw_type_info.flag_methods(
                    o, "IFace2", "IEdge", "IVertex"
                ),
                default=0,
            )

            type_name = _SELECTED_ENTITY_TYPE_NAMES.get(
                int(raw_type) if isinstance(raw_type, (int, float)) else -1,
                f"UNKNOWN({raw_type})",
            )
            entry: dict[str, Any] = {"index": i, "type": type_name}

            if type_name == "FACE":
                box = self._attempt(lambda o=obj: o.GetBox(), default=None)
                if box and len(box) == 6:
                    entry["bounding_box_mm"] = {
                        "min": [c * 1000 for c in box[0:3]],
                        "max": [c * 1000 for c in box[3:6]],
                    }
                    entry["center_mm"] = [
                        (box[j] + box[j + 3]) / 2 * 1000 for j in range(3)
                    ]
                area = self._attempt(lambda o=obj: o.GetArea(), default=None)
                if area is not None:
                    entry["area_mm2"] = area * 1e6
                normal = self._attempt(lambda o=obj: o.Normal, default=None)
                if normal:
                    entry["normal"] = [round(float(n), 6) for n in normal]

            elif type_name == "EDGE":
                # Shared with select_face_edges: start/end/length plus
                # curve kind and, for circles, centre/axis/radius.
                entry.update(describe_edge(self, obj))

            elif type_name == "VERTEX":
                point = self._attempt(lambda o=obj: o.GetPoint(), default=None)
                if point:
                    entry["point_mm"] = [c * 1000 for c in point]

            results.append(entry)
        return results

    return self._handle_com_operation("get_selected_entities", _describe)
select_face_edges async
select_face_edges(point: list[float] | None = None, side: str | None = None, plane: str | None = None, edge_type: str = 'all', append: bool = False) -> AdapterResult[dict[str, Any]]

Multi-select every edge bounding one face of the active part.

See SolidWorksAdapter.select_face_edges for the contract; the face resolution and selection live in face_selection.FaceEdgeSelector.

Source code in src/solidworks_mcp/adapters/solidworks/selection.py
async def select_face_edges(
    self,
    point: list[float] | None = None,
    side: str | None = None,
    plane: str | None = None,
    edge_type: str = "all",
    append: bool = False,
) -> AdapterResult[dict[str, Any]]:
    """Multi-select every edge bounding one face of the active part.

    See ``SolidWorksAdapter.select_face_edges`` for the contract; the face
    resolution and selection live in ``face_selection.FaceEdgeSelector``.
    """
    problem = validate_face_query(point, side, plane, edge_type)
    if problem:
        return AdapterResult(status=AdapterResultStatus.ERROR, error=problem)
    if not self.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )
    from .io import _doc_type

    doc_type = _doc_type(self)
    if doc_type is not None and doc_type != 1:
        return AdapterResult(
            status=AdapterResultStatus.ERROR,
            error=(
                "select_face_edges requires an active part document "
                f"(active document type is {doc_type})."
            ),
        )

    def _select_edges() -> dict[str, Any]:
        selector = FaceEdgeSelector(self)
        faces = selector.resolve(point, side, plane)
        edges = selector.edges(faces, edge_type)
        if not edges:
            kind = "" if edge_type == "all" else f"{edge_type} "
            raise LookupError(
                f"The selected face has no {kind}edges "
                f"(face centre {faces[0].describe()['center_mm']} mm)."
            )
        count = selector.select(edges, append)
        described = [f.describe() for f in faces]
        return {
            "face": described[0],
            "faces": described,
            "edge_type": edge_type,
            "edges": [info for _edge, info in edges],
            "selected_edges": len(edges),
            "selection_count": count,
        }

    return self._run_clean("select_face_edges", _select_edges)
select_feature async
select_feature(feature_name: str, append: bool = False) -> AdapterResult[dict[str, Any]]

Select a named feature, sketch, plane or component.

Parameters:

Name Type Description Default
feature_name str

Feature name, optionally @document qualified.

required
append bool

Add to the current selection instead of replacing it.

False

Returns:

Type Description
AdapterResult[dict[str, Any]]

AdapterResult[dict[str, Any]]: The selector payload plus

AdapterResult[dict[str, Any]]

selection_count. ERROR - never SUCCESS with

AdapterResult[dict[str, Any]]

selected: False - when no strategy could select the name.

Source code in src/solidworks_mcp/adapters/solidworks/selection.py
async def select_feature(
    self, feature_name: str, append: bool = False
) -> AdapterResult[dict[str, Any]]:
    """Select a named feature, sketch, plane or component.

    Args:
        feature_name: Feature name, optionally ``@document`` qualified.
        append: Add to the current selection instead of replacing it.

    Returns:
        AdapterResult[dict[str, Any]]: The selector payload plus
        ``selection_count``. ERROR - never SUCCESS with
        ``selected: False`` - when no strategy could select the name.
    """
    if not self.currentModel:
        return AdapterResult(
            status=AdapterResultStatus.ERROR, error="No active model"
        )

    def _select() -> dict[str, Any]:
        payload = self._feature_selector.select_feature(feature_name, append)
        if not payload or not payload.get("selected"):
            raise LookupError(
                f"Feature not found or could not be selected: {feature_name}"
            )
        payload["selection_count"] = selection_count(self, self.currentModel)
        return payload

    return self._run_clean("select_feature", _select)

SolidWorksSketchMixin

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