mcp-mesh 0.5.5__py3-none-any.whl → 0.5.6__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_mesh/__init__.py CHANGED
@@ -31,7 +31,7 @@ from .engine.decorator_registry import (
31
31
  get_decorator_stats,
32
32
  )
33
33
 
34
- __version__ = "0.5.5"
34
+ __version__ = "0.5.6"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -44,6 +44,19 @@ async def heartbeat_lifespan_task(heartbeat_config: dict[str, Any]) -> None:
44
44
 
45
45
  try:
46
46
  while True:
47
+ # Check if shutdown is complete before executing heartbeat
48
+ try:
49
+ from ...shared.simple_shutdown import should_stop_heartbeat
50
+
51
+ if should_stop_heartbeat():
52
+ logger.info(
53
+ f"🛑 Heartbeat stopped for agent '{agent_id}' due to shutdown"
54
+ )
55
+ break
56
+ except ImportError:
57
+ # If simple_shutdown is not available, continue normally
58
+ pass
59
+
47
60
  try:
48
61
  # Execute heartbeat pipeline
49
62
  success = await heartbeat_orchestrator.execute_heartbeat(
@@ -96,6 +96,8 @@ class FastAPIServerSetupStep(PipelineStep):
96
96
  result.add_error(f"Failed to wrap server '{server_key}': {e}")
97
97
 
98
98
  # Create FastAPI application with proper FastMCP lifespan integration (AFTER wrappers)
99
+ # Store context for shutdown coordination
100
+ self._current_context = context
99
101
  fastapi_app = self._create_fastapi_app(
100
102
  agent_config, fastmcp_servers, heartbeat_config, mcp_wrappers
101
103
  )
@@ -333,8 +335,24 @@ class FastAPIServerSetupStep(PipelineStep):
333
335
  try:
334
336
  yield
335
337
  finally:
336
- # Graceful shutdown - unregister from registry
337
- await self._graceful_shutdown(main_app)
338
+ # Registry cleanup using simple shutdown
339
+ context = getattr(self, "_current_context", {})
340
+ registry_url = context.get(
341
+ "registry_url", "http://localhost:8001"
342
+ )
343
+ agent_id = context.get("agent_id", "unknown")
344
+
345
+ try:
346
+ from ...shared.simple_shutdown import (
347
+ _simple_shutdown_coordinator,
348
+ )
349
+
350
+ _simple_shutdown_coordinator.set_shutdown_context(
351
+ registry_url, agent_id
352
+ )
353
+ await _simple_shutdown_coordinator.perform_registry_cleanup()
354
+ except Exception as e:
355
+ self.logger.error(f"❌ Registry cleanup error: {e}")
338
356
 
339
357
  # Clean up all lifespans in reverse order
340
358
  for ctx in reversed(lifespan_contexts):
@@ -359,8 +377,24 @@ class FastAPIServerSetupStep(PipelineStep):
359
377
  try:
360
378
  yield
361
379
  finally:
362
- # Graceful shutdown - unregister from registry
363
- await self._graceful_shutdown(main_app)
380
+ # Registry cleanup using simple shutdown
381
+ context = getattr(self, "_current_context", {})
382
+ registry_url = context.get(
383
+ "registry_url", "http://localhost:8001"
384
+ )
385
+ agent_id = context.get("agent_id", "unknown")
386
+
387
+ try:
388
+ from ...shared.simple_shutdown import (
389
+ _simple_shutdown_coordinator,
390
+ )
391
+
392
+ _simple_shutdown_coordinator.set_shutdown_context(
393
+ registry_url, agent_id
394
+ )
395
+ await _simple_shutdown_coordinator.perform_registry_cleanup()
396
+ except Exception as e:
397
+ self.logger.error(f"❌ Registry cleanup error: {e}")
364
398
 
365
399
  primary_lifespan = graceful_shutdown_only_lifespan
366
400
 
@@ -373,6 +407,8 @@ class FastAPIServerSetupStep(PipelineStep):
373
407
  lifespan=primary_lifespan,
374
408
  )
375
409
 
410
+ # Registry cleanup is now integrated directly into the lifespan above
411
+
376
412
  # Store app reference for global shutdown coordination
377
413
  app.state.shutdown_step = self
378
414
 
@@ -305,10 +305,8 @@ class DebounceCoordinator:
305
305
 
306
306
  except KeyboardInterrupt:
307
307
  self.logger.info(
308
- "🔴 Received KeyboardInterrupt, performing graceful shutdown..."
308
+ "🔴 Received KeyboardInterrupt, shutdown will be handled by FastAPI lifespan"
309
309
  )
310
- # Perform graceful shutdown before exiting
311
- self._perform_graceful_shutdown()
312
310
  except Exception as e:
313
311
  self.logger.error(f"❌ FastAPI server error: {e}")
314
312
  raise
@@ -427,64 +425,7 @@ class DebounceCoordinator:
427
425
  self.logger.warning(f"⚠️ Could not setup MCP heartbeat: {e}")
428
426
  # Don't fail - heartbeat is optional for MCP agents
429
427
 
430
- def _perform_graceful_shutdown(self) -> None:
431
- """Perform graceful shutdown by unregistering from registry."""
432
- try:
433
- # Run graceful shutdown asynchronously
434
- import asyncio
435
-
436
- asyncio.run(self._graceful_shutdown_async())
437
- except Exception as e:
438
- self.logger.error(f"❌ Graceful shutdown failed: {e}")
439
-
440
- async def _graceful_shutdown_async(self) -> None:
441
- """Async graceful shutdown implementation."""
442
- try:
443
- # Get the latest pipeline context from the orchestrator
444
- if self._orchestrator is None:
445
- self.logger.warning(
446
- "🚨 No orchestrator available for graceful shutdown"
447
- )
448
- return
449
-
450
- # Access the pipeline context through the orchestrator
451
- pipeline_context = getattr(self._orchestrator.pipeline, "_last_context", {})
452
-
453
- # Get registry configuration
454
- registry_url = pipeline_context.get("registry_url")
455
- agent_id = pipeline_context.get("agent_id")
456
-
457
- if not registry_url or not agent_id:
458
- self.logger.warning(
459
- f"🚨 Cannot perform graceful shutdown: missing registry_url={registry_url} or agent_id={agent_id}"
460
- )
461
- return
462
-
463
- # Create registry client for shutdown
464
- from ...generated.mcp_mesh_registry_client.api_client import ApiClient
465
- from ...generated.mcp_mesh_registry_client.configuration import (
466
- Configuration,
467
- )
468
- from ...shared.registry_client_wrapper import RegistryClientWrapper
469
-
470
- config = Configuration(host=registry_url)
471
- api_client = ApiClient(configuration=config)
472
- registry_wrapper = RegistryClientWrapper(api_client)
473
-
474
- # Perform graceful unregistration
475
- success = await registry_wrapper.unregister_agent(agent_id)
476
- if success:
477
- self.logger.info(
478
- f"🏁 Graceful shutdown completed for agent '{agent_id}'"
479
- )
480
- else:
481
- self.logger.warning(
482
- f"⚠️ Graceful shutdown failed for agent '{agent_id}' - continuing shutdown"
483
- )
484
-
485
- except Exception as e:
486
- # Don't fail the shutdown process due to unregistration errors
487
- self.logger.error(f"❌ Graceful shutdown error: {e} - continuing shutdown")
428
+ # Graceful shutdown is now handled by FastAPI lifespan in simple_shutdown.py
488
429
 
489
430
  def _check_auto_run_enabled(self) -> bool:
490
431
  """Check if auto-run is enabled (defaults to True for persistent service behavior)."""
@@ -703,9 +644,7 @@ def start_runtime() -> None:
703
644
 
704
645
  logger.info("🔧 Starting MCP Mesh runtime with debouncing")
705
646
 
706
- # Install minimal signal handlers for graceful shutdown
707
- # This restores the DELETE /heartbeats call that was removed during DNS fix
708
- _install_minimal_signal_handlers()
647
+ # Signal handlers removed - cleanup now handled by FastAPI lifespan
709
648
 
710
649
  # Create orchestrator and set up debouncing
711
650
  orchestrator = get_global_orchestrator()
@@ -722,96 +661,7 @@ def start_runtime() -> None:
722
661
  # through the debounce coordinator
723
662
 
724
663
 
725
- def _install_minimal_signal_handlers() -> None:
726
- """
727
- Install minimal signal handlers that only perform graceful shutdown.
728
-
729
- This restores the DELETE /heartbeats functionality that was removed during
730
- the DNS threading fix while avoiding complex operations that could conflict
731
- with DNS resolution in containerized environments.
732
- """
733
- import signal
734
-
735
- def graceful_shutdown_signal_handler(signum, frame):
736
- """Handle shutdown signals by performing graceful unregistration."""
737
- try:
738
- logger.info(f"🚨 Received signal {signum}, performing graceful shutdown...")
739
-
740
- # Get the global orchestrator to access pipeline context
741
- orchestrator = get_global_orchestrator()
742
- if orchestrator and hasattr(orchestrator.pipeline, "_last_context"):
743
- pipeline_context = orchestrator.pipeline._last_context
744
-
745
- # Get registry configuration
746
- registry_url = pipeline_context.get("registry_url")
747
- agent_id = pipeline_context.get("agent_id")
748
-
749
- if registry_url and agent_id:
750
- # Perform synchronous graceful shutdown
751
- import asyncio
752
-
753
- try:
754
- # Create new event loop for shutdown
755
- loop = asyncio.new_event_loop()
756
- asyncio.set_event_loop(loop)
757
-
758
- # Run graceful shutdown
759
- loop.run_until_complete(
760
- _perform_signal_graceful_shutdown(registry_url, agent_id)
761
- )
762
- loop.close()
763
-
764
- logger.info(
765
- f"🏁 Graceful shutdown completed for agent '{agent_id}'"
766
- )
767
- except Exception as e:
768
- logger.error(f"❌ Graceful shutdown error: {e}")
769
- else:
770
- logger.warning(
771
- f"🚨 Cannot perform graceful shutdown: missing registry_url={registry_url} or agent_id={agent_id}"
772
- )
773
- else:
774
- logger.warning(
775
- "🚨 No orchestrator context available for graceful shutdown"
776
- )
777
-
778
- except Exception as e:
779
- logger.error(f"❌ Signal handler error: {e}")
780
- finally:
781
- # Exit cleanly
782
- logger.info("🛑 Exiting...")
783
- sys.exit(0)
784
-
785
- # Install handlers for common termination signals
786
- signal.signal(signal.SIGTERM, graceful_shutdown_signal_handler)
787
- signal.signal(signal.SIGINT, graceful_shutdown_signal_handler)
788
-
789
- logger.debug("🛡️ Minimal signal handlers installed for graceful shutdown")
790
-
791
-
792
- async def _perform_signal_graceful_shutdown(registry_url: str, agent_id: str) -> None:
793
- """Perform graceful shutdown from signal handler context."""
794
- try:
795
- # Create registry client for shutdown
796
- from ...generated.mcp_mesh_registry_client.api_client import ApiClient
797
- from ...generated.mcp_mesh_registry_client.configuration import Configuration
798
- from ...shared.registry_client_wrapper import RegistryClientWrapper
799
-
800
- config = Configuration(host=registry_url)
801
- api_client = ApiClient(configuration=config)
802
- registry_wrapper = RegistryClientWrapper(api_client)
803
-
804
- # Perform graceful unregistration (this sends DELETE /heartbeats)
805
- success = await registry_wrapper.unregister_agent(agent_id)
806
- if success:
807
- logger.info(
808
- f"✅ Agent '{agent_id}' successfully unregistered from registry"
809
- )
810
- else:
811
- logger.warning(f"⚠️ Failed to unregister agent '{agent_id}' from registry")
812
-
813
- except Exception as e:
814
- logger.error(f"❌ Signal graceful shutdown error: {e}")
664
+ # Signal handlers removed - cleanup now handled by FastAPI lifespan in simple_shutdown.py
815
665
 
816
666
 
817
667
  # Minimal signal handlers restored to provide graceful shutdown with DELETE /heartbeats
@@ -0,0 +1,217 @@
1
+ """
2
+ Simple shutdown coordination for MCP Mesh agents.
3
+
4
+ Provides clean shutdown via FastAPI lifespan events and basic signal handling.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import signal
10
+ from contextlib import asynccontextmanager
11
+ from typing import Any, Optional
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class SimpleShutdownCoordinator:
17
+ """Lightweight shutdown coordination using FastAPI lifespan."""
18
+
19
+ def __init__(self):
20
+ self._shutdown_requested = False
21
+ self._registry_url: Optional[str] = None
22
+ self._agent_id: Optional[str] = None
23
+ self._shutdown_complete = False # Flag to prevent race conditions
24
+
25
+ def set_shutdown_context(self, registry_url: str, agent_id: str) -> None:
26
+ """Set context for shutdown cleanup."""
27
+ self._registry_url = registry_url
28
+ self._agent_id = agent_id
29
+ logger.debug(
30
+ f"🔧 Shutdown context set: agent_id={agent_id}, registry_url={registry_url}"
31
+ )
32
+
33
+ def install_signal_handlers(self) -> None:
34
+ """Install minimal signal handlers as backup."""
35
+
36
+ def shutdown_signal_handler(signum, frame):
37
+ # Avoid logging in signal handler to prevent reentrant call issues
38
+ self._shutdown_requested = True
39
+
40
+ signal.signal(signal.SIGINT, shutdown_signal_handler)
41
+ signal.signal(signal.SIGTERM, shutdown_signal_handler)
42
+ logger.debug("📡 Signal handlers installed")
43
+
44
+ def is_shutdown_requested(self) -> bool:
45
+ """Check if shutdown was requested via signal."""
46
+ return self._shutdown_requested
47
+
48
+ def is_shutdown_complete(self) -> bool:
49
+ """Check if shutdown cleanup is complete."""
50
+ return self._shutdown_complete
51
+
52
+ def mark_shutdown_complete(self) -> None:
53
+ """Mark shutdown cleanup as complete to prevent further operations."""
54
+ self._shutdown_complete = True
55
+ logger.debug("🏁 Shutdown marked as complete")
56
+
57
+ async def perform_registry_cleanup(self) -> None:
58
+ """Perform registry cleanup by calling DELETE /agents/{agent_id}."""
59
+ # Try to get the actual agent_id from DecoratorRegistry if available
60
+ actual_agent_id = self._agent_id
61
+ try:
62
+ from _mcp_mesh.engine.decorator_registry import DecoratorRegistry
63
+
64
+ agent_config = DecoratorRegistry.get_resolved_agent_config()
65
+ if agent_config and "agent_id" in agent_config:
66
+ resolved_agent_id = agent_config["agent_id"]
67
+ if resolved_agent_id and resolved_agent_id != "unknown":
68
+ actual_agent_id = resolved_agent_id
69
+ logger.debug(
70
+ f"🔧 Using resolved agent_id from DecoratorRegistry: {actual_agent_id}"
71
+ )
72
+ except Exception as e:
73
+ logger.debug(f"Could not get agent_id from DecoratorRegistry: {e}")
74
+
75
+ if (
76
+ not self._registry_url
77
+ or not actual_agent_id
78
+ or actual_agent_id == "unknown"
79
+ ):
80
+ logger.warning(
81
+ f"⚠️ Missing registry URL or agent ID for cleanup: registry_url={self._registry_url}, agent_id={actual_agent_id}"
82
+ )
83
+ return
84
+
85
+ try:
86
+ from _mcp_mesh.generated.mcp_mesh_registry_client.api_client import (
87
+ ApiClient,
88
+ )
89
+ from _mcp_mesh.generated.mcp_mesh_registry_client.configuration import (
90
+ Configuration,
91
+ )
92
+ from _mcp_mesh.shared.registry_client_wrapper import RegistryClientWrapper
93
+
94
+ config = Configuration(host=self._registry_url)
95
+ api_client = ApiClient(configuration=config)
96
+ registry_wrapper = RegistryClientWrapper(api_client)
97
+
98
+ success = await registry_wrapper.unregister_agent(actual_agent_id)
99
+ if success:
100
+ logger.info(f"✅ Agent '{actual_agent_id}' unregistered from registry")
101
+ self.mark_shutdown_complete()
102
+ else:
103
+ logger.warning(f"⚠️ Failed to unregister agent '{actual_agent_id}'")
104
+ self.mark_shutdown_complete() # Mark complete even on failure to prevent loops
105
+
106
+ except Exception as e:
107
+ logger.error(f"❌ Registry cleanup error: {e}")
108
+ self.mark_shutdown_complete() # Mark complete even on error to prevent loops
109
+
110
+ def create_shutdown_lifespan(self, original_lifespan=None):
111
+ """Create lifespan function that includes registry cleanup."""
112
+
113
+ @asynccontextmanager
114
+ async def shutdown_lifespan(app):
115
+ # Startup phase
116
+ if original_lifespan:
117
+ # If user had a lifespan, run their startup code
118
+ async with original_lifespan(app):
119
+ yield
120
+ else:
121
+ yield
122
+
123
+ # Shutdown phase
124
+ logger.info("🔄 FastAPI shutdown initiated, performing registry cleanup...")
125
+ await self.perform_registry_cleanup()
126
+ logger.info("🏁 Registry cleanup completed")
127
+
128
+ return shutdown_lifespan
129
+
130
+ def inject_shutdown_lifespan(self, app, registry_url: str, agent_id: str) -> None:
131
+ """Inject shutdown lifespan into FastAPI app."""
132
+ self.set_shutdown_context(registry_url, agent_id)
133
+
134
+ # Store original lifespan if it exists
135
+ original_lifespan = getattr(app, "router", {}).get("lifespan", None)
136
+
137
+ # Replace with our shutdown-aware lifespan
138
+ new_lifespan = self.create_shutdown_lifespan(original_lifespan)
139
+ app.router.lifespan = new_lifespan
140
+
141
+ logger.info(f"🔌 Shutdown lifespan injected for agent '{agent_id}'")
142
+
143
+
144
+ # Global instance
145
+ _simple_shutdown_coordinator = SimpleShutdownCoordinator()
146
+
147
+
148
+ def inject_shutdown_lifespan(app, registry_url: str, agent_id: str) -> None:
149
+ """Inject shutdown lifespan into FastAPI app (module-level function)."""
150
+ _simple_shutdown_coordinator.inject_shutdown_lifespan(app, registry_url, agent_id)
151
+
152
+
153
+ def install_signal_handlers() -> None:
154
+ """Install signal handlers (module-level function)."""
155
+ _simple_shutdown_coordinator.install_signal_handlers()
156
+
157
+
158
+ def should_stop_heartbeat() -> bool:
159
+ """Check if heartbeat should stop due to shutdown."""
160
+ return _simple_shutdown_coordinator.is_shutdown_complete()
161
+
162
+
163
+ def start_blocking_loop_with_shutdown_support(thread) -> None:
164
+ """
165
+ Keep main thread alive while uvicorn in the thread handles requests.
166
+
167
+ Install signal handlers in main thread for proper registry cleanup since
168
+ signals to threads can be unreliable for FastAPI lifespan shutdown.
169
+ """
170
+ logger.info("🔒 MAIN THREAD: Installing signal handlers for registry cleanup")
171
+
172
+ # Install signal handlers for proper registry cleanup
173
+ _simple_shutdown_coordinator.install_signal_handlers()
174
+
175
+ logger.info(
176
+ "🔒 MAIN THREAD: Waiting for uvicorn thread - signals handled by main thread"
177
+ )
178
+
179
+ try:
180
+ # Wait for thread while handling signals in main thread
181
+ while thread.is_alive():
182
+ thread.join(timeout=1.0)
183
+
184
+ # Check if shutdown was requested via signal
185
+ if _simple_shutdown_coordinator.is_shutdown_requested():
186
+ logger.info(
187
+ "🔄 MAIN THREAD: Shutdown requested, performing registry cleanup..."
188
+ )
189
+
190
+ # Perform registry cleanup in main thread
191
+ import asyncio
192
+
193
+ try:
194
+ # Run cleanup in main thread
195
+ asyncio.run(_simple_shutdown_coordinator.perform_registry_cleanup())
196
+ except Exception as e:
197
+ logger.error(f"❌ Registry cleanup error: {e}")
198
+
199
+ logger.info("🏁 MAIN THREAD: Registry cleanup completed, exiting")
200
+ break
201
+
202
+ except KeyboardInterrupt:
203
+ logger.info(
204
+ "🔄 MAIN THREAD: KeyboardInterrupt received, performing registry cleanup..."
205
+ )
206
+
207
+ # Perform registry cleanup on Ctrl+C
208
+ import asyncio
209
+
210
+ try:
211
+ asyncio.run(_simple_shutdown_coordinator.perform_registry_cleanup())
212
+ except Exception as e:
213
+ logger.error(f"❌ Registry cleanup error: {e}")
214
+
215
+ logger.info("🏁 MAIN THREAD: Registry cleanup completed")
216
+
217
+ logger.info("🏁 MAIN THREAD: Uvicorn thread completed")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-mesh
3
- Version: 0.5.5
3
+ Version: 0.5.6
4
4
  Summary: Kubernetes-native platform for distributed MCP applications
5
5
  Project-URL: Homepage, https://github.com/dhyansraj/mcp-mesh
6
6
  Project-URL: Documentation, https://github.com/dhyansraj/mcp-mesh/tree/main/docs
@@ -1,4 +1,4 @@
1
- _mcp_mesh/__init__.py,sha256=nHqtNT23PCrZB_z_T1tH7X9lMXSB2G3j0_ZGa5NtCz0,2719
1
+ _mcp_mesh/__init__.py,sha256=uw4Yj9HoBfp-_CyH8izKGYKrrCI-dF9uwMLMNgkIMN0,2719
2
2
  _mcp_mesh/engine/__init__.py,sha256=2ennzbo7yJcpkXO9BqN69TruLjJfmJY4Y5VEsG644K4,3630
3
3
  _mcp_mesh/engine/async_mcp_client.py,sha256=UcbQjxtgVfeRw6DHTZhAzN1gkcKlTg-lUPEePRPQWAU,6306
4
4
  _mcp_mesh/engine/decorator_registry.py,sha256=0-GI7DILpbB6GRLIYhZu0DcdHEA62d89jDYxkK1ywiM,22422
@@ -78,17 +78,17 @@ _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py,sha256=QTzYdL81WERkOaBV
78
78
  _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py,sha256=uB9o298X7GbOaKUw4ij_fUAOCpK0n2brx_oWqWGTXFY,11296
79
79
  _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py,sha256=Jb7EVJO13trUVO3aCSgzGqAtoc4vie5kDrYLZtOkiXg,11601
80
80
  _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py,sha256=ydVx-Vb_RgW1WPCboHVdZfEwNbgDngFV6Y9elZIBrAw,3602
81
- _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py,sha256=yCt731vgniSfxuClIcMvlMtDrubTu_9crgwiOwOYoBQ,2510
81
+ _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py,sha256=4XPPlaJ6rz-FkDO3bxzVxHmVF-aq1CCaTW4vIBXrB0c,3016
82
82
  _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py,sha256=4abbOKN3echwX82PV0RvxF6cJZUu0pMgisOpILZ_ZzY,2875
83
83
  _mcp_mesh/pipeline/mcp_startup/__init__.py,sha256=gS0xNmVx66bkLUMw64olMsN40ZLPH3ymwlLixZ4NuTs,1239
84
84
  _mcp_mesh/pipeline/mcp_startup/configuration.py,sha256=6LRLIxrqFMU76qrBb6GjGknUlKPZZ9iqOlxE7F9ZhLs,2808
85
85
  _mcp_mesh/pipeline/mcp_startup/decorator_collection.py,sha256=RHC6MHtfP9aP0hZ-IJjISZu72e0Pml3LU0qr7dc284w,2294
86
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py,sha256=pxw-_mJ74bKmI3dD-Goi-eXBZxHnNNWcP6X9zc65n7A,38408
86
+ _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py,sha256=ktCn1IB8J3Iz7T0iUJF_ytwwO_RRbJN3dNQ6qZLY6iI,40229
87
87
  _mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py,sha256=ktsE9EZYdyZbCtCKB6HVdzGFMQ0E9n0-7I55LRO99sE,10270
88
88
  _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py,sha256=v85B0ynomvYu87eIvLe-aSZ7-Iwov2VtM4Fg3PkmrZs,3865
89
89
  _mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py,sha256=v3Fl0PvW5s7Ib_Cy7WtXA7gDvsFGiz54a-IlQRTcLPg,10410
90
90
  _mcp_mesh/pipeline/mcp_startup/server_discovery.py,sha256=i3t12Dd2nEg3nbifyMGvm2SYr3WYiYJbicBakS3ZeuM,8007
91
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py,sha256=oaXHBo-p8eh1r3so3BWzdMtpMqPFFO1zISt0TPBKoqo,33103
91
+ _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py,sha256=KldSG3xfGNm0iexnCMPkDsi3nuIVXBwneDBuoT5gJO4,26756
92
92
  _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py,sha256=Y_VeZcvyT3y9phWtGD7cX92NzKZIzF2J6kRJUO8q-9U,2291
93
93
  _mcp_mesh/pipeline/shared/__init__.py,sha256=s9xmdf6LkoetrVRGd7Zp3NUxcJCW6YZ_yNKzUBcnYys,352
94
94
  _mcp_mesh/pipeline/shared/base_step.py,sha256=kyPbNUX79NyGrE_0Q-e-Aek7m1J0TW036njWfv0iZ0I,1080
@@ -101,11 +101,11 @@ _mcp_mesh/shared/content_extractor.py,sha256=culjhieFl_J6EMDv1VFKvS38O3IMhWMs8fH
101
101
  _mcp_mesh/shared/defaults.py,sha256=5qazybkn7QHi418dXCS0b6QlNQl3DMg97ItzNGkc8d4,1851
102
102
  _mcp_mesh/shared/fast_heartbeat_status.py,sha256=OquEsX9ZTbxY1lIsll0Mbb2KDzSJD76sLMOlr3Z73Sc,5188
103
103
  _mcp_mesh/shared/fastapi_middleware_manager.py,sha256=_h10dSL9mgQstpJW_ZM2cpkU6yTKaYKlZaKXMk2i6IA,14638
104
- _mcp_mesh/shared/graceful_shutdown_manager.py,sha256=wH7iz9TNDr-lMnmvQH6UFSu-VrG6WgHB47Cu98icnGk,9239
105
104
  _mcp_mesh/shared/host_resolver.py,sha256=ycs6gXnI1zJX5KiqiLJPX5GkHX8r4j8NMHQOlG2J2X8,2964
106
105
  _mcp_mesh/shared/logging_config.py,sha256=n9AqShZ5BZgyrkoTlvux6ECRVpM9dUYvmGB0NPMl-Ak,2477
107
106
  _mcp_mesh/shared/registry_client_wrapper.py,sha256=d8yL-MiCrQr_WYdRFStOd531qaLv9kZjh0zJAmCJ-Cc,16976
108
107
  _mcp_mesh/shared/server_discovery.py,sha256=W5nsN-GvEVFD-7XkbMTxh-9FUIEiyWOxP3GYr8GNi3E,13142
108
+ _mcp_mesh/shared/simple_shutdown.py,sha256=jnF1rTR2yR619LZnEjNlu-ZdKlB3PovxKqG0VZ3HDgE,8319
109
109
  _mcp_mesh/shared/sse_parser.py,sha256=OEPnfL9xL3rsjQrbyvfUO82WljPSDeO6Z61uUwN1NAo,8035
110
110
  _mcp_mesh/shared/support_types.py,sha256=k-ICF_UwDkHxQ1D5LwFZrp-UrNb4E5dzw02CRuLW9iI,7264
111
111
  _mcp_mesh/tracing/agent_context_helper.py,sha256=BIJ3Kc4Znd6emMAu97aUhSoxSIza3qYUmObLgc9ONiA,4765
@@ -116,9 +116,9 @@ _mcp_mesh/tracing/redis_metadata_publisher.py,sha256=F78E34qnI3D0tOmbHUTBsLbDst2
116
116
  _mcp_mesh/tracing/trace_context_helper.py,sha256=6tEkwjWFqMBe45zBlhacktmIpzJWTF950ph3bwL3cNc,5994
117
117
  _mcp_mesh/tracing/utils.py,sha256=t9lJuTH7CeuzAiiAaD0WxsJMFJPdzZFR0w6-vyR9f2E,3849
118
118
  mesh/__init__.py,sha256=l5RSMV8Kx0h7cvku8YkZPbTHjEPWciGT0bcEB2O_eNU,3242
119
- mesh/decorators.py,sha256=9fhQFk5Ud34nYteRNc6FXOD_cVLLhlIOp9r9vtmJPjs,36186
119
+ mesh/decorators.py,sha256=oBWoRE-FA3qUGygAUtk3-eAYBckwTGfTzvXOgCag4ys,36678
120
120
  mesh/types.py,sha256=g37DXAzya-xGPa1_WKlW3T3_VqyTn8ZVepIDSrhBTkc,10815
121
- mcp_mesh-0.5.5.dist-info/METADATA,sha256=pwYxy23zDrf8A0AfF4Gh8beldtvt0KJ3TGR309BL4ss,4879
122
- mcp_mesh-0.5.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
123
- mcp_mesh-0.5.5.dist-info/licenses/LICENSE,sha256=_EBQHRQThv9FPOLc5eFOUdeeRO0mYwChC7cx60dM1tM,1078
124
- mcp_mesh-0.5.5.dist-info/RECORD,,
121
+ mcp_mesh-0.5.6.dist-info/METADATA,sha256=Mh8nwL5wjm53WMFPOqcVHCRmcvbPa4sgMmX7bQV-yiw,4879
122
+ mcp_mesh-0.5.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
123
+ mcp_mesh-0.5.6.dist-info/licenses/LICENSE,sha256=_EBQHRQThv9FPOLc5eFOUdeeRO0mYwChC7cx60dM1tM,1078
124
+ mcp_mesh-0.5.6.dist-info/RECORD,,
mesh/decorators.py CHANGED
@@ -12,6 +12,7 @@ from typing import Any, TypeVar
12
12
  # Import from _mcp_mesh for registry and runtime integration
13
13
  from _mcp_mesh.engine.decorator_registry import DecoratorRegistry
14
14
  from _mcp_mesh.shared.config_resolver import ValidationRule, get_config_value
15
+ from _mcp_mesh.shared.simple_shutdown import start_blocking_loop_with_shutdown_support
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
@@ -98,50 +99,49 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
98
99
  f"🚀 IMMEDIATE UVICORN: Starting uvicorn server on {http_host}:{port}"
99
100
  )
100
101
 
101
- # Create uvicorn config and server but DON'T start it with asyncio.run()
102
- # This prevents the dual event loop conflict
102
+ # Use uvicorn.run() for proper signal handling (enables FastAPI lifespan shutdown)
103
103
  logger.info(
104
- "⚡ IMMEDIATE UVICORN: Creating uvicorn config without starting event loop"
105
- )
106
-
107
- config = uvicorn.Config(app=app, host=http_host, port=port, log_level="info")
108
- server = uvicorn.Server(config)
109
-
110
- # DON'T start the server here - let the pipeline handle the event loop
111
- logger.info(
112
- "✅ IMMEDIATE UVICORN: Uvicorn server configured (will be started by pipeline)"
104
+ "⚡ IMMEDIATE UVICORN: Starting server with uvicorn.run() for proper signal handling"
113
105
  )
114
106
 
115
107
  # Start uvicorn server in background thread (NON-daemon to keep process alive)
116
108
  def run_server():
117
- """Run uvicorn server in background thread - keeps process alive."""
109
+ """Run uvicorn server in background thread with proper signal handling."""
118
110
  try:
119
111
  logger.info(
120
112
  f"🌟 IMMEDIATE UVICORN: Starting server on {http_host}:{port}"
121
113
  )
122
- server.run() # This blocks and keeps the thread alive
114
+ # Use uvicorn.run() instead of Server().run() for proper signal handling
115
+ uvicorn.run(
116
+ app,
117
+ host=http_host,
118
+ port=port,
119
+ log_level="info",
120
+ timeout_graceful_shutdown=30, # Allow time for registry cleanup
121
+ access_log=False, # Reduce noise
122
+ )
123
123
  except Exception as e:
124
124
  logger.error(f"❌ IMMEDIATE UVICORN: Server failed: {e}")
125
125
  import traceback
126
126
 
127
127
  logger.error(f"Server traceback: {traceback.format_exc()}")
128
128
 
129
- # Start server in daemon thread (matches working test setup pattern)
130
- thread = threading.Thread(target=run_server, daemon=True)
129
+ # Start server in non-daemon thread so it can handle signals properly
130
+ thread = threading.Thread(target=run_server, daemon=False)
131
131
  thread.start()
132
132
 
133
133
  logger.info(
134
- "🔒 IMMEDIATE UVICORN: Server thread started (daemon=True) - matches working test setup"
134
+ "🔒 IMMEDIATE UVICORN: Server thread started (daemon=False) - can handle signals"
135
135
  )
136
136
 
137
137
  # Store server reference in DecoratorRegistry BEFORE starting (critical timing)
138
138
  server_info = {
139
139
  "app": app,
140
- "server": server, # Include uvicorn server object
141
- "config": config, # Include config for reference
140
+ "server": None, # No server object with uvicorn.run()
141
+ "config": None, # No config object needed
142
142
  "host": http_host,
143
143
  "port": port,
144
- "thread": thread, # Server thread (daemon)
144
+ "thread": thread, # Server thread (non-daemon)
145
145
  "type": "immediate_uvicorn_running",
146
146
  "status": "running", # Server is now running in background thread
147
147
  }
@@ -162,9 +162,18 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
162
162
  f"✅ IMMEDIATE UVICORN: Uvicorn server running on {http_host}:{port} (daemon thread)"
163
163
  )
164
164
 
165
+ # Set up registry context for shutdown cleanup (use defaults initially)
166
+ import os
167
+
168
+ from _mcp_mesh.shared.simple_shutdown import _simple_shutdown_coordinator
169
+
170
+ registry_url = os.getenv("MCP_MESH_REGISTRY_URL", "http://localhost:8000")
171
+ agent_id = "unknown" # Will be updated by pipeline when available
172
+ _simple_shutdown_coordinator.set_shutdown_context(registry_url, agent_id)
173
+
165
174
  # CRITICAL FIX: Keep main thread alive to prevent shutdown state
166
175
  # This matches the working test setup pattern that prevents DNS resolution failures
167
- # Uses graceful shutdown manager for clean code organization
176
+ # Uses simple shutdown with signal handlers for clean registry cleanup
168
177
  start_blocking_loop_with_shutdown_support(thread)
169
178
 
170
179
  except Exception as e:
@@ -1,236 +0,0 @@
1
- """
2
- Graceful Shutdown Manager for MCP Mesh agents.
3
-
4
- This utility class manages graceful shutdown functionality, including signal handlers,
5
- shutdown context management, and coordination with FastAPI lifecycle and registry.
6
- """
7
-
8
- import asyncio
9
- import logging
10
- import os
11
- import signal
12
- import sys
13
- from typing import Any, Optional
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- class GracefulShutdownManager:
19
- """Manages graceful shutdown for MCP Mesh agents."""
20
-
21
- def __init__(self):
22
- self._shutdown_requested: bool = False
23
- self._shutdown_context: dict[str, Any] = {}
24
-
25
- def set_shutdown_context(self, context: dict[str, Any]) -> None:
26
- """Set context for graceful shutdown (called from pipeline)."""
27
- self._shutdown_context.update(context)
28
-
29
- # Extract FastAPI app from context for coordinated shutdown
30
- fastapi_app = context.get("fastapi_app")
31
- if fastapi_app:
32
- self._shutdown_context["fastapi_app"] = fastapi_app
33
- logger.debug(
34
- f"🔧 FastAPI app stored for coordinated shutdown: {type(fastapi_app)}"
35
- )
36
-
37
- logger.debug(
38
- f"🔧 Shutdown context updated: agent_id={context.get('agent_id')}, registry_url={context.get('registry_url')}"
39
- )
40
-
41
- def install_signal_handlers(self) -> None:
42
- """Install signal handlers that set the shutdown flag."""
43
-
44
- def shutdown_signal_handler(signum, frame):
45
- """Handle shutdown signals by setting the shutdown flag."""
46
- logger.info(
47
- f"🚨 SIGNAL HANDLER: Received signal {signum}, setting shutdown flag"
48
- )
49
- self._shutdown_requested = True
50
-
51
- # Install handlers for common termination signals
52
- signal.signal(signal.SIGTERM, shutdown_signal_handler)
53
- signal.signal(signal.SIGINT, shutdown_signal_handler)
54
-
55
- logger.info("🛡️ Signal handlers installed for graceful shutdown")
56
-
57
- def is_shutdown_requested(self) -> bool:
58
- """Check if shutdown has been requested."""
59
- return self._shutdown_requested
60
-
61
- def perform_graceful_shutdown_from_main_thread(self) -> None:
62
- """Perform graceful shutdown from main thread (non-async context)."""
63
- try:
64
- # Check if we have FastAPI app for coordinated shutdown
65
- fastapi_app = self._shutdown_context.get("fastapi_app")
66
-
67
- if (
68
- fastapi_app
69
- and hasattr(fastapi_app, "state")
70
- and hasattr(fastapi_app.state, "shutdown_step")
71
- ):
72
- # Use FastAPI lifespan shutdown mechanism
73
- logger.info("🚨 Triggering coordinated FastAPI shutdown...")
74
- shutdown_step = fastapi_app.state.shutdown_step
75
-
76
- # Create minimal context and call the existing graceful shutdown
77
- loop = asyncio.new_event_loop()
78
- asyncio.set_event_loop(loop)
79
-
80
- try:
81
- loop.run_until_complete(
82
- shutdown_step._graceful_shutdown(fastapi_app)
83
- )
84
- logger.info("✅ FastAPI coordinated shutdown completed")
85
- return
86
- finally:
87
- loop.close()
88
-
89
- # Fallback: Direct registry call if no FastAPI coordination available
90
- logger.info("🚨 Using fallback direct registry shutdown...")
91
-
92
- # Get registry_url from context or environment (with default)
93
- registry_url = self._shutdown_context.get("registry_url")
94
- if not registry_url:
95
- registry_url = os.getenv(
96
- "MCP_MESH_REGISTRY_URL", "http://localhost:8000"
97
- )
98
- logger.debug(
99
- f"🔧 Using registry URL from environment/default: {registry_url}"
100
- )
101
-
102
- # Get agent_id from context or generate the same one used for registration
103
- agent_id = self._shutdown_context.get("agent_id")
104
- if not agent_id:
105
- agent_id = self._get_or_create_agent_id()
106
- logger.debug(f"🔧 Using agent ID from shared state: {agent_id}")
107
-
108
- if not registry_url or not agent_id:
109
- logger.warning(
110
- f"⚠️ Cannot perform graceful shutdown: missing registry_url={registry_url} or agent_id={agent_id}"
111
- )
112
- return
113
-
114
- # Create registry client and perform shutdown synchronously
115
- loop = asyncio.new_event_loop()
116
- asyncio.set_event_loop(loop)
117
-
118
- try:
119
- loop.run_until_complete(
120
- self._perform_graceful_shutdown_async(registry_url, agent_id)
121
- )
122
- finally:
123
- loop.close()
124
-
125
- except Exception as e:
126
- logger.error(f"❌ Graceful shutdown error: {e}")
127
-
128
- async def _perform_graceful_shutdown_async(
129
- self, registry_url: str, agent_id: str
130
- ) -> None:
131
- """Async graceful shutdown implementation."""
132
- try:
133
- # Create registry client for shutdown
134
- from _mcp_mesh.generated.mcp_mesh_registry_client.api_client import (
135
- ApiClient,
136
- )
137
- from _mcp_mesh.generated.mcp_mesh_registry_client.configuration import (
138
- Configuration,
139
- )
140
- from _mcp_mesh.shared.registry_client_wrapper import RegistryClientWrapper
141
-
142
- config = Configuration(host=registry_url)
143
- api_client = ApiClient(configuration=config)
144
- registry_wrapper = RegistryClientWrapper(api_client)
145
-
146
- # Perform graceful unregistration (this sends DELETE /heartbeats)
147
- success = await registry_wrapper.unregister_agent(agent_id)
148
- if success:
149
- logger.info(
150
- f"✅ Agent '{agent_id}' successfully unregistered from registry"
151
- )
152
- else:
153
- logger.warning(
154
- f"⚠️ Failed to unregister agent '{agent_id}' from registry"
155
- )
156
-
157
- except Exception as e:
158
- logger.error(f"❌ Graceful shutdown error: {e}")
159
-
160
- def _get_or_create_agent_id(self) -> str:
161
- """Get agent ID using the existing decorator registry function."""
162
- from mesh.decorators import _get_or_create_agent_id
163
-
164
- return _get_or_create_agent_id()
165
-
166
- def start_blocking_loop_with_shutdown_support(self, thread) -> None:
167
- """
168
- Start the main thread blocking loop with graceful shutdown support.
169
-
170
- This keeps the main thread alive while monitoring for shutdown signals
171
- and maintaining the server thread.
172
- """
173
- logger.info(
174
- "🔒 MAIN THREAD: Blocking to keep alive (prevents threading shutdown state)"
175
- )
176
-
177
- # Install signal handlers
178
- self.install_signal_handlers()
179
-
180
- try:
181
- while True:
182
- # Check shutdown flag (set by signal handlers)
183
- if self.is_shutdown_requested():
184
- logger.info(
185
- "🚨 MAIN THREAD: Shutdown requested, performing graceful cleanup..."
186
- )
187
- self.perform_graceful_shutdown_from_main_thread()
188
- break
189
-
190
- if thread.is_alive():
191
- thread.join(timeout=1) # Check every second
192
- else:
193
- logger.warning("⚠️ Server thread died, exiting...")
194
- break
195
- except KeyboardInterrupt:
196
- logger.info("👋 MAIN THREAD: Received interrupt, shutting down gracefully")
197
- self.perform_graceful_shutdown_from_main_thread()
198
- except Exception as e:
199
- logger.error(f"❌ MAIN THREAD: Error in blocking loop: {e}")
200
-
201
- logger.info("🏁 MAIN THREAD: Exiting blocking loop")
202
-
203
-
204
- # Global instance for backward compatibility with existing decorators.py usage
205
- _global_shutdown_manager = GracefulShutdownManager()
206
-
207
-
208
- def get_global_shutdown_manager() -> GracefulShutdownManager:
209
- """Get the global shutdown manager instance."""
210
- return _global_shutdown_manager
211
-
212
-
213
- # Convenience functions for backward compatibility
214
- def set_shutdown_context(context: dict[str, Any]) -> None:
215
- """Set context for graceful shutdown (global convenience function)."""
216
- _global_shutdown_manager.set_shutdown_context(context)
217
-
218
-
219
- def install_graceful_shutdown_handlers() -> None:
220
- """Install signal handlers that set the shutdown flag (global convenience function)."""
221
- _global_shutdown_manager.install_signal_handlers()
222
-
223
-
224
- def is_shutdown_requested() -> bool:
225
- """Check if shutdown has been requested (global convenience function)."""
226
- return _global_shutdown_manager.is_shutdown_requested()
227
-
228
-
229
- def perform_graceful_shutdown_from_main_thread() -> None:
230
- """Perform graceful shutdown from main thread (global convenience function)."""
231
- _global_shutdown_manager.perform_graceful_shutdown_from_main_thread()
232
-
233
-
234
- def start_blocking_loop_with_shutdown_support(thread) -> None:
235
- """Start the main thread blocking loop with graceful shutdown support (global convenience function)."""
236
- _global_shutdown_manager.start_blocking_loop_with_shutdown_support(thread)