mcp-mesh 0.4.1__py3-none-any.whl → 0.5.0__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.
Files changed (55) hide show
  1. _mcp_mesh/__init__.py +14 -3
  2. _mcp_mesh/engine/async_mcp_client.py +6 -19
  3. _mcp_mesh/engine/dependency_injector.py +161 -74
  4. _mcp_mesh/engine/full_mcp_proxy.py +25 -20
  5. _mcp_mesh/engine/mcp_client_proxy.py +5 -19
  6. _mcp_mesh/generated/.openapi-generator/FILES +2 -0
  7. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +2 -0
  8. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +1 -0
  9. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +305 -0
  10. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +1 -0
  11. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +10 -1
  12. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +4 -4
  13. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +108 -0
  14. _mcp_mesh/pipeline/__init__.py +2 -2
  15. _mcp_mesh/pipeline/api_heartbeat/__init__.py +16 -0
  16. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +515 -0
  17. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +117 -0
  18. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +140 -0
  19. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +247 -0
  20. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +309 -0
  21. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +332 -0
  22. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +147 -0
  23. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +97 -0
  24. _mcp_mesh/pipeline/api_startup/__init__.py +20 -0
  25. _mcp_mesh/pipeline/api_startup/api_pipeline.py +61 -0
  26. _mcp_mesh/pipeline/api_startup/api_server_setup.py +292 -0
  27. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +302 -0
  28. _mcp_mesh/pipeline/api_startup/route_collection.py +56 -0
  29. _mcp_mesh/pipeline/api_startup/route_integration.py +318 -0
  30. _mcp_mesh/pipeline/{startup → mcp_startup}/fastmcpserver_discovery.py +4 -4
  31. _mcp_mesh/pipeline/{startup → mcp_startup}/heartbeat_loop.py +1 -1
  32. _mcp_mesh/pipeline/{startup → mcp_startup}/startup_orchestrator.py +170 -5
  33. _mcp_mesh/shared/config_resolver.py +0 -3
  34. _mcp_mesh/shared/logging_config.py +2 -1
  35. _mcp_mesh/shared/sse_parser.py +217 -0
  36. {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/METADATA +1 -1
  37. {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/RECORD +55 -37
  38. mesh/__init__.py +6 -2
  39. mesh/decorators.py +143 -1
  40. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/__init__.py +0 -0
  41. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/dependency_resolution.py +0 -0
  42. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/fast_heartbeat_check.py +0 -0
  43. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_orchestrator.py +0 -0
  44. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_pipeline.py +0 -0
  45. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_send.py +0 -0
  46. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/lifespan_integration.py +0 -0
  47. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/registry_connection.py +0 -0
  48. /_mcp_mesh/pipeline/{startup → mcp_startup}/__init__.py +0 -0
  49. /_mcp_mesh/pipeline/{startup → mcp_startup}/configuration.py +0 -0
  50. /_mcp_mesh/pipeline/{startup → mcp_startup}/decorator_collection.py +0 -0
  51. /_mcp_mesh/pipeline/{startup → mcp_startup}/fastapiserver_setup.py +0 -0
  52. /_mcp_mesh/pipeline/{startup → mcp_startup}/heartbeat_preparation.py +0 -0
  53. /_mcp_mesh/pipeline/{startup → mcp_startup}/startup_pipeline.py +0 -0
  54. {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/WHEEL +0 -0
  55. {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,140 @@
1
+ """
2
+ API health check step for API heartbeat pipeline.
3
+
4
+ Validates FastAPI application health status and endpoint availability
5
+ for heartbeat reporting to the registry.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any
10
+
11
+ from ..shared.base_step import PipelineStep
12
+ from ..shared.pipeline_types import PipelineResult
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class APIHealthCheckStep(PipelineStep):
18
+ """
19
+ Check FastAPI application health status.
20
+
21
+ Validates that the FastAPI application is running properly
22
+ and endpoints are accessible for dependency injection.
23
+ """
24
+
25
+ def __init__(self, required: bool = True):
26
+ super().__init__(
27
+ name="api-health-check",
28
+ required=required,
29
+ )
30
+
31
+ async def execute(self, context: dict[str, Any]) -> PipelineResult:
32
+ """
33
+ Check FastAPI application health status.
34
+
35
+ Args:
36
+ context: Pipeline context containing fastapi_app and service info
37
+
38
+ Returns:
39
+ PipelineResult with health_status in context
40
+ """
41
+ self.logger.info("🏥 [DEBUG] Checking FastAPI application health status")
42
+
43
+ try:
44
+ # Get FastAPI app from context
45
+ fastapi_app = context.get("fastapi_app")
46
+ service_id = context.get("service_id") or context.get("agent_id", "unknown")
47
+
48
+ if not fastapi_app:
49
+ error_msg = "No FastAPI application found in context for health check"
50
+ self.logger.error(f"❌ {error_msg}")
51
+
52
+ from ..shared.pipeline_types import PipelineStatus
53
+ result = PipelineResult(
54
+ status=PipelineStatus.FAILED,
55
+ message=error_msg,
56
+ context=context
57
+ )
58
+ result.add_error(error_msg)
59
+ return result
60
+
61
+ # Check FastAPI app basic properties
62
+ app_title = getattr(fastapi_app, "title", "Unknown API")
63
+ app_version = getattr(fastapi_app, "version", "1.0.0")
64
+
65
+ # Count available routes with dependency injection
66
+ routes_with_mesh = self._count_mesh_routes(fastapi_app)
67
+ total_routes = len(getattr(fastapi_app, "routes", []))
68
+
69
+ self.logger.debug(
70
+ f"🔍 FastAPI app health: {app_title} v{app_version}, "
71
+ f"{routes_with_mesh}/{total_routes} routes with mesh injection"
72
+ )
73
+
74
+ # Build health status for API service
75
+ # For API services, we create a simplified health status dict instead of using
76
+ # the strict HealthStatus model which requires capabilities (designed for MCP agents)
77
+ from datetime import UTC, datetime
78
+
79
+ health_status_dict = {
80
+ "agent_name": service_id,
81
+ "status": "healthy",
82
+ "timestamp": datetime.now(UTC).isoformat(),
83
+ "version": app_version,
84
+ "metadata": {
85
+ "service_type": "api",
86
+ "app_title": app_title,
87
+ "app_version": app_version,
88
+ "routes_total": total_routes,
89
+ "routes_with_mesh": routes_with_mesh,
90
+ "health_check_timestamp": datetime.now(UTC).isoformat(),
91
+ }
92
+ }
93
+
94
+ self.logger.info(
95
+ f"🏥 API health check passed: {app_title} v{app_version} "
96
+ f"({routes_with_mesh} mesh routes)"
97
+ )
98
+
99
+ return PipelineResult(
100
+ message=f"API health check passed for {app_title}",
101
+ context={
102
+ "health_status": health_status_dict,
103
+ "app_title": app_title,
104
+ "app_version": app_version,
105
+ "routes_total": total_routes,
106
+ "routes_with_mesh": routes_with_mesh,
107
+ }
108
+ )
109
+
110
+ except Exception as e:
111
+ error_msg = f"API health check failed: {e}"
112
+ self.logger.error(f"❌ {error_msg}")
113
+
114
+ from ..shared.pipeline_types import PipelineStatus
115
+ result = PipelineResult(
116
+ status=PipelineStatus.FAILED,
117
+ message=error_msg,
118
+ context=context
119
+ )
120
+ result.add_error(str(e))
121
+ return result
122
+
123
+ def _count_mesh_routes(self, fastapi_app: Any) -> int:
124
+ """Count routes that have mesh dependency injection applied."""
125
+ try:
126
+ mesh_routes = 0
127
+ routes = getattr(fastapi_app, "routes", [])
128
+
129
+ for route in routes:
130
+ # Check if route has dependency injection wrapper
131
+ endpoint = getattr(route, "endpoint", None)
132
+ if endpoint and hasattr(endpoint, "__wrapped__"):
133
+ # This indicates our dependency injection wrapper
134
+ mesh_routes += 1
135
+
136
+ return mesh_routes
137
+
138
+ except Exception as e:
139
+ self.logger.warning(f"⚠️ Could not count mesh routes: {e}")
140
+ return 0
@@ -0,0 +1,247 @@
1
+ """
2
+ API heartbeat orchestrator for managing periodic pipeline execution.
3
+
4
+ Provides a high-level interface for executing API heartbeat pipelines
5
+ with proper context management and error handling for FastAPI services.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ from datetime import UTC, datetime
11
+ from typing import Any, Dict, Optional
12
+
13
+ from .api_heartbeat_pipeline import APIHeartbeatPipeline
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class APIHeartbeatOrchestrator:
19
+ """
20
+ Orchestrates API heartbeat pipeline execution for periodic registry communication.
21
+
22
+ Manages the context preparation, pipeline execution, and result processing
23
+ for the periodic heartbeat cycle of FastAPI services using @mesh.route decorators.
24
+ """
25
+
26
+ def __init__(self):
27
+ self.logger = logging.getLogger(f"{__name__}.APIHeartbeatOrchestrator")
28
+ self.pipeline = APIHeartbeatPipeline()
29
+ self._heartbeat_count = 0
30
+
31
+ async def execute_api_heartbeat(
32
+ self, service_id: str, context: dict[str, Any]
33
+ ) -> bool:
34
+ """
35
+ Execute a complete API heartbeat cycle with comprehensive error handling.
36
+
37
+ Args:
38
+ service_id: Service identifier for the FastAPI application
39
+ context: Full pipeline context from API startup
40
+
41
+ Returns:
42
+ bool: True if heartbeat succeeded, False if failed
43
+ """
44
+ self._heartbeat_count += 1
45
+
46
+ try:
47
+ # Prepare heartbeat context with validation
48
+ heartbeat_context = self._prepare_api_heartbeat_context(service_id, context)
49
+
50
+ # Validate required context before proceeding
51
+ if not self._validate_api_heartbeat_context(heartbeat_context):
52
+ self.logger.error(
53
+ f"❌ API heartbeat #{self._heartbeat_count} failed: invalid context"
54
+ )
55
+ return False
56
+
57
+ # Log heartbeat request details for debugging
58
+ self._log_api_heartbeat_request(heartbeat_context, self._heartbeat_count)
59
+
60
+ # Execute API heartbeat pipeline with timeout protection
61
+ self.logger.info(f"💓 Executing API heartbeat #{self._heartbeat_count} for service '{service_id}'")
62
+
63
+ # Add timeout to prevent hanging heartbeats (30 seconds max)
64
+ import asyncio
65
+
66
+ try:
67
+ self.logger.debug("Starting API heartbeat pipeline execution")
68
+ result = await asyncio.wait_for(
69
+ self.pipeline.execute_api_heartbeat_cycle(heartbeat_context),
70
+ timeout=30.0,
71
+ )
72
+ if result.is_success():
73
+ self.logger.debug("✅ API heartbeat pipeline completed successfully")
74
+ else:
75
+ self.logger.error(f"❌ API heartbeat pipeline failed: {result.message}")
76
+ except TimeoutError:
77
+ self.logger.error(
78
+ f"❌ API heartbeat #{self._heartbeat_count} timed out after 30 seconds"
79
+ )
80
+ return False
81
+ except Exception as e:
82
+ self.logger.error(f"❌ [DEBUG] Pipeline execution exception: {e}")
83
+ import traceback
84
+ self.logger.error(f"❌ [DEBUG] Traceback: {traceback.format_exc()}")
85
+ return False
86
+
87
+ # Process results
88
+ success = self._process_api_heartbeat_result(
89
+ result, service_id, self._heartbeat_count
90
+ )
91
+
92
+ # Log periodic status updates
93
+ if self._heartbeat_count % 10 == 0:
94
+ elapsed_time = self._heartbeat_count * 5 # Using 5s interval (MeshDefaults.HEALTH_INTERVAL)
95
+ self.logger.info(
96
+ f"💓 API heartbeat #{self._heartbeat_count} for service '{service_id}' - "
97
+ f"running for {elapsed_time} seconds"
98
+ )
99
+
100
+ return success
101
+
102
+ except Exception as e:
103
+ # Log detailed error information for debugging
104
+ import traceback
105
+
106
+ self.logger.error(
107
+ f"❌ API heartbeat #{self._heartbeat_count} failed for service '{service_id}': {e}\n"
108
+ f"Traceback: {traceback.format_exc()}"
109
+ )
110
+ return False
111
+
112
+ def _prepare_api_heartbeat_context(
113
+ self, service_id: str, startup_context: dict[str, Any]
114
+ ) -> dict[str, Any]:
115
+ """Prepare context for API heartbeat pipeline execution."""
116
+
117
+ # Get FastAPI app and other essential components from startup context
118
+ fastapi_app = startup_context.get("fastapi_app")
119
+ display_config = startup_context.get("display_config", {})
120
+
121
+ # Get API service metadata from startup context
122
+ api_service_metadata = startup_context.get("api_service_metadata", {})
123
+ self.logger.debug(f"🔍 Startup context has api_service_metadata: {len(api_service_metadata) > 0}")
124
+ if api_service_metadata:
125
+ capabilities = api_service_metadata.get("capabilities", [])
126
+ self.logger.debug(f"🔍 API service has {len(capabilities)} route capabilities")
127
+
128
+ # Build heartbeat-specific context
129
+ heartbeat_context = {
130
+ "service_id": service_id,
131
+ "agent_id": service_id, # For compatibility with registry calls
132
+ "fastapi_app": fastapi_app,
133
+ "display_config": display_config,
134
+ # Include registry and configuration from startup
135
+ "agent_config": startup_context.get("agent_config", {}),
136
+ "registration_data": startup_context.get("registration_data", {}),
137
+ "registry_wrapper": startup_context.get("registry_wrapper"),
138
+ # CRITICAL: Include API service metadata with route dependencies
139
+ "api_service_metadata": api_service_metadata,
140
+ }
141
+
142
+ return heartbeat_context
143
+
144
+ def _validate_api_heartbeat_context(self, heartbeat_context: dict[str, Any]) -> bool:
145
+ """Validate that API heartbeat context has all required components."""
146
+
147
+ required_fields = ["service_id", "fastapi_app"]
148
+
149
+ for field in required_fields:
150
+ if field not in heartbeat_context or heartbeat_context[field] is None:
151
+ self.logger.error(
152
+ f"❌ API heartbeat context validation failed: missing '{field}'"
153
+ )
154
+ return False
155
+
156
+ # Additional validation for FastAPI app
157
+ fastapi_app = heartbeat_context.get("fastapi_app")
158
+ if not hasattr(fastapi_app, "routes"):
159
+ self.logger.error(
160
+ "❌ API heartbeat context validation failed: invalid FastAPI app object"
161
+ )
162
+ return False
163
+
164
+ return True
165
+
166
+ def _log_api_heartbeat_request(
167
+ self, heartbeat_context: dict[str, Any], heartbeat_count: int
168
+ ) -> None:
169
+ """Log API heartbeat request details for debugging."""
170
+
171
+ service_id = heartbeat_context.get("service_id", "unknown")
172
+ fastapi_app = heartbeat_context.get("fastapi_app")
173
+ display_config = heartbeat_context.get("display_config", {})
174
+
175
+ # Extract app information for logging
176
+ app_info = {}
177
+ if fastapi_app:
178
+ app_info = {
179
+ "title": getattr(fastapi_app, "title", "Unknown API"),
180
+ "version": getattr(fastapi_app, "version", "1.0.0"),
181
+ "routes_count": len(getattr(fastapi_app, "routes", [])),
182
+ }
183
+
184
+ # Log heartbeat details
185
+ self.logger.debug(
186
+ f"🔍 API Heartbeat #{heartbeat_count} for '{service_id}': "
187
+ f"app={app_info}, display={display_config}"
188
+ )
189
+
190
+ def _process_api_heartbeat_result(
191
+ self, result: Any, service_id: str, heartbeat_count: int
192
+ ) -> bool:
193
+ """Process API heartbeat pipeline result and log appropriately."""
194
+
195
+ if result.is_success():
196
+ # Check for heartbeat response in result context
197
+ heartbeat_response = result.context.get("heartbeat_response")
198
+ heartbeat_success = result.context.get("heartbeat_success", False)
199
+
200
+ self.logger.debug(f"API heartbeat result - success: {heartbeat_success}")
201
+
202
+ # Check if heartbeat was skipped due to optimization
203
+ heartbeat_skipped = result.context.get("heartbeat_skipped", False)
204
+ skip_reason = result.context.get("skip_reason")
205
+
206
+ if heartbeat_success and heartbeat_response:
207
+ # Log response details for debugging
208
+ try:
209
+ response_json = json.dumps(
210
+ heartbeat_response, indent=2, default=str
211
+ )
212
+ self.logger.debug(
213
+ f"🔍 API heartbeat response #{heartbeat_count}:\n{response_json}"
214
+ )
215
+ except Exception as e:
216
+ self.logger.debug(
217
+ f"🔍 API heartbeat response #{heartbeat_count}: {heartbeat_response} "
218
+ f"(json serialization failed: {e})"
219
+ )
220
+
221
+ self.logger.info(
222
+ f"💚 API heartbeat #{heartbeat_count} sent successfully for service '{service_id}'"
223
+ )
224
+ return True
225
+ elif heartbeat_success and heartbeat_skipped:
226
+ # Heartbeat was skipped for optimization - this is success
227
+ self.logger.debug(
228
+ f"🚀 API heartbeat #{heartbeat_count} skipped for service '{service_id}' - {skip_reason}"
229
+ )
230
+ return True
231
+ else:
232
+ self.logger.warning(
233
+ f"💔 [UPDATED] API heartbeat #{heartbeat_count} failed for service '{service_id}' - "
234
+ f"no response or unsuccessful (heartbeat_success={heartbeat_success}, heartbeat_response={heartbeat_response})"
235
+ )
236
+ return False
237
+ else:
238
+ self.logger.warning(
239
+ f"💔 [UPDATED-PIPELINE] API heartbeat #{heartbeat_count} pipeline failed for service '{service_id}': {result.message}"
240
+ )
241
+
242
+ # Log detailed errors
243
+ if hasattr(result, 'errors') and result.errors:
244
+ for error in result.errors:
245
+ self.logger.warning(f" - API heartbeat error: {error}")
246
+
247
+ return False