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.
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/PKG-INFO +1 -1
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/__init__.py +1 -1
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +13 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +40 -4
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +4 -154
- mcp_mesh-0.5.6/_mcp_mesh/shared/simple_shutdown.py +217 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/mesh/decorators.py +29 -20
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/pyproject.toml +4 -4
- mcp_mesh-0.5.5/_mcp_mesh/shared/graceful_shutdown_manager.py +0 -236
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/.gitignore +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/LICENSE +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/README.md +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/async_mcp_client.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/decorator_registry.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/dependency_injector.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/full_mcp_proxy.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/http_wrapper.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/mcp_client_proxy.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/self_dependency_proxy.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/session_aware_client.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/session_manager.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/signature_analyzer.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/engine/unified_mcp_proxy.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/.openapi-generator/FILES +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/.openapi-generator/VERSION +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/.openapi-generator-ignore +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -0
- {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
- {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
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -0
- {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
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -0
- {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
- {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
- {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
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/api_pipeline.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/api_server_setup.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/fastapi_discovery.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/middleware_integration.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/route_collection.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/api_startup/route_integration.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/configuration.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/decorator_collection.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/server_discovery.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/shared/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/shared/base_step.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/shared/pipeline_types.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/pipeline/shared/registry_connection.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/__init__.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/config_resolver.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/content_extractor.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/defaults.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/fast_heartbeat_status.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/fastapi_middleware_manager.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/host_resolver.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/logging_config.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/registry_client_wrapper.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/server_discovery.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/sse_parser.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/shared/support_types.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/agent_context_helper.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/context.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/execution_tracer.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/fastapi_tracing_middleware.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/redis_metadata_publisher.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/trace_context_helper.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/_mcp_mesh/tracing/utils.py +0 -0
- {mcp_mesh-0.5.5 → mcp_mesh-0.5.6}/mesh/__init__.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
#
|
|
337
|
-
|
|
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
|
-
#
|
|
363
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
130
|
-
thread = threading.Thread(target=run_server, daemon=
|
|
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=
|
|
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":
|
|
141
|
-
"config":
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
169
|
+
minversion = "0.5.6"
|
|
170
170
|
addopts = "-ra -q --strict-markers --strict-config"
|
|
171
171
|
testpaths = [
|
|
172
172
|
"tests",
|