mcp-proxy-adapter 4.1.1__py3-none-any.whl → 6.1.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.
- mcp_proxy_adapter/__main__.py +12 -0
- mcp_proxy_adapter/api/app.py +254 -33
- mcp_proxy_adapter/api/handlers.py +32 -6
- mcp_proxy_adapter/api/middleware/__init__.py +36 -30
- 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 +135 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
- mcp_proxy_adapter/api/middleware/unified_security.py +152 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -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 +7 -0
- 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 +483 -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 +159 -2
- mcp_proxy_adapter/core/app_factory.py +326 -0
- mcp_proxy_adapter/core/auth_validator.py +606 -0
- mcp_proxy_adapter/core/certificate_utils.py +827 -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 +235 -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 +277 -0
- mcp_proxy_adapter/core/server_adapter.py +345 -0
- mcp_proxy_adapter/core/server_engine.py +364 -0
- mcp_proxy_adapter/core/settings.py +1 -0
- mcp_proxy_adapter/core/ssl_utils.py +233 -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/README.md +230 -97
- mcp_proxy_adapter/examples/README_EN.md +258 -0
- mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
- mcp_proxy_adapter/examples/__pycache__/security_configurations.cpython-312.pyc +0 -0
- mcp_proxy_adapter/examples/__pycache__/security_test_client.cpython-312.pyc +0 -0
- mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
- mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
- mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +39 -0
- mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +25 -0
- mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
- mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
- mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
- mcp_proxy_adapter/examples/cert_config.json +9 -0
- mcp_proxy_adapter/examples/certs/admin.crt +32 -0
- mcp_proxy_adapter/examples/certs/admin.key +52 -0
- mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
- mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
- mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
- mcp_proxy_adapter/examples/certs/client.crt +32 -0
- mcp_proxy_adapter/examples/certs/client.key +52 -0
- mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
- mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
- mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
- mcp_proxy_adapter/examples/certs/client_user.key +52 -0
- mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
- mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
- mcp_proxy_adapter/examples/certs/readonly.key +52 -0
- mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/server.crt +32 -0
- mcp_proxy_adapter/examples/certs/server.key +52 -0
- mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
- mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
- mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
- mcp_proxy_adapter/examples/certs/user.crt +32 -0
- mcp_proxy_adapter/examples/certs/user.key +52 -0
- mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
- mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
- mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
- mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
- mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
- mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
- mcp_proxy_adapter/examples/commands/__init__.py +1 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
- mcp_proxy_adapter/examples/debug_request_state.py +144 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
- mcp_proxy_adapter/examples/demo_client.py +341 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
- mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
- mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
- mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
- mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
- mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
- mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
- mcp_proxy_adapter/examples/full_application/main.py +138 -0
- mcp_proxy_adapter/examples/full_application/roles.json +21 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
- mcp_proxy_adapter/examples/generate_certificates.py +121 -0
- mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
- mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
- mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
- mcp_proxy_adapter/examples/roles.json +38 -0
- mcp_proxy_adapter/examples/run_example.py +81 -0
- mcp_proxy_adapter/examples/run_security_tests.py +326 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
- mcp_proxy_adapter/examples/security_test_client.py +743 -0
- mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
- mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
- mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
- mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
- mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
- mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
- mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
- mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
- mcp_proxy_adapter/examples/test_examples.py +344 -0
- mcp_proxy_adapter/examples/universal_client.py +628 -0
- mcp_proxy_adapter/main.py +186 -0
- mcp_proxy_adapter/utils/config_generator.py +639 -0
- mcp_proxy_adapter/version.py +2 -1
- mcp_proxy_adapter-6.1.0.dist-info/METADATA +205 -0
- mcp_proxy_adapter-6.1.0.dist-info/RECORD +193 -0
- mcp_proxy_adapter-6.1.0.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.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/__init__.py +0 -7
- 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.1.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,602 @@
|
|
1
|
+
"""
|
2
|
+
Proxy Client Module
|
3
|
+
|
4
|
+
This module provides a client for registering with MCP proxy servers
|
5
|
+
using mcp_security_framework for secure authentication and connections.
|
6
|
+
|
7
|
+
Author: Vasiliy Zdanovskiy
|
8
|
+
email: vasilyvz@gmail.com
|
9
|
+
"""
|
10
|
+
|
11
|
+
import asyncio
|
12
|
+
import json
|
13
|
+
import time
|
14
|
+
import ssl
|
15
|
+
from typing import Dict, Any, Optional, Tuple, List
|
16
|
+
from urllib.parse import urljoin, urlparse
|
17
|
+
from pathlib import Path
|
18
|
+
|
19
|
+
import aiohttp
|
20
|
+
from aiohttp import ClientTimeout, TCPConnector
|
21
|
+
|
22
|
+
# Import framework components
|
23
|
+
try:
|
24
|
+
from mcp_security_framework.core.client_security import ClientSecurityManager
|
25
|
+
from mcp_security_framework.schemas.config import ClientSecurityConfig
|
26
|
+
from mcp_security_framework.schemas.models import AuthResult, ValidationResult
|
27
|
+
from mcp_security_framework.utils.crypto_utils import generate_api_key, create_jwt_token
|
28
|
+
from mcp_security_framework.utils.cert_utils import validate_certificate_format
|
29
|
+
SECURITY_FRAMEWORK_AVAILABLE = True
|
30
|
+
except ImportError:
|
31
|
+
SECURITY_FRAMEWORK_AVAILABLE = False
|
32
|
+
ClientSecurityManager = None
|
33
|
+
ClientSecurityConfig = None
|
34
|
+
AuthResult = None
|
35
|
+
ValidationResult = None
|
36
|
+
|
37
|
+
from mcp_proxy_adapter.core.logging import logger
|
38
|
+
|
39
|
+
|
40
|
+
class ProxyClientError(Exception):
|
41
|
+
"""Exception raised when proxy client operations fail."""
|
42
|
+
pass
|
43
|
+
|
44
|
+
|
45
|
+
class ProxyClient:
|
46
|
+
"""
|
47
|
+
Client for registering with MCP proxy servers.
|
48
|
+
|
49
|
+
Provides secure registration, heartbeat, and discovery functionality
|
50
|
+
using mcp_security_framework for authentication and SSL/TLS.
|
51
|
+
"""
|
52
|
+
|
53
|
+
def __init__(self, config: Dict[str, Any]):
|
54
|
+
"""
|
55
|
+
Initialize proxy client.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
config: Client configuration
|
59
|
+
"""
|
60
|
+
self.config = config
|
61
|
+
self.registration_config = config.get("registration", {})
|
62
|
+
|
63
|
+
# Basic settings
|
64
|
+
self.proxy_url = self.registration_config.get("server_url")
|
65
|
+
self.server_id = self.registration_config.get("proxy_info", {}).get("name", "mcp_proxy_adapter")
|
66
|
+
self.server_name = self.registration_config.get("proxy_info", {}).get("name", "MCP Proxy Adapter")
|
67
|
+
self.description = self.registration_config.get("proxy_info", {}).get("description", "")
|
68
|
+
|
69
|
+
# Authentication settings
|
70
|
+
self.auth_method = self.registration_config.get("auth_method", "none")
|
71
|
+
self.auth_config = self._get_auth_config()
|
72
|
+
|
73
|
+
# Heartbeat settings
|
74
|
+
heartbeat_config = self.registration_config.get("heartbeat", {})
|
75
|
+
self.heartbeat_interval = heartbeat_config.get("interval", 300)
|
76
|
+
self.heartbeat_timeout = heartbeat_config.get("timeout", 30)
|
77
|
+
self.retry_attempts = heartbeat_config.get("retry_attempts", 3)
|
78
|
+
self.retry_delay = heartbeat_config.get("retry_delay", 60)
|
79
|
+
|
80
|
+
# Auto discovery settings
|
81
|
+
discovery_config = self.registration_config.get("auto_discovery", {})
|
82
|
+
self.discovery_enabled = discovery_config.get("enabled", False)
|
83
|
+
self.discovery_urls = discovery_config.get("discovery_urls", [])
|
84
|
+
self.discovery_interval = discovery_config.get("discovery_interval", 3600)
|
85
|
+
|
86
|
+
# Initialize security manager
|
87
|
+
self.security_manager = self._create_security_manager()
|
88
|
+
|
89
|
+
# State
|
90
|
+
self.registered = False
|
91
|
+
self.server_key: Optional[str] = None
|
92
|
+
self.server_url: Optional[str] = None
|
93
|
+
self.heartbeat_task: Optional[asyncio.Task] = None
|
94
|
+
self.discovery_task: Optional[asyncio.Task] = None
|
95
|
+
|
96
|
+
logger.info("Proxy client initialized with security framework integration")
|
97
|
+
|
98
|
+
def _get_auth_config(self) -> Dict[str, Any]:
|
99
|
+
"""Get authentication configuration based on auth method."""
|
100
|
+
if self.auth_method == "certificate":
|
101
|
+
return self.registration_config.get("certificate", {})
|
102
|
+
elif self.auth_method == "token":
|
103
|
+
return self.registration_config.get("token", {})
|
104
|
+
elif self.auth_method == "api_key":
|
105
|
+
return self.registration_config.get("api_key", {})
|
106
|
+
else:
|
107
|
+
return {}
|
108
|
+
|
109
|
+
def _create_security_manager(self) -> Optional[ClientSecurityManager]:
|
110
|
+
"""Create client security manager."""
|
111
|
+
if not SECURITY_FRAMEWORK_AVAILABLE:
|
112
|
+
logger.warning("mcp_security_framework not available, using basic client")
|
113
|
+
return None
|
114
|
+
|
115
|
+
try:
|
116
|
+
# Create client security configuration
|
117
|
+
client_security_config = self.registration_config.get("client_security", {})
|
118
|
+
|
119
|
+
if not client_security_config.get("enabled", False):
|
120
|
+
logger.info("Client security disabled in configuration")
|
121
|
+
return None
|
122
|
+
|
123
|
+
# Create security config
|
124
|
+
security_config = {
|
125
|
+
"security": {
|
126
|
+
"ssl": {
|
127
|
+
"enabled": client_security_config.get("ssl_enabled", False),
|
128
|
+
"client_cert_file": client_security_config.get("certificate_auth", {}).get("cert_file"),
|
129
|
+
"client_key_file": client_security_config.get("certificate_auth", {}).get("key_file"),
|
130
|
+
"ca_cert_file": client_security_config.get("certificate_auth", {}).get("ca_cert_file"),
|
131
|
+
"verify_mode": "CERT_REQUIRED",
|
132
|
+
"min_tls_version": "TLSv1.2",
|
133
|
+
"check_hostname": True,
|
134
|
+
"check_expiry": True
|
135
|
+
},
|
136
|
+
"auth": {
|
137
|
+
"enabled": True,
|
138
|
+
"methods": client_security_config.get("auth_methods", ["api_key"]),
|
139
|
+
"api_keys": {
|
140
|
+
client_security_config.get("api_key_auth", {}).get("key", "default"): {
|
141
|
+
"roles": ["proxy_client"],
|
142
|
+
"permissions": ["register", "heartbeat", "discover"]
|
143
|
+
}
|
144
|
+
}
|
145
|
+
}
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
return ClientSecurityManager(security_config)
|
150
|
+
|
151
|
+
except Exception as e:
|
152
|
+
logger.error(f"Failed to create security manager: {e}")
|
153
|
+
return None
|
154
|
+
|
155
|
+
def set_server_url(self, server_url: str) -> None:
|
156
|
+
"""
|
157
|
+
Set the server URL for registration.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
server_url: The URL where this server is accessible.
|
161
|
+
"""
|
162
|
+
self.server_url = server_url
|
163
|
+
logger.info(f"Proxy client server URL set to: {server_url}")
|
164
|
+
|
165
|
+
def _get_auth_headers(self) -> Dict[str, str]:
|
166
|
+
"""
|
167
|
+
Get authentication headers for requests.
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
Dictionary of authentication headers
|
171
|
+
"""
|
172
|
+
headers = {"Content-Type": "application/json"}
|
173
|
+
|
174
|
+
if not self.security_manager:
|
175
|
+
return headers
|
176
|
+
|
177
|
+
try:
|
178
|
+
if self.auth_method == "certificate":
|
179
|
+
return self.security_manager.get_client_auth_headers("certificate")
|
180
|
+
elif self.auth_method == "token":
|
181
|
+
token = self.auth_config.get("token")
|
182
|
+
return self.security_manager.get_client_auth_headers("jwt", token=token)
|
183
|
+
elif self.auth_method == "api_key":
|
184
|
+
api_key = self.auth_config.get("key")
|
185
|
+
return self.security_manager.get_client_auth_headers("api_key", api_key=api_key)
|
186
|
+
else:
|
187
|
+
return headers
|
188
|
+
except Exception as e:
|
189
|
+
logger.error(f"Failed to get auth headers: {e}")
|
190
|
+
return headers
|
191
|
+
|
192
|
+
def _create_ssl_context(self) -> Optional[ssl.SSLContext]:
|
193
|
+
"""
|
194
|
+
Create SSL context for secure connections.
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
SSL context or None if SSL not needed
|
198
|
+
"""
|
199
|
+
if not self.security_manager:
|
200
|
+
return None
|
201
|
+
|
202
|
+
try:
|
203
|
+
return self.security_manager.create_client_ssl_context()
|
204
|
+
except Exception as e:
|
205
|
+
logger.error(f"Failed to create SSL context: {e}")
|
206
|
+
return None
|
207
|
+
|
208
|
+
async def register(self) -> bool:
|
209
|
+
"""
|
210
|
+
Register with the proxy server.
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
True if registration was successful, False otherwise.
|
214
|
+
"""
|
215
|
+
if not self.proxy_url:
|
216
|
+
logger.error("Proxy URL not configured")
|
217
|
+
return False
|
218
|
+
|
219
|
+
if not self.server_url:
|
220
|
+
logger.error("Server URL not set")
|
221
|
+
return False
|
222
|
+
|
223
|
+
# Prepare registration data
|
224
|
+
proxy_info = self.registration_config.get("proxy_info", {})
|
225
|
+
registration_data = {
|
226
|
+
"server_id": self.server_id,
|
227
|
+
"server_url": self.server_url,
|
228
|
+
"server_name": self.server_name,
|
229
|
+
"description": self.description,
|
230
|
+
"version": proxy_info.get("version", "1.0.0"),
|
231
|
+
"capabilities": proxy_info.get("capabilities", ["jsonrpc", "rest"]),
|
232
|
+
"endpoints": proxy_info.get("endpoints", {
|
233
|
+
"jsonrpc": "/api/jsonrpc",
|
234
|
+
"rest": "/cmd",
|
235
|
+
"health": "/health"
|
236
|
+
}),
|
237
|
+
"auth_method": self.auth_method,
|
238
|
+
"security_enabled": self.security_manager is not None
|
239
|
+
}
|
240
|
+
|
241
|
+
logger.info(f"Attempting to register with proxy at {self.proxy_url}")
|
242
|
+
logger.debug(f"Registration data: {registration_data}")
|
243
|
+
|
244
|
+
for attempt in range(self.retry_attempts):
|
245
|
+
try:
|
246
|
+
success, result = await self._make_request("/register", registration_data)
|
247
|
+
|
248
|
+
if success:
|
249
|
+
self.registered = True
|
250
|
+
self.server_key = result.get("server_key")
|
251
|
+
logger.info(f"✅ Successfully registered with proxy. Server key: {self.server_key}")
|
252
|
+
|
253
|
+
# Start heartbeat and discovery
|
254
|
+
await self._start_background_tasks()
|
255
|
+
|
256
|
+
return True
|
257
|
+
else:
|
258
|
+
error_msg = result.get("error", {}).get("message", "Unknown error")
|
259
|
+
logger.warning(f"❌ Registration attempt {attempt + 1} failed: {error_msg}")
|
260
|
+
|
261
|
+
if attempt < self.retry_attempts - 1:
|
262
|
+
logger.info(f"Retrying in {self.retry_delay} seconds...")
|
263
|
+
await asyncio.sleep(self.retry_delay)
|
264
|
+
|
265
|
+
except Exception as e:
|
266
|
+
logger.error(f"❌ Registration attempt {attempt + 1} failed with exception: {e}")
|
267
|
+
|
268
|
+
if attempt < self.retry_attempts - 1:
|
269
|
+
logger.info(f"Retrying in {self.retry_delay} seconds...")
|
270
|
+
await asyncio.sleep(self.retry_delay)
|
271
|
+
|
272
|
+
logger.error(f"❌ Failed to register with proxy after {self.retry_attempts} attempts")
|
273
|
+
return False
|
274
|
+
|
275
|
+
async def unregister(self) -> bool:
|
276
|
+
"""
|
277
|
+
Unregister from the proxy server.
|
278
|
+
|
279
|
+
Returns:
|
280
|
+
True if unregistration was successful, False otherwise.
|
281
|
+
"""
|
282
|
+
if not self.registered or not self.server_key:
|
283
|
+
logger.info("Not registered with proxy, skipping unregistration")
|
284
|
+
return True
|
285
|
+
|
286
|
+
# Stop background tasks
|
287
|
+
await self._stop_background_tasks()
|
288
|
+
|
289
|
+
# Extract copy_number from server_key
|
290
|
+
try:
|
291
|
+
copy_number = int(self.server_key.split("_")[-1])
|
292
|
+
except (ValueError, IndexError):
|
293
|
+
copy_number = 1
|
294
|
+
|
295
|
+
unregistration_data = {
|
296
|
+
"server_id": self.server_id,
|
297
|
+
"copy_number": copy_number
|
298
|
+
}
|
299
|
+
|
300
|
+
logger.info(f"Attempting to unregister from proxy at {self.proxy_url}")
|
301
|
+
|
302
|
+
try:
|
303
|
+
success, result = await self._make_request("/unregister", unregistration_data)
|
304
|
+
|
305
|
+
if success:
|
306
|
+
unregistered = result.get("unregistered", False)
|
307
|
+
if unregistered:
|
308
|
+
logger.info("✅ Successfully unregistered from proxy")
|
309
|
+
else:
|
310
|
+
logger.warning("⚠️ Server was not found in proxy registry")
|
311
|
+
|
312
|
+
self.registered = False
|
313
|
+
self.server_key = None
|
314
|
+
return True
|
315
|
+
else:
|
316
|
+
error_msg = result.get("error", {}).get("message", "Unknown error")
|
317
|
+
logger.error(f"❌ Failed to unregister from proxy: {error_msg}")
|
318
|
+
return False
|
319
|
+
|
320
|
+
except Exception as e:
|
321
|
+
logger.error(f"❌ Unregistration failed with exception: {e}")
|
322
|
+
return False
|
323
|
+
|
324
|
+
async def send_heartbeat(self) -> bool:
|
325
|
+
"""
|
326
|
+
Send heartbeat to proxy server.
|
327
|
+
|
328
|
+
Returns:
|
329
|
+
True if heartbeat was successful, False otherwise.
|
330
|
+
"""
|
331
|
+
if not self.server_key:
|
332
|
+
return False
|
333
|
+
|
334
|
+
heartbeat_data = {
|
335
|
+
"server_id": self.server_id,
|
336
|
+
"server_key": self.server_key,
|
337
|
+
"timestamp": int(time.time()),
|
338
|
+
"status": "healthy"
|
339
|
+
}
|
340
|
+
|
341
|
+
try:
|
342
|
+
success, result = await self._make_request("/heartbeat", heartbeat_data)
|
343
|
+
|
344
|
+
if success:
|
345
|
+
logger.debug("Heartbeat sent successfully")
|
346
|
+
return True
|
347
|
+
else:
|
348
|
+
logger.warning(f"Heartbeat failed: {result.get('error', {}).get('message', 'Unknown error')}")
|
349
|
+
return False
|
350
|
+
|
351
|
+
except Exception as e:
|
352
|
+
logger.error(f"Heartbeat error: {e}")
|
353
|
+
return False
|
354
|
+
|
355
|
+
async def discover_proxies(self) -> List[Dict[str, Any]]:
|
356
|
+
"""
|
357
|
+
Discover available proxy servers.
|
358
|
+
|
359
|
+
Returns:
|
360
|
+
List of discovered proxy servers.
|
361
|
+
"""
|
362
|
+
if not self.discovery_enabled:
|
363
|
+
return []
|
364
|
+
|
365
|
+
discovered_proxies = []
|
366
|
+
|
367
|
+
for discovery_url in self.discovery_urls:
|
368
|
+
try:
|
369
|
+
success, result = await self._make_request("/discover", {}, base_url=discovery_url)
|
370
|
+
|
371
|
+
if success:
|
372
|
+
proxies = result.get("proxies", [])
|
373
|
+
discovered_proxies.extend(proxies)
|
374
|
+
logger.info(f"Discovered {len(proxies)} proxies from {discovery_url}")
|
375
|
+
else:
|
376
|
+
logger.warning(f"Discovery failed for {discovery_url}")
|
377
|
+
|
378
|
+
except Exception as e:
|
379
|
+
logger.error(f"Discovery error for {discovery_url}: {e}")
|
380
|
+
|
381
|
+
return discovered_proxies
|
382
|
+
|
383
|
+
async def _make_request(self, endpoint: str, data: Dict[str, Any], base_url: Optional[str] = None) -> Tuple[bool, Dict[str, Any]]:
|
384
|
+
"""
|
385
|
+
Make HTTP request to proxy server.
|
386
|
+
|
387
|
+
Args:
|
388
|
+
endpoint: API endpoint
|
389
|
+
data: Request data
|
390
|
+
base_url: Base URL (optional, uses self.proxy_url if not provided)
|
391
|
+
|
392
|
+
Returns:
|
393
|
+
Tuple of (success, result)
|
394
|
+
"""
|
395
|
+
url = urljoin(base_url or self.proxy_url, endpoint)
|
396
|
+
|
397
|
+
# Get authentication headers
|
398
|
+
headers = self._get_auth_headers()
|
399
|
+
|
400
|
+
# Create SSL context if needed
|
401
|
+
ssl_context = self._create_ssl_context()
|
402
|
+
|
403
|
+
# Create connector with SSL context
|
404
|
+
connector = None
|
405
|
+
if ssl_context:
|
406
|
+
connector = TCPConnector(ssl=ssl_context)
|
407
|
+
|
408
|
+
try:
|
409
|
+
timeout = ClientTimeout(total=self.heartbeat_timeout)
|
410
|
+
|
411
|
+
async with aiohttp.ClientSession(connector=connector) as session:
|
412
|
+
async with session.post(
|
413
|
+
url,
|
414
|
+
json=data,
|
415
|
+
headers=headers,
|
416
|
+
timeout=timeout
|
417
|
+
) as response:
|
418
|
+
result = await response.json()
|
419
|
+
|
420
|
+
# Validate response if security manager available
|
421
|
+
if self.security_manager:
|
422
|
+
self.security_manager.validate_server_response(dict(response.headers))
|
423
|
+
|
424
|
+
return response.status == 200, result
|
425
|
+
except Exception as e:
|
426
|
+
logger.error(f"Request failed: {e}")
|
427
|
+
return False, {"error": {"message": str(e)}}
|
428
|
+
finally:
|
429
|
+
if connector:
|
430
|
+
await connector.close()
|
431
|
+
|
432
|
+
async def _start_background_tasks(self) -> None:
|
433
|
+
"""Start heartbeat and discovery background tasks."""
|
434
|
+
# Start heartbeat
|
435
|
+
if self.registration_config.get("heartbeat", {}).get("enabled", True):
|
436
|
+
self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
437
|
+
logger.info("Heartbeat task started")
|
438
|
+
|
439
|
+
# Start discovery
|
440
|
+
if self.discovery_enabled:
|
441
|
+
self.discovery_task = asyncio.create_task(self._discovery_loop())
|
442
|
+
logger.info("Discovery task started")
|
443
|
+
|
444
|
+
async def _stop_background_tasks(self) -> None:
|
445
|
+
"""Stop background tasks."""
|
446
|
+
# Stop heartbeat
|
447
|
+
if self.heartbeat_task and not self.heartbeat_task.done():
|
448
|
+
self.heartbeat_task.cancel()
|
449
|
+
try:
|
450
|
+
await self.heartbeat_task
|
451
|
+
except asyncio.CancelledError:
|
452
|
+
pass
|
453
|
+
logger.info("Heartbeat task stopped")
|
454
|
+
|
455
|
+
# Stop discovery
|
456
|
+
if self.discovery_task and not self.discovery_task.done():
|
457
|
+
self.discovery_task.cancel()
|
458
|
+
try:
|
459
|
+
await self.discovery_task
|
460
|
+
except asyncio.CancelledError:
|
461
|
+
pass
|
462
|
+
logger.info("Discovery task stopped")
|
463
|
+
|
464
|
+
async def _heartbeat_loop(self) -> None:
|
465
|
+
"""Heartbeat loop to keep registration alive."""
|
466
|
+
while self.registered:
|
467
|
+
try:
|
468
|
+
await asyncio.sleep(self.heartbeat_interval)
|
469
|
+
|
470
|
+
if not self.registered:
|
471
|
+
break
|
472
|
+
|
473
|
+
# Send heartbeat
|
474
|
+
success = await self.send_heartbeat()
|
475
|
+
if not success:
|
476
|
+
logger.warning("Heartbeat failed, attempting to re-register")
|
477
|
+
await self.register()
|
478
|
+
|
479
|
+
except asyncio.CancelledError:
|
480
|
+
break
|
481
|
+
except Exception as e:
|
482
|
+
logger.error(f"Heartbeat error: {e}")
|
483
|
+
|
484
|
+
async def _discovery_loop(self) -> None:
|
485
|
+
"""Discovery loop to find new proxy servers."""
|
486
|
+
while self.registered:
|
487
|
+
try:
|
488
|
+
await asyncio.sleep(self.discovery_interval)
|
489
|
+
|
490
|
+
if not self.registered:
|
491
|
+
break
|
492
|
+
|
493
|
+
# Discover proxies
|
494
|
+
proxies = await self.discover_proxies()
|
495
|
+
if proxies:
|
496
|
+
logger.info(f"Discovered {len(proxies)} proxy servers")
|
497
|
+
|
498
|
+
# Register with new proxies if configured
|
499
|
+
if self.registration_config.get("auto_discovery", {}).get("register_on_discovery", False):
|
500
|
+
for proxy in proxies:
|
501
|
+
proxy_url = proxy.get("url")
|
502
|
+
if proxy_url and proxy_url != self.proxy_url:
|
503
|
+
logger.info(f"Attempting to register with discovered proxy: {proxy_url}")
|
504
|
+
# Store original URL and try to register with new proxy
|
505
|
+
original_url = self.proxy_url
|
506
|
+
self.proxy_url = proxy_url
|
507
|
+
await self.register()
|
508
|
+
self.proxy_url = original_url
|
509
|
+
|
510
|
+
except asyncio.CancelledError:
|
511
|
+
break
|
512
|
+
except Exception as e:
|
513
|
+
logger.error(f"Discovery error: {e}")
|
514
|
+
|
515
|
+
def get_status(self) -> Dict[str, Any]:
|
516
|
+
"""
|
517
|
+
Get current client status.
|
518
|
+
|
519
|
+
Returns:
|
520
|
+
Dictionary with client status information.
|
521
|
+
"""
|
522
|
+
status = {
|
523
|
+
"enabled": self.registration_config.get("enabled", False),
|
524
|
+
"registered": self.registered,
|
525
|
+
"server_key": self.server_key,
|
526
|
+
"server_url": self.server_url,
|
527
|
+
"proxy_url": self.proxy_url,
|
528
|
+
"server_id": self.server_id,
|
529
|
+
"auth_method": self.auth_method,
|
530
|
+
"heartbeat_active": self.heartbeat_task is not None and not self.heartbeat_task.done(),
|
531
|
+
"discovery_active": self.discovery_task is not None and not self.discovery_task.done()
|
532
|
+
}
|
533
|
+
|
534
|
+
# Add security information
|
535
|
+
if self.security_manager:
|
536
|
+
status["security_enabled"] = True
|
537
|
+
status["ssl_enabled"] = self.security_manager.is_ssl_enabled()
|
538
|
+
status["auth_methods"] = self.security_manager.get_supported_auth_methods()
|
539
|
+
else:
|
540
|
+
status["security_enabled"] = False
|
541
|
+
|
542
|
+
return status
|
543
|
+
|
544
|
+
|
545
|
+
# Global proxy client instance
|
546
|
+
proxy_client: Optional[ProxyClient] = None
|
547
|
+
|
548
|
+
|
549
|
+
def initialize_proxy_client(config: Dict[str, Any]) -> None:
|
550
|
+
"""
|
551
|
+
Initialize global proxy client.
|
552
|
+
|
553
|
+
Args:
|
554
|
+
config: Application configuration
|
555
|
+
"""
|
556
|
+
global proxy_client
|
557
|
+
proxy_client = ProxyClient(config)
|
558
|
+
|
559
|
+
|
560
|
+
async def register_with_proxy(server_url: str) -> bool:
|
561
|
+
"""
|
562
|
+
Register with proxy server.
|
563
|
+
|
564
|
+
Args:
|
565
|
+
server_url: The URL where this server is accessible.
|
566
|
+
|
567
|
+
Returns:
|
568
|
+
True if registration was successful, False otherwise.
|
569
|
+
"""
|
570
|
+
if not proxy_client:
|
571
|
+
logger.error("Proxy client not initialized")
|
572
|
+
return False
|
573
|
+
|
574
|
+
proxy_client.set_server_url(server_url)
|
575
|
+
return await proxy_client.register()
|
576
|
+
|
577
|
+
|
578
|
+
async def unregister_from_proxy() -> bool:
|
579
|
+
"""
|
580
|
+
Unregister from proxy server.
|
581
|
+
|
582
|
+
Returns:
|
583
|
+
True if unregistration was successful, False otherwise.
|
584
|
+
"""
|
585
|
+
if not proxy_client:
|
586
|
+
logger.error("Proxy client not initialized")
|
587
|
+
return False
|
588
|
+
|
589
|
+
return await proxy_client.unregister()
|
590
|
+
|
591
|
+
|
592
|
+
def get_proxy_client_status() -> Dict[str, Any]:
|
593
|
+
"""
|
594
|
+
Get proxy client status.
|
595
|
+
|
596
|
+
Returns:
|
597
|
+
Dictionary with client status information.
|
598
|
+
"""
|
599
|
+
if not proxy_client:
|
600
|
+
return {"error": "Proxy client not initialized"}
|
601
|
+
|
602
|
+
return proxy_client.get_status()
|