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