Skip to content

solidworks_mcp.tools

solidworks_mcp.tools

Tools for SolidWorks MCP Server.

This module provides all the MCP tools for SolidWorks automation, organized by category.

Attributes

__all__ module-attribute

__all__ = ['register_tools']

Functions:

register_analysis_tools async

register_analysis_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register analysis tools with FastMCP.

Registers comprehensive analysis tools for SolidWorks model evaluation including mass properties, interference checking, geometry analysis, and material properties. These tools provide critical engineering data for design validation and optimization.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config dict[str, Any]

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Example
from solidworks_mcp.tools.analysis import register_analysis_tools

tool_count = await register_analysis_tools(mcp, adapter, config)
print(f"Registered {tool_count} analysis tools")
            Note:
                Analysis tools require an active SolidWorks document with geometry.
                Some analyses may require specific material assignments for accurate results.
Source code in src/solidworks_mcp/tools/analysis.py
async def register_analysis_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register analysis tools with FastMCP.

    Registers comprehensive analysis tools for SolidWorks model evaluation including mass
    properties, interference checking, geometry analysis, and material properties. These
    tools provide critical engineering data for design validation and optimization.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (dict[str, Any]): Configuration values for the operation.

    Returns:
        int: The computed numeric result.

    Example:
                        ```python
                        from solidworks_mcp.tools.analysis import register_analysis_tools

                        tool_count = await register_analysis_tools(mcp, adapter, config)
                        print(f"Registered {tool_count} analysis tools")
                        ```

                    Note:
                        Analysis tools require an active SolidWorks document with geometry.
                        Some analyses may require specific material assignments for accurate results.
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0

    @mcp.tool()
    async def calculate_mass_properties(
        input_data: MassPropertiesInput,
    ) -> dict[str, Any]:
        """Measure the active part or assembly's mass properties: volume, surface area, mass, centre of mass and moments of inertia.

        Use it to check weight or balance. To compare a model with the values you
        expect, use `verify_model`; `get_mass_properties` does the same as this tool.
        Mass depends on the assigned material (`set_material`). Needs: an open part or
        assembly. Read-only. It rebuilds first. Returns: `mass_properties` with `volume`
        (mm³), `surface_area` (mm²), `mass` (kg), `center_of_mass` (mm),
        `moments_of_inertia` (kg·mm², about the centre of mass) and
        `principal_axes`.

        Args:
            input_data: Nothing: it always measures the active model.

        Returns:
            dict[str, Any]: The mass properties.
        """
        result = await adapter.get_mass_properties()

        require_success(result, context="Failed to calculate mass properties")
        props = result.data
        assert props is not None
        return {
            "status": "success",
            "message": "Mass properties calculated successfully",
            "mass_properties": {
                "volume": {"value": props.volume, "units": "mm³"},
                "surface_area": {"value": props.surface_area, "units": "mm²"},
                "mass": {"value": props.mass, "units": "kg"},
                "center_of_mass": {
                    "x": props.center_of_mass[0],
                    "y": props.center_of_mass[1],
                    "z": props.center_of_mass[2],
                    "units": "mm",
                },
                "moments_of_inertia": {
                    **{
                        key: _kg_mm2(props.moments_of_inertia[key])
                        for key in ("Ixx", "Iyy", "Izz", "Ixy", "Ixz", "Iyz")
                    },
                    "units": "kg·mm²",
                },
                "principal_axes": props.principal_axes,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def get_mass_properties(
        input_data: MassPropertiesInput | None = None,
    ) -> dict[str, Any]:
        """Measure the active part or assembly's mass properties: volume, surface area, mass, centre of mass and moments of inertia.

        The same as `calculate_mass_properties`, which it calls. To compare a model with
        the values you expect, use `verify_model`. Mass depends on the assigned material
        (`set_material`). Needs: an open part or assembly. Read-only. It rebuilds first.
        Returns: `mass_properties` with `volume` (mm³), `surface_area` (mm²), `mass`
        (kg), `center_of_mass` (mm), `moments_of_inertia` (kg·mm², about the centre
        of mass) and `principal_axes`.

        Args:
            input_data: Optional, and empty: it always measures the active model.

        Returns:
            dict[str, Any]: The mass properties.
        """
        return await calculate_mass_properties(input_data or MassPropertiesInput())

    @mcp.tool()
    async def check_interference(input_data: InterferenceCheckInput) -> dict[str, Any]:
        """Find where the components of the active assembly overlap (Tools > Interference Detection).

        Use it after inserting and mating components. A count of 0 is a real
        measurement. To fail a build on interference, use `verify_model` with
        `no_interference`. Needs: an open, active assembly. Read-only. Returns: `data`
        with `interference_found`, `interference_count`, `interferences` (component
        pairs and overlap volume in mm³) and `scope`.

        Args:
            input_data: Nothing is required. `components` limits the report to
                interferences involving those components; `include_hidden` also
                checks hidden components, which are left out by default.

        Returns:
            dict[str, Any]: The interferences found.
        """
        require_capability(
            adapter,
            "check_interference",
            "Active adapter does not support check_interference; no "
            "interference analysis was performed. Use SolidWorks "
            "directly (Tools > Interference Detection) to check this "
            "assembly.",
        )
        options = input_data.model_dump()
        options["ignore_hidden"] = not input_data.include_hidden
        result = await adapter.check_interference(options)
        require_success(result, "Interference check failed")
        return {
            "status": "success",
            "message": "Interference check completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def analyze_geometry(input_data: GeometryAnalysisInput) -> dict[str, Any]:
        """Run a geometry analysis on the active model, such as curvature, draft or wall thickness.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. For distances
        and angles today, use `measure_entities`; for size and volume, use
        `verify_model` or `calculate_mass_properties`. Read-only. Returns: once
        available, the analysis under `data`.

        Args:
            input_data: The analysis. `analysis_type` (e.g. curvature, draft,
                thickness) is required; `parameters` has a default.

        Returns:
            dict[str, Any]: The analysis, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "analyze_geometry",
            f"Active adapter does not support analyze_geometry; no "
            f"{input_data.analysis_type} analysis was performed.",
        )
        result = await adapter.analyze_geometry(input_data.model_dump())
        require_success(result, "Failed to analyze geometry")
        return {
            "status": "success",
            "message": f"Geometry analysis ({input_data.analysis_type}) completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def get_material_properties() -> dict[str, Any]:
        """Report the physical properties of the active part's material: density, modulus, Poisson's ratio, strength.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. For the
        material's name today, use `get_material`; for the materials you can assign, use
        `list_available_materials`. Read-only. Returns: once available, the properties
        under `data`.
        """
        require_capability(
            adapter,
            "get_material_properties",
            "Active adapter does not support get_material_properties",
        )
        result = await adapter.get_material_properties()
        require_success(result, "Failed to get material properties")
        return {
            "status": "success",
            "message": "Material properties retrieved successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def verify_model(
        input_data: VerifyModelInput | dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Check the active part or assembly against what you meant to build, one pass/fail check per expected value.

        Use it as the last step of a build: a modelling tool's success doesn't prove the
        geometry is right. Give only the values you know. Units: mm, mm³, kg;
        `bounding_box_mm` is [x, y, z] in model axes. Widen `relative_tolerance` when
        your sums ignore fillets. Needs: an open part or assembly. Read-only. Returns:
        `verdict` (pass, fail, inconclusive or report_only), `checks` (each `expected`,
        `actual`, `tolerance`, `passed`; null when unmeasured) and `measured`. A
        mismatch is still `status: success`.

        Args:
            input_data: The expectations, all optional: `mass_kg`, `volume_mm3`,
                `bounding_box_mm`, `feature_count`, `solid_body_count`, and for an
                assembly `component_count`, `mate_count` and `no_interference`;
                `relative_tolerance` (0.01) and `length_tolerance_mm` (0.1) have
                defaults. With none it only reports what it measured.

        Returns:
            dict[str, Any]: The verdict and checks.
        """
        expectations = normalize_input(input_data, VerifyModelInput)
        require_capability(
            adapter,
            "get_model_geometry_summary",
            "Active adapter cannot read the model back, so nothing was verified.",
        )
        summary_result = await adapter.get_model_geometry_summary()
        require_success(summary_result, context="Failed to read the model back")
        summary = dict(summary_result.data or {})
        is_assembly = summary.get("document_type") == "assembly"

        if not is_assembly:
            misplaced = [
                name
                for name in _ASSEMBLY_ONLY
                if getattr(expectations, name) is not None
            ]
            if misplaced:
                raise InvalidInputError(
                    f"{', '.join(misplaced)} only apply to assemblies; the active "
                    "document is a part"
                )

        measured: dict[str, Any] = dict(summary)
        checks: list[dict[str, Any]] = []
        rel = expectations.relative_tolerance

        if expectations.mass_kg is not None:
            mass_result = await adapter.get_mass_properties()
            mass = (
                mass_result.data.mass
                if mass_result.is_success and mass_result.data
                else None
            )
            material_result = await adapter.get_material()
            material = (
                (material_result.data or {}).get("material")
                if material_result.is_success
                else None
            )
            measured["mass_kg"] = mass
            measured["material"] = material
            check = _relative_check("mass_kg", expectations.mass_kg, mass, rel, "kg")
            if check["passed"] is False:
                check["note"] = (
                    f"Material: {material}"
                    if material
                    else "No material assigned; SolidWorks used a default density"
                )
            checks.append(check)

        if expectations.volume_mm3 is not None:
            checks.append(
                _relative_check(
                    "volume_mm3",
                    expectations.volume_mm3,
                    summary.get("volume_mm3"),
                    rel,
                    "mm³",
                )
            )

        if expectations.bounding_box_mm is not None:
            checks.append(
                _box_check(
                    expectations.bounding_box_mm,
                    summary.get("bounding_box_mm"),
                    expectations.length_tolerance_mm,
                )
            )

        if expectations.feature_count is not None:
            features_result = await adapter.list_features()
            names = (
                _count_modelling_features(features_result.data or [])
                if features_result.is_success
                else None
            )
            measured["features"] = names
            checks.append(
                _exact_check(
                    "feature_count",
                    expectations.feature_count,
                    None if names is None else len(names),
                    "features",
                )
            )

        for name, unit in (
            ("solid_body_count", "bodies"),
            ("component_count", "components"),
            ("mate_count", "mates"),
        ):
            expected = getattr(expectations, name)
            if expected is not None:
                checks.append(_exact_check(name, expected, summary.get(name), unit))

        if expectations.no_interference is not None:
            interference = await adapter.check_interference({})
            found = (
                (interference.data or {}).get("interference_found")
                if interference.is_success
                else None
            )
            measured["interference"] = (
                interference.data if interference.is_success else None
            )
            clear = None if found is None else not found
            checks.append(
                _check(
                    "no_interference",
                    expectations.no_interference,
                    clear,
                    None,
                    None if clear is None else clear == expectations.no_interference,
                    note=None if interference.is_success else interference.error,
                )
            )

        verdict = _verdict(checks)
        return {
            "status": "success",
            "message": _verdict_message(
                verdict, checks, str(summary.get("document_type") or "document")
            ),
            "verdict": verdict,
            "checks": checks,
            "measured": measured,
            "execution_time": summary_result.execution_time,
        }

    # Future analysis tools:
    # - perform_fea_analysis (if FEA capabilities are available)
    # - analyze_flow (computational fluid dynamics)
    # - thermal_analysis
    # - vibration_analysis
    # - stress_concentration_analysis

    tool_count = 4  # Keep legacy reported count expected by tests
    return tool_count

register_automation_tools async

register_automation_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register automation tools with FastMCP.

Registers comprehensive automation tools for SolidWorks workflow orchestration including VBA generation, macro recording, batch processing, and template management.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config dict[str, Any]

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Source code in src/solidworks_mcp/tools/automation.py
async def register_automation_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register automation tools with FastMCP.

    Registers comprehensive automation tools for SolidWorks workflow orchestration including
    VBA generation, macro recording, batch processing, and template management.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (dict[str, Any]): Configuration values for the operation.

    Returns:
        int: The computed numeric result.
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0

    @mcp.tool()
    async def generate_vba_code(input_data: GenerateVBAInput) -> dict[str, Any]:
        """Write a VBA macro skeleton for an operation you describe: the SolidWorks connection, a check of the active document's type and optional error handling, with the operation itself left as a TODO.

        Use it as a starting point for a custom macro. For a complete macro for a
        specific feature, use a `generate_vba_*` tool (extrusion, revolve, mates,
        drawing views, batch export). The macro reports through `Debug.Print`, never a
        dialog, so it runs without waiting on anyone: save it as a .swb file and run it
        with `execute_macro` (its `Sub main()` calls the Sub named `sub_name`; SolidWorks
        runs a .swb macro only through `main`). It stops, printing why,
        when the active document isn't `target_document`. Read-only. Nothing runs in
        SolidWorks. Returns: `vba_code` with `code`, `sub_name`, `operation`,
        `target_document`, `lines_of_code` and `includes_error_handling`.

        Args:
            input_data: The macro to write. Nothing is required: give
                `operation_description` (it also names the Sub) and
                `target_document` (Part, Assembly or Drawing);
                `include_error_handling` is on by default.

        Returns:
            dict[str, Any]: The macro text.
        """
        description = input_data.operation_description or ""
        sub_name = _vba_sub_name(description)
        code = _vba_skeleton(
            sub_name,
            description,
            input_data.target_document,
            input_data.include_error_handling,
        )
        return {
            "status": "success",
            "message": f"Generated VBA code for: {description}",
            "vba_code": {
                "code": code,
                "sub_name": sub_name,
                "operation": description,
                "target_document": input_data.target_document,
                "lines_of_code": len(code.split("\n")),
                "includes_error_handling": input_data.include_error_handling,
            },
        }

    @mcp.tool(name="automation_start_macro_recording")
    async def start_macro_recording(input_data: RecordMacroInput) -> dict[str, Any]:
        """Start SolidWorks' macro recorder (the automation family's copy of `start_macro_recording`).

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Recording is
        done in SolidWorks today (Tools > Macro > Record). Changes: once available,
        starts a recording session. Returns: once available, the session details under
        `data`.

        Args:
            input_data: The recording. Nothing is required: `macro_name`,
                `output_file`, `recording_mode` and the capture switches have
                defaults.

        Returns:
            dict[str, Any]: The session, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "start_macro_recording",
            "Active adapter does not support start_macro_recording; "
            f"no recording of {input_data.macro_name} was started.",
        )
        result = await adapter.start_macro_recording(input_data.model_dump())
        require_success(result, "Failed to start macro recording")
        return {
            "status": "success",
            "message": f"Started recording macro: {input_data.macro_name}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool(name="automation_stop_macro_recording")
    async def stop_macro_recording(input_data: dict[str, Any]) -> dict[str, Any]:
        """Stop a macro recording and save it (the automation family's copy of `stop_macro_recording`).

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Recording is
        done in SolidWorks today (Tools > Macro > Stop). Changes: once available, ends
        the session and writes the macro file. Returns: once available, the saved
        macro's details.

        Args:
            input_data: A mapping; nothing in it is used yet.

        Returns:
            dict[str, Any]: A `not_supported` error.
        """
        raise UnsupportedOperationError(
            "No adapter capability exists for stopping a macro "
            "recording; this tool never started a real recording "
            "session, so there is nothing to stop or save."
        )

    @mcp.tool()
    async def batch_process_files(input_data: BatchProcessInput) -> dict[str, Any]:
        """Run one operation (rebuild, save as, export or property update) on every matching file in a folder.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To export
        several files today, open each with `open_model` and use `export_step`.
        Changes: once available, opens, changes and saves each file. Returns: once available, a per-file result under `data`.

        Args:
            input_data: The batch. `source_directory` is required; `operation_type`,
                `file_pattern`, `target_format` and `recursive` have defaults.

        Returns:
            dict[str, Any]: The results, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "batch_process_files",
            "Active adapter does not support batch_process_files; no "
            f"files in {input_data.source_directory} were processed.",
        )
        result = await adapter.batch_process_files(input_data.model_dump())
        require_success(result, "Batch processing failed")
        return {
            "status": "success",
            "message": f"Batch {input_data.operation_type} completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def manage_design_table(input_data: DesignTableInput) -> dict[str, Any]:
        """Create, read or update a design table (an Excel sheet that drives a model's configurations).

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To list a
        model's configurations today, use `list_configurations`. Changes: once
        available, inserts or edits the design table and its configurations. Returns:
        once available, the table's state under `data`.

        Args:
            input_data: The design table. Nothing is required: give `model_path`,
                `operation` and `excel_file`; `parameters`, `configurations` and the
                auto-update switches have defaults.

        Returns:
            dict[str, Any]: The result, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "manage_design_table",
            "Active adapter does not support manage_design_table; no "
            f"{input_data.table_type} operation was performed.",
        )
        result = await adapter.manage_design_table(input_data.model_dump())
        require_success(result, "Design table management failed")
        return {
            "status": "success",
            "message": f"Design table {input_data.table_type} completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def execute_workflow(input_data: WorkflowInput) -> dict[str, Any]:
        """Run a named sequence of tool steps, in order or in parallel, with an error policy.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Call the
        tools one at a time instead. Changes: once available, whatever the steps change.
        Returns: once available, each step's result under `data`.

        Args:
            input_data: The workflow. `workflow_name` and `steps` (a list of step
                mappings) are required; `parallel_execution` and `error_handling`
                have defaults.

        Returns:
            dict[str, Any]: The step results, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "execute_workflow",
            "Active adapter does not support execute_workflow; "
            f"workflow '{input_data.workflow_name}' was not run.",
        )
        result = await adapter.execute_workflow(input_data.model_dump())
        require_success(result, "Workflow execution failed")
        return {
            "status": "success",
            "message": f"Workflow '{input_data.workflow_name}' completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_template(input_data: TemplateInput) -> dict[str, Any]:
        """Create a part, assembly or drawing template file (.prtdot, .asmdot, .drwdot) with standard settings.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To capture an
        existing model's settings, `extract_template` is also not available yet.
        Changes: once available, writes the template file. Returns: once available, the
        template's path and settings under `data`.

        Args:
            input_data: The template. `template_type` and `template_name` are
                required; `source_model`, `output_path` and the include switches have
                defaults.

        Returns:
            dict[str, Any]: The template, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "create_template",
            "Active adapter does not support create_template; no "
            f"{input_data.template_type} template "
            f"'{input_data.template_name}' was created.",
        )
        result = await adapter.create_template(input_data.model_dump())
        require_success(result, "Template creation failed")
        return {
            "status": "success",
            "message": f"Created {input_data.template_type} template: {input_data.template_name}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def optimize_performance(input_data: dict[str, Any]) -> dict[str, Any]:
        """Review SolidWorks' performance settings and apply faster ones.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Changes: once
        available, SolidWorks system options. Returns: once available, the settings
        changed under `data`.

        Args:
            input_data: A mapping of optimisation options; none is required.

        Returns:
            dict[str, Any]: The changes, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "optimize_performance",
            "Active adapter does not support optimize_performance; "
            "no settings were analyzed or changed.",
        )
        result = await adapter.optimize_performance(input_data)
        require_success(result, "Performance optimization failed")
        return {
            "status": "success",
            "message": "Performance optimization completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    tool_count = 8  # Number of tools registered
    return tool_count

register_docs_discovery_tools async

register_docs_discovery_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register the docs discovery and API lookup tools with FastMCP.

None of them use the adapter or SolidWorks itself: they read the installed type library.

Parameters:

Name Type Description Default
mcp FastMCP

The server to register on.

required
adapter SolidWorksAdapter

Unused; kept for the registrar's uniform signature.

required
config dict[str, Any]

Server configuration (solidworks_year, the index refresh settings).

required

Returns:

Name Type Description
int int

The number of tools registered.

Source code in src/solidworks_mcp/tools/docs_discovery.py
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
async def register_docs_discovery_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register the docs discovery and API lookup tools with FastMCP.

    None of them use the adapter or SolidWorks itself: they read the
    installed type library.

    Args:
        mcp (FastMCP): The server to register on.
        adapter (SolidWorksAdapter): Unused; kept for the registrar's
            uniform signature.
        config (dict[str, Any]): Server configuration (``solidworks_year``,
            the index refresh settings).

    Returns:
        int: The number of tools registered.
    """
    mcp = with_tool_boundary(mcp)

    @mcp.tool()
    async def discover_solidworks_docs(
        input_data: DiscoverDocsInput | None = None,
    ) -> dict[str, Any]:
        """Rebuild the SolidWorks API index on purpose. The lookups don't need it.

        The lookup tools (lookup_api_method, lookup_api_interface,
        find_related_api_members, search_solidworks_api_help) build the index
        themselves on first use, so call this only to rebuild it, to read a
        specific SolidWorks year, or to save a copy elsewhere. It reads every
        interface in the installed SolidWorks type library (each member's
        parameters, return type and description), plus the VBA library
        references. SolidWorks must be installed, not running; it is never
        started or called. Changes: writes the index file, replacing the
        saved one.

        Returns ``summary`` (interface, method and property counts),
        ``type_library`` (version, year and .tlb path), ``year`` and
        ``output_file``, the saved index (megabytes, so it is not returned).
        Fails with a message naming the installed years when ``year`` isn't
        installed, or saying SolidWorks isn't installed here.

        Example input: ``{"year": 2026}``

        Args:
            input_data (DiscoverDocsInput | None): The rebuild. Nothing is
                required: `year` defaults to the installed release,
                `output_dir` to the index folder, and `include_vba` to on.

        Returns:
            dict[str, Any]: ``summary``, ``type_library``, ``year``,
            ``output_file`` and ``rag_indexed``, plus
            ``status``/``message``/``execution_time``.
        """
        start_time = time.time()
        normalized = normalize_input(input_data, DiscoverDocsInput)
        output_dir = Path(normalized.output_dir) if normalized.output_dir else None
        year = _resolve_solidworks_year(normalized.year, config)

        logger.info("Starting SolidWorks documentation discovery...")
        discovery, output_file = _build_index(
            year,
            strict=normalized.year is not None,
            output_dir=output_dir,
            include_vba=normalized.include_vba,
        )
        index = discovery.index
        summary = discovery.create_search_summary()

        return {
            "status": "success",
            "message": f"Discovered {summary['total_com_objects']} COM objects, "
            f"{summary['total_methods']} methods, "
            f"{summary['total_properties']} properties",
            "type_library": index["type_library"],
            "summary": summary,
            "year": index["type_library"]["year"],
            "output_file": str(output_file),
            "rag_indexed": _rebuild_rag_index(output_file),
            "execution_time": time.time() - start_time,
        }

    @mcp.tool()
    async def search_solidworks_api_help(
        input_data: SearchApiHelpInput | None = None,
    ) -> dict[str, Any]:
        """Search SolidWorks API interface and member names by keyword.

        Use this when you don't know the exact name: "chamfer" finds
        IFeatureManager.InsertFeatureChamfer. Then call lookup_api_method on
        a match for its parameters and return type. Matches come from the
        SolidWorks type library installed on this machine.

        Setup: none. SolidWorks must be installed, not running; it is never
        contacted. Read-only. The first call builds the index (about a
        second) and later calls reuse it. ``index_status`` is ``ready``
        (saved index), ``rebuilt`` (built for this call) or ``unavailable``
        (no index could be built; ``guidance`` says why).

        Returns ``matches`` (interface, member, member_type, score),
        ``guidance``, and ``fallback_help`` suggesting MCP tools for the
        task, with ``year`` and ``type_library_version`` of the library that
        answered.

        Example input: ``{"query": "chamfer", "max_results": 5}``

        Args:
            input_data (SearchApiHelpInput | None): The search. `query`
                (one or more words) is required; `max_results` and `year`
                have defaults.

        Returns:
            dict[str, Any]: ``matches``, ``guidance``, ``fallback_help``,
            ``year``, ``type_library_version``, ``index_status`` and
            ``source_index_file``, plus ``status``/``message``/``execution_time``.
        """
        start_time = time.time()
        normalized = normalize_input(input_data, SearchApiHelpInput)
        source = _index_for_lookup(normalized.year, normalized.index_file, config)

        matches = _search_index(source.index, normalized.query, normalized.max_results)
        guidance_lines = []
        if matches:
            guidance_lines.append(
                f"Found {len(matches)} API matches for '{normalized.query}'."
            )
            guidance_lines.append(
                "Use lookup_api_method on a match for its parameters and return type."
            )
        else:
            guidance_lines.append(
                "No indexed API hits found; using workflow fallback guidance."
            )
        if source.note:
            guidance_lines.append(source.note)

        return {
            "status": "success",
            "message": "SolidWorks API help search completed",
            "query": normalized.query,
            **source.payload(),
            "matches": [match.model_dump(exclude_unset=True) for match in matches],
            "fallback_help": _fallback_help_for_query(normalized.query),
            "guidance": " ".join(guidance_lines),
            "execution_time": time.time() - start_time,
        }

    @mcp.tool()
    async def lookup_api_method(
        input_data: LookupApiMethodInput | None = None,
    ) -> dict[str, Any]:
        """Look up one SolidWorks API method or property: parameters, return type, description.

        Use this before writing or debugging a SolidWorks API call (VBA, a
        macro or COM) to check that the member exists on that interface and
        how to call it. Answers come from the SolidWorks type library
        installed on this machine, so they match the installed version.

        Setup: none. SolidWorks must be installed, not running; it is never
        contacted. Read-only. The first lookup builds the index (about a
        second). ``index_status`` is ``ready``, ``rebuilt`` or
        ``unavailable``.

        Names match regardless of case, as COM does. ``result.member`` is
        SolidWorks' own spelling, with a ``note`` when yours differs. Each
        parameter has a ``type`` and a ``direction``: ``in``, ``out``
        (SolidWorks fills it) or ``in_out`` (pass a variable by reference).
        ``return_type`` is null when the method returns nothing; a property
        has its value type there and an ``access``. ``result.found`` false
        means there is no such member, and ``result.note`` says why,
        including when no index could be built (SolidWorks isn't installed
        here, or the requested year isn't).

        Example input: ``{"interface": "ISldWorks", "method": "OpenDoc6"}``

        Args:
            input_data (LookupApiMethodInput | None): The member to look up.
                `interface` (e.g. "ISldWorks") and `method` (a method or
                property name) are required; `year` has a default.

        Returns:
            dict[str, Any]: ``result``, ``year``, ``type_library_version``,
            ``index_status`` and ``source_index_file``, plus
            ``status``/``message``/``execution_time``.
        """
        start_time = time.time()
        normalized = normalize_input(input_data, LookupApiMethodInput)
        source = _index_for_lookup(normalized.year, normalized.index_file, config)
        result = source.annotate(
            _lookup_method(source.index, normalized.interface, normalized.method)
        )
        return {
            "status": "success",
            "message": (
                "Method found"
                if result.found
                else "No indexed match for that interface/method"
            ),
            **source.payload(),
            "result": result.model_dump(exclude_unset=True),
            "execution_time": time.time() - start_time,
        }

    @mcp.tool()
    async def lookup_api_interface(
        input_data: LookupApiInterfaceInput | None = None,
    ) -> dict[str, Any]:
        """List every method and property of a SolidWorks COM interface.

        Use this to see what an interface offers, such as IFeatureManager,
        or to check which interface a member is really on before calling it.
        For one member's parameters, use lookup_api_method. Names match
        regardless of case.

        Setup: none. SolidWorks must be installed, not running; it is never
        contacted. Read-only. The first call builds the index (about a
        second) and later calls reuse it. ``index_status`` is ``ready``
        (saved index), ``rebuilt`` (built for this call) or ``unavailable``
        (no index could be built).

        Returns ``result`` with ``methods``, ``properties`` and their counts.
        ``result.found`` false means the interface isn't in the type library,
        or no index could be built, in which case ``result.note`` says why.

        Example input: ``{"interface": "IFeatureManager"}``

        Args:
            input_data (LookupApiInterfaceInput | None): The interface.
                `interface` (e.g. "IFeatureManager") is required; `year` has
                a default.

        Returns:
            dict[str, Any]: ``result``, ``year``, ``type_library_version``,
            ``index_status`` and ``source_index_file``, plus
            ``status``/``message``/``execution_time``.
        """
        start_time = time.time()
        normalized = normalize_input(input_data, LookupApiInterfaceInput)
        source = _index_for_lookup(normalized.year, normalized.index_file, config)
        result = _lookup_interface(source.index, normalized.interface)
        source.annotate(result)
        return {
            "status": "success",
            "message": (
                f"{result.method_count} methods, {result.property_count} properties"
                if result.found
                else "Interface not found in the indexed documentation"
            ),
            **source.payload(),
            "result": result.model_dump(exclude_unset=True),
            "execution_time": time.time() - start_time,
        }

    @mcp.tool()
    async def find_related_api_members(
        input_data: FindRelatedApiInput | None = None,
    ) -> dict[str, Any]:
        """Find SolidWorks API members related to an interface or member name.

        For an interface: its own members. For a member: the other members
        of every interface that also has it (often used together), plus
        members whose names contain it. Use it to explore around a call you
        know, such as what sits beside InsertSketch.

        Setup: none. SolidWorks must be installed, not running; it is never
        contacted. Read-only. The first call builds the index (about a
        second) and later calls reuse it. ``index_status`` is ``ready``
        (saved index), ``rebuilt`` (built for this call) or ``unavailable``
        (no index could be built).

        Returns ``result.related``: each member's interface, name, type and
        why it is related. ``result.found`` false means nothing matched, or
        no index could be built, in which case ``result.note`` says why.

        Example input: ``{"name": "InsertSketch", "max_results": 10}``

        Args:
            input_data (FindRelatedApiInput | None): What to explore.
                `name` (an interface or member) is required; `max_results`
                and `year` have defaults.

        Returns:
            dict[str, Any]: ``result``, ``year``, ``type_library_version``,
            ``index_status`` and ``source_index_file``, plus
            ``status``/``message``/``execution_time``.
        """
        start_time = time.time()
        normalized = normalize_input(input_data, FindRelatedApiInput)
        source = _index_for_lookup(normalized.year, normalized.index_file, config)
        result = _find_related_api_members(
            source.index, normalized.name, normalized.max_results
        )
        source.annotate(result)
        return {
            "status": "success",
            "message": (
                f"{len(result.related)} related member(s)"
                if result.found
                else "No related members found"
            ),
            **source.payload(),
            "result": result.model_dump(exclude_unset=True),
            "execution_time": time.time() - start_time,
        }

    # discover_solidworks_docs, search_solidworks_api_help, lookup_api_method,
    # lookup_api_interface, find_related_api_members
    tool_count = 5
    return tool_count

register_drawing_analysis_tools async

register_drawing_analysis_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: Any) -> int

Register advanced drawing analysis tools with FastMCP.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config Any

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Example

tool_count = await register_drawing_analysis_tools(mcp, adapter, config)

Source code in src/solidworks_mcp/tools/drawing_analysis.py
async def register_drawing_analysis_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: Any
) -> int:
    """Register advanced drawing analysis tools with FastMCP.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (Any): Configuration values for the operation.

    Returns:
        int: The computed numeric result.

    Example:
                        >>> tool_count = await register_drawing_analysis_tools(mcp, adapter, config)
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0

    @mcp.tool()
    async def analyze_drawing_comprehensive(
        input_data: DrawingAnalysisInput,
    ) -> dict[str, Any]:
        """Run every drawing check (dimensions, views, annotations, standards) on a drawing file in one call.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To see what a
        drawing holds today, use `list_drawing_views`. Read-only. Returns: once
        available, the combined findings under `data`.

        Args:
            input_data: The drawing to check. `drawing_path` (.slddrw) is required;
                `analysis_type`, `analysis_depth`, `standards_check` and
                `generate_report` have defaults.

        Returns:
            dict[str, Any]: The findings, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "analyze_drawing_comprehensive",
            "Active adapter does not support "
            "analyze_drawing_comprehensive; no analysis of "
            f"{input_data.drawing_path} was performed.",
        )
        result = await adapter.analyze_drawing_comprehensive(input_data.model_dump())
        require_success(result, "Failed to analyze drawing")
        return {
            "status": "success",
            "message": "Comprehensive drawing analysis completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def analyze_drawing_dimensions(
        input_data: DimensionAnalysisInput,
    ) -> dict[str, Any]:
        """Check a drawing's dimensions for consistent precision, tolerance formatting and completeness.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To bring the
        model's dimensions into a drawing, use `insert_model_dimensions`. Read-only.
        Returns: once available, the dimension findings under `data`.

        Args:
            input_data: The drawing to check. `drawing_path` is required; the
                `check_precision`, `check_tolerances` and `check_completeness`
                switches default to on.

        Returns:
            dict[str, Any]: The findings, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "analyze_drawing_dimensions",
            "Active adapter does not support analyze_drawing_dimensions; "
            f"no analysis of {input_data.drawing_path} was performed.",
        )
        result = await adapter.analyze_drawing_dimensions(input_data.model_dump())
        require_success(result, "Failed to analyze dimensions")
        return {
            "status": "success",
            "message": "Dimension analysis completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def analyze_drawing_annotations(
        input_data: AnnotationAnalysisInput,
    ) -> dict[str, Any]:
        """Check a drawing's notes, symbols and text styles for consistency.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Read-only.
        Returns: once available, the annotation findings under `data`.

        Args:
            input_data: The drawing to check. `drawing_path` is required; the
                `check_notes`, `check_symbols`, `check_text_styles` and
                `check_annotations` switches default to on.

        Returns:
            dict[str, Any]: The findings, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "analyze_drawing_annotations",
            "Active adapter does not support analyze_drawing_annotations; "
            f"no analysis of {input_data.drawing_path} was performed.",
        )
        result = await adapter.analyze_drawing_annotations(input_data.model_dump())
        require_success(result, "Failed to analyze annotations")
        return {
            "status": "success",
            "message": "Annotation analysis completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def check_drawing_compliance(
        input_data: ComplianceCheckInput,
    ) -> dict[str, Any]:
        """Check a drawing's title block and sheet format against a drafting standard (ISO, ANSI or DIN).

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. For the same
        check on the active drawing, `check_drawing_standards` is also not available
        yet. Read-only. Returns: once available, the compliance findings under `data`.

        Args:
            input_data: The drawing and standard. `drawing_path` is required;
                `standard` defaults to "ISO", and `check_title_block` and
                `check_sheet_format` default to on.

        Returns:
            dict[str, Any]: The findings, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "check_drawing_compliance",
            "Active adapter does not support check_drawing_compliance; "
            f"no {input_data.standard} compliance check was performed.",
        )
        result = await adapter.check_drawing_compliance(input_data.model_dump())
        require_success(result, "Compliance check failed")
        return {
            "status": "success",
            "message": f"Standards compliance check completed for {input_data.standard}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def analyze_drawing_views(input_data: dict[str, Any]) -> dict[str, Any]:
        """Check how a drawing's views are chosen and laid out.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To list the
        views on the active drawing today, use `list_drawing_views`. Read-only. Returns:
        once available, the view findings under `data`.

        Args:
            input_data: A mapping with the `drawing_path` of the drawing to check.

        Returns:
            dict[str, Any]: The findings, or a `not_supported` error.
        """
        if hasattr(adapter, "analyze_drawing_views"):
            result = await adapter.analyze_drawing_views(input_data)
            require_success(result, "Failed to analyze drawing views")
            return {
                "status": "success",
                "message": "Drawing view analysis completed",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        drawing_path = input_data.get("drawing_path", "")

        raise UnsupportedOperationError(
            "Active adapter does not support analyze_drawing_views; "
            f"no analysis of {drawing_path} was performed."
        )

    @mcp.tool()
    async def generate_drawing_report(input_data: dict[str, Any]) -> dict[str, Any]:
        """Write a quality report that combines every drawing check.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Read-only.
        Returns: once available, the report under `data`.

        Args:
            input_data: A mapping with the `drawing_path` of the drawing to report
                on.

        Returns:
            dict[str, Any]: The report, or a `not_supported` error.
        """
        if hasattr(adapter, "generate_drawing_report"):
            result = await adapter.generate_drawing_report(input_data)
            require_success(result, "Failed to generate drawing report")
            return {
                "status": "success",
                "message": "Drawing quality report generated successfully",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        drawing_path = input_data.get("drawing_path", "")

        raise UnsupportedOperationError(
            "Active adapter does not support generate_drawing_report; "
            f"no report for {drawing_path} was generated."
        )

    @mcp.tool()
    async def compare_drawing_versions(
        input_data: DrawingVersionsInput,
    ) -> dict[str, Any]:
        """Compare two drawing files on disk: whether they are byte-identical, and how their sizes differ.

        Use it to tell whether a drawing changed between two saved versions. It does not
        open SolidWorks, so it can't say which views, dimensions or notes differ.
        Identical files mean nothing changed, but different files may not: SolidWorks
        writes a different file each time it saves, even with no change. Needs:
        both files on disk. Read-only. Returns: `comparison` with `file_1` and `file_2`
        (size, modified time, SHA-256), `identical` and `size_delta_bytes`.

        Args:
            input_data: Both full paths, `drawing_version_1` and
                `drawing_version_2`; both are required.

        Returns:
            dict[str, Any]: The comparison.
        """
        input_data = normalize_input(input_data, DrawingVersionsInput)
        drawing_v1 = input_data.drawing_version_1
        drawing_v2 = input_data.drawing_version_2

        file_1 = file_fact_snapshot(drawing_v1)
        file_2 = file_fact_snapshot(drawing_v2)

        missing = [
            path
            for path, snapshot in ((drawing_v1, file_1), (drawing_v2, file_2))
            if snapshot is None
        ]
        if missing:
            raise ToolOperationError(
                "Cannot compare drawing versions; file(s) not found "
                f"on disk: {', '.join(missing)}. No SolidWorks-level "
                "diff was performed."
            )

        assert file_1 is not None
        assert file_2 is not None
        identical = file_1["sha256"] == file_2["sha256"]

        return {
            "status": "success",
            "message": (
                "Drawing files are byte-identical"
                if identical
                else "Drawing files differ on disk"
            ),
            "comparison": {
                "file_1": file_1,
                "file_2": file_2,
                "identical": identical,
                "size_delta_bytes": file_2["size_bytes"] - file_1["size_bytes"],
            },
            "note": (
                "This is a filesystem-level comparison only; no "
                "SolidWorks session was opened, so geometric, "
                "dimension, and annotation differences were not "
                "analyzed."
            ),
        }

    @mcp.tool()
    async def validate_drawing_completeness(
        input_data: dict[str, Any],
    ) -> dict[str, Any]:
        """Check that a drawing carries everything needed to manufacture the part.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Read-only.
        Returns: once available, the missing items under `data`.

        Args:
            input_data: A mapping with the `drawing_path` of the drawing to check.

        Returns:
            dict[str, Any]: The findings, or a `not_supported` error.
        """
        drawing_path = input_data.get("drawing_path", "")

        require_capability(
            adapter,
            "validate_drawing_completeness",
            "Active adapter does not support "
            "validate_drawing_completeness; no validation of "
            f"{drawing_path} was performed.",
        )
        result = await adapter.validate_drawing_completeness(input_data)
        require_success(result, "Failed to validate completeness")
        return {
            "status": "success",
            "message": "Drawing completeness validation completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    tool_count = 8  # Legacy count expected by tests
    return tool_count

register_drawing_automation_tools async

register_drawing_automation_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register drawing-automation tools with FastMCP.

Parameters:

Name Type Description Default
mcp FastMCP

The MCP server.

required
adapter SolidWorksAdapter

Adapter used by every tool.

required
config dict[str, Any]

Server configuration (unused).

required

Returns:

Name Type Description
int int

Number of tools registered.

Source code in src/solidworks_mcp/tools/drawing_automation.py
async def register_drawing_automation_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register drawing-automation tools with FastMCP.

    Args:
        mcp (FastMCP): The MCP server.
        adapter (SolidWorksAdapter): Adapter used by every tool.
        config (dict[str, Any]): Server configuration (unused).

    Returns:
        int: Number of tools registered.
    """

    mcp = with_tool_boundary(mcp)

    def _supports(method: str) -> None:
        require_capability(
            adapter,
            method,
            f"The active adapter does not support {method}; nothing was done.",
        )

    @mcp.tool()
    async def insert_model_dimensions(
        input_data: InsertModelDimensionsInput | None = None,
    ) -> dict[str, Any]:
        """Bring the model's dimensions into the active drawing's views, each dimension shown once.

        Imports the model's "marked for drawing" dimensions view by view; a repeat in a
        later view is deleted. Pictorial views (isometric, dimetric, trimetric) are
        skipped unless named. Falls back to SolidWorks AutoDimension only when nothing
        could be imported. SolidWorks places them inside the views, so run
        `arrange_dimensions` next. Needs: an open, active drawing with views. Changes:
        adds dimensions to the views. Returns: `data` with per-view `views`
        (before/after counts, `duplicates_removed`), `dimensions_added`,
        `unique_dimensions`, `skipped_pictorial_views` and the `method` used. An unknown
        view is `not_found`.

        Args:
            input_data: Which views. Nothing is required: give `view_name` for one
                view, or omit it for every view; `include_hole_wizard` has a default.

        Returns:
            dict[str, Any]: The dimensions added.
        """
        input_data = normalize_input(input_data, InsertModelDimensionsInput)
        _supports("insert_model_dimensions")
        result = await adapter.insert_model_dimensions(
            input_data.view_name, input_data.include_hole_wizard
        )
        require_success(result, context="Failed to insert model dimensions")
        data = result.data or {}
        return {
            "status": "success",
            "message": (
                f"Added {data.get('dimensions_added', 0)} dimension(s) to "
                f"{len(data.get('views', []))} view(s) ({data.get('method')})"
            ),
            "data": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def arrange_dimensions(
        input_data: ArrangeDimensionsInput | None = None,
    ) -> dict[str, Any]:
        """Move drawing dimensions out of their views into tidy rows so nothing overlaps, then check the sheet.

        Rows start 8 mm outside each view, 7 mm apart, shortest span nearest the view. A
        dimension with no room stays put and is listed in `unplaced`. The check reads
        back every text box and line: no text on text, no line through text or across a
        view, nothing off the sheet. Use it after `insert_model_dimensions`. Needs: an
        open, active drawing. Changes: moves dimensions on the sheet. Returns: `data`
        with `dimensions`, `moved`, `overlaps_before`, `overlaps_after` and `unplaced`.
        If any overlap remains the call is an error whose `data` lists
        `remaining_overlaps`; try a smaller sheet scale (`set_sheet_scale`).

        Args:
            input_data: Which views. Nothing is required: give `view_name` for one
                view's dimensions, or omit it for every view.

        Returns:
            dict[str, Any]: The arrangement and its check.
        """
        input_data = normalize_input(input_data, ArrangeDimensionsInput)
        _supports("arrange_dimensions")
        result = await adapter.arrange_dimensions(input_data.view_name)
        require_success(result, context="Failed to arrange dimensions", with_data=True)
        data = result.data or {}
        return {
            "status": "success",
            "message": (
                f"Arranged {data.get('dimensions', 0)} dimension(s): "
                f"{data.get('overlaps_before', 0)} overlap(s) before, "
                f"{data.get('overlaps_after', 0)} after"
            ),
            "data": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def insert_bom_table(
        input_data: InsertBomTableInput | None = None,
    ) -> dict[str, Any]:
        """Insert a bill-of-materials table for an assembly view on the active drawing.

        Use it on an assembly drawing after placing a view of the assembly (see
        `list_drawing_views` for view names). Needs: an open, active drawing with a view
        of an assembly. Changes: adds a BOM table to the sheet. Returns: `data` with
        `view`, `bom_type`, `template` and `row_count`. An unknown view is `not_found`
        and the message lists the views.

        Args:
            input_data: The table. `view_name` (a view of an assembly) is
                required; `bom_type` ("top_level_only", "parts_only" or "indented"),
                `template_path` and the position `x_mm`/`y_mm` (the sheet's BOM
                anchor when omitted) have defaults.

        Returns:
            dict[str, Any]: The table inserted.
        """
        _supports("insert_bom_table")
        input_data = normalize_input(input_data, InsertBomTableInput)
        result = await adapter.insert_bom_table(
            input_data.view_name,
            input_data.template_path,
            input_data.bom_type,
            input_data.x_mm,
            input_data.y_mm,
        )
        require_success(result, context="Failed to insert the BOM table")
        return {
            "status": "success",
            "message": f"Inserted a {input_data.bom_type} BOM on {input_data.view_name}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_gtol(input_data: AddGtolInput | None = None) -> dict[str, Any]:
        """Add a GD&T feature control frame to the active drawing, e.g. position Ø0.2 (M) to A|B|C.

        Attach it to a drawing dimension (`dimension_view` + `dimension_name`; the frame
        sits under the dimension's text, the usual place for a hole's position
        tolerance), or place it at `x_mm`/`y_mm` with an optional leader to the edge at
        `attach_x_mm`/`attach_y_mm`. Add the datums it refers to with `add_datum_tag`.
        Needs: an open, active drawing. Changes: adds the frame. Returns: `data`, the
        frame as read back from SolidWorks. No edge at the attach point is `not_found`.

        Args:
            input_data: The frame. `characteristic` (position, flatness,
                perpendicularity, parallelism, profile_surface or concentricity) and
                `tolerance` are required, plus either the dimension or the sheet
                position; `diameter_zone`, `material_condition` (none, mmc, lmc) and
                up to three `datums` (primary first) have defaults.

        Returns:
            dict[str, Any]: The frame added.
        """
        _supports("add_gtol")
        input_data = normalize_input(input_data, AddGtolInput)
        attach = input_data.attach_point()
        result = await adapter.add_gtol(
            input_data.characteristic,
            input_data.tolerance,
            input_data.x_mm,
            input_data.y_mm,
            input_data.diameter_zone,
            input_data.material_condition,
            input_data.datums,
            attach,
            input_data.attach_dimension(),
        )
        require_success(result, context="Failed to add the feature control frame")
        return {
            "status": "success",
            "message": (
                f"Added a {input_data.characteristic} frame "
                f"({input_data.tolerance:g}"
                + (f" to {'|'.join(input_data.datums)}" if input_data.datums else "")
                + ")"
            ),
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_datum_tag(
        input_data: AddDatumTagInput | None = None,
    ) -> dict[str, Any]:
        """Add a datum feature symbol (A, B, C, ...) on an edge of the active drawing.

        Use it to mark the datums a feature control frame from `add_gtol` refers to.
        Needs: an open, active drawing with an edge at the attach point. Changes: adds
        the datum tag. Returns: `data` with the `label` as read back. No edge at the
        point is `not_found`.

        Args:
            input_data: The tag. `label` (one letter), `attach_x_mm`/`attach_y_mm`
                (a point on the edge it marks) and `x_mm`/`y_mm` (where the tag
                goes) are all required, in mm on the sheet.

        Returns:
            dict[str, Any]: The tag added.
        """
        _supports("add_datum_tag")
        input_data = normalize_input(input_data, AddDatumTagInput)
        result = await adapter.add_datum_tag(
            input_data.label,
            (input_data.attach_x_mm, input_data.attach_y_mm),
            input_data.x_mm,
            input_data.y_mm,
        )
        require_success(result, context="Failed to add the datum tag")
        return {
            "status": "success",
            "message": f"Added datum {input_data.label.strip().upper()}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def set_sheet_scale(
        input_data: SetSheetScaleInput | None = None,
    ) -> dict[str, Any]:
        """Set the active drawing sheet's scale, e.g. 1:2; views that use the sheet scale follow it.

        Use it when views are too large for the sheet, or when `arrange_dimensions`
        reports overlaps it can't clear. Needs: an open, active drawing. Changes: the
        sheet scale. Returns: `data` with the `sheet` name and the new `scale`.

        Args:
            input_data: The scale. `numerator` and `denominator` are required and
                must be above zero, e.g. 1 and 2 for 1:2.

        Returns:
            dict[str, Any]: The new scale.
        """
        _supports("set_sheet_scale")
        input_data = normalize_input(input_data, SetSheetScaleInput)
        result = await adapter.set_sheet_scale(
            input_data.numerator, input_data.denominator
        )
        require_success(result, context="Failed to set the sheet scale")
        return {
            "status": "success",
            "message": f"Sheet scale set to {(result.data or {}).get('scale')}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def generate_drawing_package(
        input_data: GenerateDrawingPackageInput | None = None,
    ) -> dict[str, Any]:
        """Create a complete A3 drawing of a part or assembly in one call, and save it with a PDF.

        It opens a new drawing with an A3 ISO frame and fills the title block (title,
        number, material, weight, drawn by, date, revision). It places front, plan, side
        and isometric views at one standard scale, shows the model dimensions once in
        tidy rows, and adds view captions and a NOTES block. Then it checks nothing
        overlaps. Use it instead of the step-by-step drawing tools when you want the
        whole drawing. Needs: the model file on disk. Changes: saves
        `<drawing_number>.SLDDRW` and a PDF, replacing files of those names, and by
        default writes Description, Material and Weight into the model and saves it
        (turn off with `write_model_properties`). Returns: `data` with `scale`, `views`,
        `dimensions`, `overlap_check`, `title_block`, `drawing_path`, `pdf_path` and
        `steps`. A remaining overlap makes the call an error carrying that data; for
        long title-block text pass `title_block_text_height_mm` (e.g. 3).

        Args:
            input_data: The drawing. `model_path` (.sldprt or .sldasm) is
                required; `output_dir`, the title-block fields (`title`,
                `drawing_number`, `author`, `revision`, `company`), `notes`,
                `include_isometric`, `include_bom` and `write_model_properties` have
                defaults.

        Returns:
            dict[str, Any]: The drawing and its check.
        """
        require_capability(
            adapter,
            "create_standard_drawing",
            "The active adapter does not support generate_drawing_package; "
            "nothing was done.",
        )
        input_data = normalize_input(input_data, GenerateDrawingPackageInput)
        result = await adapter.create_standard_drawing(
            input_data.model_path,
            input_data.output_dir,
            input_data.title,
            input_data.subtitle,
            input_data.drawing_number,
            input_data.author,
            input_data.revision,
            input_data.company,
            None,
            input_data.notes,
            input_data.include_isometric,
            input_data.write_model_properties,
            input_data.title_block_text_height_mm,
            input_data.include_bom,
        )
        require_success(
            result, context="Failed to create the standard drawing", with_data=True
        )
        data = result.data or {}
        return {
            "status": "success",
            "message": (
                f"Created {data.get('drawing_path')} at {data.get('scale')} "
                f"with PDF {data.get('pdf_path')}"
            ),
            "data": data,
            "execution_time": result.execution_time,
        }

    return 7

register_drawing_tools async

register_drawing_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register drawing tools with FastMCP.

Registers comprehensive technical drawing tools for SolidWorks automation including view creation, dimensioning, annotation, and drawing standards validation. Essential for automated documentation workflows.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config dict[str, Any]

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Example
from solidworks_mcp.tools.drawing import register_drawing_tools

tool_count = await register_drawing_tools(mcp, adapter, config)
print(f"Registered {tool_count} drawing tools")
Source code in src/solidworks_mcp/tools/drawing.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
async def register_drawing_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register drawing tools with FastMCP.

    Registers comprehensive technical drawing tools for SolidWorks automation including view
    creation, dimensioning, annotation, and drawing standards validation. Essential for
    automated documentation workflows.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (dict[str, Any]): Configuration values for the operation.

    Returns:
        int: The computed numeric result.

    Example:
                        ```python
                        from solidworks_mcp.tools.drawing import register_drawing_tools

                        tool_count = await register_drawing_tools(mcp, adapter, config)
                        print(f"Registered {tool_count} drawing tools")
                        ```
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0

    @mcp.tool()
    async def create_drawing_view(input_data: CreateDrawingViewInput) -> dict[str, Any]:
        """Place one named view of a part or assembly on the active drawing sheet.

        Use it to add views one at a time; for the three standard views in one call, use
        `create_technical_drawing`, and for a whole drawing from a model, use
        `generate_drawing_package`. `add_drawing_view` does the same as this tool.
        Needs: an open, active drawing and the model file on disk. Changes: adds a view
        to the sheet. Returns: `data` with the view's `name`, `orientation`, `position`,
        `scale` and the view counts before and after.

        Args:
            input_data: The view. `model_path` and `view_type` (front, back, left, right, top, bottom, isometric (iso), trimetric, dimetric or current) are
                required; `position_x`/`position_y` (mm on the sheet) and `scale`
                have defaults.

        Returns:
            dict[str, Any]: The placed view.
        """
        require_capability(
            adapter,
            "create_drawing_view",
            "Active adapter does not support create_drawing_view; no "
            f"{input_data.view_type} view of {input_data.model_path} was created.",
        )
        result = await adapter.create_drawing_view(input_data.model_dump())
        require_success(result, "Failed to create drawing view")
        return {
            "status": "success",
            "message": f"Created {input_data.view_type} view of {input_data.model_path}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_dimension(input_data: AddDimensionInput) -> dict[str, Any]:
        """Add one dimension between drawing entities at a position on the sheet.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To bring the
        model's own dimensions into a drawing today, use `insert_model_dimensions`.
        Changes: once available, adds a dimension to the drawing. Returns: once
        available, the dimension under `data`.

        Args:
            input_data: The dimension. `dimension_type`, `entity1`, `position_x`
                and `position_y` are required; `entity2` and `precision` have
                defaults.

        Returns:
            dict[str, Any]: The dimension, or a `not_supported` error.
        """
        payload = input_data.model_dump()
        dimension_type = payload["dimension_type"]

        require_capability(
            adapter,
            "add_dimension",
            f"Active adapter does not support add_dimension; no "
            f"{dimension_type} dimension was added.",
        )
        result = await adapter.add_dimension(payload)
        require_success(result, "Failed to add dimension")
        return {
            "status": "success",
            "message": "Dimension added successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_note(input_data: AddNoteInput) -> dict[str, Any]:
        """Place a text note on the active drawing sheet at a position in millimetres.

        Use it for general notes such as material or finish. For a title block, use
        `generate_drawing_package`, which fills one. Needs: an open, active drawing.
        Changes: adds a note to the sheet. Returns: `data` with `text`, `position`,
        `positioned` (whether the position was applied) and `font_size_points`.

        Args:
            input_data: The note. `text`, `position_x` and `position_y` (mm from
                the sheet's lower-left corner) are required; `font_size` (points)
                has a default.

        Returns:
            dict[str, Any]: The note placed.
        """
        require_capability(
            adapter,
            "add_note",
            "Active adapter does not support add_note; no note was added.",
        )
        result = await adapter.add_note(input_data.model_dump())
        require_success(result, "Failed to add note")
        return {
            "status": "success",
            "message": f"Added note: {input_data.text[:30]}...",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_section_view(input_data: CreateSectionViewInput) -> dict[str, Any]:
        """Cut a section view along a line across a drawing view.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Use
        SolidWorks directly (Insert > Drawing View > Section). Changes: once available,
        adds a section view. Returns: once available, the view under `data`.

        Args:
            input_data: The section. `section_line_start`, `section_line_end`,
                `view_position_x` and `view_position_y` are required; `scale` and
                `label` have defaults.

        Returns:
            dict[str, Any]: The view, or a `not_supported` error.
        """
        raise UnsupportedOperationError(
            "No adapter capability exists for cutting a section view "
            "along a section line; create_section_view cannot compute "
            "the cut geometry, so no view was created. Use SolidWorks "
            "directly (Insert > Drawing View > Section) instead."
        )

    @mcp.tool()
    async def create_detail_view(input_data: CreateDetailViewInput) -> dict[str, Any]:
        """Magnify a circular area of a drawing view as a detail view.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Use
        SolidWorks directly (Insert > Drawing View > Detail). Changes: once available,
        adds a detail view. Returns: once available, the view under `data`.

        Args:
            input_data: The detail. `center_x`, `center_y`, `radius`,
                `view_position_x` and `view_position_y` are required; `scale` and
                `label` have defaults.

        Returns:
            dict[str, Any]: The view, or a `not_supported` error.
        """
        raise UnsupportedOperationError(
            "No adapter capability exists for magnifying the "
            "geometry inside a detail circle; create_detail_view "
            "cannot compute the detail geometry, so no view was "
            "created. Use SolidWorks directly (Insert > Drawing "
            "View > Detail) instead."
        )

    @mcp.tool()
    async def update_sheet_format(input_data: UpdateSheetFormatInput) -> dict[str, Any]:
        """Change the active drawing's sheet format file and title-block fields.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release.
        `generate_drawing_package` sets the sheet format and fills the title block when
        it creates a drawing. Changes: once available, replaces the sheet format.
        Returns: once available, the new format under `data`.

        Args:
            input_data: The format. `format_file` is required; `sheet_size` and
                the title-block fields have defaults.

        Returns:
            dict[str, Any]: The format, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "update_sheet_format",
            "Active adapter does not support update_sheet_format; the "
            "sheet format and title block were not changed.",
        )
        result = await adapter.update_sheet_format(input_data.model_dump())
        require_success(result, "Failed to update sheet format")
        return {
            "status": "success",
            "message": f"Updated sheet format to {input_data.sheet_size}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def auto_dimension_view(input_data: dict[str, Any]) -> dict[str, Any]:
        """Dimension a drawing view automatically with SolidWorks' auto-dimension scheme.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Use
        `insert_model_dimensions` and then `arrange_dimensions` today. Changes: once
        available, adds dimensions to the view. Returns: once available, the dimensions
        added under `data`.

        Args:
            input_data: A mapping with the `view_name` to dimension.

        Returns:
            dict[str, Any]: The dimensions, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "auto_dimension_view",
            "Active adapter does not support auto_dimension_view; no "
            "dimensions were added.",
        )
        result = await adapter.auto_dimension_view(input_data)
        require_success(result, "Failed to auto-dimension view")
        return {
            "status": "success",
            "message": "Auto-dimensioned drawing view",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def check_drawing_standards(input_data: dict[str, Any]) -> dict[str, Any]:
        """Check the active drawing against a drafting standard and score it.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. SolidWorks
        Design Checker does this today. Read-only. Returns: once available, the findings
        under `data`.

        Args:
            input_data: A mapping with the `standard` to check against.

        Returns:
            dict[str, Any]: The findings, or a `not_supported` error.
        """
        raise UnsupportedOperationError(
            "No adapter capability exists for checking drafting "
            "standards compliance; check_drawing_standards cannot "
            "inspect the drawing, so no compliance score was "
            "computed. Use SolidWorks Design Checker for this."
        )

    @mcp.tool()
    async def create_technical_drawing(
        input_data: DrawingCreationInput,
    ) -> dict[str, Any]:
        """Place a model's three standard views (front, top and side) on the active drawing sheet in one call.

        Use it to start a drawing's views quickly; for one view, use
        `create_drawing_view`, and to create and save a complete drawing from a model
        file, use `generate_drawing_package`. Views are laid out in `projection`
        (third-angle by default). Needs: an open, active drawing and the model file on
        disk. Changes: adds three views to the sheet. Returns: `data` with `views` (the
        names added), `model_path`, `projection` and the view counts.

        Args:
            input_data: The model to draw. Give `model_file`; `projection` is
                third_angle unless you pass first_angle.

        Returns:
            dict[str, Any]: The views added.
        """
        require_capability(
            adapter,
            "create_technical_drawing",
            "Active adapter does not support create_technical_drawing; "
            "no drawing was created.",
        )
        result = await adapter.create_technical_drawing(input_data.model_dump())
        require_success(result, "Failed to create technical drawing")
        return {
            "status": "success",
            "message": "Technical drawing created successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_drawing_view(input_data: DrawingViewInput) -> dict[str, Any]:
        """Place one named view of a part or assembly on the active drawing sheet; the same as `create_drawing_view`.

        Prefer `create_drawing_view`; this tool reaches the same code and is kept for
        existing callers. It only adds views: an `operation` other than "add" is
        refused. Needs: an open, active drawing and the model file on
        disk. Changes: adds a view to the sheet. Returns: `data` with the view's `name`,
        `orientation`, `position`, `scale` and the view counts.

        Args:
            input_data: The view. Give `model_file` and `view_type` (front, back, left, right, top, bottom, isometric (iso), trimetric, dimetric or current);
                `position` ([x, y] mm on the sheet) and `scale` have defaults.

        Returns:
            dict[str, Any]: The placed view.
        """
        if input_data.operation.strip().lower() != "add":
            raise InvalidInputError(
                f"operation '{input_data.operation}' is not supported: "
                "add_drawing_view only adds views, and the drawing was not changed."
            )
        require_capability(
            adapter,
            "add_drawing_view",
            "Active adapter does not support add_drawing_view; no view was added.",
        )
        result = await adapter.add_drawing_view(input_data.model_dump())
        require_success(result, "Failed to add drawing view")
        return {
            "status": "success",
            "message": "Drawing view added successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_annotation(input_data: AnnotationInput) -> dict[str, Any]:
        """Add an annotation (a note, balloon or surface-finish symbol) to the active drawing.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. For a text
        note today, use `add_note`; for GD&T, use `add_gtol` and `add_datum_tag`.
        Changes: once available, adds the annotation. Returns: once available, the
        annotation under `data`.

        Args:
            input_data: The annotation. `annotation_type` and `text` are required;
                the position and font fields have defaults.

        Returns:
            dict[str, Any]: The annotation, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "add_annotation",
            "Active adapter does not support add_annotation; no annotation was added.",
        )
        result = await adapter.add_annotation(input_data.model_dump())
        require_success(result, "Failed to add annotation")
        return {
            "status": "success",
            "message": "Annotation added successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def update_title_block(input_data: dict[str, Any]) -> dict[str, Any]:
        """Fill in the active drawing's title-block fields (title, drawn by, drawing number and so on).

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release.
        `generate_drawing_package` fills the title block when it creates a drawing, and
        `set_custom_property` sets the properties a title block reads. Changes: once
        available, rewrites the title-block fields. Returns: once available, the fields
        set under `data`.

        Args:
            input_data: A mapping of title-block field names to values.

        Returns:
            dict[str, Any]: The fields, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "update_title_block",
            "Active adapter does not support update_title_block; the "
            "title block was not changed.",
        )
        result = await adapter.update_title_block(input_data)
        require_success(result, "Failed to update title block")
        return {
            "status": "success",
            "message": "Title block updated successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def list_drawing_views() -> dict[str, Any]:
        """List the views on the active drawing, by name.

        Use it before `auto_center_marks`, `insert_model_dimensions` or any tool that
        takes a view name. Needs: an open, active drawing. Read-only. Returns: `views`
        (names, sheet formats left out) and `count`.
        """
        result = await adapter.list_drawing_views()
        require_success(result, context="Failed to list drawing views")
        views = list(result.data or [])
        return {
            "status": "success",
            "message": (
                f"{len(views)} view(s): {', '.join(views)}"
                if views
                else "The drawing has no views"
            ),
            "views": views,
            "count": len(views),
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def auto_center_marks(
        input_data: AutoCenterMarksInput,
    ) -> dict[str, Any]:
        """Insert centre marks on the holes, slots and (optionally) fillets of one drawing view.

        Use it after placing views, before dimensioning holes. Adding none is still a
        success: the view may have no unmarked circular edges. Needs: an open drawing
        with the named view (see `list_drawing_views`). Changes: adds centre-mark
        annotations to the view. Returns: `data` with `center_marks_added` and the
        counts before and after.

        Args:
            input_data: The view. `view_name` is required; `mark_holes` and
                `mark_slots` default to on, `mark_fillets` to off.

        Returns:
            dict[str, Any]: The marks added.
        """
        require_capability(
            adapter,
            "auto_center_marks",
            "Active adapter does not support auto_center_marks; "
            "no centre marks were inserted.",
        )
        input_data = normalize_input(input_data, AutoCenterMarksInput)
        result = await adapter.auto_center_marks(
            input_data.view_name,
            input_data.mark_holes,
            input_data.mark_fillets,
            input_data.mark_slots,
        )
        require_success(result, context="Failed to insert centre marks")
        data = result.data if isinstance(result.data, dict) else {}
        added = data.get("center_marks_added")
        return {
            "status": "success",
            "message": (
                f"Inserted {added} centre mark(s) on {input_data.view_name}"
                if isinstance(added, int)
                else f"Ran centre-mark insertion on {input_data.view_name}"
            ),
            "data": data,
            "execution_time": result.execution_time,
        }

    tool_count = 14  # Number of tools registered
    return tool_count

register_engineering_data_tools async

register_engineering_data_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register engineering-data tools with FastMCP.

Parameters:

Name Type Description Default
mcp FastMCP

The MCP server.

required
adapter SolidWorksAdapter

Adapter used by every tool.

required
config dict[str, Any]

Server configuration (unused).

required

Returns:

Name Type Description
int int

Number of tools registered.

Source code in src/solidworks_mcp/tools/engineering_data.py
async def register_engineering_data_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register engineering-data tools with FastMCP.

    Args:
        mcp (FastMCP): The MCP server.
        adapter (SolidWorksAdapter): Adapter used by every tool.
        config (dict[str, Any]): Server configuration (unused).

    Returns:
        int: Number of tools registered.
    """

    mcp = with_tool_boundary(mcp)

    def _supports(method: str) -> None:
        require_capability(
            adapter,
            method,
            f"The active adapter does not support {method}; nothing was done.",
        )

    @mcp.tool()
    async def list_model_dimensions(
        input_data: ListModelDimensionsInput | None = None,
    ) -> dict[str, Any]:
        """List every dimension in the active part or assembly with its full name and value.

        Use it to find a dimension in an existing model before changing it: each
        `full_name` (e.g. "D1@Sketch1") is the name `get_dimension` and `set_dimension`
        take. Needs: an open part or assembly. Read-only. Returns: `data`, one entry per
        dimension with `full_name`, `feature`, `value` (document units), `system_value`
        (metres or radians) and `read_only`.

        Args:
            input_data: Nothing is required: give `feature_name` to list one
                feature's dimensions, or omit it for every dimension.

        Returns:
            dict[str, Any]: The dimensions.
        """
        input_data = normalize_input(input_data, ListModelDimensionsInput)
        _supports("list_model_dimensions")
        result = await adapter.list_model_dimensions(input_data.feature_name)
        require_success(result, context="Failed to list model dimensions")
        return _success(result, f"Listed {len(result.data or [])} dimension(s)")

    @mcp.tool()
    async def measure_entities(input_data: MeasureEntitiesInput) -> dict[str, Any]:
        """Measure faces, edges or vertices of the active model with SolidWorks' Measure tool.

        One face gives area and perimeter; one edge its length, and radius and diameter
        for an arc; two entities give the distance and angle between them. Use it to
        check a feature's size or position; for the whole model, use `verify_model`.
        Needs: an open part or assembly. Read-only. It clears the selection afterwards.
        Returns: `data` with `selected` and the values in metres, square metres and
        radians (`distance_m`, `angle_rad`, `length_m`, `radius_m`, `diameter_m`,
        `area_m2`, `perimeter_m`; for two entities `is_parallel`).

        Args:
            input_data: What to measure. `entities` (one or two) is required, each
                "face:x,y,z", "edge:x,y,z", "vertex:x,y,z" (a point on it, in metres)
                or a name like "Edge<1>".

        Returns:
            dict[str, Any]: The measurements.
        """
        input_data = normalize_input(input_data, MeasureEntitiesInput)
        _supports("measure_entities")
        result = await adapter.measure_entities(input_data.entities)
        require_success(result, context="Failed to measure")
        return _success(result, f"Measured {', '.join(input_data.entities)}")

    @mcp.tool()
    async def get_material(
        input_data: GetMaterialInput | None = None,
    ) -> dict[str, Any]:
        """Report the material assigned to the active part.

        Use it before `calculate_mass_properties` or `verify_model` with a mass: without
        a material SolidWorks uses a default density. Needs: an open, active part (any
        other document is `wrong_document_type`). Read-only. Returns: `data` with
        `material` (null when none is assigned), `database` and `density_kg_m3` (from
        the material's library entry; null when unknown). For the other properties
        (modulus, strength), open the material in SolidWorks.

        Args:
            input_data: Nothing is required: `configuration` defaults to the active
                one.

        Returns:
            dict[str, Any]: The material.
        """
        input_data = normalize_input(input_data, GetMaterialInput)
        _supports("get_material")
        result = await adapter.get_material(input_data.configuration)
        require_success(result, context="Failed to read the material")
        material = (result.data or {}).get("material")
        return _success(
            result, f"Material: {material}" if material else "No material assigned"
        )

    @mcp.tool()
    async def set_material(input_data: SetMaterialInput) -> dict[str, Any]:
        """Assign a library material to the active part, e.g. "AISI 1020" or "6061-T6 (SS)".

        The material sets the mass and what a title block's Material field shows. The
        name must match the library exactly; find it with `list_available_materials`.
        The material is read back, so success means it applied. Needs: an open, active
        part. Changes: the part's material. Returns: `data` with `material`, `database`
        and `configuration`.

        Args:
            input_data: The material. `material_name` is required; `database`
                ("SOLIDWORKS Materials") and `configuration` (all) have defaults.

        Returns:
            dict[str, Any]: The material assigned.
        """
        input_data = normalize_input(input_data, SetMaterialInput)
        _supports("set_material")
        result = await adapter.set_material(
            input_data.material_name, input_data.database, input_data.configuration
        )
        require_success(result, context="Failed to assign the material")
        return _success(result, f"Assigned {input_data.material_name}")

    @mcp.tool()
    async def list_available_materials(
        input_data: ListMaterialsInput | None = None,
    ) -> dict[str, Any]:
        """List the materials in the installed SolidWorks material libraries.

        Use it to find the exact name `set_material` needs; filter with `search` (e.g.
        "steel" or "6061"). It reads every .sldmat library SolidWorks is set to use.
        Needs: SolidWorks installed. Read-only. Returns: `data`, each material's `name`,
        `classification` and `database`; the message gives the total and the folders
        read, so an empty search can be told from a missing library.

        Args:
            input_data: Nothing is required: `search` filters by name or
                classification, not case-sensitive.

        Returns:
            dict[str, Any]: The materials.
        """
        input_data = normalize_input(input_data, ListMaterialsInput)
        _supports("list_available_materials")
        result = await adapter.list_available_materials(input_data.search)
        require_success(result, context="Failed to list materials")
        searched = result.metadata or {}
        found = len(result.data or [])
        message = f"Found {found} material(s)"
        if "total" in searched:
            message += (
                f" of {searched['total']} in {'; '.join(searched.get('folders', []))}"
            )
        return _success(result, message)

    @mcp.tool()
    async def get_custom_properties(
        input_data: CustomPropertiesInput | None = None,
    ) -> dict[str, Any]:
        """Read the active document's custom properties, such as PartNo, Description or Revision.

        File-level properties (the default) are what drawing title blocks and BOMs
        usually read; give `configuration` for that configuration's own. Needs: an open,
        active document. Read-only. Returns: `data` with `properties`, each name mapped
        to its `value` and `resolved` value (a $PRP link evaluated).

        Args:
            input_data: Nothing is required: `configuration` reads that
                configuration's properties instead of the file's.

        Returns:
            dict[str, Any]: The properties.
        """
        input_data = normalize_input(input_data, CustomPropertiesInput)
        _supports("get_custom_properties")
        result = await adapter.get_custom_properties(input_data.configuration)
        require_success(result, context="Failed to read custom properties")
        count = len((result.data or {}).get("properties", {}))
        return _success(result, f"Read {count} custom propert(ies)")

    @mcp.tool()
    async def set_custom_property(input_data: SetCustomPropertyInput) -> dict[str, Any]:
        """Create or overwrite one text custom property on the active document, e.g. PartNo or DrawnBy.

        Use it to fill what a title block or BOM reads. The value is read back, so
        success means it was stored. Needs: an open, active document. Changes: writes
        the property, replacing one of the same name (save the file to keep it).
        Returns: `data` with `name`, `value`, `resolved` and `configuration`.

        Args:
            input_data: The property. `name` and `value` (text, or a link such as
                $PRP:"SW-Mass") are required; `configuration` defaults to file-level.

        Returns:
            dict[str, Any]: The property stored.
        """
        input_data = normalize_input(input_data, SetCustomPropertyInput)
        _supports("set_custom_property")
        result = await adapter.set_custom_property(
            input_data.name, input_data.value, input_data.configuration
        )
        require_success(result, context="Failed to set the custom property")
        return _success(result, f"Set custom property {input_data.name}")

    @mcp.tool()
    async def export_flat_pattern(input_data: ExportFlatPatternInput) -> dict[str, Any]:
        """Export the active sheet-metal part's flat pattern to DXF or DWG, for laser or waterjet cutting.

        It writes the flat outline only, without bend lines or sketches. A part with no
        sheet-metal body is `not_found` and nothing is written. Needs: an open, active
        sheet-metal part. Changes: writes the file, replacing one already there.
        Returns: `data` with `file_path` and `size_bytes`.

        Args:
            input_data: The export. `file_path` (ending in .dxf or .dwg) is
                required.

        Returns:
            dict[str, Any]: The file written.
        """
        input_data = normalize_input(input_data, ExportFlatPatternInput)
        _supports("export_flat_pattern")
        result = await adapter.export_flat_pattern(input_data.file_path)
        require_success(result, context="Failed to export the flat pattern")
        written = (result.data or {}).get("file_path")
        return _success(result, f"Exported the flat pattern to {written}")

    return 8

register_export_tools async

register_export_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register export tools with FastMCP.

Registers comprehensive file export tools for SolidWorks automation supporting multiple industry-standard formats including neutral CAD formats, manufacturing formats, and documentation outputs.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config dict[str, Any]

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Example
from solidworks_mcp.tools.export import register_export_tools

tool_count = await register_export_tools(mcp, adapter, config)
print(f"Registered {tool_count} export tools")
Source code in src/solidworks_mcp/tools/export.py
async def register_export_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register export tools with FastMCP.

    Registers comprehensive file export tools for SolidWorks automation supporting multiple
    industry-standard formats including neutral CAD formats, manufacturing formats, and
    documentation outputs.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (dict[str, Any]): Configuration values for the operation.

    Returns:
        int: The computed numeric result.

    Example:
                        ```python
                        from solidworks_mcp.tools.export import register_export_tools

                        tool_count = await register_export_tools(mcp, adapter, config)
                        print(f"Registered {tool_count} export tools")
                        ```
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0

    @mcp.tool()
    async def export_step(input_data: ExportFileInput) -> dict[str, Any]:
        """Export the active document to STEP, like File > Save As with the STEP type.

        Use it to hand a model to another CAD system; it is the most widely read
        exchange format. SolidWorks picks the format from the file's extension, so end
        `file_path` in .step or .stp. Needs: an open, active part or assembly. Changes:
        writes the file, creating its folder and replacing a file already there; the
        open document is unchanged. Returns: `export` with `file_path` and `format`.

        Args:
            input_data: The export. `file_path` (ending in .step or .stp) is required;
                `format_type` is not needed: the file extension decides the
                format.

        Returns:
            dict[str, Any]: The file written.
        """
        input_data = _file_input(input_data, "step")
        file_path = input_data.file_path

        if hasattr(adapter, "export_step"):
            result = await adapter.export_step(input_data.model_dump())
        else:
            result = await adapter.export_file(file_path, "step")

        require_success(result, context="Failed to export STEP")
        if hasattr(adapter, "export_step"):
            return {
                "status": "success",
                "message": f"Exported to STEP format: {file_path}",
                "data": result.data,
                "execution_time": result.execution_time,
            }
        return {
            "status": "success",
            "message": f"Exported to STEP format: {file_path}",
            "export": {
                "file_path": file_path,
                "format": "STEP",
                "size_estimate": "Unknown",  # Would get actual file size
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def export_iges(input_data: ExportFileInput) -> dict[str, Any]:
        """Export the active document to IGES, like File > Save As with the IGES type.

        Use it only when the receiver can't read STEP; prefer `export_step`. SolidWorks
        picks the format from the file's extension, so end `file_path` in .igs or .iges.
        Needs: an open, active part or assembly. Changes: writes the file, creating its
        folder and replacing a file already there; the open document is unchanged.
        Returns: `export` with `file_path` and `format`.

        Args:
            input_data: The export. `file_path` (ending in .igs or .iges) is required;
                `format_type` is not needed: the file extension decides the
                format.

        Returns:
            dict[str, Any]: The file written.
        """
        input_data = _file_input(input_data, "iges")
        if hasattr(adapter, "export_iges"):
            result = await adapter.export_iges(input_data.model_dump())
        else:
            result = await adapter.export_file(input_data.file_path, "iges")

        require_success(result, context="Failed to export IGES")
        if hasattr(adapter, "export_iges"):
            return {
                "status": "success",
                "message": f"Exported to IGES format: {input_data.file_path}",
                "data": result.data,
                "execution_time": result.execution_time,
            }
        return {
            "status": "success",
            "message": f"Exported to IGES format: {input_data.file_path}",
            "export": {
                "file_path": input_data.file_path,
                "format": "IGES",
                "size_estimate": "Unknown",
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def export_stl(input_data: ExportFileInput) -> dict[str, Any]:
        """Export the active document to STL, like File > Save As with the STL type.

        Use it for 3D printing. An assembly is exported whole, with lightweight
        components resolved first. SolidWorks picks the format from the file's
        extension, so end `file_path` in .stl. Needs: an open, active part or assembly.
        Changes: writes the file, creating its folder and replacing a file already
        there; the open document is unchanged. Returns: `export` with `file_path` and
        `format`.

        Args:
            input_data: The export. `file_path` (ending in .stl) is required;
                `format_type` is not needed: the file extension decides the
                format.

        Returns:
            dict[str, Any]: The file written.
        """
        input_data = _file_input(input_data, "stl")
        if hasattr(adapter, "export_stl"):
            result = await adapter.export_stl(input_data.model_dump())
        else:
            result = await adapter.export_file(input_data.file_path, "stl")

        require_success(result, context="Failed to export STL")
        if hasattr(adapter, "export_stl"):
            return {
                "status": "success",
                "message": f"Exported to STL format: {input_data.file_path}",
                "data": result.data,
                "execution_time": result.execution_time,
            }
        return {
            "status": "success",
            "message": f"Exported to STL format: {input_data.file_path}",
            "export": {
                "file_path": input_data.file_path,
                "format": "STL",
                "use_case": "3D printing and rapid prototyping",
                "size_estimate": "Unknown",
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def export_pdf(input_data: ExportFileInput) -> dict[str, Any]:
        """Export the active document to PDF, like File > Save As with the PDF type.

        Use it to share a drawing; `generate_drawing_package` already saves a PDF with
        the drawing it creates. SolidWorks picks the format from the file's extension,
        so end `file_path` in .pdf. Needs: an open, active drawing (a part or assembly
        gives a PDF of the view). Changes: writes the file, creating its folder and
        replacing a file already there; the open document is unchanged. Returns:
        `export` with `file_path` and `format`.

        Args:
            input_data: The export. `file_path` (ending in .pdf) is required;
                `format_type` is not needed: the file extension decides the
                format.

        Returns:
            dict[str, Any]: The file written.
        """
        input_data = _file_input(input_data, "pdf")
        if hasattr(adapter, "export_pdf"):
            result = await adapter.export_pdf(input_data.model_dump())
        else:
            result = await adapter.export_file(input_data.file_path, "pdf")

        require_success(result, context="Failed to export PDF")
        if hasattr(adapter, "export_pdf"):
            return {
                "status": "success",
                "message": f"Exported to PDF format: {input_data.file_path}",
                "data": result.data,
                "execution_time": result.execution_time,
            }
        return {
            "status": "success",
            "message": f"Exported to PDF format: {input_data.file_path}",
            "export": {
                "file_path": input_data.file_path,
                "format": "PDF",
                "use_case": "Documentation and sharing",
                "size_estimate": "Unknown",
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def export_dwg(input_data: ExportFileInput) -> dict[str, Any]:
        """Export the active document to DWG, like File > Save As with the DWG type.

        Use it to hand a drawing to AutoCAD users. SolidWorks picks the format from the
        file's extension, so end `file_path` in .dwg. Needs: an open, active drawing.
        Changes: writes the file, creating its folder and replacing a file already
        there; the open document is unchanged. Returns: `export` with `file_path` and
        `format`.

        Args:
            input_data: The export. `file_path` (ending in .dwg) is required;
                `format_type` is not needed: the file extension decides the
                format.

        Returns:
            dict[str, Any]: The file written.
        """
        input_data = _file_input(input_data, "dwg")
        if hasattr(adapter, "export_dwg"):
            result = await adapter.export_dwg(input_data.model_dump())
        else:
            result = await adapter.export_file(input_data.file_path, "dwg")

        require_success(result, context="Failed to export DWG")
        if hasattr(adapter, "export_dwg"):
            return {
                "status": "success",
                "message": f"Exported to DWG format: {input_data.file_path}",
                "data": result.data,
                "execution_time": result.execution_time,
            }
        return {
            "status": "success",
            "message": f"Exported to DWG format: {input_data.file_path}",
            "export": {
                "file_path": input_data.file_path,
                "format": "DWG",
                "use_case": "AutoCAD compatibility",
                "size_estimate": "Unknown",
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def export_image(input_data: ExportImageInput) -> dict[str, Any]:
        """Save a picture of the active model in a standard view, as PNG or JPG, at a chosen size.

        Use it to show a result, or to check a model's shape after a change. The view is
        set first (front, back, left, right, top, bottom, isometric, dimetric,
        trimetric, or current to keep the user's view). Needs: an open, active document.
        Changes: writes the image file, and turns the model to the view asked for.
        Returns: `data` with `file_path`, `format`, `dimensions` (the size written),
        `requested_dimensions` and `view`.

        Args:
            input_data: The image. `file_path` (or `output_path`) is needed; the
                format follows its extension. `width`/`height` (or `resolution` as
                "1920x1080") and `view_orientation` (isometric) have defaults.

        Returns:
            dict[str, Any]: The image written.
        """
        input_data = normalize_input(input_data, ExportImageInput)
        if hasattr(adapter, "export_image"):
            result = await adapter.export_image(input_data.model_dump())
            require_success(result, "Failed to export image")
            return {
                "status": "success",
                "message": f"Exported image: {input_data.file_path}",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        if hasattr(adapter, "export_file") and input_data.file_path:
            result = await adapter.export_file(  # type: ignore[assignment]
                input_data.file_path, input_data.format_type
            )
            require_success(result, "Failed to export image")
            return {
                "status": "success",
                "message": f"Exported image: {input_data.file_path}",
                "export": {
                    "file_path": input_data.file_path,
                    "format": input_data.format_type.upper(),
                    "dimensions": f"{input_data.width}x{input_data.height}",
                    "view": input_data.view_orientation,
                    "use_case": "Documentation and presentations",
                },
                "execution_time": result.execution_time,
            }

        raise UnsupportedOperationError(
            "Active adapter does not support export_image or "
            "export_file; no image was exported."
        )

    @mcp.tool()
    async def batch_export(input_data: BatchExportInput) -> dict[str, Any]:
        """Export every matching SolidWorks file in a folder to one format.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Today, open
        each file with `open_model` and use an `export_*` tool. Changes: once available,
        writes one exported file per source file. Returns: once available, a per-file
        result under `data`.

        Args:
            input_data: The batch. `source_directory` and `output_directory` are
                required; `format_type`, `file_pattern` and `recursive` have
                defaults.

        Returns:
            dict[str, Any]: The results, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "batch_export",
            "Active adapter does not support batch_export; no files were exported.",
        )
        result = await adapter.batch_export(input_data.model_dump())
        require_success(result, "Batch export failed")
        return {
            "status": "success",
            "message": f"Batch export completed to {input_data.format_type} format",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    tool_count = 7  # Number of tools registered
    return tool_count

register_file_management_tools async

register_file_management_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register file management tools with FastMCP.

Registers essential file operations for SolidWorks document management including save operations, file format conversions, and file property access. These tools provide fundamental document lifecycle management capabilities.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config dict[str, Any]

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Example
from solidworks_mcp.tools.file_management import register_file_management_tools

tool_count = await register_file_management_tools(mcp, adapter, config)
print(f"Registered {tool_count} file management tools")
            Note:
                File management tools require an active SolidWorks document.
                Save operations preserve the current document state and metadata.
Source code in src/solidworks_mcp/tools/file_management.py
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
async def register_file_management_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register file management tools with FastMCP.

    Registers essential file operations for SolidWorks document management including save
    operations, file format conversions, and file property access. These tools provide
    fundamental document lifecycle management capabilities.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (dict[str, Any]): Configuration values for the operation.

    Returns:
        int: The computed numeric result.

    Example:
                        ```python
                        from solidworks_mcp.tools.file_management import register_file_management_tools

                        tool_count = await register_file_management_tools(mcp, adapter, config)
                        print(f"Registered {tool_count} file management tools")
                        ```

                    Note:
                        File management tools require an active SolidWorks document.
                        Save operations preserve the current document state and metadata.
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0

    def _coerce_input(model_cls: Any, payload: Any) -> Any:
        """Accept legacy dict payloads from compatibility wrapper as well as model instances.

        Args:
            model_cls (Any): The model cls value.
            payload (Any): The payload value.

        Returns:
            Any: The result produced by the operation.
        """
        return (
            payload
            if isinstance(payload, model_cls)
            else model_cls.model_validate(payload)
        )

    def _result_value(payload: Any, *keys: str, default: Any = None) -> Any:
        """Read a value from adapter result payloads that may be dicts or model objects.

        Args:
            payload (Any): The payload value.
            *keys (str): Additional positional arguments forwarded to the call.
            default (Any): Fallback value returned when the operation fails. Defaults to None.

        Returns:
            Any: The result produced by the operation.
        """
        if payload is None:
            return default

        if isinstance(payload, dict):
            for key in keys:
                if key in payload and payload[key] is not None:
                    return payload[key]
            return default

        for key in keys:
            value = getattr(payload, key, None)
            if value is not None:
                return value
        return default

    def _prepare_save_target(
        raw_path: str,
        *,
        required_extension: str,
        overwrite: bool,
    ) -> str:
        """Normalize and validate a save target before adapter/COM calls.

        Enforces pre-save guardrails used by save convenience tools so invalid paths are
        rejected consistently in both mock and real adapter modes. The helper performs:
        path trimming, extension normalization, parent-directory existence check,
        directory writability check, and overwrite policy enforcement.

        Args:
            raw_path (str): User-supplied path where the file should be saved.
            required_extension (str): Required extension for the target document type
                (for example, ``.sldprt`` or ``.sldasm``).
            overwrite (bool): Whether saving is allowed to replace an existing file.

        Returns:
            str: The normalized save path.

        Raises:
            InvalidInputError: When the path is empty, has the wrong extension, its
                directory is missing or not writable, or it exists and overwrite is
                False.

        Example:
            ```python
            target = _prepare_save_target(
                r"C:/Exports/bracket.step",
                required_extension=".sldprt",
                overwrite=False,
            )

            # target == "C:/Exports/bracket.sldprt" when valid; raises
            # InvalidInputError when the directory is missing/not writable,
            # or when the target exists and overwrite is False.
            ```

        Note:
            Validation is intentionally executed before adapter calls to avoid opaque COM
            failures and to provide deterministic, user-friendly error messages.
        """
        file_path = raw_path.strip()
        if not file_path:
            raise InvalidInputError("Invalid file_path: path is empty or whitespace.")

        cleaned = file_path.strip()
        extension_name = required_extension.lstrip(".")
        if (
            cleaned.count(".") == 1
            and cleaned.startswith(".")
            and cleaned[1:].lower() == extension_name
        ):
            # Reject extension-only input such as ".sldprt".
            raise InvalidInputError(
                "Invalid file_path: missing base filename before extension."
            )

        target = Path(file_path)
        if target.suffix.lower() != required_extension.lower():
            # Normalize extension so save_part/save_assembly always use their canonical type.
            target = target.with_suffix(required_extension)

        target_dir = target.parent
        if not target_dir.exists():
            raise InvalidInputError(f"Target directory does not exist: {target_dir}")

        if not target_dir.is_dir():
            raise InvalidInputError(
                f"Target parent path is not a directory: {target_dir}"
            )

        if not os.access(target_dir, os.W_OK):
            raise InvalidInputError(f"Target directory is not writable: {target_dir}")

        if target.exists() and target.is_dir():
            raise InvalidInputError(
                f"Target path is a directory, expected file path: {target}"
            )

        if target.exists() and not overwrite:
            raise InvalidInputError(
                f"File already exists and overwrite=False: {target}. Set overwrite=True to replace it."
            )

        return str(target)

    def _get_attr_or_call(obj: Any, name: str, default: Any = None) -> Any:
        """Read COM values that may be exposed as either properties or methods."""
        candidate = getattr(obj, name, None)
        if candidate is None:
            return default
        if callable(candidate):
            try:
                value = candidate()
            except Exception:
                return default
            return default if value is None else value
        return candidate

    def _copy_with_collision_handling(source_path: Path, target_dir: Path) -> Path:
        """Copy a source file into target_dir, suffixing when name collisions occur."""
        destination = target_dir / source_path.name
        try:
            same_target = destination.exists() and source_path.resolve(
                strict=False
            ) == destination.resolve(strict=False)
        except Exception:
            same_target = False
        if same_target:
            return destination

        if not destination.exists():
            shutil.copy2(source_path, destination)
            return destination

        stem = source_path.stem
        suffix = source_path.suffix
        index = 1
        while True:
            candidate = target_dir / f"{stem}_{index}{suffix}"
            if not candidate.exists():
                shutil.copy2(source_path, candidate)
                return candidate
            index += 1

    def _copy_active_assembly_with_references(
        target_assembly_path: str,
    ) -> dict[str, Any]:
        """Copy active assembly and resolved dependencies into target folder."""
        current_model = getattr(adapter, "currentModel", None)
        if current_model is None:
            raise ToolOperationError("No active model available to copy references.")

        source_assembly_raw = _get_attr_or_call(current_model, "GetPathName")
        source_assembly = (
            Path(str(source_assembly_raw)) if source_assembly_raw else None
        )
        if source_assembly is None or not source_assembly.exists():
            raise ToolOperationError(
                "Active assembly must be saved before include_references=True can copy dependencies."
            )

        target_assembly = Path(target_assembly_path)
        target_dir = target_assembly.parent
        target_dir.mkdir(parents=True, exist_ok=True)

        dependency_callable = getattr(current_model, "GetDependencies2", None)
        if not callable(dependency_callable):
            raise ToolOperationError(
                "Adapter does not expose GetDependencies2; cannot copy assembly references in this mode."
            )

        try:
            raw_dependencies = dependency_callable(True, True, False)
        except Exception as exc:
            raise ToolOperationError(
                f"Failed to enumerate assembly dependencies: {exc}"
            ) from exc

        unique_sources: list[Path] = [source_assembly]
        for dep in dependency_paths(raw_dependencies):
            if dep not in unique_sources:
                unique_sources.append(dep)

        copied_files: list[str] = []
        missing_sources: list[str] = []

        for source in unique_sources:
            if not source.exists():
                missing_sources.append(str(source))
                continue

            if source.resolve(strict=False) == source_assembly.resolve(strict=False):
                shutil.copy2(source, target_assembly)
                copied_files.append(str(target_assembly))
            else:
                copied_target = _copy_with_collision_handling(source, target_dir)
                copied_files.append(str(copied_target))

        copied_files = sorted(copied_files)
        return {
            "status": "success",
            "message": f"Assembly and references copied to: {target_dir}",
            "file_path": str(target_assembly),
            "copied_file_count": len(copied_files),
            "copied_files": copied_files,
            "missing_source_files": missing_sources,
            "copy_method": "tool_dependency_copy",
        }

    @mcp.tool()
    async def save_file(input_data: SaveFileInput) -> dict[str, Any]:
        """Save the active document, to its own path or to a new path in its own format.

        Use it after changes you want to keep. For a part or an assembly with checks on
        the target path (folder exists, no accidental overwrite), use `save_part` or
        `save_assembly`; to write another format, use `save_as` or an `export_*` tool.
        Needs: an open, active document, already saved once unless you give a path.
        Changes: writes the file; a different file at `file_path` is replaced. Returns:
        `data` from the save (usually empty).

        Args:
            input_data: The save. Nothing is required: give `file_path` to save to a
                new path, or omit it to save in place.

        Returns:
            dict[str, Any]: The save result.
        """
        input_data = _coerce_input(SaveFileInput, input_data)
        require_capability(
            adapter,
            "save_file",
            "Active adapter does not support save_file; the file was not saved.",
        )
        result = await adapter.save_file(input_data.file_path)
        require_success(result, "Failed to save file")
        return {
            "status": "success",
            "message": "File saved successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def save_as(input_data: SaveAsInput) -> dict[str, Any]:
        """Save the active document under a new path, as a SolidWorks file or in an exchange format such as STEP or IGES.

        For a SolidWorks format (solidworks, sldprt, sldasm, slddrw) it saves a copy
        like File > Save As; any other `format_type` is passed to the exporter. For one
        known format with its options, prefer `export_step`, `export_stl` and the other
        `export_*` tools. Needs: an open, active document. Changes: writes the file. A
        file already at `file_path` is kept and the call refused, unless `overwrite`
        is on. Returns: `file_path` and `format`.

        Args:
            input_data: The target. `file_path` is required; `format_type` defaults
                to "solidworks", and `overwrite` is off.

        Returns:
            dict[str, Any]: The path written.
        """
        input_data = _coerce_input(SaveAsInput, input_data)
        if Path(input_data.file_path).exists() and not input_data.overwrite:
            raise InvalidInputError(
                f"A file already exists at {input_data.file_path}; it was left "
                "untouched. Pass overwrite: true to replace it, or choose another path."
            )
        if hasattr(adapter, "save_file") and input_data.format_type.lower() in {
            "solidworks",
            "sldprt",
            "sldasm",
            "slddrw",
        }:
            result = await adapter.save_file(input_data.file_path)
            require_success(result, "Failed to save file")
            return {
                "status": "success",
                "message": f"File saved as: {input_data.file_path}",
                "file_path": input_data.file_path,
                "format": input_data.format_type,
                "execution_time": result.execution_time,
            }

        require_capability(
            adapter,
            "export_file",
            "Active adapter does not support save_file or export_file; "
            f"no file was saved as {input_data.file_path}.",
        )
        result = await adapter.export_file(
            input_data.file_path,
            input_data.format_type,
        )
        require_success(result, "Failed to export file")
        return {
            "status": "success",
            "message": f"File exported as: {input_data.file_path}",
            "file_path": input_data.file_path,
            "format": input_data.format_type,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def get_file_properties() -> dict[str, Any]:
        """Report the active document's file details: name, path, size, dates, type, configuration and unsaved changes.

        Use it to check what is open and whether it has unsaved changes before saving or
        closing. For the model itself (rebuild status included), use `get_model_info`;
        for custom properties, use `get_custom_properties`. Needs: an open, active
        document. Read-only. Returns: `properties` with `file_name`, `file_path`,
        `file_size_bytes`, `created_date` and `modified_date` (Unix timestamps; empty
        for a document never saved), `document_type`, `configuration`, `is_dirty` and
        `feature_count`.
        """
        require_capability(
            adapter, "get_model_info", "Active adapter does not support model metadata"
        )

        result = await adapter.get_model_info()
        if not result.is_success or not isinstance(result.data, dict):
            raise ToolOperationError(result.error or "No active SolidWorks document")

        model_info = dict(result.data)
        raw_path = str(model_info.get("path") or "")
        file_path = Path(raw_path) if raw_path else None
        properties: dict[str, Any] = {
            "file_name": (
                file_path.name
                if file_path is not None
                else str(model_info.get("title") or "")
            ),
            "file_path": raw_path,
            "file_size_bytes": None,
            "created_date": None,
            "modified_date": None,
            "document_type": model_info.get("type"),
            "configuration": model_info.get("configuration"),
            "is_dirty": model_info.get("is_dirty"),
            "feature_count": model_info.get("feature_count"),
        }

        if file_path is not None and file_path.is_file():
            stat = file_path.stat()
            properties.update(
                {
                    "file_size_bytes": stat.st_size,
                    "created_date": stat.st_ctime,
                    "modified_date": stat.st_mtime,
                }
            )

        return {
            "status": "success",
            "properties": properties,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def get_model_info() -> dict[str, Any]:
        """Report the active document's title, path, type, active configuration, unsaved changes, feature count and rebuild status.

        Use it first in a session, to see which document is active (it follows a switch
        made in the SolidWorks window). For file size and dates, use
        `get_file_properties`; for every open document, use `list_open_documents`.
        Needs: an open document. Read-only. Returns: `model_info` with `title`, `path`,
        `type`, `configuration`, `is_dirty`, `feature_count` and `rebuild_status` (0
        rebuilt, 1 needs a rebuild).
        """
        require_capability(
            adapter, "get_model_info", "Active adapter does not support get_model_info"
        )
        result = await adapter.get_model_info()
        require_success(result, "Failed to get model info")
        return {
            "status": "success",
            "model_info": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def list_features(input_data: ListFeaturesInput) -> dict[str, Any]:
        """List the feature tree of the active part or assembly, in order.

        Use it before editing a model, to find a feature's exact name for
        `delete_feature`, `suppress_feature` or `rename_feature`. For an assembly it
        also lists each component's features, down to `max_assembly_depth` levels.
        Needs: an open part or assembly. Read-only. Returns: `features` (each with
        `name`, `type`, `suppressed` and, in an assembly, `component`), `count`, and
        `assembly_tree`, the same list grouped by component.

        Args:
            input_data: What to list. Nothing is required: `include_suppressed`
                (off) and `max_assembly_depth` (2) have defaults.

        Returns:
            dict[str, Any]: The feature list.
        """
        input_data = _coerce_input(ListFeaturesInput, input_data)
        require_capability(
            adapter, "list_features", "Active adapter does not support list_features"
        )
        result = await adapter.list_features(
            include_suppressed=input_data.include_suppressed,
            max_assembly_depth=input_data.max_assembly_depth,
        )
        require_success(result, "Failed to list features")
        return {
            "status": "success",
            "features": result.data or [],
            "count": len(result.data or []),
            # Additive, derived view of the same flat "features"
            # list above - grouped by component/sub-component
            # for callers that want the assembly structure
            # rather than a flat tag-and-scan. Does not change
            # the meaning of "features"/"count".
            "assembly_tree": build_component_tree(result.data),
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def get_selected_entities() -> dict[str, Any]:
        """Describe the faces, edges and vertices selected in the SolidWorks window right now.

        Use it when the user has clicked something and says "this face": it reads the
        selection instead of guessing from a screenshot. To select edges yourself, use
        `select_face_edges`. Needs: an open document. Read-only. Returns: `entities`
        (each with `index` and `type`; a face adds `bounding_box_mm`, `center_mm`,
        `area_mm2` and `normal` when flat, an edge `start_mm`, `end_mm` and
        straight-line `length_mm`, a vertex `point_mm`) and `count`.
        """
        require_capability(
            adapter,
            "get_selected_entities",
            "Active adapter does not support get_selected_entities",
        )
        result = await adapter.get_selected_entities()
        require_success(result, "Failed to get selected entities")
        return {
            "status": "success",
            "entities": result.data or [],
            "count": len(result.data or []),
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def classify_feature_tree(
        input_data: ClassifyFeatureTreeInput,
    ) -> dict[str, Any]:
        """Classify the active model's kind (part, sheet metal, assembly, drawing and so on) from its feature tree, and suggest how to work on it.

        Use it before rebuilding or editing an unfamiliar model, to pick a workflow. It
        reads the model unless you pass `model_info` and `features` you already have
        (from `get_model_info` and `list_features`). Needs: an open document, unless
        both are passed. Read-only. Returns: `classification` with `family`,
        `recommended_workflow`, `confidence`, `needs_vba`, `evidence`, `warnings`,
        `next_actions` and `feature_count`, plus the `model_info` and `features` used.

        Args:
            input_data: What to classify. Nothing is required: pass `model_info`
                and `features` to classify them without reading the model;
                `include_suppressed` is off by default.

        Returns:
            dict[str, Any]: The classification.
        """
        input_data = _coerce_input(ClassifyFeatureTreeInput, input_data)
        model_info = input_data.model_info
        features = input_data.features
        execution_time = 0.0

        # Reading the model can fail (nothing open); classifying an empty
        # model instead would give a confident wrong answer.
        if model_info is None:
            require_capability(
                adapter,
                "get_model_info",
                "Active adapter does not support get_model_info",
            )
            model_result = await adapter.get_model_info()
            require_success(
                model_result, context="Failed to read the model to classify"
            )
            model_info = model_result.data
            execution_time = max(execution_time, model_result.execution_time or 0.0)

        if features is None:
            require_capability(
                adapter,
                "list_features",
                "Active adapter does not support list_features",
            )
            feature_result = await adapter.list_features(
                include_suppressed=input_data.include_suppressed
            )
            require_success(
                feature_result, context="Failed to list features to classify"
            )
            features = feature_result.data or []
            execution_time = max(execution_time, feature_result.execution_time or 0.0)

        classification = classify_feature_tree_snapshot(model_info, features or [])  # type: ignore[arg-type]
        return {
            "status": "success",
            "classification": classification,
            "model_info": model_info or {},
            "features": features or [],
            "execution_time": execution_time,
        }

    @mcp.tool()
    async def list_configurations() -> dict[str, Any]:
        """List the names of the active model's configurations.

        Use it to see which configurations a part or assembly has. Needs: an open part
        or assembly. Read-only. Returns: `configurations` (names) and `count`.
        `get_model_info` says which one is active.
        """
        require_capability(
            adapter,
            "list_configurations",
            "Active adapter does not support list_configurations",
        )
        result = await adapter.list_configurations()
        require_success(result, "Failed to list configurations")
        return {
            "status": "success",
            "configurations": result.data or [],
            "count": len(result.data or []),
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def manage_file_properties(input_data: FileOperationInput) -> dict[str, Any]:
        """Read or change the file-level properties of a SolidWorks file on disk.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To read or
        set custom properties on the active model today, use `get_custom_properties` and
        `set_custom_property`. Changes: once available, the file's properties. Returns:
        once available, the properties under `data`.

        Args:
            input_data: The operation. `operation` is required; `file_path` or
                `source_path`, `properties` and `include_system` have defaults.

        Returns:
            dict[str, Any]: The properties, or a `not_supported` error.
        """
        input_data = _coerce_input(FileOperationInput, input_data)
        require_capability(
            adapter,
            "manage_file_properties",
            "Active adapter does not support manage_file_properties; "
            f"no {input_data.operation} operation was performed.",
        )
        result = await adapter.manage_file_properties(input_data.model_dump())
        require_success(result, "Failed to manage file properties")
        return {
            "status": "success",
            "message": "File properties managed successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def convert_file_format(input_data: FormatConversionInput) -> dict[str, Any]:
        """Convert a SolidWorks file on disk to another format without opening it first.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Today, open
        the file with `open_model` and use `save_as` or an `export_*` tool such as
        `export_step`. Changes: once available, writes the converted file. Returns: once
        available, the converted file under `data`.

        Args:
            input_data: The conversion. `source_file` and `target_format` are
                required; `target_file`, `quality` and `units` have defaults.

        Returns:
            dict[str, Any]: The conversion, or a `not_supported` error.
        """
        input_data = _coerce_input(FormatConversionInput, input_data)
        require_capability(
            adapter,
            "convert_file_format",
            "Active adapter does not support convert_file_format; "
            f"{input_data.source_file} was not converted.",
        )
        result = await adapter.convert_file_format(input_data.model_dump())
        require_success(result, "Failed to convert file format")
        return {
            "status": "success",
            "message": "File converted successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def batch_file_operations(input_data: FileOperationInput) -> dict[str, Any]:
        """Copy, move, rename or delete SolidWorks files on disk in one call.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To copy an
        assembly with every part it uses, use `pack_and_go_assembly`. Changes: once
        available, copies, moves, renames or deletes files. Returns: once available, a
        per-file result under `data`.

        Args:
            input_data: The operation. `operation` (copy, move, delete or rename) is
                required; `source_path`, `target_path` and `parameters` have
                defaults.

        Returns:
            dict[str, Any]: The results, or a `not_supported` error.
        """
        input_data = _coerce_input(FileOperationInput, input_data)
        require_capability(
            adapter,
            "batch_file_operations",
            "Active adapter does not support batch_file_operations; "
            f"no {input_data.operation} batch operation was performed.",
        )
        result = await adapter.batch_file_operations(input_data.model_dump())
        require_success(result, "Failed to run batch file operations")
        return {
            "status": "success",
            "message": "Batch file operations completed successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def load_part(input_data: LoadPartInput) -> dict[str, Any]:
        """Open a part (.sldprt) from disk and make it the active document.

        Use it to start work on an existing part; for any document type, use
        `open_model`, and to switch to a document that is already open, use
        `activate_document`. A path that isn't a .sldprt is refused. Needs: the file on
        disk. Changes: opens the part (no file is written). Returns: `model` with
        `name`, `type`, `path` and `configuration` (the one SolidWorks opened, or
        null when it can't be read).

        Args:
            input_data: The part. `file_path` (a .sldprt) is required.

        Returns:
            dict[str, Any]: The opened part.
        """
        input_data = _coerce_input(LoadPartInput, input_data)
        # Ensure file path ends with .sldprt
        file_path = input_data.file_path
        if not file_path.lower().endswith(".sldprt"):
            raise InvalidInputError(f"File must be a .sldprt part file: {file_path}")

        result = await adapter.open_model(file_path)
        require_success(result, context="Failed to load part")
        model = result.data
        title = _result_value(model, "title", "name", default=file_path)
        path = _result_value(model, "path", "file_path", default=file_path)
        configuration = _result_value(model, "configuration")
        return {
            "status": "success",
            "message": f"Loaded part: {title}",
            "model": {
                "name": title,
                "type": "Part",
                "path": path,
                "configuration": configuration,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def load_assembly(input_data: LoadAssemblyInput) -> dict[str, Any]:
        """Open an assembly (.sldasm) from disk and make it the active document.

        Use it to start work on an existing assembly; for any document type, use
        `open_model`, and to switch to a document that is already open, use
        `activate_document`. A path that isn't a .sldasm is refused. Needs: the file on
        disk. Changes: opens the assembly (no file is written). Returns: `model` with
        `name`, `type`, `path` and `configuration` (the one SolidWorks opened, or
        null when it can't be read).

        Args:
            input_data: The assembly. `file_path` (a .sldasm) is required.

        Returns:
            dict[str, Any]: The opened assembly.
        """
        input_data = _coerce_input(LoadAssemblyInput, input_data)
        # Ensure file path ends with .sldasm
        file_path = input_data.file_path
        if not file_path.lower().endswith(".sldasm"):
            raise InvalidInputError(
                f"File must be a .sldasm assembly file: {file_path}"
            )

        result = await adapter.open_model(file_path)
        require_success(result, context="Failed to load assembly")
        model = result.data
        title = _result_value(model, "title", "name", default=file_path)
        path = _result_value(model, "path", "file_path", default=file_path)
        configuration = _result_value(model, "configuration")
        return {
            "status": "success",
            "message": f"Loaded assembly: {title}",
            "model": {
                "name": title,
                "type": "Assembly",
                "path": path,
                "configuration": configuration,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def save_part(input_data: SavePartInput | None = None) -> dict[str, Any]:
        """Save the active part in place, or as a new .sldprt with the target path checked first.

        Before writing to a new path it checks the folder exists and is writable,
        changes the extension to .sldprt, and refuses an existing file unless
        `overwrite` is on. For an assembly, use `save_assembly`; for other formats, use
        `save_as` or an `export_*` tool. Needs: an open, active part. Changes: writes
        the part file. Returns: `file_path` when saved to a new path.

        Args:
            input_data: The save. Nothing is required: give `file_path` for a new
                path or omit it to save in place; `overwrite` is off.

        Returns:
            dict[str, Any]: The save result.
        """
        if input_data is None:
            input_data = SavePartInput()
        else:
            input_data = _coerce_input(SavePartInput, input_data)

        # If file_path provided, use save_as; otherwise use regular save
        if input_data.file_path:
            file_path = _prepare_save_target(
                input_data.file_path,
                required_extension=".sldprt",
                overwrite=input_data.overwrite,
            )

            result = await adapter.save_file(file_path)
            require_success(result, context="Failed to save part")
            return {
                "status": "success",
                "message": f"Part saved as: {file_path}",
                "file_path": file_path,
                "execution_time": result.execution_time,
            }
        else:
            # Save to current location
            result = await adapter.save_file(None)
            require_success(result, context="Failed to save part")
            return {
                "status": "success",
                "message": "Part saved successfully",
                "execution_time": result.execution_time,
            }

    @mcp.tool()
    async def save_assembly(
        input_data: SaveAssemblyInput | None = None,
    ) -> dict[str, Any]:
        """Save the active assembly in place, or as a new .sldasm, optionally copying the parts it uses alongside.

        Before writing to a new path it checks the folder exists and is writable,
        changes the extension to .sldasm, and refuses an existing file unless
        `overwrite` is on. With `include_references`, it instead copies the assembly
        file as last saved, and the files it references, into the new folder; unsaved
        changes are left out and references are not updated. For a self-contained copy,
        use `pack_and_go_assembly`. Needs: an open, active assembly, saved once for
        `include_references`. Changes: writes the assembly file (and copies). Returns:
        `file_path`, plus `copied_files` and `missing_source_files` when copying
        references.

        Args:
            input_data: The save. Nothing is required: give `file_path` for a new
                path or omit it to save in place; `overwrite` and
                `include_references` are off.

        Returns:
            dict[str, Any]: The save result.
        """
        if input_data is None:
            input_data = SaveAssemblyInput()
        else:
            input_data = _coerce_input(SaveAssemblyInput, input_data)

        # If file_path provided, use save_as; otherwise use regular save
        if input_data.file_path:
            file_path = _prepare_save_target(
                input_data.file_path,
                required_extension=".sldasm",
                overwrite=input_data.overwrite,
            )

            if input_data.include_references:
                return _copy_active_assembly_with_references(file_path)

            result = await adapter.save_file(file_path)
            require_success(result, context="Failed to save assembly")
            return {
                "status": "success",
                "message": f"Assembly saved as: {file_path}",
                "file_path": file_path,
                "execution_time": result.execution_time,
            }
        else:
            # Save to current location
            result = await adapter.save_file(None)
            require_success(result, context="Failed to save assembly")
            return {
                "status": "success",
                "message": "Assembly saved successfully",
                "execution_time": result.execution_time,
            }

    @mcp.tool()
    async def pack_and_go_assembly(input_data: PackAndGoInput) -> dict[str, Any]:
        """Copy an assembly and every file it uses into one folder that opens on its own.

        Use it to hand over or archive an assembly. SolidWorks lists the files the
        assembly uses; each is copied under its own name, as saved on disk, so save
        first: a `warning` names any open file whose unsaved changes the copy doesn't
        have. The copy opens with its own parts, since SolidWorks looks in the
        assembly's folder first. It fails, copying nothing, when a file is missing,
        two files share a name, or `target_dir` holds the originals. It refuses a
        folder that already has files unless `overwrite` is on, and then replaces only
        the files it copies. It never closes or changes a document. It can add an
        isometric PNG preview. Needs: the .sldasm on disk. Changes: writes the copies
        into `target_dir`, creating it if needed. Returns: `source_assembly`,
        `target_dir`, `copied_files`, `source_files`, `unsaved_changes` and, with a
        preview, `preview_image`.

        Args:
            input_data: The copy. `source_path` (a .sldasm) and `target_dir` are
                required; `export_preview` and `overwrite` are off.

        Returns:
            dict[str, Any]: The files copied.
        """
        input_data = _coerce_input(PackAndGoInput, input_data)

        from pathlib import Path as _Path

        source = _Path(input_data.source_path)
        out_dir = _Path(input_data.target_dir)

        if not source.exists():
            raise ToolOperationError(f"Source assembly not found: {source}")

        if not source.suffix.lower() == ".sldasm":
            raise InvalidInputError(
                f"source_path must be a .sldasm file, got: {source.suffix}"
            )

        if not input_data.overwrite and out_dir.exists() and any(out_dir.iterdir()):
            raise InvalidInputError(
                f"target_dir already contains files: {out_dir}. "
                "Set overwrite=True to replace them."
            )

        result = await adapter.pack_and_go_assembly(  # type: ignore[attr-defined]
            source_path=str(source),
            target_dir=str(out_dir),
        )

        require_success(result)

        report: dict[str, Any] = {
            "status": "success",
            "method": "dependency_copy",
            "execution_time": result.execution_time,
            **result.data,
        }
        warnings: list[str] = []
        unsaved = result.data.get("unsaved_changes") or []
        if unsaved:
            warnings.append(
                f"{', '.join(unsaved)} had unsaved changes, which are not in the "
                "copy; save, then pack again to include them."
            )

        if input_data.export_preview:
            preview_path = out_dir / "assembly_preview.png"
            img_result = await adapter.export_image(
                {
                    "file_path": str(preview_path),
                    "format_type": "png",
                    "width": 1600,
                    "height": 1000,
                    "view_orientation": "isometric",
                }
            )
            report["preview_image"] = (
                str(preview_path) if img_result.is_success else None
            )
            if not img_result.is_success:
                warnings.append(
                    f"The copy is complete, but the preview couldn't be written: "
                    f"{img_result.error}"
                )

        if warnings:
            report["warning"] = " ".join(warnings)
        return report

    @mcp.tool()
    async def list_open_documents() -> dict[str, Any]:
        """List every document open in SolidWorks and say which one is active.

        Use it before `activate_document`, or when tool calls seem to act on the wrong
        document. Needs: SolidWorks running. Read-only. Returns: `documents` (each with
        `title`, `path`, `type` and `is_active`) and `count`.
        """
        result = await adapter.list_open_documents()
        require_success(result, context="Failed to list open documents")
        docs = result.data if isinstance(result.data, list) else []
        active = next((d.get("title") for d in docs if d.get("is_active")), None)
        return {
            "status": "success",
            "message": (
                f"{len(docs)} document(s) open"
                + (f"; active: {active}" if active else "")
            ),
            "documents": docs,
            "count": len(docs),
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def activate_document(
        input_data: ActivateDocumentInput,
    ) -> dict[str, Any]:
        """Make an already-open document the active one, so the next tools act on it.

        Match it by window title, full path or file name (not case-sensitive); see
        `list_open_documents`. To open a file that isn't open yet, use `open_model`.
        Needs: the document open in SolidWorks. Changes: the active document. Returns:
        `data` with `activated` (the title) and `verified` (read back from SolidWorks;
        null when it couldn't be read). No match is an error that lists the open
        documents.

        Args:
            input_data: The document. `title_or_path` is required.

        Returns:
            dict[str, Any]: The activated document.
        """
        input_data = _coerce_input(ActivateDocumentInput, input_data)
        result = await adapter.activate_document(input_data.title_or_path)
        require_success(result, context="Failed to activate document")
        data = result.data if isinstance(result.data, dict) else {}
        activated = data.get("activated", input_data.title_or_path)
        message = f"Activated {activated}"
        if data.get("verified") is None:
            message += " (could not be read back to confirm)"
        return {
            "status": "success",
            "message": message,
            "data": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def save_body_as_part(
        input_data: SaveBodyAsPartInput,
    ) -> dict[str, Any]:
        """Save one solid body of the active multibody part as a new part file (Insert > Features > Save Bodies).

        Use it to split a multibody part into parts, e.g. after `create_extrusion`
        without merging. Needs: an open, active part with the named body and an existing
        target folder. Changes: adds a Save Bodies feature to the part and writes the
        new .sldprt. Returns: `data` with `body`, `file_path`, `feature` and
        `solid_bodies` (every body name, to correct a wrong one).

        Args:
            input_data: The body. `body_name` and `file_path` (a .sldprt) are
                required.

        Returns:
            dict[str, Any]: The file written.
        """
        require_capability(
            adapter,
            "save_body_as_part",
            "Active adapter does not support save_body_as_part; no file was written.",
        )
        input_data = _coerce_input(SaveBodyAsPartInput, input_data)
        result = await adapter.save_body_as_part(
            input_data.body_name, input_data.file_path
        )
        require_success(result, context="Failed to save body as part")
        data = result.data if isinstance(result.data, dict) else {}
        return {
            "status": "success",
            "message": (
                f"Saved body {input_data.body_name} to "
                f"{data.get('file_path', input_data.file_path)}"
            ),
            "data": data,
            "execution_time": result.execution_time,
        }

    tool_count = 19  # Total number of registered tools in this module
    return tool_count

register_macro_recording_tools async

register_macro_recording_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: Any) -> int

Register macro recording and playback tools with FastMCP.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config Any

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Example

tool_count = await register_macro_recording_tools(mcp, adapter, config)

Source code in src/solidworks_mcp/tools/macro_recording.py
async def register_macro_recording_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: Any
) -> int:
    """Register macro recording and playback tools with FastMCP.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (Any): Configuration values for the operation.

    Returns:
        int: The computed numeric result.

    Example:
                        >>> tool_count = await register_macro_recording_tools(mcp, adapter, config)
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0

    @mcp.tool()
    async def start_macro_recording(input_data: MacroRecordingInput) -> dict[str, Any]:
        """Start SolidWorks' macro recorder, to capture what the user does as a macro.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Recording is
        done in SolidWorks today (Tools > Macro > Record). Changes: once available,
        starts a recording session. Returns: once available, the session details under
        `data`.

        Args:
            input_data: The recording. `output_file` (where the macro is saved) is
                required; `macro_name`, `recording_mode`, the capture switches and
                `timeout_seconds` have defaults.

        Returns:
            dict[str, Any]: The session, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "start_macro_recording",
            "Active adapter does not support start_macro_recording; "
            f"no recording of {input_data.macro_name} was started.",
        )
        result = await adapter.start_macro_recording(input_data.model_dump())
        require_success(result, "Failed to start recording")
        return {
            "status": "success",
            "message": f"Macro recording started: {input_data.macro_name}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def stop_macro_recording(input_data: dict[str, Any]) -> dict[str, Any]:
        """Stop the macro recorder and save the recorded macro.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Recording is
        done in SolidWorks today (Tools > Macro > Stop). Changes: once available, ends
        the session and writes the macro file. Returns: once available, the saved
        macro's details under `data`.

        Args:
            input_data: A mapping; nothing in it is used yet.

        Returns:
            dict[str, Any]: The saved macro, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "stop_macro_recording",
            "No adapter capability exists for stopping a macro "
            "recording; this tool never started a real recording "
            "session, so there is no macro code to save.",
        )
        result = await adapter.stop_macro_recording(input_data)
        require_success(result, "Failed to stop recording")
        return {
            "status": "success",
            "message": "Macro recording completed and saved",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def execute_macro(input_data: MacroPlaybackInput) -> dict[str, Any]:
        """Run a binary SolidWorks macro (.swp, or legacy .swb) in the running SolidWorks, calling its `main` procedure once.

        Use it to run an existing macro file. VBA source (.bas, .vb, plain text, or what
        the `generate_vba_*` tools return) is refused as invalid input before SolidWorks
        sees it. The macro must not wait for the user (no MsgBox or forms), or the call
        hangs. It runs `repeat_count` times, `pause_between_runs` seconds apart, and
        stops at the first run that fails. Needs: SolidWorks running and the macro file
        on disk. Changes: whatever the macro does; it can't be undone from here.
        Returns: `runs_completed` and `data` from the last run, with `macro_path`,
        `module_name`, `proc_name` and `errors`.

        Args:
            input_data: The macro to run. Give `macro_path` (or `macro_file`);
                `proc_name` (main), `repeat_count` (1) and `pause_between_runs` (0 s)
                have defaults.

        Returns:
            dict[str, Any]: The run's result.
        """
        input_data = normalize_input(input_data, MacroPlaybackInput)
        require_capability(
            adapter,
            "execute_macro",
            "Active adapter does not support execute_macro; "
            f"{input_data.macro_path} was not run, and no features "
            "were created.",
        )
        payload = input_data.model_dump()
        total = input_data.repeat_count
        execution_time = 0.0
        result = None
        for run in range(total):
            if run and input_data.pause_between_runs:
                await asyncio.sleep(input_data.pause_between_runs)
            result = await adapter.execute_macro(payload)
            if not result.is_success:
                raise ToolOperationError(
                    f"Run {run + 1} of {total} failed after {run} completed: "
                    f"{result.error or 'the macro reported an error'}",
                    execution_time=execution_time + (result.execution_time or 0.0),
                    data=result.data,
                )
            execution_time += result.execution_time or 0.0
        runs = "1 run" if total == 1 else f"{total} runs"
        return {
            "status": "success",
            "message": f"Macro completed {runs}",
            "runs_completed": total,
            "data": result.data if result is not None else None,
            "execution_time": execution_time,
        }

    @mcp.tool()
    async def analyze_macro(input_data: MacroAnalysisInput) -> dict[str, Any]:
        """Analyse a macro's structure, dependencies and complexity, and suggest optimisations.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Read-only.
        Returns: once available, the findings under `data`.

        Args:
            input_data: The macro. Give `macro_path` (or `macro_file`);
                `analysis_type`, `analysis_depth` and `suggest_optimizations` have
                defaults.

        Returns:
            dict[str, Any]: The findings, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "analyze_macro",
            "Active adapter does not support analyze_macro; "
            f"{input_data.macro_path} was not analyzed.",
        )
        result = await adapter.analyze_macro(input_data.model_dump())
        require_success(result, "Failed to analyze macro")
        return {
            "status": "success",
            "message": f"Macro analysis completed for {input_data.macro_path}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def batch_execute_macros(input_data: MacroBatchInput) -> dict[str, Any]:
        """Run several macros in sequence against the files in a folder.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To run one
        macro, use `execute_macro`. Changes: once available, whatever the macros do.
        Returns: once available, each run's result under `data`.

        Args:
            input_data: The batch. `macro_list` (macro file paths) is required;
                `target_directory`, `file_pattern`, `execution_order` and
                `stop_on_error` have defaults.

        Returns:
            dict[str, Any]: The results, or a `not_supported` error.
        """
        payload = input_data.model_dump()

        if hasattr(adapter, "batch_execute_macros"):
            result = await adapter.batch_execute_macros(payload)
            require_success(result, "Failed batch execution")
            return {
                "status": "success",
                "message": "Batch macro execution completed successfully",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        macro_list = payload.get("macro_list", [])

        raise UnsupportedOperationError(
            "Active adapter does not support batch_execute_macros; "
            f"none of the {len(macro_list)} macro(s) were run."
        )

    @mcp.tool()
    async def optimize_macro(input_data: dict[str, Any]) -> dict[str, Any]:
        """Suggest improvements to a macro's code for speed and reliability.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Read-only. It
        would suggest changes, not make them. Returns: once available, the suggestions
        under `data`.

        Args:
            input_data: A mapping with the macro's `macro_path` (or `macro_file`).

        Returns:
            dict[str, Any]: The suggestions, or a `not_supported` error.
        """
        payload = dict(input_data)

        if hasattr(adapter, "optimize_macro"):
            result = await adapter.optimize_macro(payload)
            require_success(result, "Failed to optimize macro")
            return {
                "status": "success",
                "message": "Macro optimization completed successfully",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        macro_path = payload.get("macro_path") or payload.get("macro_file", "")

        raise UnsupportedOperationError(
            "Active adapter does not support optimize_macro; "
            f"{macro_path} was not analyzed or changed."
        )

    @mcp.tool()
    async def create_macro_library(input_data: dict[str, Any]) -> dict[str, Any]:
        """Set up a shared folder of categorised, documented macros.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Changes: once
        available, creates the library folder and its index. Returns: once available,
        the library's contents under `data`.

        Args:
            input_data: A mapping with the `library_path` to create.

        Returns:
            dict[str, Any]: The library, or a `not_supported` error.
        """
        payload = dict(input_data)

        if hasattr(adapter, "create_macro_library"):
            result = await adapter.create_macro_library(payload)
            require_success(result, "Failed to create library")
            return {
                "status": "success",
                "message": "Macro library created successfully",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        library_path = payload.get("library_path", "")

        raise UnsupportedOperationError(
            "Active adapter does not support create_macro_library; "
            f"no library was created at {library_path or '(unspecified path)'}."
        )

    tool_count = 8  # Macro recording and management tools
    return tool_count

register_modeling_tools async

register_modeling_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register modeling tools with FastMCP.

Registers comprehensive modeling tools for SolidWorks automation including model creation, feature creation, and model management operations.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config dict[str, Any]

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Example
from solidworks_mcp.tools.modeling import register_modeling_tools

tool_count = await register_modeling_tools(mcp, adapter, config)
print(f"Registered {tool_count} modeling tools")
Source code in src/solidworks_mcp/tools/modeling.py
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
async def register_modeling_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register modeling tools with FastMCP.

    Registers comprehensive modeling tools for SolidWorks automation including model
    creation, feature creation, and model management operations.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (dict[str, Any]): Configuration values for the operation.

    Returns:
        int: The computed numeric result.

    Example:
                        ```python
                        from solidworks_mcp.tools.modeling import register_modeling_tools

                        tool_count = await register_modeling_tools(mcp, adapter, config)
                        print(f"Registered {tool_count} modeling tools")
                        ```
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0

    @mcp.tool()
    async def open_model(input_data: OpenModelInput) -> dict[str, Any]:
        """Open a part, assembly or drawing from disk and make it the active document.

        Use it to start work on an existing file. To switch to a document that is
        already open, use `activate_document`. Needs: the file on disk (a vault file
        must be checked out). Changes: opens the document; nothing is written. Returns:
        `model` with `title`, `type`, `path` and `configuration`.

        Args:
            input_data: The file. `file_path` (.sldprt, .sldasm or .slddrw) is
                required.

        Returns:
            dict[str, Any]: The opened document.
        """
        input_data = normalize_input(input_data, OpenModelInput)
        result = await adapter.open_model(input_data.file_path)

        require_success(result, context="Failed to open model")
        model = result.data
        title = _result_value(model, "title", "name", default=input_data.file_path)
        model_type = _result_value(model, "type", default="Part")
        path = _result_value(model, "path", "file_path", default=input_data.file_path)
        configuration = _result_value(model, "configuration", default="Default")
        return {
            "status": "success",
            "message": f"Opened {model_type}: {title}",
            "model": {
                "title": title,
                "name": title,
                "type": model_type,
                "path": path,
                "configuration": configuration,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_part(input_data: CreatePartInput) -> dict[str, Any]:
        """Create a new, empty part document and make it active, ready for `create_sketch`.

        Uses `template` (a .prtdot) or the default part template, then sets `units` if
        given, and assigns `material` if given. The part is not saved until
        `save_part`. Needs: SolidWorks running. Changes: opens a new part document.
        Returns: `part` with `name`, `units`, `template` and `material` (the one
        assigned, or null), plus a `warning` when the material couldn't be assigned;
        the part is created either way.

        Args:
            input_data: The part. `name` is required but only a label: the reply
                gives SolidWorks' own name (Part1, ...); `template`, `units` (mm, cm, m, in, ft) and
                `material` (a name from `list_available_materials`) are optional.

        Returns:
            dict[str, Any]: The new part.
        """
        input_data = normalize_input(input_data, CreatePartInput)
        result = await adapter.create_part(
            input_data.name, input_data.units, input_data.template
        )

        require_success(result, context="Failed to create part")
        model = result.data
        part_name = _result_value(model, "name", default=input_data.name)
        units = _result_value(model, "units", default=input_data.units or "mm")
        material = None
        warning = None
        if input_data.material:
            assigned = await adapter.set_material(input_data.material)
            if assigned.is_success:
                material = input_data.material
            else:
                warning = (
                    f"The part was created without a material: "
                    f"'{input_data.material}' could not be assigned "
                    f"({assigned.error}). Use list_available_materials, then "
                    "set_material."
                )
        reply: dict[str, Any] = {
            "status": "success",
            "message": f"Created new part: {part_name}",
            "part": {
                "name": part_name,
                "units": units,
                "material": material,
                "template": input_data.template,
            },
            "execution_time": result.execution_time,
        }
        if warning:
            reply["warning"] = warning
        return reply

    @mcp.tool()
    async def create_assembly(input_data: CreateAssemblyInput) -> dict[str, Any]:
        """Create a new, empty assembly document and make it active, ready for `insert_component`.

        Uses `template` (an .asmdot) or the default assembly template. It is not saved
        until `save_assembly`. Needs: SolidWorks running. Changes: opens a new assembly
        document. Returns: `assembly` with `name` and `template`. Then call
        `insert_component` for each part.

        Args:
            input_data: The assembly. `name` is required but only a label: the
                reply gives SolidWorks' own name (Assem1, ...); `template` has a
                default.

        Returns:
            dict[str, Any]: The new assembly.
        """
        input_data = normalize_input(input_data, CreateAssemblyInput)
        result = await adapter.create_assembly(input_data.name, input_data.template)

        require_success(result, context="Failed to create assembly")
        model = result.data
        assembly_name = _result_value(model, "name", default=input_data.name)
        return {
            "status": "success",
            "message": f"Created new assembly: {assembly_name}",
            "assembly": {
                "name": assembly_name,
                "template": input_data.template,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_drawing(input_data: CreateDrawingInput) -> dict[str, Any]:
        """Create a new, empty drawing document and make it active.

        Uses `template` (a .drwdot) or the default drawing template. Then place views
        with `create_drawing_view`; for a whole drawing from a model in one call, use
        `generate_drawing_package` instead. Needs: SolidWorks running. Changes: opens a
        new drawing document. Returns: `drawing` with `name` and `template`.

        Args:
            input_data: The drawing. `name` is required but only a label: the
                reply gives SolidWorks' own name (Draw1, ...); `template` has a
                default.

        Returns:
            dict[str, Any]: The new drawing.
        """
        input_data = normalize_input(input_data, CreateDrawingInput)
        result = await adapter.create_drawing(input_data.name, input_data.template)

        require_success(result, context="Failed to create drawing")
        model = result.data
        drawing_name = _result_value(model, "name", default=input_data.name)
        return {
            "status": "success",
            "message": f"Created new drawing: {drawing_name}",
            "drawing": {
                "name": drawing_name,
                "template": input_data.template,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def close_model(input_data: CloseModelInput) -> dict[str, Any]:
        """Close the active document, saving it first only if asked.

        Use it when you are done with a document. Unsaved changes are lost unless `save`
        is on. Needs: an open, active document. Changes: closes the document, and with
        `save` writes it first. Returns: `saved`, whether a save was asked for.

        Args:
            input_data: The close. Nothing is required: `save` is off.

        Returns:
            dict[str, Any]: The close result.
        """
        input_data = normalize_input(input_data, CloseModelInput)
        result = await adapter.close_model(input_data.save)

        require_success(result, context="Failed to close model")
        return {
            "status": "success",
            "message": "Model closed successfully",
            "saved": input_data.save,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_extrusion(input_data: CreateExtrusionInput) -> dict[str, Any]:
        """Extrude the sketch you just closed into a solid boss, added to the active part.

        Use it after `exit_sketch`; to remove material, use `create_cut_extrude`. It
        extrudes the sketch `sketch_name` names, or the one `exit_sketch` closed last;
        a name that isn't in the feature tree fails before anything is built. End
        conditions: Blind (default), MidPlane, ThroughAll, UpToNext; `both_directions`
        means MidPlane. With `thin_feature` and `thin_thickness` it makes a wall from the
        sketch's outline, and an open chain works (a bent plate from its centreline).
        Needs: an open part and a closed profile (open is fine when thin). Changes:
        adds a Boss-Extrude feature. Returns: `extrusion` (`name`, `sketch` when one
        was named, `depth`, the `end_condition` SolidWorks used, `both_directions`,
        and `thin` when thin) and
        `measured_change` (volume, bounding box and solid-body count before and after;
        left out when the model can't be read).

        Args:
            input_data: The extrusion. `depth` (mm) is required; `sketch_name`,
                `end_condition`, `both_directions`, `reverse_direction`,
                `draft_angle`, `thin_feature`/`thin_thickness` and `merge_result`
                have defaults.

        Returns:
            dict[str, Any]: The created feature.
        """
        input_data = normalize_input(input_data, CreateExtrusionInput)
        # Convert input to ExtrusionParameters
        params = ExtrusionParameters(
            depth=input_data.depth,
            draft_angle=input_data.draft_angle,
            reverse_direction=input_data.reverse_direction,
            both_directions=input_data.both_directions,
            thin_feature=input_data.thin_feature,
            thin_thickness=input_data.thin_thickness,
            thin_side=input_data.thin_side,
            end_condition=input_data.end_condition,
            merge_result=input_data.merge_result,
            feature_scope=False,
            auto_select=True,
            sketch_name=input_data.sketch_name,
        )

        before = await _read_summary(adapter)
        result = await adapter.create_extrusion(params)

        require_success(result, context="Failed to create extrusion")
        after = await _read_summary(adapter) if before is not None else None
        feature = result.data
        name = _result_value(feature, "feature_name", "name", default="Extrusion")
        applied = _result_value(feature, "parameters", default=None) or {}
        extrusion: dict[str, Any] = {
            "name": name,
            "sketch": applied.get("sketch_name"),
            "depth": input_data.depth,
            "end_condition": applied.get("end_condition")
            or ("MidPlane" if input_data.both_directions else input_data.end_condition),
            "both_directions": input_data.both_directions,
        }
        if input_data.thin_feature:
            extrusion["thin"] = {
                "thickness": input_data.thin_thickness,
                "side": input_data.thin_side,
            }
        reply = {
            "status": "success",
            "message": f"Created extrusion: {name}",
            "extrusion": extrusion,
            "execution_time": result.execution_time,
        }
        return _attach_measured_change(reply, before, after)

    @mcp.tool()
    async def create_revolve(input_data: CreateRevolveInput) -> dict[str, Any]:
        """Revolve a closed sketch around an axis, adding a solid to the active part.

        Use it for turned shapes (shafts, knobs). It revolves the sketch `sketch_name`
        names, or the one `exit_sketch` closed last, about the reference axis
        `axis_entity` names (from `create_axis`), or about the centreline drawn in the
        sketch with `add_centerline`. A sketch or axis that isn't in the model fails
        before anything is built. Needs: an open part and a closed profile with a
        centreline or a reference axis. Changes: adds a Revolve feature. Returns:
        `revolve` (`name`, `sketch` and `axis_entity` when named, `angle`) and
        `measured_change` (volume, bounding box and solid-body count before and
        after; left out when the model can't be read).

        Args:
            input_data: The revolve. `angle` (degrees, 360 for a full turn) is
                required; `sketch_name`, `axis_entity`, `both_directions`,
                `reverse_direction`, `thin_feature` and `merge_result` have defaults.

        Returns:
            dict[str, Any]: The created feature.
        """
        input_data = normalize_input(input_data, CreateRevolveInput)
        # Convert input to RevolveParameters
        params = RevolveParameters(
            angle=input_data.angle,
            reverse_direction=input_data.reverse_direction,
            both_directions=input_data.both_directions,
            thin_feature=input_data.thin_feature,
            thin_thickness=input_data.thin_thickness,
            merge_result=input_data.merge_result,
            sketch_name=input_data.sketch_name,
            axis_entity=input_data.axis_entity,
        )

        before = await _read_summary(adapter)
        result = await adapter.create_revolve(params)

        require_success(result, context="Failed to create revolve")
        after = await _read_summary(adapter) if before is not None else None
        feature = result.data
        name = _result_value(feature, "feature_name", "name", default="Revolve")
        applied = _result_value(feature, "parameters", default=None) or {}
        reply = {
            "status": "success",
            "message": f"Created revolve: {name}",
            "revolve": {
                "name": name,
                "sketch": applied.get("sketch_name"),
                "axis_entity": applied.get("axis_entity"),
                "angle": input_data.angle,
            },
            "execution_time": result.execution_time,
        }
        return _attach_measured_change(reply, before, after)

    @mcp.tool()
    async def get_dimension(input_data: GetDimensionInput) -> dict[str, Any]:
        """Read one dimension of the active model by its full name, e.g. "D1@Sketch1".

        Find names with `list_model_dimensions`. Needs: an open part or assembly.
        Read-only. Returns: `dimension` with `name`, `value` and `units`: "mm" for a
        length, "deg" for an angle, and null for a count (such as a pattern's
        instance count).

        Args:
            input_data: The dimension. Give `name` (or `dimension_name`), e.g.
                "D1@Boss-Extrude1".

        Returns:
            dict[str, Any]: The dimension's value.
        """
        input_data = normalize_input(input_data, GetDimensionInput)
        result = await adapter.get_dimension(input_data.name)

        require_success(result, context="Failed to get dimension")
        reading = result.data or {}
        dimension_value = reading.get("value")
        dimension_units = reading.get("units")
        unit_text = f" {dimension_units}" if dimension_units else ""
        return {
            "status": "success",
            "message": f"Dimension {input_data.name} = {dimension_value}{unit_text}",
            "dimension": {
                "name": input_data.name,
                "value": dimension_value,
                "units": dimension_units,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def set_dimension(input_data: SetDimensionInput) -> dict[str, Any]:
        """Change one length dimension of the active model, in mm, and rebuild.

        Use it to resize an existing model; find the full name with
        `list_model_dimensions` and check the result with `rebuild_model` or
        `verify_model`. The value is always millimetres. Needs: an open part or
        assembly. Changes: the dimension and the geometry it drives. Returns:
        `dimension_update` with `name`, `old_value` and `new_value`.

        Args:
            input_data: The change. `value` (mm) is required, with `name` (or
                `dimension_name`) such as "D1@Sketch1"; `units` is not used.

        Returns:
            dict[str, Any]: The old and new values.
        """
        input_data = normalize_input(input_data, SetDimensionInput)
        result = await adapter.set_dimension(input_data.name, input_data.value)

        require_success(result, context="Failed to set dimension")
        payload = result.data
        return {
            "status": "success",
            "message": f"Set dimension {input_data.name} = {input_data.value} mm",
            "dimension_update": {
                "name": input_data.name,
                "old_value": _result_value(payload, "old_value"),
                "new_value": _result_value(
                    payload, "new_value", default=input_data.value
                ),
                "units": input_data.units or "mm",
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def set_units(input_data: SetUnitsInput) -> dict[str, Any]:
        """Set the active document's length units: mm, cm, m, in or ft.

        Use it on a new part before sketching if you want other than the template's
        units. Aliases such as "inch" are accepted. The setting is read back, so success
        means it applied. Tool inputs stay in mm whatever the document shows. Needs: an
        open, active document. Changes: the document's unit system. Returns: `data` with
        `unit_system`, the values applied and observed, and `verified`.

        Args:
            input_data: The units. `unit_system` (mm, cm, m, in or ft) is
                required.

        Returns:
            dict[str, Any]: The units set.
        """
        input_data = normalize_input(input_data, SetUnitsInput)
        result = await adapter.set_units(input_data.unit_system)
        require_success(result, context="Failed to set units")
        data = result.data if isinstance(result.data, dict) else {}
        applied = data.get("unit_system", input_data.unit_system)
        message = f"Set document units to {applied}"
        if data.get("verified") is None:
            message += " (could not be read back to confirm)"
        return {
            "status": "success",
            "message": message,
            "data": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_cut_extrude(input_data: CreateCutExtrudeInput) -> dict[str, Any]:
        """Cut material from the active part with a closed sketch profile: holes, slots, windows.

        Use it after `exit_sketch`. Pass `sketch_name` unless the sketch you just closed
        is the one: with no name only that sketch is used, and any doubt is an error
        naming the sketches found. A one-way cut that finds no material is retried the
        other way (`direction_flipped`). For a sketch inside the body, use
        ThroughAllBoth. Needs: an open part with a solid body. Changes: adds a
        Cut-Extrude feature. Returns: `cut_extrude` (`name`, `sketch_name`,
        `direction_flipped`) and `measured_change` (volume, bounding box and solid-body
        count before and after; left out when the model can't be read).

        Args:
            input_data: The cut. `depth` (mm) is required; `sketch_name`,
                `end_condition` (Blind, ThroughAll, ThroughAllBoth),
                `both_directions`, `reverse_direction` and `draft_angle` have
                defaults.

        Returns:
            dict[str, Any]: The created cut.
        """
        input_data = normalize_input(input_data, CreateCutExtrudeInput)
        params = ExtrusionParameters(
            depth=input_data.depth,
            draft_angle=input_data.draft_angle,
            reverse_direction=input_data.reverse_direction,
            both_directions=input_data.both_directions,
            thin_feature=False,
            thin_thickness=None,
            end_condition=input_data.end_condition,
            merge_result=False,
            feature_scope=False,
            auto_select=True,
            sketch_name=input_data.sketch_name,
        )
        before = await _read_summary(adapter)
        result = await adapter.create_cut_extrude(params)
        require_success(result, context="Failed to create cut-extrude")
        after = await _read_summary(adapter) if before is not None else None
        feature = result.data
        feature_params = _result_value(feature, "parameters", default={})
        sketch_used = (
            feature_params.get("sketch_name")
            if isinstance(feature_params, dict)
            else None
        ) or input_data.sketch_name
        flipped = bool(
            isinstance(feature_params, dict) and feature_params.get("direction_flipped")
        )
        name = _result_value(feature, "feature_name", "name", default="Cut-Extrude")
        message = f"Created cut-extrude: {name}"
        if flipped:
            message += (
                " (the requested direction found no material, so it cut the other way)"
            )
        reply = {
            "status": "success",
            "message": message,
            "cut_extrude": {
                "name": name,
                "depth": input_data.depth,
                "sketch_name": sketch_used,
                "direction_flipped": flipped,
            },
            "execution_time": result.execution_time,
        }
        return _attach_measured_change(reply, before, after)

    @mcp.tool()
    async def add_fillet(input_data: AddFilletInput) -> dict[str, Any]:
        """Round edges of the active part with a constant-radius fillet.

        Name the edges as "Edge<1>", a point "x,y,z" (metres) or "face:x,y,z" for every
        edge of a face; or leave `edge_names` empty to use the current selection (e.g.
        from `select_face_edges`). A fillet that leaves the volume unchanged is an
        error. Needs: an open part with a solid body. Changes: adds a Fillet feature.
        Returns: `fillet` with `name`, `radius` and `edges`.

        Args:
            input_data: The fillet. `radius` (mm) is required; `edge_names`
                defaults to the current selection.

        Returns:
            dict[str, Any]: The created fillet.
        """
        input_data = normalize_input(input_data, AddFilletInput)
        result = await adapter.add_fillet(input_data.radius, input_data.edge_names)
        require_success(result, context="Failed to add fillet")
        feature = result.data
        return {
            "status": "success",
            "message": f"Created fillet: {_result_value(feature, 'feature_name', 'name', default='Fillet')}",
            "fillet": {
                "name": _result_value(
                    feature, "feature_name", "name", default="Fillet"
                ),
                "radius": input_data.radius,
                "edges": input_data.edge_names,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_chamfer(input_data: AddChamferInput) -> dict[str, Any]:
        """Bevel edges of the active part with a chamfer: `distance` along one face at `angle` (45 by default).

        Name the edges as "Edge<1>", a point "x,y,z" (metres) or "face:x,y,z" for every
        edge of a face; or leave `edge_names` empty to use the current selection (e.g.
        from `select_face_edges`). At 45 degrees both legs equal `distance`; at another
        angle, `flip` swaps which face `distance` is measured on. Needs: an open part
        with a solid body. Changes: adds a Chamfer feature. Returns: `chamfer` with
        `name`, `distance`, `angle`, `flip` and `edges`.

        Args:
            input_data: The chamfer. `distance` (mm) is required; `edge_names`
                defaults to the current selection.

        Returns:
            dict[str, Any]: The created chamfer.
        """
        input_data = normalize_input(input_data, AddChamferInput)
        result = await adapter.add_chamfer(
            input_data.distance,
            input_data.edge_names,
            input_data.angle,
            input_data.flip,
        )
        require_success(result, context="Failed to add chamfer")
        feature = result.data
        return {
            "status": "success",
            "message": f"Created chamfer: {_result_value(feature, 'feature_name', 'name', default='Chamfer')}",
            "chamfer": {
                "name": _result_value(
                    feature, "feature_name", "name", default="Chamfer"
                ),
                "distance": input_data.distance,
                "angle": input_data.angle,
                "flip": input_data.flip,
                "edges": input_data.edge_names,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_sweep(input_data: CreateSweepInput) -> dict[str, Any]:
        """Sweep a closed profile sketch along a path sketch to make a solid (Insert > Boss/Base > Sweep).

        Draw the closed profile and the open path as two sketches first. The profile is
        the sketch you closed last that isn't the path. Needs: an open part with both
        sketches. Changes: adds a Sweep feature. Returns: `sweep` with `name`, `path`,
        `twist_along_path` and `twist_angle`.

        Args:
            input_data: The sweep. `path` (the path sketch's name) is required;
                `twist_along_path`, `twist_angle` (degrees) and `merge_result` have
                defaults.

        Returns:
            dict[str, Any]: The created sweep.
        """
        input_data = normalize_input(input_data, CreateSweepInput)
        params = SweepParameters(
            path=input_data.path,
            twist_along_path=input_data.twist_along_path,
            twist_angle=input_data.twist_angle,
            merge_result=input_data.merge_result,
        )
        result = await adapter.create_sweep(params)
        require_success(result, context="Failed to create sweep")
        feature = result.data
        return {
            "status": "success",
            "message": f"Created sweep: {_result_value(feature, 'feature_name', 'name', default='Sweep')}",
            "sweep": {
                "name": _result_value(feature, "feature_name", "name", default="Sweep"),
                "path": input_data.path,
                "twist_along_path": input_data.twist_along_path,
                "twist_angle": input_data.twist_angle,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_loft(input_data: CreateLoftInput) -> dict[str, Any]:
        """Loft a solid through two or more closed profile sketches, in order (Insert > Boss/Base > Loft).

        Use it for shapes that change section, such as a wing or a transition duct. Put
        each profile on its own plane (see `create_reference_plane`). Needs: an open
        part with the profile sketches. Changes: adds a Loft feature. Returns: `loft`
        with `name`, `profiles` and `guide_curves`.

        Args:
            input_data: The loft. `profiles` (sketch names, in order) is required;
                `guide_curves`, `start_tangent`, `end_tangent` and `merge_result`
                have defaults.

        Returns:
            dict[str, Any]: The created loft.
        """
        input_data = normalize_input(input_data, CreateLoftInput)
        params = LoftParameters(
            profiles=input_data.profiles,
            guide_curves=input_data.guide_curves,
            start_tangent=input_data.start_tangent,
            end_tangent=input_data.end_tangent,
            merge_result=input_data.merge_result,
        )
        result = await adapter.create_loft(params)
        require_success(result, context="Failed to create loft")
        feature = result.data
        return {
            "status": "success",
            "message": f"Created loft: {_result_value(feature, 'feature_name', 'name', default='Loft')}",
            "loft": {
                "name": _result_value(feature, "feature_name", "name", default="Loft"),
                "profiles": input_data.profiles,
                "guide_curves": input_data.guide_curves,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def insert_component(input_data: InsertComponentInput) -> dict[str, Any]:
        """Insert a part or sub-assembly file into the active assembly at a position in mm.

        Use it after `create_assembly`, then position components with `add_mate`. The
        file must contain solid geometry; an empty or missing file is an error. Needs:
        an open, active assembly and the file on disk. Changes: adds a component.
        Returns: `component` with its instance name (`component`, which `add_mate`
        takes), `file_path`, `position` and the counts before and after.

        Args:
            input_data: The component. `file_path` is required; `x`, `y`, `z` (mm
                from the assembly origin) default to 0.

        Returns:
            dict[str, Any]: The inserted component.
        """
        input_data = normalize_input(input_data, InsertComponentInput)
        result = await adapter.insert_component(
            input_data.file_path, input_data.x, input_data.y, input_data.z
        )
        require_success(result, context="Failed to insert component")
        data = result.data if isinstance(result.data, dict) else {}
        return {
            "status": "success",
            "message": f"Inserted {data.get('component', 'component')}",
            "component": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_mate(input_data: AddMateInput) -> dict[str, Any]:
        """Mate two components of the active assembly: coincident, concentric, parallel, perpendicular, tangent, distance or angle.

        Each side is a named reference plane or axis (`entity_a`, e.g. "Front Plane"),
        or a face given by a point on it (`face_a`, mm, in that part's own coordinates):
        a pin's round face and a hole's face make a concentric mate. Get instance names
        from `list_components`. Needs: an open assembly with both components. Changes:
        adds a mate and may move components. Returns: `mate` with `mate_type`,
        `components`, `entities`, `geometry_moved` and `moved_components`.

        Args:
            input_data: The mate. `component_a` and `component_b` are required;
                each side takes `entity_*` or `face_*` (default "Front Plane");
                `mate_type` (coincident), `alignment`, `distance` (mm) and `angle`
                (degrees) have defaults.

        Returns:
            dict[str, Any]: The mate added.
        """
        input_data = normalize_input(input_data, AddMateInput)
        result = await adapter.add_mate(
            input_data.component_a,
            input_data.component_b,
            input_data.entity_a or "Front Plane",
            input_data.entity_b or "Front Plane",
            input_data.mate_type,
            input_data.alignment,
            input_data.distance,
            input_data.angle,
            input_data.face_a,
            input_data.face_b,
        )
        require_success(result, context="Failed to add mate")
        data = result.data if isinstance(result.data, dict) else {}
        message = (
            f"Added {input_data.mate_type} mate between "
            f"{input_data.component_a} and {input_data.component_b}"
        )
        # SolidWorks accepts a mate whose constraint is already
        # satisfied and moves nothing. That is a real success, but
        # saying so plainly beats leaving the caller to infer it.
        if data.get("geometry_moved") is False:
            message += " (no component moved - the constraint was already satisfied)"
        return {
            "status": "success",
            "message": message,
            "mate": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def set_component_state(input_data: SetComponentStateInput) -> dict[str, Any]:
        """Fix or float a component of the active assembly, or make a sub-assembly rigid or flexible.

        Float a part so mates can position it (the first part inserted is fixed); fix
        one to hold it still. Make a sub-assembly flexible so its own mates move in
        this assembly (a crank whose knob turns). Only what you pass changes: the
        component's suppression, visibility and configuration are kept. Needs: an
        open assembly. Changes: the component's fixed or solving state. Returns:
        `state` with `component`, `before` and `after` (`fixed`, `flexible` and the
        properties kept).

        Args:
            input_data: The component and what to change: `fixed`, `flexible` or
                both.

        Returns:
            dict[str, Any]: The state before and after.
        """
        input_data = normalize_input(input_data, SetComponentStateInput)
        result = await adapter.set_component_state(
            input_data.component, input_data.fixed, input_data.flexible
        )
        require_success(result, context="Failed to set the component state")
        data = result.data if isinstance(result.data, dict) else {}
        after = data.get("after", {})
        words = []
        if input_data.fixed is not None:
            words.append("fixed" if after.get("fixed") else "floating")
        if input_data.flexible is not None:
            words.append("flexible" if after.get("flexible") else "rigid")
        return {
            "status": "success",
            "message": f"{input_data.component} is now {' and '.join(words)}",
            "state": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def list_components() -> dict[str, Any]:
        """List the top-level components of the active assembly by instance name, e.g. "bracket-1".

        Use it before `add_mate`, which takes these names. Needs: an open, active
        assembly. Read-only. Returns: `components` (instance names).
        """
        result = await adapter.list_components()
        require_success(result, context="Failed to list components")
        components = result.data if isinstance(result.data, list) else []
        return {
            "status": "success",
            "message": f"{len(components)} component(s) in the assembly",
            "components": components,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def delete_feature(input_data: DeleteFeatureInput) -> dict[str, Any]:
        """Delete a feature or sketch from the active model, along with the features that depend on it.

        Ask the user before you call it: deleting a sketch also deletes every feature
        built from it (a cut, a boss), and deleting a feature deletes everything that
        depends on it. Use `suppress_feature` instead when you might want it back. Find
        names and dependents with `list_features`. Needs: an open part or assembly.
        Changes: removes the feature and its dependents (`undo` can restore them).
        Returns: `data` with `deleted`, `removed_features` (every feature that went,
        dependents included), `features_before` and `features_after`; the message names
        the dependents, and a `warning` says so whenever more than the named feature
        went: tell the user.

        Args:
            input_data: The feature. `name` (e.g. "Fillet1") is required.

        Returns:
            dict[str, Any]: What was deleted.
        """
        input_data = normalize_input(input_data, DeleteFeatureInput)
        result = await adapter.delete_feature(input_data.name)
        require_success(result, context="Failed to delete feature")
        data = result.data if isinstance(result.data, dict) else {}
        removed = data.get("removed_features") or [input_data.name]
        dependents = [n for n in removed if n != input_data.name.split("@", 1)[0]]
        reply: dict[str, Any] = {
            "status": "success",
            "message": (
                f"Deleted {input_data.name}"
                + (
                    f" and its dependents: {', '.join(dependents)}"
                    if dependents
                    else ""
                )
            ),
            "data": data,
            "execution_time": result.execution_time,
        }
        if dependents:
            reply["warning"] = (
                f"Deleting {input_data.name} also deleted {', '.join(dependents)}. "
                "Tell the user; `undo` restores them."
            )
        return reply

    @mcp.tool()
    async def suppress_feature(input_data: SuppressFeatureInput) -> dict[str, Any]:
        """Suppress or unsuppress a feature of the active model; suppressed features stay in the tree but are not built.

        Use it to turn off a feature that breaks the rebuild without deleting it. Needs:
        an open part or assembly. Changes: the feature's suppression state, and its
        children's. Returns: `data` with `feature`, `requested`, `suppressed` (read
        back; null when unreadable) and `was_suppressed`.

        Args:
            input_data: The feature. `name` is required; `suppress` defaults to on
                (pass false to unsuppress).

        Returns:
            dict[str, Any]: The new state.
        """
        input_data = normalize_input(input_data, SuppressFeatureInput)
        result = await adapter.suppress_feature(input_data.name, input_data.suppress)
        require_success(result, context="Failed to suppress feature")
        data = result.data if isinstance(result.data, dict) else {}
        verb = "Suppressed" if input_data.suppress else "Unsuppressed"
        message = f"{verb} {input_data.name}"
        if data.get("suppressed") is None:
            message += " (state could not be read back)"
        return {
            "status": "success",
            "message": message,
            "data": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def rename_feature(input_data: RenameFeatureInput) -> dict[str, Any]:
        """Rename a feature in the active model's feature tree.

        Use it to give features meaningful names (e.g. "MountingHoles"). The new name is
        read back: a name another feature already uses is an error. Renaming to the
        current name succeeds with no change. Needs: an open part or assembly. Changes:
        the feature's name. Returns: `data` with `old_name`, `new_name`, `renamed` and,
        when unchanged, `reason`.

        Args:
            input_data: The rename. `old_name` and `new_name` are required.

        Returns:
            dict[str, Any]: The rename.
        """
        input_data = normalize_input(input_data, RenameFeatureInput)
        result = await adapter.rename_feature(input_data.old_name, input_data.new_name)
        require_success(result, context="Failed to rename feature")
        data = result.data if isinstance(result.data, dict) else {}
        if data.get("renamed") is False:
            return {
                "status": "success",
                "message": (
                    f"{input_data.old_name} unchanged: "
                    + str(data.get("reason") or "no change")
                ),
                "data": data,
                "execution_time": result.execution_time,
            }
        return {
            "status": "success",
            "message": (
                f"Renamed {data.get('old_name', input_data.old_name)} to "
                f"{data.get('new_name', input_data.new_name)}"
            ),
            "data": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def select_feature(input_data: SelectFeatureInput) -> dict[str, Any]:
        """Select a feature, sketch, plane or component by name in the active document.

        Use it to set up a selection that the next SolidWorks operation acts on.
        `append` adds to the selection instead of replacing it. A name that can't be
        selected is an error. Needs: an open document. Changes: the selection only.
        Returns: `data` with `selected_name` and `selection_count`.

        Args:
            input_data: The selection. `feature_name` is required; `append` is off.

        Returns:
            dict[str, Any]: The selection.
        """
        input_data = normalize_input(input_data, SelectFeatureInput)
        result = await adapter.select_feature(
            input_data.feature_name, input_data.append
        )
        require_success(result)
        data = result.data if isinstance(result.data, dict) else {}
        return {
            "status": "success",
            "message": (
                f"Selected {data.get('selected_name', input_data.feature_name)} "
                f"({data.get('selection_count', '?')} selected)"
            ),
            "data": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def select_face_edges(input_data: SelectFaceEdgesInput) -> dict[str, Any]:
        """Select every edge of one face of the active part, ready for `add_fillet` or `add_chamfer` with no edge names.

        Pick the face with one of `point` (a point on it, mm), `side` (top, bottom,
        front, back, right, left or +X..-Z; the furthest flat face that way) or `plane`
        (the face lying on that plane). `edge_type` narrows to linear or circular edges
        (hole rims). Needs: an open part. Changes: the selection only. Returns: `data`
        with `face`, `edges`, `selected_edges` and `selection_count`.

        Args:
            input_data: The face. Give exactly one of `point`, `side` or `plane`;
                `edge_type` (all) and `append` (off) have defaults.

        Returns:
            dict[str, Any]: The edges selected.
        """
        input_data = normalize_input(input_data, SelectFaceEdgesInput)
        result = await adapter.select_face_edges(
            input_data.point,
            input_data.side,
            input_data.plane,
            input_data.edge_type,
            input_data.append,
        )
        require_success(result)
        data = result.data if isinstance(result.data, dict) else {}
        kinds = sorted({e.get("curve", "other") for e in data.get("edges", [])})
        return {
            "status": "success",
            "message": (
                f"Selected {data.get('selected_edges', 0)} edge(s) "
                f"({', '.join(kinds)}) on the face at "
                f"{data.get('face', {}).get('center_mm')} mm"
            ),
            "data": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def rebuild_model(
        input_data: RebuildModelInput | None = None,
    ) -> dict[str, Any]:
        """Rebuild the active model and report each feature that fails, with the reason and a fix.

        Use it after `set_dimension` or other edits to confirm the model is sound.
        Failing features make the call an error whose `data.errors` names each one, its
        SolidWorks error, what it depends on and a `hint`; warnings alone still succeed.
        Needs: an open part or assembly. Changes: rebuilds the model. Returns: `data`
        with `rebuilt`, `errors`, `error_count`, `warning_count` and `summary`.

        Args:
            input_data: Nothing is required: `top_level_only` is off (a full
                rebuild).

        Returns:
            dict[str, Any]: The rebuild report.
        """
        input_data = normalize_input(input_data or {}, RebuildModelInput)
        result = require_success(
            await adapter.rebuild_model(input_data.top_level_only), with_data=True
        )
        data = result.data if isinstance(result.data, dict) else None
        response: dict[str, Any] = {
            "status": "success",
            "message": (data or {}).get("summary") or "Rebuilt",
            "execution_time": result.execution_time,
        }
        if data is not None:
            response["data"] = data
        return response

    @mcp.tool()
    async def undo(input_data: UndoInput | None = None) -> dict[str, Any]:
        """Undo the last operations in the active model (Edit > Undo).

        Use it to step back after a feature went wrong. SolidWorks accepts an undo with
        nothing to undo, so the reply says whether the feature tree changed. Needs: an
        open document. Changes: reverts the last `count` operations. Returns: `data`
        with `requested_steps`, `features_before`, `features_after` and `tree_changed`.

        Args:
            input_data: Nothing is required: `count` defaults to 1.

        Returns:
            dict[str, Any]: What changed.
        """
        input_data = normalize_input(input_data or UndoInput(), UndoInput)
        result = await adapter.undo(input_data.count)
        require_success(result, context="Failed to undo")
        data = result.data if isinstance(result.data, dict) else {}
        changed = data.get("tree_changed")
        if changed is False:
            message = (
                f"Undo of {input_data.count} step(s) left the feature "
                "tree unchanged - there was nothing to undo"
            )
        else:
            message = f"Undid {input_data.count} step(s)"
        return {
            "status": "success",
            "message": message,
            "data": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_reference_plane(
        input_data: CreateReferencePlaneInput,
    ) -> dict[str, Any]:
        """Add a reference plane offset from, at an angle to, or on a plane or flat face.

        Use it to sketch somewhere other than the three default planes: pass the new
        plane's name to `create_sketch`. Offset: `reference` and a non-zero `offset`
        (mm). Angle: `reference`, `angle` (degrees) and `axis`, an axis lying in the
        reference (make one with `create_axis`). Face: `face_point`, a point on a flat
        face (sloped or not), with an `offset` or 0 for a plane on the face. Needs: an
        open part. Changes: adds a plane feature. Returns: `plane` with `name` and the
        inputs used.

        Args:
            input_data: The plane. Exactly one of `reference` and `face_point`;
                then `offset`, or `angle` with `axis`; `flip` is off.

        Returns:
            dict[str, Any]: The new plane.
        """
        input_data = normalize_input(input_data, CreateReferencePlaneInput)
        result = await adapter.create_reference_plane(
            input_data.reference,
            input_data.offset,
            input_data.angle,
            input_data.flip,
            input_data.axis,
            input_data.face_point,
        )
        require_success(result, context="Failed to create reference plane")
        data = result.data if isinstance(result.data, dict) else {}
        return {
            "status": "success",
            "message": f"Created reference plane: {data.get('name', 'Plane')}",
            "plane": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_axis(input_data: CreateAxisInput) -> dict[str, Any]:
        """Add a reference axis along the model's x, y or z direction, through the origin.

        Use it before `pattern_circular`, which rotates features about an axis. It is
        built where two default planes meet. Needs: an open part. Changes: adds an axis
        feature. Returns: `axis` with `name` (e.g. "Axis1", for
        `create_reference_plane`'s `axis`), `reference`, `planes` and the feature counts.

        Args:
            input_data: The axis. `reference` ("x", "y" or "z") is required.

        Returns:
            dict[str, Any]: The new axis.
        """
        input_data = normalize_input(input_data, CreateAxisInput)
        result = await adapter.create_axis(input_data.reference)
        require_success(result, context="Failed to create axis")
        data = result.data if isinstance(result.data, dict) else {}
        axis_name = data.get("reference", input_data.reference)
        return {
            "status": "success",
            "message": f"Created {axis_name} axis",
            "axis": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def create_reference_point(
        input_data: CreateReferencePointInput,
    ) -> dict[str, Any]:
        """Add a reference point on an edge (at a distance or percentage along it) or at the centre of a face.

        Use it to mark a position for a later feature or measurement. Pick the edge or
        face with a point on it in mm. Needs: an open part with that edge or face.
        Changes: adds a point feature. Returns: `point` with `mode`, `at_mm`,
        `distance_mm` or `percent`, and the feature counts.

        Args:
            input_data: The point. `mode` (along_curve or face_center) and `x`,
                `y`, `z` (mm) are required; for along_curve give one of `distance`
                (mm) or `percent`.

        Returns:
            dict[str, Any]: The new point.
        """
        input_data = normalize_input(input_data, CreateReferencePointInput)
        result = await adapter.create_reference_point(
            input_data.mode,
            input_data.x,
            input_data.y,
            input_data.z,
            input_data.distance,
            input_data.percent,
        )
        require_success(result, context="Failed to create reference point")
        data = result.data if isinstance(result.data, dict) else {}
        where = (
            f"{data.get('percent')}% along a curve"
            if data.get("percent") is not None
            else (
                f"{data.get('distance_mm')} mm along a curve"
                if data.get("distance_mm") is not None
                else "a face centre"
            )
        )
        return {
            "status": "success",
            "message": f"Created a reference point at {where}",
            "point": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def mirror_feature(input_data: MirrorFeatureInput) -> dict[str, Any]:
        """Mirror solid bodies or features of the active part about a plane.

        Use it for symmetric parts: model one half, then mirror. Body mirroring (the
        default) is what works for lofts and sweeps; to mirror a cut, name the cut and
        set `mirror_bodies` off. It fails when the volume doesn't change, which means
        nothing was mirrored. Needs: an open part with the named bodies or features.
        Changes: adds a Mirror feature. Returns: `mirror` with `name`, `mirrored`,
        `mirror_plane`, `merge`, `mirror_bodies`, and `volume_before`,
        `volume_after` and `volume_change` (mm³, negative for a cut).

        Args:
            input_data: The mirror. `features` (at least one name) and
                `mirror_plane` (e.g. "Right Plane") are required; `merge` and
                `mirror_bodies` are on.

        Returns:
            dict[str, Any]: The mirror created.
        """
        input_data = normalize_input(input_data, MirrorFeatureInput)
        result = await adapter.mirror_feature(
            input_data.features,
            input_data.mirror_plane,
            input_data.merge,
            input_data.mirror_bodies,
        )
        require_success(result, context="Failed to mirror feature")
        data = result.data if isinstance(result.data, dict) else {}
        return {
            "status": "success",
            "message": f"Mirrored {', '.join(input_data.features)} "
            f"about {input_data.mirror_plane}",
            "mirror": data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def pattern_circular(input_data: PatternCircularInput) -> dict[str, Any]:
        """Repeat features of the active part around an axis, e.g. a ring of bosses or ribs.

        Create the axis first with `create_axis`. Bosses and cuts both work (a ring of
        holes is a patterned cut). It fails when the volume doesn't change, which
        means no instance landed on the part. Needs: an open part with the named
        features and axis. Changes: adds a circular pattern feature. Returns:
        `pattern` with `name`, `patterned`, `axis`, `count`, `angle`,
        `equal_spacing`, and `volume_before`, `volume_after` and `volume_change`
        (mm³, negative for cuts).

        Args:
            input_data: The pattern. `features` (at least one), `axis` and `count`
                (2 or more, the original included) are required; `angle` (360) and
                `equal_spacing` (on) have defaults.

        Returns:
            dict[str, Any]: The pattern created.
        """
        input_data = normalize_input(input_data, PatternCircularInput)
        result = await adapter.pattern_circular(
            input_data.features,
            input_data.axis,
            input_data.count,
            input_data.angle,
            input_data.equal_spacing,
        )
        require_success(result, context="Failed to pattern feature")
        data = result.data if isinstance(result.data, dict) else {}
        return {
            "status": "success",
            "message": f"Patterned {', '.join(input_data.features)} "
            f"around {input_data.axis} ({input_data.count} instances)",
            "pattern": data,
            "execution_time": result.execution_time,
        }

    # Counted from the registry rather than hand-maintained. The literal that
    # used to live here had drifted: it read 15 while 16 tools were actually
    # registered, so the function misreported its own work and the test
    # enshrined the wrong number.
    tool_count = len(await mcp.list_tools())
    return tool_count

register_server_admin_tools async

register_server_admin_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register the server-administration tools with FastMCP.

Parameters:

Name Type Description Default
mcp FastMCP

The MCP server.

required
adapter SolidWorksAdapter

Adapter used by every tool.

required
config dict[str, Any]

Server configuration (unused).

required

Returns:

Name Type Description
int int

Number of tools registered.

Source code in src/solidworks_mcp/tools/server_admin.py
async def register_server_admin_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register the server-administration tools with FastMCP.

    Args:
        mcp (FastMCP): The MCP server.
        adapter (SolidWorksAdapter): Adapter used by every tool.
        config (dict[str, Any]): Server configuration (unused).

    Returns:
        int: Number of tools registered.
    """
    mcp = with_tool_boundary(mcp)

    @mcp.tool()
    async def reset_circuit_breaker(
        input_data: ResetCircuitBreakerInput | None = None,
    ) -> dict[str, Any]:
        """Let calls through again to operations the server paused after repeated failures.

        Use it when a tool returns `error_type: circuit_open` and you have fixed what
        made the calls fail (a missing sketch, a wrong reference): without a reset, the
        operation stays paused until its timeout. Resetting does not fix the cause; a
        call that fails again counts towards a new pause. Changes: only the server's
        own state; no SolidWorks document is touched. Returns: `data` with `reset`, the
        operations that were paused (empty when none were).

        Args:
            input_data: Optional: `operation` resets only that operation; leave it out
                to reset every one.

        Returns:
            dict[str, Any]: The operations that were reset.
        """
        input_data = normalize_input(input_data, ResetCircuitBreakerInput)
        result = await adapter.reset_circuit_breakers(input_data.operation)
        require_success(result, context="Failed to reset the circuit breaker")
        names = list((result.data or {}).get("reset", []))
        message = f"Reset {', '.join(names)}" if names else "No operation was paused"
        return {"status": "success", "message": message, "data": {"reset": names}}

    return 1

register_sketching_tools async

register_sketching_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]) -> int

Register sketching tools with FastMCP.

Registers comprehensive sketching tools for SolidWorks automation including geometry creation, constraints, dimensions, and pattern operations.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config dict[str, Any]

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Example
from solidworks_mcp.tools.sketching import register_sketching_tools

tool_count = await register_sketching_tools(mcp, adapter, config)
print(f"Registered {tool_count} sketching tools")
Source code in src/solidworks_mcp/tools/sketching.py
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
async def register_sketching_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
) -> int:
    """Register sketching tools with FastMCP.

    Registers comprehensive sketching tools for SolidWorks automation including geometry
    creation, constraints, dimensions, and pattern operations.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (dict[str, Any]): Configuration values for the operation.

    Returns:
        int: The computed numeric result.

    Example:
                        ```python
                        from solidworks_mcp.tools.sketching import register_sketching_tools

                        tool_count = await register_sketching_tools(mcp, adapter, config)
                        print(f"Registered {tool_count} sketching tools")
                        ```
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0
    # SolidWorks won't rename a sketch while it is open for editing (it
    # accepts the name, but the tree keeps the old one), so the name
    # create_sketch was asked for is given when exit_sketch closes it:
    # SolidWorks' name -> the name asked for.
    names_on_exit: dict[str, str] = {}

    @mcp.tool()
    async def create_sketch(input_data: CreateSketchInput) -> dict[str, Any]:
        """Open a new sketch on a plane of the active part, ready for lines, circles and other sketch entities.

        Use it first in every feature: sketch, draw, `exit_sketch`, then
        `create_extrusion` or `create_cut_extrude`. Planes: Front, Top, Right (or XY,
        XZ, YZ), or a plane made by `create_reference_plane`. Coordinates in the sketch
        are mm from the origin. Needs: an open part. Changes: adds a sketch and opens it
        for editing; give `sketch_name` and `exit_sketch` names it that when it closes
        it (SolidWorks won't rename an open sketch). Returns: `sketch` with `name` (the
        name in the feature tree now) and `plane`, plus `name_on_exit` when you gave
        `sketch_name`.

        Args:
            input_data: The sketch. `plane` is required; `sketch_name` names it.

        Returns:
            dict[str, Any]: The new sketch.
        """
        input_data = normalize_input(input_data, CreateSketchInput)
        result = await adapter.create_sketch(input_data.plane)

        require_success(result, context="Failed to create sketch")
        sketch_name = (
            result.data.get("sketch_name")
            if isinstance(result.data, dict)
            else result.data
        )
        reply: dict[str, Any] = {
            "status": "success",
            "message": f"Created sketch '{sketch_name}' on {input_data.plane} plane",
            "sketch": {
                "name": sketch_name,
                "plane": input_data.plane,
            },
            "execution_time": result.execution_time,
        }
        names_on_exit.clear()  # a sketch left open is abandoned with its name
        wanted = (input_data.sketch_name or "").strip()
        if wanted and wanted != sketch_name:
            names_on_exit[str(sketch_name)] = wanted
            reply["sketch"]["name_on_exit"] = wanted
            reply["message"] += f"; exit_sketch names it '{wanted}'"
        return reply

    @mcp.tool()
    async def add_line(input_data: AddLineInput) -> dict[str, Any]:
        """Draw a straight line between two points in the open sketch, in mm.

        Use it for profiles made of straight edges; for a closed box, use
        `add_rectangle`, and for a revolve axis, `add_centerline`. Needs: an open sketch
        (`create_sketch`). Changes: adds a line (a construction line with
        `construction`). Returns: `line` with `id`, `start`, `end` and `length`. Its
        `id` is what `add_sketch_constraint` and `add_sketch_dimension` take.

        Args:
            input_data: The line. Give `x1`, `y1`, `x2`, `y2` (mm; or `start_x`,
                `start_y`, `end_x`, `end_y`); `construction` is off.

        Returns:
            dict[str, Any]: The line added.
        """
        input_data = normalize_input(input_data, AddLineInput)
        if hasattr(adapter, "add_sketch_line"):
            result = await adapter.add_sketch_line(
                input_data.x1,
                input_data.y1,
                input_data.x2,
                input_data.y2,
                input_data.construction,
            )
        else:
            result = await adapter.add_line(
                input_data.x1,  # type: ignore[arg-type]
                input_data.y1,  # type: ignore[arg-type]
                input_data.x2,  # type: ignore[arg-type]
                input_data.y2,  # type: ignore[arg-type]
            )

        require_success(result, context="Failed to add line")
        line_id = (
            result.data.get("entity_id") or result.data.get("id")
            if isinstance(result.data, dict)
            else result.data
        )
        x1, y1 = input_data.x1 or 0.0, input_data.y1 or 0.0
        x2, y2 = input_data.x2 or 0.0, input_data.y2 or 0.0
        return {
            "status": "success",
            "message": f"Added line from ({x1}, {y1}) to ({x2}, {y2})",
            "line": {
                "id": line_id,
                "start": {"x": x1, "y": y1},
                "end": {"x": x2, "y": y2},
                "construction": input_data.construction,
                "length": ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_circle(input_data: AddCircleInput) -> dict[str, Any]:
        """Draw a circle in the open sketch from its centre and radius, in mm.

        Use it for holes and round bosses. Needs: an open sketch (`create_sketch`).
        Changes: adds a circle (a construction circle with `construction`). Returns:
        `circle` with `id`, `center`, `radius` and `diameter`. Its `id` is what
        `add_sketch_constraint` and `add_sketch_dimension` take.

        Args:
            input_data: The circle. `center_x`, `center_y` and `radius` (mm) are
                required; `construction` is off.

        Returns:
            dict[str, Any]: The circle added.
        """
        input_data = normalize_input(input_data, AddCircleInput)
        if hasattr(adapter, "add_sketch_circle"):
            result = await adapter.add_sketch_circle(
                input_data.center_x,
                input_data.center_y,
                input_data.radius,
                input_data.construction,
            )
        else:
            result = await adapter.add_circle(
                input_data.center_x, input_data.center_y, input_data.radius
            )

        require_success(result, context="Failed to add circle")
        circle_id = (
            result.data.get("entity_id")
            if isinstance(result.data, dict)
            else result.data
        )
        return {
            "status": "success",
            "message": f"Added circle at ({input_data.center_x}, {input_data.center_y}) with radius {input_data.radius}mm",
            "circle": {
                "id": circle_id,
                "center": {"x": input_data.center_x, "y": input_data.center_y},
                "radius": input_data.radius,
                "construction": input_data.construction,
                "diameter": input_data.radius * 2,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_rectangle(input_data: AddRectangleInput) -> dict[str, Any]:
        """Draw a closed, axis-aligned rectangle in the open sketch from two opposite corners, in mm.

        Use it for blocks, plates and rectangular cut-outs. Needs: an open sketch
        (`create_sketch`). Changes: adds four connected lines. Returns: `rectangle` with
        `id`, `corner1`, `corner2`, `width`, `height` and `area`. Its `id` is what
        `add_sketch_constraint` and `add_sketch_dimension` take.

        Args:
            input_data: The rectangle. Give `x1`, `y1`, `x2`, `y2` (mm; or
                `corner1_x` ... `corner2_y`); `construction` is off.

        Returns:
            dict[str, Any]: The rectangle added.
        """
        input_data = normalize_input(input_data, AddRectangleInput)
        if hasattr(adapter, "add_sketch_rectangle"):
            result = await adapter.add_sketch_rectangle(
                input_data.x1,
                input_data.y1,
                input_data.x2,
                input_data.y2,
                input_data.construction,
            )
        else:
            result = await adapter.add_rectangle(
                input_data.x1,  # type: ignore[arg-type]
                input_data.y1,  # type: ignore[arg-type]
                input_data.x2,  # type: ignore[arg-type]
                input_data.y2,  # type: ignore[arg-type]
            )

        require_success(result, context="Failed to add rectangle")
        rect_id = (
            result.data.get("entity_id")
            if isinstance(result.data, dict)
            else result.data
        )
        rx1, ry1 = input_data.x1 or 0.0, input_data.y1 or 0.0
        rx2, ry2 = input_data.x2 or 0.0, input_data.y2 or 0.0
        width = abs(rx2 - rx1)
        height = abs(ry2 - ry1)

        return {
            "status": "success",
            "message": f"Added rectangle from ({rx1}, {ry1}) to ({rx2}, {ry2})",
            "rectangle": {
                "id": rect_id,
                "corner1": {"x": rx1, "y": ry1},
                "corner2": {"x": rx2, "y": ry2},
                "construction": input_data.construction,
                "width": width,
                "height": height,
                "area": width * height,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def exit_sketch() -> dict[str, Any]:
        """Close the open sketch so a feature can use it.

        Call it after drawing and before `create_extrusion`, `create_revolve` or
        `create_cut_extrude`; those use the sketch closed last. Needs: an open sketch.
        Changes: leaves sketch editing; the sketch stays in the feature tree, renamed
        to the `sketch_name` `create_sketch` was given. Returns: a message, plus, after
        a `sketch_name`, `sketch` with `name` (the name in the feature tree, which later
        tools take as `sketch_name`) and a `warning` when the name couldn't be given
        (another feature has it).
        """
        result = await adapter.exit_sketch()

        require_success(result, context="Failed to exit sketch")
        reply: dict[str, Any] = {
            "status": "success",
            "message": "Exited sketch editing mode",
            "execution_time": result.execution_time,
        }
        if names_on_exit:
            current, wanted = names_on_exit.popitem()
            renamed = await adapter.rename_feature(current, wanted)
            name = current
            if renamed.is_success:
                name = (renamed.data or {}).get("new_name") or wanted
            else:
                reply["warning"] = (
                    f"The sketch keeps SolidWorks' name '{current}': it could "
                    f"not be named '{wanted}' ({renamed.error})."
                )
            reply["sketch"] = {"name": name}
        return reply

    @mcp.tool()
    async def check_sketch_fully_defined(
        input_data: CheckSketchDefinitionInput | None = None,
    ) -> dict[str, Any]:
        """Check whether a sketch is fully defined, under-defined or over-defined.

        Use it after adding relations and dimensions, to see if the sketch still has
        free geometry. Needs: an open part with the sketch. Read-only. Returns: `sketch`
        with `sketch_name`, `is_fully_defined` (null when unknown), `definition_state`
        and where the answer came from (`source`).

        Args:
            input_data: Nothing is required: give `sketch_name`, or omit it for the
                open or last sketch.

        Returns:
            dict[str, Any]: The definition state.
        """
        normalized = normalize_input(
            input_data or {},
            CheckSketchDefinitionInput,
        )
        result = await adapter.check_sketch_fully_defined(normalized.sketch_name)

        require_success(result, context="Failed to inspect sketch definition")

        payload = result.data or {}
        state = payload.get("definition_state", "unknown")
        return {
            "status": "success",
            "message": f"Sketch definition state: {state}",
            "sketch": payload,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_arc(input_data: AddArcInput) -> dict[str, Any]:
        """Draw a circular arc in the open sketch from its centre, start point and end point, in mm.

        Use it for rounded ends and curved edges of a profile. Needs: an open sketch
        (`create_sketch`). Changes: adds an arc. Returns: `arc` with `id`, `center`,
        `start_point` and `end_point`. Its `id` is what `add_sketch_constraint` and
        `add_sketch_dimension` take.

        Args:
            input_data: The arc. `center_x`, `center_y`, `start_x`, `start_y`,
                `end_x` and `end_y` (mm) are all required.

        Returns:
            dict[str, Any]: The arc added.
        """
        input_data = normalize_input(input_data, AddArcInput)
        result = await adapter.add_arc(
            input_data.center_x,
            input_data.center_y,
            input_data.start_x,
            input_data.start_y,
            input_data.end_x,
            input_data.end_y,
        )

        require_success(result, context="Failed to add arc")
        arc_id = result.data
        return {
            "status": "success",
            "message": f"Added arc centered at ({input_data.center_x}, {input_data.center_y})",
            "arc": {
                "id": arc_id,
                "center": {"x": input_data.center_x, "y": input_data.center_y},
                "start_point": {
                    "x": input_data.start_x,
                    "y": input_data.start_y,
                },
                "end_point": {"x": input_data.end_x, "y": input_data.end_y},
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_spline(input_data: AddSplineInput) -> dict[str, Any]:
        """Draw a smooth spline through a list of points in the open sketch, in mm.

        Use it for free-form curves such as an aerofoil or a handle profile. Needs: an
        open sketch (`create_sketch`). Changes: adds a spline. Returns: `spline` with
        `id`, `control_points` and `point_count`.

        Args:
            input_data: The spline. `points` (a list of {"x", "y"} in mm) is
                required.

        Returns:
            dict[str, Any]: The spline added.
        """
        result = await adapter.add_spline(input_data.points)

        require_success(result, context="Failed to add spline")
        spline_id = result.data
        return {
            "status": "success",
            "message": f"Added spline with {len(input_data.points)} control points",
            "spline": {
                "id": spline_id,
                "control_points": input_data.points,
                "point_count": len(input_data.points),
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_centerline(input_data: AddLineInput) -> dict[str, Any]:
        """Draw a centreline (construction line) in the open sketch, in mm.

        Use it as the axis for `create_revolve`, or as the line for `sketch_mirror` and
        symmetric relations. It is never part of the profile. Needs: an open sketch
        (`create_sketch`). Changes: adds a centreline. Returns: `centerline` with `id`,
        `start_point` and `end_point`.

        Args:
            input_data: The line. Give `x1`, `y1`, `x2`, `y2` (mm; or `start_x`,
                `start_y`, `end_x`, `end_y`); `construction` is not used.

        Returns:
            dict[str, Any]: The centreline added.
        """
        input_data = normalize_input(input_data, AddLineInput)

        result = await adapter.add_centerline(
            input_data.x1,  # type: ignore[arg-type]
            input_data.y1,  # type: ignore[arg-type]
            input_data.x2,  # type: ignore[arg-type]
            input_data.y2,  # type: ignore[arg-type]
        )

        require_success(result, context="Failed to add centerline")
        centerline_id = result.data
        return {
            "status": "success",
            "message": f"Added centerline from ({input_data.x1}, {input_data.y1}) to ({input_data.x2}, {input_data.y2})",
            "centerline": {
                "id": centerline_id,
                "start_point": {"x": input_data.x1, "y": input_data.y1},
                "end_point": {"x": input_data.x2, "y": input_data.y2},
                "type": "construction",
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_polygon(input_data: dict[str, Any]) -> dict[str, Any]:
        """Draw a regular polygon (e.g. a hexagon) in the open sketch from its centre, radius and number of sides.

        Use it for nuts, hex bosses and pockets. Needs: an open sketch
        (`create_sketch`). Changes: adds the polygon's sides. Returns: `polygon` with
        `id`, `center`, `radius` and `sides`.

        Args:
            input_data: A mapping: `center_x`, `center_y` (mm, default 0), `radius`
                (mm, circumscribed, default 10) and `sides` (default 6).

        Returns:
            dict[str, Any]: The polygon added.
        """
        center_x = input_data.get("center_x", 0.0)
        center_y = input_data.get("center_y", 0.0)
        radius = input_data.get("radius", 10.0)
        sides = input_data.get("sides", 6)

        result = await adapter.add_polygon(center_x, center_y, radius, sides)

        require_success(result, context="Failed to add polygon")
        polygon_id = result.data
        return {
            "status": "success",
            "message": f"Added {sides}-sided polygon at ({center_x}, {center_y}) with radius {radius}mm",
            "polygon": {
                "id": polygon_id,
                "center": {"x": center_x, "y": center_y},
                "radius": radius,
                "sides": sides,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_ellipse(input_data: dict[str, Any]) -> dict[str, Any]:
        """Draw an ellipse in the open sketch from its centre and axis lengths, in mm.

        Use it for oval holes and bosses. Needs: an open sketch (`create_sketch`).
        Changes: adds an ellipse. Returns: `ellipse` with `id`, `center`, `major_axis`
        and `minor_axis`.

        Args:
            input_data: A mapping: `center_x`, `center_y` (mm, default 0),
                `major_axis` (default 20) and `minor_axis` (default 10), in mm.

        Returns:
            dict[str, Any]: The ellipse added.
        """
        center_x = input_data.get("center_x", 0.0)
        center_y = input_data.get("center_y", 0.0)
        major_axis = input_data.get("major_axis", 20.0)
        minor_axis = input_data.get("minor_axis", 10.0)

        result = await adapter.add_ellipse(center_x, center_y, major_axis, minor_axis)

        require_success(result, context="Failed to add ellipse")
        ellipse_id = result.data
        return {
            "status": "success",
            "message": f"Added ellipse at ({center_x}, {center_y}) with axes {major_axis}x{minor_axis}mm",
            "ellipse": {
                "id": ellipse_id,
                "center": {"x": center_x, "y": center_y},
                "major_axis": major_axis,
                "minor_axis": minor_axis,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_sketch_constraint(input_data: AddRelationInput) -> dict[str, Any]:
        """Add a geometric relation between sketch entities: horizontal, vertical, parallel, perpendicular, tangent, coincident, concentric, equal, symmetric, collinear or fix.

        Use it to hold design intent, then check with `check_sketch_fully_defined`.
        Entities are the `id`s the add_* tools returned. Needs: an open sketch
        (`create_sketch`). Changes: adds the relation. Returns: `constraint` with `id`,
        `type` and the entities.

        Args:
            input_data: The relation. `entity1` and `relation_type` are required;
                `entity2` when the relation needs two, and `entity3` (the
                centreline) only for symmetric.

        Returns:
            dict[str, Any]: The relation added.
        """
        input_data = normalize_input(input_data, AddRelationInput)
        result = await adapter.add_sketch_constraint(
            input_data.entity1,
            input_data.entity2,
            input_data.relation_type,
            input_data.entity3,
        )

        require_success(result, context="Failed to add constraint")
        constraint_id = result.data
        return {
            "status": "success",
            "message": f"Added {input_data.relation_type} constraint between {input_data.entity1} and {input_data.entity2}",
            "constraint": {
                "id": constraint_id,
                "type": input_data.relation_type,
                "entity1": input_data.entity1,
                "entity2": input_data.entity2,
                "entity3": input_data.entity3,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def add_sketch_dimension(input_data: AddDimensionInput) -> dict[str, Any]:
        """Add a driving dimension to sketch entities: linear, angular, radial or diameter.

        Use it to fix a size so the sketch is fully defined and can be changed later
        with `set_dimension`. Entities are the `id`s the add_* tools returned; two
        connected lines for angular, a circle or arc for radial or diameter. No dialog
        opens. Needs: an open sketch (`create_sketch`). Changes: adds the dimension and
        resizes the geometry to `value`. Returns: `dimension` with `id`, `type`, `value`
        and the entities.

        Args:
            input_data: The dimension. `entity1` and `value` (mm, or degrees for
                angular) are required; `entity2` and `dimension_type` (linear) have
                defaults.

        Returns:
            dict[str, Any]: The dimension added.
        """
        input_data = normalize_input(input_data, AddDimensionInput)
        result = await adapter.add_sketch_dimension(
            input_data.entity1,
            input_data.entity2,
            input_data.dimension_type,
            input_data.value,
        )

        require_success(result, context="Failed to add dimension")
        dimension_id = result.data
        return {
            "status": "success",
            "message": f"Added {input_data.dimension_type} dimension of {input_data.value}mm to {input_data.entity1}",
            "dimension": {
                "id": dimension_id,
                "type": input_data.dimension_type,
                "value": input_data.value,
                "entity1": input_data.entity1,
                "entity2": input_data.entity2,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def sketch_linear_pattern(input_data: dict[str, Any]) -> dict[str, Any]:
        """Repeat sketch entities in a straight line at a fixed spacing.

        Use it for rows of holes or slots before one cut. Needs: an open sketch
        (`create_sketch`). Changes: adds copies of the entities. Returns: `pattern` with
        `id`, `entities`, `count`, `spacing` and `direction`.

        Args:
            input_data: A mapping: `entities` (ids to repeat), `direction_x`/
                `direction_y` (default 1, 0), `spacing` (mm, default 10) and
                `count` (instances, default 3).

        Returns:
            dict[str, Any]: The pattern added.
        """
        entities = input_data.get("entities", [])
        direction_x = input_data.get("direction_x", 1.0)
        direction_y = input_data.get("direction_y", 0.0)
        spacing = input_data.get("spacing", 10.0)
        count = input_data.get("count", 3)

        result = await adapter.sketch_linear_pattern(
            entities, direction_x, direction_y, spacing, count
        )

        require_success(result, context="Failed to create linear pattern")
        pattern_id = result.data
        return {
            "status": "success",
            "message": f"Created linear pattern with {count} instances, spacing {spacing}mm",
            "pattern": {
                "id": pattern_id,
                "type": "linear",
                "entities": entities,
                "count": count,
                "spacing": spacing,
                "direction": {"x": direction_x, "y": direction_y},
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def sketch_circular_pattern(input_data: dict[str, Any]) -> dict[str, Any]:
        """Repeat sketch entities around the sketch origin, e.g. a bolt circle.

        The centre is always the sketch origin, so draw the first entity at the radius
        you want from it. Then cut once with `create_cut_extrude`. Needs: an open sketch
        (`create_sketch`). Changes: adds copies of the entities. Returns: `pattern` with
        `id`, `entities`, `count` and `angle`.

        Args:
            input_data: A mapping: `entities` (ids to repeat), `count` (instances,
                default 6) and `angle` (degrees spread over, default 360).

        Returns:
            dict[str, Any]: The pattern added.
        """
        entities = input_data.get("entities", [])
        angle = input_data.get("angle", 360.0)
        count = input_data.get("count", 6)

        result = await adapter.sketch_circular_pattern(entities, angle, count)

        require_success(result, context="Failed to create circular pattern")
        pattern_id = result.data
        return {
            "status": "success",
            "message": (
                f"Created circular pattern with {count} instances "
                "around the sketch origin"
            ),
            "pattern": {
                "id": pattern_id,
                "type": "circular",
                "entities": entities,
                "count": count,
                "center": {"x": 0.0, "y": 0.0},
                "angle": angle,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def sketch_mirror(input_data: dict[str, Any]) -> dict[str, Any]:
        """Mirror sketch entities about a centreline in the open sketch.

        Use it for symmetric profiles: draw one half, add a centreline with
        `add_centerline`, then mirror. Needs: an open sketch (`create_sketch`). Changes:
        adds mirrored copies. Returns: `mirror` with `id`, `entities` and `mirror_line`.

        Args:
            input_data: A mapping: `entities` (ids to mirror) and `mirror_line`
                (the centreline's id).

        Returns:
            dict[str, Any]: The mirror added.
        """
        entities = input_data.get("entities", [])
        mirror_line = input_data.get("mirror_line", "")

        result = await adapter.sketch_mirror(entities, mirror_line)

        require_success(result, context="Failed to mirror entities")
        mirror_id = result.data
        return {
            "status": "success",
            "message": f"Mirrored {len(entities)} entities about {mirror_line}",
            "mirror": {
                "id": mirror_id,
                "entities": entities,
                "mirror_line": mirror_line,
            },
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def sketch_offset(input_data: dict[str, Any]) -> dict[str, Any]:
        """Offset sketch entities by a distance in mm, outward or inward, e.g. for a wall or a clearance.

        Needs: an open sketch (`create_sketch`). Changes: adds the offset entities.
        Returns: `offset` with `id`, `entities`, `distance` and `direction` (outward or
        inward).

        Args:
            input_data: A mapping: `entities` (ids to offset), `offset_distance`
                (mm, default 5) and `reverse_direction` (inward; default off).

        Returns:
            dict[str, Any]: The offset added.
        """
        entities = input_data.get("entities", [])
        offset_distance = input_data.get("offset_distance", 5.0)
        reverse_direction = input_data.get("reverse_direction", False)

        result = await adapter.sketch_offset(
            entities, offset_distance, reverse_direction
        )

        require_success(result, context="Failed to create offset")
        offset_id = result.data
        direction = "outward" if not reverse_direction else "inward"
        return {
            "status": "success",
            "message": f"Created offset of {len(entities)} entities by {offset_distance}mm ({direction})",
            "offset": {
                "id": offset_id,
                "entities": entities,
                "distance": offset_distance,
                "direction": direction,
            },
            "execution_time": result.execution_time,
        }

    # Additional sketch tools can be added here in the future:
    # - add_arc (3-point arc, center-point arc)
    # - add_spline (free-form curves)
    # - add_dimension (dimensional constraints)
    # - add_relation (geometric constraints like parallel, perpendicular)
    # - add_pattern (circular, linear patterns)
    # - trim_entities (trim overlapping geometry)
    # - mirror_entities (mirror sketch geometry)

    @mcp.tool()
    async def sketch_tutorial_simple_hole() -> dict[str, Any]:
        """Demonstrate the sketch steps: draw a 5 mm circle at the origin on the Top plane and close the sketch.

        It is a fixed example; for a real hole, use `create_sketch`, `add_circle`,
        `exit_sketch` and `create_cut_extrude`. Needs: an open part. Changes: adds a
        sketch with one circle. Returns: `steps_completed`, `next_step` (cut the hole
        with `create_cut_extrude`) and `suggested_cut`, that call's input.
        """
        steps_completed = []

        # Step 1: Create sketch
        sketch_result = await adapter.create_sketch("Top")
        require_success(sketch_result, context="Failed to create sketch")
        sketch_name = (
            sketch_result.data.get("sketch_name")
            if isinstance(sketch_result.data, dict)
            else sketch_result.data
        )
        steps_completed.append(f"Created sketch: {sketch_name}")

        # Step 2: Add circle
        circle_result = await adapter.add_circle(0, 0, 2.5)
        require_success(circle_result, context="Failed to add circle")
        steps_completed.append(f"Added 5mm diameter circle: {circle_result.data}")

        # Step 3: Exit sketch
        exit_result = await adapter.exit_sketch()
        require_success(exit_result, context="Failed to exit sketch")
        steps_completed.append("Exited sketch")

        return {
            "status": "success",
            "message": "Tutorial completed: Simple hole sketch created",
            "steps_completed": steps_completed,
            "next_step": (
                "Cut the hole through the part with create_cut_extrude, using "
                "suggested_cut as its input"
            ),
            "suggested_cut": {
                "sketch_name": sketch_name,
                "depth": 10.0,
                "end_condition": "ThroughAll",
            },
        }

    @mcp.tool()
    async def tutorial_simple_hole(
        input_data: TutorialSimpleHoleInput,
    ) -> dict[str, Any]:
        """Draw a circle on a plane and cut it as a hole, as one worked example.

        Not available yet on real SolidWorks: its cut step has no COM implementation,
        so the call draws and closes the sketch, then fails. It is planned for a future
        release. Use `create_sketch`, `add_circle`, `exit_sketch` and
        `create_cut_extrude` instead. Needs: an open part with a solid body.
        Changes: adds a sketch (and, with the mock adapter, a cut). Returns: `tutorial`
        with `plane`, `diameter`, `depth` and `steps`.

        Args:
            input_data: The hole. `plane`, `center_x`, `center_y` and `diameter`
                (mm) are required; `depth` defaults to the diameter.

        Returns:
            dict[str, Any]: The steps run.
        """
        steps: list[dict[str, Any]] = []

        sketch_result = await adapter.create_sketch(input_data.plane)
        require_success(sketch_result, context="Failed to create sketch")
        steps.append({"step": "Create sketch", "status": "success"})

        circle_result = await adapter.add_sketch_circle(
            input_data.center_x,
            input_data.center_y,
            input_data.diameter / 2,
            False,
        )
        require_success(circle_result, context="Failed to add circle")
        steps.append({"step": "Add circle", "status": "success"})

        exit_result = await adapter.exit_sketch()
        require_success(exit_result, context="Failed to exit sketch")
        steps.append({"step": "Exit sketch", "status": "success"})

        cut_depth = (
            input_data.depth if input_data.depth is not None else input_data.diameter
        )
        cut_result = await adapter.create_cut("HoleSketch", cut_depth)
        require_success(cut_result, context="Failed to create cut extrude")
        steps.append({"step": "Create cut extrude", "status": "success"})

        return {
            "status": "success",
            "message": "Completed simple hole tutorial",
            "tutorial": {
                "plane": input_data.plane,
                "diameter": input_data.diameter,
                "depth": input_data.depth,
                "steps": steps,
            },
        }

    tool_count = 6  # Legacy count expected by tests
    return tool_count

register_template_management_tools async

register_template_management_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: Any) -> int

Register template management tools with FastMCP.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config Any

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Example

tool_count = await register_template_management_tools(mcp, adapter, config)

Source code in src/solidworks_mcp/tools/template_management.py
async def register_template_management_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: Any
) -> int:
    """Register template management tools with FastMCP.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (Any): Configuration values for the operation.

    Returns:
        int: The computed numeric result.

    Example:
                        >>> tool_count = await register_template_management_tools(mcp, adapter, config)
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0

    @mcp.tool()
    async def extract_template(input_data: TemplateExtractionInput) -> dict[str, Any]:
        """Capture a model's settings (custom properties, dimensions, units) as a reusable template file.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Changes: once
        available, writes the template to `save_path`, replacing any file there.
        Returns: once available, the captured settings under `data`.

        Args:
            input_data: The extraction. `source_model`, `template_name`,
                `template_type` and `save_path` are required;
                `include_custom_properties` and `include_dimensions` default to on.

        Returns:
            dict[str, Any]: The template, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "extract_template",
            "Active adapter does not support extract_template; no "
            f"template was extracted from {input_data.source_model}.",
        )
        result = await adapter.extract_template(input_data.model_dump())
        require_success(result, "Failed to extract template")
        return {
            "status": "success",
            "message": f"Template '{input_data.template_name}' extracted from {input_data.source_model}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def apply_template(input_data: TemplateApplicationInput) -> dict[str, Any]:
        """Apply a saved template's properties, dimensions and materials to a model.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. To set a
        material or a property today, use `set_material` or `set_custom_property`.
        Changes: once available, overwrites the target model's matching settings.
        Returns: once available, what changed under `data`.

        Args:
            input_data: The application. `template_path` and `target_model` are
                required; `overwrite_existing`, `apply_dimensions` and
                `apply_materials` have defaults.

        Returns:
            dict[str, Any]: The changes, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "apply_template",
            "Active adapter does not support apply_template; "
            f"{input_data.target_model} was not changed.",
        )
        result = await adapter.apply_template(input_data.model_dump())
        require_success(result, "Failed to apply template")
        return {
            "status": "success",
            "message": f"Template applied to {input_data.target_model}",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def batch_apply_template(input_data: TemplateBatchInput) -> dict[str, Any]:
        """Apply one template to every matching model in a folder.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Changes: once
        available, overwrites settings in each matching file, optionally keeping
        backups. Returns: once available, a per-file result under `data`.

        Args:
            input_data: The batch. `template_path` and `source_folder` are
                required; `file_pattern`, `recursive` and `backup_originals` have
                defaults.

        Returns:
            dict[str, Any]: The results, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "batch_apply_template",
            "Active adapter does not support batch_apply_template; "
            f"no files in {input_data.source_folder} were changed.",
        )
        result = await adapter.batch_apply_template(input_data.model_dump())
        require_success(result, "Failed batch template application")
        return {
            "status": "success",
            "message": "Batch template application completed",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    @mcp.tool()
    async def compare_templates(input_data: TemplateComparisonInput) -> dict[str, Any]:
        """Compare two template files on disk: whether they are byte-identical, and how their sizes differ.

        Use it to tell whether two templates are the same file. It does not read the
        templates' settings, so it can't say which properties or dimensions differ.
        Needs: both files on disk. Read-only. Returns: `comparison` with `file_1` and
        `file_2` (size, modified time, SHA-256), `identical` and `size_delta_bytes`.

        Args:
            input_data: The two templates: `template1_path` and `template2_path`,
                both required.

        Returns:
            dict[str, Any]: The comparison.
        """
        file_1 = file_fact_snapshot(input_data.template1_path)
        file_2 = file_fact_snapshot(input_data.template2_path)

        missing = [
            path
            for path, snapshot in (
                (input_data.template1_path, file_1),
                (input_data.template2_path, file_2),
            )
            if snapshot is None
        ]
        if missing:
            raise ToolOperationError(
                "Cannot compare templates; file(s) not found on "
                f"disk: {', '.join(missing)}. No template-property "
                "diff was performed."
            )

        assert file_1 is not None
        assert file_2 is not None
        identical = file_1["sha256"] == file_2["sha256"]

        return {
            "status": "success",
            "message": (
                "Template files are byte-identical"
                if identical
                else "Template files differ on disk"
            ),
            "comparison": {
                "file_1": file_1,
                "file_2": file_2,
                "identical": identical,
                "size_delta_bytes": file_2["size_bytes"] - file_1["size_bytes"],
            },
            "note": (
                "This is a filesystem-level comparison only; template "
                "properties, dimensions, and formatting were not "
                "analyzed."
            ),
        }

    @mcp.tool()
    async def save_to_template_library(input_data: dict[str, Any]) -> dict[str, Any]:
        """Add a template to a shared, categorised template library.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Changes: once
        available, copies the template into the library, replacing an entry with the
        same name. Returns: once available, the library entry under `data`.

        Args:
            input_data: A mapping with the `template_name` and the template's path.

        Returns:
            dict[str, Any]: The entry, or a `not_supported` error.
        """
        if hasattr(adapter, "save_to_template_library"):
            result = await adapter.save_to_template_library(input_data)
            require_success(result, "Failed to save to library")
            return {
                "status": "success",
                "message": "Template saved to library",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        template_name = input_data.get("template_name", "")

        raise UnsupportedOperationError(
            "Active adapter does not support save_to_template_library; "
            f"'{template_name}' was not saved to any library. There is "
            "no local template registry — this tool needs a real "
            "adapter-backed library service."
        )

    @mcp.tool()
    async def list_template_library(input_data: dict[str, Any]) -> dict[str, Any]:
        """List the templates in the shared template library.

        Not available yet: no adapter implements it, so every call returns an error with
        `error_type: "not_supported"`. It is planned for a future release. Read-only.
        Returns: once available, the library's templates under `data`.

        Args:
            input_data: A mapping of optional filters; none is required.

        Returns:
            dict[str, Any]: The templates, or a `not_supported` error.
        """
        require_capability(
            adapter,
            "list_template_library",
            "Active adapter does not support list_template_library; "
            "there is no local template registry to list.",
        )
        result = await adapter.list_template_library(input_data)
        require_success(result, "Failed to list template library")
        return {
            "status": "success",
            "message": "Template library listed successfully",
            "data": result.data,
            "execution_time": result.execution_time,
        }

    tool_count = 6  # Template management tools
    return tool_count

register_tools async

register_tools(mcp: FastMCP, adapter: Any, config: Any) -> int

Register all SolidWorks MCP tools.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter Any

Adapter instance used for the operation.

required
config Any

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Source code in src/solidworks_mcp/tools/__init__.py
async def register_tools(mcp: FastMCP, adapter: Any, config: Any) -> int:
    """Register all SolidWorks MCP tools.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (Any): Adapter instance used for the operation.
        config (Any): Configuration values for the operation.

    Returns:
        int: The computed numeric result.
    """
    logger.info("Registering SolidWorks MCP tools...")

    # Register tool categories
    await register_modeling_tools(mcp, adapter, config)
    await register_sketching_tools(mcp, adapter, config)
    await register_drawing_tools(mcp, adapter, config)
    await register_drawing_analysis_tools(mcp, adapter, config)
    await register_drawing_automation_tools(mcp, adapter, config)
    await register_engineering_data_tools(mcp, adapter, config)
    await register_analysis_tools(mcp, adapter, config)
    await register_export_tools(mcp, adapter, config)
    await register_automation_tools(mcp, adapter, config)
    await register_file_management_tools(mcp, adapter, config)
    await register_vba_generation_tools(mcp, adapter, config)
    await register_template_management_tools(mcp, adapter, config)
    await register_macro_recording_tools(mcp, adapter, config)
    await register_docs_discovery_tools(mcp, adapter, config)
    await register_server_admin_tools(mcp, adapter, config)

    tool_count = len(await mcp.list_tools())

    logger.info(f"Registered {tool_count} SolidWorks tools")
    return tool_count

register_vba_generation_tools async

register_vba_generation_tools(mcp: FastMCP, adapter: SolidWorksAdapter, config: Any) -> int

Register VBA generation tools with FastMCP.

Parameters:

Name Type Description Default
mcp FastMCP

The mcp value.

required
adapter SolidWorksAdapter

Adapter instance used for the operation.

required
config Any

Configuration values for the operation.

required

Returns:

Name Type Description
int int

The computed numeric result.

Example

tool_count = await register_vba_generation_tools(mcp, adapter, config)

Source code in src/solidworks_mcp/tools/vba_generation.py
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
async def register_vba_generation_tools(
    mcp: FastMCP, adapter: SolidWorksAdapter, config: Any
) -> int:
    """Register VBA generation tools with FastMCP.

    Args:
        mcp (FastMCP): The mcp value.
        adapter (SolidWorksAdapter): Adapter instance used for the operation.
        config (Any): Configuration values for the operation.

    Returns:
        int: The computed numeric result.

    Example:
                        >>> tool_count = await register_vba_generation_tools(mcp, adapter, config)
    """
    mcp = with_tool_boundary(mcp)
    tool_count = 0

    @mcp.tool()
    async def generate_vba_extrusion(input_data: VBAExtrusionInput) -> dict[str, Any]:
        """Write a VBA macro that makes an extrusion with the full FeatureExtrusion options: both directions, draft, thin wall, end conditions.

        Use it when `create_extrusion` doesn't cover an option you need, or to hand the
        step to someone as a macro. The macro comes back as text for the SolidWorks VBA
        editor (Alt+F11); `execute_macro` runs only binary .swp files, so it can't run
        this directly. Read-only. Nothing runs in SolidWorks. Returns: `vba_code`, the
        `parameters` used and `usage_instructions`.

        Args:
            input_data: The extrusion. `depth` (mm) is required; everything else
                (`sketch_name`, draft, thin-wall and end-condition options) has a
                default.

        Returns:
            dict[str, Any]: The macro text.
        """
        if hasattr(adapter, "generate_vba_extrusion"):
            result = await adapter.generate_vba_extrusion(input_data.model_dump())
            require_success(result, "Failed to generate VBA code")
            return {
                "status": "success",
                "message": "Generated VBA code for advanced extrusion",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        vba_code = f"""Sub CreateAdvancedExtrusion()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swFeatMgr As SldWorks.FeatureManager
Dim swFeat As SldWorks.Feature
Dim myFeature As Object

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc

If swModel Is Nothing Then
    MsgBox "No active document"
    Exit Sub
End If

Set swFeatMgr = swModel.FeatureManager

' Advanced extrusion with full parameter control
myFeature = swFeatMgr.FeatureExtruThin2( _
    {input_data.depth / 1000.0}, _  ' Depth (meters)
    {input_data.depth2 / 1000.0}, _ ' Depth2 (meters)
    {str(input_data.both_directions).lower()}, _ ' BothDirections
    {input_data.draft_angle * 3.14159 / 180.0}, _ ' DraftAngle (radians)
    {0}, _ ' DraftAngle2
    {str(input_data.draft_outward).lower()}, _ ' DraftOutward
    {str(input_data.draft_outward).lower()}, _ ' DraftOutward2
    {str(input_data.merge_result).lower()}, _ ' MergeResult
    {str(input_data.feature_scope).lower()}, _ ' FeatureScope
    {str(input_data.auto_select).lower()}, _ ' AutoSelect
    {input_data.thin_thickness / 1000.0}, _ ' Thickness (meters)
    {input_data.thin_thickness / 1000.0}, _ ' Thickness2
    False, _ ' ReverseOffset
    {str(input_data.both_directions).lower()}, _ ' BothDirectionThin
    {str(input_data.cap_ends).lower()}, _ ' CapEnds
    0, _ ' EndCondition
    0 _ ' EndCondition2
)

If myFeature Is Nothing Then
    MsgBox "Failed to create extrusion"
Else
    MsgBox "Advanced extrusion created successfully"
End If

End Sub"""

        return {
            "status": "success",
            "message": "Generated VBA code for advanced extrusion",
            "vba_code": vba_code,
            "parameters": {
                "depth": input_data.depth,
                "both_directions": input_data.both_directions,
                "thin_feature": input_data.thin_feature,
                "parameter_count": 17,
                "complexity": "complex",
            },
            "usage_instructions": [
                "1. Open SolidWorks VBA editor (Tools > Macro > Edit)",
                "2. Create new module and paste the generated code",
                "3. Ensure you have an active sketch selected",
                "4. Run the macro (F5)",
            ],
        }

    @mcp.tool()
    async def generate_vba_revolve(input_data: VBARevolveInput) -> dict[str, Any]:
        """Write a VBA macro that revolves the selected sketch, with both-direction and thin-wall options.

        Use it when `create_revolve` doesn't cover an option you need. The macro comes
        back as text for the SolidWorks VBA editor (Alt+F11); `execute_macro` runs only
        binary .swp files, so it can't run this directly. Read-only. Nothing runs in
        SolidWorks. Returns: `vba_code`, the `parameters` used and `usage_instructions`.

        Args:
            input_data: The revolve. Give `angle` in degrees (it defaults to 0, a
                revolve of nothing); `sketch_name`, `axis_reference`, both-direction
                and thin-wall options have defaults.

        Returns:
            dict[str, Any]: The macro text.
        """
        if hasattr(adapter, "generate_vba_revolve"):
            result = await adapter.generate_vba_revolve(input_data.model_dump())
            require_success(result, "Failed to generate VBA code")
            return {
                "status": "success",
                "message": "Generated VBA code for advanced revolve",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        vba_code = f"""Sub CreateAdvancedRevolve()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swFeatMgr As SldWorks.FeatureManager
Dim myFeature As Object

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc
Set swFeatMgr = swModel.FeatureManager

' Advanced revolve with thin-wall options
myFeature = swFeatMgr.FeatureRevolve2( _
    True, _ ' SingleDir
    {str(input_data.both_directions).lower()}, _ ' IsSolid
    False, _ ' IsThin
    False, _ ' ReverseDir
    3, _ ' Type (swEndCondBlind)
    3, _ ' Type2
    {(input_data.angle or 0.0) * 3.14159 / 180.0}, _ ' Angle (radians)
    {(input_data.angle2 or 0.0) * 3.14159 / 180.0}, _ ' Angle2
    {str(input_data.merge_result).lower()}, _ ' MergeResult
    False, _ ' FeatureScope
    False, _ ' Auto
    True, _ ' AssemblyFeatureScope
    0, _ ' AutoSelectComponents
    False, _ ' PropagateFeatureToParts
    True, _ ' CreateSelectionSet
    False, _ ' CurveToOrderUse
    False _ ' UseMachinedSurface
)

If myFeature Is Nothing Then
    MsgBox "Failed to create revolve"
Else
    MsgBox "Advanced revolve created successfully"
End If

End Sub"""

        return {
            "status": "success",
            "message": "Generated VBA code for advanced revolve",
            "vba_code": vba_code,
            "parameters": {
                "angle": input_data.angle,
                "both_directions": input_data.both_directions,
                "thin_feature": input_data.thin_feature,
                "parameter_count": 16,
                "complexity": "complex",
            },
            "usage_instructions": [
                "1. Create a sketch profile and axis of revolution",
                "2. Select the sketch and axis",
                "3. Run the generated VBA macro",
                "4. Check the revolve feature in the feature tree",
            ],
        }

    @mcp.tool()
    async def generate_vba_assembly_insert(
        input_data: VBAAssemblyInput,
    ) -> dict[str, Any]:
        """Write a VBA macro that inserts a component into the active assembly at a point.

        To insert a component directly, use `insert_component`. The macro comes back as
        text for the SolidWorks VBA editor (Alt+F11); `execute_macro` runs only binary
        .swp files, so it can't run this directly. Read-only. Nothing runs in
        SolidWorks. Returns: `vba_code`, the `parameters` used and `usage_instructions`.

        Args:
            input_data: The component. Give `component_path` (the .sldprt or
                .sldasm); `insertion_point` and `rotation` default to the origin.

        Returns:
            dict[str, Any]: The macro text.
        """
        if hasattr(adapter, "generate_vba_assembly_insert"):
            result = await adapter.generate_vba_assembly_insert(input_data.model_dump())
            require_success(result, "Failed to generate VBA code")
            return {
                "status": "success",
                "message": "Generated VBA code for component insertion",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        vba_code = f'''Sub InsertComponent()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swAssy As SldWorks.AssemblyDoc
Dim swComp As SldWorks.Component2

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc
Set swAssy = swModel

' Insert component at specified location
Set swComp = swAssy.AddComponent5( _
    "{input_data.component_path}", _
    0, _ ' Configuration
    "", _ ' Config name
    False, _ ' LoadModel
    "", _ ' ReferenceName
    {input_data.insertion_point[0] / 1000.0}, _ ' X position (meters)
    {input_data.insertion_point[1] / 1000.0}, _ ' Y position (meters)
    {input_data.insertion_point[2] / 1000.0} _ ' Z position (meters)
)

If swComp Is Nothing Then
    MsgBox "Failed to insert component"
Else
    MsgBox "Component inserted successfully: " & swComp.Name2
End If

End Sub'''

        return {
            "status": "success",
            "message": "Generated VBA code for component insertion",
            "vba_code": vba_code,
            "parameters": {
                "component_path": input_data.component_path,
                "insertion_point": input_data.insertion_point,
                "operation_type": input_data.operation_type,
            },
            "usage_instructions": [
                "1. Open the target assembly document",
                "2. Ensure the component file path exists",
                "3. Run the macro to insert the component",
                "4. Add mates as needed for positioning",
            ],
        }

    @mcp.tool()
    async def generate_vba_drawing_views(input_data: VBADrawingInput) -> dict[str, Any]:
        """Write a VBA macro that places a model's standard views (front, top, right) on the active drawing.

        To make a drawing directly, use `generate_drawing_package` or
        `create_drawing_view`. The macro comes back as text for the SolidWorks VBA
        editor (Alt+F11); `execute_macro` runs only binary .swp files, so it can't run
        this directly. Read-only. Nothing runs in SolidWorks. Returns: `vba_code`, the
        `parameters` used and `usage_instructions`.

        Args:
            input_data: The views. Give `model_path` (the model to draw);
                `scale`, `sheet_format` and the view layout have defaults. The
                drawing must be open and active when the macro runs.

        Returns:
            dict[str, Any]: The macro text.
        """
        if hasattr(adapter, "generate_vba_drawing_views"):
            result = await adapter.generate_vba_drawing_views(input_data.model_dump())
            require_success(result, "Failed to generate VBA code")
            return {
                "status": "success",
                "message": "Generated VBA code for drawing views",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        vba_code = f'''Sub CreateDrawingViews()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swDraw As SldWorks.DrawingDoc
Dim swView As SldWorks.View
Dim swSheet As SldWorks.Sheet

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc
Set swDraw = swModel

If swDraw Is Nothing Then
    MsgBox "No active drawing document"
    Exit Sub
End If

Set swSheet = swDraw.GetCurrentSheet

' Create standard 3 view drawing
' Front view
Set swView = swDraw.CreateDrawViewFromModelView3( _
    "{input_data.model_path}", _
    "*Front", _
    0.1, _ ' X position
    0.15, _ ' Y position
    0 _ ' Z position
)

swView.ScaleRatio = Array({input_data.scale}, 1)

' Top view
Set swView = swDraw.CreateDrawViewFromModelView3( _
    "{input_data.model_path}", _
    "*Top", _
    0.1, _
    0.25, _
    0 _
)

swView.ScaleRatio = Array({input_data.scale}, 1)

' Right view
Set swView = swDraw.CreateDrawViewFromModelView3( _
    "{input_data.model_path}", _
    "*Right", _
    0.25, _
    0.15, _
    0 _
)

swView.ScaleRatio = Array({input_data.scale}, 1)

MsgBox "Drawing views created successfully"

End Sub'''

        return {
            "status": "success",
            "message": "Generated VBA code for drawing views",
            "vba_code": vba_code,
            "parameters": {
                "model_path": input_data.model_path,
                "scale": input_data.scale,
                "sheet_format": input_data.sheet_format,
            },
            "usage_instructions": [
                "1. Create a new drawing document",
                "2. Verify the 3D model path is correct",
                "3. Run the macro to create standard views",
                "4. Adjust view positions as needed",
            ],
        }

    @mcp.tool()
    async def generate_vba_batch_export(input_data: VBABatchInput) -> dict[str, Any]:
        """Write a VBA macro that opens every matching file in a folder and saves a STEP copy of each to another folder.

        To export one file directly, use `export_step`. The macro comes back as text for
        the SolidWorks VBA editor (Alt+F11); `execute_macro` runs only binary .swp
        files, so it can't run this directly. Read-only. Nothing runs in SolidWorks.
        Returns: `vba_code`, the `parameters` used and `usage_instructions`.

        Args:
            input_data: The batch. Give `operation_type` (e.g. "export"),
                `file_pattern` (e.g. "*.sldprt"), `source_folder` and
                `target_folder`; `recursive` defaults to on.

        Returns:
            dict[str, Any]: The macro text.
        """
        if hasattr(adapter, "generate_vba_batch_export"):
            result = await adapter.generate_vba_batch_export(input_data.model_dump())
            require_success(result, "Failed to generate VBA code")
            return {
                "status": "success",
                "message": f"Generated VBA code for batch {input_data.operation_type}",
                "data": result.data,
                "execution_time": result.execution_time,
            }

        vba_code = f'''Sub BatchExport()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim fso As FileSystemObject
Dim folder As Folder
Dim file As file
Dim filePath As String
Dim exportPath As String
Dim processedCount As Integer

Set swApp = Application.SldWorks
Set fso = New FileSystemObject
Set folder = fso.GetFolder("{input_data.source_folder}")

processedCount = 0

' Process all files matching pattern
For Each file In folder.Files
    If LCase(file.Name) Like LCase("{input_data.file_pattern}") Then
        filePath = file.Path

        ' Open the file
        Set swModel = swApp.OpenDoc6(filePath, 1, 1, "", 0, 0)

        If Not swModel Is Nothing Then
            ' Generate export path
            exportPath = "{input_data.target_folder}" & "\\" & _
                fso.GetBaseName(file.Name) & ".step"

            ' Export as STEP
            swModel.SaveAs2 exportPath, 0, True, False

            ' Close the model
            swApp.CloseDoc swModel.GetTitle

            processedCount = processedCount + 1
        End If
    End If
Next file

MsgBox "Batch export completed. Processed " & processedCount & " files."

End Sub'''

        return {
            "status": "success",
            "message": f"Generated VBA code for batch {input_data.operation_type}",
            "vba_code": vba_code,
            "parameters": {
                "operation_type": input_data.operation_type,
                "file_pattern": input_data.file_pattern,
                "source_folder": input_data.source_folder,
                "target_folder": input_data.target_folder,
                "recursive": input_data.recursive,
            },
            "usage_instructions": [
                "1. Verify source and target folder paths exist",
                "2. Close any open documents to avoid conflicts",
                "3. Run the macro - it will process all matching files",
                "4. Check the target folder for exported files",
            ],
        }

    @mcp.tool()
    async def generate_vba_part_modeling(input_data: dict[str, Any]) -> dict[str, Any]:
        """Write a VBA macro for a shell or a fillet on the active part.

        Use it for a shell, which no direct tool makes yet; for a fillet, `add_fillet`
        works directly. The macro comes back as text for the SolidWorks VBA editor
        (Alt+F11); `execute_macro` runs only binary .swp files, so it can't run this
        directly. Read-only. Nothing runs in SolidWorks. Returns: `vba_code`, the
        `parameters` used and `usage_instructions`.

        Args:
            input_data: A mapping with `operation`, "shell" (the default, with
                `thickness` in mm, default 2) or "fillet" (with `radius` in mm,
                default 5). Any other operation is refused.

        Returns:
            dict[str, Any]: The macro text.
        """
        operation = input_data.get("operation", "shell")
        thickness = input_data.get("thickness", 2.0)

        if operation == "shell":
            vba_code = f"""Sub CreateShell()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swFeatMgr As SldWorks.FeatureManager
Dim swFeat As SldWorks.Feature

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc
Set swFeatMgr = swModel.FeatureManager

' Create shell feature
Set swFeat = swFeatMgr.FeatureShell2( _
    {thickness / 1000.0}, _ ' Thickness (meters)
    False, _ ' OutwardThickness
    False, _ ' MultiThickness
    False, _ ' ShowPreview
    False, _ ' MultipleFaceDef
    False _ ' MultipleThicknessDef
)

If swFeat Is Nothing Then
    MsgBox "Failed to create shell"
Else
    MsgBox "Shell created successfully with thickness: {thickness}mm"
End If

End Sub"""
        elif operation == "fillet":
            radius = input_data.get("radius", 5.0)
            vba_code = f"""Sub CreateFillet()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swFeatMgr As SldWorks.FeatureManager

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc
Set swFeatMgr = swModel.FeatureManager

' Create fillet feature
swFeatMgr.FeatureFillet4( _
    {radius / 1000.0}, _ ' Radius (meters)
    0, _ ' SecondRadius
    0, _ ' RollOnOff
    0, _ ' RollFirstRadius
    0, _ ' RollSecondRadius
    0, _ ' RollSmoothTransition
    0, _ ' ConicRho
    0, _ ' SetbackDistance
    False, _ ' AssemblyFeatureScope
    False, _ ' AutoSelectComponents
    False, _ ' PropagateFeatureToParts
    False, _ ' CreateSelectionSet
    False, _ ' EnableSolidPreview
    0 _ ' CornerType
)

swModel.ClearSelection2 True
MsgBox "Fillet created with radius: {radius}mm"

End Sub"""
        else:
            raise InvalidInputError(f"Unsupported operation: {operation}")

        return {
            "status": "success",
            "message": f"Generated VBA code for {operation} operation",
            "vba_code": vba_code,
            "parameters": input_data,
            "usage_instructions": [
                "1. Select the appropriate faces/edges",
                "2. Run the generated VBA macro",
                "3. Verify the feature was created correctly",
            ],
        }

    @mcp.tool()
    async def generate_vba_assembly_mates(input_data: dict[str, Any]) -> dict[str, Any]:
        """Write a VBA macro that adds a concentric, distance, parallel, perpendicular or coincident mate between two selected faces or edges.

        To add a mate directly, use `add_mate`. The macro comes back as text for the
        SolidWorks VBA editor (Alt+F11); `execute_macro` runs only binary .swp files, so
        it can't run this directly. The faces must be selected in SolidWorks before the
        macro runs. Read-only. Nothing runs in SolidWorks. Returns: `vba_code`, the
        `parameters` used and `usage_instructions`.

        Args:
            input_data: A mapping with `mate_type` (default "concentric"),
                `component1` and `component2` (names, for the comments), and
                `distance` in mm for a distance mate.

        Returns:
            dict[str, Any]: The macro text.
        """
        mate_type = input_data.get("mate_type", "concentric")
        component1 = input_data.get("component1", "Part1")
        component2 = input_data.get("component2", "Part2")
        distance = input_data.get("distance", 0.0)

        mate_type_map = {
            "concentric": "swMateType_CONCENTRIC",
            "distance": "swMateType_DISTANCE",
            "parallel": "swMateType_PARALLEL",
            "perpendicular": "swMateType_PERPENDICULAR",
            "coincident": "swMateType_COINCIDENT",
        }

        sw_mate_type = mate_type_map.get(mate_type, "swMateType_CONCENTRIC")

        vba_code = f"""Sub CreateAssemblyMate()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swAssy As SldWorks.AssemblyDoc
Dim swMateFeats As Variant
Dim swMate As SldWorks.Feature

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc
Set swAssy = swModel

' Clear existing selections
swModel.ClearSelection2 True

' Note: Entity selection needs to be done before running this macro
' Select appropriate faces/edges on {component1} and {component2}

' Create {mate_type} mate
swMateFeats = swAssy.AddMate5( _
    {sw_mate_type}, _ ' Mate type
    swMateAlign_ALIGNED, _ ' Alignment
    False, _ ' Flip
    {distance / 1000.0}, _ ' Distance (meters)
    0, _ ' Distance2
    0, _ ' Angle
    0, _ ' Angle2
    False, _ ' LockRotation
    0, _ ' WidthMateOption
    0, _ ' For Positioning Only
    0 _ ' LockToSketch
)

If IsEmpty(swMateFeats) Then
    MsgBox "Failed to create mate. Check entity selection."
Else
    Set swMate = swMateFeats(0)
    MsgBox "Mate created: " & swMate.Name
End If

End Sub"""

        return {
            "status": "success",
            "message": f"Generated VBA code for {mate_type} mate",
            "vba_code": vba_code,
            "parameters": {
                "mate_type": mate_type,
                "component1": component1,
                "component2": component2,
                "distance": distance,
            },
            "usage_instructions": [
                "1. Open the assembly document",
                "2. Select the two faces/edges to mate",
                "3. Run the generated macro",
                "4. Verify the mate was created in the feature tree",
            ],
        }

    @mcp.tool()
    async def generate_vba_drawing_dimensions(
        input_data: dict[str, Any],
    ) -> dict[str, Any]:
        """Write a VBA macro that adds a dimension to the selected drawing entities and sets its precision.

        To bring the model's dimensions into a drawing directly, use
        `insert_model_dimensions`. The macro comes back as text for the SolidWorks VBA
        editor (Alt+F11); `execute_macro` runs only binary .swp files, so it can't run
        this directly. The entities must be selected before the macro runs. Read-only.
        Nothing runs in SolidWorks. Returns: `vba_code`, the `parameters` used and
        `usage_instructions`.

        Args:
            input_data: A mapping with `dimension_type` ("linear", the default,
                "radial", "angular" or "diameter") and `precision` (decimal places,
                default 2).

        Returns:
            dict[str, Any]: The macro text.
        """
        dimension_type = input_data.get("dimension_type", "linear")
        precision = input_data.get("precision", 2)

        vba_code = f'''Sub CreateDrawingDimensions()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swDraw As SldWorks.DrawingDoc
Dim swView As SldWorks.View
Dim swDisp As SldWorks.DisplayDimension
Dim swDim As SldWorks.Dimension

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc
Set swDraw = swModel

' Get active drawing view
Set swView = swDraw.GetFirstView ' Sheet
Set swView = swView.GetNextView ' First drawing view

If swView Is Nothing Then
    MsgBox "No drawing view found"
    Exit Sub
End If

swModel.ClearSelection2 True

' Note: Select appropriate entities before running
' For linear dimensions: select two edges or points
' For radial: select arc or circle
' For angular: select two lines

Select Case "{dimension_type}"
    Case "linear"
        Set swDisp = swModel.AddDimension2(0.05, 0.05, 0)
    Case "radial"
        Set swDisp = swModel.AddDimension2(0.05, 0.05, 0)
    Case "angular"
        Set swDisp = swModel.AddDimension2(0.05, 0.05, 0)
    Case "diameter"
        Set swDisp = swModel.AddDimension2(0.05, 0.05, 0)
End Select

If Not swDisp Is Nothing Then
    Set swDim = swDisp.GetDimension2(0)
    ' Set precision
    swDim.SetPrecision2 swLinear, {precision}
    MsgBox "Dimension created with {precision} decimal places"
Else
    MsgBox "Failed to create dimension. Check entity selection."
End If

End Sub'''

        return {
            "status": "success",
            "message": f"Generated VBA code for {dimension_type} dimension",
            "vba_code": vba_code,
            "parameters": {
                "dimension_type": dimension_type,
                "precision": precision,
            },
            "usage_instructions": [
                "1. Open a drawing with views",
                "2. Select two edges, points, or appropriate entities",
                "3. Run the macro to create dimension",
                "4. Adjust dimension position as needed",
            ],
        }

    @mcp.tool()
    async def generate_vba_file_operations(
        input_data: dict[str, Any],
    ) -> dict[str, Any]:
        """Write a VBA macro that sets a custom property in every configuration, or runs Pack and Go on the active document.

        To set a property directly, use `set_custom_property`; for Pack and Go, use
        `pack_and_go_assembly`. The macro comes back as text for the SolidWorks VBA
        editor (Alt+F11); `execute_macro` runs only binary .swp files, so it can't run
        this directly. Read-only. Nothing runs in SolidWorks. Returns: `vba_code`, the
        `parameters` used and `usage_instructions`.

        Args:
            input_data: A mapping with `operation`: "custom_properties" (the
                default, with `property_name` and `property_value`) or
                "pack_and_go" (with `target_folder`). Any other operation is
                refused.

        Returns:
            dict[str, Any]: The macro text.
        """
        operation = input_data.get("operation", "custom_properties")

        if operation == "custom_properties":
            property_name = input_data.get("property_name", "Material")
            property_value = input_data.get("property_value", "Steel")

            vba_code = f'''Sub SetCustomProperties()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swCustPropMgr As SldWorks.CustomPropertyManager
Dim configNames As Variant
Dim i As Integer

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc

' Get configuration names
configNames = swModel.GetConfigurationNames

' Set property for all configurations
For i = 0 To UBound(configNames)
    Set swCustPropMgr = swModel.Extension.CustomPropertyManager(configNames(i))

    swCustPropMgr.Add3 "{property_name}", swCustomInfoText, _
        "{property_value}", swCustomPropertyAddOption_ReplaceValue
Next i

' Also set at file level
Set swCustPropMgr = swModel.Extension.CustomPropertyManager("")
swCustPropMgr.Add3 "{property_name}", swCustomInfoText, _
    "{property_value}", swCustomPropertyAddOption_ReplaceValue

MsgBox "Custom property '{property_name}' set to '{property_value}'"

End Sub'''

        elif operation == "pack_and_go":
            target_folder = input_data.get("target_folder", "C:\\PackAndGo")

            vba_code = f'''Sub PackAndGo()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swPackAndGo As SldWorks.PackAndGo
Dim pgStatus As Long
Dim pgWarnings As Long

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc

Set swPackAndGo = swApp.GetPackAndGo

' Set Pack and Go settings
swPackAndGo.IncludeDrawings = True
swPackAndGo.IncludeSimulationResults = False
swPackAndGo.IncludeToolboxComponents = False
swPackAndGo.FlattenToSingleFolder = True

' Set destination folder
swPackAndGo.SetSaveToName swModel.GetTitle, "{target_folder}"

' Execute Pack and Go
swPackAndGo.PackAndGo2 pgStatus, pgWarnings

If pgStatus = 0 Then
    MsgBox "Pack and Go completed successfully"
Else
    MsgBox "Pack and Go failed with status: " & pgStatus
End If

End Sub'''
        else:
            raise InvalidInputError(f"Unsupported operation: {operation}")

        return {
            "status": "success",
            "message": f"Generated VBA code for {operation}",
            "vba_code": vba_code,
            "parameters": input_data,
            "usage_instructions": [
                "1. Open the target document",
                "2. Run the generated macro",
                "3. Check results in properties or file system",
            ],
        }

    @mcp.tool()
    async def generate_vba_macro_recorder(input_data: dict[str, Any]) -> dict[str, Any]:
        """Write a VBA starting point: a macro skeleton to fill in, or a note on starting and stopping SolidWorks' macro recorder.

        Use "create_template" for an empty macro with the usual SolidWorks setup.
        Recording itself is done in SolidWorks (Tools > Macro > Record). The macro comes
        back as text for the SolidWorks VBA editor (Alt+F11); `execute_macro` runs only
        binary .swp files, so it can't run this directly. Read-only. Nothing runs in
        SolidWorks. Returns: `vba_code`, the `parameters` used and `usage_instructions`.

        Args:
            input_data: A mapping with `operation`: "start_recording" (the
                default), "stop_recording" or "create_template" (with
                `macro_name`). Any other operation is refused.

        Returns:
            dict[str, Any]: The macro text.
        """
        operation = input_data.get("operation", "start_recording")

        if operation == "start_recording":
            vba_code = """Sub StartMacroRecording()
Dim swApp As SldWorks.SldWorks
Set swApp = Application.SldWorks

' Enable macro recording
swApp.UnloadAddIn "SldWorks.Addin.Utilities.MacroRecorder"
swApp.LoadAddIn "SldWorks.Addin.Utilities.MacroRecorder"

' Note: Use Tools > Macro > Record in SolidWorks interface
' This code prepares the environment for recording

MsgBox "Macro recording environment prepared. Use Tools > Macro > Record to start."

End Sub"""

        elif operation == "stop_recording":
            vba_code = """Sub StopMacroRecording()
Dim swApp As SldWorks.SldWorks
Set swApp = Application.SldWorks

' Note: Use Tools > Macro > Stop Record in SolidWorks interface
' This code helps manage the recording session

MsgBox "Stop recording using Tools > Macro > Stop Record"

End Sub"""

        elif operation == "create_template":
            macro_name = input_data.get("macro_name", "CustomMacro")
            vba_code = f"""Sub {macro_name}Template()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
' Add more variables as needed

Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc

' TODO: Add your custom automation code here
' This template provides the basic SolidWorks API structure

' Error handling
If swModel Is Nothing Then
    MsgBox "No active document"
    Exit Sub
End If

' Your automation logic goes here...

MsgBox "Custom macro template ready for customization"

End Sub"""
        else:
            raise InvalidInputError(f"Unsupported operation: {operation}")

        return {
            "status": "success",
            "message": f"Generated VBA code for macro {operation}",
            "vba_code": vba_code,
            "parameters": input_data,
            "usage_instructions": [
                "1. Open SolidWorks VBA editor (Alt+F11)",
                "2. Create new module and paste code",
                "3. Customize as needed for your workflow",
            ],
        }

    # Core VBA generation tools
    tool_count = 10
    return tool_count