mcp-mesh 0.5.4__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.
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
 
@@ -24,6 +25,164 @@ _runtime_processor: Any | None = None
24
25
  _SHARED_AGENT_ID: str | None = None
25
26
 
26
27
 
28
+ def _start_uvicorn_immediately(http_host: str, http_port: int):
29
+ """
30
+ Start basic uvicorn server immediately to prevent Python interpreter shutdown.
31
+
32
+ This prevents the DNS threading conflicts by ensuring uvicorn takes control
33
+ before the script ends and Python enters shutdown state.
34
+ """
35
+ logger.info(
36
+ f"🎯 IMMEDIATE UVICORN: _start_uvicorn_immediately() called with host={http_host}, port={http_port}"
37
+ )
38
+
39
+ try:
40
+ import asyncio
41
+ import threading
42
+ import time
43
+
44
+ import uvicorn
45
+ from fastapi import FastAPI
46
+
47
+ logger.info(
48
+ "📦 IMMEDIATE UVICORN: Successfully imported uvicorn, FastAPI, threading, asyncio"
49
+ )
50
+
51
+ # Get stored FastMCP lifespan if available
52
+ fastmcp_lifespan = None
53
+ try:
54
+ from _mcp_mesh.engine.decorator_registry import DecoratorRegistry
55
+
56
+ fastmcp_lifespan = DecoratorRegistry.get_fastmcp_lifespan()
57
+ if fastmcp_lifespan:
58
+ logger.info(
59
+ "✅ IMMEDIATE UVICORN: Found stored FastMCP lifespan, will integrate with FastAPI"
60
+ )
61
+ else:
62
+ logger.info(
63
+ "🔍 IMMEDIATE UVICORN: No FastMCP lifespan found, creating basic FastAPI app"
64
+ )
65
+ except Exception as e:
66
+ logger.warning(f"⚠️ IMMEDIATE UVICORN: Failed to get FastMCP lifespan: {e}")
67
+
68
+ # Create FastAPI app with FastMCP lifespan if available
69
+ if fastmcp_lifespan:
70
+ app = FastAPI(title="MCP Mesh Agent (Starting)", lifespan=fastmcp_lifespan)
71
+ logger.info(
72
+ "📦 IMMEDIATE UVICORN: Created FastAPI app with FastMCP lifespan integration"
73
+ )
74
+ else:
75
+ app = FastAPI(title="MCP Mesh Agent (Starting)")
76
+ logger.info("📦 IMMEDIATE UVICORN: Created minimal FastAPI app")
77
+
78
+ # Add basic health endpoint
79
+ @app.get("/health")
80
+ def health():
81
+ return {
82
+ "status": "immediate_uvicorn",
83
+ "message": "MCP Mesh agent started via immediate uvicorn",
84
+ }
85
+
86
+ @app.get("/immediate-status")
87
+ def immediate_status():
88
+ return {
89
+ "immediate_uvicorn": True,
90
+ "message": "This server was started immediately in decorator",
91
+ }
92
+
93
+ logger.info("📦 IMMEDIATE UVICORN: Added health endpoints")
94
+
95
+ # Determine port (0 means auto-assign)
96
+ port = http_port if http_port > 0 else 8080
97
+
98
+ logger.info(
99
+ f"🚀 IMMEDIATE UVICORN: Starting uvicorn server on {http_host}:{port}"
100
+ )
101
+
102
+ # Use uvicorn.run() for proper signal handling (enables FastAPI lifespan shutdown)
103
+ logger.info(
104
+ "⚡ IMMEDIATE UVICORN: Starting server with uvicorn.run() for proper signal handling"
105
+ )
106
+
107
+ # Start uvicorn server in background thread (NON-daemon to keep process alive)
108
+ def run_server():
109
+ """Run uvicorn server in background thread with proper signal handling."""
110
+ try:
111
+ logger.info(
112
+ f"🌟 IMMEDIATE UVICORN: Starting server on {http_host}:{port}"
113
+ )
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
+ except Exception as e:
124
+ logger.error(f"❌ IMMEDIATE UVICORN: Server failed: {e}")
125
+ import traceback
126
+
127
+ logger.error(f"Server traceback: {traceback.format_exc()}")
128
+
129
+ # Start server in non-daemon thread so it can handle signals properly
130
+ thread = threading.Thread(target=run_server, daemon=False)
131
+ thread.start()
132
+
133
+ logger.info(
134
+ "🔒 IMMEDIATE UVICORN: Server thread started (daemon=False) - can handle signals"
135
+ )
136
+
137
+ # Store server reference in DecoratorRegistry BEFORE starting (critical timing)
138
+ server_info = {
139
+ "app": app,
140
+ "server": None, # No server object with uvicorn.run()
141
+ "config": None, # No config object needed
142
+ "host": http_host,
143
+ "port": port,
144
+ "thread": thread, # Server thread (non-daemon)
145
+ "type": "immediate_uvicorn_running",
146
+ "status": "running", # Server is now running in background thread
147
+ }
148
+
149
+ # Import here to avoid circular imports
150
+ from _mcp_mesh.engine.decorator_registry import DecoratorRegistry
151
+
152
+ DecoratorRegistry.store_immediate_uvicorn_server(server_info)
153
+
154
+ logger.info(
155
+ "🔄 IMMEDIATE UVICORN: Server reference stored in DecoratorRegistry BEFORE pipeline starts"
156
+ )
157
+
158
+ # Give server a moment to start
159
+ time.sleep(1)
160
+
161
+ logger.info(
162
+ f"✅ IMMEDIATE UVICORN: Uvicorn server running on {http_host}:{port} (daemon thread)"
163
+ )
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
+
174
+ # CRITICAL FIX: Keep main thread alive to prevent shutdown state
175
+ # This matches the working test setup pattern that prevents DNS resolution failures
176
+ # Uses simple shutdown with signal handlers for clean registry cleanup
177
+ start_blocking_loop_with_shutdown_support(thread)
178
+
179
+ except Exception as e:
180
+ logger.error(
181
+ f"❌ IMMEDIATE UVICORN: Failed to start immediate uvicorn server: {e}"
182
+ )
183
+ # Don't fail decorator application - pipeline can still try to start normally
184
+
185
+
27
186
  def _trigger_debounced_processing():
28
187
  """
29
188
  Trigger debounced processing when a decorator is applied.
@@ -37,6 +196,7 @@ def _trigger_debounced_processing():
37
196
  coordinator = get_debounce_coordinator()
38
197
  coordinator.trigger_processing()
39
198
  logger.debug("⚡ Triggered debounced processing")
199
+
40
200
  except ImportError:
41
201
  # Pipeline orchestrator not available - graceful degradation
42
202
  logger.debug(
@@ -391,10 +551,18 @@ def agent(
391
551
  if auto_run_interval < 1:
392
552
  raise ValueError("auto_run_interval must be at least 1 second")
393
553
 
394
- # Use centralized host resolution for external hostname
554
+ # Separate binding host (for uvicorn server) from external host (for registry)
395
555
  from _mcp_mesh.shared.host_resolver import HostResolver
396
556
 
397
- final_http_host = HostResolver.get_external_host()
557
+ # HOST variable for uvicorn binding (documented in environment-variables.md)
558
+ binding_host = get_config_value(
559
+ "HOST",
560
+ default="0.0.0.0",
561
+ rule=ValidationRule.STRING_RULE,
562
+ )
563
+
564
+ # External hostname for registry advertisement (MCP_MESH_HTTP_HOST)
565
+ external_host = HostResolver.get_external_host()
398
566
 
399
567
  final_http_port = get_config_value(
400
568
  "MCP_MESH_HTTP_PORT",
@@ -449,7 +617,7 @@ def agent(
449
617
  "name": name,
450
618
  "version": version,
451
619
  "description": description,
452
- "http_host": final_http_host,
620
+ "http_host": external_host,
453
621
  "http_port": final_http_port,
454
622
  "enable_http": final_enable_http,
455
623
  "namespace": final_namespace,
@@ -476,10 +644,89 @@ def agent(
476
644
  except Exception as e:
477
645
  logger.error(f"Runtime registration failed for agent {name}: {e}")
478
646
 
479
- # Auto-run functionality is now handled by the pipeline architecture
647
+ # Auto-run functionality: start uvicorn immediately to prevent Python shutdown state
480
648
  if final_auto_run:
481
649
  logger.info(
482
- f"🚀 Auto-run enabled for agent '{name}' - pipeline will start service automatically"
650
+ f"🚀 AGENT DECORATOR: Auto-run enabled for agent '{name}' - starting uvicorn immediately to prevent shutdown state"
651
+ )
652
+
653
+ # Create FastMCP lifespan before starting uvicorn for proper integration
654
+ fastmcp_lifespan = None
655
+ try:
656
+ # Try to create FastMCP server and extract lifespan
657
+ logger.info(
658
+ "🔍 AGENT DECORATOR: Creating FastMCP server for lifespan extraction"
659
+ )
660
+
661
+ # Look for FastMCP app in current module
662
+ import sys
663
+
664
+ current_module = sys.modules.get(target.__module__)
665
+ if current_module:
666
+ # Look for 'app' attribute (standard FastMCP pattern)
667
+ if hasattr(current_module, "app"):
668
+ fastmcp_server = current_module.app
669
+ logger.info(
670
+ f"🔍 AGENT DECORATOR: Found FastMCP server: {type(fastmcp_server)}"
671
+ )
672
+
673
+ # Create FastMCP HTTP app with stateless transport to get lifespan
674
+ if hasattr(fastmcp_server, "http_app") and callable(
675
+ fastmcp_server.http_app
676
+ ):
677
+ try:
678
+ fastmcp_http_app = fastmcp_server.http_app(
679
+ stateless_http=True, transport="streamable-http"
680
+ )
681
+ if hasattr(fastmcp_http_app, "lifespan"):
682
+ fastmcp_lifespan = fastmcp_http_app.lifespan
683
+ logger.info(
684
+ "✅ AGENT DECORATOR: Extracted FastMCP lifespan for FastAPI integration"
685
+ )
686
+
687
+ # Store both lifespan and HTTP app in DecoratorRegistry for uvicorn and pipeline to use
688
+ DecoratorRegistry.store_fastmcp_lifespan(
689
+ fastmcp_lifespan
690
+ )
691
+ DecoratorRegistry.store_fastmcp_http_app(
692
+ fastmcp_http_app
693
+ )
694
+ logger.info(
695
+ "✅ AGENT DECORATOR: Stored FastMCP HTTP app for proper mounting"
696
+ )
697
+ else:
698
+ logger.warning(
699
+ "⚠️ AGENT DECORATOR: FastMCP HTTP app has no lifespan attribute"
700
+ )
701
+ except Exception as e:
702
+ logger.warning(
703
+ f"⚠️ AGENT DECORATOR: Failed to create FastMCP HTTP app: {e}"
704
+ )
705
+ else:
706
+ logger.warning(
707
+ "⚠️ AGENT DECORATOR: FastMCP server has no http_app method"
708
+ )
709
+ else:
710
+ logger.info(
711
+ "🔍 AGENT DECORATOR: No FastMCP 'app' found in current module - will handle in pipeline"
712
+ )
713
+ else:
714
+ logger.warning(
715
+ "⚠️ AGENT DECORATOR: Could not access current module for FastMCP discovery"
716
+ )
717
+
718
+ except Exception as e:
719
+ logger.warning(
720
+ f"⚠️ AGENT DECORATOR: FastMCP lifespan creation failed: {e}"
721
+ )
722
+
723
+ logger.info(
724
+ f"🎯 AGENT DECORATOR: About to call _start_uvicorn_immediately({binding_host}, {final_http_port})"
725
+ )
726
+ # Start basic uvicorn server immediately to prevent interpreter shutdown
727
+ _start_uvicorn_immediately(binding_host, final_http_port)
728
+ logger.info(
729
+ "✅ AGENT DECORATOR: _start_uvicorn_immediately() call completed"
483
730
  )
484
731
 
485
732
  return target
@@ -494,17 +741,17 @@ def route(
494
741
  ) -> Callable[[T], T]:
495
742
  """
496
743
  FastAPI route handler decorator for dependency injection.
497
-
744
+
498
745
  Enables automatic dependency injection of MCP agents into FastAPI route handlers,
499
746
  eliminating the need for manual MCP client management in backend services.
500
-
747
+
501
748
  Args:
502
749
  dependencies: Optional list of agent capabilities to inject (default: [])
503
750
  **kwargs: Additional metadata for the route
504
-
751
+
505
752
  Returns:
506
753
  The original route handler function with dependency injection enabled
507
-
754
+
508
755
  Example:
509
756
  @app.post("/upload")
510
757
  @mesh.route(dependencies=["pdf-extractor", "user-service"])
@@ -518,28 +765,30 @@ def route(
518
765
  await user_service.update_profile(user_data, result)
519
766
  return {"success": True}
520
767
  """
521
-
768
+
522
769
  def decorator(target: T) -> T:
523
770
  # Validate and process dependencies (reuse logic from tool decorator)
524
771
  if dependencies is not None:
525
772
  if not isinstance(dependencies, list):
526
773
  raise ValueError("dependencies must be a list")
527
-
774
+
528
775
  validated_dependencies = []
529
776
  for dep in dependencies:
530
777
  if isinstance(dep, str):
531
778
  # Simple string dependency
532
- validated_dependencies.append({
533
- "capability": dep,
534
- "tags": [],
535
- })
779
+ validated_dependencies.append(
780
+ {
781
+ "capability": dep,
782
+ "tags": [],
783
+ }
784
+ )
536
785
  elif isinstance(dep, dict):
537
786
  # Complex dependency with metadata
538
787
  if "capability" not in dep:
539
788
  raise ValueError("dependency must have 'capability' field")
540
789
  if not isinstance(dep["capability"], str):
541
790
  raise ValueError("dependency capability must be a string")
542
-
791
+
543
792
  # Validate optional dependency fields
544
793
  dep_tags = dep.get("tags", [])
545
794
  if not isinstance(dep_tags, list):
@@ -547,11 +796,11 @@ def route(
547
796
  for tag in dep_tags:
548
797
  if not isinstance(tag, str):
549
798
  raise ValueError("all dependency tags must be strings")
550
-
799
+
551
800
  dep_version = dep.get("version")
552
801
  if dep_version is not None and not isinstance(dep_version, str):
553
802
  raise ValueError("dependency version must be a string")
554
-
803
+
555
804
  dependency_dict = {
556
805
  "capability": dep["capability"],
557
806
  "tags": dep_tags,
@@ -563,20 +812,20 @@ def route(
563
812
  raise ValueError("dependencies must be strings or dictionaries")
564
813
  else:
565
814
  validated_dependencies = []
566
-
815
+
567
816
  # Build route metadata
568
817
  metadata = {
569
818
  "dependencies": validated_dependencies,
570
819
  "description": getattr(target, "__doc__", None),
571
820
  **kwargs,
572
821
  }
573
-
822
+
574
823
  # Store metadata on function
575
824
  target._mesh_route_metadata = metadata
576
-
825
+
577
826
  # Register with DecoratorRegistry using custom decorator type
578
827
  DecoratorRegistry.register_custom_decorator("mesh_route", target, metadata)
579
-
828
+
580
829
  # Try to add tracing middleware to any FastAPI apps we can find immediately
581
830
  # This ensures middleware is added before the app starts
582
831
  try:
@@ -584,20 +833,22 @@ def route(
584
833
  except Exception as e:
585
834
  # Don't fail decorator application due to middleware issues
586
835
  logger.debug(f"Failed to add immediate tracing middleware: {e}")
587
-
836
+
588
837
  logger.debug(
589
838
  f"🔍 Route '{target.__name__}' registered with {len(validated_dependencies)} dependencies"
590
839
  )
591
-
840
+
592
841
  try:
593
842
  # Import here to avoid circular imports
594
843
  from _mcp_mesh.engine.dependency_injector import get_global_injector
595
844
 
596
- # Extract dependency names for injector
845
+ # Extract dependency names for injector
597
846
  dependency_names = [dep["capability"] for dep in validated_dependencies]
598
847
 
599
848
  # Log the original function pointer
600
- logger.debug(f"🔸 ORIGINAL route function pointer: {target} at {hex(id(target))}")
849
+ logger.debug(
850
+ f"🔸 ORIGINAL route function pointer: {target} at {hex(id(target))}"
851
+ )
601
852
 
602
853
  injector = get_global_injector()
603
854
  wrapped = injector.create_injection_wrapper(target, dependency_names)
@@ -612,12 +863,14 @@ def route(
612
863
 
613
864
  # Store the wrapper on the original function for reference
614
865
  target._mesh_injection_wrapper = wrapped
615
-
866
+
616
867
  # Also store a flag on the wrapper itself so route integration can detect it
617
868
  wrapped._mesh_is_injection_wrapper = True
618
869
 
619
870
  # Return the wrapped function - FastAPI will register this wrapper when it runs
620
- logger.debug(f"✅ Returning injection wrapper for route '{target.__name__}'")
871
+ logger.debug(
872
+ f"✅ Returning injection wrapper for route '{target.__name__}'"
873
+ )
621
874
  logger.debug(f"🔹 Returning WRAPPER: {wrapped} at {hex(id(wrapped))}")
622
875
 
623
876
  # Trigger debounced processing before returning
@@ -629,32 +882,36 @@ def route(
629
882
  logger.error(
630
883
  f"Route dependency injection setup failed for {target.__name__}: {e}"
631
884
  )
632
-
885
+
633
886
  # Fallback: return original function and trigger processing
634
887
  _trigger_debounced_processing()
635
888
  return target
636
-
889
+
637
890
  return decorator
638
891
 
639
892
 
640
893
  def _add_tracing_middleware_immediately():
641
894
  """
642
895
  Request tracing middleware injection using monkey-patch approach.
643
-
896
+
644
897
  This sets up automatic middleware injection for both existing and future
645
898
  FastAPI apps, eliminating timing issues with app startup/lifespan.
646
899
  """
647
900
  try:
648
- from _mcp_mesh.shared.fastapi_middleware_manager import get_fastapi_middleware_manager
649
-
901
+ from _mcp_mesh.shared.fastapi_middleware_manager import (
902
+ get_fastapi_middleware_manager,
903
+ )
904
+
650
905
  manager = get_fastapi_middleware_manager()
651
906
  success = manager.request_middleware_injection()
652
-
907
+
653
908
  if success:
654
- logger.debug("🔍 TRACING: Middleware injection setup completed (monkey-patch + discovery)")
909
+ logger.debug(
910
+ "🔍 TRACING: Middleware injection setup completed (monkey-patch + discovery)"
911
+ )
655
912
  else:
656
913
  logger.debug("🔍 TRACING: Middleware injection setup failed")
657
-
914
+
658
915
  except Exception as e:
659
916
  # Never fail decorator application
660
917
  logger.debug(f"🔍 TRACING: Middleware injection setup failed: {e}")
@@ -662,3 +919,13 @@ def _add_tracing_middleware_immediately():
662
919
 
663
920
  # Middleware injection is now handled by FastAPIMiddlewareManager
664
921
  # in _mcp_mesh.shared.fastapi_middleware_manager
922
+
923
+
924
+ # Graceful shutdown functions have been moved to _mcp_mesh.shared.graceful_shutdown_manager
925
+ # This maintains backward compatibility for existing pipeline code
926
+
927
+
928
+ def set_shutdown_context(context: dict[str, Any]):
929
+ """Set context for graceful shutdown (called from pipeline)."""
930
+ # Delegate to the shared graceful shutdown manager
931
+ set_global_shutdown_context(context)