mcp-proxy-adapter 4.1.1__py3-none-any.whl → 6.0.1__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.
- mcp_proxy_adapter/__main__.py +32 -0
- mcp_proxy_adapter/api/app.py +290 -33
- mcp_proxy_adapter/api/handlers.py +32 -6
- mcp_proxy_adapter/api/middleware/__init__.py +38 -32
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
- mcp_proxy_adapter/api/middleware/factory.py +243 -0
- mcp_proxy_adapter/api/middleware/logging.py +32 -6
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +201 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
- mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
- mcp_proxy_adapter/commands/__init__.py +19 -4
- mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
- mcp_proxy_adapter/commands/base.py +66 -32
- mcp_proxy_adapter/commands/builtin_commands.py +95 -0
- mcp_proxy_adapter/commands/catalog_manager.py +838 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
- mcp_proxy_adapter/commands/command_registry.py +711 -354
- mcp_proxy_adapter/commands/dependency_manager.py +245 -0
- mcp_proxy_adapter/commands/echo_command.py +81 -0
- mcp_proxy_adapter/commands/health_command.py +8 -1
- mcp_proxy_adapter/commands/help_command.py +21 -14
- mcp_proxy_adapter/commands/hooks.py +200 -167
- mcp_proxy_adapter/commands/key_management_command.py +506 -0
- mcp_proxy_adapter/commands/load_command.py +176 -0
- mcp_proxy_adapter/commands/plugins_command.py +235 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
- mcp_proxy_adapter/commands/reload_command.py +48 -50
- mcp_proxy_adapter/commands/result.py +1 -0
- mcp_proxy_adapter/commands/role_test_command.py +141 -0
- mcp_proxy_adapter/commands/roles_management_command.py +697 -0
- mcp_proxy_adapter/commands/security_command.py +488 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +366 -0
- mcp_proxy_adapter/commands/token_management_command.py +529 -0
- mcp_proxy_adapter/commands/transport_management_command.py +144 -0
- mcp_proxy_adapter/commands/unload_command.py +158 -0
- mcp_proxy_adapter/config.py +394 -14
- mcp_proxy_adapter/core/app_factory.py +410 -0
- mcp_proxy_adapter/core/app_runner.py +272 -0
- mcp_proxy_adapter/core/auth_validator.py +606 -0
- mcp_proxy_adapter/core/certificate_utils.py +1045 -0
- mcp_proxy_adapter/core/client.py +574 -0
- mcp_proxy_adapter/core/client_manager.py +284 -0
- mcp_proxy_adapter/core/client_security.py +384 -0
- mcp_proxy_adapter/core/config_converter.py +405 -0
- mcp_proxy_adapter/core/config_validator.py +218 -0
- mcp_proxy_adapter/core/logging.py +19 -3
- mcp_proxy_adapter/core/mtls_asgi.py +156 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/protocol_manager.py +385 -0
- mcp_proxy_adapter/core/proxy_client.py +602 -0
- mcp_proxy_adapter/core/proxy_registration.py +522 -0
- mcp_proxy_adapter/core/role_utils.py +426 -0
- mcp_proxy_adapter/core/security_adapter.py +370 -0
- mcp_proxy_adapter/core/security_factory.py +239 -0
- mcp_proxy_adapter/core/security_integration.py +286 -0
- mcp_proxy_adapter/core/server_adapter.py +282 -0
- mcp_proxy_adapter/core/server_engine.py +270 -0
- mcp_proxy_adapter/core/settings.py +1 -0
- mcp_proxy_adapter/core/ssl_utils.py +234 -0
- mcp_proxy_adapter/core/transport_manager.py +292 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/custom_openapi.py +22 -11
- mcp_proxy_adapter/examples/__init__.py +13 -4
- mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/commands/__init__.py +5 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
- mcp_proxy_adapter/examples/debug_request_state.py +112 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
- mcp_proxy_adapter/examples/demo_client.py +275 -0
- mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
- mcp_proxy_adapter/examples/generate_certificates.py +177 -0
- mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
- mcp_proxy_adapter/examples/run_example.py +59 -0
- mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
- mcp_proxy_adapter/examples/run_security_tests.py +544 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
- mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
- mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
- mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/security_test_client.py +782 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
- mcp_proxy_adapter/examples/test_config.py +148 -0
- mcp_proxy_adapter/examples/test_config_generator.py +86 -0
- mcp_proxy_adapter/examples/test_examples.py +281 -0
- mcp_proxy_adapter/examples/universal_client.py +620 -0
- mcp_proxy_adapter/main.py +93 -0
- mcp_proxy_adapter/utils/config_generator.py +1008 -0
- mcp_proxy_adapter/version.py +5 -2
- mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
- mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
- mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
- mcp_proxy_adapter/api/middleware/auth.py +0 -146
- mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
- mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
- mcp_proxy_adapter/examples/README.md +0 -124
- mcp_proxy_adapter/examples/basic_server/README.md +0 -60
- mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
- mcp_proxy_adapter/examples/basic_server/config.json +0 -35
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
- mcp_proxy_adapter/examples/basic_server/server.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
- mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
- mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
- mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
- mcp_proxy_adapter/examples/deployment/README.md +0 -49
- mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
- mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
- mcp_proxy_adapter/examples/deployment/config.json +0 -29
- mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
- mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
- mcp_proxy_adapter/examples/deployment/run.sh +0 -43
- mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
- mcp_proxy_adapter/schemas/base_schema.json +0 -114
- mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +0 -3
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
- mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
- mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
- mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
- mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
- mcp_proxy_adapter/tests/commands/__init__.py +0 -3
- mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
- mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
- mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
- mcp_proxy_adapter/tests/conftest.py +0 -131
- mcp_proxy_adapter/tests/functional/__init__.py +0 -3
- mcp_proxy_adapter/tests/functional/test_api.py +0 -253
- mcp_proxy_adapter/tests/integration/__init__.py +0 -3
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
- mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
- mcp_proxy_adapter/tests/performance/__init__.py +0 -3
- mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
- mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
- mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
- mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
- mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
- mcp_proxy_adapter/tests/test_base_command.py +0 -123
- mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
- mcp_proxy_adapter/tests/test_command_registry.py +0 -281
- mcp_proxy_adapter/tests/test_config.py +0 -127
- mcp_proxy_adapter/tests/test_utils.py +0 -65
- mcp_proxy_adapter/tests/unit/__init__.py +0 -3
- mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
- mcp_proxy_adapter/tests/unit/test_config.py +0 -217
- mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
- mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,579 @@
|
|
1
|
+
"""
|
2
|
+
Unified Configuration Adapter for mcp_security_framework integration.
|
3
|
+
|
4
|
+
Author: Vasiliy Zdanovskiy
|
5
|
+
email: vasilyvz@gmail.com
|
6
|
+
|
7
|
+
This module provides a unified adapter for converting mcp_proxy_adapter configuration
|
8
|
+
to SecurityConfig format used by mcp_security_framework.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import json
|
12
|
+
import logging
|
13
|
+
from typing import Dict, Any, Optional, List, Union
|
14
|
+
from pathlib import Path
|
15
|
+
from dataclasses import dataclass
|
16
|
+
|
17
|
+
# Import mcp_security_framework components
|
18
|
+
try:
|
19
|
+
from mcp_security_framework import SecurityConfig
|
20
|
+
from mcp_security_framework.schemas.config import (
|
21
|
+
AuthConfig, SSLConfig, PermissionConfig, RateLimitConfig
|
22
|
+
)
|
23
|
+
SECURITY_FRAMEWORK_AVAILABLE = True
|
24
|
+
except ImportError:
|
25
|
+
SECURITY_FRAMEWORK_AVAILABLE = False
|
26
|
+
SecurityConfig = None
|
27
|
+
AuthConfig = None
|
28
|
+
SSLConfig = None
|
29
|
+
PermissionConfig = None
|
30
|
+
RateLimitConfig = None
|
31
|
+
|
32
|
+
from mcp_proxy_adapter.core.logging import logger
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class ValidationResult:
|
37
|
+
"""Result of configuration validation."""
|
38
|
+
|
39
|
+
is_valid: bool
|
40
|
+
errors: List[str]
|
41
|
+
warnings: List[str]
|
42
|
+
details: Dict[str, Any]
|
43
|
+
|
44
|
+
def __post_init__(self):
|
45
|
+
"""Initialize with empty lists if None."""
|
46
|
+
if self.errors is None:
|
47
|
+
self.errors = []
|
48
|
+
if self.warnings is None:
|
49
|
+
self.warnings = []
|
50
|
+
if self.details is None:
|
51
|
+
self.details = {}
|
52
|
+
|
53
|
+
|
54
|
+
class UnifiedConfigAdapter:
|
55
|
+
"""
|
56
|
+
Unified adapter for converting mcp_proxy_adapter configuration to SecurityConfig.
|
57
|
+
|
58
|
+
This adapter handles:
|
59
|
+
- Legacy configuration format compatibility
|
60
|
+
- Configuration validation
|
61
|
+
- Conversion to mcp_security_framework format
|
62
|
+
- Default value management
|
63
|
+
"""
|
64
|
+
|
65
|
+
def __init__(self):
|
66
|
+
"""Initialize the unified configuration adapter."""
|
67
|
+
self.validation_errors = []
|
68
|
+
self.validation_warnings = []
|
69
|
+
|
70
|
+
def convert_to_security_config(self, config: Dict[str, Any]) -> Optional[SecurityConfig]:
|
71
|
+
"""
|
72
|
+
Convert mcp_proxy_adapter configuration to SecurityConfig.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
config: mcp_proxy_adapter configuration dictionary
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
SecurityConfig instance or None if conversion failed
|
79
|
+
"""
|
80
|
+
if not SECURITY_FRAMEWORK_AVAILABLE:
|
81
|
+
logger.error("mcp_security_framework not available, cannot convert configuration")
|
82
|
+
return None
|
83
|
+
|
84
|
+
try:
|
85
|
+
# Validate configuration first
|
86
|
+
validation_result = self.validate_configuration(config)
|
87
|
+
if not validation_result.is_valid:
|
88
|
+
logger.error(f"Configuration validation failed: {validation_result.errors}")
|
89
|
+
return None
|
90
|
+
|
91
|
+
# Convert configuration sections
|
92
|
+
auth_config = self._convert_auth_config(config)
|
93
|
+
ssl_config = self._convert_ssl_config(config)
|
94
|
+
permission_config = self._convert_permission_config(config)
|
95
|
+
rate_limit_config = self._convert_rate_limit_config(config)
|
96
|
+
|
97
|
+
# Create SecurityConfig
|
98
|
+
security_config = SecurityConfig(
|
99
|
+
auth=auth_config,
|
100
|
+
ssl=ssl_config,
|
101
|
+
permissions=permission_config,
|
102
|
+
rate_limit=rate_limit_config
|
103
|
+
)
|
104
|
+
|
105
|
+
logger.info("Configuration successfully converted to SecurityConfig")
|
106
|
+
return security_config
|
107
|
+
|
108
|
+
except Exception as e:
|
109
|
+
logger.error(f"Failed to convert configuration: {e}")
|
110
|
+
return None
|
111
|
+
|
112
|
+
def validate_configuration(self, config: Dict[str, Any]) -> ValidationResult:
|
113
|
+
"""
|
114
|
+
Validate configuration before conversion.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
config: Configuration dictionary to validate
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
ValidationResult with validation details
|
121
|
+
"""
|
122
|
+
self.validation_errors = []
|
123
|
+
self.validation_warnings = []
|
124
|
+
|
125
|
+
# Debug: Check SSL config at start of validation
|
126
|
+
if "security" in config:
|
127
|
+
ssl_config = config["security"].get("ssl", {})
|
128
|
+
print(f"🔍 Debug: SSL config at start of validation: enabled={ssl_config.get('enabled', False)}")
|
129
|
+
|
130
|
+
# Debug: Check if root ssl section exists
|
131
|
+
if "ssl" in config:
|
132
|
+
print(f"🔍 Debug: Root SSL section found: enabled={config['ssl'].get('enabled', False)}")
|
133
|
+
|
134
|
+
# Check if config is a dictionary
|
135
|
+
if not isinstance(config, dict):
|
136
|
+
return ValidationResult(
|
137
|
+
is_valid=False,
|
138
|
+
errors=["Configuration must be a dictionary"],
|
139
|
+
warnings=[],
|
140
|
+
details={}
|
141
|
+
)
|
142
|
+
|
143
|
+
# Validate security section
|
144
|
+
self._validate_security_section(config)
|
145
|
+
|
146
|
+
# Validate legacy sections for compatibility
|
147
|
+
self._validate_legacy_sections(config)
|
148
|
+
|
149
|
+
# Check for conflicts
|
150
|
+
self._check_configuration_conflicts(config)
|
151
|
+
|
152
|
+
# Validate individual sections
|
153
|
+
self._validate_auth_section(config)
|
154
|
+
self._validate_ssl_section(config)
|
155
|
+
self._validate_permissions_section(config)
|
156
|
+
self._validate_rate_limit_section(config)
|
157
|
+
|
158
|
+
return ValidationResult(
|
159
|
+
is_valid=len(self.validation_errors) == 0,
|
160
|
+
errors=self.validation_errors.copy(),
|
161
|
+
warnings=self.validation_warnings.copy(),
|
162
|
+
details={
|
163
|
+
"has_security_section": "security" in config,
|
164
|
+
"has_legacy_sections": any(key in config for key in ["ssl", "roles", "auth_enabled"]),
|
165
|
+
"total_errors": len(self.validation_errors),
|
166
|
+
"total_warnings": len(self.validation_warnings)
|
167
|
+
}
|
168
|
+
)
|
169
|
+
|
170
|
+
def _validate_security_section(self, config: Dict[str, Any]):
|
171
|
+
"""Validate security section."""
|
172
|
+
security_config = config.get("security", {})
|
173
|
+
|
174
|
+
if not isinstance(security_config, dict):
|
175
|
+
self.validation_errors.append("Security section must be a dictionary")
|
176
|
+
return
|
177
|
+
|
178
|
+
# Check for unknown keys in security section
|
179
|
+
known_keys = {"enabled", "auth", "ssl", "permissions", "rate_limit", "public_paths"}
|
180
|
+
unknown_keys = set(security_config.keys()) - known_keys
|
181
|
+
|
182
|
+
if unknown_keys:
|
183
|
+
self.validation_warnings.append(f"Unknown keys in security section: {unknown_keys}")
|
184
|
+
|
185
|
+
def _validate_legacy_sections(self, config: Dict[str, Any]):
|
186
|
+
"""Validate legacy configuration sections."""
|
187
|
+
legacy_sections = ["ssl", "roles", "auth_enabled", "rate_limit_enabled"]
|
188
|
+
|
189
|
+
for section in legacy_sections:
|
190
|
+
if section in config:
|
191
|
+
self.validation_warnings.append(f"Legacy section '{section}' found, consider migrating to security section")
|
192
|
+
|
193
|
+
def _check_configuration_conflicts(self, config: Dict[str, Any]):
|
194
|
+
"""Check for configuration conflicts."""
|
195
|
+
security_config = config.get("security", {})
|
196
|
+
|
197
|
+
# Check for SSL configuration conflicts
|
198
|
+
if "ssl" in config and "ssl" in security_config:
|
199
|
+
self.validation_warnings.append("SSL configuration found in both root and security sections")
|
200
|
+
|
201
|
+
# Check for auth configuration conflicts
|
202
|
+
if "auth_enabled" in config and "auth" in security_config:
|
203
|
+
self.validation_warnings.append("Auth configuration found in both root and security sections")
|
204
|
+
|
205
|
+
def _validate_auth_section(self, config: Dict[str, Any]):
|
206
|
+
"""Validate authentication section."""
|
207
|
+
auth_config = self._get_auth_config(config)
|
208
|
+
|
209
|
+
if not isinstance(auth_config, dict):
|
210
|
+
self.validation_errors.append("Auth configuration must be a dictionary")
|
211
|
+
return
|
212
|
+
|
213
|
+
# Validate auth methods
|
214
|
+
methods = auth_config.get("methods", [])
|
215
|
+
if not isinstance(methods, list):
|
216
|
+
self.validation_errors.append("Auth methods must be a list")
|
217
|
+
else:
|
218
|
+
valid_methods = {"api_key", "jwt", "certificate"}
|
219
|
+
invalid_methods = set(methods) - valid_methods
|
220
|
+
if invalid_methods:
|
221
|
+
self.validation_errors.append(f"Invalid auth methods: {invalid_methods}")
|
222
|
+
|
223
|
+
# Validate API keys
|
224
|
+
api_keys = auth_config.get("api_keys", {})
|
225
|
+
if not isinstance(api_keys, dict):
|
226
|
+
self.validation_errors.append("API keys must be a dictionary")
|
227
|
+
|
228
|
+
# Validate JWT configuration
|
229
|
+
if "jwt" in methods:
|
230
|
+
jwt_secret = auth_config.get("jwt_secret", "")
|
231
|
+
if not jwt_secret:
|
232
|
+
self.validation_warnings.append("JWT secret is empty or not set")
|
233
|
+
|
234
|
+
def _validate_ssl_section(self, config: Dict[str, Any]):
|
235
|
+
"""Validate SSL section."""
|
236
|
+
ssl_config = self._get_ssl_config(config)
|
237
|
+
|
238
|
+
if not isinstance(ssl_config, dict):
|
239
|
+
self.validation_errors.append("SSL configuration must be a dictionary")
|
240
|
+
return
|
241
|
+
|
242
|
+
# Validate certificate files
|
243
|
+
if ssl_config.get("enabled", False):
|
244
|
+
cert_file = ssl_config.get("cert_file")
|
245
|
+
key_file = ssl_config.get("key_file")
|
246
|
+
|
247
|
+
print(f"🔍 Debug: _validate_ssl_section: cert_file={cert_file}")
|
248
|
+
print(f"🔍 Debug: _validate_ssl_section: key_file={key_file}")
|
249
|
+
print(f"🔍 Debug: _validate_ssl_section: cert_file exists={Path(cert_file).exists() if cert_file else 'None'}")
|
250
|
+
print(f"🔍 Debug: _validate_ssl_section: key_file exists={Path(key_file).exists() if key_file else 'None'}")
|
251
|
+
|
252
|
+
if cert_file and not Path(cert_file).exists():
|
253
|
+
self.validation_warnings.append(f"SSL certificate file not found: {cert_file}")
|
254
|
+
|
255
|
+
if key_file and not Path(key_file).exists():
|
256
|
+
self.validation_warnings.append(f"SSL key file not found: {key_file}")
|
257
|
+
|
258
|
+
def _validate_permissions_section(self, config: Dict[str, Any]):
|
259
|
+
"""Validate permissions section."""
|
260
|
+
permissions_config = self._get_permissions_config(config)
|
261
|
+
|
262
|
+
if not isinstance(permissions_config, dict):
|
263
|
+
self.validation_errors.append("Permissions configuration must be a dictionary")
|
264
|
+
return
|
265
|
+
|
266
|
+
# Validate roles file
|
267
|
+
if permissions_config.get("enabled", False):
|
268
|
+
roles_file = permissions_config.get("roles_file")
|
269
|
+
if roles_file and not Path(roles_file).exists():
|
270
|
+
self.validation_warnings.append(f"Roles file not found: {roles_file}")
|
271
|
+
|
272
|
+
def _validate_rate_limit_section(self, config: Dict[str, Any]):
|
273
|
+
"""Validate rate limit section."""
|
274
|
+
rate_limit_config = self._get_rate_limit_config(config)
|
275
|
+
|
276
|
+
if not isinstance(rate_limit_config, dict):
|
277
|
+
self.validation_errors.append("Rate limit configuration must be a dictionary")
|
278
|
+
return
|
279
|
+
|
280
|
+
# Validate rate limit values only if enabled
|
281
|
+
if rate_limit_config.get("enabled", False):
|
282
|
+
requests_per_minute = rate_limit_config.get("requests_per_minute", rate_limit_config.get("default_requests_per_minute", 0))
|
283
|
+
if requests_per_minute <= 0:
|
284
|
+
self.validation_errors.append("requests_per_minute must be greater than 0 when rate limiting is enabled")
|
285
|
+
|
286
|
+
def _convert_auth_config(self, config: Dict[str, Any]) -> AuthConfig:
|
287
|
+
"""Convert authentication configuration."""
|
288
|
+
auth_config = self._get_auth_config(config)
|
289
|
+
|
290
|
+
# Get authentication methods
|
291
|
+
methods = auth_config.get("methods", ["api_key"])
|
292
|
+
|
293
|
+
# Get API keys from multiple sources
|
294
|
+
api_keys = auth_config.get("api_keys", {})
|
295
|
+
if not api_keys:
|
296
|
+
# Try legacy SSL config
|
297
|
+
legacy_ssl = config.get("ssl", {})
|
298
|
+
api_keys = legacy_ssl.get("api_keys", {})
|
299
|
+
|
300
|
+
return AuthConfig(
|
301
|
+
enabled=auth_config.get("enabled", True),
|
302
|
+
methods=methods,
|
303
|
+
api_keys=api_keys,
|
304
|
+
jwt_secret=auth_config.get("jwt_secret", ""),
|
305
|
+
jwt_algorithm=auth_config.get("jwt_algorithm", "HS256"),
|
306
|
+
jwt_expiration=auth_config.get("jwt_expiration", 3600)
|
307
|
+
)
|
308
|
+
|
309
|
+
def _convert_ssl_config(self, config: Dict[str, Any]) -> SSLConfig:
|
310
|
+
"""Convert SSL configuration."""
|
311
|
+
ssl_config = self._get_ssl_config(config)
|
312
|
+
|
313
|
+
return SSLConfig(
|
314
|
+
enabled=ssl_config.get("enabled", False),
|
315
|
+
cert_file=ssl_config.get("cert_file"),
|
316
|
+
key_file=ssl_config.get("key_file"),
|
317
|
+
ca_cert=ssl_config.get("ca_cert"),
|
318
|
+
min_tls_version=ssl_config.get("min_tls_version", "TLSv1.2"),
|
319
|
+
verify_client=ssl_config.get("verify_client", False),
|
320
|
+
client_cert_required=ssl_config.get("client_cert_required", False),
|
321
|
+
cipher_suites=ssl_config.get("cipher_suites", [])
|
322
|
+
)
|
323
|
+
|
324
|
+
def _convert_permission_config(self, config: Dict[str, Any]) -> PermissionConfig:
|
325
|
+
"""Convert permissions configuration."""
|
326
|
+
permissions_config = self._get_permissions_config(config)
|
327
|
+
|
328
|
+
return PermissionConfig(
|
329
|
+
enabled=permissions_config.get("enabled", True),
|
330
|
+
roles_file=permissions_config.get("roles_file", "roles.json"),
|
331
|
+
default_role=permissions_config.get("default_role", "user"),
|
332
|
+
deny_by_default=permissions_config.get("deny_by_default", True),
|
333
|
+
role_mappings=permissions_config.get("role_mappings", {})
|
334
|
+
)
|
335
|
+
|
336
|
+
def _convert_rate_limit_config(self, config: Dict[str, Any]) -> RateLimitConfig:
|
337
|
+
"""Convert rate limit configuration."""
|
338
|
+
rate_limit_config = self._get_rate_limit_config(config)
|
339
|
+
|
340
|
+
return RateLimitConfig(
|
341
|
+
enabled=rate_limit_config.get("enabled", False),
|
342
|
+
requests_per_minute=rate_limit_config.get("requests_per_minute", 60),
|
343
|
+
requests_per_hour=rate_limit_config.get("requests_per_hour", 1000),
|
344
|
+
requests_per_day=rate_limit_config.get("requests_per_day", 10000),
|
345
|
+
burst_limit=rate_limit_config.get("burst_limit", 10),
|
346
|
+
by_ip=rate_limit_config.get("by_ip", True),
|
347
|
+
by_user=rate_limit_config.get("by_user", True),
|
348
|
+
by_endpoint=rate_limit_config.get("by_endpoint", False),
|
349
|
+
exempt_roles=rate_limit_config.get("exempt_roles", []),
|
350
|
+
exempt_endpoints=rate_limit_config.get("exempt_endpoints", [])
|
351
|
+
)
|
352
|
+
|
353
|
+
def _get_auth_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
354
|
+
"""Get authentication configuration from config."""
|
355
|
+
security_config = config.get("security", {})
|
356
|
+
|
357
|
+
# Ensure security_config is a dictionary
|
358
|
+
if not isinstance(security_config, dict):
|
359
|
+
return {}
|
360
|
+
|
361
|
+
auth_config = security_config.get("auth", {})
|
362
|
+
|
363
|
+
# Handle legacy auth_enabled flag
|
364
|
+
if config.get("auth_enabled") is not None:
|
365
|
+
auth_config["enabled"] = config["auth_enabled"]
|
366
|
+
|
367
|
+
return auth_config
|
368
|
+
|
369
|
+
def _get_ssl_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
370
|
+
"""Get SSL configuration from config."""
|
371
|
+
security_config = config.get("security", {})
|
372
|
+
|
373
|
+
# Ensure security_config is a dictionary
|
374
|
+
if not isinstance(security_config, dict):
|
375
|
+
return {}
|
376
|
+
|
377
|
+
ssl_config = security_config.get("ssl", {})
|
378
|
+
|
379
|
+
# Debug: Check SSL config before merging
|
380
|
+
print(f"🔍 Debug: _get_ssl_config: security.ssl key_file={ssl_config.get('key_file')}")
|
381
|
+
|
382
|
+
# Merge with legacy SSL config, but prioritize security.ssl over legacy ssl
|
383
|
+
legacy_ssl = config.get("ssl", {})
|
384
|
+
if legacy_ssl:
|
385
|
+
print(f"🔍 Debug: _get_ssl_config: legacy ssl key_file={legacy_ssl.get('key_file')}")
|
386
|
+
# Only merge legacy config if security.ssl is not enabled or missing
|
387
|
+
if not ssl_config.get("enabled", False):
|
388
|
+
ssl_config.update(legacy_ssl)
|
389
|
+
else:
|
390
|
+
# If security.ssl is enabled, only merge non-conflicting fields
|
391
|
+
for key, value in legacy_ssl.items():
|
392
|
+
if key not in ssl_config or ssl_config[key] is None:
|
393
|
+
ssl_config[key] = value
|
394
|
+
|
395
|
+
print(f"🔍 Debug: _get_ssl_config: final key_file={ssl_config.get('key_file')}")
|
396
|
+
return ssl_config
|
397
|
+
|
398
|
+
def _get_permissions_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
399
|
+
"""Get permissions configuration from config."""
|
400
|
+
security_config = config.get("security", {})
|
401
|
+
|
402
|
+
# Ensure security_config is a dictionary
|
403
|
+
if not isinstance(security_config, dict):
|
404
|
+
return {}
|
405
|
+
|
406
|
+
permissions_config = security_config.get("permissions", {})
|
407
|
+
|
408
|
+
# Merge with legacy roles config
|
409
|
+
legacy_roles = config.get("roles", {})
|
410
|
+
if legacy_roles:
|
411
|
+
permissions_config.update(legacy_roles)
|
412
|
+
|
413
|
+
return permissions_config
|
414
|
+
|
415
|
+
def _get_rate_limit_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
416
|
+
"""Get rate limit configuration from config."""
|
417
|
+
security_config = config.get("security", {})
|
418
|
+
|
419
|
+
# Ensure security_config is a dictionary
|
420
|
+
if not isinstance(security_config, dict):
|
421
|
+
return {}
|
422
|
+
|
423
|
+
rate_limit_config = security_config.get("rate_limit", {})
|
424
|
+
|
425
|
+
# Handle legacy rate_limit_enabled flag
|
426
|
+
if config.get("rate_limit_enabled") is not None:
|
427
|
+
rate_limit_config["enabled"] = config["rate_limit_enabled"]
|
428
|
+
|
429
|
+
return rate_limit_config
|
430
|
+
|
431
|
+
def get_public_paths(self, config: Dict[str, Any]) -> List[str]:
|
432
|
+
"""
|
433
|
+
Get public paths from configuration.
|
434
|
+
|
435
|
+
Args:
|
436
|
+
config: Configuration dictionary
|
437
|
+
|
438
|
+
Returns:
|
439
|
+
List of public paths
|
440
|
+
"""
|
441
|
+
security_config = config.get("security", {})
|
442
|
+
|
443
|
+
# Ensure security_config is a dictionary
|
444
|
+
if not isinstance(security_config, dict):
|
445
|
+
return [
|
446
|
+
"/health",
|
447
|
+
"/docs",
|
448
|
+
"/redoc",
|
449
|
+
"/openapi.json",
|
450
|
+
"/favicon.ico"
|
451
|
+
]
|
452
|
+
|
453
|
+
public_paths = security_config.get("public_paths", [])
|
454
|
+
|
455
|
+
# Add default public paths if none specified
|
456
|
+
if not public_paths:
|
457
|
+
public_paths = [
|
458
|
+
"/health",
|
459
|
+
"/docs",
|
460
|
+
"/redoc",
|
461
|
+
"/openapi.json",
|
462
|
+
"/favicon.ico"
|
463
|
+
]
|
464
|
+
|
465
|
+
return public_paths
|
466
|
+
|
467
|
+
def get_security_enabled(self, config: Dict[str, Any]) -> bool:
|
468
|
+
"""
|
469
|
+
Check if security is enabled in configuration.
|
470
|
+
|
471
|
+
Args:
|
472
|
+
config: Configuration dictionary
|
473
|
+
|
474
|
+
Returns:
|
475
|
+
True if security is enabled
|
476
|
+
"""
|
477
|
+
security_config = config.get("security", {})
|
478
|
+
|
479
|
+
# Ensure security_config is a dictionary
|
480
|
+
if not isinstance(security_config, dict):
|
481
|
+
return True # Default to enabled for safety
|
482
|
+
|
483
|
+
return security_config.get("enabled", True)
|
484
|
+
|
485
|
+
def migrate_legacy_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
486
|
+
"""
|
487
|
+
Migrate legacy configuration to new format.
|
488
|
+
|
489
|
+
Args:
|
490
|
+
config: Legacy configuration dictionary
|
491
|
+
|
492
|
+
Returns:
|
493
|
+
Migrated configuration dictionary
|
494
|
+
"""
|
495
|
+
migrated_config = config.copy()
|
496
|
+
|
497
|
+
# Create security section if it doesn't exist
|
498
|
+
if "security" not in migrated_config:
|
499
|
+
migrated_config["security"] = {}
|
500
|
+
|
501
|
+
security_config = migrated_config["security"]
|
502
|
+
|
503
|
+
# Migrate SSL configuration
|
504
|
+
if "ssl" in migrated_config and "ssl" not in security_config:
|
505
|
+
security_config["ssl"] = migrated_config["ssl"]
|
506
|
+
# Don't remove legacy SSL yet for backward compatibility
|
507
|
+
|
508
|
+
# Migrate roles configuration
|
509
|
+
if "roles" in migrated_config and "permissions" not in security_config:
|
510
|
+
security_config["permissions"] = migrated_config["roles"]
|
511
|
+
# Don't remove legacy roles yet for backward compatibility
|
512
|
+
|
513
|
+
# Migrate auth_enabled flag
|
514
|
+
if "auth_enabled" in migrated_config and "auth" not in security_config:
|
515
|
+
security_config["auth"] = {"enabled": migrated_config["auth_enabled"]}
|
516
|
+
|
517
|
+
# Migrate rate_limit_enabled flag
|
518
|
+
if "rate_limit_enabled" in migrated_config and "rate_limit" not in security_config:
|
519
|
+
security_config["rate_limit"] = {"enabled": migrated_config["rate_limit_enabled"]}
|
520
|
+
|
521
|
+
logger.info("Legacy configuration migrated to new format")
|
522
|
+
return migrated_config
|
523
|
+
|
524
|
+
def get_default_config(self) -> Dict[str, Any]:
|
525
|
+
"""
|
526
|
+
Get default configuration.
|
527
|
+
|
528
|
+
Returns:
|
529
|
+
Default configuration dictionary
|
530
|
+
"""
|
531
|
+
return {
|
532
|
+
"security": {
|
533
|
+
"enabled": True,
|
534
|
+
"auth": {
|
535
|
+
"enabled": True,
|
536
|
+
"methods": ["api_key"],
|
537
|
+
"api_keys": {},
|
538
|
+
"jwt_secret": "",
|
539
|
+
"jwt_algorithm": "HS256",
|
540
|
+
"jwt_expiration": 3600
|
541
|
+
},
|
542
|
+
"ssl": {
|
543
|
+
"enabled": False,
|
544
|
+
"cert_file": None,
|
545
|
+
"key_file": None,
|
546
|
+
"ca_cert": None,
|
547
|
+
"min_tls_version": "TLSv1.2",
|
548
|
+
"verify_client": False,
|
549
|
+
"client_cert_required": False,
|
550
|
+
"cipher_suites": []
|
551
|
+
},
|
552
|
+
"permissions": {
|
553
|
+
"enabled": True,
|
554
|
+
"roles_file": "roles.json",
|
555
|
+
"default_role": "user",
|
556
|
+
"deny_by_default": True,
|
557
|
+
"role_mappings": {}
|
558
|
+
},
|
559
|
+
"rate_limit": {
|
560
|
+
"enabled": True,
|
561
|
+
"requests_per_minute": 60,
|
562
|
+
"requests_per_hour": 1000,
|
563
|
+
"requests_per_day": 10000,
|
564
|
+
"burst_limit": 10,
|
565
|
+
"by_ip": True,
|
566
|
+
"by_user": True,
|
567
|
+
"by_endpoint": False,
|
568
|
+
"exempt_roles": [],
|
569
|
+
"exempt_endpoints": []
|
570
|
+
},
|
571
|
+
"public_paths": [
|
572
|
+
"/health",
|
573
|
+
"/docs",
|
574
|
+
"/redoc",
|
575
|
+
"/openapi.json",
|
576
|
+
"/favicon.ico"
|
577
|
+
]
|
578
|
+
}
|
579
|
+
}
|
@@ -96,14 +96,25 @@ class CustomOpenAPIGenerator:
|
|
96
96
|
Returns:
|
97
97
|
Dict containing the parameter schema.
|
98
98
|
"""
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
99
|
+
try:
|
100
|
+
# Get command schema
|
101
|
+
cmd_schema = cmd_class.get_schema()
|
102
|
+
|
103
|
+
# Add title and description
|
104
|
+
cmd_schema["title"] = f"Parameters for {cmd_class.name}"
|
105
|
+
cmd_schema["description"] = f"Parameters for the {cmd_class.name} command"
|
106
|
+
|
107
|
+
return cmd_schema
|
108
|
+
except Exception as e:
|
109
|
+
# Return default schema if command schema generation fails
|
110
|
+
logger.warning(f"Failed to get schema for command {cmd_class.name}: {e}")
|
111
|
+
return {
|
112
|
+
"type": "object",
|
113
|
+
"title": f"Parameters for {cmd_class.name}",
|
114
|
+
"description": f"Parameters for the {cmd_class.name} command (schema generation failed)",
|
115
|
+
"properties": {},
|
116
|
+
"additionalProperties": True
|
117
|
+
}
|
107
118
|
|
108
119
|
def generate(self, title: Optional[str] = None, description: Optional[str] = None, version: Optional[str] = None) -> Dict[str, Any]:
|
109
120
|
"""
|
@@ -262,7 +273,7 @@ class CustomOpenAPIGenerator:
|
|
262
273
|
# Add commands to schema
|
263
274
|
self._add_commands_to_schema(schema)
|
264
275
|
|
265
|
-
logger.
|
276
|
+
logger.debug(f"Generated OpenAPI schema with {len(registry.get_all_commands())} commands")
|
266
277
|
|
267
278
|
return schema
|
268
279
|
|
@@ -358,9 +369,9 @@ def custom_openapi_with_fallback(app: FastAPI) -> Dict[str, Any]:
|
|
358
369
|
# Use the first registered generator
|
359
370
|
generator_name = list(_openapi_generators.keys())[0]
|
360
371
|
generator_func = _openapi_generators[generator_name]
|
361
|
-
logger.
|
372
|
+
logger.debug(f"Using custom OpenAPI generator: {generator_name}")
|
362
373
|
return generator_func(app)
|
363
374
|
|
364
375
|
# Fall back to default generator
|
365
|
-
logger.
|
376
|
+
logger.debug("Using default OpenAPI generator")
|
366
377
|
return custom_openapi(app)
|
@@ -1,7 +1,16 @@
|
|
1
|
-
"""
|
2
|
-
|
1
|
+
"""MCP Proxy Adapter Examples Package.
|
2
|
+
|
3
|
+
This package contains comprehensive examples demonstrating various
|
4
|
+
features and usage patterns of the MCP Proxy Adapter framework.
|
5
|
+
|
6
|
+
Examples include:
|
7
|
+
- Basic framework usage
|
8
|
+
- Full application with proxy registration
|
9
|
+
- Security configurations (HTTP, HTTPS, mTLS)
|
10
|
+
- Client implementations
|
11
|
+
- Command creation and registration
|
3
12
|
|
4
|
-
|
13
|
+
For detailed documentation, see the main README.md file.
|
5
14
|
"""
|
6
15
|
|
7
|
-
__version__ = "
|
16
|
+
__version__ = "6.2.21"
|
@@ -0,0 +1,9 @@
|
|
1
|
+
"""Basic Framework Example.
|
2
|
+
|
3
|
+
This example demonstrates the fundamental usage of MCP Proxy Adapter
|
4
|
+
with minimal configuration and basic command registration.
|
5
|
+
|
6
|
+
Note: This package provides a basic example of MCP Proxy Adapter usage.
|
7
|
+
The main application is created dynamically in main.py and not exported
|
8
|
+
as a global variable for this example.
|
9
|
+
"""
|