mcp-proxy-adapter 6.0.0__py3-none-any.whl → 6.1.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.
Files changed (264) hide show
  1. mcp_proxy_adapter/api/app.py +174 -80
  2. mcp_proxy_adapter/api/handlers.py +16 -5
  3. mcp_proxy_adapter/api/middleware/__init__.py +9 -4
  4. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  5. mcp_proxy_adapter/api/middleware/factory.py +36 -12
  6. mcp_proxy_adapter/api/middleware/protocol_middleware.py +32 -13
  7. mcp_proxy_adapter/api/middleware/unified_security.py +160 -0
  8. mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -0
  9. mcp_proxy_adapter/commands/__init__.py +7 -1
  10. mcp_proxy_adapter/commands/base.py +7 -4
  11. mcp_proxy_adapter/commands/builtin_commands.py +8 -2
  12. mcp_proxy_adapter/commands/command_registry.py +8 -0
  13. mcp_proxy_adapter/commands/echo_command.py +81 -0
  14. mcp_proxy_adapter/commands/help_command.py +21 -14
  15. mcp_proxy_adapter/commands/proxy_registration_command.py +326 -185
  16. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  17. mcp_proxy_adapter/commands/security_command.py +488 -0
  18. mcp_proxy_adapter/commands/ssl_setup_command.py +2 -2
  19. mcp_proxy_adapter/commands/token_management_command.py +1 -1
  20. mcp_proxy_adapter/config.py +81 -21
  21. mcp_proxy_adapter/core/app_factory.py +326 -0
  22. mcp_proxy_adapter/core/client_security.py +384 -0
  23. mcp_proxy_adapter/core/logging.py +8 -3
  24. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  25. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  26. mcp_proxy_adapter/core/protocol_manager.py +139 -8
  27. mcp_proxy_adapter/core/proxy_client.py +602 -0
  28. mcp_proxy_adapter/core/proxy_registration.py +299 -47
  29. mcp_proxy_adapter/core/security_adapter.py +12 -15
  30. mcp_proxy_adapter/core/security_integration.py +285 -0
  31. mcp_proxy_adapter/core/server_adapter.py +345 -0
  32. mcp_proxy_adapter/core/server_engine.py +364 -0
  33. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  34. mcp_proxy_adapter/docs/EN/TROUBLESHOOTING.md +285 -0
  35. mcp_proxy_adapter/docs/RU/TROUBLESHOOTING.md +285 -0
  36. mcp_proxy_adapter/examples/README.md +230 -97
  37. mcp_proxy_adapter/examples/README_EN.md +258 -0
  38. mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
  39. mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
  40. mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
  41. mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +43 -0
  42. mcp_proxy_adapter/examples/basic_framework/configs/https_no_protocol_middleware.json +36 -0
  43. mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +29 -0
  44. mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_protocol_middleware.json +34 -0
  45. mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
  46. mcp_proxy_adapter/examples/basic_framework/configs/mtls_simple.json +35 -0
  47. mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
  48. mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
  49. mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
  50. mcp_proxy_adapter/examples/cert_config.json +9 -0
  51. mcp_proxy_adapter/examples/certs/admin.crt +32 -0
  52. mcp_proxy_adapter/examples/certs/admin.key +52 -0
  53. mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
  54. mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
  55. mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
  56. mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
  57. mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
  58. mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
  59. mcp_proxy_adapter/examples/certs/client.crt +32 -0
  60. mcp_proxy_adapter/examples/certs/client.key +52 -0
  61. mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
  62. mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
  63. mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
  64. mcp_proxy_adapter/examples/certs/client_user.key +52 -0
  65. mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
  66. mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
  67. mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
  68. mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
  69. mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
  70. mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
  71. mcp_proxy_adapter/examples/certs/readonly.key +52 -0
  72. mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
  73. mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
  74. mcp_proxy_adapter/examples/certs/server.crt +32 -0
  75. mcp_proxy_adapter/examples/certs/server.key +52 -0
  76. mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
  77. mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
  78. mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
  79. mcp_proxy_adapter/examples/certs/user.crt +32 -0
  80. mcp_proxy_adapter/examples/certs/user.key +52 -0
  81. mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
  82. mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
  83. mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
  84. mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
  85. mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
  86. mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
  87. mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
  88. mcp_proxy_adapter/examples/commands/__init__.py +1 -0
  89. mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
  90. mcp_proxy_adapter/examples/debug_request_state.py +144 -0
  91. mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
  92. mcp_proxy_adapter/examples/demo_client.py +341 -0
  93. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
  94. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
  95. mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
  96. mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
  97. mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
  98. mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
  99. mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
  100. mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
  101. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
  102. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
  103. mcp_proxy_adapter/examples/full_application/main.py +138 -0
  104. mcp_proxy_adapter/examples/full_application/roles.json +21 -0
  105. mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
  106. mcp_proxy_adapter/examples/generate_certificates.py +121 -0
  107. mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
  108. mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
  109. mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
  110. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
  111. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
  112. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
  113. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
  114. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
  115. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
  116. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
  117. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
  118. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
  119. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
  120. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
  121. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
  122. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
  123. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
  124. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
  125. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
  126. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
  127. mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
  128. mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
  129. mcp_proxy_adapter/examples/roles.json +38 -0
  130. mcp_proxy_adapter/examples/run_example.py +81 -0
  131. mcp_proxy_adapter/examples/run_security_tests.py +326 -0
  132. mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
  133. mcp_proxy_adapter/examples/security_test_client.py +743 -0
  134. mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
  135. mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
  136. mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
  137. mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
  138. mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
  139. mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
  140. mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
  141. mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
  142. mcp_proxy_adapter/examples/test_config_generator.py +110 -0
  143. mcp_proxy_adapter/examples/test_examples.py +344 -0
  144. mcp_proxy_adapter/examples/universal_client.py +628 -0
  145. mcp_proxy_adapter/main.py +21 -10
  146. mcp_proxy_adapter/utils/config_generator.py +727 -0
  147. mcp_proxy_adapter/version.py +5 -2
  148. mcp_proxy_adapter-6.1.1.dist-info/METADATA +205 -0
  149. mcp_proxy_adapter-6.1.1.dist-info/RECORD +197 -0
  150. mcp_proxy_adapter-6.1.1.dist-info/entry_points.txt +2 -0
  151. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.1.dist-info}/licenses/LICENSE +2 -2
  152. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  153. mcp_proxy_adapter/api/middleware/auth_adapter.py +0 -235
  154. mcp_proxy_adapter/api/middleware/mtls_adapter.py +0 -305
  155. mcp_proxy_adapter/api/middleware/mtls_middleware.py +0 -296
  156. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  157. mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +0 -241
  158. mcp_proxy_adapter/api/middleware/roles_adapter.py +0 -365
  159. mcp_proxy_adapter/api/middleware/roles_middleware.py +0 -381
  160. mcp_proxy_adapter/api/middleware/security.py +0 -376
  161. mcp_proxy_adapter/api/middleware/token_auth_middleware.py +0 -261
  162. mcp_proxy_adapter/examples/__init__.py +0 -7
  163. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  164. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  165. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  166. mcp_proxy_adapter/examples/basic_server/config.json +0 -70
  167. mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +0 -54
  168. mcp_proxy_adapter/examples/basic_server/config_http.json +0 -70
  169. mcp_proxy_adapter/examples/basic_server/config_http_only.json +0 -52
  170. mcp_proxy_adapter/examples/basic_server/config_https.json +0 -58
  171. mcp_proxy_adapter/examples/basic_server/config_mtls.json +0 -58
  172. mcp_proxy_adapter/examples/basic_server/config_ssl.json +0 -46
  173. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  174. mcp_proxy_adapter/examples/basic_server/server.py +0 -114
  175. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  176. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  177. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -566
  178. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  179. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  180. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  181. mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +0 -105
  182. mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +0 -129
  183. mcp_proxy_adapter/examples/custom_commands/config.json +0 -118
  184. mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +0 -46
  185. mcp_proxy_adapter/examples/custom_commands/config_https_only.json +0 -46
  186. mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +0 -33
  187. mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +0 -46
  188. mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +0 -33
  189. mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +0 -33
  190. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  191. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  192. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  193. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  194. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  195. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  196. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  197. mcp_proxy_adapter/examples/custom_commands/full_help_response.json +0 -1
  198. mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +0 -629
  199. mcp_proxy_adapter/examples/custom_commands/get_openapi.py +0 -103
  200. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  201. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  202. mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +0 -129
  203. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  204. mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +0 -278
  205. mcp_proxy_adapter/examples/custom_commands/server.py +0 -252
  206. mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +0 -75
  207. mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +0 -299
  208. mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +0 -278
  209. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  210. mcp_proxy_adapter/examples/custom_commands/test_openapi.py +0 -27
  211. mcp_proxy_adapter/examples/custom_commands/test_registry.py +0 -23
  212. mcp_proxy_adapter/examples/custom_commands/test_simple.py +0 -19
  213. mcp_proxy_adapter/examples/custom_project_example/README.md +0 -103
  214. mcp_proxy_adapter/examples/custom_project_example/README_EN.md +0 -103
  215. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  216. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  217. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  218. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  219. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  220. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  221. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  222. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  223. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  224. mcp_proxy_adapter/examples/simple_custom_commands/README.md +0 -149
  225. mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +0 -149
  226. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  227. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  228. mcp_proxy_adapter/schemas/roles_schema.json +0 -162
  229. mcp_proxy_adapter/tests/__init__.py +0 -0
  230. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  231. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  232. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  233. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  234. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  235. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  236. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  237. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  238. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  239. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  240. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  241. mcp_proxy_adapter/tests/conftest.py +0 -131
  242. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  243. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  244. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  245. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  246. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  247. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  248. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  249. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  250. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  251. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  252. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  253. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  254. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  255. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  256. mcp_proxy_adapter/tests/test_config.py +0 -127
  257. mcp_proxy_adapter/tests/test_utils.py +0 -65
  258. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  259. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  260. mcp_proxy_adapter/tests/unit/test_config.py +0 -270
  261. mcp_proxy_adapter-6.0.0.dist-info/METADATA +0 -201
  262. mcp_proxy_adapter-6.0.0.dist-info/RECORD +0 -179
  263. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.1.dist-info}/WHEEL +0 -0
  264. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.1.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()