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,37 @@
|
|
|
1
|
+
"""Custom exceptions for SolidWorks MCP Server."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SolidWorksMCPError(Exception):
|
|
5
|
+
"""Base exception for SolidWorks MCP Server errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SolidWorksConnectionError(SolidWorksMCPError):
|
|
11
|
+
"""Exception raised when SolidWorks connection fails."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SolidWorksOperationError(SolidWorksMCPError):
|
|
17
|
+
"""Exception raised when SolidWorks operation fails."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigurationError(SolidWorksMCPError):
|
|
23
|
+
"""Exception raised for configuration issues."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuthenticationError(SolidWorksMCPError):
|
|
29
|
+
"""Exception raised for authentication failures."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RateLimitError(SolidWorksMCPError):
|
|
35
|
+
"""Exception raised when rate limit is exceeded."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Security module for SolidWorks MCP Server.
|
|
2
|
+
|
|
3
|
+
Provides authentication, authorization, and security features based on the configured
|
|
4
|
+
security level.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..config import SolidWorksMCPConfig
|
|
10
|
+
from .auth import setup_authentication, validate_api_key
|
|
11
|
+
from .cors import setup_cors
|
|
12
|
+
from .rate_limiting import setup_rate_limiting
|
|
13
|
+
from .runtime import SecurityEnforcer
|
|
14
|
+
|
|
15
|
+
_security_enforcer: SecurityEnforcer | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_security_enforcer() -> SecurityEnforcer | None:
|
|
19
|
+
"""Return the active runtime security enforcer, if configured.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
SecurityEnforcer | None: The result produced by the operation.
|
|
23
|
+
"""
|
|
24
|
+
return _security_enforcer
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def setup_security(mcp: Any, config: SolidWorksMCPConfig) -> None:
|
|
28
|
+
"""Configure security middleware based on selected security level.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
mcp (Any): The mcp value.
|
|
32
|
+
config (SolidWorksMCPConfig): Configuration values for the operation.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
None: None.
|
|
36
|
+
"""
|
|
37
|
+
from ..config import SecurityLevel
|
|
38
|
+
|
|
39
|
+
if config.security_level == SecurityLevel.MINIMAL:
|
|
40
|
+
# Local only, minimal security
|
|
41
|
+
global _security_enforcer
|
|
42
|
+
_security_enforcer = SecurityEnforcer(config)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
if config.security_level in [SecurityLevel.STANDARD, SecurityLevel.STRICT]:
|
|
46
|
+
# Setup API key authentication
|
|
47
|
+
if config.api_key:
|
|
48
|
+
setup_authentication(mcp, config)
|
|
49
|
+
|
|
50
|
+
# Setup CORS
|
|
51
|
+
if config.enable_cors:
|
|
52
|
+
setup_cors(mcp, config)
|
|
53
|
+
|
|
54
|
+
# Setup rate limiting
|
|
55
|
+
if config.enable_rate_limiting:
|
|
56
|
+
setup_rate_limiting(mcp, config)
|
|
57
|
+
|
|
58
|
+
_security_enforcer = SecurityEnforcer(config)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"setup_security",
|
|
63
|
+
"setup_authentication",
|
|
64
|
+
"validate_api_key",
|
|
65
|
+
"setup_cors",
|
|
66
|
+
"setup_rate_limiting",
|
|
67
|
+
"get_security_enforcer",
|
|
68
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Authentication and authorization for SolidWorks MCP Server."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Any, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
from ..config import SolidWorksMCPConfig
|
|
9
|
+
|
|
10
|
+
F = TypeVar("F", bound=Callable[..., Awaitable[Any]])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_authentication(mcp: Any, config: SolidWorksMCPConfig) -> None:
|
|
14
|
+
"""Configure authentication middleware hooks.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
mcp (Any): The mcp value.
|
|
18
|
+
config (SolidWorksMCPConfig): Configuration values for the operation.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
None: None.
|
|
22
|
+
"""
|
|
23
|
+
api_key = getattr(config, "api_key", None)
|
|
24
|
+
api_keys = getattr(config, "api_keys", [])
|
|
25
|
+
api_key_required = bool(getattr(config, "api_key_required", False))
|
|
26
|
+
auth_mode = "api_key" if (api_key or api_keys or api_key_required) else "none"
|
|
27
|
+
try:
|
|
28
|
+
mcp._security_auth_enabled = True
|
|
29
|
+
mcp._security_auth_mode = auth_mode
|
|
30
|
+
except (AttributeError, TypeError):
|
|
31
|
+
# Some tests intentionally pass plain object() instances without __dict__.
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_api_key(provided_key: str, expected_key: str) -> bool:
|
|
36
|
+
"""Validate API key using constant-time comparison.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
provided_key (str): The provided key value.
|
|
40
|
+
expected_key (str): The expected key value.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
bool: True if validate api key, otherwise False.
|
|
44
|
+
"""
|
|
45
|
+
if not provided_key or not expected_key:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
return secrets.compare_digest(provided_key, expected_key)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def require_auth(config: SolidWorksMCPConfig) -> Callable[[F], F]:
|
|
52
|
+
"""Decorate a coroutine with authentication checks.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config (SolidWorksMCPConfig): Configuration values for the operation.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Callable[[F], F]: The result produced by the operation.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def decorator(func: F) -> F:
|
|
62
|
+
"""Wrap a coroutine with request-level authentication checks.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
func (F): The func value.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
F: The result produced by the operation.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
RuntimeError: Authentication failed: invalid api_key.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
@wraps(func)
|
|
75
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
76
|
+
"""Run the wrapped coroutine after API key validation.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
*args (Any): Additional positional arguments forwarded to the call.
|
|
80
|
+
**kwargs (Any): Additional keyword arguments forwarded to the call.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Any: The result produced by the operation.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
RuntimeError: Authentication failed: invalid api_key.
|
|
87
|
+
"""
|
|
88
|
+
security_level = str(getattr(config, "security_level", "minimal"))
|
|
89
|
+
if security_level == "minimal":
|
|
90
|
+
return await func(*args, **kwargs)
|
|
91
|
+
|
|
92
|
+
api_key_required = bool(getattr(config, "api_key_required", False))
|
|
93
|
+
api_key = getattr(config, "api_key", None)
|
|
94
|
+
api_keys = getattr(config, "api_keys", [])
|
|
95
|
+
if not (api_key_required or api_key or api_keys):
|
|
96
|
+
return await func(*args, **kwargs)
|
|
97
|
+
|
|
98
|
+
payload = kwargs.get("input_data")
|
|
99
|
+
if payload is None and args:
|
|
100
|
+
payload = args[0]
|
|
101
|
+
|
|
102
|
+
payload_dict: dict[str, Any] = {}
|
|
103
|
+
if payload is not None and hasattr(payload, "model_dump"):
|
|
104
|
+
payload_dict = cast(dict[str, Any], payload.model_dump())
|
|
105
|
+
elif isinstance(payload, dict):
|
|
106
|
+
payload_dict = payload
|
|
107
|
+
|
|
108
|
+
provided_key_value = payload_dict.get("api_key")
|
|
109
|
+
provided_key = str(provided_key_value) if provided_key_value else ""
|
|
110
|
+
|
|
111
|
+
expected_key = ""
|
|
112
|
+
if api_key is not None and hasattr(api_key, "get_secret_value"):
|
|
113
|
+
expected_key = api_key.get_secret_value()
|
|
114
|
+
elif isinstance(api_key, str):
|
|
115
|
+
expected_key = api_key
|
|
116
|
+
elif api_keys:
|
|
117
|
+
expected_key = api_keys[0]
|
|
118
|
+
|
|
119
|
+
if not validate_api_key(
|
|
120
|
+
provided_key=provided_key, expected_key=expected_key
|
|
121
|
+
):
|
|
122
|
+
raise RuntimeError("authentication failed: invalid api_key")
|
|
123
|
+
|
|
124
|
+
return await func(*args, **kwargs)
|
|
125
|
+
|
|
126
|
+
return wrapper # type: ignore[return-value]
|
|
127
|
+
|
|
128
|
+
return decorator
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""CORS (Cross-Origin Resource Sharing) configuration for remote deployments."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..config import SolidWorksMCPConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setup_cors(mcp: Any, config: SolidWorksMCPConfig) -> None:
|
|
9
|
+
"""Configure CORS middleware for remote deployments.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
mcp (Any): The mcp value.
|
|
13
|
+
config (SolidWorksMCPConfig): Configuration values for the operation.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
None: None.
|
|
17
|
+
"""
|
|
18
|
+
cors_origins = getattr(config, "cors_origins", [])
|
|
19
|
+
allowed_origins = getattr(config, "allowed_origins", [])
|
|
20
|
+
enable_cors = bool(getattr(config, "enable_cors", False))
|
|
21
|
+
origins = cors_origins or allowed_origins
|
|
22
|
+
try:
|
|
23
|
+
mcp._security_cors_enabled = enable_cors
|
|
24
|
+
mcp._security_cors_origins = list(origins)
|
|
25
|
+
except (AttributeError, TypeError):
|
|
26
|
+
# Some tests intentionally pass plain object() instances without __dict__.
|
|
27
|
+
return
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Rate limiting for API endpoints."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..config import SolidWorksMCPConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RateLimiter:
|
|
11
|
+
"""Simple in-memory rate limiter.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
max_requests (int): The max requests value.
|
|
15
|
+
time_window (int): The time window value. Defaults to 60.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
max_requests (Any): The max requests value.
|
|
19
|
+
time_window (Any): The time window value.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, max_requests: int, time_window: int = 60):
|
|
23
|
+
"""Initialize the rate limiter.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
max_requests (int): The max requests value.
|
|
27
|
+
time_window (int): The time window value. Defaults to 60.
|
|
28
|
+
"""
|
|
29
|
+
self.max_requests = max_requests
|
|
30
|
+
self.time_window = time_window
|
|
31
|
+
self.requests: dict[str, list[float]] = defaultdict(list)
|
|
32
|
+
|
|
33
|
+
def is_allowed(self, client_id: str) -> bool:
|
|
34
|
+
"""Check if client is within rate limits.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
client_id (str): The client id value.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
bool: True if allowed, otherwise False.
|
|
41
|
+
"""
|
|
42
|
+
now = time.time()
|
|
43
|
+
window_start = now - self.time_window
|
|
44
|
+
|
|
45
|
+
# Remove old requests
|
|
46
|
+
self.requests[client_id] = [
|
|
47
|
+
req_time for req_time in self.requests[client_id] if req_time > window_start
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Check if under limit
|
|
51
|
+
if len(self.requests[client_id]) < self.max_requests:
|
|
52
|
+
self.requests[client_id].append(now)
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Global rate limiter instance
|
|
59
|
+
_rate_limiter: RateLimiter | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def setup_rate_limiting(mcp: Any, config: SolidWorksMCPConfig) -> None:
|
|
63
|
+
"""Initialize in-memory rate limiting.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
mcp (Any): The mcp value.
|
|
67
|
+
config (SolidWorksMCPConfig): Configuration values for the operation.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
None: None.
|
|
71
|
+
"""
|
|
72
|
+
global _rate_limiter
|
|
73
|
+
_rate_limiter = RateLimiter(
|
|
74
|
+
max_requests=config.rate_limit_per_minute,
|
|
75
|
+
time_window=60, # 1 minute
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def check_rate_limit(client_id: str) -> bool:
|
|
80
|
+
"""Check if client is within rate limits.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
client_id (str): The client id value.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
bool: True if check rate limit, otherwise False.
|
|
87
|
+
"""
|
|
88
|
+
if _rate_limiter is None:
|
|
89
|
+
return True
|
|
90
|
+
return _rate_limiter.is_allowed(client_id)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Runtime security enforcement helpers for tool invocations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import secrets
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from ..config import SecurityLevel, SolidWorksMCPConfig
|
|
10
|
+
from .auth import validate_api_key
|
|
11
|
+
from .rate_limiting import check_rate_limit
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SecurityError(RuntimeError):
|
|
15
|
+
"""Raised when a request violates configured security policies."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class SecurityContext:
|
|
20
|
+
"""Extracted invocation security context.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
api_key (str | None): The api key value.
|
|
24
|
+
client_id (str): The client id value.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
client_id: str
|
|
28
|
+
api_key: str | None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SecurityEnforcer:
|
|
32
|
+
"""Enforce authentication and rate-limit policies at runtime.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config (SolidWorksMCPConfig): Configuration values for the operation.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
_config (Any): The config value.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, config: SolidWorksMCPConfig) -> None:
|
|
42
|
+
"""Initialize the security enforcer.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
config (SolidWorksMCPConfig): Configuration values for the operation.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
None: None.
|
|
49
|
+
"""
|
|
50
|
+
self._config = config
|
|
51
|
+
|
|
52
|
+
def enforce(self, tool_name: str, payload: object) -> None:
|
|
53
|
+
"""Validate invocation against runtime security policy.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
tool_name (str): The tool name value.
|
|
57
|
+
payload (object): The payload value.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
None: None.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
SecurityError: Authentication failed: invalid api_key.
|
|
64
|
+
"""
|
|
65
|
+
context = self._extract_context(payload)
|
|
66
|
+
|
|
67
|
+
if self._config.enable_rate_limiting and not check_rate_limit(
|
|
68
|
+
context.client_id
|
|
69
|
+
):
|
|
70
|
+
raise SecurityError(
|
|
71
|
+
f"rate limit exceeded for client '{context.client_id}' while calling '{tool_name}'"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not self._is_auth_required():
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
expected_key = self._expected_api_key()
|
|
78
|
+
if not expected_key:
|
|
79
|
+
raise SecurityError("authentication required but no API key configured")
|
|
80
|
+
|
|
81
|
+
provided_key = context.api_key
|
|
82
|
+
if provided_key is None:
|
|
83
|
+
raise SecurityError("authentication required but api_key was not provided")
|
|
84
|
+
|
|
85
|
+
if not validate_api_key(provided_key=provided_key, expected_key=expected_key):
|
|
86
|
+
raise SecurityError("authentication failed: invalid api_key")
|
|
87
|
+
|
|
88
|
+
def _extract_context(self, payload: object) -> SecurityContext:
|
|
89
|
+
"""Extract client and auth information from payload object.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
payload (object): The payload value.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
SecurityContext: The result produced by the operation.
|
|
96
|
+
"""
|
|
97
|
+
if hasattr(payload, "model_dump"):
|
|
98
|
+
payload_dict = payload.model_dump()
|
|
99
|
+
elif isinstance(payload, dict):
|
|
100
|
+
payload_dict = payload
|
|
101
|
+
else:
|
|
102
|
+
payload_dict = {}
|
|
103
|
+
|
|
104
|
+
client_id_raw = payload_dict.get("client_id", "anonymous")
|
|
105
|
+
client_id = str(client_id_raw).strip() if client_id_raw else "anonymous"
|
|
106
|
+
|
|
107
|
+
api_key_raw = payload_dict.get("api_key")
|
|
108
|
+
api_key = str(api_key_raw) if api_key_raw is not None else None
|
|
109
|
+
|
|
110
|
+
return SecurityContext(client_id=client_id, api_key=api_key)
|
|
111
|
+
|
|
112
|
+
def _is_auth_required(self) -> bool:
|
|
113
|
+
"""Return whether API key validation should be enforced.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
bool: True if auth required, otherwise False.
|
|
117
|
+
"""
|
|
118
|
+
if bool(getattr(self._config, "api_key_required", False)):
|
|
119
|
+
return True
|
|
120
|
+
if getattr(self._config, "security_level", None) == SecurityLevel.STRICT:
|
|
121
|
+
return True
|
|
122
|
+
return getattr(self._config, "api_key", None) is not None or bool(
|
|
123
|
+
getattr(self._config, "api_keys", [])
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _expected_api_key(self) -> str | None:
|
|
127
|
+
"""Return configured expected API key for validation.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
str | None: The result produced by the operation.
|
|
131
|
+
"""
|
|
132
|
+
api_key = getattr(self._config, "api_key", None)
|
|
133
|
+
if api_key is not None:
|
|
134
|
+
get_secret_value = getattr(api_key, "get_secret_value", None)
|
|
135
|
+
if callable(get_secret_value):
|
|
136
|
+
return cast("str | None", get_secret_value())
|
|
137
|
+
return cast(str, api_key)
|
|
138
|
+
|
|
139
|
+
api_keys = getattr(self._config, "api_keys", [])
|
|
140
|
+
if api_keys:
|
|
141
|
+
return cast("str | None", api_keys[0])
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def constant_time_equals(left: str, right: str) -> bool:
|
|
146
|
+
"""Compare two strings in constant time.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
left (str): The left value.
|
|
150
|
+
right (str): The right value.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
bool: True if constant time equals, otherwise False.
|
|
154
|
+
"""
|
|
155
|
+
return secrets.compare_digest(left, right)
|