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,309 @@
1
+ """
2
+ API heartbeat pipeline for FastAPI service health monitoring.
3
+
4
+ Provides structured execution of API service heartbeat operations with proper
5
+ error handling and logging. Runs periodically to maintain registry communication
6
+ and service health status for FastAPI applications using @mesh.route decorators.
7
+ """
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ from ...shared.fast_heartbeat_status import FastHeartbeatStatus, FastHeartbeatStatusUtil
13
+ from ..shared.mesh_pipeline import MeshPipeline
14
+ from ..shared.pipeline_types import PipelineStatus
15
+ from .api_registry_connection import APIRegistryConnectionStep
16
+ from .api_health_check import APIHealthCheckStep
17
+ from .api_fast_heartbeat_check import APIFastHeartbeatStep
18
+ from .api_heartbeat_send import APIHeartbeatSendStep
19
+ from .api_dependency_resolution import APIDependencyResolutionStep
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class APIHeartbeatPipeline(MeshPipeline):
25
+ """
26
+ Specialized pipeline for API service heartbeat operations with fast optimization.
27
+
28
+ Executes the core API heartbeat steps in sequence:
29
+ 1. Registry connection preparation
30
+ 2. API health check (validate FastAPI app status)
31
+ 3. Fast heartbeat check (HEAD request)
32
+ 4. Heartbeat sending (conditional POST request)
33
+ 5. Dependency resolution (conditional)
34
+
35
+ Steps 4 and 5 only run if fast heartbeat indicates changes are needed.
36
+ Provides optimization for NO_CHANGES and resilience for error conditions.
37
+
38
+ API services now support:
39
+ - Service availability and health status monitoring
40
+ - Efficient HEAD->conditional POST pattern (like MCP agents)
41
+ - Dynamic dependency resolution and injection
42
+ - Route handler dependency updates from registry responses
43
+ """
44
+
45
+ def __init__(self, name: str = "api-heartbeat-pipeline"):
46
+ super().__init__(name=name)
47
+ self._setup_api_heartbeat_steps()
48
+
49
+ def _setup_api_heartbeat_steps(self) -> None:
50
+ """Setup the API heartbeat pipeline steps with fast optimization."""
51
+ # API heartbeat steps with fast optimization pattern
52
+ steps = [
53
+ APIRegistryConnectionStep(), # Prepare registry communication
54
+ APIHealthCheckStep(), # Check FastAPI app health status
55
+ APIFastHeartbeatStep(), # Fast heartbeat check (HEAD request)
56
+ APIHeartbeatSendStep(), # Conditional heartbeat send (POST request)
57
+ APIDependencyResolutionStep(), # Conditional dependency resolution
58
+ ]
59
+
60
+ self.add_steps(steps)
61
+ self.logger.debug(f"API heartbeat pipeline configured with {len(steps)} steps")
62
+
63
+ # Log the pipeline strategy
64
+ self.logger.info(
65
+ f"🌐 API Heartbeat Pipeline initialized: fast optimization for FastAPI apps"
66
+ )
67
+ self.logger.debug(
68
+ f"📋 Pipeline steps: {[step.name for step in steps]}"
69
+ )
70
+
71
+ async def execute_api_heartbeat_cycle(
72
+ self, heartbeat_context: dict[str, Any]
73
+ ) -> Any:
74
+ """
75
+ Execute a complete API heartbeat cycle with fast optimization and enhanced error handling.
76
+
77
+ Args:
78
+ heartbeat_context: Context containing registry_wrapper, service_id,
79
+ health_status, fastapi_app, etc.
80
+
81
+ Returns:
82
+ PipelineResult with execution status and any context updates
83
+ """
84
+ self.logger.debug("Starting API heartbeat pipeline execution")
85
+
86
+ # Initialize pipeline context with heartbeat-specific data
87
+ self.context.clear()
88
+ self.context.update(heartbeat_context)
89
+
90
+ try:
91
+ # Execute the pipeline with conditional logic for fast optimization
92
+ result = await self._execute_with_conditional_logic()
93
+
94
+ if result.is_success():
95
+ self.logger.debug("✅ API heartbeat pipeline completed successfully")
96
+ elif result.status == PipelineStatus.PARTIAL:
97
+ self.logger.warning(
98
+ f"⚠️ API heartbeat pipeline completed partially: {result.message}"
99
+ )
100
+ # Log which steps failed
101
+ if result.errors:
102
+ for error in result.errors:
103
+ self.logger.warning(f" - Step error: {error}")
104
+ else:
105
+ self.logger.error(f"❌ API heartbeat pipeline failed: {result.message}")
106
+ # Log detailed error information
107
+ if result.errors:
108
+ for error in result.errors:
109
+ self.logger.error(f" - Pipeline error: {error}")
110
+
111
+ return result
112
+
113
+ except Exception as e:
114
+ # Log detailed error information for debugging
115
+ import traceback
116
+
117
+ self.logger.error(
118
+ f"❌ API heartbeat pipeline failed with exception: {e}\n"
119
+ f"Context keys: {list(self.context.keys())}\n"
120
+ f"Traceback: {traceback.format_exc()}"
121
+ )
122
+
123
+ # Create failure result with detailed context
124
+ from ..shared.pipeline_types import PipelineResult
125
+
126
+ failure_result = PipelineResult(
127
+ status=PipelineStatus.FAILED,
128
+ message=f"API heartbeat pipeline exception: {str(e)[:200]}...",
129
+ context=self.context,
130
+ )
131
+ failure_result.add_error(str(e))
132
+
133
+ return failure_result
134
+
135
+ async def _execute_with_conditional_logic(self) -> "PipelineResult":
136
+ """
137
+ Execute API pipeline with conditional logic based on fast heartbeat status.
138
+
139
+ Always executes:
140
+ - APIRegistryConnectionStep
141
+ - APIHealthCheckStep
142
+ - APIFastHeartbeatStep
143
+
144
+ Conditionally executes based on fast heartbeat status:
145
+ - NO_CHANGES: Skip remaining steps (optimization)
146
+ - TOPOLOGY_CHANGED, AGENT_UNKNOWN: Execute all remaining steps
147
+ - REGISTRY_ERROR, NETWORK_ERROR: Skip remaining steps (resilience)
148
+
149
+ Returns:
150
+ PipelineResult with execution status and context
151
+ """
152
+ from ..shared.pipeline_types import PipelineResult
153
+
154
+ overall_result = PipelineResult(
155
+ message="API heartbeat pipeline execution completed"
156
+ )
157
+
158
+ # Track which steps were executed for logging
159
+ executed_steps = []
160
+ skipped_steps = []
161
+
162
+ try:
163
+ # Always execute registry connection, health check, and fast heartbeat steps
164
+ mandatory_steps = self.steps[
165
+ :3
166
+ ] # APIRegistryConnectionStep, APIHealthCheckStep, APIFastHeartbeatStep
167
+ conditional_steps = self.steps[
168
+ 3:
169
+ ] # APIHeartbeatSendStep, APIDependencyResolutionStep
170
+
171
+ # Execute mandatory steps
172
+ for step in mandatory_steps:
173
+ self.logger.debug(f"Executing mandatory step: {step.name}")
174
+
175
+ step_result = await step.execute(self.context)
176
+ executed_steps.append(step.name)
177
+
178
+ # Merge step context into pipeline context
179
+ self.context.update(step_result.context)
180
+
181
+ # If step fails, handle accordingly
182
+ if not step_result.is_success():
183
+ overall_result.status = PipelineStatus.FAILED
184
+ overall_result.message = (
185
+ f"Mandatory step '{step.name}' failed: {step_result.message}"
186
+ )
187
+ overall_result.add_error(
188
+ f"Step '{step.name}': {step_result.message}"
189
+ )
190
+
191
+ if step.required:
192
+ # Stop execution if required step fails
193
+ for key, value in self.context.items():
194
+ overall_result.add_context(key, value)
195
+ return overall_result
196
+
197
+ # Check fast heartbeat status for conditional execution
198
+ fast_heartbeat_status = self.context.get("fast_heartbeat_status")
199
+
200
+ if fast_heartbeat_status is None:
201
+ # Fast heartbeat step failed to set status - fallback to full execution
202
+ self.logger.warning(
203
+ "⚠️ API fast heartbeat status not found - falling back to full execution"
204
+ )
205
+ should_execute_remaining = True
206
+ reason = "fallback (missing status)"
207
+ elif FastHeartbeatStatusUtil.should_skip_for_optimization(
208
+ fast_heartbeat_status
209
+ ):
210
+ # NO_CHANGES - skip for optimization
211
+ should_execute_remaining = False
212
+ reason = "optimization (no changes detected)"
213
+ self.logger.info(
214
+ f"🚀 API heartbeat: Skipping remaining steps for optimization: {reason}"
215
+ )
216
+ elif FastHeartbeatStatusUtil.should_skip_for_resilience(
217
+ fast_heartbeat_status
218
+ ):
219
+ # REGISTRY_ERROR, NETWORK_ERROR - skip for resilience
220
+ should_execute_remaining = False
221
+ reason = "resilience (preserve existing state)"
222
+ self.logger.warning(
223
+ f"⚠️ API heartbeat: Skipping remaining steps for resilience: {reason}"
224
+ )
225
+ elif FastHeartbeatStatusUtil.requires_full_heartbeat(fast_heartbeat_status):
226
+ # TOPOLOGY_CHANGED, AGENT_UNKNOWN - execute full pipeline
227
+ should_execute_remaining = True
228
+ reason = "changes detected or re-registration needed"
229
+ self.logger.info(f"🔄 API heartbeat: Executing remaining steps: {reason}")
230
+ else:
231
+ # Unknown status - fallback to full execution
232
+ self.logger.warning(
233
+ f"⚠️ Unknown API fast heartbeat status '{fast_heartbeat_status}' - falling back to full execution"
234
+ )
235
+ should_execute_remaining = True
236
+ reason = "fallback (unknown status)"
237
+
238
+ # Execute or skip conditional steps based on decision
239
+ if should_execute_remaining:
240
+ for step in conditional_steps:
241
+ self.logger.debug(f"Executing conditional step: {step.name}")
242
+
243
+ step_result = await step.execute(self.context)
244
+ executed_steps.append(step.name)
245
+
246
+ # Merge step context into pipeline context
247
+ self.context.update(step_result.context)
248
+
249
+ # Handle step failure
250
+ if not step_result.is_success():
251
+ if step.required:
252
+ overall_result.status = PipelineStatus.FAILED
253
+ overall_result.message = f"Required step '{step.name}' failed: {step_result.message}"
254
+ overall_result.add_error(
255
+ f"Step '{step.name}': {step_result.message}"
256
+ )
257
+ break
258
+ else:
259
+ # Optional step failed - mark as partial success
260
+ if overall_result.status == PipelineStatus.SUCCESS:
261
+ overall_result.status = PipelineStatus.PARTIAL
262
+ overall_result.add_error(
263
+ f"Optional step '{step.name}': {step_result.message}"
264
+ )
265
+ self.logger.warning(
266
+ f"⚠️ Optional step '{step.name}' failed: {step_result.message}"
267
+ )
268
+ else:
269
+ # Mark skipped steps
270
+ for step in conditional_steps:
271
+ skipped_steps.append(step.name)
272
+
273
+ # For skipped heartbeat due to NO_CHANGES, set success context
274
+ if fast_heartbeat_status == FastHeartbeatStatus.NO_CHANGES:
275
+ overall_result.add_context("heartbeat_success", True)
276
+ overall_result.add_context("heartbeat_skipped", True)
277
+ overall_result.add_context("skip_reason", "no_changes_optimization")
278
+
279
+ # Set final result message
280
+ if executed_steps and skipped_steps:
281
+ overall_result.message = (
282
+ f"API pipeline completed with conditional execution - "
283
+ f"executed: {executed_steps}, skipped: {skipped_steps} ({reason})"
284
+ )
285
+ elif executed_steps:
286
+ overall_result.message = (
287
+ f"API pipeline completed - executed: {executed_steps} ({reason})"
288
+ )
289
+ else:
290
+ overall_result.message = (
291
+ f"API pipeline completed - all steps skipped ({reason})"
292
+ )
293
+
294
+ # Add final context
295
+ for key, value in self.context.items():
296
+ overall_result.add_context(key, value)
297
+
298
+ return overall_result
299
+
300
+ except Exception as e:
301
+ # Handle unexpected exceptions
302
+ overall_result.status = PipelineStatus.FAILED
303
+ overall_result.message = f"API pipeline execution failed with exception: {e}"
304
+ overall_result.add_error(str(e))
305
+ for key, value in self.context.items():
306
+ overall_result.add_context(key, value)
307
+
308
+ self.logger.error(f"❌ API conditional pipeline execution failed: {e}")
309
+ return overall_result
@@ -0,0 +1,332 @@
1
+ """
2
+ API heartbeat send step for API heartbeat pipeline.
3
+
4
+ Sends service health status and registration data to the registry
5
+ for FastAPI applications using @mesh.route decorators.
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 APIHeartbeatSendStep(PipelineStep):
18
+ """
19
+ Send API service heartbeat to registry.
20
+
21
+ Communicates service health status and registration information
22
+ to the registry for monitoring and discovery purposes.
23
+ """
24
+
25
+ def __init__(self, required: bool = True):
26
+ super().__init__(
27
+ name="api-heartbeat-send",
28
+ required=required,
29
+ )
30
+
31
+ async def execute(self, context: dict[str, Any]) -> PipelineResult:
32
+ """
33
+ Send API service heartbeat to registry.
34
+
35
+ Args:
36
+ context: Pipeline context containing registry_wrapper, health_status, service_id
37
+
38
+ Returns:
39
+ PipelineResult with heartbeat_response in context
40
+ """
41
+ self.logger.debug("Sending API service heartbeat to registry")
42
+
43
+ try:
44
+ # Get required components from context
45
+ registry_wrapper = context.get("registry_wrapper")
46
+ health_status = context.get("health_status")
47
+ service_id = context.get("service_id") or context.get("agent_id", "unknown")
48
+
49
+ if not registry_wrapper:
50
+ error_msg = "No registry wrapper available for heartbeat"
51
+ self.logger.error(f"❌ {error_msg}")
52
+
53
+ from ..shared.pipeline_types import PipelineStatus
54
+ result = PipelineResult(
55
+ status=PipelineStatus.FAILED,
56
+ message=error_msg,
57
+ context=context
58
+ )
59
+ result.add_error(error_msg)
60
+ return result
61
+
62
+ if not health_status:
63
+ error_msg = "No health status available for heartbeat"
64
+ self.logger.error(f"❌ {error_msg}")
65
+
66
+ from ..shared.pipeline_types import PipelineStatus
67
+ result = PipelineResult(
68
+ status=PipelineStatus.FAILED,
69
+ message=error_msg,
70
+ context=context
71
+ )
72
+ result.add_error(error_msg)
73
+ return result
74
+
75
+ # Prepare heartbeat data for API service
76
+ heartbeat_data = self._prepare_api_heartbeat_data(
77
+ health_status, service_id, context
78
+ )
79
+
80
+ self.logger.debug(f"📡 Sending heartbeat for API service '{service_id}'")
81
+
82
+ # Send heartbeat to registry using the same format as test_api_service.json
83
+ # Import json at the beginning
84
+ import aiohttp
85
+ import json
86
+
87
+ try:
88
+ # For API services, send directly to registry using the format that works
89
+ # Get registry URL
90
+ registry_url = context.get("registry_url", "http://localhost:8000")
91
+
92
+ # Build the API service payload using actual dependencies from @mesh.route decorators
93
+ display_config = context.get("display_config", {})
94
+
95
+ # Extract all dependencies from registered @mesh.route decorators
96
+ all_route_dependencies = self._extract_all_route_dependencies(context)
97
+
98
+ api_service_payload = {
99
+ "agent_id": service_id,
100
+ "agent_type": "api",
101
+ "tools": [
102
+ {
103
+ "function_name": "api_endpoint_handler",
104
+ "dependencies": all_route_dependencies
105
+ }
106
+ ],
107
+ "http_host": display_config.get("display_host", "127.0.0.1"),
108
+ "http_port": display_config.get("display_port", 8080)
109
+ }
110
+
111
+ self.logger.debug(f"📡 Sending API service payload to {registry_url}/heartbeat")
112
+ self.logger.debug(f"🔍 POST payload: {json.dumps(api_service_payload, indent=2)}")
113
+
114
+ try:
115
+ async with aiohttp.ClientSession() as session:
116
+ async with session.post(
117
+ f"{registry_url}/heartbeat",
118
+ headers={"Content-Type": "application/json"},
119
+ data=json.dumps(api_service_payload)
120
+ ) as response:
121
+ self.logger.debug(f"{registry_url} \"POST /heartbeat HTTP/1.1\" {response.status}")
122
+ if response.status == 200:
123
+ heartbeat_response = await response.json()
124
+ else:
125
+ response_text = await response.text()
126
+ self.logger.error(f"❌ Registry error {response.status}: {response_text}")
127
+ raise Exception(f"Registry returned {response.status}: {response_text}")
128
+
129
+ except Exception as http_error:
130
+ self.logger.error(f"❌ HTTP request failed: {http_error}")
131
+ raise http_error
132
+
133
+ if heartbeat_response:
134
+ self.logger.info(f"💚 API heartbeat successful for service '{service_id}'")
135
+
136
+ return PipelineResult(
137
+ message=f"API heartbeat sent for service {service_id}",
138
+ context={
139
+ "heartbeat_response": heartbeat_response,
140
+ "heartbeat_success": True,
141
+ "heartbeat_data": heartbeat_data,
142
+ }
143
+ )
144
+ else:
145
+ error_msg = f"Registry heartbeat failed for service {service_id}"
146
+ self.logger.warning(f"⚠️ {error_msg}")
147
+
148
+ from ..shared.pipeline_types import PipelineStatus
149
+ result = PipelineResult(
150
+ status=PipelineStatus.FAILED,
151
+ message=error_msg,
152
+ context=context
153
+ )
154
+ result.add_error(error_msg)
155
+ return result
156
+
157
+ except Exception as e:
158
+ error_msg = f"Registry communication failed: {e}"
159
+ self.logger.error(f"❌ {error_msg}")
160
+
161
+ from ..shared.pipeline_types import PipelineStatus
162
+ result = PipelineResult(
163
+ status=PipelineStatus.FAILED,
164
+ message=error_msg,
165
+ context=context
166
+ )
167
+ result.add_error(str(e))
168
+ return result
169
+
170
+ except Exception as e:
171
+ error_msg = f"API heartbeat send failed: {e}"
172
+ self.logger.error(f"❌ {error_msg}")
173
+
174
+ from ..shared.pipeline_types import PipelineStatus
175
+ result = PipelineResult(
176
+ status=PipelineStatus.FAILED,
177
+ message=error_msg,
178
+ context=context
179
+ )
180
+ result.add_error(str(e))
181
+ return result
182
+
183
+ def _prepare_api_heartbeat_data(
184
+ self, health_status: Any, service_id: str, context: dict[str, Any]
185
+ ) -> dict[str, Any]:
186
+ """Prepare heartbeat data specific to API services."""
187
+ try:
188
+ # Extract FastAPI-specific information
189
+ app_title = context.get("app_title", "Unknown API")
190
+ app_version = context.get("app_version", "1.0.0")
191
+ routes_total = context.get("routes_total", 0)
192
+ routes_with_mesh = context.get("routes_with_mesh", 0)
193
+
194
+ # Get display configuration
195
+ display_config = context.get("display_config", {})
196
+ host = display_config.get("host", "0.0.0.0")
197
+ port = display_config.get("port", 8080)
198
+
199
+ heartbeat_data = {
200
+ "service_id": service_id,
201
+ "service_type": "api",
202
+ "app_title": app_title,
203
+ "app_version": app_version,
204
+ "host": host,
205
+ "port": port,
206
+ "routes": {
207
+ "total": routes_total,
208
+ "with_mesh": routes_with_mesh,
209
+ },
210
+ "health_status": health_status if isinstance(health_status, dict) else {
211
+ "status": health_status.status.value if hasattr(health_status, "status") and hasattr(health_status.status, "value") else str(getattr(health_status, "status", "healthy")),
212
+ "timestamp": health_status.timestamp.isoformat() if hasattr(health_status, "timestamp") and hasattr(health_status.timestamp, "isoformat") else str(getattr(health_status, "timestamp", "")),
213
+ "version": getattr(health_status, "version", "1.0.0"),
214
+ "metadata": getattr(health_status, "metadata", {}),
215
+ }
216
+ }
217
+
218
+ return heartbeat_data
219
+
220
+ except Exception as e:
221
+ self.logger.warning(f"⚠️ Could not prepare heartbeat data: {e}")
222
+ return {
223
+ "service_id": service_id,
224
+ "service_type": "api",
225
+ "error": f"Failed to prepare heartbeat data: {e}"
226
+ }
227
+
228
+ def _extract_all_route_dependencies(self, context: dict[str, Any]) -> list[dict[str, Any]]:
229
+ """
230
+ Extract all unique dependencies from @mesh.route decorators in the FastAPI app.
231
+
232
+ This method looks at the actual route dependencies that were discovered during
233
+ the API startup pipeline and extracts them for registry registration.
234
+
235
+ Args:
236
+ context: Pipeline context containing FastAPI app and route information
237
+
238
+ Returns:
239
+ List of unique dependency objects in the format expected by registry
240
+ """
241
+ try:
242
+ # Try to get dependencies from startup context (preferred method)
243
+ api_service_metadata = context.get("api_service_metadata", {})
244
+ route_capabilities = api_service_metadata.get("capabilities", [])
245
+
246
+ self.logger.debug(f"🔍 api_service_metadata keys: {list(api_service_metadata.keys())}")
247
+ self.logger.debug(f"🔍 route_capabilities count: {len(route_capabilities)}")
248
+ self.logger.debug(f"🔍 route_capabilities: {route_capabilities}")
249
+
250
+ # Extract dependencies from route capabilities
251
+ all_dependencies = []
252
+ seen_capabilities = set()
253
+
254
+ for route_capability in route_capabilities:
255
+ route_deps = route_capability.get("dependencies", [])
256
+ for dep in route_deps:
257
+ # dep should already be a string (capability name)
258
+ if isinstance(dep, str) and dep not in seen_capabilities:
259
+ seen_capabilities.add(dep)
260
+ # Convert to object format for registry
261
+ all_dependencies.append({
262
+ "capability": dep,
263
+ "tags": [] # No tags info available at this level
264
+ })
265
+
266
+ # If we found dependencies from startup context, use them
267
+ if all_dependencies:
268
+ self.logger.info(
269
+ f"🔍 Extracted {len(all_dependencies)} unique dependencies from API startup: "
270
+ f"{[dep['capability'] for dep in all_dependencies]}"
271
+ )
272
+ return all_dependencies
273
+
274
+ # Fallback: try to extract directly from FastAPI app routes
275
+ fastapi_app = context.get("fastapi_app")
276
+ if fastapi_app:
277
+ return self._extract_dependencies_from_routes(fastapi_app)
278
+
279
+ # Final fallback: empty dependencies
280
+ self.logger.warning("⚠️ No route dependencies found in context or FastAPI app")
281
+ return []
282
+
283
+ except Exception as e:
284
+ self.logger.error(f"❌ Failed to extract route dependencies: {e}")
285
+ return []
286
+
287
+ def _extract_dependencies_from_routes(self, fastapi_app: Any) -> list[dict[str, Any]]:
288
+ """
289
+ Fallback method to extract dependencies directly from FastAPI route metadata.
290
+
291
+ Args:
292
+ fastapi_app: FastAPI application instance
293
+
294
+ Returns:
295
+ List of unique dependency objects
296
+ """
297
+ try:
298
+ all_dependencies = []
299
+ seen_capabilities = set()
300
+
301
+ routes = getattr(fastapi_app, "routes", [])
302
+ for route in routes:
303
+ endpoint = getattr(route, "endpoint", None)
304
+ if endpoint and hasattr(endpoint, "_mesh_route_metadata"):
305
+ metadata = endpoint._mesh_route_metadata
306
+ route_deps = metadata.get("dependencies", [])
307
+
308
+ for dep in route_deps:
309
+ if isinstance(dep, dict):
310
+ capability = dep.get("capability")
311
+ if capability and capability not in seen_capabilities:
312
+ seen_capabilities.add(capability)
313
+ all_dependencies.append({
314
+ "capability": capability,
315
+ "tags": dep.get("tags", [])
316
+ })
317
+ elif isinstance(dep, str) and dep not in seen_capabilities:
318
+ seen_capabilities.add(dep)
319
+ all_dependencies.append({
320
+ "capability": dep,
321
+ "tags": []
322
+ })
323
+
324
+ self.logger.info(
325
+ f"🔍 Extracted {len(all_dependencies)} unique dependencies from FastAPI routes: "
326
+ f"{[dep['capability'] for dep in all_dependencies]}"
327
+ )
328
+ return all_dependencies
329
+
330
+ except Exception as e:
331
+ self.logger.error(f"❌ Failed to extract dependencies from routes: {e}")
332
+ return []