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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. solidworks_mcp/__init__.py +57 -0
  2. solidworks_mcp/adapters/__init__.py +40 -0
  3. solidworks_mcp/adapters/base.py +1882 -0
  4. solidworks_mcp/adapters/circuit_breaker.py +2052 -0
  5. solidworks_mcp/adapters/com_executor.py +252 -0
  6. solidworks_mcp/adapters/complexity_analyzer.py +272 -0
  7. solidworks_mcp/adapters/connection_pool.py +1884 -0
  8. solidworks_mcp/adapters/factory.py +366 -0
  9. solidworks_mcp/adapters/intelligent_router.py +218 -0
  10. solidworks_mcp/adapters/mock_adapter.py +4016 -0
  11. solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
  12. solidworks_mcp/adapters/solidworks/__init__.py +17 -0
  13. solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
  14. solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
  15. solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
  16. solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
  17. solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
  18. solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
  19. solidworks_mcp/adapters/solidworks/features.py +3362 -0
  20. solidworks_mcp/adapters/solidworks/io.py +3649 -0
  21. solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
  22. solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
  23. solidworks_mcp/adapters/solidworks/selection.py +308 -0
  24. solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
  25. solidworks_mcp/adapters/sw_feature_errors.py +66 -0
  26. solidworks_mcp/adapters/sw_type_info.py +317 -0
  27. solidworks_mcp/adapters/vba_adapter.py +318 -0
  28. solidworks_mcp/adapters/vba_macro_executor.py +215 -0
  29. solidworks_mcp/agents/__init__.py +68 -0
  30. solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
  31. solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
  32. solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
  33. solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
  34. solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
  35. solidworks_mcp/agents/harness.py +216 -0
  36. solidworks_mcp/agents/history_db.py +1508 -0
  37. solidworks_mcp/agents/retrieval_index.py +164 -0
  38. solidworks_mcp/agents/schemas.py +165 -0
  39. solidworks_mcp/agents/smoke_test.py +285 -0
  40. solidworks_mcp/agents/soc_exporter.py +650 -0
  41. solidworks_mcp/agents/soc_pickup.py +332 -0
  42. solidworks_mcp/agents/soc_rewind.py +221 -0
  43. solidworks_mcp/agents/vector_rag.py +665 -0
  44. solidworks_mcp/cache/__init__.py +5 -0
  45. solidworks_mcp/cache/response_cache.py +164 -0
  46. solidworks_mcp/config.py +729 -0
  47. solidworks_mcp/exceptions.py +37 -0
  48. solidworks_mcp/security/__init__.py +68 -0
  49. solidworks_mcp/security/auth.py +128 -0
  50. solidworks_mcp/security/cors.py +27 -0
  51. solidworks_mcp/security/rate_limiting.py +90 -0
  52. solidworks_mcp/security/runtime.py +155 -0
  53. solidworks_mcp/server.py +777 -0
  54. solidworks_mcp/server_cli_fixed.py +120 -0
  55. solidworks_mcp/setup/__init__.py +1 -0
  56. solidworks_mcp/setup/cli.py +399 -0
  57. solidworks_mcp/setup/clients.py +157 -0
  58. solidworks_mcp/setup/config_file.py +230 -0
  59. solidworks_mcp/setup/handshake.py +202 -0
  60. solidworks_mcp/setup/launcher.py +102 -0
  61. solidworks_mcp/setup/legacy.py +100 -0
  62. solidworks_mcp/setup/registration.py +193 -0
  63. solidworks_mcp/setup/solidworks.py +49 -0
  64. solidworks_mcp/tools/__init__.py +64 -0
  65. solidworks_mcp/tools/analysis.py +723 -0
  66. solidworks_mcp/tools/automation.py +578 -0
  67. solidworks_mcp/tools/docs_discovery.py +1476 -0
  68. solidworks_mcp/tools/drawing.py +1015 -0
  69. solidworks_mcp/tools/drawing_analysis.py +456 -0
  70. solidworks_mcp/tools/drawing_automation.py +627 -0
  71. solidworks_mcp/tools/engineering_data.py +466 -0
  72. solidworks_mcp/tools/error_classifier.py +232 -0
  73. solidworks_mcp/tools/export.py +833 -0
  74. solidworks_mcp/tools/file_facts.py +40 -0
  75. solidworks_mcp/tools/file_management.py +1473 -0
  76. solidworks_mcp/tools/input_compat.py +17 -0
  77. solidworks_mcp/tools/macro_recording.py +464 -0
  78. solidworks_mcp/tools/modeling.py +2360 -0
  79. solidworks_mcp/tools/sketching.py +1422 -0
  80. solidworks_mcp/tools/template_management.py +367 -0
  81. solidworks_mcp/tools/tool_boundary.py +280 -0
  82. solidworks_mcp/tools/vba_generation.py +1145 -0
  83. solidworks_mcp/ui/__init__.py +9 -0
  84. solidworks_mcp/ui/local_llm.py +725 -0
  85. solidworks_mcp/ui/routers/__init__.py +10 -0
  86. solidworks_mcp/ui/routers/checkpoint.py +24 -0
  87. solidworks_mcp/ui/routers/docs.py +47 -0
  88. solidworks_mcp/ui/routers/llm.py +83 -0
  89. solidworks_mcp/ui/routers/local_model.py +69 -0
  90. solidworks_mcp/ui/routers/model.py +75 -0
  91. solidworks_mcp/ui/routers/preview.py +38 -0
  92. solidworks_mcp/ui/routers/session.py +161 -0
  93. solidworks_mcp/ui/routers/viewer.py +154 -0
  94. solidworks_mcp/ui/schemas.py +502 -0
  95. solidworks_mcp/ui/server.py +298 -0
  96. solidworks_mcp/ui/service.py +481 -0
  97. solidworks_mcp/ui/services/__init__.py +136 -0
  98. solidworks_mcp/ui/services/_utils.py +958 -0
  99. solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
  100. solidworks_mcp/ui/services/docs_service.py +282 -0
  101. solidworks_mcp/ui/services/llm_service.py +956 -0
  102. solidworks_mcp/ui/services/model_service.py +459 -0
  103. solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
  104. solidworks_mcp/ui/services/preview_service.py +489 -0
  105. solidworks_mcp/ui/services/session_service.py +1389 -0
  106. solidworks_mcp/utils/__init__.py +9 -0
  107. solidworks_mcp/utils/feature_tree_classifier.py +342 -0
  108. solidworks_mcp/utils/logging.py +76 -0
  109. solidworks_mcp/utils/validation.py +77 -0
  110. solidworks_mcp/version.py +8 -0
  111. solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
  112. solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
  113. solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
  114. solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
  115. solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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