mcp-mesh 0.5.5__tar.gz → 0.5.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/PKG-INFO +1 -1
  2. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/__init__.py +1 -1
  3. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +13 -0
  4. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +40 -4
  5. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +4 -154
  6. mcp_mesh-0.5.6/_mcp_mesh/shared/simple_shutdown.py +217 -0
  7. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/mesh/decorators.py +29 -20
  8. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/pyproject.toml +4 -4
  9. mcp_mesh-0.5.5/_mcp_mesh/shared/graceful_shutdown_manager.py +0 -236
  10. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/.gitignore +0 -0
  11. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/LICENSE +0 -0
  12. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/README.md +0 -0
  13. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/__init__.py +0 -0
  14. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/async_mcp_client.py +0 -0
  15. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/decorator_registry.py +0 -0
  16. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/dependency_injector.py +0 -0
  17. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/full_mcp_proxy.py +0 -0
  18. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/http_wrapper.py +0 -0
  19. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/mcp_client_proxy.py +0 -0
  20. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/self_dependency_proxy.py +0 -0
  21. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/session_aware_client.py +0 -0
  22. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/session_manager.py +0 -0
  23. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/signature_analyzer.py +0 -0
  24. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/unified_mcp_proxy.py +0 -0
  25. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/.openapi-generator/FILES +0 -0
  26. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/.openapi-generator/VERSION +0 -0
  27. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/.openapi-generator-ignore +0 -0
  28. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -0
  29. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -0
  30. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -0
  31. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -0
  32. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -0
  33. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -0
  34. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -0
  35. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -0
  36. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -0
  37. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -0
  38. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -0
  39. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -0
  40. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -0
  41. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -0
  42. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -0
  43. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -0
  44. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -0
  45. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -0
  46. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -0
  47. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -0
  48. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -0
  49. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -0
  50. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -0
  51. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -0
  52. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -0
  53. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -0
  54. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -0
  55. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -0
  56. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -0
  57. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -0
  58. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -0
  59. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -0
  60. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -0
  61. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -0
  62. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -0
  63. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -0
  64. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -0
  65. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -0
  66. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -0
  67. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
  68. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -0
  69. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/__init__.py +0 -0
  70. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/__init__.py +0 -0
  71. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -0
  72. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -0
  73. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -0
  74. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -0
  75. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -0
  76. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -0
  77. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +0 -0
  78. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -0
  79. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/__init__.py +0 -0
  80. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/api_pipeline.py +0 -0
  81. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/api_server_setup.py +0 -0
  82. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/fastapi_discovery.py +0 -0
  83. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/middleware_integration.py +0 -0
  84. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/route_collection.py +0 -0
  85. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/route_integration.py +0 -0
  86. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/__init__.py +0 -0
  87. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -0
  88. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -0
  89. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -0
  90. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -0
  91. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -0
  92. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -0
  93. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/__init__.py +0 -0
  94. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/configuration.py +0 -0
  95. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/decorator_collection.py +0 -0
  96. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +0 -0
  97. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +0 -0
  98. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +0 -0
  99. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/server_discovery.py +0 -0
  100. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +0 -0
  101. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/shared/__init__.py +0 -0
  102. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/shared/base_step.py +0 -0
  103. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -0
  104. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/shared/pipeline_types.py +0 -0
  105. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/shared/registry_connection.py +0 -0
  106. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/__init__.py +0 -0
  107. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/config_resolver.py +0 -0
  108. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/content_extractor.py +0 -0
  109. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/defaults.py +0 -0
  110. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/fast_heartbeat_status.py +0 -0
  111. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/fastapi_middleware_manager.py +0 -0
  112. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/host_resolver.py +0 -0
  113. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/logging_config.py +0 -0
  114. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/registry_client_wrapper.py +0 -0
  115. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/server_discovery.py +0 -0
  116. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/sse_parser.py +0 -0
  117. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/support_types.py +0 -0
  118. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/agent_context_helper.py +0 -0
  119. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/context.py +0 -0
  120. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/execution_tracer.py +0 -0
  121. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/fastapi_tracing_middleware.py +0 -0
  122. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/redis_metadata_publisher.py +0 -0
  123. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/trace_context_helper.py +0 -0
  124. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/utils.py +0 -0
  125. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/mesh/__init__.py +0 -0
  126. {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/mesh/types.py +0 -0
@@ -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
@@ -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")
@@ -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:
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "mcp-mesh"
9
- version = "0.5.5"
9
+ version = "0.5.6"
10
10
  description = "Kubernetes-native platform for distributed MCP applications"
11
11
  readme = "README.md"
12
12
  license = { text = "MIT" }
@@ -117,7 +117,7 @@ extend-exclude = '''
117
117
  '''
118
118
 
119
119
  [tool.ruff]
120
- target-version = "0.5.5"
120
+ target-version = "0.5.6"
121
121
  line-length = 88
122
122
 
123
123
  [tool.ruff.lint]
@@ -154,7 +154,7 @@ ignore = [
154
154
  "tests/**" = ["E712", "F841", "B007", "C401", "F401"] # Relax style requirements for test files
155
155
 
156
156
  [tool.mypy]
157
- python_version = "0.5.5"
157
+ python_version = "0.5.6"
158
158
  check_untyped_defs = false # Temporarily relaxed
159
159
  disallow_any_generics = false # Temporarily relaxed
160
160
  disallow_incomplete_defs = false # Temporarily relaxed
@@ -166,7 +166,7 @@ warn_return_any = false # Temporarily relaxed
166
166
  exclude = ["tests/", ".*agent_server_generated.*", ".*registry_client_generated.*"] # Skip type checking for test and generated files
167
167
 
168
168
  [tool.pytest.ini_options]
169
- minversion = "0.5.5"
169
+ minversion = "0.5.6"
170
170
  addopts = "-ra -q --strict-markers --strict-config"
171
171
  testpaths = [
172
172
  "tests",