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.
- mcp_proxy_adapter/api/app.py +136 -90
- mcp_proxy_adapter/commands/__init__.py +4 -0
- mcp_proxy_adapter/commands/registration_status_command.py +119 -0
- mcp_proxy_adapter/core/app_runner.py +29 -2
- mcp_proxy_adapter/core/async_proxy_registration.py +285 -0
- mcp_proxy_adapter/core/signal_handler.py +170 -0
- mcp_proxy_adapter/examples/config_builder.py +465 -55
- mcp_proxy_adapter/examples/generate_config.py +22 -15
- mcp_proxy_adapter/main.py +37 -1
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.6.8.dist-info → mcp_proxy_adapter-6.7.0.dist-info}/METADATA +1 -1
- {mcp_proxy_adapter-6.6.8.dist-info → mcp_proxy_adapter-6.7.0.dist-info}/RECORD +15 -12
- {mcp_proxy_adapter-6.6.8.dist-info → mcp_proxy_adapter-6.7.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.6.8.dist-info → mcp_proxy_adapter-6.7.0.dist-info}/entry_points.txt +0 -0
- {mcp_proxy_adapter-6.6.8.dist-info → mcp_proxy_adapter-6.7.0.dist-info}/top_level.txt +0 -0
@@ -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()
|