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.
Files changed (253) hide show
  1. mcp_proxy_adapter/__main__.py +12 -0
  2. mcp_proxy_adapter/api/app.py +254 -33
  3. mcp_proxy_adapter/api/handlers.py +32 -6
  4. mcp_proxy_adapter/api/middleware/__init__.py +36 -30
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +243 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
  10. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  11. mcp_proxy_adapter/api/middleware/unified_security.py +152 -0
  12. mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -0
  13. mcp_proxy_adapter/commands/__init__.py +19 -4
  14. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  15. mcp_proxy_adapter/commands/base.py +66 -32
  16. mcp_proxy_adapter/commands/builtin_commands.py +95 -0
  17. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  18. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  19. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  20. mcp_proxy_adapter/commands/command_registry.py +711 -354
  21. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  22. mcp_proxy_adapter/commands/echo_command.py +81 -0
  23. mcp_proxy_adapter/commands/health_command.py +7 -0
  24. mcp_proxy_adapter/commands/help_command.py +21 -14
  25. mcp_proxy_adapter/commands/hooks.py +200 -167
  26. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  27. mcp_proxy_adapter/commands/load_command.py +176 -0
  28. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  29. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  30. mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
  31. mcp_proxy_adapter/commands/reload_command.py +48 -50
  32. mcp_proxy_adapter/commands/result.py +1 -0
  33. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  34. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  35. mcp_proxy_adapter/commands/security_command.py +488 -0
  36. mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
  37. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  38. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  39. mcp_proxy_adapter/commands/unload_command.py +158 -0
  40. mcp_proxy_adapter/config.py +159 -2
  41. mcp_proxy_adapter/core/app_factory.py +326 -0
  42. mcp_proxy_adapter/core/auth_validator.py +606 -0
  43. mcp_proxy_adapter/core/certificate_utils.py +827 -0
  44. mcp_proxy_adapter/core/client_security.py +384 -0
  45. mcp_proxy_adapter/core/config_converter.py +405 -0
  46. mcp_proxy_adapter/core/config_validator.py +218 -0
  47. mcp_proxy_adapter/core/logging.py +19 -3
  48. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  49. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  50. mcp_proxy_adapter/core/protocol_manager.py +235 -0
  51. mcp_proxy_adapter/core/proxy_client.py +602 -0
  52. mcp_proxy_adapter/core/proxy_registration.py +522 -0
  53. mcp_proxy_adapter/core/role_utils.py +426 -0
  54. mcp_proxy_adapter/core/security_adapter.py +370 -0
  55. mcp_proxy_adapter/core/security_factory.py +239 -0
  56. mcp_proxy_adapter/core/security_integration.py +277 -0
  57. mcp_proxy_adapter/core/server_adapter.py +345 -0
  58. mcp_proxy_adapter/core/server_engine.py +364 -0
  59. mcp_proxy_adapter/core/settings.py +1 -0
  60. mcp_proxy_adapter/core/ssl_utils.py +233 -0
  61. mcp_proxy_adapter/core/transport_manager.py +292 -0
  62. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  63. mcp_proxy_adapter/custom_openapi.py +22 -11
  64. mcp_proxy_adapter/examples/README.md +230 -97
  65. mcp_proxy_adapter/examples/README_EN.md +258 -0
  66. mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
  67. mcp_proxy_adapter/examples/__pycache__/security_configurations.cpython-312.pyc +0 -0
  68. mcp_proxy_adapter/examples/__pycache__/security_test_client.cpython-312.pyc +0 -0
  69. mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
  70. mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
  71. mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +39 -0
  72. mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +25 -0
  73. mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
  74. mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
  75. mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
  76. mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
  77. mcp_proxy_adapter/examples/cert_config.json +9 -0
  78. mcp_proxy_adapter/examples/certs/admin.crt +32 -0
  79. mcp_proxy_adapter/examples/certs/admin.key +52 -0
  80. mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
  81. mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
  82. mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
  83. mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
  84. mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
  85. mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
  86. mcp_proxy_adapter/examples/certs/client.crt +32 -0
  87. mcp_proxy_adapter/examples/certs/client.key +52 -0
  88. mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
  89. mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
  90. mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
  91. mcp_proxy_adapter/examples/certs/client_user.key +52 -0
  92. mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
  93. mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
  94. mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
  95. mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
  96. mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
  97. mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
  98. mcp_proxy_adapter/examples/certs/readonly.key +52 -0
  99. mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
  100. mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
  101. mcp_proxy_adapter/examples/certs/server.crt +32 -0
  102. mcp_proxy_adapter/examples/certs/server.key +52 -0
  103. mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
  104. mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
  105. mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
  106. mcp_proxy_adapter/examples/certs/user.crt +32 -0
  107. mcp_proxy_adapter/examples/certs/user.key +52 -0
  108. mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
  109. mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
  110. mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
  111. mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
  112. mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
  113. mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
  114. mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
  115. mcp_proxy_adapter/examples/commands/__init__.py +1 -0
  116. mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
  117. mcp_proxy_adapter/examples/debug_request_state.py +144 -0
  118. mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
  119. mcp_proxy_adapter/examples/demo_client.py +341 -0
  120. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
  121. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
  122. mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
  123. mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
  124. mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
  125. mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
  126. mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
  127. mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
  128. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
  129. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
  130. mcp_proxy_adapter/examples/full_application/main.py +138 -0
  131. mcp_proxy_adapter/examples/full_application/roles.json +21 -0
  132. mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
  133. mcp_proxy_adapter/examples/generate_certificates.py +121 -0
  134. mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
  135. mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
  136. mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
  137. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
  138. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
  139. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
  140. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
  141. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
  142. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
  143. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
  144. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
  145. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
  146. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
  147. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
  148. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
  149. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
  150. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
  151. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
  152. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
  153. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
  154. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
  155. mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
  156. mcp_proxy_adapter/examples/roles.json +38 -0
  157. mcp_proxy_adapter/examples/run_example.py +81 -0
  158. mcp_proxy_adapter/examples/run_security_tests.py +326 -0
  159. mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
  160. mcp_proxy_adapter/examples/security_test_client.py +743 -0
  161. mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
  162. mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
  163. mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
  164. mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
  165. mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
  166. mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
  167. mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
  168. mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
  169. mcp_proxy_adapter/examples/test_examples.py +344 -0
  170. mcp_proxy_adapter/examples/universal_client.py +628 -0
  171. mcp_proxy_adapter/main.py +186 -0
  172. mcp_proxy_adapter/utils/config_generator.py +639 -0
  173. mcp_proxy_adapter/version.py +2 -1
  174. mcp_proxy_adapter-6.1.0.dist-info/METADATA +205 -0
  175. mcp_proxy_adapter-6.1.0.dist-info/RECORD +193 -0
  176. mcp_proxy_adapter-6.1.0.dist-info/entry_points.txt +2 -0
  177. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/licenses/LICENSE +2 -2
  178. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  179. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  180. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  181. mcp_proxy_adapter/examples/__init__.py +0 -7
  182. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  183. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  184. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  185. mcp_proxy_adapter/examples/basic_server/config.json +0 -35
  186. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  187. mcp_proxy_adapter/examples/basic_server/server.py +0 -103
  188. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  189. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  190. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
  191. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  192. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  193. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  194. mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
  195. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  196. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  197. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  198. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  199. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  200. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  201. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  202. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  203. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  204. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  205. mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
  206. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  207. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  208. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  209. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  210. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  211. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  212. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  213. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  214. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  215. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  216. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  217. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  218. mcp_proxy_adapter/tests/__init__.py +0 -0
  219. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  220. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  221. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  222. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  223. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  224. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  225. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  226. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  227. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  228. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  229. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  230. mcp_proxy_adapter/tests/conftest.py +0 -131
  231. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  232. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  233. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  234. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  235. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  236. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  237. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  238. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  239. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  240. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  241. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  242. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  243. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  244. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  245. mcp_proxy_adapter/tests/test_config.py +0 -127
  246. mcp_proxy_adapter/tests/test_utils.py +0 -65
  247. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  248. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  249. mcp_proxy_adapter/tests/unit/test_config.py +0 -217
  250. mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
  251. mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
  252. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/WHEEL +0 -0
  253. {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()