solidworks-mcp-python 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. solidworks_mcp/__init__.py +57 -0
  2. solidworks_mcp/adapters/__init__.py +40 -0
  3. solidworks_mcp/adapters/base.py +1882 -0
  4. solidworks_mcp/adapters/circuit_breaker.py +2052 -0
  5. solidworks_mcp/adapters/com_executor.py +252 -0
  6. solidworks_mcp/adapters/complexity_analyzer.py +272 -0
  7. solidworks_mcp/adapters/connection_pool.py +1884 -0
  8. solidworks_mcp/adapters/factory.py +366 -0
  9. solidworks_mcp/adapters/intelligent_router.py +218 -0
  10. solidworks_mcp/adapters/mock_adapter.py +4016 -0
  11. solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
  12. solidworks_mcp/adapters/solidworks/__init__.py +17 -0
  13. solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
  14. solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
  15. solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
  16. solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
  17. solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
  18. solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
  19. solidworks_mcp/adapters/solidworks/features.py +3362 -0
  20. solidworks_mcp/adapters/solidworks/io.py +3649 -0
  21. solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
  22. solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
  23. solidworks_mcp/adapters/solidworks/selection.py +308 -0
  24. solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
  25. solidworks_mcp/adapters/sw_feature_errors.py +66 -0
  26. solidworks_mcp/adapters/sw_type_info.py +317 -0
  27. solidworks_mcp/adapters/vba_adapter.py +318 -0
  28. solidworks_mcp/adapters/vba_macro_executor.py +215 -0
  29. solidworks_mcp/agents/__init__.py +68 -0
  30. solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
  31. solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
  32. solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
  33. solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
  34. solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
  35. solidworks_mcp/agents/harness.py +216 -0
  36. solidworks_mcp/agents/history_db.py +1508 -0
  37. solidworks_mcp/agents/retrieval_index.py +164 -0
  38. solidworks_mcp/agents/schemas.py +165 -0
  39. solidworks_mcp/agents/smoke_test.py +285 -0
  40. solidworks_mcp/agents/soc_exporter.py +650 -0
  41. solidworks_mcp/agents/soc_pickup.py +332 -0
  42. solidworks_mcp/agents/soc_rewind.py +221 -0
  43. solidworks_mcp/agents/vector_rag.py +665 -0
  44. solidworks_mcp/cache/__init__.py +5 -0
  45. solidworks_mcp/cache/response_cache.py +164 -0
  46. solidworks_mcp/config.py +729 -0
  47. solidworks_mcp/exceptions.py +37 -0
  48. solidworks_mcp/security/__init__.py +68 -0
  49. solidworks_mcp/security/auth.py +128 -0
  50. solidworks_mcp/security/cors.py +27 -0
  51. solidworks_mcp/security/rate_limiting.py +90 -0
  52. solidworks_mcp/security/runtime.py +155 -0
  53. solidworks_mcp/server.py +777 -0
  54. solidworks_mcp/server_cli_fixed.py +120 -0
  55. solidworks_mcp/setup/__init__.py +1 -0
  56. solidworks_mcp/setup/cli.py +399 -0
  57. solidworks_mcp/setup/clients.py +157 -0
  58. solidworks_mcp/setup/config_file.py +230 -0
  59. solidworks_mcp/setup/handshake.py +202 -0
  60. solidworks_mcp/setup/launcher.py +102 -0
  61. solidworks_mcp/setup/legacy.py +100 -0
  62. solidworks_mcp/setup/registration.py +193 -0
  63. solidworks_mcp/setup/solidworks.py +49 -0
  64. solidworks_mcp/tools/__init__.py +64 -0
  65. solidworks_mcp/tools/analysis.py +723 -0
  66. solidworks_mcp/tools/automation.py +578 -0
  67. solidworks_mcp/tools/docs_discovery.py +1476 -0
  68. solidworks_mcp/tools/drawing.py +1015 -0
  69. solidworks_mcp/tools/drawing_analysis.py +456 -0
  70. solidworks_mcp/tools/drawing_automation.py +627 -0
  71. solidworks_mcp/tools/engineering_data.py +466 -0
  72. solidworks_mcp/tools/error_classifier.py +232 -0
  73. solidworks_mcp/tools/export.py +833 -0
  74. solidworks_mcp/tools/file_facts.py +40 -0
  75. solidworks_mcp/tools/file_management.py +1473 -0
  76. solidworks_mcp/tools/input_compat.py +17 -0
  77. solidworks_mcp/tools/macro_recording.py +464 -0
  78. solidworks_mcp/tools/modeling.py +2360 -0
  79. solidworks_mcp/tools/sketching.py +1422 -0
  80. solidworks_mcp/tools/template_management.py +367 -0
  81. solidworks_mcp/tools/tool_boundary.py +280 -0
  82. solidworks_mcp/tools/vba_generation.py +1145 -0
  83. solidworks_mcp/ui/__init__.py +9 -0
  84. solidworks_mcp/ui/local_llm.py +725 -0
  85. solidworks_mcp/ui/routers/__init__.py +10 -0
  86. solidworks_mcp/ui/routers/checkpoint.py +24 -0
  87. solidworks_mcp/ui/routers/docs.py +47 -0
  88. solidworks_mcp/ui/routers/llm.py +83 -0
  89. solidworks_mcp/ui/routers/local_model.py +69 -0
  90. solidworks_mcp/ui/routers/model.py +75 -0
  91. solidworks_mcp/ui/routers/preview.py +38 -0
  92. solidworks_mcp/ui/routers/session.py +161 -0
  93. solidworks_mcp/ui/routers/viewer.py +154 -0
  94. solidworks_mcp/ui/schemas.py +502 -0
  95. solidworks_mcp/ui/server.py +298 -0
  96. solidworks_mcp/ui/service.py +481 -0
  97. solidworks_mcp/ui/services/__init__.py +136 -0
  98. solidworks_mcp/ui/services/_utils.py +958 -0
  99. solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
  100. solidworks_mcp/ui/services/docs_service.py +282 -0
  101. solidworks_mcp/ui/services/llm_service.py +956 -0
  102. solidworks_mcp/ui/services/model_service.py +459 -0
  103. solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
  104. solidworks_mcp/ui/services/preview_service.py +489 -0
  105. solidworks_mcp/ui/services/session_service.py +1389 -0
  106. solidworks_mcp/utils/__init__.py +9 -0
  107. solidworks_mcp/utils/feature_tree_classifier.py +342 -0
  108. solidworks_mcp/utils/logging.py +76 -0
  109. solidworks_mcp/utils/validation.py +77 -0
  110. solidworks_mcp/version.py +8 -0
  111. solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
  112. solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
  113. solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
  114. solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
  115. solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2360 @@
1
+ """Modeling tools for SolidWorks MCP Server.
2
+
3
+ Provides tools for creating and manipulating SolidWorks models, including parts,
4
+ assemblies, drawings, and features like extrusions, revolves, etc.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from fastmcp import FastMCP
10
+ from pydantic import BaseModel, Field
11
+
12
+ from ..adapters.base import (
13
+ AdapterResult,
14
+ ExtrusionParameters,
15
+ LoftParameters,
16
+ RevolveParameters,
17
+ SolidWorksAdapter,
18
+ SweepParameters,
19
+ )
20
+ from .input_compat import CompatInput
21
+ from .tool_boundary import (
22
+ normalize_input,
23
+ require_success,
24
+ with_tool_boundary,
25
+ )
26
+
27
+ # Input schemas using Python 3.14 built-in types
28
+
29
+
30
+ def _result_value(data: Any, *keys: str, default: Any = None) -> Any:
31
+ """Build internal result value.
32
+
33
+ Args:
34
+ data (Any): The data value.
35
+ *keys (str): Additional positional arguments forwarded to the call.
36
+ default (Any): Fallback value returned when the operation fails. Defaults to None.
37
+
38
+ Returns:
39
+ Any: The result produced by the operation.
40
+ """
41
+ if isinstance(data, dict):
42
+ for key in keys:
43
+ if key in data and data[key] is not None:
44
+ return data[key]
45
+ return default
46
+
47
+ for key in keys:
48
+ if hasattr(data, key):
49
+ value = getattr(data, key)
50
+ if value is not None:
51
+ return value
52
+ return default
53
+
54
+
55
+ async def _read_summary(adapter: Any) -> AdapterResult[dict[str, Any]] | None:
56
+ """Read the geometry summary, or ``None`` if the adapter has no such read.
57
+
58
+ Args:
59
+ adapter: The active adapter.
60
+
61
+ Returns:
62
+ AdapterResult[dict[str, Any]] | None: The summary result.
63
+ """
64
+ reader = getattr(adapter, "get_model_geometry_summary", None)
65
+ if reader is None:
66
+ return None
67
+ result: AdapterResult[dict[str, Any]] = await reader()
68
+ return result
69
+
70
+
71
+ def _box_size(summary: dict[str, Any]) -> Any:
72
+ """Return a summary's bounding-box size ``[x, y, z]``, or ``None``."""
73
+ box = summary.get("bounding_box_mm")
74
+ return box.get("size") if isinstance(box, dict) else None
75
+
76
+
77
+ def _attach_measured_change(
78
+ reply: dict[str, Any],
79
+ before: AdapterResult[dict[str, Any]] | None,
80
+ after: AdapterResult[dict[str, Any]] | None,
81
+ ) -> dict[str, Any]:
82
+ """Add the feature's measured volume and size change to its reply.
83
+
84
+ Best-effort: when the "before" read failed (or the adapter has none) the
85
+ reply is returned unchanged; anything else unmeasurable is ``None`` with a
86
+ ``note``. A feature that succeeded without changing the volume gets a
87
+ ``warning``, since that is how SolidWorks reports a silent no-op.
88
+
89
+ Args:
90
+ reply: The feature tool's success reply.
91
+ before: Summary read before the feature.
92
+ after: Summary read after the feature.
93
+
94
+ Returns:
95
+ dict[str, Any]: ``reply``, with ``measured_change`` when measurable.
96
+ """
97
+ if before is None or not before.is_success:
98
+ return reply
99
+ old = before.data or {}
100
+ new = (after.data or {}) if after is not None and after.is_success else None
101
+ volume_before = old.get("volume_mm3")
102
+ volume_after = new.get("volume_mm3") if new is not None else None
103
+ numbers = (int, float)
104
+ delta = (
105
+ round(volume_after - volume_before, 4)
106
+ if isinstance(volume_before, numbers) and isinstance(volume_after, numbers)
107
+ else None
108
+ )
109
+ change: dict[str, Any] = {
110
+ "volume_before_mm3": volume_before,
111
+ "volume_after_mm3": volume_after,
112
+ "volume_delta_mm3": delta,
113
+ "bounding_box_before_mm": _box_size(old),
114
+ "bounding_box_after_mm": _box_size(new) if new is not None else None,
115
+ "solid_body_count_before": old.get("solid_body_count"),
116
+ "solid_body_count_after": (
117
+ new.get("solid_body_count") if new is not None else None
118
+ ),
119
+ }
120
+ if new is None:
121
+ error = after.error if after is not None else None
122
+ change["note"] = f"Could not read the model after the feature: {error}"
123
+ elif delta is None:
124
+ change["note"] = "Volume is not determinable with the active adapter"
125
+ reply["measured_change"] = change
126
+ if delta == 0:
127
+ reply["warning"] = (
128
+ "The feature reported success but changed no volume; SolidWorks "
129
+ "may have accepted it without doing anything. Check the sketch, "
130
+ "the direction and the depth."
131
+ )
132
+ return reply
133
+
134
+
135
+ class OpenModelInput(BaseModel):
136
+ """Input schema for opening a SolidWorks model.
137
+
138
+ Attributes:
139
+ file_path (str): The file path value.
140
+ """
141
+
142
+ file_path: str = Field(
143
+ description="Full path to the SolidWorks file (.sldprt, .sldasm, .slddrw)"
144
+ )
145
+
146
+
147
+ class CreatePartInput(CompatInput):
148
+ """Input schema for creating a new SolidWorks part.
149
+
150
+ Attributes:
151
+ material (str | None): The material value.
152
+ name (str): The name value.
153
+ template (str | None): The template value.
154
+ units (str | None): The units value.
155
+ """
156
+
157
+ name: str = Field(description="Name for the new part")
158
+ template: str | None = Field(
159
+ default=None,
160
+ description=(
161
+ "Part template (.prtdot) path; omit to use the SolidWorks default"
162
+ ),
163
+ )
164
+ units: str | None = Field(default=None, description="Document units")
165
+ material: str | None = Field(default=None, description="Material name")
166
+
167
+ def model_post_init(self, __context: Any) -> None:
168
+ """Provide model post init support for the create part input.
169
+
170
+ Args:
171
+ __context (Any): The context value.
172
+
173
+ Returns:
174
+ None: None.
175
+
176
+ Raises:
177
+ ValueError: Name is required.
178
+ """
179
+ if not self.name.strip():
180
+ raise ValueError("name is required")
181
+
182
+
183
+ class CreateExtrusionInput(CompatInput):
184
+ """Input schema for creating an extrusion feature.
185
+
186
+ Attributes:
187
+ both_directions (bool): The both directions value.
188
+ depth (float): The depth value.
189
+ direction (str): The direction value.
190
+ draft_angle (float): The draft angle value.
191
+ end_condition (str): The end condition value.
192
+ merge_result (bool): The merge result value.
193
+ reverse (bool | None): The reverse value.
194
+ reverse_direction (bool): The reverse direction value.
195
+ sketch_name (str): The sketch name value.
196
+ thin_feature (bool): The thin feature value.
197
+ thin_thickness (float | None): The thin thickness value.
198
+ """
199
+
200
+ sketch_name: str = Field(description="Sketch name to extrude")
201
+ depth: float = Field(description="Extrusion depth in millimeters")
202
+ direction: str = Field(default="blind", description="Extrusion direction")
203
+ reverse: bool | None = Field(default=None, description="Reverse direction alias")
204
+ draft_angle: float = Field(default=0.0, description="Draft angle in degrees")
205
+ reverse_direction: bool = Field(
206
+ default=False, description="Reverse extrusion direction"
207
+ )
208
+ both_directions: bool = Field(
209
+ default=False, description="Extrude in both directions"
210
+ )
211
+ thin_feature: bool = Field(default=False, description="Create as thin wall feature")
212
+ thin_thickness: float | None = Field(
213
+ default=None, description="Thickness for thin wall feature in mm"
214
+ )
215
+ end_condition: str = Field(default="Blind", description="End condition type")
216
+ merge_result: bool = Field(default=True, description="Merge with existing geometry")
217
+
218
+ def model_post_init(self, __context: Any) -> None:
219
+ """Provide model post init support for the create extrusion input.
220
+
221
+ Args:
222
+ __context (Any): The context value.
223
+
224
+ Returns:
225
+ None: None.
226
+
227
+ Raises:
228
+ ValueError: Sketch_name is required.
229
+ """
230
+ if self.depth <= 0:
231
+ raise ValueError("depth must be positive")
232
+ if not self.sketch_name.strip():
233
+ raise ValueError("sketch_name is required")
234
+ if self.reverse is not None:
235
+ self.reverse_direction = self.reverse
236
+
237
+
238
+ class CreateRevolveInput(CompatInput):
239
+ """Input schema for creating a revolve feature.
240
+
241
+ Attributes:
242
+ angle (float): The angle value.
243
+ axis_entity (str): The axis entity value.
244
+ both_directions (bool): The both directions value.
245
+ direction (str): The direction value.
246
+ merge_result (bool): The merge result value.
247
+ reverse_direction (bool): The reverse direction value.
248
+ sketch_name (str): The sketch name value.
249
+ thin_feature (bool): The thin feature value.
250
+ thin_thickness (float | None): The thin thickness value.
251
+ """
252
+
253
+ sketch_name: str = Field(description="Sketch name to revolve")
254
+ axis_entity: str = Field(description="Axis entity for the revolve")
255
+ angle: float = Field(description="Revolve angle in degrees")
256
+ direction: str = Field(default="one_direction", description="Revolve direction")
257
+ reverse_direction: bool = Field(
258
+ default=False, description="Reverse revolve direction"
259
+ )
260
+ both_directions: bool = Field(
261
+ default=False, description="Revolve in both directions"
262
+ )
263
+ thin_feature: bool = Field(default=False, description="Create as thin wall feature")
264
+ thin_thickness: float | None = Field(
265
+ default=None, description="Thickness for thin wall feature in mm"
266
+ )
267
+ merge_result: bool = Field(default=True, description="Merge with existing geometry")
268
+
269
+ def model_post_init(self, __context: Any) -> None:
270
+ """Provide model post init support for the create revolve input.
271
+
272
+ Args:
273
+ __context (Any): The context value.
274
+
275
+ Returns:
276
+ None: None.
277
+
278
+ Raises:
279
+ ValueError: Angle must be positive.
280
+ """
281
+ if self.angle <= 0:
282
+ raise ValueError("angle must be positive")
283
+
284
+
285
+ class CreateSweepInput(BaseModel):
286
+ """Input schema for creating a sweep feature.
287
+
288
+ Attributes:
289
+ merge_result (bool): The merge result value.
290
+ path (str): The path value.
291
+ twist_along_path (bool): The twist along path value.
292
+ twist_angle (float): The twist angle value.
293
+ """
294
+
295
+ path: str = Field(description="Name or ID of the sweep path")
296
+ twist_along_path: bool = Field(default=False, description="Twist along path")
297
+ twist_angle: float = Field(default=0.0, description="Twist angle in degrees")
298
+ merge_result: bool = Field(default=True, description="Merge with existing geometry")
299
+
300
+
301
+ class CreateLoftInput(BaseModel):
302
+ """Input schema for creating a loft feature.
303
+
304
+ Attributes:
305
+ end_tangent (str | None): The end tangent value.
306
+ guide_curves (list[str] | None): The guide curves value.
307
+ merge_result (bool): The merge result value.
308
+ profiles (list[str]): The profiles value.
309
+ start_tangent (str | None): The start tangent value.
310
+ """
311
+
312
+ profiles: list[str] = Field(description="List of profile names or IDs")
313
+ guide_curves: list[str] | None = Field(
314
+ default=None, description="List of guide curve names or IDs"
315
+ )
316
+ start_tangent: str | None = Field(
317
+ default=None, description="Start tangent condition"
318
+ )
319
+ end_tangent: str | None = Field(default=None, description="End tangent condition")
320
+ merge_result: bool = Field(default=True, description="Merge with existing geometry")
321
+
322
+
323
+ class GetDimensionInput(CompatInput):
324
+ """Input schema for getting a dimension value.
325
+
326
+ Attributes:
327
+ dimension_name (str | None): The dimension name value.
328
+ name (str | None): The name value.
329
+ """
330
+
331
+ name: str | None = Field(
332
+ default=None,
333
+ description="Dimension name (e.g., 'D1@Sketch1', 'D1@Boss-Extrude1')",
334
+ )
335
+ dimension_name: str | None = Field(default=None, description="Dimension name alias")
336
+
337
+ def model_post_init(self, __context: Any) -> None:
338
+ """Provide model post init support for the get dimension input.
339
+
340
+ Args:
341
+ __context (Any): The context value.
342
+
343
+ Returns:
344
+ None: None.
345
+
346
+ Raises:
347
+ ValueError: Name is required.
348
+ """
349
+ if self.name is None:
350
+ self.name = self.dimension_name
351
+ if not self.name:
352
+ raise ValueError("name is required")
353
+
354
+
355
+ class SetDimensionInput(CompatInput):
356
+ """Input schema for setting a dimension value.
357
+
358
+ Attributes:
359
+ dimension_name (str | None): The dimension name value.
360
+ name (str | None): The name value.
361
+ units (str | None): The units value.
362
+ value (float): The value value.
363
+ """
364
+
365
+ name: str | None = Field(
366
+ default=None,
367
+ description="Dimension name (e.g., 'D1@Sketch1', 'D1@Boss-Extrude1')",
368
+ )
369
+ dimension_name: str | None = Field(default=None, description="Dimension name alias")
370
+ value: float = Field(description="New dimension value in millimeters")
371
+ units: str | None = Field(default=None, description="Units alias")
372
+
373
+ def model_post_init(self, __context: Any) -> None:
374
+ """Provide model post init support for the set dimension input.
375
+
376
+ Args:
377
+ __context (Any): The context value.
378
+
379
+ Returns:
380
+ None: None.
381
+
382
+ Raises:
383
+ ValueError: Name is required.
384
+ """
385
+ if self.name is None:
386
+ self.name = self.dimension_name
387
+ if not self.name:
388
+ raise ValueError("name is required")
389
+
390
+
391
+ class SetUnitsInput(CompatInput):
392
+ """Input schema for setting the active document's unit system.
393
+
394
+ Attributes:
395
+ unit_system (str): Target linear unit - ``mm``, ``cm``, ``m``, ``in``
396
+ or ``ft`` (aliases such as ``inch`` / ``millimeters`` accepted).
397
+ """
398
+
399
+ unit_system: str = Field(description="Linear unit system: mm, cm, m, in, or ft")
400
+
401
+ def model_post_init(self, __context: Any) -> None:
402
+ if not self.unit_system.strip():
403
+ raise ValueError("unit_system is required")
404
+
405
+
406
+ class CloseModelInput(BaseModel):
407
+ """Input schema for closing a model.
408
+
409
+ Attributes:
410
+ save (bool): The save value.
411
+ """
412
+
413
+ save: bool = Field(default=False, description="Save the model before closing")
414
+
415
+
416
+ class CreateCutExtrudeInput(CompatInput):
417
+ """Input schema for creating a cut-extrude feature.
418
+
419
+ Attributes:
420
+ depth (float): Cut depth in millimeters.
421
+ draft_angle (float): Draft angle in degrees.
422
+ end_condition (str): End condition type.
423
+ reverse_direction (bool): Reverse cut direction.
424
+ both_directions (bool): With ``"ThroughAll"``, cut both ways.
425
+ sketch_name (str | None): Sketch to cut from.
426
+ """
427
+
428
+ depth: float = Field(description="Cut depth in millimeters")
429
+ draft_angle: float = Field(default=0.0, description="Draft angle in degrees")
430
+ reverse_direction: bool = Field(
431
+ default=False,
432
+ description=(
433
+ "Cut against the sketch normal instead of along it. Usually not "
434
+ "needed: if the requested direction finds no material, the cut is "
435
+ "retried the other way and the reply sets direction_flipped"
436
+ ),
437
+ )
438
+ end_condition: str = Field(
439
+ default="Blind",
440
+ description=(
441
+ "'Blind' (cut 'depth' mm), 'ThroughAll' (through the body, one "
442
+ "way) or 'ThroughAllBoth' (through the body both ways, for a "
443
+ "sketch inside the body)"
444
+ ),
445
+ )
446
+ both_directions: bool = Field(
447
+ default=False,
448
+ description=(
449
+ "With end_condition 'ThroughAll', cut through the body both ways "
450
+ "(same as 'ThroughAllBoth')"
451
+ ),
452
+ )
453
+ sketch_name: str | None = Field(
454
+ default=None,
455
+ description=(
456
+ "Sketch to cut from, e.g. 'Sketch3'. If omitted, the sketch most "
457
+ "recently created/exited in this session is used, but only if it "
458
+ "still exists in the model — there is no other fallback. Pass "
459
+ "this explicitly whenever more than one unconsumed sketch could "
460
+ "exist, or after any operation that might have changed the "
461
+ "active sketch."
462
+ ),
463
+ )
464
+
465
+ def model_post_init(self, __context: Any) -> None:
466
+ if self.depth <= 0:
467
+ raise ValueError("depth must be positive")
468
+
469
+
470
+ class AddFilletInput(CompatInput):
471
+ """Input schema for adding a fillet feature.
472
+
473
+ Attributes:
474
+ edge_names (list[str]): Edge names to fillet.
475
+ radius (float): Fillet radius in millimeters.
476
+ """
477
+
478
+ radius: float = Field(description="Fillet radius in millimeters")
479
+ edge_names: list[str] = Field(
480
+ default_factory=list,
481
+ description=(
482
+ "Edges (or faces) to fillet. Accepts a topology name (e.g. "
483
+ "'Edge<1>'), a coordinate hint in metres ('x,y,z') to select the "
484
+ "nearest edge, or 'face:x,y,z' to select a whole face — which "
485
+ "fillets every edge bounding it in one call. Prefer 'face:' over "
486
+ "a coordinate on an edge that sits exactly on a shared boundary "
487
+ "between two faces (e.g. a hole rim flush with a flat face): the "
488
+ "nearest-edge match there can silently resolve to a different, "
489
+ "unrelated edge. Leave empty to use whatever is *currently "
490
+ "selected* in the live SolidWorks UI (e.g. entities read back "
491
+ "via get_selected_entities and left selected) without touching "
492
+ "the selection — this is the most reliable option when the user "
493
+ "has hand-picked specific edges, since it never round-trips "
494
+ "through coordinates. Falls back to filleting all edges only if "
495
+ "nothing at all is selected."
496
+ ),
497
+ )
498
+
499
+ def model_post_init(self, __context: Any) -> None:
500
+ if self.radius <= 0:
501
+ raise ValueError("radius must be positive")
502
+
503
+
504
+ class AddChamferInput(CompatInput):
505
+ """Input schema for adding an equal-distance chamfer feature.
506
+
507
+ Attributes:
508
+ distance (float): Chamfer distance in millimeters.
509
+ edge_names (list[str]): Edge names to chamfer.
510
+ """
511
+
512
+ distance: float = Field(description="Chamfer distance in millimeters")
513
+ edge_names: list[str] = Field(
514
+ default_factory=list,
515
+ description=(
516
+ "Edges (or faces) to chamfer. Accepts a topology name (e.g. "
517
+ "'Edge<1>'), a coordinate hint in metres ('x,y,z') to select the "
518
+ "nearest edge, or 'face:x,y,z' to select a whole face — which "
519
+ "chamfers every edge bounding it in one call. Prefer 'face:' over "
520
+ "a coordinate on an edge that sits exactly on a shared boundary "
521
+ "between two faces (e.g. a hole rim flush with a flat face): the "
522
+ "nearest-edge match there can silently resolve to a different, "
523
+ "unrelated edge. Leave empty to use whatever is *currently "
524
+ "selected* in the live SolidWorks UI (e.g. entities read back "
525
+ "via get_selected_entities and left selected) without touching "
526
+ "the selection — this is the most reliable option when the user "
527
+ "has hand-picked specific edges, since it never round-trips "
528
+ "through coordinates. Falls back to chamfering all edges only if "
529
+ "nothing at all is selected."
530
+ ),
531
+ )
532
+
533
+ def model_post_init(self, __context: Any) -> None:
534
+ if self.distance <= 0:
535
+ raise ValueError("distance must be positive")
536
+
537
+
538
+ class CreateAssemblyInput(CompatInput):
539
+ """Input schema for creating a new assembly.
540
+
541
+ Attributes:
542
+ components (list[str]): The components value.
543
+ name (str): The name value.
544
+ template (str | None): The template value.
545
+ """
546
+
547
+ name: str = Field(description="Name for the new assembly")
548
+ template: str | None = Field(
549
+ default=None,
550
+ description=(
551
+ "Assembly template (.asmdot) path; omit to use the SolidWorks default"
552
+ ),
553
+ )
554
+ components: list[str] = Field(default_factory=list, description="Component list")
555
+
556
+
557
+ class InsertComponentInput(CompatInput):
558
+ """Input schema for inserting a component into an assembly.
559
+
560
+ Attributes:
561
+ file_path (str): Path to the part or sub-assembly.
562
+ x (float): X position in millimetres.
563
+ y (float): Y position in millimetres.
564
+ z (float): Z position in millimetres.
565
+ """
566
+
567
+ file_path: str = Field(
568
+ description="Absolute path to the .sldprt or .sldasm to insert"
569
+ )
570
+ x: float = Field(default=0.0, description="X position in millimetres")
571
+ y: float = Field(default=0.0, description="Y position in millimetres")
572
+ z: float = Field(default=0.0, description="Z position in millimetres")
573
+
574
+ def model_post_init(self, __context: Any) -> None:
575
+ if not self.file_path.strip():
576
+ raise ValueError("file_path is required")
577
+
578
+
579
+ class DeleteFeatureInput(CompatInput):
580
+ """Input schema for deleting a feature.
581
+
582
+ Attributes:
583
+ name (str): Feature or sketch name to delete.
584
+ """
585
+
586
+ name: str = Field(
587
+ description="Feature or sketch name, e.g. 'Boss-Extrude1' or 'Sketch1'"
588
+ )
589
+
590
+
591
+ class SuppressFeatureInput(CompatInput):
592
+ """Input schema for suppressing or unsuppressing a feature.
593
+
594
+ Attributes:
595
+ name (str): Feature name to toggle.
596
+ suppress (bool): True to suppress, False to unsuppress.
597
+ """
598
+
599
+ name: str = Field(description="Feature name, e.g. 'Fillet1'")
600
+ suppress: bool = Field(
601
+ default=True, description="True to suppress, False to unsuppress"
602
+ )
603
+
604
+
605
+ class RenameFeatureInput(CompatInput):
606
+ """Input schema for renaming a feature.
607
+
608
+ Attributes:
609
+ old_name (str): Current feature name.
610
+ new_name (str): New feature name.
611
+ """
612
+
613
+ old_name: str = Field(description="Current feature name, e.g. 'Fillet1'")
614
+ new_name: str = Field(description="New feature name, e.g. 'MountingEdgeFillet'")
615
+
616
+ def model_post_init(self, __context: Any) -> None:
617
+ if not self.old_name.strip():
618
+ raise ValueError("old_name is required")
619
+ if not self.new_name.strip():
620
+ raise ValueError("new_name is required")
621
+
622
+
623
+ class SelectFeatureInput(CompatInput):
624
+ """Input schema for selecting a named feature.
625
+
626
+ Attributes:
627
+ feature_name (str): Feature, sketch, plane or component name.
628
+ append (bool): Add to the current selection instead of replacing it.
629
+ """
630
+
631
+ feature_name: str = Field(
632
+ description="Feature, sketch, plane or component name, e.g. "
633
+ "'Boss-Extrude1', 'Sketch1', 'Front Plane' (an '@Part1' suffix is ok)"
634
+ )
635
+ append: bool = Field(
636
+ default=False,
637
+ description="Add to the current selection instead of replacing it",
638
+ )
639
+
640
+ def model_post_init(self, __context: Any) -> None:
641
+ self.feature_name = self.feature_name.strip()
642
+ if not self.feature_name:
643
+ raise ValueError("feature_name is required")
644
+
645
+
646
+ class SelectFaceEdgesInput(CompatInput):
647
+ """Input schema for multi-selecting every edge of one face.
648
+
649
+ Attributes:
650
+ point (list[float] | None): A point on the face, [x, y, z] in mm.
651
+ side (str | None): Which way the face points.
652
+ plane (str | None): Reference plane the face lies on.
653
+ edge_type (str): Edge filter: all, linear or circular.
654
+ append (bool): Add to the current selection instead of replacing it.
655
+ """
656
+
657
+ point: list[float] | None = Field(
658
+ default=None, description="A point on the face, [x, y, z] in mm"
659
+ )
660
+ side: str | None = Field(
661
+ default=None,
662
+ description="Outward direction of the face: +X/-X/+Y/-Y/+Z/-Z or "
663
+ "right/left/top/bottom/front/back (top=+Y, front=+Z, right=+X, as in "
664
+ "SolidWorks' default views); picks the furthest flat face that way",
665
+ )
666
+ plane: str | None = Field(
667
+ default=None,
668
+ description="Reference plane name, e.g. 'Front Plane'; picks the body "
669
+ "face(s) lying on it",
670
+ )
671
+ edge_type: str = Field(
672
+ default="all", description="'all', 'linear' or 'circular' (e.g. hole rims)"
673
+ )
674
+ append: bool = Field(
675
+ default=False,
676
+ description="Add to the current selection instead of replacing it",
677
+ )
678
+
679
+ def model_post_init(self, __context: Any) -> None:
680
+ from ..adapters.solidworks.face_selection import validate_face_query
681
+
682
+ self.edge_type = str(self.edge_type or "all").strip().lower()
683
+ problem = validate_face_query(self.point, self.side, self.plane, self.edge_type)
684
+ if problem:
685
+ raise ValueError(problem)
686
+
687
+
688
+ class RebuildModelInput(CompatInput):
689
+ """Input schema for forcing a model rebuild.
690
+
691
+ Attributes:
692
+ top_level_only (bool): Rebuild only the top level of an assembly.
693
+ """
694
+
695
+ top_level_only: bool = Field(
696
+ default=False,
697
+ description="Rebuild only the top level of an assembly (default: full "
698
+ "rebuild including sub-assemblies)",
699
+ )
700
+
701
+
702
+ class UndoInput(CompatInput):
703
+ """Input schema for undoing model operations.
704
+
705
+ Attributes:
706
+ count (int): Number of operations to undo.
707
+ """
708
+
709
+ count: int = Field(default=1, ge=1, description="Number of operations to undo")
710
+
711
+
712
+ class AddMateInput(CompatInput):
713
+ """Input schema for mating two assembly components.
714
+
715
+ Attributes:
716
+ component_a (str): First component instance name.
717
+ component_b (str): Second component instance name.
718
+ entity_a (str): Named feature on the first component.
719
+ entity_b (str): Named feature on the second component.
720
+ mate_type (str): Mate type.
721
+ alignment (str): Mate alignment.
722
+ distance (float): Distance in millimetres for a distance mate.
723
+ angle (float): Angle in degrees for an angle mate.
724
+ """
725
+
726
+ component_a: str = Field(
727
+ description="First component instance name, as reported by list_components"
728
+ )
729
+ component_b: str = Field(description="Second component instance name")
730
+ entity_a: str = Field(
731
+ default="Front Plane",
732
+ description=(
733
+ "Named feature on the first component. Only tree features "
734
+ "(reference planes and axes) can be selected"
735
+ ),
736
+ )
737
+ entity_b: str = Field(
738
+ default="Front Plane", description="Named feature on the second component"
739
+ )
740
+ mate_type: str = Field(
741
+ default="coincident",
742
+ description=(
743
+ "coincident, concentric, perpendicular, parallel, tangent, "
744
+ "distance or angle"
745
+ ),
746
+ )
747
+ alignment: str = Field(
748
+ default="aligned", description="aligned, anti_aligned or closest"
749
+ )
750
+ distance: float = Field(
751
+ default=0.0, description="Distance in millimetres, for a distance mate"
752
+ )
753
+ angle: float = Field(default=0.0, description="Angle in degrees, for an angle mate")
754
+
755
+ def model_post_init(self, __context: Any) -> None:
756
+ if not self.component_a.strip() or not self.component_b.strip():
757
+ raise ValueError("component_a and component_b are required")
758
+
759
+
760
+ class CreateReferencePlaneInput(CompatInput):
761
+ """Input schema for creating a reference plane.
762
+
763
+ Attributes:
764
+ reference (str): Name of the reference plane or planar face.
765
+ offset (float): Offset distance in millimetres.
766
+ angle (float): Angle in degrees.
767
+ flip (bool): Reverse the offset or angle direction.
768
+ """
769
+
770
+ reference: str = Field(
771
+ description="Name of the reference plane or planar face to offset "
772
+ "from, e.g. 'Front Plane'"
773
+ )
774
+ offset: float = Field(
775
+ default=0.0,
776
+ description="Offset distance in millimetres. Must be non-zero — a "
777
+ "plane coincident with its reference is not useful",
778
+ )
779
+ angle: float = Field(
780
+ default=0.0,
781
+ description="Angle in degrees. Not yet supported: an angled plane "
782
+ "needs a second reference this tool cannot take, so a non-zero "
783
+ "value errors. Use offset for parallel planes",
784
+ )
785
+ flip: bool = Field(default=False, description="Reverse the offset direction")
786
+
787
+ def model_post_init(self, __context: Any) -> None:
788
+ if not self.reference.strip():
789
+ raise ValueError("reference is required")
790
+
791
+
792
+ class CreateAxisInput(CompatInput):
793
+ """Input schema for creating a reference axis.
794
+
795
+ Attributes:
796
+ reference (str): Principal axis direction.
797
+ """
798
+
799
+ reference: str = Field(
800
+ description="Principal axis direction: 'x', 'y' or 'z' "
801
+ "(case-insensitive; a leading +/- is stripped)"
802
+ )
803
+
804
+ def model_post_init(self, __context: Any) -> None:
805
+ if not self.reference.strip():
806
+ raise ValueError("reference is required")
807
+
808
+
809
+ class CreateReferencePointInput(CompatInput):
810
+ """Input schema for creating a reference point.
811
+
812
+ Attributes:
813
+ mode (str): "along_curve" or "face_center".
814
+ x (float): X of a point on the target edge/face, in millimetres.
815
+ y (float): Y of a point on the target edge/face, in millimetres.
816
+ z (float): Z of a point on the target edge/face, in millimetres.
817
+ distance (float | None): For "along_curve", offset from the edge
818
+ start in millimetres.
819
+ percent (float | None): For "along_curve", position as 0-100 of the
820
+ edge length.
821
+ """
822
+
823
+ mode: str = Field(
824
+ description="'along_curve' (a point on an edge) or 'face_center' "
825
+ "(the centroid of a face)"
826
+ )
827
+ x: float = Field(description="X of a point on the target edge/face, in mm")
828
+ y: float = Field(description="Y of a point on the target edge/face, in mm")
829
+ z: float = Field(description="Z of a point on the target edge/face, in mm")
830
+ distance: float | None = Field(
831
+ default=None,
832
+ description="along_curve only: offset from the edge start in mm "
833
+ "(give exactly one of distance or percent)",
834
+ )
835
+ percent: float | None = Field(
836
+ default=None,
837
+ description="along_curve only: position as 0-100 of the edge length",
838
+ )
839
+
840
+ def model_post_init(self, __context: Any) -> None:
841
+ if str(self.mode or "").strip().lower() not in (
842
+ "along_curve",
843
+ "face_center",
844
+ ):
845
+ raise ValueError("mode must be 'along_curve' or 'face_center'")
846
+
847
+
848
+ class MirrorFeatureInput(CompatInput):
849
+ """Input schema for mirroring solid bodies or features.
850
+
851
+ Attributes:
852
+ features (list[str]): Body or feature names to mirror.
853
+ mirror_plane (str): Plane name to mirror about.
854
+ merge (bool): Merge the mirrored result with the original.
855
+ mirror_bodies (bool): Mirror whole bodies rather than features.
856
+ """
857
+
858
+ features: list[str] = Field(
859
+ default_factory=list,
860
+ description="Body or feature names to mirror. At least one required. "
861
+ "For a body mirror these are the names as they appear under Solid "
862
+ "Bodies, which for a lofted wing is the loft's own name",
863
+ )
864
+ mirror_plane: str = Field(
865
+ description="Plane name to mirror about, e.g. 'Right Plane'"
866
+ )
867
+ merge: bool = Field(
868
+ default=True,
869
+ description="Merge the mirrored result with the original body",
870
+ )
871
+ mirror_bodies: bool = Field(
872
+ default=True,
873
+ description="Mirror whole solid bodies rather than individual "
874
+ "features. Defaults to True, which is what works for anything "
875
+ "built from a loft or sweep - a feature mirror only resolves when "
876
+ "the feature's own sketch sits on the mirror plane",
877
+ )
878
+
879
+ def model_post_init(self, __context: Any) -> None:
880
+ if not self.features:
881
+ raise ValueError("features must contain at least one name")
882
+ if not self.mirror_plane.strip():
883
+ raise ValueError("mirror_plane is required")
884
+
885
+
886
+ class PatternCircularInput(CompatInput):
887
+ """Input schema for patterning features around an axis.
888
+
889
+ Attributes:
890
+ features (list[str]): Names of the features to pattern.
891
+ axis (str): Name of the axis to rotate about.
892
+ count (int): Total number of instances including the original.
893
+ angle (float): Total angle in degrees to spread instances over.
894
+ equal_spacing (bool): Space instances evenly across angle.
895
+ """
896
+
897
+ features: list[str] = Field(
898
+ default_factory=list,
899
+ description="Names of the features to pattern. At least one required.",
900
+ )
901
+ axis: str = Field(
902
+ description="Name of the axis to rotate about, e.g. 'Axis1' as "
903
+ "created by create_axis"
904
+ )
905
+ count: int = Field(
906
+ description="Total number of instances including the original. "
907
+ "Must be at least 2 - a pattern of one is just the original feature"
908
+ )
909
+ angle: float = Field(
910
+ default=360.0,
911
+ description="Total angle in degrees to spread the instances over. "
912
+ "Defaults to a full 360",
913
+ )
914
+ equal_spacing: bool = Field(
915
+ default=True,
916
+ description="Space instances evenly across 'angle'",
917
+ )
918
+
919
+ def model_post_init(self, __context: Any) -> None:
920
+ if not self.features:
921
+ raise ValueError("features must contain at least one name")
922
+ if not self.axis.strip():
923
+ raise ValueError("axis is required")
924
+ if self.count < 2:
925
+ raise ValueError("count must be at least 2")
926
+
927
+
928
+ class CreateDrawingInput(CompatInput):
929
+ """Input schema for creating a new drawing.
930
+
931
+ Attributes:
932
+ model_path (str | None): The model path value.
933
+ name (str): The name value.
934
+ sheet_format (str | None): The sheet format value.
935
+ template (str | None): The template value.
936
+ """
937
+
938
+ name: str = Field(description="Name for the new drawing")
939
+ template: str | None = Field(
940
+ default=None,
941
+ description=(
942
+ "Drawing template (.drwdot) path; omit to use the SolidWorks default"
943
+ ),
944
+ )
945
+ model_path: str | None = Field(default=None, description="Source model path")
946
+ sheet_format: str | None = Field(default=None, description="Sheet format template")
947
+
948
+
949
+ async def register_modeling_tools(
950
+ mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
951
+ ) -> int:
952
+ """Register modeling tools with FastMCP.
953
+
954
+ Registers comprehensive modeling tools for SolidWorks automation including model
955
+ creation, feature creation, and model management operations.
956
+
957
+ Args:
958
+ mcp (FastMCP): The mcp value.
959
+ adapter (SolidWorksAdapter): Adapter instance used for the operation.
960
+ config (dict[str, Any]): Configuration values for the operation.
961
+
962
+ Returns:
963
+ int: The computed numeric result.
964
+
965
+ Example:
966
+ ```python
967
+ from solidworks_mcp.tools.modeling import register_modeling_tools
968
+
969
+ tool_count = await register_modeling_tools(mcp, adapter, config)
970
+ print(f"Registered {tool_count} modeling tools")
971
+ ```
972
+ """
973
+ mcp = with_tool_boundary(mcp)
974
+ tool_count = 0
975
+
976
+ @mcp.tool()
977
+ async def open_model(input_data: OpenModelInput) -> dict[str, Any]:
978
+ """Open a SolidWorks model (part, assembly, or drawing).
979
+
980
+ Opens an existing SolidWorks file and makes it the active document for further
981
+ operations. Supports all standard SolidWorks file formats and provides detailed model
982
+ information upon successful opening.
983
+
984
+ Args:
985
+ input_data (OpenModelInput): The input data value.
986
+
987
+ Returns:
988
+ dict[str, Any]: A dictionary containing the resulting values.
989
+
990
+ Example:
991
+ ```python
992
+ result = await open_model({
993
+ "file_path": "C:/Models/bracket.sldprt"
994
+ })
995
+
996
+ if result["status"] == "success":
997
+ model = result["model"]
998
+ print(f"Opened {model['type']}: {model['name']}")
999
+ print(f"Configuration: {model['configuration']}")
1000
+ ```
1001
+
1002
+ Note:
1003
+ File path must be absolute and accessible to SolidWorks.
1004
+ Model becomes the active document for subsequent operations.
1005
+ """
1006
+ input_data = normalize_input(input_data, OpenModelInput)
1007
+ result = await adapter.open_model(input_data.file_path)
1008
+
1009
+ require_success(result, context="Failed to open model")
1010
+ model = result.data
1011
+ title = _result_value(model, "title", "name", default=input_data.file_path)
1012
+ model_type = _result_value(model, "type", default="Part")
1013
+ path = _result_value(model, "path", "file_path", default=input_data.file_path)
1014
+ configuration = _result_value(model, "configuration", default="Default")
1015
+ return {
1016
+ "status": "success",
1017
+ "message": f"Opened {model_type}: {title}",
1018
+ "model": {
1019
+ "title": title,
1020
+ "name": title,
1021
+ "type": model_type,
1022
+ "path": path,
1023
+ "configuration": configuration,
1024
+ },
1025
+ "execution_time": result.execution_time,
1026
+ }
1027
+
1028
+ @mcp.tool()
1029
+ async def create_part(input_data: CreatePartInput) -> dict[str, Any]:
1030
+ """Create a new SolidWorks part document.
1031
+
1032
+ Creates a new SolidWorks part document from ``template`` (a .prtdot path) or,
1033
+ without one, the default part template. The new part becomes the active document
1034
+ and is ready for modeling operations such as sketch creation and feature addition.
1035
+
1036
+ Args:
1037
+ input_data (CreatePartInput): The input data value.
1038
+
1039
+ Returns:
1040
+ dict[str, Any]: A dictionary containing the resulting values.
1041
+
1042
+ Example:
1043
+ ```python
1044
+ result = await create_part()
1045
+
1046
+ if result["status"] == "success":
1047
+ part = result["model"]
1048
+ print(f"Created new part: {part['name']}")
1049
+ # Ready for sketching and feature creation
1050
+ ```
1051
+
1052
+ Note:
1053
+ - Uses ``template`` when given, else the default part template
1054
+ - Part document is created in memory (not saved)
1055
+ - Use save operations to persist to disk
1056
+ - Subsequent modeling operations will apply to this part
1057
+ """
1058
+ input_data = normalize_input(input_data, CreatePartInput)
1059
+ result = await adapter.create_part(
1060
+ input_data.name, input_data.units, input_data.template
1061
+ )
1062
+
1063
+ require_success(result, context="Failed to create part")
1064
+ model = result.data
1065
+ part_name = _result_value(model, "name", default=input_data.name)
1066
+ units = _result_value(model, "units", default=input_data.units or "mm")
1067
+ return {
1068
+ "status": "success",
1069
+ "message": f"Created new part: {part_name}",
1070
+ "part": {
1071
+ "name": part_name,
1072
+ "units": units,
1073
+ "material": input_data.material,
1074
+ "template": input_data.template,
1075
+ },
1076
+ "execution_time": result.execution_time,
1077
+ }
1078
+
1079
+ @mcp.tool()
1080
+ async def create_assembly(input_data: CreateAssemblyInput) -> dict[str, Any]:
1081
+ """Create a new SolidWorks assembly document.
1082
+
1083
+ Creates a new SolidWorks assembly document from ``template`` (a .asmdot path) or,
1084
+ without one, the default assembly template. The new assembly becomes the active
1085
+ document and is ready for component insertion, mating, and assembly-level operations.
1086
+
1087
+ Args:
1088
+ input_data (CreateAssemblyInput): The input data value.
1089
+
1090
+ Returns:
1091
+ dict[str, Any]: A dictionary containing the resulting values.
1092
+
1093
+ Example:
1094
+ ```python
1095
+ result = await create_assembly()
1096
+
1097
+ if result["status"] == "success":
1098
+ assembly = result["model"]
1099
+ print(f"Created new assembly: {assembly['name']}")
1100
+ # Ready for component insertion and mating
1101
+ ```
1102
+
1103
+ Note:
1104
+ - Uses ``template`` when given, else the default assembly template
1105
+ - Assembly document is created in memory (not saved)
1106
+ - Use save operations to persist to disk
1107
+ - Ready for component insertion and mate creation
1108
+ - Assembly tree will initially be empty
1109
+
1110
+ The new assembly will become the active document.
1111
+ """
1112
+ input_data = normalize_input(input_data, CreateAssemblyInput)
1113
+ result = await adapter.create_assembly(input_data.name, input_data.template)
1114
+
1115
+ require_success(result, context="Failed to create assembly")
1116
+ model = result.data
1117
+ assembly_name = _result_value(model, "name", default=input_data.name)
1118
+ return {
1119
+ "status": "success",
1120
+ "message": f"Created new assembly: {assembly_name}",
1121
+ "assembly": {
1122
+ "name": assembly_name,
1123
+ "components": input_data.components,
1124
+ "template": input_data.template,
1125
+ },
1126
+ "execution_time": result.execution_time,
1127
+ }
1128
+
1129
+ @mcp.tool()
1130
+ async def create_drawing(input_data: CreateDrawingInput) -> dict[str, Any]:
1131
+ """Create a new SolidWorks drawing document.
1132
+
1133
+ This tool creates a new drawing document from ``template`` (a .drwdot path) or,
1134
+ without one, the default drawing template. The new drawing will become the active
1135
+ document.
1136
+
1137
+ Args:
1138
+ input_data (CreateDrawingInput): The input data value.
1139
+
1140
+ Returns:
1141
+ dict[str, Any]: A dictionary containing the resulting values.
1142
+ """
1143
+ input_data = normalize_input(input_data, CreateDrawingInput)
1144
+ result = await adapter.create_drawing(input_data.name, input_data.template)
1145
+
1146
+ require_success(result, context="Failed to create drawing")
1147
+ model = result.data
1148
+ drawing_name = _result_value(model, "name", default=input_data.name)
1149
+ sheet_format = _result_value(
1150
+ model, "sheet_format", default=input_data.sheet_format
1151
+ )
1152
+ return {
1153
+ "status": "success",
1154
+ "message": f"Created new drawing: {drawing_name}",
1155
+ "drawing": {
1156
+ "name": drawing_name,
1157
+ "model_path": input_data.model_path,
1158
+ "sheet_format": sheet_format,
1159
+ "template": input_data.template,
1160
+ },
1161
+ "execution_time": result.execution_time,
1162
+ }
1163
+
1164
+ @mcp.tool()
1165
+ async def close_model(input_data: CloseModelInput) -> dict[str, Any]:
1166
+ """Close the current SolidWorks model.
1167
+
1168
+ Closes the currently active SolidWorks document with an option to save changes before
1169
+ closing. This is essential for proper model lifecycle management and preventing data
1170
+ loss.
1171
+
1172
+ Args:
1173
+ input_data (CloseModelInput): The input data value.
1174
+
1175
+ Returns:
1176
+ dict[str, Any]: A dictionary containing the resulting values.
1177
+
1178
+ Example:
1179
+ ```python
1180
+ # Close without saving
1181
+ result = await close_model({"save": False})
1182
+
1183
+ # Save and close
1184
+ result = await close_model({"save": True})
1185
+
1186
+ if result["status"] == "success":
1187
+ print(f"Model closed, saved: {result['saved']}")
1188
+ ```
1189
+
1190
+ Note:
1191
+ - Unsaved changes will be lost if save=False
1192
+ - Always save important work before closing
1193
+ - Model must be open to close it
1194
+ """
1195
+ input_data = normalize_input(input_data, CloseModelInput)
1196
+ result = await adapter.close_model(input_data.save)
1197
+
1198
+ require_success(result, context="Failed to close model")
1199
+ return {
1200
+ "status": "success",
1201
+ "message": "Model closed successfully",
1202
+ "saved": input_data.save,
1203
+ "execution_time": result.execution_time,
1204
+ }
1205
+
1206
+ @mcp.tool()
1207
+ async def create_extrusion(input_data: CreateExtrusionInput) -> dict[str, Any]:
1208
+ """Create an extrusion feature from the active sketch.
1209
+
1210
+ Creates a 3D extrusion feature (boss or cut) from the currently active 2D sketch.
1211
+ Supports advanced options like draft angles, thin features, bidirectional extrusion, and
1212
+ various end conditions for professional modeling workflows.
1213
+
1214
+ Args:
1215
+ input_data (CreateExtrusionInput): The input data value.
1216
+
1217
+ Returns:
1218
+ dict[str, Any]: A dictionary containing the resulting values.
1219
+
1220
+ Example:
1221
+ ```python
1222
+ # Simple boss extrusion
1223
+ result = await create_extrusion({
1224
+ "depth": 25.0,
1225
+ "merge_result": True
1226
+ })
1227
+
1228
+ # Cut with draft angle
1229
+ result = await create_extrusion({
1230
+ "depth": 10.0,
1231
+ "reverse_direction": True,
1232
+ "draft_angle": 2.0
1233
+ })
1234
+
1235
+ # Thin wall feature
1236
+ result = await create_extrusion({
1237
+ "depth": 50.0,
1238
+ "thin_feature": True,
1239
+ "thin_thickness": 2.0
1240
+ })
1241
+ ```
1242
+ """
1243
+ input_data = normalize_input(input_data, CreateExtrusionInput)
1244
+ # Convert input to ExtrusionParameters
1245
+ params = ExtrusionParameters(
1246
+ depth=input_data.depth,
1247
+ draft_angle=input_data.draft_angle,
1248
+ reverse_direction=input_data.reverse_direction,
1249
+ both_directions=input_data.both_directions,
1250
+ thin_feature=input_data.thin_feature,
1251
+ thin_thickness=input_data.thin_thickness,
1252
+ end_condition=input_data.end_condition,
1253
+ merge_result=input_data.merge_result,
1254
+ feature_scope=False,
1255
+ auto_select=True,
1256
+ )
1257
+
1258
+ before = await _read_summary(adapter)
1259
+ result = await adapter.create_extrusion(params)
1260
+
1261
+ require_success(result, context="Failed to create extrusion")
1262
+ after = await _read_summary(adapter) if before is not None else None
1263
+ feature = result.data
1264
+ reply = {
1265
+ "status": "success",
1266
+ "message": f"Created extrusion: {_result_value(feature, 'feature_name', 'name', default='Extrusion')}",
1267
+ "extrusion": {
1268
+ "name": _result_value(
1269
+ feature, "feature_name", "name", default="Extrusion"
1270
+ ),
1271
+ "sketch": input_data.sketch_name,
1272
+ "depth": input_data.depth,
1273
+ "direction": input_data.direction,
1274
+ },
1275
+ "execution_time": result.execution_time,
1276
+ }
1277
+ return _attach_measured_change(reply, before, after)
1278
+
1279
+ @mcp.tool()
1280
+ async def create_revolve(input_data: CreateRevolveInput) -> dict[str, Any]:
1281
+ """Create a revolve feature from the active sketch.
1282
+
1283
+ Creates a 3D revolve feature by rotating the active 2D sketch profile around a specified
1284
+ axis of revolution. Supports full and partial revolves, thin features, and bidirectional
1285
+ revolution for comprehensive rotational modeling.
1286
+
1287
+ Args:
1288
+ input_data (CreateRevolveInput): The input data value.
1289
+
1290
+ Returns:
1291
+ dict[str, Any]: A dictionary containing the resulting values.
1292
+
1293
+ Example:
1294
+ ```python
1295
+ # Full revolution (cylinder)
1296
+ result = await create_revolve({
1297
+ "angle": 360.0,
1298
+ "merge_result": True
1299
+ })
1300
+
1301
+ # Partial revolution (arc section)
1302
+ result = await create_revolve({
1303
+ "angle": 120.0,
1304
+ "both_directions": True
1305
+ })
1306
+
1307
+ # Thin wall revolution (pipe)
1308
+ result = await create_revolve({
1309
+ "angle": 360.0,
1310
+ "thin_feature": True,
1311
+ "thin_thickness": 3.0
1312
+ })
1313
+ ```
1314
+ """
1315
+ input_data = normalize_input(input_data, CreateRevolveInput)
1316
+ # Convert input to RevolveParameters
1317
+ params = RevolveParameters(
1318
+ angle=input_data.angle,
1319
+ reverse_direction=input_data.reverse_direction,
1320
+ both_directions=input_data.both_directions,
1321
+ thin_feature=input_data.thin_feature,
1322
+ thin_thickness=input_data.thin_thickness,
1323
+ merge_result=input_data.merge_result,
1324
+ )
1325
+
1326
+ before = await _read_summary(adapter)
1327
+ result = await adapter.create_revolve(params)
1328
+
1329
+ require_success(result, context="Failed to create revolve")
1330
+ after = await _read_summary(adapter) if before is not None else None
1331
+ feature = result.data
1332
+ reply = {
1333
+ "status": "success",
1334
+ "message": f"Created revolve: {_result_value(feature, 'feature_name', 'name', default='Revolve')}",
1335
+ "revolve": {
1336
+ "name": _result_value(
1337
+ feature, "feature_name", "name", default="Revolve"
1338
+ ),
1339
+ "sketch": input_data.sketch_name,
1340
+ "axis_entity": input_data.axis_entity,
1341
+ "angle": input_data.angle,
1342
+ "direction": input_data.direction,
1343
+ },
1344
+ "execution_time": result.execution_time,
1345
+ }
1346
+ return _attach_measured_change(reply, before, after)
1347
+
1348
+ @mcp.tool()
1349
+ async def get_dimension(input_data: GetDimensionInput) -> dict[str, Any]:
1350
+ """Get the value of a dimension from the current model.
1351
+
1352
+ Retrieves the current value of a named dimension from the active SolidWorks model.
1353
+ Dimensions can be from sketches, features, or global dimensions. Useful for parametric
1354
+ modeling and design validation.
1355
+
1356
+ Args:
1357
+ input_data (GetDimensionInput): The input data value.
1358
+
1359
+ Returns:
1360
+ dict[str, Any]: A dictionary containing the resulting values.
1361
+
1362
+ Example:
1363
+ ```python
1364
+ # Get sketch dimension
1365
+ result = await get_dimension({
1366
+ "name": "D1@Sketch1"
1367
+ })
1368
+
1369
+ if result["status"] == "success":
1370
+ dim = result["dimension"]
1371
+ print(f"Dimension {dim['name']}: {dim['value']} {dim['units']}")
1372
+
1373
+ # Get feature dimension
1374
+ result = await get_dimension({
1375
+ "name": "D1@Boss-Extrude1"
1376
+ })
1377
+ ```
1378
+ """
1379
+ input_data = normalize_input(input_data, GetDimensionInput)
1380
+ result = await adapter.get_dimension(input_data.name)
1381
+
1382
+ require_success(result, context="Failed to get dimension")
1383
+ value = result.data
1384
+ dimension_value = _result_value(value, "value", default=value)
1385
+ dimension_units = _result_value(value, "units", default="mm")
1386
+ return {
1387
+ "status": "success",
1388
+ "message": f"Dimension {input_data.name} = {dimension_value} {dimension_units}",
1389
+ "dimension": {
1390
+ "name": input_data.name,
1391
+ "value": dimension_value,
1392
+ "units": dimension_units,
1393
+ },
1394
+ "execution_time": result.execution_time,
1395
+ }
1396
+
1397
+ @mcp.tool()
1398
+ async def set_dimension(input_data: SetDimensionInput) -> dict[str, Any]:
1399
+ """Set the value of a dimension in the current model.
1400
+
1401
+ This tool modifies the value of a named dimension and rebuilds the model. Use this to
1402
+ parametrically modify your model dimensions.
1403
+
1404
+ Args:
1405
+ input_data (SetDimensionInput): The input data value.
1406
+
1407
+ Returns:
1408
+ dict[str, Any]: A dictionary containing the resulting values.
1409
+ """
1410
+ input_data = normalize_input(input_data, SetDimensionInput)
1411
+ result = await adapter.set_dimension(input_data.name, input_data.value)
1412
+
1413
+ require_success(result, context="Failed to set dimension")
1414
+ payload = result.data
1415
+ return {
1416
+ "status": "success",
1417
+ "message": f"Set dimension {input_data.name} = {input_data.value} mm",
1418
+ "dimension_update": {
1419
+ "name": input_data.name,
1420
+ "old_value": _result_value(payload, "old_value"),
1421
+ "new_value": _result_value(
1422
+ payload, "new_value", default=input_data.value
1423
+ ),
1424
+ "units": input_data.units or "mm",
1425
+ },
1426
+ "execution_time": result.execution_time,
1427
+ }
1428
+
1429
+ @mcp.tool()
1430
+ async def set_units(input_data: SetUnitsInput) -> dict[str, Any]:
1431
+ """Set the active document's linear unit system.
1432
+
1433
+ Accepts ``mm``, ``cm``, ``m``, ``in`` or ``ft`` (plus common aliases
1434
+ such as ``inch`` or ``millimeters``). The setting is applied and then
1435
+ read back from the document; if the readback disagrees the tool
1436
+ reports an error rather than a false success.
1437
+
1438
+ Args:
1439
+ input_data (SetUnitsInput): The target unit system.
1440
+
1441
+ Returns:
1442
+ dict[str, Any]: Status and what was applied.
1443
+
1444
+ Example:
1445
+ ```python
1446
+ await set_units({"unit_system": "in"})
1447
+ ```
1448
+ """
1449
+ input_data = normalize_input(input_data, SetUnitsInput)
1450
+ result = await adapter.set_units(input_data.unit_system)
1451
+ require_success(result, context="Failed to set units")
1452
+ data = result.data if isinstance(result.data, dict) else {}
1453
+ applied = data.get("unit_system", input_data.unit_system)
1454
+ message = f"Set document units to {applied}"
1455
+ if data.get("verified") is None:
1456
+ message += " (could not be read back to confirm)"
1457
+ return {
1458
+ "status": "success",
1459
+ "message": message,
1460
+ "data": data,
1461
+ "execution_time": result.execution_time,
1462
+ }
1463
+
1464
+ @mcp.tool()
1465
+ async def create_cut_extrude(input_data: CreateCutExtrudeInput) -> dict[str, Any]:
1466
+ """Cut material from a sketch profile in the active model.
1467
+
1468
+ Creates a Cut-Extrude feature (Insert > Cut > Extrude). Use this after
1469
+ exit_sketch when you want to remove material — e.g. to create windows,
1470
+ holes, slots, or any through/blind cut in an existing solid body.
1471
+
1472
+ Sketch selection is never a silent guess: pass ``sketch_name``
1473
+ explicitly whenever you're not certain which sketch this should act
1474
+ on (e.g. more than one sketch could be unconsumed, or other tool
1475
+ calls ran in between). If omitted, only the sketch most recently
1476
+ created/exited in this same session is used, and only when it still
1477
+ exists in the model — there is no further fallback. Any ambiguity or
1478
+ mismatch is reported back as an error naming the sketch(es) actually
1479
+ found in the model, rather than cutting the wrong profile.
1480
+
1481
+ Direction: a one-way cut (Blind or ThroughAll) goes along the sketch
1482
+ plane's normal. From a sketch on a face, or on a plane that touches
1483
+ the body only on one side (e.g. a Top Plane sketch under a block
1484
+ extruded up from it), that can point away from the material. The
1485
+ tool then cuts the other way and returns
1486
+ ``cut_extrude.direction_flipped: true``, so you don't need to guess
1487
+ ``reverse_direction``. For a sketch on a plane through the middle of
1488
+ the body, use ``ThroughAllBoth`` (or ``ThroughAll`` with
1489
+ ``both_directions``) to cut both sides.
1490
+
1491
+ Args:
1492
+ input_data (CreateCutExtrudeInput): Depth, direction, draft, and
1493
+ optional sketch_name parameters.
1494
+
1495
+ Returns:
1496
+ dict[str, Any]: Status and ``cut_extrude`` (name, depth,
1497
+ sketch_name, direction_flipped), plus ``measured_change``
1498
+ (volume and bounding box before and after) when the model
1499
+ can be read. On error, ``message`` identifies exactly which
1500
+ stage failed (sketch not found, sketch selection rejected by
1501
+ SolidWorks, the FeatureCut COM error, or ``error_type:
1502
+ feature_not_built`` when SolidWorks built nothing in either
1503
+ direction) rather than a generic failure string.
1504
+
1505
+ Example:
1506
+ ```python
1507
+ # Cut a blind pocket 10 mm deep from a named sketch
1508
+ result = await create_cut_extrude({"depth": 10.0, "sketch_name": "Sketch3"})
1509
+
1510
+ # Through-all cut using the most recently exited sketch
1511
+ result = await create_cut_extrude({"depth": 200.0})
1512
+ ```
1513
+ """
1514
+ input_data = normalize_input(input_data, CreateCutExtrudeInput)
1515
+ params = ExtrusionParameters(
1516
+ depth=input_data.depth,
1517
+ draft_angle=input_data.draft_angle,
1518
+ reverse_direction=input_data.reverse_direction,
1519
+ both_directions=input_data.both_directions,
1520
+ thin_feature=False,
1521
+ thin_thickness=None,
1522
+ end_condition=input_data.end_condition,
1523
+ merge_result=False,
1524
+ feature_scope=False,
1525
+ auto_select=True,
1526
+ sketch_name=input_data.sketch_name,
1527
+ )
1528
+ before = await _read_summary(adapter)
1529
+ result = await adapter.create_cut_extrude(params)
1530
+ require_success(result, context="Failed to create cut-extrude")
1531
+ after = await _read_summary(adapter) if before is not None else None
1532
+ feature = result.data
1533
+ feature_params = _result_value(feature, "parameters", default={})
1534
+ sketch_used = (
1535
+ feature_params.get("sketch_name")
1536
+ if isinstance(feature_params, dict)
1537
+ else None
1538
+ ) or input_data.sketch_name
1539
+ flipped = bool(
1540
+ isinstance(feature_params, dict) and feature_params.get("direction_flipped")
1541
+ )
1542
+ name = _result_value(feature, "feature_name", "name", default="Cut-Extrude")
1543
+ message = f"Created cut-extrude: {name}"
1544
+ if flipped:
1545
+ message += (
1546
+ " (the requested direction found no material, so it cut the other way)"
1547
+ )
1548
+ reply = {
1549
+ "status": "success",
1550
+ "message": message,
1551
+ "cut_extrude": {
1552
+ "name": name,
1553
+ "depth": input_data.depth,
1554
+ "sketch_name": sketch_used,
1555
+ "direction_flipped": flipped,
1556
+ },
1557
+ "execution_time": result.execution_time,
1558
+ }
1559
+ return _attach_measured_change(reply, before, after)
1560
+
1561
+ @mcp.tool()
1562
+ async def add_fillet(input_data: AddFilletInput) -> dict[str, Any]:
1563
+ """Add a fillet (rounded edge) to selected edges of the current model.
1564
+
1565
+ Rounds the specified named edges with the given radius. Edge names use the
1566
+ SolidWorks convention, e.g. 'Edge<1>', or you can leave edge_names empty to
1567
+ fillet all edges if the adapter supports it.
1568
+
1569
+ Args:
1570
+ input_data (AddFilletInput): Radius and edge names.
1571
+
1572
+ Returns:
1573
+ dict[str, Any]: Status and feature details.
1574
+
1575
+ Example:
1576
+ ```python
1577
+ # Fillet two specific edges with 2 mm radius
1578
+ result = await add_fillet({"radius": 2.0, "edge_names": ["Edge<1>", "Edge<2>"]})
1579
+ ```
1580
+ """
1581
+ input_data = normalize_input(input_data, AddFilletInput)
1582
+ result = await adapter.add_fillet(input_data.radius, input_data.edge_names)
1583
+ require_success(result, context="Failed to add fillet")
1584
+ feature = result.data
1585
+ return {
1586
+ "status": "success",
1587
+ "message": f"Created fillet: {_result_value(feature, 'feature_name', 'name', default='Fillet')}",
1588
+ "fillet": {
1589
+ "name": _result_value(
1590
+ feature, "feature_name", "name", default="Fillet"
1591
+ ),
1592
+ "radius": input_data.radius,
1593
+ "edges": input_data.edge_names,
1594
+ },
1595
+ "execution_time": result.execution_time,
1596
+ }
1597
+
1598
+ @mcp.tool()
1599
+ async def add_chamfer(input_data: AddChamferInput) -> dict[str, Any]:
1600
+ """Add an equal-distance chamfer to selected edges of the current model.
1601
+
1602
+ Chamfers the specified named edges with the given distance. Edge names use
1603
+ the SolidWorks convention, e.g. 'Edge<1>', or you can leave edge_names empty
1604
+ to chamfer all edges if the adapter supports it.
1605
+
1606
+ Args:
1607
+ input_data (AddChamferInput): Distance and edge names.
1608
+
1609
+ Returns:
1610
+ dict[str, Any]: Status and feature details.
1611
+
1612
+ Example:
1613
+ ```python
1614
+ # Chamfer two specific edges with a 1.5 mm distance
1615
+ result = await add_chamfer({"distance": 1.5, "edge_names": ["Edge<1>", "Edge<2>"]})
1616
+ ```
1617
+ """
1618
+ input_data = normalize_input(input_data, AddChamferInput)
1619
+ result = await adapter.add_chamfer(input_data.distance, input_data.edge_names)
1620
+ require_success(result, context="Failed to add chamfer")
1621
+ feature = result.data
1622
+ return {
1623
+ "status": "success",
1624
+ "message": f"Created chamfer: {_result_value(feature, 'feature_name', 'name', default='Chamfer')}",
1625
+ "chamfer": {
1626
+ "name": _result_value(
1627
+ feature, "feature_name", "name", default="Chamfer"
1628
+ ),
1629
+ "distance": input_data.distance,
1630
+ "edges": input_data.edge_names,
1631
+ },
1632
+ "execution_time": result.execution_time,
1633
+ }
1634
+
1635
+ @mcp.tool()
1636
+ async def create_sweep(input_data: CreateSweepInput) -> dict[str, Any]:
1637
+ """Sweep the active profile sketch along a named path sketch.
1638
+
1639
+ Creates a swept boss/protrusion (Insert > Boss/Base > Sweep). Requires
1640
+ two sketches in the active part: a closed profile sketch and an open
1641
+ path sketch named by ``path``. The profile is inferred as the sketch
1642
+ that is not the path (in the usual "draw profile, draw path, sweep"
1643
+ flow this is unambiguous). Optionally applies a constant twist along
1644
+ the path.
1645
+
1646
+ Args:
1647
+ input_data (CreateSweepInput): Path name and twist/merge options.
1648
+
1649
+ Returns:
1650
+ dict[str, Any]: Status and feature details.
1651
+
1652
+ Example:
1653
+ ```python
1654
+ # Sweep a circular profile along "Sketch2"
1655
+ result = await create_sweep({"path": "Sketch2"})
1656
+
1657
+ # Sweep with a 90-degree twist along the path
1658
+ result = await create_sweep({
1659
+ "path": "Sketch2",
1660
+ "twist_along_path": True,
1661
+ "twist_angle": 90.0,
1662
+ })
1663
+ ```
1664
+ """
1665
+ input_data = normalize_input(input_data, CreateSweepInput)
1666
+ params = SweepParameters(
1667
+ path=input_data.path,
1668
+ twist_along_path=input_data.twist_along_path,
1669
+ twist_angle=input_data.twist_angle,
1670
+ merge_result=input_data.merge_result,
1671
+ )
1672
+ result = await adapter.create_sweep(params)
1673
+ require_success(result, context="Failed to create sweep")
1674
+ feature = result.data
1675
+ return {
1676
+ "status": "success",
1677
+ "message": f"Created sweep: {_result_value(feature, 'feature_name', 'name', default='Sweep')}",
1678
+ "sweep": {
1679
+ "name": _result_value(feature, "feature_name", "name", default="Sweep"),
1680
+ "path": input_data.path,
1681
+ "twist_along_path": input_data.twist_along_path,
1682
+ "twist_angle": input_data.twist_angle,
1683
+ },
1684
+ "execution_time": result.execution_time,
1685
+ }
1686
+
1687
+ @mcp.tool()
1688
+ async def create_loft(input_data: CreateLoftInput) -> dict[str, Any]:
1689
+ """Loft a solid between two or more profile sketches.
1690
+
1691
+ Creates a lofted boss/protrusion (Insert > Boss/Base > Loft) blending
1692
+ the listed profile sketches in order. Each profile must be a closed
1693
+ contour. Optional guide curves shape the transition between profiles.
1694
+
1695
+ Args:
1696
+ input_data (CreateLoftInput): Profile names, optional guide curves,
1697
+ and tangency/merge options.
1698
+
1699
+ Returns:
1700
+ dict[str, Any]: Status and feature details.
1701
+
1702
+ Example:
1703
+ ```python
1704
+ # Loft between two profiles (e.g. a tapered bevel)
1705
+ result = await create_loft({"profiles": ["Sketch1", "Sketch2"]})
1706
+
1707
+ # Loft with guide curves
1708
+ result = await create_loft({
1709
+ "profiles": ["Sketch1", "Sketch2"],
1710
+ "guide_curves": ["Sketch3"],
1711
+ })
1712
+ ```
1713
+ """
1714
+ input_data = normalize_input(input_data, CreateLoftInput)
1715
+ params = LoftParameters(
1716
+ profiles=input_data.profiles,
1717
+ guide_curves=input_data.guide_curves,
1718
+ start_tangent=input_data.start_tangent,
1719
+ end_tangent=input_data.end_tangent,
1720
+ merge_result=input_data.merge_result,
1721
+ )
1722
+ result = await adapter.create_loft(params)
1723
+ require_success(result, context="Failed to create loft")
1724
+ feature = result.data
1725
+ return {
1726
+ "status": "success",
1727
+ "message": f"Created loft: {_result_value(feature, 'feature_name', 'name', default='Loft')}",
1728
+ "loft": {
1729
+ "name": _result_value(feature, "feature_name", "name", default="Loft"),
1730
+ "profiles": input_data.profiles,
1731
+ "guide_curves": input_data.guide_curves,
1732
+ },
1733
+ "execution_time": result.execution_time,
1734
+ }
1735
+
1736
+ @mcp.tool()
1737
+ async def insert_component(input_data: InsertComponentInput) -> dict[str, Any]:
1738
+ """Insert a part or sub-assembly into the active assembly.
1739
+
1740
+ Call ``create_assembly`` first. Position is in millimetres from the
1741
+ assembly origin.
1742
+
1743
+ **The component file must contain solid geometry** — SolidWorks
1744
+ silently refuses to insert an empty part, so a missing or empty
1745
+ component surfaces as an error rather than a fabricated success.
1746
+
1747
+ Args:
1748
+ input_data (InsertComponentInput): File path and position.
1749
+
1750
+ Returns:
1751
+ dict[str, Any]: Status, the component's instance name and counts.
1752
+
1753
+ Example:
1754
+ ```python
1755
+ await create_assembly({"name": "bracket_asm"})
1756
+ await insert_component({"file_path": "C:/parts/plate.sldprt"})
1757
+ await insert_component({"file_path": "C:/parts/plate.sldprt", "x": 100.0})
1758
+ ```
1759
+ """
1760
+ input_data = normalize_input(input_data, InsertComponentInput)
1761
+ result = await adapter.insert_component(
1762
+ input_data.file_path, input_data.x, input_data.y, input_data.z
1763
+ )
1764
+ require_success(result, context="Failed to insert component")
1765
+ data = result.data if isinstance(result.data, dict) else {}
1766
+ return {
1767
+ "status": "success",
1768
+ "message": f"Inserted {data.get('component', 'component')}",
1769
+ "component": data,
1770
+ "execution_time": result.execution_time,
1771
+ }
1772
+
1773
+ @mcp.tool()
1774
+ async def add_mate(input_data: AddMateInput) -> dict[str, Any]:
1775
+ """Mate two components in the active assembly.
1776
+
1777
+ Positions components relative to one another — coincident planes to
1778
+ stack or align them, concentric to line up axes, distance to hold a
1779
+ gap.
1780
+
1781
+ Entities are selected by feature name, so only each component's
1782
+ reference planes and axes can be mated. Mating to a specific face or
1783
+ edge needs entity names this adapter cannot enumerate.
1784
+
1785
+ Args:
1786
+ input_data (AddMateInput): Components, entities and mate type.
1787
+
1788
+ Returns:
1789
+ dict[str, Any]: Status and mate details.
1790
+
1791
+ Example:
1792
+ ```python
1793
+ components = await list_components()
1794
+ await add_mate({
1795
+ "component_a": components["components"][0],
1796
+ "component_b": components["components"][1],
1797
+ "mate_type": "coincident",
1798
+ })
1799
+ ```
1800
+ """
1801
+ input_data = normalize_input(input_data, AddMateInput)
1802
+ result = await adapter.add_mate(
1803
+ input_data.component_a,
1804
+ input_data.component_b,
1805
+ input_data.entity_a,
1806
+ input_data.entity_b,
1807
+ input_data.mate_type,
1808
+ input_data.alignment,
1809
+ input_data.distance,
1810
+ input_data.angle,
1811
+ )
1812
+ require_success(result, context="Failed to add mate")
1813
+ data = result.data if isinstance(result.data, dict) else {}
1814
+ message = (
1815
+ f"Added {input_data.mate_type} mate between "
1816
+ f"{input_data.component_a} and {input_data.component_b}"
1817
+ )
1818
+ # SolidWorks accepts a mate whose constraint is already
1819
+ # satisfied and moves nothing. That is a real success, but
1820
+ # saying so plainly beats leaving the caller to infer it.
1821
+ if data.get("geometry_moved") is False:
1822
+ message += " (no component moved - the constraint was already satisfied)"
1823
+ return {
1824
+ "status": "success",
1825
+ "message": message,
1826
+ "mate": data,
1827
+ "execution_time": result.execution_time,
1828
+ }
1829
+
1830
+ @mcp.tool()
1831
+ async def list_components() -> dict[str, Any]:
1832
+ """List the top-level components of the active assembly.
1833
+
1834
+ Returns:
1835
+ dict[str, Any]: Status and the component instance names.
1836
+ """
1837
+ result = await adapter.list_components()
1838
+ require_success(result, context="Failed to list components")
1839
+ components = result.data if isinstance(result.data, list) else []
1840
+ return {
1841
+ "status": "success",
1842
+ "message": f"{len(components)} component(s) in the assembly",
1843
+ "components": components,
1844
+ "execution_time": result.execution_time,
1845
+ }
1846
+
1847
+ @mcp.tool()
1848
+ async def delete_feature(input_data: DeleteFeatureInput) -> dict[str, Any]:
1849
+ """Delete a feature or sketch from the active model.
1850
+
1851
+ Deleting a parent feature also removes everything that depends on it —
1852
+ SolidWorks' normal cascade — so the response lists every feature that
1853
+ went, not just the one named.
1854
+
1855
+ Args:
1856
+ input_data (DeleteFeatureInput): The feature to delete.
1857
+
1858
+ Returns:
1859
+ dict[str, Any]: Status and what was removed.
1860
+
1861
+ Example:
1862
+ ```python
1863
+ await delete_feature({"name": "Boss-Extrude1"})
1864
+ ```
1865
+ """
1866
+ input_data = normalize_input(input_data, DeleteFeatureInput)
1867
+ result = await adapter.delete_feature(input_data.name)
1868
+ require_success(result, context="Failed to delete feature")
1869
+ data = result.data if isinstance(result.data, dict) else {}
1870
+ removed = data.get("removed_features") or [input_data.name]
1871
+ return {
1872
+ "status": "success",
1873
+ "message": (
1874
+ f"Deleted {input_data.name}"
1875
+ + (
1876
+ f" and {len(removed) - 1} dependent feature(s)"
1877
+ if len(removed) > 1
1878
+ else ""
1879
+ )
1880
+ ),
1881
+ "data": data,
1882
+ "execution_time": result.execution_time,
1883
+ }
1884
+
1885
+ @mcp.tool()
1886
+ async def suppress_feature(input_data: SuppressFeatureInput) -> dict[str, Any]:
1887
+ """Suppress or unsuppress a feature in the active model.
1888
+
1889
+ Suppressing rolls a feature and its children out of the model without
1890
+ deleting it — the reversible way to turn off a feature that is causing
1891
+ trouble.
1892
+
1893
+ Args:
1894
+ input_data (SuppressFeatureInput): The feature and desired state.
1895
+
1896
+ Returns:
1897
+ dict[str, Any]: Status and the resulting suppression state.
1898
+
1899
+ Example:
1900
+ ```python
1901
+ await suppress_feature({"name": "Fillet1"})
1902
+ await suppress_feature({"name": "Fillet1", "suppress": False})
1903
+ ```
1904
+ """
1905
+ input_data = normalize_input(input_data, SuppressFeatureInput)
1906
+ result = await adapter.suppress_feature(input_data.name, input_data.suppress)
1907
+ require_success(result, context="Failed to suppress feature")
1908
+ data = result.data if isinstance(result.data, dict) else {}
1909
+ verb = "Suppressed" if input_data.suppress else "Unsuppressed"
1910
+ message = f"{verb} {input_data.name}"
1911
+ if data.get("suppressed") is None:
1912
+ message += " (state could not be read back)"
1913
+ return {
1914
+ "status": "success",
1915
+ "message": message,
1916
+ "data": data,
1917
+ "execution_time": result.execution_time,
1918
+ }
1919
+
1920
+ @mcp.tool()
1921
+ async def rename_feature(input_data: RenameFeatureInput) -> dict[str, Any]:
1922
+ """Rename a feature on the feature tree.
1923
+
1924
+ SolidWorks silently refuses a rename onto a name another feature
1925
+ already uses, so the new name is read back and a mismatch is reported
1926
+ as an error rather than a quiet success. Renaming a feature to its
1927
+ current name is a no-op success.
1928
+
1929
+ Args:
1930
+ input_data (RenameFeatureInput): The feature and its new name.
1931
+
1932
+ Returns:
1933
+ dict[str, Any]: Status and the old and new names.
1934
+
1935
+ Example:
1936
+ ```python
1937
+ await rename_feature({"old_name": "Fillet1", "new_name": "EdgeBreak"})
1938
+ ```
1939
+ """
1940
+ input_data = normalize_input(input_data, RenameFeatureInput)
1941
+ result = await adapter.rename_feature(input_data.old_name, input_data.new_name)
1942
+ require_success(result, context="Failed to rename feature")
1943
+ data = result.data if isinstance(result.data, dict) else {}
1944
+ if data.get("renamed") is False:
1945
+ return {
1946
+ "status": "success",
1947
+ "message": (
1948
+ f"{input_data.old_name} unchanged: "
1949
+ + str(data.get("reason") or "no change")
1950
+ ),
1951
+ "data": data,
1952
+ "execution_time": result.execution_time,
1953
+ }
1954
+ return {
1955
+ "status": "success",
1956
+ "message": (
1957
+ f"Renamed {data.get('old_name', input_data.old_name)} to "
1958
+ f"{data.get('new_name', input_data.new_name)}"
1959
+ ),
1960
+ "data": data,
1961
+ "execution_time": result.execution_time,
1962
+ }
1963
+
1964
+ @mcp.tool()
1965
+ async def select_feature(input_data: SelectFeatureInput) -> dict[str, Any]:
1966
+ """Select a named feature, sketch, plane or component.
1967
+
1968
+ Replaces the current selection by default; pass ``append: true`` to
1969
+ add to it (e.g. to select several features in turn). A name that
1970
+ cannot be selected is an error, never a quiet success.
1971
+
1972
+ Args:
1973
+ input_data (SelectFeatureInput): The feature name and append flag.
1974
+
1975
+ Returns:
1976
+ dict[str, Any]: Status, the matched name and how many entities are
1977
+ now selected.
1978
+
1979
+ Example:
1980
+ ```python
1981
+ await select_feature({"feature_name": "Boss-Extrude1"})
1982
+ await select_feature({"feature_name": "Sketch1", "append": True})
1983
+ ```
1984
+ """
1985
+ input_data = normalize_input(input_data, SelectFeatureInput)
1986
+ result = await adapter.select_feature(
1987
+ input_data.feature_name, input_data.append
1988
+ )
1989
+ require_success(result)
1990
+ data = result.data if isinstance(result.data, dict) else {}
1991
+ return {
1992
+ "status": "success",
1993
+ "message": (
1994
+ f"Selected {data.get('selected_name', input_data.feature_name)} "
1995
+ f"({data.get('selection_count', '?')} selected)"
1996
+ ),
1997
+ "data": data,
1998
+ "execution_time": result.execution_time,
1999
+ }
2000
+
2001
+ @mcp.tool()
2002
+ async def select_face_edges(input_data: SelectFaceEdgesInput) -> dict[str, Any]:
2003
+ """Select every edge of one face of the active part (multi-select).
2004
+
2005
+ Identify the face with exactly one of ``point`` (a point on it, mm),
2006
+ ``side`` (``top``/``bottom``/``front``/``back``/``right``/``left`` or
2007
+ ``+X``..``-Z``; top=+Y, front=+Z, right=+X as in SolidWorks' default
2008
+ views - the furthest flat face facing that way) or ``plane`` (a
2009
+ reference plane name - the body face lying on it). ``edge_type``
2010
+ narrows to ``linear`` or ``circular`` edges (e.g. hole rims).
2011
+
2012
+ The edges become the live SolidWorks selection, so the next
2013
+ ``add_chamfer`` / ``add_fillet`` call with an empty ``edge_names``
2014
+ acts on exactly them. Anything that changes the selection in between
2015
+ (including the user clicking in SolidWorks) changes what is used.
2016
+
2017
+ Args:
2018
+ input_data (SelectFaceEdgesInput): Face identifier, edge filter and
2019
+ append flag.
2020
+
2021
+ Returns:
2022
+ dict[str, Any]: Status plus ``data.face`` (centre, normal, area),
2023
+ ``data.edges`` (one entry per selected edge: curve type,
2024
+ endpoints/length or centre/radius) and ``data.selection_count``.
2025
+
2026
+ Example:
2027
+ ```python
2028
+ # Chamfer the hole rims on the top face
2029
+ await select_face_edges({"side": "top", "edge_type": "circular"})
2030
+ await add_chamfer({"distance": 0.5, "edge_names": []})
2031
+ ```
2032
+ """
2033
+ input_data = normalize_input(input_data, SelectFaceEdgesInput)
2034
+ result = await adapter.select_face_edges(
2035
+ input_data.point,
2036
+ input_data.side,
2037
+ input_data.plane,
2038
+ input_data.edge_type,
2039
+ input_data.append,
2040
+ )
2041
+ require_success(result)
2042
+ data = result.data if isinstance(result.data, dict) else {}
2043
+ kinds = sorted({e.get("curve", "other") for e in data.get("edges", [])})
2044
+ return {
2045
+ "status": "success",
2046
+ "message": (
2047
+ f"Selected {data.get('selected_edges', 0)} edge(s) "
2048
+ f"({', '.join(kinds)}) on the face at "
2049
+ f"{data.get('face', {}).get('center_mm')} mm"
2050
+ ),
2051
+ "data": data,
2052
+ "execution_time": result.execution_time,
2053
+ }
2054
+
2055
+ @mcp.tool()
2056
+ async def rebuild_model(
2057
+ input_data: RebuildModelInput | None = None,
2058
+ ) -> dict[str, Any]:
2059
+ """Force a rebuild of the active model and report what is wrong.
2060
+
2061
+ Use after programmatic edits (``set_dimension``, feature edits) to
2062
+ confirm the model rebuilds cleanly. When features fail, the response
2063
+ is an error whose ``data.errors`` names each failing feature with its
2064
+ SolidWorks error code, the features/sketches it depends on, and a
2065
+ concrete ``hint`` for fixing it. Warnings alone still succeed.
2066
+
2067
+ Args:
2068
+ input_data (RebuildModelInput | None): ``top_level_only`` for
2069
+ assemblies; omit for a full rebuild.
2070
+
2071
+ Returns:
2072
+ dict[str, Any]: Status, a one-line summary, and ``data`` with
2073
+ ``errors``, ``error_count`` and ``warning_count``.
2074
+
2075
+ Example:
2076
+ ```python
2077
+ await rebuild_model()
2078
+ ```
2079
+ """
2080
+ input_data = normalize_input(input_data or {}, RebuildModelInput)
2081
+ result = require_success(
2082
+ await adapter.rebuild_model(input_data.top_level_only), with_data=True
2083
+ )
2084
+ data = result.data if isinstance(result.data, dict) else None
2085
+ response: dict[str, Any] = {
2086
+ "status": "success",
2087
+ "message": (data or {}).get("summary") or "Rebuilt",
2088
+ "execution_time": result.execution_time,
2089
+ }
2090
+ if data is not None:
2091
+ response["data"] = data
2092
+ return response
2093
+
2094
+ @mcp.tool()
2095
+ async def undo(input_data: UndoInput | None = None) -> dict[str, Any]:
2096
+ """Undo the last operations in the active model.
2097
+
2098
+ SolidWorks accepts an undo with nothing left to undo without
2099
+ complaining, so the response reports whether the feature tree actually
2100
+ changed rather than assuming it did.
2101
+
2102
+ Args:
2103
+ input_data (UndoInput | None): How many steps to undo.
2104
+
2105
+ Returns:
2106
+ dict[str, Any]: Status and whether the tree changed.
2107
+
2108
+ Example:
2109
+ ```python
2110
+ await undo({"count": 1})
2111
+ ```
2112
+ """
2113
+ input_data = normalize_input(input_data or UndoInput(), UndoInput)
2114
+ result = await adapter.undo(input_data.count)
2115
+ require_success(result, context="Failed to undo")
2116
+ data = result.data if isinstance(result.data, dict) else {}
2117
+ changed = data.get("tree_changed")
2118
+ if changed is False:
2119
+ message = (
2120
+ f"Undo of {input_data.count} step(s) left the feature "
2121
+ "tree unchanged - there was nothing to undo"
2122
+ )
2123
+ else:
2124
+ message = f"Undid {input_data.count} step(s)"
2125
+ return {
2126
+ "status": "success",
2127
+ "message": message,
2128
+ "data": data,
2129
+ "execution_time": result.execution_time,
2130
+ }
2131
+
2132
+ @mcp.tool()
2133
+ async def create_reference_plane(
2134
+ input_data: CreateReferencePlaneInput,
2135
+ ) -> dict[str, Any]:
2136
+ """Create a reference plane offset from an existing plane or planar face.
2137
+
2138
+ Adds a new plane to the feature tree, parallel to and offset from the
2139
+ named reference. The returned plane name can be passed straight to
2140
+ ``create_sketch`` to sketch anywhere other than the six built-in
2141
+ planes.
2142
+
2143
+ Angled planes are not supported by this tool: an angled plane needs a
2144
+ second reference (an axis or edge) to rotate about, which this
2145
+ signature cannot take. Use ``offset`` for parallel planes. An
2146
+ ``offset`` of zero is rejected — a plane coincident with its
2147
+ reference is not useful.
2148
+
2149
+ Args:
2150
+ input_data (CreateReferencePlaneInput): Reference name, offset,
2151
+ angle and flip.
2152
+
2153
+ Returns:
2154
+ dict[str, Any]: Status and the new plane's details.
2155
+
2156
+ Example:
2157
+ ```python
2158
+ plane = await create_reference_plane(
2159
+ {"reference": "Front Plane", "offset": 76.2}
2160
+ )
2161
+ await create_sketch({"plane": plane["plane"]["name"]})
2162
+ ```
2163
+ """
2164
+ input_data = normalize_input(input_data, CreateReferencePlaneInput)
2165
+ result = await adapter.create_reference_plane(
2166
+ input_data.reference,
2167
+ input_data.offset,
2168
+ input_data.angle,
2169
+ input_data.flip,
2170
+ )
2171
+ require_success(result, context="Failed to create reference plane")
2172
+ data = result.data if isinstance(result.data, dict) else {}
2173
+ return {
2174
+ "status": "success",
2175
+ "message": f"Created reference plane: {data.get('name', 'Plane')}",
2176
+ "plane": data,
2177
+ "execution_time": result.execution_time,
2178
+ }
2179
+
2180
+ @mcp.tool()
2181
+ async def create_axis(input_data: CreateAxisInput) -> dict[str, Any]:
2182
+ """Create a reference axis along a principal direction.
2183
+
2184
+ Builds an axis from the intersection of two built-in planes: x from
2185
+ Top + Front, y from Front + Right, z from Top + Right.
2186
+
2187
+ Args:
2188
+ input_data (CreateAxisInput): The principal direction ('x', 'y'
2189
+ or 'z').
2190
+
2191
+ Returns:
2192
+ dict[str, Any]: Status and the axis details (planes used).
2193
+
2194
+ Example:
2195
+ ```python
2196
+ result = await create_axis({"reference": "z"})
2197
+ ```
2198
+ """
2199
+ input_data = normalize_input(input_data, CreateAxisInput)
2200
+ result = await adapter.create_axis(input_data.reference)
2201
+ require_success(result, context="Failed to create axis")
2202
+ data = result.data if isinstance(result.data, dict) else {}
2203
+ axis_name = data.get("reference", input_data.reference)
2204
+ return {
2205
+ "status": "success",
2206
+ "message": f"Created {axis_name} axis",
2207
+ "axis": data,
2208
+ "execution_time": result.execution_time,
2209
+ }
2210
+
2211
+ @mcp.tool()
2212
+ async def create_reference_point(
2213
+ input_data: CreateReferencePointInput,
2214
+ ) -> dict[str, Any]:
2215
+ """Create a reference point on the active part.
2216
+
2217
+ Two modes:
2218
+
2219
+ - ``along_curve`` — a point on the edge under ``(x, y, z)`` mm, placed
2220
+ ``distance`` mm from the edge start, or at ``percent`` (0–100) of
2221
+ its length. Give exactly one of ``distance`` / ``percent``.
2222
+ - ``face_center`` — a point at the centroid of the face under
2223
+ ``(x, y, z)`` mm.
2224
+
2225
+ The coordinate must lie on the target edge/face; SolidWorks resolves
2226
+ it with a coordinate pick. ``InsertReferencePoint`` always returns a
2227
+ feature, so success is confirmed by the feature tree growing.
2228
+
2229
+ Args:
2230
+ input_data (CreateReferencePointInput): Mode, coordinate, and the
2231
+ along-curve parameter.
2232
+
2233
+ Returns:
2234
+ dict[str, Any]: Status and the point details (feature counts).
2235
+
2236
+ Example:
2237
+ ```python
2238
+ await create_reference_point({
2239
+ "mode": "along_curve", "x": 25, "y": 0, "z": 10, "percent": 50
2240
+ })
2241
+ ```
2242
+ """
2243
+ input_data = normalize_input(input_data, CreateReferencePointInput)
2244
+ result = await adapter.create_reference_point(
2245
+ input_data.mode,
2246
+ input_data.x,
2247
+ input_data.y,
2248
+ input_data.z,
2249
+ input_data.distance,
2250
+ input_data.percent,
2251
+ )
2252
+ require_success(result, context="Failed to create reference point")
2253
+ data = result.data if isinstance(result.data, dict) else {}
2254
+ where = (
2255
+ f"{data.get('percent')}% along a curve"
2256
+ if data.get("percent") is not None
2257
+ else (
2258
+ f"{data.get('distance_mm')} mm along a curve"
2259
+ if data.get("distance_mm") is not None
2260
+ else "a face centre"
2261
+ )
2262
+ )
2263
+ return {
2264
+ "status": "success",
2265
+ "message": f"Created a reference point at {where}",
2266
+ "point": data,
2267
+ "execution_time": result.execution_time,
2268
+ }
2269
+
2270
+ @mcp.tool()
2271
+ async def mirror_feature(input_data: MirrorFeatureInput) -> dict[str, Any]:
2272
+ """Mirror solid bodies or features about a plane.
2273
+
2274
+ Selects the named sources and the mirror plane, then mirrors them.
2275
+ The mirror is verified by comparing model volume before and after:
2276
+ SolidWorks can report a feature even when it mirrored nothing, so a
2277
+ volume that did not grow is reported as an error rather than a
2278
+ false success.
2279
+
2280
+ Args:
2281
+ input_data (MirrorFeatureInput): Source names, mirror plane, and
2282
+ merge/mirror_bodies flags.
2283
+
2284
+ Returns:
2285
+ dict[str, Any]: Status and the mirror feature's details,
2286
+ including volume before/after and the resulting ratio.
2287
+
2288
+ Example:
2289
+ ```python
2290
+ result = await mirror_feature(
2291
+ {"features": ["Loft1"], "mirror_plane": "Right Plane"}
2292
+ )
2293
+ ```
2294
+ """
2295
+ input_data = normalize_input(input_data, MirrorFeatureInput)
2296
+ result = await adapter.mirror_feature(
2297
+ input_data.features,
2298
+ input_data.mirror_plane,
2299
+ input_data.merge,
2300
+ input_data.mirror_bodies,
2301
+ )
2302
+ require_success(result, context="Failed to mirror feature")
2303
+ data = result.data if isinstance(result.data, dict) else {}
2304
+ return {
2305
+ "status": "success",
2306
+ "message": f"Mirrored {', '.join(input_data.features)} "
2307
+ f"about {input_data.mirror_plane}",
2308
+ "mirror": data,
2309
+ "execution_time": result.execution_time,
2310
+ }
2311
+
2312
+ @mcp.tool()
2313
+ async def pattern_circular(input_data: PatternCircularInput) -> dict[str, Any]:
2314
+ """Pattern features around an axis.
2315
+
2316
+ Selects the named axis and features, then creates a circular
2317
+ pattern. The pattern is verified by comparing model volume before
2318
+ and after: SolidWorks can report a feature even when it patterned
2319
+ nothing, so a volume that did not grow is reported as an error
2320
+ rather than a false success.
2321
+
2322
+ Args:
2323
+ input_data (PatternCircularInput): Feature names, axis, count,
2324
+ angle, and equal_spacing.
2325
+
2326
+ Returns:
2327
+ dict[str, Any]: Status and the pattern's details, including
2328
+ volume before/after and the resulting ratio.
2329
+
2330
+ Example:
2331
+ ```python
2332
+ result = await pattern_circular(
2333
+ {"features": ["Boss-Extrude1"], "axis": "Axis1", "count": 4}
2334
+ )
2335
+ ```
2336
+ """
2337
+ input_data = normalize_input(input_data, PatternCircularInput)
2338
+ result = await adapter.pattern_circular(
2339
+ input_data.features,
2340
+ input_data.axis,
2341
+ input_data.count,
2342
+ input_data.angle,
2343
+ input_data.equal_spacing,
2344
+ )
2345
+ require_success(result, context="Failed to pattern feature")
2346
+ data = result.data if isinstance(result.data, dict) else {}
2347
+ return {
2348
+ "status": "success",
2349
+ "message": f"Patterned {', '.join(input_data.features)} "
2350
+ f"around {input_data.axis} ({input_data.count} instances)",
2351
+ "pattern": data,
2352
+ "execution_time": result.execution_time,
2353
+ }
2354
+
2355
+ # Counted from the registry rather than hand-maintained. The literal that
2356
+ # used to live here had drifted: it read 15 while 16 tools were actually
2357
+ # registered, so the function misreported its own work and the test
2358
+ # enshrined the wrong number.
2359
+ tool_count = len(await mcp.list_tools())
2360
+ return tool_count