mcp-proxy-adapter 6.6.8__py3-none-any.whl → 6.7.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.
@@ -0,0 +1,285 @@
1
+ """
2
+ Asynchronous proxy registration with heartbeat functionality.
3
+
4
+ This module provides automatic proxy registration in a separate thread
5
+ with continuous heartbeat monitoring and automatic re-registration on failures.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ import asyncio
12
+ import time
13
+ import threading
14
+ from typing import Dict, Any, Optional
15
+ from dataclasses import dataclass
16
+ from enum import Enum
17
+
18
+ from mcp_proxy_adapter.core.logging import logger
19
+ from mcp_proxy_adapter.core.proxy_registration import ProxyRegistrationManager
20
+
21
+
22
+ class RegistrationState(Enum):
23
+ """Registration state enumeration."""
24
+ DISABLED = "disabled"
25
+ NOT_REGISTERED = "not_registered"
26
+ REGISTERING = "registering"
27
+ REGISTERED = "registered"
28
+ HEARTBEAT_FAILED = "heartbeat_failed"
29
+ ERROR = "error"
30
+
31
+
32
+ @dataclass
33
+ class RegistrationStatus:
34
+ """Registration status information."""
35
+ state: RegistrationState
36
+ server_key: Optional[str] = None
37
+ last_attempt: Optional[float] = None
38
+ last_success: Optional[float] = None
39
+ last_error: Optional[str] = None
40
+ attempt_count: int = 0
41
+ success_count: int = 0
42
+
43
+
44
+ class AsyncProxyRegistrationManager:
45
+ """
46
+ Asynchronous proxy registration manager with heartbeat functionality.
47
+
48
+ Runs registration and heartbeat in a separate thread, automatically
49
+ handling re-registration on failures and continuous monitoring.
50
+ """
51
+
52
+ def __init__(self, config: Dict[str, Any]):
53
+ """
54
+ Initialize the async proxy registration manager.
55
+
56
+ Args:
57
+ config: Application configuration
58
+ """
59
+ self.config = config
60
+ self.registration_manager = ProxyRegistrationManager(config)
61
+ self.status = RegistrationStatus(state=RegistrationState.NOT_REGISTERED)
62
+ self._thread: Optional[threading.Thread] = None
63
+ self._stop_event = threading.Event()
64
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
65
+
66
+ # Get heartbeat configuration
67
+ heartbeat_config = config.get("registration", {}).get("heartbeat", {})
68
+ self.heartbeat_enabled = heartbeat_config.get("enabled", True)
69
+ self.heartbeat_interval = heartbeat_config.get("interval", 30)
70
+ self.heartbeat_timeout = heartbeat_config.get("timeout", 10)
71
+ self.retry_attempts = heartbeat_config.get("retry_attempts", 3)
72
+ self.retry_delay = heartbeat_config.get("retry_delay", 5)
73
+
74
+ logger.info(f"AsyncProxyRegistrationManager initialized: heartbeat_enabled={self.heartbeat_enabled}, interval={self.heartbeat_interval}s")
75
+
76
+ def is_enabled(self) -> bool:
77
+ """Check if proxy registration is enabled."""
78
+ return self.registration_manager.is_enabled()
79
+
80
+ def set_server_url(self, server_url: str):
81
+ """Set the server URL for registration."""
82
+ self.registration_manager.set_server_url(server_url)
83
+ logger.info(f"Server URL set for async registration: {server_url}")
84
+
85
+ def start(self):
86
+ """Start the async registration and heartbeat thread."""
87
+ if not self.is_enabled():
88
+ logger.info("Proxy registration is disabled, not starting async manager")
89
+ self.status.state = RegistrationState.DISABLED
90
+ return
91
+
92
+ if self._thread and self._thread.is_alive():
93
+ logger.warning("Async registration thread is already running")
94
+ return
95
+
96
+ logger.info("Starting async proxy registration and heartbeat thread")
97
+ self._stop_event.clear()
98
+ self._thread = threading.Thread(target=self._run_async_loop, daemon=True)
99
+ self._thread.start()
100
+
101
+ def stop(self):
102
+ """Stop the async registration and heartbeat thread."""
103
+ if not self._thread or not self._thread.is_alive():
104
+ return
105
+
106
+ logger.info("Stopping async proxy registration and heartbeat thread")
107
+ self._stop_event.set()
108
+
109
+ # Unregister if registered
110
+ if self.status.state == RegistrationState.REGISTERED:
111
+ try:
112
+ # Run unregistration in the thread's event loop
113
+ if self._loop and not self._loop.is_closed():
114
+ future = asyncio.run_coroutine_threadsafe(
115
+ self.registration_manager.unregister_server(),
116
+ self._loop
117
+ )
118
+ future.result(timeout=10)
119
+ logger.info("Successfully unregistered from proxy during shutdown")
120
+ except Exception as e:
121
+ logger.error(f"Failed to unregister during shutdown: {e}")
122
+
123
+ self._thread.join(timeout=5)
124
+ if self._thread.is_alive():
125
+ logger.warning("Async registration thread did not stop gracefully")
126
+
127
+ def _run_async_loop(self):
128
+ """Run the async event loop in the thread."""
129
+ try:
130
+ self._loop = asyncio.new_event_loop()
131
+ asyncio.set_event_loop(self._loop)
132
+ self._loop.run_until_complete(self._async_main())
133
+ except Exception as e:
134
+ logger.error(f"Async registration loop error: {e}")
135
+ finally:
136
+ if self._loop and not self._loop.is_closed():
137
+ self._loop.close()
138
+
139
+ async def _async_main(self):
140
+ """Main async loop for registration and heartbeat."""
141
+ logger.info("Async registration loop started")
142
+
143
+ # Initial registration attempt
144
+ await self._attempt_registration()
145
+
146
+ # Main loop
147
+ while not self._stop_event.is_set():
148
+ try:
149
+ if self.status.state == RegistrationState.REGISTERED:
150
+ if self.heartbeat_enabled:
151
+ # Perform heartbeat
152
+ await self._perform_heartbeat()
153
+ else:
154
+ # Just wait if heartbeat is disabled
155
+ await asyncio.sleep(self.heartbeat_interval)
156
+ else:
157
+ # Try to register
158
+ await self._attempt_registration()
159
+ # Wait before retry
160
+ await asyncio.sleep(self.retry_delay)
161
+
162
+ except Exception as e:
163
+ logger.error(f"Error in async registration loop: {e}")
164
+ self.status.state = RegistrationState.ERROR
165
+ self.status.last_error = str(e)
166
+ await asyncio.sleep(self.retry_delay)
167
+
168
+ logger.info("Async registration loop stopped")
169
+
170
+ async def _attempt_registration(self):
171
+ """Attempt to register with the proxy."""
172
+ if self.status.state == RegistrationState.REGISTERING:
173
+ return # Already registering
174
+
175
+ self.status.state = RegistrationState.REGISTERING
176
+ self.status.attempt_count += 1
177
+ self.status.last_attempt = time.time()
178
+
179
+ logger.info(f"Attempting proxy registration (attempt #{self.status.attempt_count})")
180
+
181
+ try:
182
+ success = await self.registration_manager.register_server()
183
+
184
+ if success:
185
+ self.status.state = RegistrationState.REGISTERED
186
+ self.status.last_success = time.time()
187
+ self.status.success_count += 1
188
+ self.status.last_error = None
189
+ logger.info(f"✅ Proxy registration successful (attempt #{self.status.attempt_count})")
190
+ else:
191
+ self.status.state = RegistrationState.NOT_REGISTERED
192
+ self.status.last_error = "Registration returned False"
193
+ logger.warning(f"❌ Proxy registration failed (attempt #{self.status.attempt_count}): Registration returned False")
194
+
195
+ except Exception as e:
196
+ self.status.state = RegistrationState.NOT_REGISTERED
197
+ self.status.last_error = str(e)
198
+ logger.error(f"❌ Proxy registration failed (attempt #{self.status.attempt_count}): {e}")
199
+
200
+ async def _perform_heartbeat(self):
201
+ """Perform heartbeat to the proxy."""
202
+ try:
203
+ # Use the registration manager's heartbeat functionality
204
+ success = await self.registration_manager.heartbeat()
205
+
206
+ if success:
207
+ logger.debug("💓 Heartbeat successful")
208
+ self.status.last_success = time.time()
209
+ else:
210
+ logger.warning("💓 Heartbeat failed")
211
+ self.status.state = RegistrationState.HEARTBEAT_FAILED
212
+ self.status.last_error = "Heartbeat failed"
213
+
214
+ # Try to re-register after heartbeat failure
215
+ logger.info("Attempting re-registration after heartbeat failure")
216
+ await self._attempt_registration()
217
+
218
+ except Exception as e:
219
+ logger.error(f"💓 Heartbeat error: {e}")
220
+ self.status.state = RegistrationState.HEARTBEAT_FAILED
221
+ self.status.last_error = str(e)
222
+
223
+ # Try to re-register after heartbeat error
224
+ logger.info("Attempting re-registration after heartbeat error")
225
+ await self._attempt_registration()
226
+
227
+ # Wait for next heartbeat
228
+ await asyncio.sleep(self.heartbeat_interval)
229
+
230
+ def get_status(self) -> Dict[str, Any]:
231
+ """Get current registration status."""
232
+ return {
233
+ "state": self.status.state.value,
234
+ "server_key": self.status.server_key,
235
+ "last_attempt": self.status.last_attempt,
236
+ "last_success": self.status.last_success,
237
+ "last_error": self.status.last_error,
238
+ "attempt_count": self.status.attempt_count,
239
+ "success_count": self.status.success_count,
240
+ "heartbeat_enabled": self.heartbeat_enabled,
241
+ "heartbeat_interval": self.heartbeat_interval,
242
+ "thread_alive": self._thread.is_alive() if self._thread else False
243
+ }
244
+
245
+
246
+ # Global instance
247
+ _async_registration_manager: Optional[AsyncProxyRegistrationManager] = None
248
+
249
+
250
+ def get_async_registration_manager() -> Optional[AsyncProxyRegistrationManager]:
251
+ """Get the global async registration manager instance."""
252
+ return _async_registration_manager
253
+
254
+
255
+ def initialize_async_registration(config: Dict[str, Any]) -> AsyncProxyRegistrationManager:
256
+ """Initialize the global async registration manager."""
257
+ global _async_registration_manager
258
+ _async_registration_manager = AsyncProxyRegistrationManager(config)
259
+ return _async_registration_manager
260
+
261
+
262
+ def start_async_registration(server_url: str):
263
+ """Start async registration with the given server URL."""
264
+ global _async_registration_manager
265
+ if _async_registration_manager:
266
+ _async_registration_manager.set_server_url(server_url)
267
+ _async_registration_manager.start()
268
+ else:
269
+ logger.error("Async registration manager not initialized")
270
+
271
+
272
+ def stop_async_registration():
273
+ """Stop async registration."""
274
+ global _async_registration_manager
275
+ if _async_registration_manager:
276
+ _async_registration_manager.stop()
277
+
278
+
279
+ def get_registration_status() -> Dict[str, Any]:
280
+ """Get current registration status."""
281
+ global _async_registration_manager
282
+ if _async_registration_manager:
283
+ return _async_registration_manager.get_status()
284
+ else:
285
+ return {"state": "not_initialized"}
@@ -0,0 +1,170 @@
1
+ """
2
+ Signal handler for graceful shutdown with proxy unregistration.
3
+
4
+ This module provides signal handling for SIGTERM, SIGINT, and SIGHUP
5
+ to ensure proper proxy unregistration before server shutdown.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ import signal
12
+ import asyncio
13
+ import threading
14
+ from typing import Optional, Callable, Any
15
+ from mcp_proxy_adapter.core.logging import logger
16
+
17
+
18
+ class SignalHandler:
19
+ """
20
+ Signal handler for graceful shutdown with proxy unregistration.
21
+ """
22
+
23
+ def __init__(self):
24
+ """Initialize signal handler."""
25
+ self._shutdown_callback: Optional[Callable] = None
26
+ self._shutdown_event = threading.Event()
27
+ self._original_handlers = {}
28
+ self._setup_signal_handlers()
29
+
30
+ def set_shutdown_callback(self, callback: Callable):
31
+ """
32
+ Set callback function to be called during shutdown.
33
+
34
+ Args:
35
+ callback: Function to call during shutdown
36
+ """
37
+ self._shutdown_callback = callback
38
+ logger.info("Shutdown callback set for signal handler")
39
+
40
+ def _setup_signal_handlers(self):
41
+ """Setup signal handlers for graceful shutdown."""
42
+ # Handle SIGTERM (termination signal)
43
+ self._original_handlers[signal.SIGTERM] = signal.signal(
44
+ signal.SIGTERM, self._handle_shutdown_signal
45
+ )
46
+
47
+ # Handle SIGINT (Ctrl+C)
48
+ self._original_handlers[signal.SIGINT] = signal.signal(
49
+ signal.SIGINT, self._handle_shutdown_signal
50
+ )
51
+
52
+ # Handle SIGHUP (hangup signal)
53
+ self._original_handlers[signal.SIGHUP] = signal.signal(
54
+ signal.SIGHUP, self._handle_shutdown_signal
55
+ )
56
+
57
+ logger.info("Signal handlers installed for SIGTERM, SIGINT, SIGHUP")
58
+
59
+ def _handle_shutdown_signal(self, signum: int, frame: Any):
60
+ """
61
+ Handle shutdown signals.
62
+
63
+ Args:
64
+ signum: Signal number
65
+ frame: Current stack frame
66
+ """
67
+ signal_name = signal.Signals(signum).name
68
+ logger.info(f"🛑 Received {signal_name} signal, initiating graceful shutdown...")
69
+
70
+ # Set shutdown event
71
+ self._shutdown_event.set()
72
+
73
+ # Call shutdown callback if set
74
+ if self._shutdown_callback:
75
+ try:
76
+ logger.info("🔄 Executing shutdown callback...")
77
+ self._shutdown_callback()
78
+ logger.info("✅ Shutdown callback completed successfully")
79
+ except Exception as e:
80
+ logger.error(f"❌ Shutdown callback failed: {e}")
81
+
82
+ # If this is SIGINT (Ctrl+C), we might want to exit immediately after a delay
83
+ if signum == signal.SIGINT:
84
+ logger.info("⏰ SIGINT received, will exit in 5 seconds if not stopped gracefully...")
85
+ threading.Timer(5.0, self._force_exit).start()
86
+
87
+ def _force_exit(self):
88
+ """Force exit if graceful shutdown takes too long."""
89
+ if self._shutdown_event.is_set():
90
+ logger.warning("⚠️ Forcing exit after timeout")
91
+ import os
92
+ os._exit(1)
93
+
94
+ def wait_for_shutdown(self, timeout: Optional[float] = None) -> bool:
95
+ """
96
+ Wait for shutdown signal.
97
+
98
+ Args:
99
+ timeout: Maximum time to wait in seconds
100
+
101
+ Returns:
102
+ True if shutdown signal received, False if timeout
103
+ """
104
+ return self._shutdown_event.wait(timeout)
105
+
106
+ def is_shutdown_requested(self) -> bool:
107
+ """
108
+ Check if shutdown has been requested.
109
+
110
+ Returns:
111
+ True if shutdown signal received
112
+ """
113
+ return self._shutdown_event.is_set()
114
+
115
+ def restore_handlers(self):
116
+ """Restore original signal handlers."""
117
+ for sig, handler in self._original_handlers.items():
118
+ if handler is not None:
119
+ signal.signal(sig, handler)
120
+ logger.info("Original signal handlers restored")
121
+
122
+
123
+ # Global signal handler instance
124
+ _signal_handler: Optional[SignalHandler] = None
125
+
126
+
127
+ def get_signal_handler() -> SignalHandler:
128
+ """Get the global signal handler instance."""
129
+ global _signal_handler
130
+ if _signal_handler is None:
131
+ _signal_handler = SignalHandler()
132
+ return _signal_handler
133
+
134
+
135
+ def setup_signal_handling(shutdown_callback: Optional[Callable] = None):
136
+ """
137
+ Setup signal handling for graceful shutdown.
138
+
139
+ Args:
140
+ shutdown_callback: Optional callback to execute during shutdown
141
+ """
142
+ handler = get_signal_handler()
143
+ if shutdown_callback:
144
+ handler.set_shutdown_callback(shutdown_callback)
145
+ logger.info("Signal handling setup completed")
146
+
147
+
148
+ def wait_for_shutdown_signal(timeout: Optional[float] = None) -> bool:
149
+ """
150
+ Wait for shutdown signal.
151
+
152
+ Args:
153
+ timeout: Maximum time to wait in seconds
154
+
155
+ Returns:
156
+ True if shutdown signal received, False if timeout
157
+ """
158
+ handler = get_signal_handler()
159
+ return handler.wait_for_shutdown(timeout)
160
+
161
+
162
+ def is_shutdown_requested() -> bool:
163
+ """
164
+ Check if shutdown has been requested.
165
+
166
+ Returns:
167
+ True if shutdown signal received
168
+ """
169
+ handler = get_signal_handler()
170
+ return handler.is_shutdown_requested()