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,1473 @@
|
|
|
1
|
+
"""File management tools for SolidWorks MCP Server.
|
|
2
|
+
|
|
3
|
+
Provides tools for managing SolidWorks files including save, save as, file properties,
|
|
4
|
+
and reference management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fastmcp import FastMCP
|
|
13
|
+
from pydantic import Field
|
|
14
|
+
|
|
15
|
+
from ..adapters.base import SolidWorksAdapter
|
|
16
|
+
from ..utils.feature_tree_classifier import (
|
|
17
|
+
build_component_tree,
|
|
18
|
+
classify_feature_tree_snapshot,
|
|
19
|
+
)
|
|
20
|
+
from .input_compat import CompatInput
|
|
21
|
+
from .tool_boundary import (
|
|
22
|
+
InvalidInputError,
|
|
23
|
+
ToolOperationError,
|
|
24
|
+
UnsupportedOperationError,
|
|
25
|
+
require_capability,
|
|
26
|
+
require_success,
|
|
27
|
+
with_tool_boundary,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Input schemas using Python 3.14 built-in types
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SaveFileInput(CompatInput):
|
|
34
|
+
"""Input schema for saving a file.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
file_path (str | None): The file path value.
|
|
38
|
+
force_save (bool): The force save value.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
force_save: bool = Field(default=True, description="Force save even if no changes")
|
|
42
|
+
file_path: str | None = Field(
|
|
43
|
+
default=None,
|
|
44
|
+
description="Optional output path. If omitted, saves the current active document.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SaveAsInput(CompatInput):
|
|
49
|
+
"""Input schema for save as operation.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
file_path (str): The file path value.
|
|
53
|
+
format_type (str): The format type value.
|
|
54
|
+
overwrite (bool): The overwrite value.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
file_path: str = Field(description="Full path for the new file")
|
|
58
|
+
format_type: str = Field(
|
|
59
|
+
default="solidworks", description="File format (solidworks, step, iges, etc.)"
|
|
60
|
+
)
|
|
61
|
+
overwrite: bool = Field(default=False, description="Overwrite existing file")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FileOperationInput(CompatInput):
|
|
65
|
+
"""Input schema for file operations.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
file_path (str | None): The file path value.
|
|
69
|
+
include_system (bool): The include system value.
|
|
70
|
+
operation (str): The operation value.
|
|
71
|
+
parameters (dict[str, Any] | None): The parameters value.
|
|
72
|
+
properties (dict[str, Any] | None): The properties value.
|
|
73
|
+
source_path (str | None): The source path value.
|
|
74
|
+
target_path (str | None): The target path value.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
operation: str = Field(
|
|
78
|
+
description="Operation to perform (copy, move, delete, rename)"
|
|
79
|
+
)
|
|
80
|
+
source_path: str | None = Field(default=None, description="Source file path")
|
|
81
|
+
file_path: str | None = Field(default=None, description="Alternative file path")
|
|
82
|
+
target_path: str | None = Field(
|
|
83
|
+
default=None, description="Target file path (for copy/move/rename)"
|
|
84
|
+
)
|
|
85
|
+
properties: dict[str, Any] | None = Field(
|
|
86
|
+
default=None, description="File properties to set"
|
|
87
|
+
)
|
|
88
|
+
parameters: dict[str, Any] | None = Field(
|
|
89
|
+
default=None, description="Operation parameters"
|
|
90
|
+
)
|
|
91
|
+
include_system: bool = Field(default=False, description="Include system properties")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FormatConversionInput(CompatInput):
|
|
95
|
+
"""Input schema for format conversion.
|
|
96
|
+
|
|
97
|
+
Attributes:
|
|
98
|
+
conversion_options (dict[str, Any] | None): The conversion options value.
|
|
99
|
+
invalid_format (str | None): The invalid format value.
|
|
100
|
+
output_path (str | None): The output path value.
|
|
101
|
+
quality (str): The quality value.
|
|
102
|
+
source_file (str): The source file value.
|
|
103
|
+
source_format (str | None): The source format value.
|
|
104
|
+
target_file (str | None): The target file value.
|
|
105
|
+
target_format (str): The target format value.
|
|
106
|
+
units (str): The units value.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
source_file: str = Field(description="Source file path")
|
|
110
|
+
target_file: str | None = Field(default=None, description="Target file path")
|
|
111
|
+
source_format: str | None = Field(default=None, description="Source file format")
|
|
112
|
+
target_format: str = Field(description="Target file format")
|
|
113
|
+
output_path: str | None = Field(default=None, description="Alternative output path")
|
|
114
|
+
conversion_options: dict[str, Any] | None = Field(
|
|
115
|
+
default=None, description="Conversion options"
|
|
116
|
+
)
|
|
117
|
+
quality: str = Field(default="high", description="Conversion quality")
|
|
118
|
+
units: str = Field(default="mm", description="Units")
|
|
119
|
+
invalid_format: str | None = Field(
|
|
120
|
+
default=None, description="Invalid format for testing"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class LoadPartInput(CompatInput):
|
|
125
|
+
"""Input schema for loading a part file.
|
|
126
|
+
|
|
127
|
+
Attributes:
|
|
128
|
+
file_path (str): The file path value.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
file_path: str = Field(description="Full path to the .sldprt file")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class LoadAssemblyInput(CompatInput):
|
|
135
|
+
"""Input schema for loading an assembly file.
|
|
136
|
+
|
|
137
|
+
Attributes:
|
|
138
|
+
file_path (str): The file path value.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
file_path: str = Field(description="Full path to the .sldasm file")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class SavePartInput(CompatInput):
|
|
145
|
+
"""Input schema for saving a part file.
|
|
146
|
+
|
|
147
|
+
Attributes:
|
|
148
|
+
file_path (str | None): The file path value.
|
|
149
|
+
overwrite (bool): The overwrite value.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
file_path: str | None = Field(
|
|
153
|
+
default=None,
|
|
154
|
+
description="Optional output path. If omitted, saves the active part to its existing location.",
|
|
155
|
+
)
|
|
156
|
+
overwrite: bool = Field(default=False, description="Overwrite existing file")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SaveAssemblyInput(CompatInput):
|
|
160
|
+
"""Input schema for saving an assembly file.
|
|
161
|
+
|
|
162
|
+
Attributes:
|
|
163
|
+
file_path (str | None): The file path value.
|
|
164
|
+
overwrite (bool): The overwrite value.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
file_path: str | None = Field(
|
|
168
|
+
default=None,
|
|
169
|
+
description="Optional output path. If omitted, saves the active assembly to its existing location.",
|
|
170
|
+
)
|
|
171
|
+
overwrite: bool = Field(default=False, description="Overwrite existing file")
|
|
172
|
+
include_references: bool = Field(
|
|
173
|
+
default=False,
|
|
174
|
+
description="When true and file_path is set, copy referenced assembly files into the target folder.",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class PackAndGoInput(CompatInput):
|
|
179
|
+
"""Input schema for Pack-and-Go assembly copy.
|
|
180
|
+
|
|
181
|
+
Attributes:
|
|
182
|
+
source_path (str): Path to the source assembly file.
|
|
183
|
+
target_dir (str): Destination directory for the self-contained copy.
|
|
184
|
+
export_preview (bool): Whether to export an isometric PNG preview.
|
|
185
|
+
overwrite (bool): Whether to overwrite files already in target_dir.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
source_path: str = Field(description="Absolute path to the source .sldasm file")
|
|
189
|
+
target_dir: str = Field(
|
|
190
|
+
description="Directory where the assembly and all referenced parts will be copied"
|
|
191
|
+
)
|
|
192
|
+
export_preview: bool = Field(
|
|
193
|
+
default=True,
|
|
194
|
+
description="Export an isometric PNG preview next to the copied files",
|
|
195
|
+
)
|
|
196
|
+
overwrite: bool = Field(
|
|
197
|
+
default=False,
|
|
198
|
+
description="Overwrite files that already exist in target_dir",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class ListFeaturesInput(CompatInput):
|
|
203
|
+
"""Input schema for feature tree listing.
|
|
204
|
+
|
|
205
|
+
Attributes:
|
|
206
|
+
include_suppressed (bool): The include suppressed value.
|
|
207
|
+
max_assembly_depth (int): Sub-assembly recursion depth for Assembly
|
|
208
|
+
documents.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
include_suppressed: bool = Field(
|
|
212
|
+
default=False,
|
|
213
|
+
description="Include suppressed features in the returned list",
|
|
214
|
+
)
|
|
215
|
+
max_assembly_depth: int = Field(
|
|
216
|
+
default=2,
|
|
217
|
+
description=(
|
|
218
|
+
"For Assembly documents, how many levels of sub-assembly to "
|
|
219
|
+
"recurse into when flattening component features. Ignored for "
|
|
220
|
+
"Part documents."
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class ClassifyFeatureTreeInput(CompatInput):
|
|
226
|
+
"""Input schema for feature-family classification.
|
|
227
|
+
|
|
228
|
+
Attributes:
|
|
229
|
+
features (list[dict[str, Any]] | None): The features value.
|
|
230
|
+
include_suppressed (bool): The include suppressed value.
|
|
231
|
+
model_info (dict[str, Any] | None): The model info value.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
include_suppressed: bool = Field(
|
|
235
|
+
default=True,
|
|
236
|
+
description="Include suppressed features when reading the active model tree",
|
|
237
|
+
)
|
|
238
|
+
model_info: dict[str, Any] | None = Field(
|
|
239
|
+
default=None,
|
|
240
|
+
description="Optional pre-fetched model info payload to classify without re-reading the active model",
|
|
241
|
+
)
|
|
242
|
+
features: list[dict[str, Any]] | None = Field(
|
|
243
|
+
default=None,
|
|
244
|
+
description="Optional feature-tree payload to classify without re-reading the active model",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class ActivateDocumentInput(CompatInput):
|
|
249
|
+
"""Input schema for activating an already-open document.
|
|
250
|
+
|
|
251
|
+
Attributes:
|
|
252
|
+
title_or_path (str): Window title, full path, or file name of a
|
|
253
|
+
document that is already open in SolidWorks.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
title_or_path: str = Field(
|
|
257
|
+
description=(
|
|
258
|
+
"Window title (e.g. 'bracket.SLDPRT'), full path, or bare file "
|
|
259
|
+
"name of a document that is already open"
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def model_post_init(self, __context: Any) -> None:
|
|
264
|
+
if not self.title_or_path.strip():
|
|
265
|
+
raise ValueError("title_or_path is required")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class SaveBodyAsPartInput(CompatInput):
|
|
269
|
+
"""Input schema for extracting a solid body to its own part file.
|
|
270
|
+
|
|
271
|
+
Attributes:
|
|
272
|
+
body_name (str): Name of a solid body in the active multibody part.
|
|
273
|
+
file_path (str): Absolute path for the new ``.sldprt``.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
body_name: str = Field(
|
|
277
|
+
description="Name of a solid body in the active part, e.g. 'Boss-Extrude1'"
|
|
278
|
+
)
|
|
279
|
+
file_path: str = Field(
|
|
280
|
+
description="Absolute path for the new .sldprt (parent directory must exist)"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def model_post_init(self, __context: Any) -> None:
|
|
284
|
+
if not self.body_name.strip():
|
|
285
|
+
raise ValueError("body_name is required")
|
|
286
|
+
if not self.file_path.strip():
|
|
287
|
+
raise ValueError("file_path is required")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def register_file_management_tools(
|
|
291
|
+
mcp: FastMCP, adapter: SolidWorksAdapter, config: dict[str, Any]
|
|
292
|
+
) -> int:
|
|
293
|
+
"""Register file management tools with FastMCP.
|
|
294
|
+
|
|
295
|
+
Registers essential file operations for SolidWorks document management including save
|
|
296
|
+
operations, file format conversions, and file property access. These tools provide
|
|
297
|
+
fundamental document lifecycle management capabilities.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
mcp (FastMCP): The mcp value.
|
|
301
|
+
adapter (SolidWorksAdapter): Adapter instance used for the operation.
|
|
302
|
+
config (dict[str, Any]): Configuration values for the operation.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
int: The computed numeric result.
|
|
306
|
+
|
|
307
|
+
Example:
|
|
308
|
+
```python
|
|
309
|
+
from solidworks_mcp.tools.file_management import register_file_management_tools
|
|
310
|
+
|
|
311
|
+
tool_count = await register_file_management_tools(mcp, adapter, config)
|
|
312
|
+
print(f"Registered {tool_count} file management tools")
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Note:
|
|
316
|
+
File management tools require an active SolidWorks document.
|
|
317
|
+
Save operations preserve the current document state and metadata.
|
|
318
|
+
"""
|
|
319
|
+
mcp = with_tool_boundary(mcp)
|
|
320
|
+
tool_count = 0
|
|
321
|
+
|
|
322
|
+
def _coerce_input(model_cls: Any, payload: Any) -> Any:
|
|
323
|
+
"""Accept legacy dict payloads from compatibility wrapper as well as model instances.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
model_cls (Any): The model cls value.
|
|
327
|
+
payload (Any): The payload value.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Any: The result produced by the operation.
|
|
331
|
+
"""
|
|
332
|
+
return (
|
|
333
|
+
payload
|
|
334
|
+
if isinstance(payload, model_cls)
|
|
335
|
+
else model_cls.model_validate(payload)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _result_value(payload: Any, *keys: str, default: Any = None) -> Any:
|
|
339
|
+
"""Read a value from adapter result payloads that may be dicts or model objects.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
payload (Any): The payload value.
|
|
343
|
+
*keys (str): Additional positional arguments forwarded to the call.
|
|
344
|
+
default (Any): Fallback value returned when the operation fails. Defaults to None.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Any: The result produced by the operation.
|
|
348
|
+
"""
|
|
349
|
+
if payload is None:
|
|
350
|
+
return default
|
|
351
|
+
|
|
352
|
+
if isinstance(payload, dict):
|
|
353
|
+
for key in keys:
|
|
354
|
+
if key in payload and payload[key] is not None:
|
|
355
|
+
return payload[key]
|
|
356
|
+
return default
|
|
357
|
+
|
|
358
|
+
for key in keys:
|
|
359
|
+
value = getattr(payload, key, None)
|
|
360
|
+
if value is not None:
|
|
361
|
+
return value
|
|
362
|
+
return default
|
|
363
|
+
|
|
364
|
+
def _prepare_save_target(
|
|
365
|
+
raw_path: str,
|
|
366
|
+
*,
|
|
367
|
+
required_extension: str,
|
|
368
|
+
overwrite: bool,
|
|
369
|
+
) -> str:
|
|
370
|
+
"""Normalize and validate a save target before adapter/COM calls.
|
|
371
|
+
|
|
372
|
+
Enforces pre-save guardrails used by save convenience tools so invalid paths are
|
|
373
|
+
rejected consistently in both mock and real adapter modes. The helper performs:
|
|
374
|
+
path trimming, extension normalization, parent-directory existence check,
|
|
375
|
+
directory writability check, and overwrite policy enforcement.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
raw_path (str): User-supplied path where the file should be saved.
|
|
379
|
+
required_extension (str): Required extension for the target document type
|
|
380
|
+
(for example, ``.sldprt`` or ``.sldasm``).
|
|
381
|
+
overwrite (bool): Whether saving is allowed to replace an existing file.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
str: The normalized save path.
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
InvalidInputError: When the path is empty, has the wrong extension, its
|
|
388
|
+
directory is missing or not writable, or it exists and overwrite is
|
|
389
|
+
False.
|
|
390
|
+
|
|
391
|
+
Example:
|
|
392
|
+
```python
|
|
393
|
+
target = _prepare_save_target(
|
|
394
|
+
r"C:/Exports/bracket.step",
|
|
395
|
+
required_extension=".sldprt",
|
|
396
|
+
overwrite=False,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# target == "C:/Exports/bracket.sldprt" when valid; raises
|
|
400
|
+
# InvalidInputError when the directory is missing/not writable,
|
|
401
|
+
# or when the target exists and overwrite is False.
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Note:
|
|
405
|
+
Validation is intentionally executed before adapter calls to avoid opaque COM
|
|
406
|
+
failures and to provide deterministic, user-friendly error messages.
|
|
407
|
+
"""
|
|
408
|
+
file_path = raw_path.strip()
|
|
409
|
+
if not file_path:
|
|
410
|
+
raise InvalidInputError("Invalid file_path: path is empty or whitespace.")
|
|
411
|
+
|
|
412
|
+
cleaned = file_path.strip()
|
|
413
|
+
extension_name = required_extension.lstrip(".")
|
|
414
|
+
if (
|
|
415
|
+
cleaned.count(".") == 1
|
|
416
|
+
and cleaned.startswith(".")
|
|
417
|
+
and cleaned[1:].lower() == extension_name
|
|
418
|
+
):
|
|
419
|
+
# Reject extension-only input such as ".sldprt".
|
|
420
|
+
raise InvalidInputError(
|
|
421
|
+
"Invalid file_path: missing base filename before extension."
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
target = Path(file_path)
|
|
425
|
+
if target.suffix.lower() != required_extension.lower():
|
|
426
|
+
# Normalize extension so save_part/save_assembly always use their canonical type.
|
|
427
|
+
target = target.with_suffix(required_extension)
|
|
428
|
+
|
|
429
|
+
target_dir = target.parent
|
|
430
|
+
if not target_dir.exists():
|
|
431
|
+
raise InvalidInputError(f"Target directory does not exist: {target_dir}")
|
|
432
|
+
|
|
433
|
+
if not target_dir.is_dir():
|
|
434
|
+
raise InvalidInputError(
|
|
435
|
+
f"Target parent path is not a directory: {target_dir}"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if not os.access(target_dir, os.W_OK):
|
|
439
|
+
raise InvalidInputError(f"Target directory is not writable: {target_dir}")
|
|
440
|
+
|
|
441
|
+
if target.exists() and target.is_dir():
|
|
442
|
+
raise InvalidInputError(
|
|
443
|
+
f"Target path is a directory, expected file path: {target}"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
if target.exists() and not overwrite:
|
|
447
|
+
raise InvalidInputError(
|
|
448
|
+
f"File already exists and overwrite=False: {target}. Set overwrite=True to replace it."
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
return str(target)
|
|
452
|
+
|
|
453
|
+
def _get_attr_or_call(obj: Any, name: str, default: Any = None) -> Any:
|
|
454
|
+
"""Read COM values that may be exposed as either properties or methods."""
|
|
455
|
+
candidate = getattr(obj, name, None)
|
|
456
|
+
if candidate is None:
|
|
457
|
+
return default
|
|
458
|
+
if callable(candidate):
|
|
459
|
+
try:
|
|
460
|
+
value = candidate()
|
|
461
|
+
except Exception:
|
|
462
|
+
return default
|
|
463
|
+
return default if value is None else value
|
|
464
|
+
return candidate
|
|
465
|
+
|
|
466
|
+
def _extract_dependency_paths(
|
|
467
|
+
raw_dependencies: Any,
|
|
468
|
+
) -> list[Path]:
|
|
469
|
+
"""Extract model file paths from SolidWorks GetDependencies2 payloads."""
|
|
470
|
+
if not isinstance(raw_dependencies, (list, tuple)):
|
|
471
|
+
return []
|
|
472
|
+
|
|
473
|
+
dependency_paths: list[Path] = []
|
|
474
|
+
for item in raw_dependencies:
|
|
475
|
+
if not isinstance(item, str):
|
|
476
|
+
continue
|
|
477
|
+
lower_item = item.lower()
|
|
478
|
+
if lower_item.endswith((".sldprt", ".sldasm", ".slddrw")):
|
|
479
|
+
dependency_paths.append(Path(item))
|
|
480
|
+
return dependency_paths
|
|
481
|
+
|
|
482
|
+
def _copy_with_collision_handling(source_path: Path, target_dir: Path) -> Path:
|
|
483
|
+
"""Copy a source file into target_dir, suffixing when name collisions occur."""
|
|
484
|
+
destination = target_dir / source_path.name
|
|
485
|
+
try:
|
|
486
|
+
same_target = destination.exists() and source_path.resolve(
|
|
487
|
+
strict=False
|
|
488
|
+
) == destination.resolve(strict=False)
|
|
489
|
+
except Exception:
|
|
490
|
+
same_target = False
|
|
491
|
+
if same_target:
|
|
492
|
+
return destination
|
|
493
|
+
|
|
494
|
+
if not destination.exists():
|
|
495
|
+
shutil.copy2(source_path, destination)
|
|
496
|
+
return destination
|
|
497
|
+
|
|
498
|
+
stem = source_path.stem
|
|
499
|
+
suffix = source_path.suffix
|
|
500
|
+
index = 1
|
|
501
|
+
while True:
|
|
502
|
+
candidate = target_dir / f"{stem}_{index}{suffix}"
|
|
503
|
+
if not candidate.exists():
|
|
504
|
+
shutil.copy2(source_path, candidate)
|
|
505
|
+
return candidate
|
|
506
|
+
index += 1
|
|
507
|
+
|
|
508
|
+
def _copy_active_assembly_with_references(
|
|
509
|
+
target_assembly_path: str,
|
|
510
|
+
) -> dict[str, Any]:
|
|
511
|
+
"""Copy active assembly and resolved dependencies into target folder."""
|
|
512
|
+
current_model = getattr(adapter, "currentModel", None)
|
|
513
|
+
if current_model is None:
|
|
514
|
+
raise ToolOperationError("No active model available to copy references.")
|
|
515
|
+
|
|
516
|
+
source_assembly_raw = _get_attr_or_call(current_model, "GetPathName")
|
|
517
|
+
source_assembly = (
|
|
518
|
+
Path(str(source_assembly_raw)) if source_assembly_raw else None
|
|
519
|
+
)
|
|
520
|
+
if source_assembly is None or not source_assembly.exists():
|
|
521
|
+
raise ToolOperationError(
|
|
522
|
+
"Active assembly must be saved before include_references=True can copy dependencies."
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
target_assembly = Path(target_assembly_path)
|
|
526
|
+
target_dir = target_assembly.parent
|
|
527
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
528
|
+
|
|
529
|
+
dependency_callable = getattr(current_model, "GetDependencies2", None)
|
|
530
|
+
if not callable(dependency_callable):
|
|
531
|
+
raise ToolOperationError(
|
|
532
|
+
"Adapter does not expose GetDependencies2; cannot copy assembly references in this mode."
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
raw_dependencies = dependency_callable(True, True, False)
|
|
537
|
+
except Exception as exc:
|
|
538
|
+
raise ToolOperationError(
|
|
539
|
+
f"Failed to enumerate assembly dependencies: {exc}"
|
|
540
|
+
) from exc
|
|
541
|
+
|
|
542
|
+
dependency_paths = _extract_dependency_paths(raw_dependencies)
|
|
543
|
+
unique_sources: list[Path] = [source_assembly]
|
|
544
|
+
for dep in dependency_paths:
|
|
545
|
+
if dep not in unique_sources:
|
|
546
|
+
unique_sources.append(dep)
|
|
547
|
+
|
|
548
|
+
copied_files: list[str] = []
|
|
549
|
+
missing_sources: list[str] = []
|
|
550
|
+
|
|
551
|
+
for source in unique_sources:
|
|
552
|
+
if not source.exists():
|
|
553
|
+
missing_sources.append(str(source))
|
|
554
|
+
continue
|
|
555
|
+
|
|
556
|
+
if source.resolve(strict=False) == source_assembly.resolve(strict=False):
|
|
557
|
+
shutil.copy2(source, target_assembly)
|
|
558
|
+
copied_files.append(str(target_assembly))
|
|
559
|
+
else:
|
|
560
|
+
copied_target = _copy_with_collision_handling(source, target_dir)
|
|
561
|
+
copied_files.append(str(copied_target))
|
|
562
|
+
|
|
563
|
+
copied_files = sorted(copied_files)
|
|
564
|
+
return {
|
|
565
|
+
"status": "success",
|
|
566
|
+
"message": f"Assembly and references copied to: {target_dir}",
|
|
567
|
+
"file_path": str(target_assembly),
|
|
568
|
+
"copied_file_count": len(copied_files),
|
|
569
|
+
"copied_files": copied_files,
|
|
570
|
+
"missing_source_files": missing_sources,
|
|
571
|
+
"copy_method": "tool_dependency_copy",
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
@mcp.tool()
|
|
575
|
+
async def save_file(input_data: SaveFileInput) -> dict[str, Any]:
|
|
576
|
+
"""Save the current SolidWorks model.
|
|
577
|
+
|
|
578
|
+
Saves the currently active SolidWorks document to its existing file location. Essential
|
|
579
|
+
for preserving work and maintaining document version control. Handles both modified and
|
|
580
|
+
unmodified documents based on force_save setting.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
input_data (SaveFileInput): The input data value.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
587
|
+
|
|
588
|
+
Example:
|
|
589
|
+
```python
|
|
590
|
+
# Force save current model
|
|
591
|
+
result = await save_file({"force_save": True})
|
|
592
|
+
|
|
593
|
+
# Save only if modified
|
|
594
|
+
result = await save_file({"force_save": False})
|
|
595
|
+
|
|
596
|
+
if result["status"] == "success":
|
|
597
|
+
print(f"File saved at {result['timestamp']}")
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Note:
|
|
601
|
+
- Requires an open SolidWorks document
|
|
602
|
+
- Preserves original file location and format
|
|
603
|
+
- Updates document timestamp and metadata
|
|
604
|
+
- No effect if document is read-only
|
|
605
|
+
"""
|
|
606
|
+
input_data = _coerce_input(SaveFileInput, input_data)
|
|
607
|
+
require_capability(
|
|
608
|
+
adapter,
|
|
609
|
+
"save_file",
|
|
610
|
+
"Active adapter does not support save_file; the file was not saved.",
|
|
611
|
+
)
|
|
612
|
+
result = await adapter.save_file(input_data.file_path)
|
|
613
|
+
require_success(result, "Failed to save file")
|
|
614
|
+
return {
|
|
615
|
+
"status": "success",
|
|
616
|
+
"message": "File saved successfully",
|
|
617
|
+
"data": result.data,
|
|
618
|
+
"execution_time": result.execution_time,
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
@mcp.tool()
|
|
622
|
+
async def save_as(input_data: SaveAsInput) -> dict[str, Any]:
|
|
623
|
+
"""Save the current model to a new location or format.
|
|
624
|
+
|
|
625
|
+
Saves the currently active SolidWorks document with a new filename, location, or file
|
|
626
|
+
format. Supports multiple export formats for interoperability with other CAD systems and
|
|
627
|
+
manufacturing workflows.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
input_data (SaveAsInput): The input data value.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
634
|
+
|
|
635
|
+
Example:
|
|
636
|
+
```python
|
|
637
|
+
# Save as new SolidWorks file
|
|
638
|
+
result = await save_as({
|
|
639
|
+
"file_path": "C:/Projects/bracket_v2.sldprt",
|
|
640
|
+
"format_type": "solidworks",
|
|
641
|
+
"overwrite": False
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
# Export to STEP format
|
|
645
|
+
result = await save_as({
|
|
646
|
+
"file_path": "C:/Exports/bracket.step",
|
|
647
|
+
"format_type": "step",
|
|
648
|
+
"overwrite": True
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
# Export for 3D printing
|
|
652
|
+
result = await save_as({
|
|
653
|
+
"file_path": "C:/3DPrint/bracket.stl",
|
|
654
|
+
"format_type": "stl"
|
|
655
|
+
})
|
|
656
|
+
```
|
|
657
|
+
"""
|
|
658
|
+
input_data = _coerce_input(SaveAsInput, input_data)
|
|
659
|
+
if hasattr(adapter, "save_file") and input_data.format_type.lower() in {
|
|
660
|
+
"solidworks",
|
|
661
|
+
"sldprt",
|
|
662
|
+
"sldasm",
|
|
663
|
+
"slddrw",
|
|
664
|
+
}:
|
|
665
|
+
result = await adapter.save_file(input_data.file_path)
|
|
666
|
+
require_success(result, "Failed to save file")
|
|
667
|
+
return {
|
|
668
|
+
"status": "success",
|
|
669
|
+
"message": f"File saved as: {input_data.file_path}",
|
|
670
|
+
"file_path": input_data.file_path,
|
|
671
|
+
"format": input_data.format_type,
|
|
672
|
+
"execution_time": result.execution_time,
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
require_capability(
|
|
676
|
+
adapter,
|
|
677
|
+
"export_file",
|
|
678
|
+
"Active adapter does not support save_file or export_file; "
|
|
679
|
+
f"no file was saved as {input_data.file_path}.",
|
|
680
|
+
)
|
|
681
|
+
result = await adapter.export_file(
|
|
682
|
+
input_data.file_path,
|
|
683
|
+
input_data.format_type,
|
|
684
|
+
)
|
|
685
|
+
require_success(result, "Failed to export file")
|
|
686
|
+
return {
|
|
687
|
+
"status": "success",
|
|
688
|
+
"message": f"File exported as: {input_data.file_path}",
|
|
689
|
+
"file_path": input_data.file_path,
|
|
690
|
+
"format": input_data.format_type,
|
|
691
|
+
"execution_time": result.execution_time,
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
@mcp.tool()
|
|
695
|
+
async def get_file_properties() -> dict[str, Any]:
|
|
696
|
+
"""Get properties of the current SolidWorks file.
|
|
697
|
+
|
|
698
|
+
Retrieves comprehensive metadata and properties of the currently active SolidWorks
|
|
699
|
+
document. Provides essential file information for document management, version control,
|
|
700
|
+
and project organization.
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
704
|
+
|
|
705
|
+
Example:
|
|
706
|
+
```python
|
|
707
|
+
result = await get_file_properties()
|
|
708
|
+
|
|
709
|
+
if result["status"] == "success":
|
|
710
|
+
props = result["properties"]
|
|
711
|
+
|
|
712
|
+
# Basic file info
|
|
713
|
+
file_info = props["file_info"]
|
|
714
|
+
print(f"File: {file_info['file_name']}")
|
|
715
|
+
print(f"Size: {file_info['file_size']}")
|
|
716
|
+
print(f"Type: {file_info['file_type']}")
|
|
717
|
+
|
|
718
|
+
# Technical properties
|
|
719
|
+
tech = props["technical_properties"]
|
|
720
|
+
print(f"Material: {tech['material']}")
|
|
721
|
+
print(f"Units: {tech['units']}")
|
|
722
|
+
|
|
723
|
+
# Document info
|
|
724
|
+
doc = props["document_info"]
|
|
725
|
+
print(f"Author: {doc['author']}")
|
|
726
|
+
print(f"Description: {doc['description']}")
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
Note:
|
|
730
|
+
- Requires an active SolidWorks document
|
|
731
|
+
- Properties may vary based on document type
|
|
732
|
+
- Some properties may be empty if not set
|
|
733
|
+
- Technical properties depend on document configuration
|
|
734
|
+
"""
|
|
735
|
+
require_capability(
|
|
736
|
+
adapter, "get_model_info", "Active adapter does not support model metadata"
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
result = await adapter.get_model_info()
|
|
740
|
+
if not result.is_success or not isinstance(result.data, dict):
|
|
741
|
+
raise ToolOperationError(result.error or "No active SolidWorks document")
|
|
742
|
+
|
|
743
|
+
model_info = dict(result.data)
|
|
744
|
+
raw_path = str(model_info.get("path") or "")
|
|
745
|
+
file_path = Path(raw_path) if raw_path else None
|
|
746
|
+
properties: dict[str, Any] = {
|
|
747
|
+
"file_name": (
|
|
748
|
+
file_path.name
|
|
749
|
+
if file_path is not None
|
|
750
|
+
else str(model_info.get("title") or "")
|
|
751
|
+
),
|
|
752
|
+
"file_path": raw_path,
|
|
753
|
+
"file_size_bytes": None,
|
|
754
|
+
"created_date": None,
|
|
755
|
+
"modified_date": None,
|
|
756
|
+
"document_type": model_info.get("type"),
|
|
757
|
+
"configuration": model_info.get("configuration"),
|
|
758
|
+
"is_dirty": model_info.get("is_dirty"),
|
|
759
|
+
"feature_count": model_info.get("feature_count"),
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if file_path is not None and file_path.is_file():
|
|
763
|
+
stat = file_path.stat()
|
|
764
|
+
properties.update(
|
|
765
|
+
{
|
|
766
|
+
"file_size_bytes": stat.st_size,
|
|
767
|
+
"created_date": stat.st_ctime,
|
|
768
|
+
"modified_date": stat.st_mtime,
|
|
769
|
+
}
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
"status": "success",
|
|
774
|
+
"properties": properties,
|
|
775
|
+
"execution_time": result.execution_time,
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
@mcp.tool()
|
|
779
|
+
async def get_model_info() -> dict[str, Any]:
|
|
780
|
+
"""Get metadata for the active SolidWorks document.
|
|
781
|
+
|
|
782
|
+
Returns a compact summary of the current model context that is useful for read-before-
|
|
783
|
+
write LLM flows (document type, active configuration, and feature count).
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
787
|
+
"""
|
|
788
|
+
require_capability(
|
|
789
|
+
adapter, "get_model_info", "Active adapter does not support get_model_info"
|
|
790
|
+
)
|
|
791
|
+
result = await adapter.get_model_info()
|
|
792
|
+
require_success(result, "Failed to get model info")
|
|
793
|
+
return {
|
|
794
|
+
"status": "success",
|
|
795
|
+
"model_info": result.data,
|
|
796
|
+
"execution_time": result.execution_time,
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
@mcp.tool()
|
|
800
|
+
async def list_features(input_data: ListFeaturesInput) -> dict[str, Any]:
|
|
801
|
+
"""List feature-tree entries for the active SolidWorks document.
|
|
802
|
+
|
|
803
|
+
Useful for read-before-write workflows where the agent must inspect existing model
|
|
804
|
+
structure before adding or editing downstream features.
|
|
805
|
+
|
|
806
|
+
Args:
|
|
807
|
+
input_data (ListFeaturesInput): The input data value.
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
811
|
+
"""
|
|
812
|
+
input_data = _coerce_input(ListFeaturesInput, input_data)
|
|
813
|
+
require_capability(
|
|
814
|
+
adapter, "list_features", "Active adapter does not support list_features"
|
|
815
|
+
)
|
|
816
|
+
result = await adapter.list_features(
|
|
817
|
+
include_suppressed=input_data.include_suppressed,
|
|
818
|
+
max_assembly_depth=input_data.max_assembly_depth,
|
|
819
|
+
)
|
|
820
|
+
require_success(result, "Failed to list features")
|
|
821
|
+
return {
|
|
822
|
+
"status": "success",
|
|
823
|
+
"features": result.data or [],
|
|
824
|
+
"count": len(result.data or []),
|
|
825
|
+
# Additive, derived view of the same flat "features"
|
|
826
|
+
# list above - grouped by component/sub-component
|
|
827
|
+
# for callers that want the assembly structure
|
|
828
|
+
# rather than a flat tag-and-scan. Does not change
|
|
829
|
+
# the meaning of "features"/"count".
|
|
830
|
+
"assembly_tree": build_component_tree(result.data),
|
|
831
|
+
"execution_time": result.execution_time,
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
@mcp.tool()
|
|
835
|
+
async def get_selected_entities() -> dict[str, Any]:
|
|
836
|
+
"""Report the faces/edges/vertices currently selected in the live SolidWorks UI.
|
|
837
|
+
|
|
838
|
+
Use this to find out exactly what the user has clicked on in SolidWorks
|
|
839
|
+
before applying a feature (e.g. "chamfer the face I selected") instead
|
|
840
|
+
of guessing from coordinates or a screenshot. Each entry includes
|
|
841
|
+
enough geometry (bounding box, center, area/length, normal where
|
|
842
|
+
applicable) to identify which face/edge is which, and the center
|
|
843
|
+
coordinates can be fed straight into add_chamfer/add_fillet's
|
|
844
|
+
"face:x,y,z" or coordinate edge_names syntax.
|
|
845
|
+
|
|
846
|
+
Returns:
|
|
847
|
+
dict[str, Any]: ``entities`` (list of selection descriptors) and
|
|
848
|
+
``count`` on success.
|
|
849
|
+
"""
|
|
850
|
+
require_capability(
|
|
851
|
+
adapter,
|
|
852
|
+
"get_selected_entities",
|
|
853
|
+
"Active adapter does not support get_selected_entities",
|
|
854
|
+
)
|
|
855
|
+
result = await adapter.get_selected_entities()
|
|
856
|
+
require_success(result, "Failed to get selected entities")
|
|
857
|
+
return {
|
|
858
|
+
"status": "success",
|
|
859
|
+
"entities": result.data or [],
|
|
860
|
+
"count": len(result.data or []),
|
|
861
|
+
"execution_time": result.execution_time,
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
@mcp.tool()
|
|
865
|
+
async def classify_feature_tree(
|
|
866
|
+
input_data: ClassifyFeatureTreeInput,
|
|
867
|
+
) -> dict[str, Any]:
|
|
868
|
+
"""Classify the active model into a feature family from model-info and tree data.
|
|
869
|
+
|
|
870
|
+
This is a read-before-write helper for delegation. It summarizes whether the current
|
|
871
|
+
document looks like a direct-MCP solid, sheet metal workflow, advanced VBA-backed part,
|
|
872
|
+
assembly, drawing, or an insufficient-evidence case.
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
input_data (ClassifyFeatureTreeInput): The input data value.
|
|
876
|
+
|
|
877
|
+
Returns:
|
|
878
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
879
|
+
"""
|
|
880
|
+
input_data = _coerce_input(ClassifyFeatureTreeInput, input_data)
|
|
881
|
+
model_info = input_data.model_info
|
|
882
|
+
features = input_data.features
|
|
883
|
+
execution_time = 0.0
|
|
884
|
+
|
|
885
|
+
if model_info is None and hasattr(adapter, "get_model_info"):
|
|
886
|
+
model_result = await adapter.get_model_info()
|
|
887
|
+
if model_result.is_success:
|
|
888
|
+
model_info = model_result.data
|
|
889
|
+
execution_time = max(execution_time, model_result.execution_time or 0.0)
|
|
890
|
+
|
|
891
|
+
if features is None:
|
|
892
|
+
if hasattr(adapter, "list_features"):
|
|
893
|
+
feature_result = await adapter.list_features(
|
|
894
|
+
include_suppressed=input_data.include_suppressed
|
|
895
|
+
)
|
|
896
|
+
if feature_result.is_success:
|
|
897
|
+
features = feature_result.data or []
|
|
898
|
+
execution_time = max(
|
|
899
|
+
execution_time, feature_result.execution_time or 0.0
|
|
900
|
+
)
|
|
901
|
+
else:
|
|
902
|
+
raise ToolOperationError(
|
|
903
|
+
feature_result.error
|
|
904
|
+
or "Failed to list features for classification"
|
|
905
|
+
)
|
|
906
|
+
else:
|
|
907
|
+
raise UnsupportedOperationError(
|
|
908
|
+
"Active adapter does not support list_features"
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
classification = classify_feature_tree_snapshot(model_info, features or []) # type: ignore[arg-type]
|
|
912
|
+
return {
|
|
913
|
+
"status": "success",
|
|
914
|
+
"classification": classification,
|
|
915
|
+
"model_info": model_info or {},
|
|
916
|
+
"features": features or [],
|
|
917
|
+
"execution_time": execution_time,
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
@mcp.tool()
|
|
921
|
+
async def list_configurations() -> dict[str, Any]:
|
|
922
|
+
"""List configuration names for the active SolidWorks document.
|
|
923
|
+
|
|
924
|
+
Returns all available configuration names so callers can select a stable target before
|
|
925
|
+
invoking feature or export operations.
|
|
926
|
+
|
|
927
|
+
Returns:
|
|
928
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
929
|
+
"""
|
|
930
|
+
require_capability(
|
|
931
|
+
adapter,
|
|
932
|
+
"list_configurations",
|
|
933
|
+
"Active adapter does not support list_configurations",
|
|
934
|
+
)
|
|
935
|
+
result = await adapter.list_configurations()
|
|
936
|
+
require_success(result, "Failed to list configurations")
|
|
937
|
+
return {
|
|
938
|
+
"status": "success",
|
|
939
|
+
"configurations": result.data or [],
|
|
940
|
+
"count": len(result.data or []),
|
|
941
|
+
"execution_time": result.execution_time,
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
@mcp.tool()
|
|
945
|
+
async def manage_file_properties(input_data: FileOperationInput) -> dict[str, Any]:
|
|
946
|
+
"""Read, update, copy, move, rename, or delete file-related properties.
|
|
947
|
+
|
|
948
|
+
Uses the requested operation and file paths to manage SolidWorks file metadata or
|
|
949
|
+
related file lifecycle tasks through the active adapter.
|
|
950
|
+
|
|
951
|
+
Args:
|
|
952
|
+
input_data (FileOperationInput): The input data value.
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
956
|
+
"""
|
|
957
|
+
input_data = _coerce_input(FileOperationInput, input_data)
|
|
958
|
+
require_capability(
|
|
959
|
+
adapter,
|
|
960
|
+
"manage_file_properties",
|
|
961
|
+
"Active adapter does not support manage_file_properties; "
|
|
962
|
+
f"no {input_data.operation} operation was performed.",
|
|
963
|
+
)
|
|
964
|
+
result = await adapter.manage_file_properties(input_data.model_dump())
|
|
965
|
+
require_success(result, "Failed to manage file properties")
|
|
966
|
+
return {
|
|
967
|
+
"status": "success",
|
|
968
|
+
"message": "File properties managed successfully",
|
|
969
|
+
"data": result.data,
|
|
970
|
+
"execution_time": result.execution_time,
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
@mcp.tool()
|
|
974
|
+
async def convert_file_format(input_data: FormatConversionInput) -> dict[str, Any]:
|
|
975
|
+
"""Convert a SolidWorks file from one format to another.
|
|
976
|
+
|
|
977
|
+
Supports exporting source files to target formats such as STEP, IGES, STL, PDF, or other
|
|
978
|
+
adapter-supported conversion outputs.
|
|
979
|
+
|
|
980
|
+
Args:
|
|
981
|
+
input_data (FormatConversionInput): The input data value.
|
|
982
|
+
|
|
983
|
+
Returns:
|
|
984
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
985
|
+
"""
|
|
986
|
+
input_data = _coerce_input(FormatConversionInput, input_data)
|
|
987
|
+
require_capability(
|
|
988
|
+
adapter,
|
|
989
|
+
"convert_file_format",
|
|
990
|
+
"Active adapter does not support convert_file_format; "
|
|
991
|
+
f"{input_data.source_file} was not converted.",
|
|
992
|
+
)
|
|
993
|
+
result = await adapter.convert_file_format(input_data.model_dump())
|
|
994
|
+
require_success(result, "Failed to convert file format")
|
|
995
|
+
return {
|
|
996
|
+
"status": "success",
|
|
997
|
+
"message": "File converted successfully",
|
|
998
|
+
"data": result.data,
|
|
999
|
+
"execution_time": result.execution_time,
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
@mcp.tool()
|
|
1003
|
+
async def batch_file_operations(input_data: FileOperationInput) -> dict[str, Any]:
|
|
1004
|
+
"""Run a file operation across multiple files as a batch workflow.
|
|
1005
|
+
|
|
1006
|
+
Intended for repetitive file management tasks such as copying, moving, renaming, or
|
|
1007
|
+
deleting groups of SolidWorks documents.
|
|
1008
|
+
|
|
1009
|
+
Args:
|
|
1010
|
+
input_data (FileOperationInput): The input data value.
|
|
1011
|
+
|
|
1012
|
+
Returns:
|
|
1013
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
1014
|
+
"""
|
|
1015
|
+
input_data = _coerce_input(FileOperationInput, input_data)
|
|
1016
|
+
require_capability(
|
|
1017
|
+
adapter,
|
|
1018
|
+
"batch_file_operations",
|
|
1019
|
+
"Active adapter does not support batch_file_operations; "
|
|
1020
|
+
f"no {input_data.operation} batch operation was performed.",
|
|
1021
|
+
)
|
|
1022
|
+
result = await adapter.batch_file_operations(input_data.model_dump())
|
|
1023
|
+
require_success(result, "Failed to run batch file operations")
|
|
1024
|
+
return {
|
|
1025
|
+
"status": "success",
|
|
1026
|
+
"message": "Batch file operations completed successfully",
|
|
1027
|
+
"data": result.data,
|
|
1028
|
+
"execution_time": result.execution_time,
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
@mcp.tool()
|
|
1032
|
+
async def load_part(input_data: LoadPartInput) -> dict[str, Any]:
|
|
1033
|
+
"""Load (open) a SolidWorks part file.
|
|
1034
|
+
|
|
1035
|
+
Convenience wrapper that opens a .sldprt file and makes it the active document. Provides
|
|
1036
|
+
a simpler alternative to open_model for parts.
|
|
1037
|
+
|
|
1038
|
+
Args:
|
|
1039
|
+
input_data (LoadPartInput): The input data value.
|
|
1040
|
+
|
|
1041
|
+
Returns:
|
|
1042
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
1043
|
+
|
|
1044
|
+
Example:
|
|
1045
|
+
```python
|
|
1046
|
+
result = await load_part({
|
|
1047
|
+
"file_path": "C:/Projects/bracket.sldprt"
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
if result["status"] == "success":
|
|
1051
|
+
print(f"Loaded: {result['model']['name']}")
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
Note:
|
|
1055
|
+
- File must be a valid .sldprt (part) file
|
|
1056
|
+
- Path must be absolute and accessible
|
|
1057
|
+
"""
|
|
1058
|
+
input_data = _coerce_input(LoadPartInput, input_data)
|
|
1059
|
+
# Ensure file path ends with .sldprt
|
|
1060
|
+
file_path = input_data.file_path
|
|
1061
|
+
if not file_path.lower().endswith(".sldprt"):
|
|
1062
|
+
raise InvalidInputError(f"File must be a .sldprt part file: {file_path}")
|
|
1063
|
+
|
|
1064
|
+
result = await adapter.open_model(file_path)
|
|
1065
|
+
require_success(result, context="Failed to load part")
|
|
1066
|
+
model = result.data
|
|
1067
|
+
title = _result_value(model, "title", "name", default=file_path)
|
|
1068
|
+
path = _result_value(model, "path", "file_path", default=file_path)
|
|
1069
|
+
configuration = _result_value(model, "configuration", default="Default")
|
|
1070
|
+
return {
|
|
1071
|
+
"status": "success",
|
|
1072
|
+
"message": f"Loaded part: {title}",
|
|
1073
|
+
"model": {
|
|
1074
|
+
"name": title,
|
|
1075
|
+
"type": "Part",
|
|
1076
|
+
"path": path,
|
|
1077
|
+
"configuration": configuration,
|
|
1078
|
+
},
|
|
1079
|
+
"execution_time": result.execution_time,
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
@mcp.tool()
|
|
1083
|
+
async def load_assembly(input_data: LoadAssemblyInput) -> dict[str, Any]:
|
|
1084
|
+
"""Load (open) a SolidWorks assembly file.
|
|
1085
|
+
|
|
1086
|
+
Convenience wrapper that opens a .sldasm file and makes it the active document. Provides
|
|
1087
|
+
a simpler alternative to open_model for assemblies.
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
input_data (LoadAssemblyInput): The input data value.
|
|
1091
|
+
|
|
1092
|
+
Returns:
|
|
1093
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
1094
|
+
|
|
1095
|
+
Example:
|
|
1096
|
+
```python
|
|
1097
|
+
result = await load_assembly({
|
|
1098
|
+
"file_path": "C:/Projects/machine_assembly.sldasm"
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
if result["status"] == "success":
|
|
1102
|
+
print(f"Loaded: {result['model']['name']}")
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
Note:
|
|
1106
|
+
- File must be a valid .sldasm (assembly) file
|
|
1107
|
+
- Path must be absolute and accessible
|
|
1108
|
+
"""
|
|
1109
|
+
input_data = _coerce_input(LoadAssemblyInput, input_data)
|
|
1110
|
+
# Ensure file path ends with .sldasm
|
|
1111
|
+
file_path = input_data.file_path
|
|
1112
|
+
if not file_path.lower().endswith(".sldasm"):
|
|
1113
|
+
raise InvalidInputError(
|
|
1114
|
+
f"File must be a .sldasm assembly file: {file_path}"
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
result = await adapter.open_model(file_path)
|
|
1118
|
+
require_success(result, context="Failed to load assembly")
|
|
1119
|
+
model = result.data
|
|
1120
|
+
title = _result_value(model, "title", "name", default=file_path)
|
|
1121
|
+
path = _result_value(model, "path", "file_path", default=file_path)
|
|
1122
|
+
configuration = _result_value(model, "configuration", default="Default")
|
|
1123
|
+
return {
|
|
1124
|
+
"status": "success",
|
|
1125
|
+
"message": f"Loaded assembly: {title}",
|
|
1126
|
+
"model": {
|
|
1127
|
+
"name": title,
|
|
1128
|
+
"type": "Assembly",
|
|
1129
|
+
"path": path,
|
|
1130
|
+
"configuration": configuration,
|
|
1131
|
+
},
|
|
1132
|
+
"execution_time": result.execution_time,
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
@mcp.tool()
|
|
1136
|
+
async def save_part(input_data: SavePartInput | None = None) -> dict[str, Any]:
|
|
1137
|
+
"""Save the active SolidWorks part document.
|
|
1138
|
+
|
|
1139
|
+
Convenience wrapper that saves the currently active part. If no file_path is provided,
|
|
1140
|
+
saves to the existing location. Otherwise, saves as a new file.
|
|
1141
|
+
|
|
1142
|
+
Args:
|
|
1143
|
+
input_data (SavePartInput | None): The input data value. Defaults to None.
|
|
1144
|
+
|
|
1145
|
+
Returns:
|
|
1146
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
1147
|
+
|
|
1148
|
+
Example:
|
|
1149
|
+
```python
|
|
1150
|
+
# Save to current location
|
|
1151
|
+
result = await save_part()
|
|
1152
|
+
|
|
1153
|
+
# Save as new file
|
|
1154
|
+
result = await save_part({
|
|
1155
|
+
"file_path": "C:/Projects/bracket_v2.sldprt",
|
|
1156
|
+
"overwrite": False
|
|
1157
|
+
})
|
|
1158
|
+
|
|
1159
|
+
if result["status"] == "success":
|
|
1160
|
+
print(f"Part saved to {result['file_path']}")
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
Note:
|
|
1164
|
+
- Active document must be a part file
|
|
1165
|
+
- When saving to new location, ensure path ends with .sldprt
|
|
1166
|
+
"""
|
|
1167
|
+
if input_data is None:
|
|
1168
|
+
input_data = SavePartInput()
|
|
1169
|
+
else:
|
|
1170
|
+
input_data = _coerce_input(SavePartInput, input_data)
|
|
1171
|
+
|
|
1172
|
+
# If file_path provided, use save_as; otherwise use regular save
|
|
1173
|
+
if input_data.file_path:
|
|
1174
|
+
file_path = _prepare_save_target(
|
|
1175
|
+
input_data.file_path,
|
|
1176
|
+
required_extension=".sldprt",
|
|
1177
|
+
overwrite=input_data.overwrite,
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
result = await adapter.save_file(file_path)
|
|
1181
|
+
require_success(result, context="Failed to save part")
|
|
1182
|
+
return {
|
|
1183
|
+
"status": "success",
|
|
1184
|
+
"message": f"Part saved as: {file_path}",
|
|
1185
|
+
"file_path": file_path,
|
|
1186
|
+
"execution_time": result.execution_time,
|
|
1187
|
+
}
|
|
1188
|
+
else:
|
|
1189
|
+
# Save to current location
|
|
1190
|
+
result = await adapter.save_file(None)
|
|
1191
|
+
require_success(result, context="Failed to save part")
|
|
1192
|
+
return {
|
|
1193
|
+
"status": "success",
|
|
1194
|
+
"message": "Part saved successfully",
|
|
1195
|
+
"execution_time": result.execution_time,
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
@mcp.tool()
|
|
1199
|
+
async def save_assembly(
|
|
1200
|
+
input_data: SaveAssemblyInput | None = None,
|
|
1201
|
+
) -> dict[str, Any]:
|
|
1202
|
+
"""Save the active SolidWorks assembly document.
|
|
1203
|
+
|
|
1204
|
+
Convenience wrapper that saves the currently active assembly. If no file_path is
|
|
1205
|
+
provided, saves to the existing location. Otherwise, saves as a new file.
|
|
1206
|
+
|
|
1207
|
+
Args:
|
|
1208
|
+
input_data (SaveAssemblyInput | None): The input data value. Defaults to None.
|
|
1209
|
+
|
|
1210
|
+
Returns:
|
|
1211
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
1212
|
+
|
|
1213
|
+
Example:
|
|
1214
|
+
```python
|
|
1215
|
+
# Save to current location
|
|
1216
|
+
result = await save_assembly()
|
|
1217
|
+
|
|
1218
|
+
# Save as new file
|
|
1219
|
+
result = await save_assembly({
|
|
1220
|
+
"file_path": "C:/Projects/machine_v2.sldasm",
|
|
1221
|
+
"overwrite": False
|
|
1222
|
+
})
|
|
1223
|
+
|
|
1224
|
+
if result["status"] == "success":
|
|
1225
|
+
print(f"Assembly saved to {result['file_path']}")
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
Note:
|
|
1229
|
+
- Active document must be an assembly file
|
|
1230
|
+
- When saving to new location, ensure path ends with .sldasm
|
|
1231
|
+
"""
|
|
1232
|
+
if input_data is None:
|
|
1233
|
+
input_data = SaveAssemblyInput()
|
|
1234
|
+
else:
|
|
1235
|
+
input_data = _coerce_input(SaveAssemblyInput, input_data)
|
|
1236
|
+
|
|
1237
|
+
# If file_path provided, use save_as; otherwise use regular save
|
|
1238
|
+
if input_data.file_path:
|
|
1239
|
+
file_path = _prepare_save_target(
|
|
1240
|
+
input_data.file_path,
|
|
1241
|
+
required_extension=".sldasm",
|
|
1242
|
+
overwrite=input_data.overwrite,
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
if input_data.include_references:
|
|
1246
|
+
return _copy_active_assembly_with_references(file_path)
|
|
1247
|
+
|
|
1248
|
+
result = await adapter.save_file(file_path)
|
|
1249
|
+
require_success(result, context="Failed to save assembly")
|
|
1250
|
+
return {
|
|
1251
|
+
"status": "success",
|
|
1252
|
+
"message": f"Assembly saved as: {file_path}",
|
|
1253
|
+
"file_path": file_path,
|
|
1254
|
+
"execution_time": result.execution_time,
|
|
1255
|
+
}
|
|
1256
|
+
else:
|
|
1257
|
+
# Save to current location
|
|
1258
|
+
result = await adapter.save_file(None)
|
|
1259
|
+
require_success(result, context="Failed to save assembly")
|
|
1260
|
+
return {
|
|
1261
|
+
"status": "success",
|
|
1262
|
+
"message": "Assembly saved successfully",
|
|
1263
|
+
"execution_time": result.execution_time,
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
@mcp.tool()
|
|
1267
|
+
async def pack_and_go_assembly(
|
|
1268
|
+
input_data: PackAndGoInput | None = None,
|
|
1269
|
+
) -> dict[str, Any]:
|
|
1270
|
+
"""Copy a SolidWorks assembly and all its referenced parts to a self-contained folder.
|
|
1271
|
+
|
|
1272
|
+
Enumerates every component referenced by the active assembly, copies the
|
|
1273
|
+
assembly and each part into ``target_dir``, then rewrites the stored
|
|
1274
|
+
component paths inside the copied assembly so it opens without any
|
|
1275
|
+
dependency on the original file locations — equivalent to the SolidWorks
|
|
1276
|
+
GUI Pack-and-Go operation.
|
|
1277
|
+
|
|
1278
|
+
Args:
|
|
1279
|
+
input_data (PackAndGoInput | None): The input data value. Defaults to None.
|
|
1280
|
+
|
|
1281
|
+
Returns:
|
|
1282
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
1283
|
+
|
|
1284
|
+
Example:
|
|
1285
|
+
```python
|
|
1286
|
+
result = await pack_and_go_assembly({
|
|
1287
|
+
"source_path": "C:/Projects/robot_arm.sldasm",
|
|
1288
|
+
"target_dir": "C:/Exports/robot_arm_pkg",
|
|
1289
|
+
"export_preview": True
|
|
1290
|
+
})
|
|
1291
|
+
|
|
1292
|
+
if result["status"] == "success":
|
|
1293
|
+
print(result["copied_files"])
|
|
1294
|
+
print(result["all_files_saved"])
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
Note:
|
|
1298
|
+
- SolidWorks must be open with the assembly loaded or openable
|
|
1299
|
+
- target_dir is created if it does not exist
|
|
1300
|
+
- Use overwrite=True to replace files in an existing target_dir
|
|
1301
|
+
"""
|
|
1302
|
+
if input_data is None:
|
|
1303
|
+
input_data = PackAndGoInput(source_path="", target_dir="")
|
|
1304
|
+
else:
|
|
1305
|
+
input_data = _coerce_input(PackAndGoInput, input_data)
|
|
1306
|
+
|
|
1307
|
+
from pathlib import Path as _Path
|
|
1308
|
+
|
|
1309
|
+
source = _Path(input_data.source_path)
|
|
1310
|
+
out_dir = _Path(input_data.target_dir)
|
|
1311
|
+
|
|
1312
|
+
if not source.exists():
|
|
1313
|
+
raise ToolOperationError(f"Source assembly not found: {source}")
|
|
1314
|
+
|
|
1315
|
+
if not source.suffix.lower() == ".sldasm":
|
|
1316
|
+
raise InvalidInputError(
|
|
1317
|
+
f"source_path must be a .sldasm file, got: {source.suffix}"
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
if not input_data.overwrite and out_dir.exists() and any(out_dir.iterdir()):
|
|
1321
|
+
raise InvalidInputError(
|
|
1322
|
+
f"target_dir already contains files: {out_dir}. "
|
|
1323
|
+
"Set overwrite=True to replace them."
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
result = await adapter.pack_and_go_assembly( # type: ignore[attr-defined]
|
|
1327
|
+
source_path=str(source),
|
|
1328
|
+
target_dir=str(out_dir),
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
require_success(result)
|
|
1332
|
+
|
|
1333
|
+
report: dict[str, Any] = {
|
|
1334
|
+
"status": "success",
|
|
1335
|
+
"method": "native_pack_and_go",
|
|
1336
|
+
"execution_time": result.execution_time,
|
|
1337
|
+
**result.data,
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if input_data.export_preview:
|
|
1341
|
+
preview_path = out_dir / "assembly_preview.png"
|
|
1342
|
+
img_result = await adapter.export_image(
|
|
1343
|
+
{
|
|
1344
|
+
"file_path": str(preview_path),
|
|
1345
|
+
"format_type": "png",
|
|
1346
|
+
"width": 1600,
|
|
1347
|
+
"height": 1000,
|
|
1348
|
+
"view_orientation": "isometric",
|
|
1349
|
+
}
|
|
1350
|
+
)
|
|
1351
|
+
report["preview_image"] = (
|
|
1352
|
+
str(preview_path) if img_result.is_success else None
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
return report
|
|
1356
|
+
|
|
1357
|
+
@mcp.tool()
|
|
1358
|
+
async def list_open_documents() -> dict[str, Any]:
|
|
1359
|
+
"""List every SolidWorks document that is currently open.
|
|
1360
|
+
|
|
1361
|
+
Read-only — does not change which document is active. Use it to
|
|
1362
|
+
discover what is open (an assembly plus its parts, a drawing plus its
|
|
1363
|
+
model) before switching with ``activate_document``.
|
|
1364
|
+
|
|
1365
|
+
Returns:
|
|
1366
|
+
dict[str, Any]: ``status`` and ``documents`` — a list of
|
|
1367
|
+
``{title, path, type, is_active}`` entries, plus ``count``.
|
|
1368
|
+
|
|
1369
|
+
Example:
|
|
1370
|
+
```python
|
|
1371
|
+
await list_open_documents()
|
|
1372
|
+
```
|
|
1373
|
+
"""
|
|
1374
|
+
result = await adapter.list_open_documents()
|
|
1375
|
+
require_success(result, context="Failed to list open documents")
|
|
1376
|
+
docs = result.data if isinstance(result.data, list) else []
|
|
1377
|
+
active = next((d.get("title") for d in docs if d.get("is_active")), None)
|
|
1378
|
+
return {
|
|
1379
|
+
"status": "success",
|
|
1380
|
+
"message": (
|
|
1381
|
+
f"{len(docs)} document(s) open"
|
|
1382
|
+
+ (f"; active: {active}" if active else "")
|
|
1383
|
+
),
|
|
1384
|
+
"documents": docs,
|
|
1385
|
+
"count": len(docs),
|
|
1386
|
+
"execution_time": result.execution_time,
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
@mcp.tool()
|
|
1390
|
+
async def activate_document(
|
|
1391
|
+
input_data: ActivateDocumentInput,
|
|
1392
|
+
) -> dict[str, Any]:
|
|
1393
|
+
"""Switch which open document is the active one.
|
|
1394
|
+
|
|
1395
|
+
Matches the argument against open documents by title, full path, or
|
|
1396
|
+
file name (case-insensitive), calls ``ActivateDoc3``, and reads the
|
|
1397
|
+
active document back to confirm the switch took.
|
|
1398
|
+
|
|
1399
|
+
Args:
|
|
1400
|
+
input_data (ActivateDocumentInput): Which document to activate.
|
|
1401
|
+
|
|
1402
|
+
Returns:
|
|
1403
|
+
dict[str, Any]: Status and the title switched to.
|
|
1404
|
+
|
|
1405
|
+
Example:
|
|
1406
|
+
```python
|
|
1407
|
+
await activate_document({"title_or_path": "bracket.SLDPRT"})
|
|
1408
|
+
```
|
|
1409
|
+
"""
|
|
1410
|
+
input_data = _coerce_input(ActivateDocumentInput, input_data)
|
|
1411
|
+
result = await adapter.activate_document(input_data.title_or_path)
|
|
1412
|
+
require_success(result, context="Failed to activate document")
|
|
1413
|
+
data = result.data if isinstance(result.data, dict) else {}
|
|
1414
|
+
activated = data.get("activated", input_data.title_or_path)
|
|
1415
|
+
message = f"Activated {activated}"
|
|
1416
|
+
if data.get("verified") is None:
|
|
1417
|
+
message += " (could not be read back to confirm)"
|
|
1418
|
+
return {
|
|
1419
|
+
"status": "success",
|
|
1420
|
+
"message": message,
|
|
1421
|
+
"data": data,
|
|
1422
|
+
"execution_time": result.execution_time,
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
@mcp.tool()
|
|
1426
|
+
async def save_body_as_part(
|
|
1427
|
+
input_data: SaveBodyAsPartInput,
|
|
1428
|
+
) -> dict[str, Any]:
|
|
1429
|
+
"""Extract one solid body from the active multibody part to a new file.
|
|
1430
|
+
|
|
1431
|
+
Runs SolidWorks' Save Bodies on a single named body, writing a
|
|
1432
|
+
standalone part to ``file_path``. The body is matched by name against
|
|
1433
|
+
the active part's solid bodies; the response lists every solid body
|
|
1434
|
+
found, so an unknown name reports the real options. Success is
|
|
1435
|
+
confirmed by the file existing on disk afterwards.
|
|
1436
|
+
|
|
1437
|
+
Args:
|
|
1438
|
+
input_data (SaveBodyAsPartInput): The body name and target path.
|
|
1439
|
+
|
|
1440
|
+
Returns:
|
|
1441
|
+
dict[str, Any]: Status, the written path, and the solid-body list.
|
|
1442
|
+
|
|
1443
|
+
Example:
|
|
1444
|
+
```python
|
|
1445
|
+
await save_body_as_part({
|
|
1446
|
+
"body_name": "Boss-Extrude1",
|
|
1447
|
+
"file_path": "C:/parts/bracket_body.sldprt"
|
|
1448
|
+
})
|
|
1449
|
+
```
|
|
1450
|
+
"""
|
|
1451
|
+
require_capability(
|
|
1452
|
+
adapter,
|
|
1453
|
+
"save_body_as_part",
|
|
1454
|
+
"Active adapter does not support save_body_as_part; no file was written.",
|
|
1455
|
+
)
|
|
1456
|
+
input_data = _coerce_input(SaveBodyAsPartInput, input_data)
|
|
1457
|
+
result = await adapter.save_body_as_part(
|
|
1458
|
+
input_data.body_name, input_data.file_path
|
|
1459
|
+
)
|
|
1460
|
+
require_success(result, context="Failed to save body as part")
|
|
1461
|
+
data = result.data if isinstance(result.data, dict) else {}
|
|
1462
|
+
return {
|
|
1463
|
+
"status": "success",
|
|
1464
|
+
"message": (
|
|
1465
|
+
f"Saved body {input_data.body_name} to "
|
|
1466
|
+
f"{data.get('file_path', input_data.file_path)}"
|
|
1467
|
+
),
|
|
1468
|
+
"data": data,
|
|
1469
|
+
"execution_time": result.execution_time,
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
tool_count = 19 # Total number of registered tools in this module
|
|
1473
|
+
return tool_count
|