mcp-proxy-adapter 6.9.43__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 (242) hide show
  1. mcp_proxy_adapter/__init__.py +47 -0
  2. mcp_proxy_adapter/__main__.py +13 -0
  3. mcp_proxy_adapter/api/__init__.py +0 -0
  4. mcp_proxy_adapter/api/app.py +66 -0
  5. mcp_proxy_adapter/api/core/__init__.py +18 -0
  6. mcp_proxy_adapter/api/core/app_factory.py +355 -0
  7. mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
  8. mcp_proxy_adapter/api/core/registration_context.py +356 -0
  9. mcp_proxy_adapter/api/core/registration_manager.py +266 -0
  10. mcp_proxy_adapter/api/core/registration_tasks.py +84 -0
  11. mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
  12. mcp_proxy_adapter/api/handlers.py +181 -0
  13. mcp_proxy_adapter/api/middleware/__init__.py +21 -0
  14. mcp_proxy_adapter/api/middleware/base.py +54 -0
  15. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +73 -0
  16. mcp_proxy_adapter/api/middleware/error_handling.py +76 -0
  17. mcp_proxy_adapter/api/middleware/factory.py +147 -0
  18. mcp_proxy_adapter/api/middleware/logging.py +31 -0
  19. mcp_proxy_adapter/api/middleware/performance.py +51 -0
  20. mcp_proxy_adapter/api/middleware/protocol_middleware.py +140 -0
  21. mcp_proxy_adapter/api/middleware/transport_middleware.py +87 -0
  22. mcp_proxy_adapter/api/middleware/unified_security.py +223 -0
  23. mcp_proxy_adapter/api/middleware/user_info_middleware.py +132 -0
  24. mcp_proxy_adapter/api/openapi/__init__.py +21 -0
  25. mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
  26. mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
  27. mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
  28. mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
  29. mcp_proxy_adapter/api/schemas.py +270 -0
  30. mcp_proxy_adapter/api/tool_integration.py +131 -0
  31. mcp_proxy_adapter/api/tools.py +163 -0
  32. mcp_proxy_adapter/cli/__init__.py +12 -0
  33. mcp_proxy_adapter/cli/commands/__init__.py +15 -0
  34. mcp_proxy_adapter/cli/commands/client.py +100 -0
  35. mcp_proxy_adapter/cli/commands/config_generate.py +35 -0
  36. mcp_proxy_adapter/cli/commands/config_validate.py +74 -0
  37. mcp_proxy_adapter/cli/commands/generate.py +259 -0
  38. mcp_proxy_adapter/cli/commands/server.py +174 -0
  39. mcp_proxy_adapter/cli/commands/sets.py +128 -0
  40. mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
  41. mcp_proxy_adapter/cli/examples/__init__.py +8 -0
  42. mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
  43. mcp_proxy_adapter/cli/examples/https_token.py +96 -0
  44. mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
  45. mcp_proxy_adapter/cli/main.py +63 -0
  46. mcp_proxy_adapter/cli/parser.py +338 -0
  47. mcp_proxy_adapter/cli/validators.py +231 -0
  48. mcp_proxy_adapter/client/jsonrpc_client/__init__.py +9 -0
  49. mcp_proxy_adapter/client/jsonrpc_client/client.py +42 -0
  50. mcp_proxy_adapter/client/jsonrpc_client/command_api.py +45 -0
  51. mcp_proxy_adapter/client/jsonrpc_client/proxy_api.py +224 -0
  52. mcp_proxy_adapter/client/jsonrpc_client/queue_api.py +60 -0
  53. mcp_proxy_adapter/client/jsonrpc_client/transport.py +108 -0
  54. mcp_proxy_adapter/client/proxy.py +123 -0
  55. mcp_proxy_adapter/commands/__init__.py +66 -0
  56. mcp_proxy_adapter/commands/auth_validation_command.py +69 -0
  57. mcp_proxy_adapter/commands/base.py +389 -0
  58. mcp_proxy_adapter/commands/builtin_commands.py +30 -0
  59. mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
  60. mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
  61. mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
  62. mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
  63. mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
  64. mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
  65. mcp_proxy_adapter/commands/catalog_manager.py +97 -0
  66. mcp_proxy_adapter/commands/cert_monitor_command.py +552 -0
  67. mcp_proxy_adapter/commands/certificate_management_command.py +562 -0
  68. mcp_proxy_adapter/commands/command_registry.py +298 -0
  69. mcp_proxy_adapter/commands/config_command.py +102 -0
  70. mcp_proxy_adapter/commands/dependency_container.py +40 -0
  71. mcp_proxy_adapter/commands/dependency_manager.py +143 -0
  72. mcp_proxy_adapter/commands/echo_command.py +48 -0
  73. mcp_proxy_adapter/commands/health_command.py +142 -0
  74. mcp_proxy_adapter/commands/help_command.py +175 -0
  75. mcp_proxy_adapter/commands/hooks.py +172 -0
  76. mcp_proxy_adapter/commands/key_management_command.py +484 -0
  77. mcp_proxy_adapter/commands/load_command.py +123 -0
  78. mcp_proxy_adapter/commands/plugins_command.py +246 -0
  79. mcp_proxy_adapter/commands/protocol_management_command.py +216 -0
  80. mcp_proxy_adapter/commands/proxy_registration_command.py +319 -0
  81. mcp_proxy_adapter/commands/queue_commands.py +750 -0
  82. mcp_proxy_adapter/commands/registration_status_command.py +76 -0
  83. mcp_proxy_adapter/commands/registry/__init__.py +18 -0
  84. mcp_proxy_adapter/commands/registry/command_info.py +103 -0
  85. mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
  86. mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
  87. mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
  88. mcp_proxy_adapter/commands/reload_command.py +136 -0
  89. mcp_proxy_adapter/commands/result.py +157 -0
  90. mcp_proxy_adapter/commands/role_test_command.py +99 -0
  91. mcp_proxy_adapter/commands/roles_management_command.py +502 -0
  92. mcp_proxy_adapter/commands/security_command.py +472 -0
  93. mcp_proxy_adapter/commands/settings_command.py +113 -0
  94. mcp_proxy_adapter/commands/ssl_setup_command.py +306 -0
  95. mcp_proxy_adapter/commands/token_management_command.py +500 -0
  96. mcp_proxy_adapter/commands/transport_management_command.py +129 -0
  97. mcp_proxy_adapter/commands/unload_command.py +92 -0
  98. mcp_proxy_adapter/config.py +32 -0
  99. mcp_proxy_adapter/core/__init__.py +8 -0
  100. mcp_proxy_adapter/core/app_factory.py +560 -0
  101. mcp_proxy_adapter/core/app_runner.py +318 -0
  102. mcp_proxy_adapter/core/auth_validator.py +508 -0
  103. mcp_proxy_adapter/core/certificate/__init__.py +20 -0
  104. mcp_proxy_adapter/core/certificate/certificate_creator.py +372 -0
  105. mcp_proxy_adapter/core/certificate/certificate_extractor.py +185 -0
  106. mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
  107. mcp_proxy_adapter/core/certificate/certificate_validator.py +388 -0
  108. mcp_proxy_adapter/core/certificate/ssl_context_manager.py +65 -0
  109. mcp_proxy_adapter/core/certificate_utils.py +249 -0
  110. mcp_proxy_adapter/core/client.py +608 -0
  111. mcp_proxy_adapter/core/client_manager.py +271 -0
  112. mcp_proxy_adapter/core/client_security.py +411 -0
  113. mcp_proxy_adapter/core/config/__init__.py +18 -0
  114. mcp_proxy_adapter/core/config/config.py +237 -0
  115. mcp_proxy_adapter/core/config/config_factory.py +22 -0
  116. mcp_proxy_adapter/core/config/config_loader.py +66 -0
  117. mcp_proxy_adapter/core/config/feature_manager.py +31 -0
  118. mcp_proxy_adapter/core/config/simple_config.py +116 -0
  119. mcp_proxy_adapter/core/config/simple_config_generator.py +100 -0
  120. mcp_proxy_adapter/core/config/simple_config_validator.py +380 -0
  121. mcp_proxy_adapter/core/config_converter.py +252 -0
  122. mcp_proxy_adapter/core/config_validator.py +211 -0
  123. mcp_proxy_adapter/core/crl_utils.py +362 -0
  124. mcp_proxy_adapter/core/errors.py +276 -0
  125. mcp_proxy_adapter/core/job_manager.py +54 -0
  126. mcp_proxy_adapter/core/logging.py +250 -0
  127. mcp_proxy_adapter/core/mtls_asgi.py +140 -0
  128. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  129. mcp_proxy_adapter/core/mtls_proxy.py +229 -0
  130. mcp_proxy_adapter/core/mtls_server.py +154 -0
  131. mcp_proxy_adapter/core/protocol_manager.py +232 -0
  132. mcp_proxy_adapter/core/proxy/__init__.py +19 -0
  133. mcp_proxy_adapter/core/proxy/auth_manager.py +26 -0
  134. mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +160 -0
  135. mcp_proxy_adapter/core/proxy/registration_client.py +186 -0
  136. mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
  137. mcp_proxy_adapter/core/proxy_client.py +184 -0
  138. mcp_proxy_adapter/core/proxy_registration.py +80 -0
  139. mcp_proxy_adapter/core/role_utils.py +103 -0
  140. mcp_proxy_adapter/core/security_adapter.py +343 -0
  141. mcp_proxy_adapter/core/security_factory.py +96 -0
  142. mcp_proxy_adapter/core/security_integration.py +342 -0
  143. mcp_proxy_adapter/core/server_adapter.py +251 -0
  144. mcp_proxy_adapter/core/server_engine.py +217 -0
  145. mcp_proxy_adapter/core/settings.py +260 -0
  146. mcp_proxy_adapter/core/signal_handler.py +107 -0
  147. mcp_proxy_adapter/core/ssl_utils.py +161 -0
  148. mcp_proxy_adapter/core/transport_manager.py +153 -0
  149. mcp_proxy_adapter/core/unified_config_adapter.py +471 -0
  150. mcp_proxy_adapter/core/utils.py +101 -0
  151. mcp_proxy_adapter/core/validation/__init__.py +21 -0
  152. mcp_proxy_adapter/core/validation/config_validator.py +219 -0
  153. mcp_proxy_adapter/core/validation/file_validator.py +131 -0
  154. mcp_proxy_adapter/core/validation/protocol_validator.py +190 -0
  155. mcp_proxy_adapter/core/validation/security_validator.py +140 -0
  156. mcp_proxy_adapter/core/validation/validation_result.py +27 -0
  157. mcp_proxy_adapter/custom_openapi.py +58 -0
  158. mcp_proxy_adapter/examples/__init__.py +16 -0
  159. mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
  160. mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
  161. mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
  162. mcp_proxy_adapter/examples/basic_framework/main.py +52 -0
  163. mcp_proxy_adapter/examples/bugfix_certificate_config.py +261 -0
  164. mcp_proxy_adapter/examples/cert_manager_bugfix.py +203 -0
  165. mcp_proxy_adapter/examples/check_config.py +413 -0
  166. mcp_proxy_adapter/examples/client_usage_example.py +164 -0
  167. mcp_proxy_adapter/examples/commands/__init__.py +5 -0
  168. mcp_proxy_adapter/examples/config_builder.py +234 -0
  169. mcp_proxy_adapter/examples/config_cli.py +282 -0
  170. mcp_proxy_adapter/examples/create_test_configs.py +174 -0
  171. mcp_proxy_adapter/examples/debug_request_state.py +130 -0
  172. mcp_proxy_adapter/examples/debug_role_chain.py +191 -0
  173. mcp_proxy_adapter/examples/demo_client.py +287 -0
  174. mcp_proxy_adapter/examples/full_application/__init__.py +13 -0
  175. mcp_proxy_adapter/examples/full_application/commands/__init__.py +8 -0
  176. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +45 -0
  177. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +52 -0
  178. mcp_proxy_adapter/examples/full_application/commands/echo_command.py +32 -0
  179. mcp_proxy_adapter/examples/full_application/commands/help_command.py +54 -0
  180. mcp_proxy_adapter/examples/full_application/commands/list_command.py +57 -0
  181. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +5 -0
  182. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +29 -0
  183. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +27 -0
  184. mcp_proxy_adapter/examples/full_application/main.py +264 -0
  185. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +81 -0
  186. mcp_proxy_adapter/examples/full_application/run_mtls.py +252 -0
  187. mcp_proxy_adapter/examples/full_application/run_simple.py +152 -0
  188. mcp_proxy_adapter/examples/full_application/test_minimal_server.py +45 -0
  189. mcp_proxy_adapter/examples/full_application/test_server.py +163 -0
  190. mcp_proxy_adapter/examples/full_application/test_simple_server.py +62 -0
  191. mcp_proxy_adapter/examples/generate_config.py +502 -0
  192. mcp_proxy_adapter/examples/proxy_registration_example.py +335 -0
  193. mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
  194. mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
  195. mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
  196. mcp_proxy_adapter/examples/queue_server_example.py +85 -0
  197. mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
  198. mcp_proxy_adapter/examples/required_certificates.py +208 -0
  199. mcp_proxy_adapter/examples/run_example.py +77 -0
  200. mcp_proxy_adapter/examples/run_full_test_suite.py +619 -0
  201. mcp_proxy_adapter/examples/run_proxy_server.py +153 -0
  202. mcp_proxy_adapter/examples/run_security_tests_fixed.py +435 -0
  203. mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
  204. mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
  205. mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
  206. mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
  207. mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
  208. mcp_proxy_adapter/examples/security_test_client.py +72 -0
  209. mcp_proxy_adapter/examples/setup/__init__.py +24 -0
  210. mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
  211. mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
  212. mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
  213. mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
  214. mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
  215. mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
  216. mcp_proxy_adapter/examples/setup_test_environment.py +235 -0
  217. mcp_proxy_adapter/examples/simple_protocol_test.py +125 -0
  218. mcp_proxy_adapter/examples/test_chk_hostname_automated.py +211 -0
  219. mcp_proxy_adapter/examples/test_config.py +205 -0
  220. mcp_proxy_adapter/examples/test_config_builder.py +110 -0
  221. mcp_proxy_adapter/examples/test_examples.py +308 -0
  222. mcp_proxy_adapter/examples/test_framework_complete.py +267 -0
  223. mcp_proxy_adapter/examples/test_mcp_server.py +187 -0
  224. mcp_proxy_adapter/examples/test_protocol_examples.py +337 -0
  225. mcp_proxy_adapter/examples/universal_client.py +674 -0
  226. mcp_proxy_adapter/examples/update_config_certificates.py +135 -0
  227. mcp_proxy_adapter/examples/validate_generator_compatibility.py +385 -0
  228. mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +61 -0
  229. mcp_proxy_adapter/integrations/__init__.py +25 -0
  230. mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
  231. mcp_proxy_adapter/main.py +313 -0
  232. mcp_proxy_adapter/openapi.py +375 -0
  233. mcp_proxy_adapter/schemas/base_schema.json +114 -0
  234. mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
  235. mcp_proxy_adapter/schemas/roles.json +37 -0
  236. mcp_proxy_adapter/schemas/roles_schema.json +162 -0
  237. mcp_proxy_adapter/version.py +5 -0
  238. mcp_proxy_adapter-6.9.43.dist-info/METADATA +739 -0
  239. mcp_proxy_adapter-6.9.43.dist-info/RECORD +242 -0
  240. mcp_proxy_adapter-6.9.43.dist-info/WHEEL +5 -0
  241. mcp_proxy_adapter-6.9.43.dist-info/entry_points.txt +12 -0
  242. mcp_proxy_adapter-6.9.43.dist-info/top_level.txt +1 -0
@@ -0,0 +1,356 @@
1
+ """Registration helper context builders for proxy interactions.
2
+
3
+ Author: Vasiliy Zdanovskiy
4
+ email: vasilyvz@gmail.com
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional, Tuple, Union
12
+
13
+
14
+ @dataclass
15
+ class ProxyCredentials:
16
+ """Client certificate and verification settings for proxy communication."""
17
+
18
+ cert: Optional[Tuple[str, str]]
19
+ verify: Union[bool, str]
20
+
21
+
22
+ @dataclass
23
+ class RegistrationContext:
24
+ """Prepared data required to register the adapter with a proxy."""
25
+
26
+ server_name: str
27
+ advertised_url: str
28
+ proxy_url: str
29
+ register_endpoint: str
30
+ capabilities: List[str]
31
+ metadata: Dict[str, Any]
32
+ use_proxy_client: bool
33
+ proxy_client_config: Dict[str, Any]
34
+ proxy_registration_config: Dict[str, Any]
35
+ credentials: ProxyCredentials
36
+
37
+
38
+ @dataclass
39
+ class HeartbeatSettings:
40
+ """Configuration for heartbeat scheduling."""
41
+
42
+ interval: int
43
+ endpoint: str
44
+
45
+
46
+ def prepare_registration_context(
47
+ config: Dict[str, Any], logger: Any
48
+ ) -> Optional[RegistrationContext]:
49
+ """Build registration context from configuration.
50
+
51
+ Returns ``None`` when registration should not be performed.
52
+ """
53
+
54
+ proxy_client_config = dict(config.get("proxy_client") or {})
55
+ proxy_registration_config = dict(config.get("proxy_registration") or {})
56
+ use_proxy_client = bool(proxy_client_config)
57
+
58
+ if use_proxy_client:
59
+ registration_config = proxy_client_config.get("registration") or {}
60
+ registration_enabled = registration_config.get("auto_on_startup", False)
61
+ else:
62
+ registration_config = proxy_registration_config
63
+ registration_enabled = proxy_registration_config.get("enabled", False)
64
+
65
+ if not registration_enabled:
66
+ logger.info(
67
+ "Proxy registration disabled (auto_on_startup=false or enabled=false)"
68
+ )
69
+ return None
70
+
71
+ if use_proxy_client:
72
+ proxy_host = proxy_client_config.get("host", "localhost")
73
+ proxy_port = proxy_client_config.get("port", 3005)
74
+ # Determine proxy scheme based on proxy protocol
75
+ # NOTE: proxy_client.protocol indicates the SERVER's protocol, not the proxy's protocol
76
+ # Determine proxy protocol:
77
+ # 1. Check explicit proxy_protocol field (if exists) - this is the proxy's actual protocol
78
+ # 2. Check if port is 3005 (test proxy) - always use HTTP for test proxy
79
+ # 3. Check if client certificates are provided AND exist on disk (indicates HTTPS/mTLS proxy)
80
+ # 4. Check server protocol as hint (if server uses HTTPS/mTLS, proxy might too)
81
+ # 5. Fallback to HTTP (for test proxy)
82
+ proxy_protocol = proxy_client_config.get("proxy_protocol")
83
+ if not proxy_protocol:
84
+ # Test proxy (port 3005) always uses HTTP
85
+ if proxy_port == 3005:
86
+ proxy_protocol = "http"
87
+ else:
88
+ # Check if client certificates are provided for proxy connection
89
+ cert_file = proxy_client_config.get("cert_file")
90
+ key_file = proxy_client_config.get("key_file")
91
+ ca_cert_file = proxy_client_config.get("ca_cert_file")
92
+ cert_tuple = _build_cert_tuple(cert_file, key_file)
93
+
94
+ if cert_tuple:
95
+ # Certificates exist and are valid - proxy uses HTTPS/mTLS
96
+ # Certificates in proxy_client are for connecting TO the proxy
97
+ # Check if CA cert is provided to determine if it's mTLS
98
+ if ca_cert_file and Path(ca_cert_file).exists():
99
+ # All certificates (cert, key, CA) provided - likely mTLS
100
+ # Use server protocol as hint for mTLS vs HTTPS
101
+ server_protocol = proxy_client_config.get("protocol", "http")
102
+ proxy_protocol = (
103
+ "mtls" if server_protocol == "mtls" else "https"
104
+ )
105
+ else:
106
+ # Only cert and key, no CA - HTTPS
107
+ proxy_protocol = "https"
108
+ else:
109
+ # No valid certificates - check server protocol as hint
110
+ server_protocol = proxy_client_config.get("protocol", "http")
111
+ if server_protocol in ("https", "mtls"):
112
+ # Server uses HTTPS/mTLS - proxy might too, but no certs provided
113
+ # Default to HTTP for safety (test proxy scenario)
114
+ proxy_protocol = "http"
115
+ else:
116
+ # Server uses HTTP - proxy likely uses HTTP too
117
+ proxy_protocol = "http"
118
+ # If proxy uses HTTPS/mTLS, use https scheme
119
+ proxy_scheme = "https" if proxy_protocol in ("https", "mtls") else "http"
120
+ proxy_base_url = f"{proxy_scheme}://{proxy_host}:{proxy_port}"
121
+ register_endpoint = registration_config.get("register_endpoint", "/register")
122
+ if register_endpoint.startswith("/proxy/"):
123
+ proxy_url = f"{proxy_base_url}/proxy"
124
+ else:
125
+ proxy_url = proxy_base_url
126
+ else:
127
+ proxy_url_candidate = (
128
+ proxy_registration_config.get("proxy_url")
129
+ or proxy_registration_config.get("server_url")
130
+ )
131
+ if not proxy_url_candidate:
132
+ logger.warning("No proxy server URL configured")
133
+ return None
134
+ proxy_url = str(proxy_url_candidate)
135
+ register_endpoint = "/register"
136
+
137
+ server_config = dict(config.get("server") or {})
138
+ host = server_config.get("host", "127.0.0.1")
139
+ port = server_config.get("port", 8000)
140
+ protocol = server_config.get("protocol", "http")
141
+ advertised_host = server_config.get("advertised_host") or host
142
+ scheme = "https" if protocol in ("https", "mtls") else "http"
143
+ advertised_url = f"{scheme}://{advertised_host}:{port}"
144
+
145
+ if use_proxy_client:
146
+ server_name = proxy_client_config.get("server_id") or proxy_client_config.get(
147
+ "server_name"
148
+ )
149
+ server_name = server_name or f"mcp-adapter-{host}-{port}"
150
+ capabilities = proxy_client_config.get("capabilities", ["jsonrpc", "health"])
151
+ metadata = {
152
+ "uuid": config.get("uuid"),
153
+ "protocol": protocol,
154
+ "host": host,
155
+ "port": port,
156
+ **(proxy_client_config.get("metadata") or {}),
157
+ }
158
+ else:
159
+ server_name = proxy_registration_config.get(
160
+ "server_id"
161
+ ) or proxy_registration_config.get("server_name")
162
+ server_name = server_name or f"mcp-adapter-{host}-{port}"
163
+ capabilities = proxy_registration_config.get(
164
+ "capabilities", ["jsonrpc", "health"]
165
+ )
166
+ metadata = {
167
+ "uuid": config.get("uuid"),
168
+ "protocol": protocol,
169
+ "host": host,
170
+ "port": port,
171
+ **(proxy_registration_config.get("metadata") or {}),
172
+ }
173
+
174
+ credentials = _resolve_registration_credentials(
175
+ use_proxy_client, proxy_client_config, proxy_registration_config
176
+ )
177
+
178
+ return RegistrationContext(
179
+ server_name=server_name,
180
+ advertised_url=advertised_url,
181
+ proxy_url=proxy_url,
182
+ register_endpoint=register_endpoint,
183
+ capabilities=capabilities,
184
+ metadata=metadata,
185
+ use_proxy_client=use_proxy_client,
186
+ proxy_client_config=proxy_client_config,
187
+ proxy_registration_config=proxy_registration_config,
188
+ credentials=credentials,
189
+ )
190
+
191
+
192
+ def resolve_runtime_credentials(
193
+ use_proxy_client: bool,
194
+ proxy_client_config: Dict[str, Any],
195
+ proxy_registration_config: Dict[str, Any],
196
+ ) -> ProxyCredentials:
197
+ """Return credentials for runtime interactions (heartbeat, unregister)."""
198
+
199
+ if use_proxy_client:
200
+ cert_tuple = _build_cert_tuple(
201
+ proxy_client_config.get("cert_file"),
202
+ proxy_client_config.get("key_file"),
203
+ )
204
+ proxy_protocol = proxy_client_config.get("protocol", "http")
205
+ ca_cert = proxy_client_config.get("ca_cert_file")
206
+
207
+ verify: Union[bool, str] = True
208
+ if proxy_protocol == "http":
209
+ verify = False
210
+ elif ca_cert:
211
+ verify = ca_cert
212
+
213
+ return ProxyCredentials(cert=cert_tuple, verify=verify)
214
+
215
+ return _resolve_registration_credentials(False, {}, proxy_registration_config)
216
+
217
+
218
+ def resolve_heartbeat_settings(
219
+ use_proxy_client: bool,
220
+ proxy_client_config: Dict[str, Any],
221
+ proxy_registration_config: Dict[str, Any],
222
+ ) -> HeartbeatSettings:
223
+ """Compute heartbeat interval and endpoint."""
224
+
225
+ if use_proxy_client:
226
+ heartbeat_config = proxy_client_config.get("heartbeat") or {}
227
+ interval = int(heartbeat_config.get("interval", 30))
228
+ endpoint = heartbeat_config.get("endpoint", "/heartbeat")
229
+ else:
230
+ heartbeat_config = proxy_registration_config.get("heartbeat") or {}
231
+ interval = int(
232
+ heartbeat_config.get(
233
+ "interval", proxy_registration_config.get("heartbeat_interval", 30)
234
+ )
235
+ )
236
+ endpoint = "/heartbeat"
237
+
238
+ return HeartbeatSettings(interval=interval, endpoint=endpoint)
239
+
240
+
241
+ def resolve_unregister_endpoint(
242
+ use_proxy_client: bool,
243
+ proxy_client_config: Dict[str, Any],
244
+ ) -> str:
245
+ """Get unregister endpoint path."""
246
+
247
+ if use_proxy_client:
248
+ registration_config = proxy_client_config.get("registration") or {}
249
+ endpoint = registration_config.get("unregister_endpoint")
250
+ if endpoint:
251
+ return str(endpoint)
252
+ return "/unregister"
253
+ return "/unregister"
254
+
255
+
256
+ def _resolve_registration_credentials(
257
+ use_proxy_client: bool,
258
+ proxy_client_config: Dict[str, Any],
259
+ proxy_registration_config: Dict[str, Any],
260
+ ) -> ProxyCredentials:
261
+ if use_proxy_client:
262
+ # Use same logic as resolve_runtime_credentials for consistency
263
+ # Check if proxy itself uses HTTPS/mTLS and requires certificates
264
+ cert_tuple = _build_cert_tuple(
265
+ proxy_client_config.get("cert_file"),
266
+ proxy_client_config.get("key_file"),
267
+ )
268
+ # Determine proxy protocol (same logic as in prepare_registration_context)
269
+ proxy_protocol = proxy_client_config.get("proxy_protocol")
270
+ if not proxy_protocol:
271
+ # Get proxy port for test proxy detection
272
+ proxy_port = proxy_client_config.get("port", 3005)
273
+ # Test proxy (port 3005) always uses HTTP
274
+ if proxy_port == 3005:
275
+ proxy_protocol = "http"
276
+ else:
277
+ # Check if client certificates are provided for proxy connection
278
+ cert_file = proxy_client_config.get("cert_file")
279
+ key_file = proxy_client_config.get("key_file")
280
+ ca_cert_file = proxy_client_config.get("ca_cert_file")
281
+ cert_tuple = _build_cert_tuple(cert_file, key_file)
282
+
283
+ if cert_tuple:
284
+ # Certificates exist and are valid - proxy uses HTTPS/mTLS
285
+ # Check if CA cert is provided to determine if it's mTLS
286
+ if ca_cert_file and Path(ca_cert_file).exists():
287
+ # All certificates (cert, key, CA) provided - likely mTLS
288
+ # Use server protocol as hint for mTLS vs HTTPS
289
+ server_protocol = proxy_client_config.get("protocol", "http")
290
+ proxy_protocol = (
291
+ "mtls" if server_protocol == "mtls" else "https"
292
+ )
293
+ else:
294
+ # Only cert and key, no CA - HTTPS
295
+ proxy_protocol = "https"
296
+ else:
297
+ # No valid certificates - check server protocol as hint
298
+ server_protocol = proxy_client_config.get("protocol", "http")
299
+ if server_protocol in ("https", "mtls"):
300
+ # Server uses HTTPS/mTLS - proxy might too, but no certs provided
301
+ # Default to HTTP for safety (test proxy scenario)
302
+ proxy_protocol = "http"
303
+ else:
304
+ # Server uses HTTP - proxy likely uses HTTP too
305
+ proxy_protocol = "http"
306
+ ca_cert = proxy_client_config.get("ca_cert_file")
307
+
308
+ verify: Union[bool, str] = True
309
+ if proxy_protocol == "http":
310
+ # HTTP proxy doesn't need certificates
311
+ verify = False
312
+ elif proxy_protocol in ("https", "mtls"):
313
+ # HTTPS/mTLS proxy requires certificates
314
+ if ca_cert:
315
+ verify = ca_cert
316
+ elif proxy_protocol == "mtls":
317
+ # mTLS requires CA cert
318
+ verify = True # Will fail if no CA, but that's expected
319
+ else:
320
+ # HTTPS can use system CA store
321
+ verify = True
322
+
323
+ return ProxyCredentials(cert=cert_tuple, verify=verify)
324
+
325
+ cert_config = proxy_registration_config.get("certificate") or {}
326
+ ssl_config = proxy_registration_config.get("ssl") or {}
327
+
328
+ cert_tuple = _build_cert_tuple(
329
+ cert_config.get("cert_file"),
330
+ cert_config.get("key_file"),
331
+ )
332
+
333
+ verify: Union[bool, str] = True
334
+ ca_cert = ssl_config.get("ca_cert")
335
+ verify_mode = ssl_config.get("verify_mode", "CERT_REQUIRED")
336
+ if verify_mode == "CERT_NONE":
337
+ verify = False
338
+ elif ca_cert:
339
+ verify = ca_cert
340
+
341
+ return ProxyCredentials(cert=cert_tuple, verify=verify)
342
+
343
+
344
+ def _build_cert_tuple(
345
+ cert_file: Optional[str],
346
+ key_file: Optional[str],
347
+ ) -> Optional[Tuple[str, str]]:
348
+ if not cert_file or not key_file:
349
+ return None
350
+
351
+ cert_path = Path(cert_file)
352
+ key_path = Path(key_file)
353
+ if not cert_path.exists() or not key_path.exists():
354
+ return None
355
+
356
+ return (str(cert_path.absolute()), str(key_path.absolute()))
@@ -0,0 +1,266 @@
1
+ """
2
+ Author: Vasiliy Zdanovskiy
3
+ email: vasilyvz@gmail.com
4
+
5
+ Registration management utilities for MCP Proxy Adapter API.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient
14
+ from mcp_proxy_adapter.core.logging import get_global_logger
15
+
16
+ from mcp_proxy_adapter.api.core.registration_context import (
17
+ HeartbeatSettings,
18
+ ProxyCredentials,
19
+ RegistrationContext,
20
+ prepare_registration_context,
21
+ resolve_heartbeat_settings,
22
+ resolve_runtime_credentials,
23
+ resolve_unregister_endpoint,
24
+ )
25
+ from mcp_proxy_adapter.api.core.registration_tasks import (
26
+ create_heartbeat_task,
27
+ unregister_from_proxy as unregister_task,
28
+ )
29
+
30
+
31
+ class RegistrationManager:
32
+ """Manager for proxy registration functionality using JsonRpcClient."""
33
+
34
+ def __init__(self) -> None:
35
+ """Initialize registration manager."""
36
+ self.logger = get_global_logger()
37
+ self.registered = False
38
+ self.registration_task: Optional[asyncio.Task] = None
39
+ self.server_name: Optional[str] = None
40
+ self.server_url: Optional[str] = None
41
+ self.proxy_url: Optional[str] = None
42
+ self.capabilities: List[str] = []
43
+ self.metadata: Dict[str, Any] = {}
44
+ self.config: Optional[Dict[str, Any]] = None
45
+ self._use_proxy_client = False
46
+ self._proxy_client_config: Dict[str, Any] = {}
47
+ self._proxy_registration_config: Dict[str, Any] = {}
48
+ self._registration_credentials: Optional[ProxyCredentials] = None
49
+ self._runtime_credentials: Optional[ProxyCredentials] = None
50
+ self._register_endpoint: str = "/register"
51
+ self._heartbeat_settings: Optional[HeartbeatSettings] = None
52
+
53
+ async def register_with_proxy(self, config: Dict[str, Any]) -> bool:
54
+ """
55
+ Register this server with the proxy using JsonRpcClient.
56
+
57
+ Supports both ``proxy_client`` (SimpleConfig format) and ``proxy_registration`` (legacy format).
58
+ Registration is controlled by ``registration.auto_on_startup`` rather than ``proxy_client.enabled``.
59
+ """
60
+
61
+ context = prepare_registration_context(config, self.logger)
62
+ if context is None:
63
+ return True
64
+
65
+ self._apply_context(context, config)
66
+
67
+ proxy_url = self.proxy_url
68
+ assert proxy_url is not None
69
+ assert self.server_name is not None
70
+ assert self.server_url is not None
71
+
72
+ client = JsonRpcClient(protocol="http", host="127.0.0.1", port=8080)
73
+
74
+ async def _register() -> Dict[str, Any]:
75
+ self._log_credentials("🔐 Registration SSL config", context.credentials)
76
+ self.logger.info(f"📡 Connecting to proxy: {proxy_url}")
77
+ self.logger.debug(
78
+ " Endpoint: %s, Server: %s -> %s",
79
+ self._register_endpoint,
80
+ self.server_name,
81
+ self.server_url,
82
+ )
83
+ return await client.register_with_proxy(
84
+ proxy_url=proxy_url,
85
+ server_name=context.server_name,
86
+ server_url=context.advertised_url,
87
+ capabilities=self.capabilities,
88
+ metadata=self.metadata,
89
+ cert=context.credentials.cert,
90
+ verify=context.credentials.verify,
91
+ )
92
+
93
+ max_retries = 5
94
+ retry_delay = 2
95
+
96
+ try:
97
+ for attempt in range(max_retries):
98
+ try:
99
+ registration_response = await _register()
100
+ if registration_response is not None:
101
+ self.logger.debug(
102
+ "Proxy registration response payload: %s",
103
+ registration_response,
104
+ )
105
+ self.logger.info(
106
+ "✅ Successfully registered with proxy as %s -> %s",
107
+ self.server_name,
108
+ self.server_url,
109
+ )
110
+ self.registered = True
111
+ return True
112
+ except Exception as exc: # noqa: BLE001
113
+ full_error = self._format_httpx_error(exc)
114
+ if attempt < max_retries - 1:
115
+ self.logger.warning(
116
+ "⚠️ Registration attempt %s/%s failed: %s. Retrying in %ss...",
117
+ attempt + 1,
118
+ max_retries,
119
+ full_error,
120
+ retry_delay,
121
+ )
122
+ await asyncio.sleep(retry_delay)
123
+ retry_delay *= 2
124
+ else:
125
+ self.logger.error(
126
+ "❌ Failed to register with proxy after %s attempts: %s",
127
+ max_retries,
128
+ full_error,
129
+ )
130
+ return False
131
+ except Exception as exc: # noqa: BLE001
132
+ self.logger.error(f"❌ Registration error: {exc}")
133
+ return False
134
+ finally:
135
+ await client.close()
136
+
137
+ async def start_heartbeat(self, _config: Dict[str, Any]) -> None:
138
+ """Start heartbeat task using JsonRpcClient."""
139
+
140
+ if not self._can_start_tasks() or not self.registered:
141
+ return
142
+
143
+ credentials = resolve_runtime_credentials(
144
+ self._use_proxy_client,
145
+ self._proxy_client_config,
146
+ self._proxy_registration_config,
147
+ )
148
+ settings = resolve_heartbeat_settings(
149
+ self._use_proxy_client,
150
+ self._proxy_client_config,
151
+ self._proxy_registration_config,
152
+ )
153
+ self._runtime_credentials = credentials
154
+ self._heartbeat_settings = settings
155
+
156
+ heartbeat_url = f"{self.proxy_url}{settings.endpoint}"
157
+ self.logger.info(
158
+ "💓 Starting heartbeat task (interval: %ss)", settings.interval
159
+ )
160
+
161
+ assert self.server_name is not None
162
+ assert self.server_url is not None
163
+
164
+ self.registration_task = create_heartbeat_task(
165
+ proxy_url=heartbeat_url,
166
+ server_name=self.server_name,
167
+ server_url=self.server_url,
168
+ capabilities=self.capabilities,
169
+ metadata=self.metadata,
170
+ settings=settings,
171
+ credentials=credentials,
172
+ logger=self.logger,
173
+ )
174
+
175
+ async def stop(self) -> None:
176
+ """Stop registration manager and unregister from proxy."""
177
+
178
+ if self.registration_task:
179
+ self.registration_task.cancel()
180
+ try:
181
+ await self.registration_task
182
+ except asyncio.CancelledError:
183
+ pass
184
+ self.registration_task = None
185
+
186
+ if not (self.registered and self._can_start_tasks() and self.config):
187
+ self.registered = False
188
+ return
189
+
190
+ credentials = self._runtime_credentials or resolve_runtime_credentials(
191
+ self._use_proxy_client,
192
+ self._proxy_client_config,
193
+ self._proxy_registration_config,
194
+ )
195
+ endpoint = resolve_unregister_endpoint(
196
+ self._use_proxy_client,
197
+ self._proxy_client_config,
198
+ )
199
+
200
+ assert self.proxy_url is not None
201
+ assert self.server_name is not None
202
+
203
+ try:
204
+ await unregister_task(
205
+ proxy_url=self.proxy_url,
206
+ server_name=self.server_name,
207
+ endpoint=endpoint,
208
+ credentials=credentials,
209
+ logger=self.logger,
210
+ )
211
+ except Exception as exc: # noqa: BLE001
212
+ self.logger.error(f"Error unregistering from proxy: {exc}")
213
+ finally:
214
+ self.registered = False
215
+
216
+ def _apply_context(
217
+ self, context: RegistrationContext, config: Dict[str, Any]
218
+ ) -> None:
219
+ self.server_name = context.server_name
220
+ self.server_url = context.advertised_url
221
+ self.proxy_url = context.proxy_url
222
+ self.capabilities = list(context.capabilities)
223
+ self.metadata = dict(context.metadata)
224
+ self.config = config
225
+ self._use_proxy_client = context.use_proxy_client
226
+ self._proxy_client_config = context.proxy_client_config
227
+ self._proxy_registration_config = context.proxy_registration_config
228
+ self._registration_credentials = context.credentials
229
+ self._runtime_credentials = None
230
+ self._register_endpoint = context.register_endpoint
231
+
232
+ def _log_credentials(self, prefix: str, credentials: ProxyCredentials) -> None:
233
+ self.logger.info(
234
+ "%s: cert=%s, verify=%s",
235
+ prefix,
236
+ credentials.cert is not None,
237
+ credentials.verify,
238
+ )
239
+ if credentials.cert:
240
+ self.logger.debug(" Client cert: %s, key: %s", *credentials.cert)
241
+ if isinstance(credentials.verify, str):
242
+ self.logger.debug(" CA cert: %s", credentials.verify)
243
+
244
+ def _format_httpx_error(self, exc: Exception) -> str:
245
+ import httpx
246
+
247
+ error_msg = str(exc) or type(exc).__name__
248
+ details: List[str] = [f"type={type(exc).__name__}"]
249
+
250
+ if isinstance(exc, httpx.HTTPStatusError):
251
+ details.append(f"status={exc.response.status_code}")
252
+ try:
253
+ details.append(f"response={exc.response.text[:200]}")
254
+ except Exception: # noqa: BLE001
255
+ pass
256
+ elif isinstance(exc, httpx.ConnectError):
257
+ details.append("connection_failed")
258
+ if hasattr(exc, "request"):
259
+ details.append(f"url={exc.request.url}")
260
+ elif isinstance(exc, httpx.TimeoutException):
261
+ details.append("timeout")
262
+
263
+ return f"{error_msg} ({', '.join(details)})" if details else error_msg
264
+
265
+ def _can_start_tasks(self) -> bool:
266
+ return bool(self.proxy_url and self.server_name and self.server_url)