mcp-proxy-adapter 4.1.1__py3-none-any.whl → 6.0.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 (101) hide show
  1. mcp_proxy_adapter/__main__.py +12 -0
  2. mcp_proxy_adapter/api/app.py +138 -11
  3. mcp_proxy_adapter/api/handlers.py +16 -1
  4. mcp_proxy_adapter/api/middleware/__init__.py +30 -29
  5. mcp_proxy_adapter/api/middleware/auth_adapter.py +235 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +219 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/mtls_adapter.py +305 -0
  10. mcp_proxy_adapter/api/middleware/mtls_middleware.py +296 -0
  11. mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
  12. mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +241 -0
  13. mcp_proxy_adapter/api/middleware/roles_adapter.py +365 -0
  14. mcp_proxy_adapter/api/middleware/roles_middleware.py +381 -0
  15. mcp_proxy_adapter/api/middleware/security.py +376 -0
  16. mcp_proxy_adapter/api/middleware/token_auth_middleware.py +261 -0
  17. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  18. mcp_proxy_adapter/commands/__init__.py +13 -4
  19. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  20. mcp_proxy_adapter/commands/base.py +61 -30
  21. mcp_proxy_adapter/commands/builtin_commands.py +89 -0
  22. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  23. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  24. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  25. mcp_proxy_adapter/commands/command_registry.py +703 -354
  26. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  27. mcp_proxy_adapter/commands/health_command.py +7 -0
  28. mcp_proxy_adapter/commands/hooks.py +200 -167
  29. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  30. mcp_proxy_adapter/commands/load_command.py +176 -0
  31. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  32. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  33. mcp_proxy_adapter/commands/proxy_registration_command.py +268 -0
  34. mcp_proxy_adapter/commands/reload_command.py +48 -50
  35. mcp_proxy_adapter/commands/result.py +1 -0
  36. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  37. mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
  38. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  39. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  40. mcp_proxy_adapter/commands/unload_command.py +158 -0
  41. mcp_proxy_adapter/config.py +99 -2
  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/config_converter.py +405 -0
  45. mcp_proxy_adapter/core/config_validator.py +218 -0
  46. mcp_proxy_adapter/core/logging.py +11 -0
  47. mcp_proxy_adapter/core/protocol_manager.py +226 -0
  48. mcp_proxy_adapter/core/proxy_registration.py +270 -0
  49. mcp_proxy_adapter/core/role_utils.py +426 -0
  50. mcp_proxy_adapter/core/security_adapter.py +373 -0
  51. mcp_proxy_adapter/core/security_factory.py +239 -0
  52. mcp_proxy_adapter/core/settings.py +1 -0
  53. mcp_proxy_adapter/core/ssl_utils.py +233 -0
  54. mcp_proxy_adapter/core/transport_manager.py +292 -0
  55. mcp_proxy_adapter/custom_openapi.py +22 -11
  56. mcp_proxy_adapter/examples/basic_server/config.json +58 -23
  57. mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +54 -0
  58. mcp_proxy_adapter/examples/basic_server/config_http.json +70 -0
  59. mcp_proxy_adapter/examples/basic_server/config_http_only.json +52 -0
  60. mcp_proxy_adapter/examples/basic_server/config_https.json +58 -0
  61. mcp_proxy_adapter/examples/basic_server/config_mtls.json +58 -0
  62. mcp_proxy_adapter/examples/basic_server/config_ssl.json +46 -0
  63. mcp_proxy_adapter/examples/basic_server/server.py +12 -1
  64. mcp_proxy_adapter/examples/custom_commands/__init__.py +1 -1
  65. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +339 -23
  66. mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +105 -0
  67. mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +129 -0
  68. mcp_proxy_adapter/examples/custom_commands/config.json +101 -18
  69. mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +46 -0
  70. mcp_proxy_adapter/examples/custom_commands/config_https_only.json +46 -0
  71. mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +33 -0
  72. mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +46 -0
  73. mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +33 -0
  74. mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +33 -0
  75. mcp_proxy_adapter/examples/custom_commands/full_help_response.json +1 -0
  76. mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +629 -0
  77. mcp_proxy_adapter/examples/custom_commands/get_openapi.py +103 -0
  78. mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +129 -0
  79. mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +278 -0
  80. mcp_proxy_adapter/examples/custom_commands/server.py +92 -68
  81. mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +75 -0
  82. mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +299 -0
  83. mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +278 -0
  84. mcp_proxy_adapter/examples/custom_commands/test_openapi.py +27 -0
  85. mcp_proxy_adapter/examples/custom_commands/test_registry.py +23 -0
  86. mcp_proxy_adapter/examples/custom_commands/test_simple.py +19 -0
  87. mcp_proxy_adapter/examples/custom_project_example/README.md +103 -0
  88. mcp_proxy_adapter/examples/custom_project_example/README_EN.md +103 -0
  89. mcp_proxy_adapter/examples/simple_custom_commands/README.md +149 -0
  90. mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +149 -0
  91. mcp_proxy_adapter/main.py +175 -0
  92. mcp_proxy_adapter/schemas/roles_schema.json +162 -0
  93. mcp_proxy_adapter/tests/unit/test_config.py +53 -0
  94. mcp_proxy_adapter/version.py +1 -1
  95. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/METADATA +2 -1
  96. mcp_proxy_adapter-6.0.0.dist-info/RECORD +179 -0
  97. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  98. mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
  99. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/WHEEL +0 -0
  100. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/licenses/LICENSE +0 -0
  101. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to get OpenAPI schema directly from the code.
4
+ """
5
+
6
+ import json
7
+ import asyncio
8
+ import sys
9
+ import os
10
+ from pathlib import Path
11
+
12
+ # Add current directory to path for imports
13
+ sys.path.insert(0, os.getcwd())
14
+
15
+ from mcp_proxy_adapter.custom_openapi import CustomOpenAPIGenerator
16
+ from mcp_proxy_adapter.commands.command_registry import registry
17
+
18
+ async def main():
19
+ """Generate and save OpenAPI schema."""
20
+ try:
21
+ # Initialize commands first
22
+ print("🔄 Initializing commands...")
23
+ result = await registry.reload_system()
24
+ print(f"✅ Commands initialized: {result}")
25
+
26
+ # Load custom commands manually
27
+ print("🔄 Loading custom commands...")
28
+ custom_commands = [
29
+ "echo_command",
30
+ "custom_help_command",
31
+ "custom_health_command",
32
+ "manual_echo_command"
33
+ ]
34
+
35
+ for cmd_name in custom_commands:
36
+ try:
37
+ # Import the command module
38
+ module = __import__(cmd_name)
39
+ print(f"✅ Loaded custom command: {cmd_name}")
40
+
41
+ # Try to register the command if it has a command class
42
+ if hasattr(module, 'EchoCommand'):
43
+ registry.register_custom(module.EchoCommand())
44
+ print(f"✅ Registered EchoCommand")
45
+ elif hasattr(module, 'CustomHelpCommand'):
46
+ registry.register_custom(module.CustomHelpCommand())
47
+ print(f"✅ Registered CustomHelpCommand")
48
+ elif hasattr(module, 'CustomHealthCommand'):
49
+ registry.register_custom(module.CustomHealthCommand())
50
+ print(f"✅ Registered CustomHealthCommand")
51
+ elif hasattr(module, 'ManualEchoCommand'):
52
+ registry.register_custom(module.ManualEchoCommand())
53
+ print(f"✅ Registered ManualEchoCommand")
54
+
55
+ except Exception as e:
56
+ print(f"⚠️ Failed to load {cmd_name}: {e}")
57
+
58
+ # Get commands after registration
59
+ print("📋 Getting commands after registration...")
60
+ commands = registry.get_all_commands()
61
+ print(f" Total commands: {len(commands)}")
62
+ print(f" Command names: {list(commands.keys())}")
63
+
64
+ # Create generator
65
+ generator = CustomOpenAPIGenerator()
66
+
67
+ # Generate schema
68
+ schema = generator.generate(
69
+ title="Extended MCP Proxy Server",
70
+ description="Advanced MCP Proxy Adapter server with custom commands and hooks",
71
+ version="2.1.0"
72
+ )
73
+
74
+ # Save to file
75
+ with open("generated_openapi.json", "w", encoding="utf-8") as f:
76
+ json.dump(schema, f, indent=2, ensure_ascii=False)
77
+
78
+ print("✅ OpenAPI schema generated successfully!")
79
+ print(f"📄 Saved to: generated_openapi.json")
80
+ print(f"📊 Schema size: {len(json.dumps(schema))} characters")
81
+
82
+ # Show basic info
83
+ print(f"\n📋 Schema info:")
84
+ print(f" Title: {schema['info']['title']}")
85
+ print(f" Version: {schema['info']['version']}")
86
+
87
+ # Get commands info
88
+ commands = registry.get_all_commands()
89
+ print(f" Commands: {len(commands)}")
90
+ print(f" Command names: {list(commands.keys())}")
91
+
92
+ # Check CommandRequest enum
93
+ command_enum = schema['components']['schemas'].get('CommandRequest', {}).get('properties', {}).get('command', {}).get('enum', [])
94
+ print(f" Commands in schema: {len(command_enum)}")
95
+ print(f" Schema commands: {command_enum}")
96
+
97
+ except Exception as e:
98
+ print(f"❌ Error generating schema: {e}")
99
+ import traceback
100
+ traceback.print_exc()
101
+
102
+ if __name__ == "__main__":
103
+ asyncio.run(main())
@@ -0,0 +1,129 @@
1
+ """
2
+ Test command for loadable commands testing.
3
+
4
+ This command demonstrates the loadable commands functionality.
5
+ """
6
+
7
+ from typing import Any, Dict
8
+
9
+ from mcp_proxy_adapter.commands.base import Command
10
+ from mcp_proxy_adapter.commands.result import SuccessResult
11
+
12
+
13
+ class TestCommandResult:
14
+ """
15
+ Result of test command execution.
16
+ """
17
+
18
+ def __init__(self, message: str, test_data: Dict[str, Any]):
19
+ """
20
+ Initialize test command result.
21
+
22
+ Args:
23
+ message: Result message
24
+ test_data: Test data
25
+ """
26
+ self.message = message
27
+ self.test_data = test_data
28
+
29
+ def to_dict(self) -> Dict[str, Any]:
30
+ """
31
+ Convert result to dictionary.
32
+
33
+ Returns:
34
+ Dictionary representation of the result.
35
+ """
36
+ return {
37
+ "success": True,
38
+ "message": self.message,
39
+ "test_data": self.test_data
40
+ }
41
+
42
+ @classmethod
43
+ def get_schema(cls) -> Dict[str, Any]:
44
+ """
45
+ Get JSON schema for the result.
46
+
47
+ Returns:
48
+ JSON schema dictionary.
49
+ """
50
+ return {
51
+ "type": "object",
52
+ "properties": {
53
+ "success": {
54
+ "type": "boolean",
55
+ "description": "Whether command was successful"
56
+ },
57
+ "message": {
58
+ "type": "string",
59
+ "description": "Result message"
60
+ },
61
+ "test_data": {
62
+ "type": "object",
63
+ "description": "Test data"
64
+ }
65
+ },
66
+ "required": ["success", "message", "test_data"]
67
+ }
68
+
69
+
70
+ class TestCommand(Command):
71
+ """
72
+ Test command for loadable commands testing.
73
+ """
74
+
75
+ name = "test"
76
+ result_class = TestCommandResult
77
+
78
+ async def execute(self, **kwargs) -> TestCommandResult:
79
+ """
80
+ Execute test command.
81
+
82
+ Args:
83
+ **kwargs: Command parameters
84
+
85
+ Returns:
86
+ TestCommandResult with test information
87
+ """
88
+ # Get parameters
89
+ test_param = kwargs.get("test_param", "default_value")
90
+ echo_text = kwargs.get("echo_text", "Hello from loadable command!")
91
+
92
+ # Create test data
93
+ test_data = {
94
+ "command_type": "loadable",
95
+ "test_param": test_param,
96
+ "echo_text": echo_text,
97
+ "timestamp": "2025-08-12T09:45:00Z",
98
+ "status": "working"
99
+ }
100
+
101
+ return TestCommandResult(
102
+ message=f"Test command executed successfully with param: {test_param}",
103
+ test_data=test_data
104
+ )
105
+
106
+ @classmethod
107
+ def get_schema(cls) -> Dict[str, Any]:
108
+ """
109
+ Get JSON schema for command parameters.
110
+
111
+ Returns:
112
+ JSON schema dictionary.
113
+ """
114
+ return {
115
+ "type": "object",
116
+ "properties": {
117
+ "test_param": {
118
+ "type": "string",
119
+ "description": "Test parameter",
120
+ "default": "default_value"
121
+ },
122
+ "echo_text": {
123
+ "type": "string",
124
+ "description": "Text to echo",
125
+ "default": "Hello from loadable command!"
126
+ }
127
+ },
128
+ "additionalProperties": False
129
+ }
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Proxy Connection Manager
4
+
5
+ Manages connection to the proxy server with regular health checks
6
+ and automatic re-registration when needed.
7
+ """
8
+
9
+ import asyncio
10
+ import time
11
+ import logging
12
+ from typing import Optional, Dict, Any
13
+ from datetime import datetime, timedelta
14
+
15
+ from mcp_proxy_adapter.core.proxy_registration import register_with_proxy, unregister_from_proxy
16
+ from mcp_proxy_adapter.config import config
17
+ from mcp_proxy_adapter.core.logging import get_logger
18
+
19
+
20
+ class ProxyConnectionManager:
21
+ """
22
+ Manages connection to proxy server with health monitoring and auto-reconnection.
23
+ """
24
+
25
+ def __init__(self, check_interval: int = 30, max_retries: int = 3):
26
+ """
27
+ Initialize proxy connection manager.
28
+
29
+ Args:
30
+ check_interval: Interval between health checks in seconds
31
+ max_retries: Maximum number of retry attempts
32
+ """
33
+ self.logger = get_logger("proxy_connection_manager")
34
+ self.check_interval = check_interval
35
+ self.max_retries = max_retries
36
+ self.is_running = False
37
+ self.last_registration = None
38
+ self.registration_count = 0
39
+ self.failed_attempts = 0
40
+ self.server_url = None
41
+ self.server_id = None
42
+
43
+ # Get configuration
44
+ proxy_config = config.get("proxy_registration", {})
45
+ self.proxy_url = proxy_config.get("proxy_url", "http://localhost:3004")
46
+ self.server_id = proxy_config.get("server_id", "mcp_proxy_adapter_custom")
47
+
48
+ # Get server configuration
49
+ server_config = config.get("server", {})
50
+ server_host = server_config.get("host", "0.0.0.0")
51
+ server_port = server_config.get("port", 8000)
52
+
53
+ # Use localhost for external access if host is 0.0.0.0
54
+ if server_host == "0.0.0.0":
55
+ server_host = "localhost"
56
+
57
+ self.server_url = f"http://{server_host}:{server_port}"
58
+
59
+ self.logger.info(f"Proxy Connection Manager initialized:")
60
+ self.logger.info(f" • Server URL: {self.server_url}")
61
+ self.logger.info(f" • Proxy URL: {self.proxy_url}")
62
+ self.logger.info(f" • Server ID: {self.server_id}")
63
+ self.logger.info(f" • Check interval: {check_interval}s")
64
+ self.logger.info(f" • Max retries: {max_retries}")
65
+
66
+ async def start(self) -> None:
67
+ """
68
+ Start the proxy connection manager.
69
+ """
70
+ if self.is_running:
71
+ self.logger.warning("Proxy Connection Manager is already running")
72
+ return
73
+
74
+ self.is_running = True
75
+ self.logger.info("🚀 Starting Proxy Connection Manager")
76
+
77
+ # Initial registration
78
+ await self.register_with_proxy()
79
+
80
+ # Start monitoring loop
81
+ asyncio.create_task(self._monitoring_loop())
82
+
83
+ async def stop(self) -> None:
84
+ """
85
+ Stop the proxy connection manager.
86
+ """
87
+ if not self.is_running:
88
+ return
89
+
90
+ self.is_running = False
91
+ self.logger.info("🛑 Stopping Proxy Connection Manager")
92
+
93
+ # Unregister from proxy
94
+ await self.unregister_from_proxy()
95
+
96
+ async def register_with_proxy(self) -> bool:
97
+ """
98
+ Register server with proxy.
99
+
100
+ Returns:
101
+ True if registration was successful, False otherwise
102
+ """
103
+ try:
104
+ self.logger.info(f"📡 Attempting to register server with proxy at {self.proxy_url}")
105
+
106
+ success = await register_with_proxy(self.server_url)
107
+
108
+ if success:
109
+ self.last_registration = datetime.now()
110
+ self.registration_count += 1
111
+ self.failed_attempts = 0
112
+ self.logger.info(f"✅ Successfully registered with proxy (attempt #{self.registration_count})")
113
+ return True
114
+ else:
115
+ self.failed_attempts += 1
116
+ self.logger.warning(f"⚠️ Failed to register with proxy (attempt #{self.failed_attempts})")
117
+ return False
118
+
119
+ except Exception as e:
120
+ self.failed_attempts += 1
121
+ self.logger.error(f"❌ Error registering with proxy: {e}")
122
+ return False
123
+
124
+ async def unregister_from_proxy(self) -> bool:
125
+ """
126
+ Unregister server from proxy.
127
+
128
+ Returns:
129
+ True if unregistration was successful, False otherwise
130
+ """
131
+ try:
132
+ self.logger.info("📡 Unregistering from proxy")
133
+
134
+ success = await unregister_from_proxy()
135
+
136
+ if success:
137
+ self.logger.info("✅ Successfully unregistered from proxy")
138
+ else:
139
+ self.logger.warning("⚠️ Failed to unregister from proxy")
140
+
141
+ return success
142
+
143
+ except Exception as e:
144
+ self.logger.error(f"❌ Error unregistering from proxy: {e}")
145
+ return False
146
+
147
+ async def check_proxy_health(self) -> bool:
148
+ """
149
+ Check if proxy is accessible and server is registered.
150
+
151
+ Returns:
152
+ True if proxy is healthy, False otherwise
153
+ """
154
+ try:
155
+ import httpx
156
+
157
+ # Check if proxy is accessible
158
+ async with httpx.AsyncClient(timeout=5.0) as client:
159
+ response = await client.get(f"{self.proxy_url}/health")
160
+
161
+ if response.status_code == 200:
162
+ self.logger.debug("✅ Proxy health check passed")
163
+ return True
164
+ else:
165
+ self.logger.warning(f"⚠️ Proxy health check failed: {response.status_code}")
166
+ return False
167
+
168
+ except Exception as e:
169
+ self.logger.warning(f"⚠️ Proxy health check error: {e}")
170
+ return False
171
+
172
+ async def _monitoring_loop(self) -> None:
173
+ """
174
+ Main monitoring loop that checks proxy health and re-registers if needed.
175
+ """
176
+ self.logger.info("🔄 Starting proxy monitoring loop")
177
+
178
+ while self.is_running:
179
+ try:
180
+ # Check if we need to re-register
181
+ should_reregister = False
182
+
183
+ # Check if last registration was too long ago (more than 5 minutes)
184
+ if self.last_registration:
185
+ time_since_registration = datetime.now() - self.last_registration
186
+ if time_since_registration > timedelta(minutes=5):
187
+ self.logger.info("⏰ Last registration was more than 5 minutes ago, re-registering")
188
+ should_reregister = True
189
+
190
+ # Check proxy health
191
+ proxy_healthy = await self.check_proxy_health()
192
+
193
+ if not proxy_healthy:
194
+ self.logger.warning("⚠️ Proxy health check failed, attempting re-registration")
195
+ should_reregister = True
196
+
197
+ # Re-register if needed
198
+ if should_reregister:
199
+ if self.failed_attempts >= self.max_retries:
200
+ self.logger.error(f"❌ Max retries ({self.max_retries}) reached, stopping re-registration attempts")
201
+ break
202
+
203
+ await self.register_with_proxy()
204
+
205
+ # Log status
206
+ if self.last_registration:
207
+ time_since = datetime.now() - self.last_registration
208
+ self.logger.info(f"📊 Status: Registered {time_since.total_seconds():.0f}s ago, "
209
+ f"attempts: {self.registration_count}, "
210
+ f"failed: {self.failed_attempts}")
211
+
212
+ # Wait for next check
213
+ await asyncio.sleep(self.check_interval)
214
+
215
+ except asyncio.CancelledError:
216
+ self.logger.info("🛑 Monitoring loop cancelled")
217
+ break
218
+ except Exception as e:
219
+ self.logger.error(f"❌ Error in monitoring loop: {e}")
220
+ await asyncio.sleep(self.check_interval)
221
+
222
+ self.logger.info("🔄 Proxy monitoring loop stopped")
223
+
224
+ def get_status(self) -> Dict[str, Any]:
225
+ """
226
+ Get current status of the connection manager.
227
+
228
+ Returns:
229
+ Dictionary with status information
230
+ """
231
+ status = {
232
+ "is_running": self.is_running,
233
+ "server_url": self.server_url,
234
+ "proxy_url": self.proxy_url,
235
+ "server_id": self.server_id,
236
+ "registration_count": self.registration_count,
237
+ "failed_attempts": self.failed_attempts,
238
+ "check_interval": self.check_interval,
239
+ "max_retries": self.max_retries
240
+ }
241
+
242
+ if self.last_registration:
243
+ time_since = datetime.now() - self.last_registration
244
+ status["last_registration"] = self.last_registration.isoformat()
245
+ status["time_since_registration"] = time_since.total_seconds()
246
+ else:
247
+ status["last_registration"] = None
248
+ status["time_since_registration"] = None
249
+
250
+ return status
251
+
252
+
253
+ # Global instance
254
+ proxy_manager = ProxyConnectionManager()
255
+
256
+
257
+ async def start_proxy_manager() -> None:
258
+ """
259
+ Start the global proxy connection manager.
260
+ """
261
+ await proxy_manager.start()
262
+
263
+
264
+ async def stop_proxy_manager() -> None:
265
+ """
266
+ Stop the global proxy connection manager.
267
+ """
268
+ await proxy_manager.stop()
269
+
270
+
271
+ def get_proxy_manager_status() -> Dict[str, Any]:
272
+ """
273
+ Get status of the global proxy connection manager.
274
+
275
+ Returns:
276
+ Dictionary with status information
277
+ """
278
+ return proxy_manager.get_status()