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,120 @@
1
+ """Utilities for server cli fixed."""
2
+
3
+ # Typer CLI setup
4
+ import asyncio
5
+
6
+ import typer
7
+ from loguru import logger
8
+
9
+ from . import utils
10
+ from .config import DeploymentMode, load_config
11
+
12
+ app = typer.Typer(
13
+ name="solidworks-mcp",
14
+ help="SolidWorks MCP Server - Model Context Protocol for SolidWorks automation",
15
+ no_args_is_help=True,
16
+ )
17
+
18
+
19
+ @app.command()
20
+ def run(
21
+ config: str = typer.Option(
22
+ None,
23
+ "--config",
24
+ help="Configuration file path",
25
+ show_default=False,
26
+ ),
27
+ mode: str = typer.Option(
28
+ None,
29
+ "--mode",
30
+ help="Deployment mode (local/remote/hybrid)",
31
+ show_default=False,
32
+ ),
33
+ host: str = typer.Option(
34
+ "localhost",
35
+ "--host",
36
+ help="Server host for remote mode",
37
+ ),
38
+ port: int = typer.Option(
39
+ 8000,
40
+ "--port",
41
+ help="Server port for remote mode",
42
+ ),
43
+ debug: bool = typer.Option(
44
+ False,
45
+ "--debug",
46
+ help="Enable debug mode",
47
+ ),
48
+ mock: bool = typer.Option(
49
+ False,
50
+ "--mock",
51
+ help="Use mock SolidWorks for testing",
52
+ ),
53
+ ) -> None:
54
+ """Start the SolidWorks MCP Server.
55
+
56
+ Args:
57
+ config (str): Configuration values for the operation. Defaults to typer.Option(
58
+ None, "--config", help="Configuration file path",
59
+ show_default=False, ).
60
+ mode (str): The mode value. Defaults to typer.Option( None, "--
61
+ mode", help="Deployment mode (local/remote/hybrid)",
62
+ show_default=False, ).
63
+ host (str): The host value. Defaults to typer.Option( "localhost", "
64
+ --host", help="Server host for remote mode", ).
65
+ port (int): The port value. Defaults to typer.Option( 8000, "--
66
+ port", help="Server port for remote mode", ).
67
+ debug (bool): The debug value. Defaults to typer.Option( False, "--
68
+ debug", help="Enable debug mode", ).
69
+ mock (bool): The mock value. Defaults to typer.Option( False, "--
70
+ mock", help="Use mock SolidWorks for testing", ).
71
+
72
+ Returns:
73
+ None: None.
74
+ """
75
+ # Load configuration
76
+ loaded_config = load_config(config)
77
+
78
+ # Override config with command-line arguments
79
+ if mode:
80
+ loaded_config.deployment_mode = DeploymentMode(mode)
81
+ if host:
82
+ loaded_config.host = host
83
+ if port:
84
+ loaded_config.port = port
85
+ if debug:
86
+ loaded_config.debug = True
87
+ loaded_config.log_level = "DEBUG"
88
+ if mock:
89
+ loaded_config.mock_solidworks = True
90
+
91
+ # Setup logging
92
+ utils.setup_logging(loaded_config)
93
+
94
+ logger.info("Starting SolidWorks MCP Server...")
95
+ import platform
96
+ import sys
97
+
98
+ logger.info(f"Platform: {platform.system()}")
99
+ logger.info(f"Python version: {sys.version}")
100
+ logger.info(f"Deployment mode: {loaded_config.deployment_mode}")
101
+ logger.info(f"Security level: {loaded_config.security_level}")
102
+
103
+ # Create and start server
104
+ from .server import SolidWorksMCPServer
105
+
106
+ server = SolidWorksMCPServer(loaded_config)
107
+
108
+ try:
109
+ asyncio.run(server.start())
110
+ except KeyboardInterrupt:
111
+ logger.info("Received interrupt signal")
112
+ except Exception as e:
113
+ logger.error(f"Server error: {e}")
114
+ raise
115
+ finally:
116
+ asyncio.run(server.stop())
117
+
118
+
119
+ if __name__ == "__main__":
120
+ app()
@@ -0,0 +1 @@
1
+ """Guided setup: connect MCP clients to this server (``solidworks-mcp setup``)."""
@@ -0,0 +1,399 @@
1
+ """``solidworks-mcp setup``: connect MCP clients to this server, step by step.
2
+
3
+ This module is only the conversation with the user: it asks, reports and
4
+ decides the order. Finding clients (:mod:`.clients`), editing their configs
5
+ (:mod:`.registration`) and checking the server (:mod:`.handshake`) live in
6
+ their own modules.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from enum import Enum
13
+ from typing import Any
14
+
15
+ import typer
16
+ from rich.console import Console
17
+ from rich.markup import escape
18
+ from rich.prompt import Confirm, Prompt
19
+ from rich.table import Table
20
+
21
+ from .clients import SERVER_NAME, Client, find_clients
22
+ from .config_file import ConfigFileError, EntryChange, current_entry
23
+ from .handshake import HandshakeResult, check_server
24
+ from .launcher import PACKAGE, in_uv_cache, server_command
25
+ from .registration import OldEntryCleaner, registrar_for
26
+ from .solidworks import installed_versions
27
+
28
+ console = Console()
29
+ app = typer.Typer(add_completion=False)
30
+
31
+
32
+ class OldEntryChoice(Enum):
33
+ """What to do with older entries that start this same server."""
34
+
35
+ ASK = "ask"
36
+ REMOVE = "remove"
37
+ KEEP = "keep"
38
+
39
+ @classmethod
40
+ def from_flags(cls, *, replace_old: bool, yes: bool) -> OldEntryChoice:
41
+ """Pick from the command-line flags.
42
+
43
+ ``--replace-old`` removes them. ``--yes`` alone keeps them, since
44
+ removing someone's entries is never done without being asked.
45
+
46
+ Args:
47
+ replace_old: ``--replace-old`` was given.
48
+ yes: ``--yes`` was given.
49
+
50
+ Returns:
51
+ The choice.
52
+ """
53
+ if replace_old:
54
+ return cls.REMOVE
55
+ return cls.KEEP if yes else cls.ASK
56
+
57
+
58
+ @app.command()
59
+ def setup(
60
+ client: list[str] | None = typer.Option(
61
+ None,
62
+ "--client",
63
+ help="Client to configure: claude-desktop, claude-code, vscode, cursor, "
64
+ "lmstudio. Repeat for several. Default: choose from a list.",
65
+ ),
66
+ yes: bool = typer.Option(
67
+ False, "--yes", "-y", help="Don't ask; configure every client found."
68
+ ),
69
+ dry_run: bool = typer.Option(
70
+ False, "--dry-run", help="Show what would change, and change nothing."
71
+ ),
72
+ remove: bool = typer.Option(
73
+ False, "--remove", help="Remove the solidworks entry instead of adding it."
74
+ ),
75
+ mock: bool = typer.Option(
76
+ False, "--mock", help="Configure mock mode, which needs no SolidWorks."
77
+ ),
78
+ skip_check: bool = typer.Option(
79
+ False, "--skip-check", help="Don't start the server to check it first."
80
+ ),
81
+ replace_old: bool = typer.Option(
82
+ False,
83
+ "--replace-old",
84
+ help="Remove older hand-made entries that start this server "
85
+ "(run-mcp.ps1 and the like) without asking.",
86
+ ),
87
+ ) -> None:
88
+ """Find your SolidWorks and MCP clients, and add this server to them.
89
+
90
+ Only the solidworks entry of each client's config is changed. The file is
91
+ backed up first, and no other entry is ever printed.
92
+
93
+ Args:
94
+ client: Client keys to configure; ``None`` to choose from a list.
95
+ yes: Configure every client found, without asking.
96
+ dry_run: Report what would change, and change nothing.
97
+ remove: Remove the entry instead of adding it.
98
+ mock: Write a mock-mode entry.
99
+ skip_check: Skip the start-up check.
100
+ replace_old: Remove older entries for this server without asking.
101
+
102
+ Raises:
103
+ typer.Exit: With code 1 if the check failed or a client couldn't be
104
+ configured.
105
+ """
106
+ console.print("[bold]SolidWorks MCP setup[/bold]\n")
107
+ if not remove:
108
+ mock = _choose_mode(mock, yes)
109
+ command, args = server_command(mock=mock)
110
+ if in_uv_cache(sys.executable) and args[:1] != [PACKAGE]:
111
+ console.print(
112
+ "[yellow]Running from uv's cache, and uvx was not found: the entry "
113
+ "will stop working when uv cleans its cache. Install uv so uvx is on "
114
+ "the PATH, then run setup again.[/yellow]"
115
+ )
116
+
117
+ chosen = _choose_clients(find_clients(), client, yes, command)
118
+ if not chosen:
119
+ console.print("No clients chosen; nothing to do.")
120
+ return
121
+
122
+ # Check before writing, so a broken launcher never reaches a config.
123
+ if not (remove or skip_check):
124
+ console.print(
125
+ "\nChecking that the server starts (mock mode, SolidWorks is not touched)..."
126
+ )
127
+ # The launcher running now: a `uvx` entry would fetch from PyPI instead.
128
+ result = _run_check(*server_command(mock=mock, lasting=False))
129
+ if not result.ok:
130
+ console.print(f"[red]{result.message}[/red] No config was changed.")
131
+ raise typer.Exit(1)
132
+ console.print(f"[green]{result.message}[/green]")
133
+
134
+ console.print()
135
+ # One client's failure doesn't stop the others; it sets the exit code.
136
+ choice = OldEntryChoice.from_flags(replace_old=replace_old, yes=yes)
137
+ failures = sum(
138
+ not _configure(target, command, args, remove, dry_run, choice)
139
+ for target in chosen
140
+ )
141
+
142
+ if dry_run:
143
+ console.print("\nDry run: nothing was changed.")
144
+ elif not remove:
145
+ console.print("\n[bold]Next[/bold]")
146
+ for target in chosen:
147
+ console.print(f" {target.name}: {target.restart_hint}")
148
+ if failures:
149
+ raise typer.Exit(1)
150
+
151
+
152
+ def _choose_mode(mock: bool, yes: bool) -> bool:
153
+ """Decide between a real and a mock-mode entry.
154
+
155
+ Args:
156
+ mock: ``--mock`` was given.
157
+ yes: Don't ask; with no SolidWorks found, still write a real entry.
158
+
159
+ Returns:
160
+ Whether to write a mock-mode entry.
161
+ """
162
+ if mock:
163
+ console.print("Mock mode: the server answers without SolidWorks.")
164
+ return True
165
+ years = installed_versions()
166
+ if years:
167
+ found = ", ".join(f"SOLIDWORKS {year}" for year in years)
168
+ console.print(f"Found {found}. The server connects to the running SolidWorks.")
169
+ return False
170
+ console.print("[yellow]No SolidWorks install was found in the registry.[/yellow]")
171
+ if yes:
172
+ return False
173
+ return Confirm.ask(
174
+ "Set up mock mode instead (for trying the tools without SolidWorks)?"
175
+ )
176
+
177
+
178
+ def _choose_clients(
179
+ clients: list[Client], keys: list[str] | None, yes: bool, command: str
180
+ ) -> list[Client]:
181
+ """Pick the clients to configure: by ``--client``, ``--yes`` or a prompt.
182
+
183
+ Args:
184
+ clients: Every supported client.
185
+ keys: Keys from ``--client``, if any.
186
+ yes: Take every client found, without asking.
187
+ command: The launcher this run will write, for the status column.
188
+
189
+ Returns:
190
+ The chosen clients, in the order offered.
191
+
192
+ Raises:
193
+ typer.BadParameter: An unknown key, or an answer that isn't a client number.
194
+ """
195
+ if keys:
196
+ by_key = {c.key: c for c in clients}
197
+ unknown = [k for k in keys if k not in by_key]
198
+ if unknown:
199
+ raise typer.BadParameter(
200
+ f"Unknown client {', '.join(unknown)}; choose from {', '.join(by_key)}.",
201
+ param_hint="--client",
202
+ )
203
+ return [by_key[k] for k in keys]
204
+
205
+ table = Table(show_header=True, header_style="bold")
206
+ for column in ("#", "Client", "Status"):
207
+ table.add_column(column)
208
+ for number, target in enumerate(clients, 1):
209
+ table.add_row(str(number), target.name, _status(target, command))
210
+ console.print(table)
211
+
212
+ if yes:
213
+ return [c for c in clients if c.found]
214
+ found = [str(n) for n, c in enumerate(clients, 1) if c.found]
215
+ answer = Prompt.ask(
216
+ "Which clients? Numbers separated by commas, or 'all'",
217
+ default=",".join(found) or "all",
218
+ )
219
+ return _parse_choice(answer, clients)
220
+
221
+
222
+ def _parse_choice(answer: str, clients: list[Client]) -> list[Client]:
223
+ """Turn an answer like ``1, 3`` or ``all`` into clients.
224
+
225
+ Args:
226
+ answer: What the user typed.
227
+ clients: The numbered clients (numbered from 1).
228
+
229
+ Returns:
230
+ The chosen clients, each once, in the order typed.
231
+
232
+ Raises:
233
+ typer.BadParameter: A part that isn't a listed number.
234
+ """
235
+ if answer.strip().lower() == "all":
236
+ return list(clients)
237
+ chosen: list[Client] = []
238
+ for part in answer.replace(" ", "").split(","):
239
+ if not part:
240
+ continue
241
+ if not (part.isdigit() and 1 <= int(part) <= len(clients)):
242
+ raise typer.BadParameter(f"'{part}' is not a client number.")
243
+ target = clients[int(part) - 1]
244
+ if target not in chosen:
245
+ chosen.append(target)
246
+ return chosen
247
+
248
+
249
+ def _status(target: Client, command: str) -> str:
250
+ """Describe a client for the table: found or not, and its config state.
251
+
252
+ The only value shown from the file is the ``solidworks`` entry's own
253
+ command, and only when it runs a different copy than ``command``.
254
+
255
+ Args:
256
+ target: The client.
257
+ command: The launcher this setup run will write.
258
+
259
+ Returns:
260
+ One rich-formatted line.
261
+ """
262
+ if not target.found:
263
+ return "[dim]not found[/dim]"
264
+ if target.config_path is None:
265
+ return "found (uses the claude command)"
266
+ where = escape(str(target.config_path))
267
+ if not target.config_path.exists():
268
+ return f"found; will create {where}"
269
+ try:
270
+ entry = current_entry(target.config_path, target.section, SERVER_NAME)
271
+ except ConfigFileError:
272
+ return f"[red]config is not valid JSON[/red]: {where}"
273
+ if entry is None:
274
+ return f"config found: {where}"
275
+ current = entry.get("command") if isinstance(entry, dict) else None
276
+ if isinstance(current, str) and current != command:
277
+ # e.g. a test copy's .venv: deleting that folder would break the client.
278
+ return (
279
+ f"has a solidworks entry: {where}\n"
280
+ f"[yellow]it runs another copy: {escape(current)}[/yellow]"
281
+ )
282
+ return f"has a solidworks entry: {where}"
283
+
284
+
285
+ def _run_check(command: str, args: list[str]) -> HandshakeResult:
286
+ """Run the start-up check in mock mode, whatever mode is being configured.
287
+
288
+ Mock mode proves the launcher and the stdio handshake without starting
289
+ or touching SolidWorks.
290
+
291
+ Args:
292
+ command: The launcher.
293
+ args: The arguments being written to the configs.
294
+
295
+ Returns:
296
+ The check's outcome.
297
+ """
298
+ check_args = args if "--mock" in args else [*args, "--mock"]
299
+ return check_server(command, check_args)
300
+
301
+
302
+ def _configure(
303
+ target: Client,
304
+ command: str,
305
+ args: list[str],
306
+ remove: bool,
307
+ dry_run: bool,
308
+ choice: OldEntryChoice,
309
+ ) -> bool:
310
+ """Add or remove the entry for one client, then deal with older entries.
311
+
312
+ Args:
313
+ target: The client.
314
+ command: The launcher.
315
+ args: Its arguments.
316
+ remove: Remove the entry instead of adding it.
317
+ dry_run: Report what would change, and change nothing.
318
+ choice: What to do about older entries for this server.
319
+
320
+ Returns:
321
+ Whether it succeeded.
322
+ """
323
+ entry: dict[str, Any] | None = None if remove else target.entry(command, args)
324
+ registrar = registrar_for(target)
325
+ try:
326
+ change = registrar.apply(entry, dry_run=dry_run)
327
+ except ConfigFileError as exc:
328
+ console.print(f"[red]{target.name}: {exc}[/red]")
329
+ return False
330
+ console.print(f"{target.name}: {_describe(change, dry_run)}")
331
+ if isinstance(registrar, OldEntryCleaner):
332
+ return _clean_old_entries(target, registrar, choice, dry_run)
333
+ return True
334
+
335
+
336
+ def _clean_old_entries(
337
+ target: Client, cleaner: OldEntryCleaner, choice: OldEntryChoice, dry_run: bool
338
+ ) -> bool:
339
+ """Offer to remove older entries that start this same server.
340
+
341
+ Only each entry's name and launcher file name are shown; nothing else
342
+ from the file is.
343
+
344
+ Args:
345
+ target: The client.
346
+ cleaner: Its registrar, which can read and remove entries.
347
+ choice: Whether to ask, remove without asking, or only report.
348
+ dry_run: Report what would change, and change nothing.
349
+
350
+ Returns:
351
+ Whether every removal that was attempted succeeded.
352
+ """
353
+ old = cleaner.old_entries()
354
+ if not old:
355
+ return True
356
+ listed = ", ".join(f"{name} ({launcher})" for name, launcher in old)
357
+ console.print(
358
+ f" {target.name} also has older entries that start this server: {listed}. "
359
+ "With both, every tool loads twice."
360
+ )
361
+ if choice is OldEntryChoice.KEEP:
362
+ console.print(
363
+ " Kept. Run solidworks-mcp setup again with --replace-old to remove them."
364
+ )
365
+ return True
366
+ if choice is OldEntryChoice.ASK and not Confirm.ask(" Remove them?", default=True):
367
+ return True
368
+ for name, _ in old:
369
+ try:
370
+ change = cleaner.remove_entry(name, dry_run=dry_run)
371
+ except ConfigFileError as exc:
372
+ console.print(f"[red] {target.name}: {exc}[/red]")
373
+ return False
374
+ console.print(f" {_describe(change, dry_run, label=name)}")
375
+ return True
376
+
377
+
378
+ def _describe(change: EntryChange, dry_run: bool, label: str = SERVER_NAME) -> str:
379
+ """Say what happened to the entry, in one line.
380
+
381
+ Args:
382
+ change: The registrar's result.
383
+ dry_run: Whether nothing was actually changed.
384
+ label: The entry's name.
385
+
386
+ Returns:
387
+ The line, naming the backup if one was made.
388
+ """
389
+ if change.action == "not run":
390
+ return f"'claude' is not on the PATH; run: {change.location}"
391
+ verb = change.action
392
+ if dry_run and verb != "unchanged":
393
+ verb = f"would be {verb}"
394
+ line = f"{label} entry {verb} ({escape(change.location)})"
395
+ if change.previous:
396
+ line += f"; it ran {escape(change.previous)} before"
397
+ if change.backup:
398
+ line += f"; backup: {change.backup.name}"
399
+ return line
@@ -0,0 +1,157 @@
1
+ """The MCP clients setup knows, where each keeps its config, and if it's installed.
2
+
3
+ Detection only looks at whether folders and files exist; it never reads a
4
+ config file (see :mod:`.config_file` for that).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import shutil
11
+ from collections.abc import Callable, Mapping
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ # The key the server is registered under in every client.
17
+ SERVER_NAME = "solidworks"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Client:
22
+ """One MCP client and how to register a server with it.
23
+
24
+ Attributes:
25
+ key: The name used with ``--client`` (e.g. ``claude-desktop``).
26
+ name: The name shown to the user.
27
+ found: Whether the client looks installed.
28
+ config_path: Its JSON config file; ``None`` for a client configured
29
+ through its own CLI (Claude Code).
30
+ section: The object in the config that holds the servers.
31
+ stdio_type: Whether entries need ``"type": "stdio"`` (VS Code).
32
+ restart_hint: What to do after setup so the client loads the server.
33
+ """
34
+
35
+ key: str
36
+ name: str
37
+ found: bool
38
+ config_path: Path | None
39
+ section: str = "mcpServers"
40
+ stdio_type: bool = False
41
+ restart_hint: str = ""
42
+
43
+ def entry(self, command: str, args: list[str]) -> dict[str, Any]:
44
+ """Build the server entry in the shape this client expects.
45
+
46
+ Args:
47
+ command: The absolute launcher path.
48
+ args: The launcher's arguments.
49
+
50
+ Returns:
51
+ The JSON entry for the config file.
52
+ """
53
+ entry: dict[str, Any] = {"command": command, "args": args}
54
+ return {"type": "stdio", **entry} if self.stdio_type else entry
55
+
56
+
57
+ def claude_desktop_config(env: Mapping[str, str]) -> tuple[Path, bool]:
58
+ """Find Claude Desktop's config file.
59
+
60
+ Claude Desktop has kept its config in three places across releases:
61
+ ``%LOCALAPPDATA%\\Claude-Data\\claude_<id>`` (newest), the MSIX package's
62
+ ``LocalCache\\Roaming\\Claude``, and ``%APPDATA%\\Claude`` (classic). An
63
+ existing file wins, newest location first, since that is the one the app
64
+ is reading. With no file, it goes in the newest location whose folder
65
+ exists, i.e. the install that is present.
66
+
67
+ Args:
68
+ env: The environment (for ``LOCALAPPDATA`` and ``APPDATA``).
69
+
70
+ Returns:
71
+ The config file's path, and whether Claude Desktop was found.
72
+ """
73
+ local = Path(env.get("LOCALAPPDATA", ""))
74
+ roaming = Path(env.get("APPDATA", ""))
75
+ folders = [
76
+ *sorted(local.glob("Claude-Data/claude_*")),
77
+ *(
78
+ p / "LocalCache/Roaming/Claude"
79
+ for p in sorted(local.glob("Packages/Claude_*"))
80
+ ),
81
+ roaming / "Claude",
82
+ ]
83
+ files = [folder / "claude_desktop_config.json" for folder in folders]
84
+ for file in files:
85
+ if file.is_file():
86
+ return file, True
87
+ for file in files:
88
+ if file.parent.is_dir():
89
+ return file, True
90
+ return files[-1], False
91
+
92
+
93
+ def find_clients(
94
+ env: Mapping[str, str] | None = None,
95
+ which: Callable[[str], str | None] = shutil.which,
96
+ ) -> list[Client]:
97
+ """List every supported client, found or not, in the order they are offered.
98
+
99
+ Args:
100
+ env: The environment; defaults to ``os.environ``.
101
+ which: How to look up a command on the PATH (for ``claude``).
102
+
103
+ Returns:
104
+ The clients, with ``found`` and ``config_path`` filled in.
105
+ """
106
+ env = os.environ if env is None else env
107
+ home = Path(env.get("USERPROFILE") or env.get("HOME") or Path.home())
108
+ roaming = Path(env.get("APPDATA", ""))
109
+
110
+ desktop_file, desktop_found = claude_desktop_config(env)
111
+ vscode_file = roaming / "Code/User/mcp.json"
112
+ cursor_file = home / ".cursor/mcp.json"
113
+ lmstudio_file = home / ".lmstudio/mcp.json"
114
+ return [
115
+ Client(
116
+ "claude-desktop",
117
+ "Claude Desktop",
118
+ desktop_found,
119
+ desktop_file,
120
+ # Claude Desktop also saves its preferences to this file, so a copy
121
+ # still running in the tray may write over the entry; quitting it
122
+ # fully before rerunning setup avoids that.
123
+ restart_hint="Quit Claude Desktop from the system tray, then reopen it; "
124
+ "Settings > Developer then shows solidworks as running. If the entry "
125
+ "is gone, quit Claude Desktop fully and run solidworks-mcp setup again.",
126
+ ),
127
+ Client(
128
+ "claude-code",
129
+ "Claude Code",
130
+ which("claude") is not None,
131
+ None,
132
+ restart_hint="Run /mcp in a new Claude Code session.",
133
+ ),
134
+ Client(
135
+ "vscode",
136
+ "VS Code",
137
+ vscode_file.parent.is_dir(),
138
+ vscode_file,
139
+ section="servers",
140
+ stdio_type=True,
141
+ restart_hint="Run 'MCP: List Servers' from the Command Palette and start solidworks.",
142
+ ),
143
+ Client(
144
+ "cursor",
145
+ "Cursor",
146
+ cursor_file.parent.is_dir(),
147
+ cursor_file,
148
+ restart_hint="Restart Cursor and check solidworks is enabled in its MCP settings.",
149
+ ),
150
+ Client(
151
+ "lmstudio",
152
+ "LM Studio",
153
+ lmstudio_file.parent.is_dir(),
154
+ lmstudio_file,
155
+ restart_hint="Restart LM Studio and enable solidworks in the Program tab.",
156
+ ),
157
+ ]