mcp-proxy-adapter 6.6.9__py3-none-any.whl → 6.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -159,18 +159,24 @@ def create_lifespan(config_path: Optional[str] = None):
159
159
  # Determine registration URL using unified logic
160
160
  early_server_url = _determine_registration_url(config)
161
161
  try:
162
- from mcp_proxy_adapter.core.proxy_registration import (
163
- proxy_registration_manager,
162
+ from mcp_proxy_adapter.core.async_proxy_registration import (
163
+ initialize_async_registration,
164
+ start_async_registration,
164
165
  )
165
166
 
166
- if proxy_registration_manager is not None:
167
- proxy_registration_manager.set_server_url(early_server_url)
168
- logger.info(
169
- "🔍 Early injection of server_url for registration: %s",
170
- early_server_url,
171
- )
167
+ # Initialize async registration manager
168
+ async_manager = initialize_async_registration(config)
169
+ logger.info(
170
+ "🔍 Initialized async registration manager with server_url: %s",
171
+ early_server_url,
172
+ )
173
+
174
+ # Start async registration in background thread
175
+ start_async_registration(early_server_url)
176
+ logger.info("🚀 Started async proxy registration and heartbeat thread")
177
+
172
178
  except Exception as e:
173
- logger.error(f"Failed to inject early server_url: {e}")
179
+ logger.error(f"Failed to initialize async registration: {e}")
174
180
 
175
181
  # Initialize system using unified logic (may perform registration)
176
182
  if config_path:
@@ -194,6 +200,27 @@ def create_lifespan(config_path: Optional[str] = None):
194
200
 
195
201
  # Determine registration URL using unified logic
196
202
  server_url = _determine_registration_url(final_config)
203
+
204
+ # Update async registration manager with final server URL
205
+ try:
206
+ from mcp_proxy_adapter.core.async_proxy_registration import (
207
+ get_async_registration_manager,
208
+ )
209
+
210
+ async_manager = get_async_registration_manager()
211
+ if async_manager:
212
+ async_manager.set_server_url(server_url)
213
+ logger.info(f"🔍 Updated async registration manager with final server_url: {server_url}")
214
+
215
+ # Get current status
216
+ status = async_manager.get_status()
217
+ logger.info(f"📊 Registration status: {status}")
218
+ else:
219
+ logger.warning("Async registration manager not available")
220
+
221
+ except Exception as e:
222
+ logger.error(f"Failed to update async registration manager: {e}")
223
+
197
224
  try:
198
225
  print("🔍 Registration server_url resolved to (print):", server_url)
199
226
  except Exception:
@@ -228,12 +255,30 @@ def create_lifespan(config_path: Optional[str] = None):
228
255
  # Shutdown events
229
256
  logger.info("Application shutting down")
230
257
 
231
- # Unregister from proxy if enabled
232
- unregistration_success = await unregister_from_proxy()
233
- if unregistration_success:
234
- logger.info("✅ Proxy unregistration completed successfully")
235
- else:
236
- logger.warning("⚠️ Proxy unregistration failed or was disabled")
258
+ # Stop async registration manager (this will also unregister)
259
+ try:
260
+ from mcp_proxy_adapter.core.async_proxy_registration import (
261
+ stop_async_registration,
262
+ get_registration_status,
263
+ )
264
+
265
+ # Get final status before stopping
266
+ final_status = get_registration_status()
267
+ logger.info(f"📊 Final registration status: {final_status}")
268
+
269
+ # Stop async registration (this will also unregister)
270
+ stop_async_registration()
271
+ logger.info("✅ Async registration manager stopped successfully")
272
+
273
+ except Exception as e:
274
+ logger.error(f"❌ Failed to stop async registration: {e}")
275
+
276
+ # Fallback to old unregistration method
277
+ unregistration_success = await unregister_from_proxy()
278
+ if unregistration_success:
279
+ logger.info("✅ Fallback proxy unregistration completed successfully")
280
+ else:
281
+ logger.warning("⚠️ Fallback proxy unregistration failed or was disabled")
237
282
 
238
283
  return lifespan
239
284
 
@@ -24,6 +24,9 @@ from mcp_proxy_adapter.commands.echo_command import EchoCommand
24
24
  from mcp_proxy_adapter.commands.proxy_registration_command import (
25
25
  ProxyRegistrationCommand,
26
26
  )
27
+ from mcp_proxy_adapter.commands.registration_status_command import (
28
+ RegistrationStatusCommand,
29
+ )
27
30
 
28
31
  __all__ = [
29
32
  "Command",
@@ -43,4 +46,5 @@ __all__ = [
43
46
  "RoleTestCommand",
44
47
  "EchoCommand",
45
48
  "ProxyRegistrationCommand",
49
+ "RegistrationStatusCommand",
46
50
  ]
@@ -0,0 +1,119 @@
1
+ """
2
+ Registration Status Command
3
+
4
+ This command provides information about the current proxy registration status,
5
+ including async registration state, heartbeat status, and statistics.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ from typing import Dict, Any
12
+ from dataclasses import dataclass
13
+
14
+ from mcp_proxy_adapter.commands.base import Command
15
+ from mcp_proxy_adapter.commands.result import SuccessResult
16
+ from mcp_proxy_adapter.core.logging import logger
17
+
18
+
19
+ @dataclass
20
+ class RegistrationStatusCommandResult(SuccessResult):
21
+ """Result of registration status command."""
22
+
23
+ status: Dict[str, Any]
24
+ message: str = "Registration status retrieved successfully"
25
+
26
+ def to_dict(self) -> Dict[str, Any]:
27
+ """Convert result to dictionary."""
28
+ return {
29
+ "success": True,
30
+ "status": self.status,
31
+ "message": self.message,
32
+ }
33
+
34
+ @classmethod
35
+ def get_schema(cls) -> Dict[str, Any]:
36
+ """Get JSON schema for result."""
37
+ return {
38
+ "type": "object",
39
+ "properties": {
40
+ "success": {
41
+ "type": "boolean",
42
+ "description": "Whether the command was successful",
43
+ },
44
+ "status": {
45
+ "type": "object",
46
+ "description": "Registration status information",
47
+ "properties": {
48
+ "state": {"type": "string", "description": "Current registration state"},
49
+ "server_key": {"type": "string", "description": "Server key if registered"},
50
+ "last_attempt": {"type": "number", "description": "Timestamp of last registration attempt"},
51
+ "last_success": {"type": "number", "description": "Timestamp of last successful registration"},
52
+ "last_error": {"type": "string", "description": "Last error message"},
53
+ "attempt_count": {"type": "integer", "description": "Total registration attempts"},
54
+ "success_count": {"type": "integer", "description": "Total successful registrations"},
55
+ "heartbeat_enabled": {"type": "boolean", "description": "Whether heartbeat is enabled"},
56
+ "heartbeat_interval": {"type": "integer", "description": "Heartbeat interval in seconds"},
57
+ "thread_alive": {"type": "boolean", "description": "Whether registration thread is alive"},
58
+ },
59
+ },
60
+ "message": {"type": "string", "description": "Result message"},
61
+ },
62
+ "required": ["success", "status", "message"],
63
+ }
64
+
65
+
66
+ class RegistrationStatusCommand(Command):
67
+ """Command to get proxy registration status."""
68
+
69
+ name = "registration_status"
70
+ descr = "Get current proxy registration status and statistics"
71
+ category = "proxy"
72
+ author = "Vasiliy Zdanovskiy"
73
+ email = "vasilyvz@gmail.com"
74
+
75
+ async def execute(self, **kwargs) -> RegistrationStatusCommandResult:
76
+ """
77
+ Execute registration status command.
78
+
79
+ Returns:
80
+ RegistrationStatusCommandResult with current status
81
+ """
82
+ logger.info("Executing registration status command")
83
+
84
+ try:
85
+ from mcp_proxy_adapter.core.async_proxy_registration import (
86
+ get_registration_status,
87
+ )
88
+
89
+ status = get_registration_status()
90
+
91
+ logger.info(f"Registration status retrieved: {status}")
92
+
93
+ return RegistrationStatusCommandResult(
94
+ status=status,
95
+ message="Registration status retrieved successfully"
96
+ )
97
+
98
+ except Exception as e:
99
+ logger.error(f"Failed to get registration status: {e}")
100
+
101
+ error_status = {
102
+ "state": "error",
103
+ "error": str(e),
104
+ "thread_alive": False
105
+ }
106
+
107
+ return RegistrationStatusCommandResult(
108
+ status=error_status,
109
+ message=f"Failed to get registration status: {e}"
110
+ )
111
+
112
+ @classmethod
113
+ def get_schema(cls) -> Dict[str, Any]:
114
+ """Get JSON schema for command parameters."""
115
+ return {
116
+ "type": "object",
117
+ "properties": {},
118
+ "additionalProperties": False,
119
+ }
@@ -16,6 +16,7 @@ from typing import Dict, Any, List, Optional
16
16
  from fastapi import FastAPI
17
17
 
18
18
  from mcp_proxy_adapter.core.logging import get_logger
19
+ from mcp_proxy_adapter.core.signal_handler import setup_signal_handling, is_shutdown_requested
19
20
 
20
21
  logger = get_logger("app_runner")
21
22
 
@@ -250,6 +251,30 @@ class ApplicationRunner:
250
251
  print(f" - {error}", file=sys.stderr)
251
252
  sys.exit(1)
252
253
 
254
+ # Setup signal handling for graceful shutdown
255
+ def shutdown_callback():
256
+ """Callback for graceful shutdown with proxy unregistration."""
257
+ print("\n🛑 Graceful shutdown initiated...")
258
+ try:
259
+ from mcp_proxy_adapter.core.async_proxy_registration import (
260
+ stop_async_registration,
261
+ get_registration_status,
262
+ )
263
+
264
+ # Get final status
265
+ final_status = get_registration_status()
266
+ print(f"📊 Final registration status: {final_status}")
267
+
268
+ # Stop async registration (this will unregister from proxy)
269
+ stop_async_registration()
270
+ print("✅ Proxy unregistration completed")
271
+
272
+ except Exception as e:
273
+ print(f"❌ Error during shutdown: {e}")
274
+
275
+ setup_signal_handling(shutdown_callback)
276
+ print("🔧 Signal handling configured for graceful shutdown")
277
+
253
278
  # Setup hooks
254
279
  self.setup_hooks()
255
280
 
@@ -276,14 +301,16 @@ class ApplicationRunner:
276
301
  import asyncio
277
302
 
278
303
  print(f"🚀 Starting server on {host}:{port}")
279
- print(" Use Ctrl+C to stop the server")
304
+ print("🛑 Use Ctrl+C or send SIGTERM for graceful shutdown")
280
305
  print("=" * 60)
281
306
 
282
307
  # Run with hypercorn
283
308
  asyncio.run(hypercorn.asyncio.serve(self.app, **server_kwargs))
284
309
 
285
310
  except KeyboardInterrupt:
286
- print("\n🛑 Server stopped by user")
311
+ print("\n🛑 Server stopped by user (Ctrl+C)")
312
+ if is_shutdown_requested():
313
+ print("✅ Graceful shutdown completed")
287
314
  except Exception as e:
288
315
  print(f"\n❌ Failed to start server: {e}", file=sys.stderr)
289
316
  sys.exit(1)
@@ -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"}