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,1476 @@
1
+ """Docs discovery tool for SolidWorks COM and VBA command indexing.
2
+
3
+ This module provides functionality to discover and catalog all available COM objects,
4
+ methods, and properties for the installed SolidWorks version, as well as VBA library
5
+ references. Useful for building context for MCP server operations and enabling
6
+ intelligent tool selection.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import platform
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Any, TypeVar
16
+
17
+ from loguru import logger
18
+ from pydantic import Field
19
+
20
+ from .input_compat import CompatInput
21
+ from .tool_boundary import (
22
+ ToolOperationError,
23
+ UnsupportedOperationError,
24
+ normalize_input,
25
+ with_tool_boundary,
26
+ )
27
+
28
+ try:
29
+ import win32com.client
30
+ from pywintypes import com_error
31
+
32
+ HAS_WIN32COM = True
33
+ except ImportError:
34
+ HAS_WIN32COM = False
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Known SolidWorks COM interface catalogue (fallback when typelib unavailable)
39
+ # ---------------------------------------------------------------------------
40
+ _KNOWN_SW_INTERFACES: dict[str, dict[str, list[str]]] = {
41
+ "ISldWorks": {
42
+ "methods": [
43
+ "OpenDoc6",
44
+ "CloseDoc",
45
+ "ActivateDoc3",
46
+ "GetActiveObject",
47
+ "GetFirstDocument",
48
+ "GetDocumentCount",
49
+ "NewDocument",
50
+ "SendMsgToUser2",
51
+ "GetProcessID",
52
+ "ExitApp",
53
+ "RevisionNumber",
54
+ "GetUserTypeLibraryFile",
55
+ "CreateNewDocument",
56
+ ],
57
+ "properties": ["Visible", "ActiveDoc", "FrameState"],
58
+ },
59
+ "IModelDoc2": {
60
+ "methods": [
61
+ "ShowNamedView2",
62
+ "ViewZoomtofit2",
63
+ "GraphicsRedraw2",
64
+ "SaveBMP",
65
+ "SaveAs3",
66
+ "GetTitle",
67
+ "GetPathName",
68
+ "GetType",
69
+ "GetFeatureCount",
70
+ "GetActiveConfiguration",
71
+ "Extension",
72
+ "SketchManager",
73
+ "FeatureManager",
74
+ "SelectionManager",
75
+ "GetMassProperties",
76
+ "Close",
77
+ "SetSaveAsFileName",
78
+ "ResolveAllLightweightComponents",
79
+ "InsertSketch2",
80
+ "SketchAddConstraints",
81
+ "ClearSelection2",
82
+ ],
83
+ "properties": ["ActiveView", "ActiveLayer"],
84
+ },
85
+ "IPartDoc": {
86
+ "methods": [
87
+ "CreateFeatureFromSketch2",
88
+ "AddMateReference",
89
+ "GetBodies2",
90
+ "GetEntityByName",
91
+ "InsertSmartComponents",
92
+ "Material",
93
+ ],
94
+ "properties": [],
95
+ },
96
+ "IAssemblyDoc": {
97
+ "methods": [
98
+ "AddComponent4",
99
+ "AddComponent5",
100
+ "InsertNewPart3",
101
+ "LayoutToAssembly",
102
+ "ResolveAllLightweightComponents",
103
+ "AddMate5",
104
+ "GetComponents",
105
+ "GetComponentCount",
106
+ ],
107
+ "properties": [],
108
+ },
109
+ "IDrawingDoc": {
110
+ "methods": [
111
+ "CreateDrawViewFromModelView3",
112
+ "InsertModelAnnotations3",
113
+ "GenerateViewPalette",
114
+ "Get3DDrawingView",
115
+ ],
116
+ "properties": [],
117
+ },
118
+ "IFeatureManager": {
119
+ "methods": [
120
+ "FeatureExtrusion3",
121
+ "FeatureCut3",
122
+ "FeatureRevolve2",
123
+ "InsertProtrusionBlend4",
124
+ "InsertShell",
125
+ "InsertFillet3",
126
+ "InsertChamfer",
127
+ "InsertDraftXpert3",
128
+ "FeatureMirror3",
129
+ "FeatureCircularPattern2",
130
+ "FeatureLinearPattern3",
131
+ "InsertBoss",
132
+ "FeatureSweep5",
133
+ "FeatureLoft3",
134
+ ],
135
+ "properties": [],
136
+ },
137
+ "ISketchManager": {
138
+ "methods": [
139
+ "InsertSketch",
140
+ "CreateLine",
141
+ "CreateArc",
142
+ "CreateCircle",
143
+ "CreateRectangle",
144
+ "CreateEllipse",
145
+ "CreateSpline",
146
+ "CreateCenterLine",
147
+ "SketchMirror",
148
+ "SketchOffset",
149
+ "AddToDB",
150
+ "InsertSketch2",
151
+ ],
152
+ "properties": ["ActiveSketch"],
153
+ },
154
+ "IModelView": {
155
+ "methods": [
156
+ "ZoomByFactor",
157
+ "ZoomToSheet",
158
+ "OrientView",
159
+ "SetCameraParameters",
160
+ ],
161
+ "properties": ["Orientation3", "Translation3", "FrameWidth", "FrameHeight"],
162
+ },
163
+ "IExtensionManager": {
164
+ "methods": ["SaveAs2", "SelectByID2", "MultiSelect2"],
165
+ "properties": [],
166
+ },
167
+ "IFeature": {
168
+ "methods": [
169
+ "GetNextFeature",
170
+ "GetName",
171
+ "GetTypeName2",
172
+ "SetSuppression2",
173
+ "IsSuppressed2",
174
+ "GetDefinition",
175
+ ],
176
+ "properties": ["Name"],
177
+ },
178
+ "IBody2": {
179
+ "methods": [
180
+ "GetFaceCount",
181
+ "GetEdgeCount",
182
+ "GetVertexCount",
183
+ "GetMassProperties",
184
+ "GetType",
185
+ ],
186
+ "properties": [],
187
+ },
188
+ "ISketchSegment": {
189
+ "methods": ["GetStartPoint2", "GetEndPoint2", "GetType", "SetEndPoint"],
190
+ "properties": [],
191
+ },
192
+ }
193
+
194
+
195
+ def _enumerate_typeinfo_members(typeinfo: Any) -> tuple[list[str], list[str]]:
196
+ """Enumerate methods and properties from a COM ITypeInfo object.
197
+
198
+ Uses raw ITypeInfo via pythoncom to walk the function table, which works reliably for
199
+ both early-bound and late-bound COM objects.
200
+
201
+ Returns (methods, properties) as sorted de-duped lists.
202
+
203
+ Args:
204
+ typeinfo (Any): The typeinfo value.
205
+
206
+ Returns:
207
+ tuple[list[str], list[str]]: A tuple containing the resulting values.
208
+ """
209
+ methods: list[str] = []
210
+ properties: list[str] = []
211
+ seen: set[str] = set()
212
+
213
+ try:
214
+ typeattr = typeinfo.GetTypeAttr()
215
+ for i in range(typeattr.cFuncs):
216
+ try:
217
+ funcdesc = typeinfo.GetFuncDesc(i)
218
+ names = typeinfo.GetNames(funcdesc.memid)
219
+ name = names[0] if names else None
220
+ if not name or name.startswith("_"):
221
+ continue
222
+ if name in seen:
223
+ continue
224
+ seen.add(name)
225
+ # invkind: 1=FUNC, 2=PROPERTYGET, 4=PROPERTYPUT, 8=PROPERTYPUTREF
226
+ if funcdesc.invkind == 1:
227
+ methods.append(name)
228
+ else:
229
+ properties.append(name)
230
+ except Exception:
231
+ continue
232
+ except Exception:
233
+ pass
234
+
235
+ return sorted(methods), sorted(properties)
236
+
237
+
238
+ def _discover_com_via_typeinfo(sw_app: Any) -> tuple[dict[str, Any], int, int]:
239
+ """Enumerate ISldWorks methods/properties via ITypeInfo.
240
+
241
+ Returns a COM index dict keyed by interface name, with the same shape as the rest of the
242
+ index (``methods``, ``properties``, ``method_count``, ``property_count``).
243
+
244
+ Args:
245
+ sw_app (Any): The sw app value.
246
+
247
+ Returns:
248
+ tuple[dict[str, Any], int, int]: com_index, total_methods, total_properties.
249
+ """
250
+ if not HAS_WIN32COM:
251
+ return {}, 0, 0
252
+
253
+ com_index: dict[str, Any] = {}
254
+ total_methods = 0
255
+ total_properties = 0
256
+
257
+ try:
258
+ # GetTypeInfo returns ITypeInfo for the IDispatch interface of the object.
259
+ typeinfo = sw_app._oleobj_.GetTypeInfo()
260
+ methods, properties = _enumerate_typeinfo_members(typeinfo)
261
+
262
+ # Walk implemented interfaces to catch inherited members.
263
+ try:
264
+ typeattr = typeinfo.GetTypeAttr()
265
+ for impl_idx in range(typeattr.cImplTypes):
266
+ try:
267
+ ref_type = typeinfo.GetRefTypeOfImplType(impl_idx)
268
+ ref_typeinfo = typeinfo.GetRefTypeInfo(ref_type)
269
+ extra_m, extra_p = _enumerate_typeinfo_members(ref_typeinfo)
270
+ for m in extra_m:
271
+ if m not in methods:
272
+ methods.append(m)
273
+ for p in extra_p:
274
+ if p not in properties:
275
+ properties.append(p)
276
+ except Exception:
277
+ continue
278
+ except Exception:
279
+ pass
280
+
281
+ methods = sorted(set(methods))
282
+ properties = sorted(set(properties))
283
+
284
+ if methods or properties:
285
+ com_index["ISldWorks"] = {
286
+ "methods": methods,
287
+ "properties": properties,
288
+ "method_count": len(methods),
289
+ "property_count": len(properties),
290
+ }
291
+ total_methods += len(methods)
292
+ total_properties += len(properties)
293
+
294
+ except Exception as exc:
295
+ logger.debug(
296
+ "[docs_discovery] ITypeInfo enumeration unavailable; using built-in interface catalog fallback: {}",
297
+ exc,
298
+ )
299
+
300
+ return com_index, total_methods, total_properties
301
+
302
+
303
+ def _discover_vba_references_via_registry() -> dict[str, Any]:
304
+ """Enumerate VBA/TypeLib references via the Windows Registry.
305
+
306
+ Scans HKEY_CLASSES_ROOT\\TypeLib for entries whose name or GUID matches SolidWorks or
307
+ common Office/VBA libraries.
308
+
309
+ Returns:
310
+ dict[str, Any]: A dictionary containing the resulting values.
311
+ """
312
+ vba_refs: dict[str, Any] = {}
313
+
314
+ # Keywords to look for in type lib names
315
+ interesting_keywords = [
316
+ "solidworks",
317
+ "sldworks",
318
+ "vba",
319
+ "visual basic",
320
+ "stdole",
321
+ "office",
322
+ "microsoft office",
323
+ ]
324
+
325
+ if platform.system() != "Windows":
326
+ return vba_refs
327
+
328
+ try:
329
+ import winreg
330
+
331
+ typelib_key = winreg.OpenKey(
332
+ winreg.HKEY_CLASSES_ROOT, "TypeLib", access=winreg.KEY_READ
333
+ )
334
+ idx = 0
335
+ while True:
336
+ try:
337
+ guid = winreg.EnumKey(typelib_key, idx)
338
+ idx += 1
339
+ except OSError:
340
+ break
341
+ try:
342
+ guid_key = winreg.OpenKey(typelib_key, guid, access=winreg.KEY_READ)
343
+ ver_idx = 0
344
+ while True:
345
+ try:
346
+ version = winreg.EnumKey(guid_key, ver_idx)
347
+ ver_idx += 1
348
+ except OSError:
349
+ break
350
+ try:
351
+ ver_key = winreg.OpenKey(
352
+ guid_key, version, access=winreg.KEY_READ
353
+ )
354
+ try:
355
+ name, _ = winreg.QueryValueEx(ver_key, "")
356
+ except OSError:
357
+ name = ""
358
+ winreg.CloseKey(ver_key)
359
+ if name and any(
360
+ kw in name.lower() for kw in interesting_keywords
361
+ ):
362
+ key = f"{name} ({guid} v{version})"
363
+ if key not in vba_refs:
364
+ vba_refs[key] = {
365
+ "description": name,
366
+ "guid": guid,
367
+ "version": version,
368
+ "status": "available",
369
+ }
370
+ except Exception:
371
+ continue
372
+ winreg.CloseKey(guid_key)
373
+ except Exception:
374
+ continue
375
+ winreg.CloseKey(typelib_key)
376
+ except Exception as exc:
377
+ logger.debug("[docs_discovery] Registry TypeLib scan failed: {}", exc)
378
+
379
+ return vba_refs
380
+
381
+
382
+ class SolidWorksDocsDiscovery:
383
+ """Discover and index SolidWorks COM and VBA documentation.
384
+
385
+ Args:
386
+ output_dir (Path | None): The output dir value. Defaults to None.
387
+
388
+ Attributes:
389
+ output_dir (Any): The output dir value.
390
+ sw_app (Any): The sw app value.
391
+ """
392
+
393
+ def __init__(self, output_dir: Path | None = None):
394
+ """Initialize docs discovery.
395
+
396
+ Args:
397
+ output_dir (Path | None): The output dir value. Defaults to None.
398
+ """
399
+ self.output_dir = output_dir or Path(".generated/docs-index")
400
+ self.output_dir.mkdir(parents=True, exist_ok=True)
401
+ self.sw_app: Any = None
402
+ self.index: dict[str, Any] = {
403
+ "version": "1.0",
404
+ "solidworks_version": None,
405
+ "com_objects": {},
406
+ "vba_references": {},
407
+ "total_methods": 0,
408
+ "total_properties": 0,
409
+ }
410
+
411
+ def connect_to_solidworks(self) -> bool:
412
+ """Attach to an already-running SolidWorks instance.
413
+
414
+ Discovery only ever attaches; it never starts SolidWorks. Launching
415
+ from a process outside the desktop session (an SSH shell, a test run,
416
+ a service) starts a hidden SLDWORKS.exe nobody can see or close, so
417
+ when no instance is running this returns False instead.
418
+
419
+ Returns:
420
+ bool: True if attached to a running SolidWorks, otherwise False.
421
+ """
422
+ if not HAS_WIN32COM:
423
+ logger.error("win32com not available; cannot index COM")
424
+ return False
425
+
426
+ if platform.system() != "Windows":
427
+ logger.error("COM discovery only available on Windows")
428
+ return False
429
+
430
+ try:
431
+ self.sw_app = win32com.client.GetActiveObject("SldWorks.Application")
432
+ return True
433
+ except com_error as e:
434
+ logger.error(
435
+ "SolidWorks is not running in this session; start it and retry "
436
+ f"(docs discovery never launches SolidWorks): {e}"
437
+ )
438
+ return False
439
+
440
+ def discover_com_objects(self) -> dict[str, Any]:
441
+ """Discover all COM objects and their methods/properties.
442
+
443
+ Uses ITypeInfo enumeration via pythoncom for ISldWorks, then supplements with the active
444
+ document's interface if a model is open. Falls back to the known-interface catalogue
445
+ when typelib introspection is unavailable.
446
+
447
+ Returns:
448
+ dict[str, Any]: A dictionary containing the resulting values.
449
+ """
450
+ if not self.sw_app:
451
+ logger.error("Not connected to SolidWorks")
452
+ return {}
453
+
454
+ com_index: dict[str, Any] = {}
455
+ total_m = 0
456
+ total_p = 0
457
+
458
+ # ------------------------------------------------------------------ #
459
+ # Primary: ITypeInfo enumeration for ISldWorks
460
+ # ------------------------------------------------------------------ #
461
+ if HAS_WIN32COM:
462
+ try:
463
+ result = _discover_com_via_typeinfo(self.sw_app)
464
+ # _discover_com_via_typeinfo returns (dict, total_m, total_p)
465
+ partial_index, t_m, t_p = result
466
+ com_index.update(partial_index)
467
+ total_m += t_m
468
+ total_p += t_p
469
+ if partial_index:
470
+ logger.info(
471
+ "[docs_discovery] ITypeInfo: {} ISldWorks methods, {} properties",
472
+ t_m,
473
+ t_p,
474
+ )
475
+ except Exception as exc:
476
+ logger.debug("[docs_discovery] ITypeInfo path failed: {}", exc)
477
+
478
+ # ------------------------------------------------------------------ #
479
+ # Supplement: active document (IModelDoc2) if a model is open
480
+ # ------------------------------------------------------------------ #
481
+ try:
482
+ active_doc = self.sw_app.ActiveDoc
483
+ if active_doc is not None and HAS_WIN32COM:
484
+ doc_typeinfo = active_doc._oleobj_.GetTypeInfo()
485
+ doc_methods, doc_props = _enumerate_typeinfo_members(doc_typeinfo)
486
+ if doc_methods or doc_props:
487
+ com_index["IModelDoc2_active"] = {
488
+ "methods": doc_methods,
489
+ "properties": doc_props,
490
+ "method_count": len(doc_methods),
491
+ "property_count": len(doc_props),
492
+ }
493
+ total_m += len(doc_methods)
494
+ total_p += len(doc_props)
495
+ logger.info(
496
+ "[docs_discovery] Active doc typeinfo: {} methods, {} properties",
497
+ len(doc_methods),
498
+ len(doc_props),
499
+ )
500
+ except Exception:
501
+ pass
502
+
503
+ # ------------------------------------------------------------------ #
504
+ # Fallback: merge known-interface catalogue for any gaps
505
+ # ------------------------------------------------------------------ #
506
+ for iface_name, members in _KNOWN_SW_INTERFACES.items():
507
+ if iface_name in com_index:
508
+ continue # already populated via typeinfo
509
+ com_index[iface_name] = {
510
+ "methods": members["methods"],
511
+ "properties": members["properties"],
512
+ "method_count": len(members["methods"]),
513
+ "property_count": len(members["properties"]),
514
+ }
515
+ total_m += len(members["methods"])
516
+ total_p += len(members["properties"])
517
+
518
+ try:
519
+ self.index["total_methods"] += total_m
520
+ self.index["total_properties"] += total_p
521
+ except TypeError:
522
+ pass # accumulator corrupted (e.g. set to a string by tests); tolerate
523
+
524
+ # Get SolidWorks version
525
+ try:
526
+ self.index["solidworks_version"] = self.sw_app.RevisionNumber()
527
+ except Exception:
528
+ pass
529
+
530
+ return com_index
531
+
532
+ def discover_vba_references(self) -> dict[str, Any]:
533
+ """Discover VBA/TypeLib references via the Windows Registry.
534
+
535
+ Scans HKEY_CLASSES_ROOT\\TypeLib for SolidWorks and common Office/VBA type libraries.
536
+
537
+ Returns:
538
+ dict[str, Any]: A dictionary containing the resulting values.
539
+ """
540
+ refs = _discover_vba_references_via_registry()
541
+ if refs:
542
+ logger.info(
543
+ "[docs_discovery] Registry TypeLib scan found {} entries", len(refs)
544
+ )
545
+ else:
546
+ logger.debug(
547
+ "[docs_discovery] Registry TypeLib scan found no matching entries"
548
+ )
549
+ return refs
550
+
551
+ def discover_all(self) -> dict[str, Any]:
552
+ """Run full discovery of COM and VBA documentation.
553
+
554
+ Returns:
555
+ dict[str, Any]: A dictionary containing the resulting values.
556
+ """
557
+ if not self.connect_to_solidworks():
558
+ logger.error("Cannot proceed with discovery without SolidWorks connection")
559
+ return self.index
560
+
561
+ logger.info("Discovering COM objects...")
562
+ self.index["com_objects"] = self.discover_com_objects()
563
+
564
+ logger.info("Discovering VBA references...")
565
+ self.index["vba_references"] = self.discover_vba_references()
566
+
567
+ return self.index
568
+
569
+ def save_index(self, filename: str = "solidworks_docs_index.json") -> Path | None:
570
+ """Save discovered documentation to JSON file.
571
+
572
+ Args:
573
+ filename (str): The filename value. Defaults to "solidworks_docs_index.json".
574
+
575
+ Returns:
576
+ Path | None: The result produced by the operation.
577
+ """
578
+ try:
579
+ output_path = self.output_dir / filename
580
+ with open(output_path, "w", encoding="utf-8") as f:
581
+ json.dump(self.index, f, indent=2)
582
+ logger.info(f"Docs index saved to {output_path}")
583
+ return output_path
584
+ except Exception as e:
585
+ logger.error(f"Failed to save docs index: {e}")
586
+ return None
587
+
588
+ def create_search_summary(self) -> dict[str, Any]:
589
+ """Create a summary of indexed documentation for search/reference.
590
+
591
+ Returns:
592
+ dict[str, Any]: A dictionary containing the resulting values.
593
+ """
594
+ summary = {
595
+ "total_com_objects": len(self.index["com_objects"]),
596
+ "total_methods": self.index["total_methods"],
597
+ "total_properties": self.index["total_properties"],
598
+ "solidworks_version": self.index["solidworks_version"],
599
+ "available_vba_libs": [
600
+ lib
601
+ for lib, info in self.index["vba_references"].items()
602
+ if info.get("status") == "available"
603
+ ],
604
+ }
605
+ return summary
606
+
607
+
608
+ class DiscoverDocsInput(CompatInput):
609
+ """Input schema for docs discovery.
610
+
611
+ Attributes:
612
+ include_vba (bool): The include vba value.
613
+ output_dir (str | None): The output dir value.
614
+ year (int | None): The year value.
615
+ """
616
+
617
+ output_dir: str | None = Field(
618
+ default=None,
619
+ description="Optional output directory for indexed documentation",
620
+ )
621
+ include_vba: bool = Field(
622
+ default=True,
623
+ description="Include VBA library reference indexing",
624
+ )
625
+ year: int | None = Field(
626
+ default=None,
627
+ description="SolidWorks year override for saved index naming (e.g., 2026)",
628
+ )
629
+
630
+
631
+ class SearchApiHelpInput(CompatInput):
632
+ """Input schema for SolidWorks API help search.
633
+
634
+ Attributes:
635
+ auto_discover_if_missing (bool): The auto discover if missing value.
636
+ index_file (str | None): The index file value.
637
+ max_results (int): The max results value.
638
+ query (str): The query value.
639
+ year (int | None): The year value.
640
+ """
641
+
642
+ query: str = Field(
643
+ description="Search phrase for SolidWorks API help (methods, properties, objects)",
644
+ min_length=2,
645
+ )
646
+ year: int | None = Field(
647
+ default=None,
648
+ description="SolidWorks year override (e.g., 2025, 2026)",
649
+ )
650
+ max_results: int = Field(
651
+ default=10,
652
+ ge=1,
653
+ le=50,
654
+ description="Maximum number of search results",
655
+ )
656
+ index_file: str | None = Field(
657
+ default=None,
658
+ description="Optional explicit index file path (JSON) to search",
659
+ )
660
+ auto_discover_if_missing: bool = Field(
661
+ default=False,
662
+ description="Generate docs index first when no index file is found",
663
+ )
664
+
665
+
666
+ class LookupApiMethodInput(CompatInput):
667
+ """Input schema for a single method/property lookup."""
668
+
669
+ interface: str = Field(
670
+ description="COM interface name, e.g. 'IFeatureManager'", min_length=2
671
+ )
672
+ method: str = Field(
673
+ description="Method or property name, e.g. 'InsertFeatureChamfer'",
674
+ min_length=1,
675
+ )
676
+ year: int | None = Field(default=None, description="SolidWorks year override")
677
+ index_file: str | None = Field(
678
+ default=None, description="Optional explicit index file path (JSON)"
679
+ )
680
+
681
+
682
+ class LookupApiInterfaceInput(CompatInput):
683
+ """Input schema for an interface member-list lookup."""
684
+
685
+ interface: str = Field(
686
+ description="COM interface name, e.g. 'ISldWorks'", min_length=2
687
+ )
688
+ year: int | None = Field(default=None, description="SolidWorks year override")
689
+ index_file: str | None = Field(
690
+ default=None, description="Optional explicit index file path (JSON)"
691
+ )
692
+
693
+
694
+ class FindRelatedApiInput(CompatInput):
695
+ """Input schema for a related-members lookup."""
696
+
697
+ name: str = Field(
698
+ description="An indexed interface or member name to find relatives of",
699
+ min_length=2,
700
+ )
701
+ year: int | None = Field(default=None, description="SolidWorks year override")
702
+ max_results: int = Field(
703
+ default=25, ge=1, le=200, description="Maximum related members to return"
704
+ )
705
+ index_file: str | None = Field(
706
+ default=None, description="Optional explicit index file path (JSON)"
707
+ )
708
+
709
+
710
+ CompatInputT = TypeVar("CompatInputT", bound=CompatInput)
711
+
712
+
713
+ def _extract_year(value: str | None) -> int | None:
714
+ """Extract a 4-digit year from any string.
715
+
716
+ Args:
717
+ value (str | None): The value value.
718
+
719
+ Returns:
720
+ int | None: The result produced by the operation.
721
+ """
722
+ if not value:
723
+ return None
724
+ match = re.search(r"\b(20\d{2})\b", value)
725
+ if not match:
726
+ return None
727
+ try:
728
+ return int(match.group(1))
729
+ except ValueError:
730
+ return None
731
+
732
+
733
+ def _detect_installed_solidworks_year() -> int | None:
734
+ """Detect the latest installed SolidWorks year from the public samples path.
735
+
736
+ Returns:
737
+ int | None: The result produced by the operation.
738
+ """
739
+ root = Path(r"C:\Users\Public\Documents\SOLIDWORKS")
740
+ if not root.exists():
741
+ return None
742
+
743
+ years: list[int] = []
744
+ try:
745
+ for child in root.iterdir():
746
+ if not child.is_dir():
747
+ continue
748
+ year = _extract_year(child.name)
749
+ if year is not None:
750
+ years.append(year)
751
+ except OSError:
752
+ # Path can be inaccessible or missing on CI runners and non-Windows hosts.
753
+ return None
754
+
755
+ if not years:
756
+ return None
757
+ return max(years)
758
+
759
+
760
+ def _resolve_solidworks_year(requested_year: int | None, config: Any) -> int | None:
761
+ """Resolve SolidWorks year from explicit request, config, then local installation.
762
+
763
+ Args:
764
+ requested_year (int | None): The requested year value.
765
+ config (Any): Configuration values for the operation.
766
+
767
+ Returns:
768
+ int | None: The result produced by the operation.
769
+ """
770
+ if requested_year:
771
+ return requested_year
772
+
773
+ config_year = getattr(config, "solidworks_year", None)
774
+ if isinstance(config_year, int):
775
+ return config_year
776
+
777
+ config_path_year = _extract_year(getattr(config, "solidworks_path", None))
778
+ if config_path_year:
779
+ return config_path_year
780
+
781
+ return _detect_installed_solidworks_year()
782
+
783
+
784
+ def _load_index_file(index_file: Path) -> dict[str, Any] | None:
785
+ """Load docs index JSON from disk.
786
+
787
+ Args:
788
+ index_file (Path): The index file value.
789
+
790
+ Returns:
791
+ dict[str, Any] | None: A dictionary containing the resulting values.
792
+ """
793
+ try:
794
+ if not index_file.exists() or not index_file.is_file():
795
+ return None
796
+ with index_file.open("r", encoding="utf-8") as handle:
797
+ data = json.load(handle)
798
+ if isinstance(data, dict):
799
+ return data
800
+ return None
801
+ except Exception:
802
+ return None
803
+
804
+
805
+ def _find_index_file(year: int | None, explicit_index_file: str | None) -> Path | None:
806
+ """Find the most appropriate index file for a requested year.
807
+
808
+ Args:
809
+ year (int | None): The year value.
810
+ explicit_index_file (str | None): The explicit index file value.
811
+
812
+ Returns:
813
+ Path | None: The result produced by the operation.
814
+ """
815
+ if explicit_index_file:
816
+ explicit = Path(explicit_index_file)
817
+ if explicit.exists():
818
+ return explicit
819
+
820
+ names = ["solidworks_docs_index.json"]
821
+ if year:
822
+ names.insert(0, f"solidworks_docs_index_{year}.json")
823
+
824
+ search_dirs = [
825
+ Path(".generated/docs-index"),
826
+ Path("tests/.generated/solidworks_integration/docs-index"),
827
+ Path("tests/.generated/docs-index"),
828
+ ]
829
+
830
+ for directory in search_dirs:
831
+ for name in names:
832
+ candidate = directory / name
833
+ if candidate.exists():
834
+ return candidate
835
+
836
+ return None
837
+
838
+
839
+ def _index_is_stale(index_file: Path | None, max_age_days: int) -> bool:
840
+ """True when an existing index file is older than ``max_age_days``."""
841
+ if index_file is None:
842
+ return False
843
+ try:
844
+ if not index_file.exists():
845
+ return False
846
+ import time
847
+
848
+ age_days = (time.time() - index_file.stat().st_mtime) / 86400.0
849
+ return age_days > max_age_days
850
+ except OSError:
851
+ return False
852
+
853
+
854
+ def _maybe_refresh_index(
855
+ index: dict[str, Any] | None,
856
+ index_file: Path | None,
857
+ year: int | None,
858
+ config: Any,
859
+ ) -> tuple[dict[str, Any] | None, Path | None]:
860
+ """Rebuild the docs index if an existing one is stale and the config allows.
861
+
862
+ Only acts on an index file that exists but is older than
863
+ ``docs_index_max_age_days`` — a *missing* index is left to the caller's
864
+ ``auto_discover_if_missing`` opt-in. Returns the (possibly refreshed)
865
+ ``(index, index_file)``. A no-op without win32com / SolidWorks, or when
866
+ ``config.docs_index_auto_refresh`` is off; any failure leaves the original
867
+ index untouched.
868
+ """
869
+ if not getattr(config, "docs_index_auto_refresh", False):
870
+ return index, index_file
871
+ max_age = int(getattr(config, "docs_index_max_age_days", 21) or 21)
872
+ if not _index_is_stale(index_file, max_age):
873
+ return index, index_file
874
+ if not (HAS_WIN32COM and platform.system() == "Windows"):
875
+ return index, index_file
876
+ try:
877
+ logger.info(
878
+ "Docs index missing or older than {} days; rebuilding via "
879
+ "discover_solidworks_docs",
880
+ max_age,
881
+ )
882
+ discovery = SolidWorksDocsDiscovery()
883
+ fresh = discovery.discover_all()
884
+ if discovery.sw_app is None:
885
+ return index, index_file
886
+ filename = (
887
+ f"solidworks_docs_index_{year}.json"
888
+ if year
889
+ else "solidworks_docs_index.json"
890
+ )
891
+ fresh_file = discovery.save_index(filename=filename)
892
+ return fresh, fresh_file or index_file
893
+ except Exception as exc: # noqa: BLE001 - keep serving the stale index
894
+ logger.warning("Docs index auto-refresh failed, using existing index: {}", exc)
895
+ return index, index_file
896
+
897
+
898
+ def _search_index(
899
+ index: dict[str, Any], query: str, max_results: int
900
+ ) -> list[dict[str, Any]]:
901
+ """Search indexed COM objects/members for a query.
902
+
903
+ Args:
904
+ index (dict[str, Any]): The index value.
905
+ query (str): Query text used for the operation.
906
+ max_results (int): The max results value.
907
+
908
+ Returns:
909
+ list[dict[str, Any]]: A list containing the resulting items.
910
+ """
911
+ tokens = [t for t in re.split(r"\s+", query.lower().strip()) if t]
912
+ if not tokens:
913
+ return []
914
+
915
+ results: list[dict[str, Any]] = []
916
+
917
+ def _score(text: str) -> int:
918
+ """Build internal score.
919
+
920
+ Args:
921
+ text (str): Input text processed by the operation.
922
+
923
+ Returns:
924
+ int: The computed numeric result.
925
+ """
926
+ lower = text.lower()
927
+ score = 0
928
+ for token in tokens:
929
+ if token == lower:
930
+ score += 10
931
+ elif token in lower:
932
+ score += 3
933
+ return score
934
+
935
+ for obj_name, obj_data in index.get("com_objects", {}).items():
936
+ obj_score = _score(obj_name)
937
+
938
+ for method in obj_data.get("methods", []):
939
+ score = obj_score + _score(str(method))
940
+ if score > 0:
941
+ results.append(
942
+ {
943
+ "object": obj_name,
944
+ "member": method,
945
+ "member_type": "method",
946
+ "score": score,
947
+ }
948
+ )
949
+
950
+ for prop in obj_data.get("properties", []):
951
+ score = obj_score + _score(str(prop))
952
+ if score > 0:
953
+ results.append(
954
+ {
955
+ "object": obj_name,
956
+ "member": prop,
957
+ "member_type": "property",
958
+ "score": score,
959
+ }
960
+ )
961
+
962
+ for lib_name, lib_data in index.get("vba_references", {}).items():
963
+ lib_text = f"{lib_name} {lib_data.get('description', '')}"
964
+ score = _score(lib_text)
965
+ if score > 0:
966
+ results.append(
967
+ {
968
+ "object": "VBA Library",
969
+ "member": lib_name,
970
+ "member_type": "reference",
971
+ "status": lib_data.get("status", "unknown"),
972
+ "score": score,
973
+ }
974
+ )
975
+
976
+ results.sort(key=lambda item: item.get("score", 0), reverse=True)
977
+ return results[:max_results]
978
+
979
+
980
+ def _resolve_interface(
981
+ index: dict[str, Any], name: str
982
+ ) -> tuple[str, dict[str, Any]] | None:
983
+ """Case-insensitively resolve an interface name to its (canonical_name, data)."""
984
+ com_objects = index.get("com_objects", {})
985
+ if name in com_objects:
986
+ return name, com_objects[name]
987
+ lowered = name.lower()
988
+ for obj_name, obj_data in com_objects.items():
989
+ if obj_name.lower() == lowered:
990
+ return obj_name, obj_data
991
+ return None
992
+
993
+
994
+ def _lookup_interface(index: dict[str, Any], interface: str) -> dict[str, Any]:
995
+ """Return every indexed member of an interface, or a no-match marker.
996
+
997
+ The index stores member *names* only (no signatures), so this returns the
998
+ full method/property lists for the interface and nothing more.
999
+ """
1000
+ resolved = _resolve_interface(index, interface)
1001
+ if resolved is None:
1002
+ return {"found": False, "interface": interface}
1003
+ obj_name, obj_data = resolved
1004
+ methods = [str(m) for m in obj_data.get("methods", [])]
1005
+ properties = [str(p) for p in obj_data.get("properties", [])]
1006
+ return {
1007
+ "found": True,
1008
+ "interface": obj_name,
1009
+ "methods": methods,
1010
+ "properties": properties,
1011
+ "method_count": len(methods),
1012
+ "property_count": len(properties),
1013
+ }
1014
+
1015
+
1016
+ def _lookup_method(
1017
+ index: dict[str, Any], interface: str, method: str
1018
+ ) -> dict[str, Any]:
1019
+ """Confirm a method/property is indexed on an interface.
1020
+
1021
+ The index has no signatures, so ``parameters`` / ``return_type`` are always
1022
+ reported as not indexed; the useful output is confirmation the member
1023
+ exists on that interface plus its sibling members for context.
1024
+ """
1025
+ resolved = _resolve_interface(index, interface)
1026
+ if resolved is None:
1027
+ return {"found": False, "interface": interface, "method": method}
1028
+ obj_name, obj_data = resolved
1029
+ lowered = method.lower()
1030
+ for member_type, key in (("method", "methods"), ("property", "properties")):
1031
+ for member in obj_data.get(key, []):
1032
+ if str(member).lower() == lowered:
1033
+ return {
1034
+ "found": True,
1035
+ "interface": obj_name,
1036
+ "member": str(member),
1037
+ "member_type": member_type,
1038
+ "parameters": None,
1039
+ "return_type": None,
1040
+ "signature_indexed": False,
1041
+ "note": (
1042
+ "The local docs index stores member names only. Run "
1043
+ "search_solidworks_api_help or consult help.solidworks.com "
1044
+ "for the full signature."
1045
+ ),
1046
+ }
1047
+ return {
1048
+ "found": False,
1049
+ "interface": obj_name,
1050
+ "method": method,
1051
+ "note": f"{method!r} is not an indexed member of {obj_name}.",
1052
+ }
1053
+
1054
+
1055
+ def _find_related_api_members(
1056
+ index: dict[str, Any], name: str, max_results: int
1057
+ ) -> dict[str, Any]:
1058
+ """Given an indexed interface or member name, return related members.
1059
+
1060
+ - An interface name -> its own members.
1061
+ - A member name -> the other members of every interface that also exposes
1062
+ it (siblings commonly used together), plus name-substring matches
1063
+ elsewhere in the index.
1064
+ """
1065
+ com_objects = index.get("com_objects", {})
1066
+ related: list[dict[str, Any]] = []
1067
+ seen: set[tuple[str, str]] = set()
1068
+
1069
+ def _add(obj_name: str, member: str, member_type: str, reason: str) -> None:
1070
+ key = (obj_name, str(member))
1071
+ if key in seen or str(member).lower() == name.lower():
1072
+ return
1073
+ seen.add(key)
1074
+ related.append(
1075
+ {
1076
+ "object": obj_name,
1077
+ "member": str(member),
1078
+ "member_type": member_type,
1079
+ "reason": reason,
1080
+ }
1081
+ )
1082
+
1083
+ resolved = _resolve_interface(index, name)
1084
+ if resolved is not None:
1085
+ obj_name, obj_data = resolved
1086
+ for m in obj_data.get("methods", []):
1087
+ _add(obj_name, m, "method", "same interface")
1088
+ for p in obj_data.get("properties", []):
1089
+ _add(obj_name, p, "property", "same interface")
1090
+ return {
1091
+ "found": True,
1092
+ "query": name,
1093
+ "resolved_as": "interface",
1094
+ "related": related[:max_results],
1095
+ }
1096
+
1097
+ lowered = name.lower()
1098
+ matched_member = False
1099
+ for obj_name, obj_data in com_objects.items():
1100
+ members = [(m, "method") for m in obj_data.get("methods", [])] + [
1101
+ (p, "property") for p in obj_data.get("properties", [])
1102
+ ]
1103
+ names_lower = {str(m).lower() for m, _ in members}
1104
+ if lowered in names_lower:
1105
+ matched_member = True
1106
+ for member, member_type in members:
1107
+ _add(obj_name, member, member_type, f"sibling on {obj_name}")
1108
+ else:
1109
+ for member, member_type in members:
1110
+ if lowered in str(member).lower():
1111
+ _add(obj_name, member, member_type, "name match")
1112
+
1113
+ return {
1114
+ "found": matched_member or bool(related),
1115
+ "query": name,
1116
+ "resolved_as": "member" if matched_member else "name-match",
1117
+ "related": related[:max_results],
1118
+ }
1119
+
1120
+
1121
+ def _fallback_help_for_query(query: str) -> dict[str, Any]:
1122
+ """Provide coherent fallback help when no docs index is available.
1123
+
1124
+ Args:
1125
+ query (str): Query text used for the operation.
1126
+
1127
+ Returns:
1128
+ dict[str, Any]: A dictionary containing the resulting values.
1129
+ """
1130
+ q = query.lower()
1131
+
1132
+ if any(word in q for word in ("revolve", "lathe", "turned")):
1133
+ return {
1134
+ "suggested_tools": [
1135
+ "create_part",
1136
+ "create_sketch",
1137
+ "add_centerline",
1138
+ "add_line",
1139
+ "add_arc",
1140
+ "exit_sketch",
1141
+ "create_revolve",
1142
+ ],
1143
+ "next_steps": [
1144
+ "Create a profile sketch and a centerline in the same sketch.",
1145
+ "Exit sketch mode before create_revolve.",
1146
+ "If real COM returns parameter mismatch, use generate_vba_revolve as fallback guidance.",
1147
+ ],
1148
+ }
1149
+
1150
+ if any(word in q for word in ("extrude", "boss", "cut")):
1151
+ return {
1152
+ "suggested_tools": [
1153
+ "create_part",
1154
+ "create_sketch",
1155
+ "add_rectangle",
1156
+ "add_circle",
1157
+ "exit_sketch",
1158
+ "create_extrusion",
1159
+ ],
1160
+ "next_steps": [
1161
+ "Ensure sketch profile is closed before create_extrusion.",
1162
+ "Use direction='cut' for removal operations.",
1163
+ ],
1164
+ }
1165
+
1166
+ return {
1167
+ "suggested_tools": [
1168
+ "discover_solidworks_docs",
1169
+ "open_model",
1170
+ "get_file_properties",
1171
+ "save_as",
1172
+ ],
1173
+ "next_steps": [
1174
+ "Run discover_solidworks_docs to index the local COM API surface.",
1175
+ "Search again with a narrower API term like a method or interface name.",
1176
+ ],
1177
+ }
1178
+
1179
+
1180
+ async def register_docs_discovery_tools(
1181
+ mcp: Any, adapter: Any, config: dict[str, Any]
1182
+ ) -> int:
1183
+ """Register docs discovery tool with FastMCP.
1184
+
1185
+ Args:
1186
+ mcp (Any): The mcp value.
1187
+ adapter (Any): Adapter instance used for the operation.
1188
+ config (dict[str, Any]): Configuration values for the operation.
1189
+
1190
+ Returns:
1191
+ int: The computed numeric result.
1192
+ """
1193
+ mcp = with_tool_boundary(mcp)
1194
+
1195
+ @mcp.tool() # type: ignore[untyped-decorator]
1196
+ async def discover_solidworks_docs(
1197
+ input_data: DiscoverDocsInput | None = None,
1198
+ ) -> dict[str, Any]:
1199
+ """Discover and index SolidWorks COM and VBA documentation.
1200
+
1201
+ Creates a searchable index of all available COM objects, methods, properties, and VBA
1202
+ libraries for the installed SolidWorks version. Useful for building context for
1203
+ intelligent MCP tool selection and documentation queries.
1204
+
1205
+ Args:
1206
+ input_data (DiscoverDocsInput | None): The input data value. Defaults to None.
1207
+
1208
+ Returns:
1209
+ dict[str, Any]: A dictionary containing the resulting values.
1210
+
1211
+ Example:
1212
+ ```python
1213
+ result = await discover_solidworks_docs({
1214
+ "output_dir": "docs-index",
1215
+ "include_vba": True
1216
+ })
1217
+
1218
+ if result["status"] == "success":
1219
+ print(f"Discovered {result['summary']['total_methods']} COM methods")
1220
+ print(f"Output saved to {result['output_file']}")
1221
+ ```
1222
+
1223
+ Note:
1224
+ - Requires Windows + SolidWorks installed and running
1225
+ - Creates .generated/docs-index directory if not present
1226
+ - Generates solidworks_docs_index.json with full catalog
1227
+ - Index can be used to build context for RAG or tool discovery
1228
+ """
1229
+ import time
1230
+
1231
+ start_time = time.time()
1232
+
1233
+ if not HAS_WIN32COM:
1234
+ raise UnsupportedOperationError(
1235
+ "win32com not available; cannot discover COM documentation"
1236
+ )
1237
+
1238
+ if platform.system() != "Windows":
1239
+ raise UnsupportedOperationError("COM discovery only available on Windows")
1240
+
1241
+ normalized = normalize_input(input_data, DiscoverDocsInput)
1242
+ output_dir = Path(normalized.output_dir) if normalized.output_dir else None
1243
+ year = _resolve_solidworks_year(normalized.year, config)
1244
+
1245
+ discovery = SolidWorksDocsDiscovery(output_dir=output_dir)
1246
+
1247
+ logger.info("Starting SolidWorks documentation discovery...")
1248
+ index = discovery.discover_all()
1249
+ if discovery.sw_app is None:
1250
+ raise ToolOperationError(
1251
+ "SolidWorks is not running in this session; start SolidWorks "
1252
+ "and retry. Docs discovery attaches to a running instance and "
1253
+ "never launches one."
1254
+ )
1255
+
1256
+ filename = (
1257
+ f"solidworks_docs_index_{year}.json"
1258
+ if year
1259
+ else "solidworks_docs_index.json"
1260
+ )
1261
+ output_file = discovery.save_index(filename=filename)
1262
+
1263
+ # Rebuild the FAISS RAG namespace so Gemma can query the API surface.
1264
+ rag_indexed = False
1265
+ if output_file is not None:
1266
+ try:
1267
+ from solidworks_mcp.agents.vector_rag import (
1268
+ build_solidworks_api_docs_index,
1269
+ )
1270
+
1271
+ rag_idx = build_solidworks_api_docs_index(output_file)
1272
+ rag_idx.save()
1273
+ rag_indexed = True
1274
+ logger.info(
1275
+ "FAISS 'solidworks-api-docs' namespace rebuilt: %d chunks",
1276
+ rag_idx.chunk_count,
1277
+ )
1278
+ except ImportError:
1279
+ logger.debug("faiss-cpu not installed; skipping RAG index rebuild")
1280
+ except Exception as _rag_exc:
1281
+ logger.warning("RAG rebuild failed: {}", _rag_exc)
1282
+
1283
+ summary = discovery.create_search_summary()
1284
+
1285
+ return {
1286
+ "status": "success",
1287
+ "message": f"Discovered {summary['total_com_objects']} COM objects, "
1288
+ f"{summary['total_methods']} methods, "
1289
+ f"{summary['total_properties']} properties",
1290
+ "index": index,
1291
+ "summary": summary,
1292
+ "year": year,
1293
+ "output_file": str(output_file) if output_file else None,
1294
+ "rag_indexed": rag_indexed,
1295
+ "execution_time": time.time() - start_time,
1296
+ }
1297
+
1298
+ @mcp.tool() # type: ignore[untyped-decorator]
1299
+ async def search_solidworks_api_help(
1300
+ input_data: SearchApiHelpInput | None = None,
1301
+ ) -> dict[str, Any]:
1302
+ """Search SolidWorks API help index and return coherent guidance.
1303
+
1304
+ This tool helps when the LLM gets stuck by mapping user intent to discovered COM members
1305
+ and practical MCP workflow guidance.
1306
+
1307
+ Args:
1308
+ input_data (SearchApiHelpInput | None): The input data value. Defaults to None.
1309
+
1310
+ Returns:
1311
+ dict[str, Any]: A dictionary containing the resulting values.
1312
+ """
1313
+
1314
+ import time
1315
+
1316
+ start_time = time.time()
1317
+
1318
+ normalized = normalize_input(input_data, SearchApiHelpInput)
1319
+ year = _resolve_solidworks_year(normalized.year, config)
1320
+
1321
+ index_file = _find_index_file(year, normalized.index_file)
1322
+ index = _load_index_file(index_file) if index_file else None
1323
+
1324
+ # Rebuild when stale (config-gated), or when missing and the caller
1325
+ # opted in.
1326
+ index, index_file = _maybe_refresh_index(index, index_file, year, config)
1327
+ if index is None and normalized.auto_discover_if_missing:
1328
+ if HAS_WIN32COM and platform.system() == "Windows":
1329
+ discovery = SolidWorksDocsDiscovery()
1330
+ discovered = discovery.discover_all()
1331
+ if discovery.sw_app is not None:
1332
+ index = discovered
1333
+ filename = (
1334
+ f"solidworks_docs_index_{year}.json"
1335
+ if year
1336
+ else "solidworks_docs_index.json"
1337
+ )
1338
+ index_file = discovery.save_index(filename=filename)
1339
+
1340
+ matches = _search_index(index or {}, normalized.query, normalized.max_results)
1341
+ fallback = _fallback_help_for_query(normalized.query)
1342
+
1343
+ guidance_lines = []
1344
+ if matches:
1345
+ guidance_lines.append(
1346
+ f"Found {len(matches)} API matches for '{normalized.query}'."
1347
+ )
1348
+ guidance_lines.append(
1349
+ "Start with the highest-score members and verify with the real adapter path."
1350
+ )
1351
+ else:
1352
+ guidance_lines.append(
1353
+ "No indexed API hits found; using workflow fallback guidance."
1354
+ )
1355
+ guidance_lines.append(
1356
+ "Consider running discover_solidworks_docs first for this machine/version."
1357
+ )
1358
+
1359
+ return {
1360
+ "status": "success",
1361
+ "message": "SolidWorks API help search completed",
1362
+ "query": normalized.query,
1363
+ "year": year,
1364
+ "source_index_file": str(index_file) if index_file else None,
1365
+ "matches": matches,
1366
+ "fallback_help": fallback,
1367
+ "guidance": " ".join(guidance_lines),
1368
+ "execution_time": time.time() - start_time,
1369
+ }
1370
+
1371
+ def _load_index_for_lookup(
1372
+ requested_year: int | None, explicit_index_file: str | None
1373
+ ) -> tuple[dict[str, Any] | None, Path | None, int | None]:
1374
+ """Shared index resolution for the granular lookup tools."""
1375
+ resolved_year = _resolve_solidworks_year(requested_year, config)
1376
+ index_file = _find_index_file(resolved_year, explicit_index_file)
1377
+ index = _load_index_file(index_file) if index_file else None
1378
+ index, index_file = _maybe_refresh_index(
1379
+ index, index_file, resolved_year, config
1380
+ )
1381
+ return index, index_file, resolved_year
1382
+
1383
+ @mcp.tool() # type: ignore[untyped-decorator]
1384
+ async def lookup_api_method(
1385
+ input_data: LookupApiMethodInput | None = None,
1386
+ ) -> dict[str, Any]:
1387
+ """Look up one SolidWorks API method or property by interface + name.
1388
+
1389
+ Confirms the member is indexed on that interface and returns its
1390
+ sibling members for context. The local index stores names only, so
1391
+ full signatures (parameters, return type) are not available here - use
1392
+ search_solidworks_api_help or help.solidworks.com for those.
1393
+ """
1394
+ import time
1395
+
1396
+ start = time.time()
1397
+ normalized = normalize_input(input_data, LookupApiMethodInput)
1398
+ index, index_file, year = _load_index_for_lookup(
1399
+ normalized.year, normalized.index_file
1400
+ )
1401
+ result = _lookup_method(index or {}, normalized.interface, normalized.method)
1402
+ return {
1403
+ "status": "success",
1404
+ "message": (
1405
+ "Method found"
1406
+ if result.get("found")
1407
+ else "No indexed match for that interface/method"
1408
+ ),
1409
+ "year": year,
1410
+ "source_index_file": str(index_file) if index_file else None,
1411
+ "result": result,
1412
+ "execution_time": time.time() - start,
1413
+ }
1414
+
1415
+ @mcp.tool() # type: ignore[untyped-decorator]
1416
+ async def lookup_api_interface(
1417
+ input_data: LookupApiInterfaceInput | None = None,
1418
+ ) -> dict[str, Any]:
1419
+ """List every indexed member (methods + properties) of a COM interface."""
1420
+ import time
1421
+
1422
+ start = time.time()
1423
+ normalized = normalize_input(input_data, LookupApiInterfaceInput)
1424
+ index, index_file, year = _load_index_for_lookup(
1425
+ normalized.year, normalized.index_file
1426
+ )
1427
+ result = _lookup_interface(index or {}, normalized.interface)
1428
+ return {
1429
+ "status": "success",
1430
+ "message": (
1431
+ f"{result['method_count']} methods, {result['property_count']} properties"
1432
+ if result.get("found")
1433
+ else "Interface not found in the indexed documentation"
1434
+ ),
1435
+ "year": year,
1436
+ "source_index_file": str(index_file) if index_file else None,
1437
+ "result": result,
1438
+ "execution_time": time.time() - start,
1439
+ }
1440
+
1441
+ @mcp.tool() # type: ignore[untyped-decorator]
1442
+ async def find_related_api_members(
1443
+ input_data: FindRelatedApiInput | None = None,
1444
+ ) -> dict[str, Any]:
1445
+ """Find API members related to a known interface or member name.
1446
+
1447
+ For an interface: its own members. For a member: the other members of
1448
+ every interface that also exposes it, plus name-substring matches.
1449
+ """
1450
+ import time
1451
+
1452
+ start = time.time()
1453
+ normalized = normalize_input(input_data, FindRelatedApiInput)
1454
+ index, index_file, year = _load_index_for_lookup(
1455
+ normalized.year, normalized.index_file
1456
+ )
1457
+ result = _find_related_api_members(
1458
+ index or {}, normalized.name, normalized.max_results
1459
+ )
1460
+ return {
1461
+ "status": "success",
1462
+ "message": (
1463
+ f"{len(result['related'])} related member(s)"
1464
+ if result.get("found")
1465
+ else "No related members found"
1466
+ ),
1467
+ "year": year,
1468
+ "source_index_file": str(index_file) if index_file else None,
1469
+ "result": result,
1470
+ "execution_time": time.time() - start,
1471
+ }
1472
+
1473
+ # discover_solidworks_docs, search_solidworks_api_help, lookup_api_method,
1474
+ # lookup_api_interface, find_related_api_members
1475
+ tool_count = 5
1476
+ return tool_count