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.
- solidworks_mcp/__init__.py +57 -0
- solidworks_mcp/adapters/__init__.py +40 -0
- solidworks_mcp/adapters/base.py +1882 -0
- solidworks_mcp/adapters/circuit_breaker.py +2052 -0
- solidworks_mcp/adapters/com_executor.py +252 -0
- solidworks_mcp/adapters/complexity_analyzer.py +272 -0
- solidworks_mcp/adapters/connection_pool.py +1884 -0
- solidworks_mcp/adapters/factory.py +366 -0
- solidworks_mcp/adapters/intelligent_router.py +218 -0
- solidworks_mcp/adapters/mock_adapter.py +4016 -0
- solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
- solidworks_mcp/adapters/solidworks/__init__.py +17 -0
- solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
- solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
- solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
- solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
- solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
- solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
- solidworks_mcp/adapters/solidworks/features.py +3362 -0
- solidworks_mcp/adapters/solidworks/io.py +3649 -0
- solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
- solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
- solidworks_mcp/adapters/solidworks/selection.py +308 -0
- solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
- solidworks_mcp/adapters/sw_feature_errors.py +66 -0
- solidworks_mcp/adapters/sw_type_info.py +317 -0
- solidworks_mcp/adapters/vba_adapter.py +318 -0
- solidworks_mcp/adapters/vba_macro_executor.py +215 -0
- solidworks_mcp/agents/__init__.py +68 -0
- solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
- solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
- solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
- solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
- solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
- solidworks_mcp/agents/harness.py +216 -0
- solidworks_mcp/agents/history_db.py +1508 -0
- solidworks_mcp/agents/retrieval_index.py +164 -0
- solidworks_mcp/agents/schemas.py +165 -0
- solidworks_mcp/agents/smoke_test.py +285 -0
- solidworks_mcp/agents/soc_exporter.py +650 -0
- solidworks_mcp/agents/soc_pickup.py +332 -0
- solidworks_mcp/agents/soc_rewind.py +221 -0
- solidworks_mcp/agents/vector_rag.py +665 -0
- solidworks_mcp/cache/__init__.py +5 -0
- solidworks_mcp/cache/response_cache.py +164 -0
- solidworks_mcp/config.py +729 -0
- solidworks_mcp/exceptions.py +37 -0
- solidworks_mcp/security/__init__.py +68 -0
- solidworks_mcp/security/auth.py +128 -0
- solidworks_mcp/security/cors.py +27 -0
- solidworks_mcp/security/rate_limiting.py +90 -0
- solidworks_mcp/security/runtime.py +155 -0
- solidworks_mcp/server.py +777 -0
- solidworks_mcp/server_cli_fixed.py +120 -0
- solidworks_mcp/setup/__init__.py +1 -0
- solidworks_mcp/setup/cli.py +399 -0
- solidworks_mcp/setup/clients.py +157 -0
- solidworks_mcp/setup/config_file.py +230 -0
- solidworks_mcp/setup/handshake.py +202 -0
- solidworks_mcp/setup/launcher.py +102 -0
- solidworks_mcp/setup/legacy.py +100 -0
- solidworks_mcp/setup/registration.py +193 -0
- solidworks_mcp/setup/solidworks.py +49 -0
- solidworks_mcp/tools/__init__.py +64 -0
- solidworks_mcp/tools/analysis.py +723 -0
- solidworks_mcp/tools/automation.py +578 -0
- solidworks_mcp/tools/docs_discovery.py +1476 -0
- solidworks_mcp/tools/drawing.py +1015 -0
- solidworks_mcp/tools/drawing_analysis.py +456 -0
- solidworks_mcp/tools/drawing_automation.py +627 -0
- solidworks_mcp/tools/engineering_data.py +466 -0
- solidworks_mcp/tools/error_classifier.py +232 -0
- solidworks_mcp/tools/export.py +833 -0
- solidworks_mcp/tools/file_facts.py +40 -0
- solidworks_mcp/tools/file_management.py +1473 -0
- solidworks_mcp/tools/input_compat.py +17 -0
- solidworks_mcp/tools/macro_recording.py +464 -0
- solidworks_mcp/tools/modeling.py +2360 -0
- solidworks_mcp/tools/sketching.py +1422 -0
- solidworks_mcp/tools/template_management.py +367 -0
- solidworks_mcp/tools/tool_boundary.py +280 -0
- solidworks_mcp/tools/vba_generation.py +1145 -0
- solidworks_mcp/ui/__init__.py +9 -0
- solidworks_mcp/ui/local_llm.py +725 -0
- solidworks_mcp/ui/routers/__init__.py +10 -0
- solidworks_mcp/ui/routers/checkpoint.py +24 -0
- solidworks_mcp/ui/routers/docs.py +47 -0
- solidworks_mcp/ui/routers/llm.py +83 -0
- solidworks_mcp/ui/routers/local_model.py +69 -0
- solidworks_mcp/ui/routers/model.py +75 -0
- solidworks_mcp/ui/routers/preview.py +38 -0
- solidworks_mcp/ui/routers/session.py +161 -0
- solidworks_mcp/ui/routers/viewer.py +154 -0
- solidworks_mcp/ui/schemas.py +502 -0
- solidworks_mcp/ui/server.py +298 -0
- solidworks_mcp/ui/service.py +481 -0
- solidworks_mcp/ui/services/__init__.py +136 -0
- solidworks_mcp/ui/services/_utils.py +958 -0
- solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
- solidworks_mcp/ui/services/docs_service.py +282 -0
- solidworks_mcp/ui/services/llm_service.py +956 -0
- solidworks_mcp/ui/services/model_service.py +459 -0
- solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
- solidworks_mcp/ui/services/preview_service.py +489 -0
- solidworks_mcp/ui/services/session_service.py +1389 -0
- solidworks_mcp/utils/__init__.py +9 -0
- solidworks_mcp/utils/feature_tree_classifier.py +342 -0
- solidworks_mcp/utils/logging.py +76 -0
- solidworks_mcp/utils/validation.py +77 -0
- solidworks_mcp/version.py +8 -0
- solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
- solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
- solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
- solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
- 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])
|