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,466 @@
1
+ """Engineering-data tools: dimensions, measurement, material, properties, flat pattern.
2
+
3
+ These read and edit the data a drawing, BOM or analysis is built from. Every tool
4
+ calls a real adapter method (``adapters/solidworks/documentation.py``) and returns
5
+ the adapter's own result - nothing here is simulated.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from fastmcp import FastMCP
13
+ from pydantic import Field
14
+
15
+ from ..adapters.base import AdapterResult, SolidWorksAdapter
16
+ from .input_compat import CompatInput
17
+ from .tool_boundary import (
18
+ normalize_input,
19
+ require_capability,
20
+ require_success,
21
+ with_tool_boundary,
22
+ )
23
+
24
+ # Formats ExportToDWG2 writes a flat pattern to.
25
+ _FLAT_PATTERN_SUFFIXES = (".dxf", ".dwg")
26
+
27
+
28
+ class ListModelDimensionsInput(CompatInput):
29
+ """Input schema for listing model dimensions.
30
+
31
+ Attributes:
32
+ feature_name (str | None): Only this feature's dimensions.
33
+ """
34
+
35
+ feature_name: str | None = Field(
36
+ default=None,
37
+ description="Only list this feature's dimensions, e.g. 'Boss-Extrude1'. "
38
+ "Omit to list every dimension in the model.",
39
+ )
40
+
41
+
42
+ class MeasureEntitiesInput(CompatInput):
43
+ """Input schema for measuring entities.
44
+
45
+ Attributes:
46
+ entities (list[str]): One or two entity specs.
47
+ """
48
+
49
+ entities: list[str] = Field(
50
+ description=(
51
+ "One or two entities to measure. Each is 'face:x,y,z' (a point inside "
52
+ "the face), 'edge:x,y,z', 'vertex:x,y,z', a bare 'x,y,z' (nearest "
53
+ "edge) or a topology name like 'Edge<1>'. Coordinates are in metres. "
54
+ "One face gives area/perimeter; one edge gives length/radius; two "
55
+ "entities give distance and angle."
56
+ ),
57
+ min_length=1,
58
+ max_length=2,
59
+ )
60
+
61
+
62
+ class GetMaterialInput(CompatInput):
63
+ """Input schema for reading a part's material.
64
+
65
+ Attributes:
66
+ configuration (str | None): Configuration name.
67
+ """
68
+
69
+ configuration: str | None = Field(
70
+ default=None, description="Configuration; the active one when omitted"
71
+ )
72
+
73
+
74
+ class SetMaterialInput(CompatInput):
75
+ """Input schema for assigning a material.
76
+
77
+ Attributes:
78
+ material_name (str): Exact library material name.
79
+ database (str | None): Material library name.
80
+ configuration (str | None): Configuration name.
81
+ """
82
+
83
+ material_name: str = Field(
84
+ description="Exact library name, e.g. 'AISI 1020', '6061-T6 (SS)', 'ABS'. "
85
+ "Use list_available_materials to find it.",
86
+ min_length=1,
87
+ )
88
+ database: str | None = Field(
89
+ default=None, description="Library; 'SOLIDWORKS Materials' when omitted"
90
+ )
91
+ configuration: str | None = Field(
92
+ default=None, description="Configuration; all configurations when omitted"
93
+ )
94
+
95
+
96
+ class ListMaterialsInput(CompatInput):
97
+ """Input schema for listing materials.
98
+
99
+ Attributes:
100
+ search (str | None): Substring filter.
101
+ """
102
+
103
+ search: str | None = Field(
104
+ default=None,
105
+ description="Case-insensitive filter on material name or classification, "
106
+ "e.g. 'steel' or '6061'",
107
+ )
108
+
109
+
110
+ class CustomPropertiesInput(CompatInput):
111
+ """Input schema for reading custom properties.
112
+
113
+ Attributes:
114
+ configuration (str | None): Configuration name.
115
+ """
116
+
117
+ configuration: str | None = Field(
118
+ default=None,
119
+ description="Configuration-specific properties; file-level (what title "
120
+ "blocks usually read) when omitted",
121
+ )
122
+
123
+
124
+ class SetCustomPropertyInput(CompatInput):
125
+ """Input schema for writing one custom property.
126
+
127
+ Attributes:
128
+ name (str): Property name.
129
+ value (str): Property value.
130
+ configuration (str | None): Configuration name.
131
+ """
132
+
133
+ name: str = Field(
134
+ description="Property name, e.g. 'PartNo', 'Description', 'DrawnBy'",
135
+ min_length=1,
136
+ )
137
+ value: str = Field(description='Text value; may be a $PRP:"SW-Mass" style link')
138
+ configuration: str | None = Field(
139
+ default=None, description="Configuration; file-level when omitted"
140
+ )
141
+
142
+
143
+ class ExportFlatPatternInput(CompatInput):
144
+ """Input schema for sheet-metal flat-pattern export.
145
+
146
+ Attributes:
147
+ file_path (str): Output .dxf or .dwg path.
148
+ """
149
+
150
+ file_path: str = Field(description="Output path ending in .dxf or .dwg")
151
+
152
+ def model_post_init(self, __context: Any) -> None:
153
+ """Refuse an output path that is not a DXF or DWG file.
154
+
155
+ Args:
156
+ __context (Any): The context value.
157
+
158
+ Returns:
159
+ None: None.
160
+
161
+ Raises:
162
+ ValueError: When ``file_path`` does not end in ``.dxf`` or ``.dwg``.
163
+ """
164
+ if not self.file_path.lower().endswith(_FLAT_PATTERN_SUFFIXES):
165
+ raise ValueError(
166
+ f"file_path must end in .dxf or .dwg, got {self.file_path!r}"
167
+ )
168
+
169
+
170
+ def _success(result: AdapterResult[Any], message: str) -> dict[str, Any]:
171
+ """Shape a successful adapter result as the standard tool response.
172
+
173
+ Args:
174
+ result: The adapter's successful result.
175
+ message: What was done, for the ``message`` field.
176
+
177
+ Returns:
178
+ dict[str, Any]: ``status``, ``message``, ``data`` and ``execution_time``.
179
+ """
180
+ return {
181
+ "status": "success",
182
+ "message": message,
183
+ "data": result.data,
184
+ "execution_time": result.execution_time,
185
+ }
186
+
187
+
188
+ async def register_engineering_data_tools(
189
+ mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
190
+ ) -> int:
191
+ """Register engineering-data tools with FastMCP.
192
+
193
+ Args:
194
+ mcp (FastMCP): The MCP server.
195
+ adapter (SolidWorksAdapter): Adapter used by every tool.
196
+ config (dict[str, Any]): Server configuration (unused).
197
+
198
+ Returns:
199
+ int: Number of tools registered.
200
+ """
201
+
202
+ mcp = with_tool_boundary(mcp)
203
+
204
+ def _supports(method: str) -> None:
205
+ require_capability(
206
+ adapter,
207
+ method,
208
+ f"The active adapter does not support {method}; nothing was done.",
209
+ )
210
+
211
+ @mcp.tool()
212
+ async def list_model_dimensions(
213
+ input_data: ListModelDimensionsInput | None = None,
214
+ ) -> dict[str, Any]:
215
+ """List every dimension in the active part or assembly with its full name.
216
+
217
+ Read-only. Each entry's ``full_name`` (e.g. ``"D1@Sketch1"``) is the
218
+ name ``set_dimension`` / ``get_dimension`` accept, so this is how to
219
+ find and edit any driving dimension in an existing model. Pass
220
+ ``feature_name`` to list one feature's dimensions only.
221
+
222
+ Args:
223
+ input_data (ListModelDimensionsInput | None): ``feature_name`` to
224
+ list one feature only; omit for every dimension.
225
+
226
+ Returns:
227
+ dict[str, Any]: ``data`` is a list of ``{full_name, feature, value
228
+ (document units), system_value (metres or radians), read_only}``.
229
+ ``not_found`` when ``feature_name`` has no dimensions;
230
+ ``wrong_document_type`` on a drawing.
231
+
232
+ Example:
233
+ ```python
234
+ await list_model_dimensions()
235
+ await list_model_dimensions({"feature_name": "Boss-Extrude1"})
236
+ ```
237
+ """
238
+ input_data = normalize_input(input_data, ListModelDimensionsInput)
239
+ _supports("list_model_dimensions")
240
+ result = await adapter.list_model_dimensions(input_data.feature_name)
241
+ require_success(result, context="Failed to list model dimensions")
242
+ return _success(result, f"Listed {len(result.data or [])} dimension(s)")
243
+
244
+ @mcp.tool()
245
+ async def measure_entities(input_data: MeasureEntitiesInput) -> dict[str, Any]:
246
+ """Measure faces, edges or vertices with SolidWorks' Measure tool.
247
+
248
+ One face gives its area and perimeter; one edge its length (and radius
249
+ and diameter for an arc); two entities give the distance and angle
250
+ between them. Entity coordinates are in metres. Clears the selection
251
+ afterwards.
252
+
253
+ Args:
254
+ input_data (MeasureEntitiesInput): One or two entity specs.
255
+
256
+ Returns:
257
+ dict[str, Any]: ``data`` holds ``selected`` plus whichever of
258
+ ``distance_m``, ``center_distance_m``, ``angle_rad``, ``radius_m``,
259
+ ``diameter_m``, ``length_m``, ``arc_length_m``, ``area_m2``,
260
+ ``perimeter_m``, ``delta_x_m``/``_y_m``/``_z_m`` and (for two
261
+ entities) ``is_parallel`` apply to the selection. ``not_found``
262
+ when an entity cannot be selected.
263
+
264
+ Example:
265
+ ```python
266
+ # Area of the face through (15, 15, 0) mm.
267
+ await measure_entities({"entities": ["face:0.015,0.015,0.0"]})
268
+ # Distance and angle between two faces.
269
+ await measure_entities(
270
+ {"entities": ["face:0.015,0.015,0.0", "face:0.015,0.015,-0.015"]}
271
+ )
272
+ ```
273
+ """
274
+ input_data = normalize_input(input_data, MeasureEntitiesInput)
275
+ _supports("measure_entities")
276
+ result = await adapter.measure_entities(input_data.entities)
277
+ require_success(result, context="Failed to measure")
278
+ return _success(result, f"Measured {', '.join(input_data.entities)}")
279
+
280
+ @mcp.tool()
281
+ async def get_material(
282
+ input_data: GetMaterialInput | None = None,
283
+ ) -> dict[str, Any]:
284
+ """Read the material assigned to the active part.
285
+
286
+ Read-only. Parts only (``wrong_document_type`` otherwise).
287
+
288
+ Args:
289
+ input_data (GetMaterialInput | None): ``configuration``; the active
290
+ one when omitted.
291
+
292
+ Returns:
293
+ dict[str, Any]: ``data`` is ``{material, database, configuration}``;
294
+ ``material`` is ``None`` when none is assigned.
295
+
296
+ Example:
297
+ ```python
298
+ await get_material()
299
+ ```
300
+ """
301
+ input_data = normalize_input(input_data, GetMaterialInput)
302
+ _supports("get_material")
303
+ result = await adapter.get_material(input_data.configuration)
304
+ require_success(result, context="Failed to read the material")
305
+ material = (result.data or {}).get("material")
306
+ return _success(
307
+ result, f"Material: {material}" if material else "No material assigned"
308
+ )
309
+
310
+ @mcp.tool()
311
+ async def set_material(input_data: SetMaterialInput) -> dict[str, Any]:
312
+ """Assign a library material to the active part.
313
+
314
+ The material drives mass properties (``get_mass_properties``) and what
315
+ a title block's Material field shows. The name must match the library
316
+ exactly; find it with ``list_available_materials``. The material is
317
+ read back after assignment, so success means it really applied.
318
+
319
+ Args:
320
+ input_data (SetMaterialInput): ``material_name``, plus optional
321
+ ``database`` and ``configuration``.
322
+
323
+ Returns:
324
+ dict[str, Any]: ``data`` is ``{material, database, configuration}``
325
+ as read back from the part. ``not_found`` when the library does
326
+ not hold that name (the part is left unchanged).
327
+
328
+ Example:
329
+ ```python
330
+ await set_material({"material_name": "AISI 1020"})
331
+ ```
332
+ """
333
+ input_data = normalize_input(input_data, SetMaterialInput)
334
+ _supports("set_material")
335
+ result = await adapter.set_material(
336
+ input_data.material_name, input_data.database, input_data.configuration
337
+ )
338
+ require_success(result, context="Failed to assign the material")
339
+ return _success(result, f"Assigned {input_data.material_name}")
340
+
341
+ @mcp.tool()
342
+ async def list_available_materials(
343
+ input_data: ListMaterialsInput | None = None,
344
+ ) -> dict[str, Any]:
345
+ """List materials in the installed SolidWorks material libraries.
346
+
347
+ Read-only; parses every ``.sldmat`` library SolidWorks is configured
348
+ to use. Filter with ``search`` (name or classification substring, e.g.
349
+ ``"steel"`` or ``"6061"``) and pass the exact ``name`` to
350
+ ``set_material``.
351
+
352
+ Args:
353
+ input_data (ListMaterialsInput | None): ``search`` to filter;
354
+ omit for every material.
355
+
356
+ Returns:
357
+ dict[str, Any]: ``data`` is a list of ``{name, classification,
358
+ database}``; ``message`` says how many of the libraries' materials
359
+ matched and which folders were read.
360
+
361
+ Example:
362
+ ```python
363
+ await list_available_materials({"search": "1020"})
364
+ ```
365
+ """
366
+ input_data = normalize_input(input_data, ListMaterialsInput)
367
+ _supports("list_available_materials")
368
+ result = await adapter.list_available_materials(input_data.search)
369
+ require_success(result, context="Failed to list materials")
370
+ searched = result.metadata or {}
371
+ found = len(result.data or [])
372
+ message = f"Found {found} material(s)"
373
+ if "total" in searched:
374
+ message += (
375
+ f" of {searched['total']} in {'; '.join(searched.get('folders', []))}"
376
+ )
377
+ return _success(result, message)
378
+
379
+ @mcp.tool()
380
+ async def get_custom_properties(
381
+ input_data: CustomPropertiesInput | None = None,
382
+ ) -> dict[str, Any]:
383
+ """Read the active document's custom properties.
384
+
385
+ Read-only. File-level properties (the default) are what drawing title
386
+ blocks and BOMs usually link to; pass ``configuration`` for
387
+ configuration-specific ones.
388
+
389
+ Args:
390
+ input_data (CustomPropertiesInput | None): ``configuration``;
391
+ file-level properties when omitted.
392
+
393
+ Returns:
394
+ dict[str, Any]: ``data`` is ``{configuration, properties}``, where
395
+ ``properties`` maps each name to ``{value, resolved}`` (``value``
396
+ may be a ``$PRP:`` link; ``resolved`` is what it evaluates to).
397
+
398
+ Example:
399
+ ```python
400
+ await get_custom_properties()
401
+ ```
402
+ """
403
+ input_data = normalize_input(input_data, CustomPropertiesInput)
404
+ _supports("get_custom_properties")
405
+ result = await adapter.get_custom_properties(input_data.configuration)
406
+ require_success(result, context="Failed to read custom properties")
407
+ count = len((result.data or {}).get("properties", {}))
408
+ return _success(result, f"Read {count} custom propert(ies)")
409
+
410
+ @mcp.tool()
411
+ async def set_custom_property(input_data: SetCustomPropertyInput) -> dict[str, Any]:
412
+ """Create or overwrite one text custom property on the active document.
413
+
414
+ Typical names: ``PartNo``, ``Description``, ``DrawnBy``, ``Revision``.
415
+ The value is read back after writing, so success means it was stored.
416
+
417
+ Args:
418
+ input_data (SetCustomPropertyInput): ``name`` and ``value``, plus
419
+ an optional ``configuration``.
420
+
421
+ Returns:
422
+ dict[str, Any]: ``data`` is ``{name, value, resolved,
423
+ configuration}`` as read back.
424
+
425
+ Example:
426
+ ```python
427
+ await set_custom_property({"name": "PartNo", "value": "LIVE-001"})
428
+ ```
429
+ """
430
+ input_data = normalize_input(input_data, SetCustomPropertyInput)
431
+ _supports("set_custom_property")
432
+ result = await adapter.set_custom_property(
433
+ input_data.name, input_data.value, input_data.configuration
434
+ )
435
+ require_success(result, context="Failed to set the custom property")
436
+ return _success(result, f"Set custom property {input_data.name}")
437
+
438
+ @mcp.tool()
439
+ async def export_flat_pattern(input_data: ExportFlatPatternInput) -> dict[str, Any]:
440
+ """Export the active sheet-metal part's flat pattern to DXF or DWG.
441
+
442
+ Exports the flat-pattern geometry only (no bend lines or sketches),
443
+ for laser or waterjet cutting. A part without a sheet-metal body is
444
+ refused (``not_found``) and nothing is written.
445
+
446
+ Args:
447
+ input_data (ExportFlatPatternInput): ``file_path`` ending in
448
+ ``.dxf`` or ``.dwg``.
449
+
450
+ Returns:
451
+ dict[str, Any]: ``data`` is ``{file_path, size_bytes}`` of the
452
+ written file.
453
+
454
+ Example:
455
+ ```python
456
+ await export_flat_pattern({"file_path": "C:/cut/bracket.dxf"})
457
+ ```
458
+ """
459
+ input_data = normalize_input(input_data, ExportFlatPatternInput)
460
+ _supports("export_flat_pattern")
461
+ result = await adapter.export_flat_pattern(input_data.file_path)
462
+ require_success(result, context="Failed to export the flat pattern")
463
+ written = (result.data or {}).get("file_path")
464
+ return _success(result, f"Exported the flat pattern to {written}")
465
+
466
+ return 8
@@ -0,0 +1,232 @@
1
+ """Classify tool failures into actionable error categories.
2
+
3
+ The MCP tool error boundary (``tool_boundary``) asks this module what kind of
4
+ failure it is looking at, so the payload can carry an ``error_type`` and a
5
+ ``hint`` telling the caller what to do next instead of an opaque message.
6
+
7
+ Adapter errors usually arrive as *strings* - ``_handle_com_operation`` flattens
8
+ COM exceptions to ``"COM error in <op>: (<hresult>, '<text>', ...)"`` - so every
9
+ rule can match on the message text, and COM rules also on the HRESULT taken
10
+ from either a ``com_error`` exception or that text.
11
+
12
+ Rules are evaluated in order; the first match wins. Add a category by
13
+ appending a rule - callers never change.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from dataclasses import dataclass, field
20
+
21
+ # HRESULTs (signed 32-bit, as pywin32 reports them).
22
+ RPC_E_DISCONNECTED = -2147417848 # 0x80010108
23
+ RPC_S_SERVER_UNAVAILABLE = -2147023174 # 0x800706BA
24
+ RPC_S_CALL_FAILED = -2147023170 # 0x800706BE
25
+ DISP_E_MEMBERNOTFOUND = -2147352573 # 0x80020003
26
+ DISP_E_TYPEMISMATCH = -2147352571 # 0x80020005
27
+
28
+ _HRESULT_IN_TEXT = re.compile(r"\((-\d{9,10}),")
29
+
30
+ OPERATION_FAILED = "operation_failed"
31
+ INTERNAL = "internal"
32
+ INVALID_INPUT = "invalid_input"
33
+ NOT_SUPPORTED = "not_supported"
34
+ NOT_SUPPORTED_HINT = (
35
+ "The active adapter cannot perform this operation. Use the real SolidWorks "
36
+ "adapter, another tool, or do this step in SolidWorks directly."
37
+ )
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class Classification:
42
+ """The category of a failure and the next step to suggest."""
43
+
44
+ error_type: str
45
+ hint: str
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class ErrorRule:
50
+ """Matches a failure by HRESULT and/or a case-insensitive message pattern."""
51
+
52
+ error_type: str
53
+ hint: str
54
+ hresults: frozenset[int] = field(default_factory=frozenset)
55
+ patterns: tuple[str, ...] = ()
56
+
57
+ def matches(self, message: str, hresult: int | None) -> bool:
58
+ """Return True when the HRESULT or any pattern matches."""
59
+ if hresult is not None and hresult in self.hresults:
60
+ return True
61
+ return any(re.search(p, message, re.IGNORECASE) for p in self.patterns)
62
+
63
+
64
+ DEFAULT_RULES: tuple[ErrorRule, ...] = (
65
+ ErrorRule(
66
+ error_type="solidworks_unavailable",
67
+ hint=(
68
+ "SolidWorks is not reachable - it was closed or crashed. Restart "
69
+ "SolidWorks (and the MCP server if it still fails), then retry."
70
+ ),
71
+ hresults=frozenset(
72
+ {RPC_E_DISCONNECTED, RPC_S_SERVER_UNAVAILABLE, RPC_S_CALL_FAILED}
73
+ ),
74
+ patterns=(
75
+ r"rpc server is unavailable",
76
+ r"object invoked has disconnected",
77
+ r"not connected to solidworks",
78
+ r"solidworks (application )?(is )?not connected",
79
+ r"solidworks is not running",
80
+ ),
81
+ ),
82
+ ErrorRule(
83
+ error_type="com_wrong_thread",
84
+ hint=(
85
+ "A SolidWorks call ran off the thread that connected - a server "
86
+ "bug. Report the tool name; restart the MCP server to recover."
87
+ ),
88
+ patterns=(r"belong to the thread that called connect",),
89
+ ),
90
+ ErrorRule(
91
+ error_type="circuit_open",
92
+ hint=(
93
+ "Calls to this operation are paused after repeated failures. Fix "
94
+ "the underlying error, then retry after the breaker timeout."
95
+ ),
96
+ patterns=(r"circuit breaker is (open|half[_-]open)",),
97
+ ),
98
+ ErrorRule(
99
+ error_type="com_member_not_found",
100
+ hint=(
101
+ "SolidWorks could not resolve this API member. Usually a server "
102
+ "bug: a method called without being flagged as one (report the "
103
+ "tool name). After a SolidWorks upgrade it can be a stale type "
104
+ "cache: delete %TEMP%\\gen_py and restart the MCP server."
105
+ ),
106
+ hresults=frozenset({DISP_E_MEMBERNOTFOUND}),
107
+ patterns=(r"member not found",),
108
+ ),
109
+ ErrorRule(
110
+ error_type="com_type_mismatch",
111
+ hint=(
112
+ "SolidWorks rejected an argument's COM type. This is a server bug; "
113
+ "report the tool name and arguments."
114
+ ),
115
+ hresults=frozenset({DISP_E_TYPEMISMATCH}),
116
+ patterns=(r"type mismatch",),
117
+ ),
118
+ ErrorRule(
119
+ error_type=NOT_SUPPORTED,
120
+ hint=NOT_SUPPORTED_HINT,
121
+ patterns=(
122
+ r"does not support",
123
+ r"no adapter capability",
124
+ r"only available on windows",
125
+ r"not available",
126
+ ),
127
+ ),
128
+ ErrorRule(
129
+ error_type="wrong_document_type",
130
+ hint=(
131
+ "The active document is the wrong kind for this tool. Create or "
132
+ "activate the right one (create_part, create_assembly, "
133
+ "create_drawing or activate_document), then retry."
134
+ ),
135
+ patterns=(
136
+ r"(requires|needs) an? (active )?(part|assembly|drawing)"
137
+ r"( or (part|assembly|drawing))? document",
138
+ ),
139
+ ),
140
+ ErrorRule(
141
+ error_type="no_active_sketch",
142
+ hint="Create or open a sketch first (create_sketch), then retry.",
143
+ patterns=(r"no active sketch",),
144
+ ),
145
+ ErrorRule(
146
+ error_type="no_active_document",
147
+ hint=(
148
+ "Open or create a document first (open_model / create_part), or "
149
+ "activate one with activate_document, then retry."
150
+ ),
151
+ patterns=(r"no active (model|document)",),
152
+ ),
153
+ ErrorRule(
154
+ error_type="feature_not_built",
155
+ hint=(
156
+ "SolidWorks accepted the call but the geometry gave it nothing to "
157
+ "build. For a cut: the profile must be a closed loop that overlaps "
158
+ "the solid within the depth; one-direction cuts already try both "
159
+ "directions, so change the sketch, its plane, the depth or use "
160
+ "end_condition 'ThroughAll' rather than retrying as is."
161
+ ),
162
+ patterns=(r"built no feature",),
163
+ ),
164
+ ErrorRule(
165
+ error_type="not_found",
166
+ hint=(
167
+ "Check the name or path - list what exists first (list_features, "
168
+ "list_open_documents, list_drawing_views) and use an exact name."
169
+ ),
170
+ patterns=(
171
+ r"\bnot found\b",
172
+ r"does not exist",
173
+ r"\bno such\b",
174
+ r"\bno [\w.-]+( \w+)? found\b",
175
+ r"\bhas no (\w+ )?(edges|faces)\b",
176
+ r"\bhas no [\w-]+ body\b",
177
+ r"\bno view named\b",
178
+ ),
179
+ ),
180
+ )
181
+
182
+ _FALLBACK_HINTS = {
183
+ OPERATION_FAILED: (
184
+ "The SolidWorks operation failed. Read the message for the cause, "
185
+ "adjust the inputs, and retry."
186
+ ),
187
+ INTERNAL: (
188
+ "Unexpected server error - this is a bug, not a modelling problem. "
189
+ "Report the tool name, arguments and this message."
190
+ ),
191
+ }
192
+
193
+
194
+ def extract_hresult(message: str, exc: BaseException | None = None) -> int | None:
195
+ """Return the COM HRESULT from a ``com_error`` or from flattened text."""
196
+ if exc is not None:
197
+ args = getattr(exc, "args", ())
198
+ if args and isinstance(args[0], int):
199
+ return args[0]
200
+ match = _HRESULT_IN_TEXT.search(message)
201
+ return int(match.group(1)) if match else None
202
+
203
+
204
+ class ErrorClassifier:
205
+ """Map a failure to a :class:`Classification` using ordered rules."""
206
+
207
+ def __init__(self, rules: tuple[ErrorRule, ...] = DEFAULT_RULES) -> None:
208
+ self._rules = rules
209
+
210
+ def classify(
211
+ self,
212
+ message: str,
213
+ exc: BaseException | None = None,
214
+ *,
215
+ fallback: str = OPERATION_FAILED,
216
+ ) -> Classification:
217
+ """Classify ``message`` (and ``exc`` when there is one).
218
+
219
+ Args:
220
+ message: The error text the caller will see.
221
+ exc: The exception, when the failure was raised rather than returned.
222
+ fallback: Category for an unrecognised failure: ``operation_failed``
223
+ for adapter errors, ``internal`` for unexpected exceptions.
224
+
225
+ Returns:
226
+ Classification: The first matching rule, else the fallback.
227
+ """
228
+ hresult = extract_hresult(message, exc)
229
+ for rule in self._rules:
230
+ if rule.matches(message, hresult):
231
+ return Classification(rule.error_type, rule.hint)
232
+ return Classification(fallback, _FALLBACK_HINTS[fallback])