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,515 @@
1
+ """
2
+ API dependency resolution step for API heartbeat pipeline.
3
+
4
+ Handles processing dependency resolution from registry response and
5
+ updating the dependency injection system for FastAPI route handlers.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ from typing import Any
11
+
12
+ from ...engine.dependency_injector import get_global_injector
13
+ from ..shared import PipelineResult, PipelineStatus, PipelineStep
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Global state for dependency hash tracking across heartbeat cycles
18
+ _last_api_dependency_hash = None
19
+
20
+
21
+ class APIDependencyResolutionStep(PipelineStep):
22
+ """
23
+ Processes dependency resolution from registry response for API services.
24
+
25
+ Takes the dependencies_resolved data from the heartbeat response
26
+ and updates dependency injection for FastAPI route handlers.
27
+
28
+ Similar to MCP dependency resolution but adapted for:
29
+ - FastAPI route handlers instead of MCP tools
30
+ - Single "api_endpoint_handler" function instead of multiple tools
31
+ - Route-level dependency mapping instead of tool-level mapping
32
+ """
33
+
34
+ def __init__(self):
35
+ super().__init__(
36
+ name="api-dependency-resolution",
37
+ required=False, # Optional - can work without dependencies
38
+ description="Process dependency resolution for API route handlers",
39
+ )
40
+
41
+ async def execute(self, context: dict[str, Any]) -> PipelineResult:
42
+ """Process dependency resolution with hash-based change detection."""
43
+ self.logger.debug("Processing API dependency resolution...")
44
+
45
+ result = PipelineResult(message="API dependency resolution processed")
46
+
47
+ try:
48
+ # Get heartbeat response and registry wrapper
49
+ heartbeat_response = context.get("heartbeat_response", {})
50
+ registry_wrapper = context.get("registry_wrapper")
51
+
52
+ if not heartbeat_response or not registry_wrapper:
53
+ result.status = PipelineStatus.SUCCESS
54
+ result.message = (
55
+ "No heartbeat response or registry wrapper - completed successfully"
56
+ )
57
+ self.logger.info("ℹ️ No heartbeat response to process - this is normal for API services")
58
+ return result
59
+
60
+ # Use the same hash-based change detection pattern as MCP
61
+ await self.process_heartbeat_response_for_api_rewiring(heartbeat_response)
62
+
63
+ # For context consistency, extract dependency count
64
+ dependencies_resolved = registry_wrapper.parse_tool_dependencies(
65
+ heartbeat_response
66
+ )
67
+ dependency_count = sum(
68
+ len(deps) if isinstance(deps, list) else 0
69
+ for deps in dependencies_resolved.values()
70
+ )
71
+
72
+ # Store processed dependencies info for context
73
+ result.add_context("dependency_count", dependency_count)
74
+ result.add_context("dependencies_resolved", dependencies_resolved)
75
+
76
+ result.message = "API dependency resolution completed (efficient hash-based)"
77
+
78
+ if dependency_count > 0:
79
+ self.logger.info(f"🔗 Dependencies resolved: {dependency_count} items")
80
+
81
+ # Log function registry status for debugging
82
+ injector = get_global_injector()
83
+ function_count = len(injector._function_registry)
84
+ self.logger.debug(f"🔍 Function registry contains {function_count} functions:")
85
+ for func_id, wrapper_func in injector._function_registry.items():
86
+ original_func = getattr(wrapper_func, '_mesh_original_func', None)
87
+ func_name = original_func.__name__ if original_func else 'unknown'
88
+ dependencies = getattr(wrapper_func, '_mesh_dependencies', [])
89
+ self.logger.debug(f" 📋 {func_id} -> {func_name} (deps: {dependencies})")
90
+
91
+ self.logger.debug("🔗 API dependency resolution step completed using hash-based change detection")
92
+
93
+ except Exception as e:
94
+ result.status = PipelineStatus.FAILED
95
+ result.message = f"API dependency resolution failed: {e}"
96
+ result.add_error(str(e))
97
+ self.logger.error(f"❌ API dependency resolution failed: {e}")
98
+
99
+ return result
100
+
101
+ def _extract_dependency_state(
102
+ self, heartbeat_response: dict[str, Any]
103
+ ) -> dict[str, dict[str, dict[str, str]]]:
104
+ """Extract dependency state structure from heartbeat response.
105
+
106
+ For API services, dependencies are typically under a single function
107
+ (usually "api_endpoint_handler") but we still follow the same pattern.
108
+
109
+ Returns:
110
+ {function_name: {capability: {endpoint, function_name, status}}}
111
+ """
112
+ state = {}
113
+ dependencies_resolved = heartbeat_response.get("dependencies_resolved", {})
114
+
115
+ for function_name, dependency_list in dependencies_resolved.items():
116
+ if not isinstance(dependency_list, list):
117
+ continue
118
+
119
+ state[function_name] = {}
120
+ for dep_resolution in dependency_list:
121
+ if (
122
+ not isinstance(dep_resolution, dict)
123
+ or "capability" not in dep_resolution
124
+ ):
125
+ continue
126
+
127
+ capability = dep_resolution["capability"]
128
+ state[function_name][capability] = {
129
+ "endpoint": dep_resolution.get("endpoint", ""),
130
+ "function_name": dep_resolution.get("function_name", ""),
131
+ "status": dep_resolution.get("status", ""),
132
+ "agent_id": dep_resolution.get("agent_id", ""),
133
+ "kwargs": dep_resolution.get("kwargs", {}), # Include kwargs config
134
+ }
135
+
136
+ return state
137
+
138
+ def _hash_dependency_state(self, state: dict) -> str:
139
+ """Create hash of dependency state structure."""
140
+ import hashlib
141
+
142
+ # Convert to sorted JSON string for consistent hashing
143
+ state_json = json.dumps(state, sort_keys=True)
144
+ return hashlib.sha256(state_json.encode()).hexdigest()[
145
+ :16
146
+ ] # First 16 chars for readability
147
+
148
+ async def process_heartbeat_response_for_api_rewiring(
149
+ self, heartbeat_response: dict[str, Any]
150
+ ) -> None:
151
+ """Process heartbeat response to update API route dependency injection.
152
+
153
+ Uses hash-based comparison to efficiently detect when ANY dependency changes
154
+ and then updates ALL affected route handlers in one operation.
155
+
156
+ Resilience logic (same as MCP):
157
+ - No response (connection error, 5xx) → Skip entirely (keep existing wiring)
158
+ - 2xx response with empty dependencies → Unwire all dependencies
159
+ - 2xx response with partial dependencies → Update to match registry exactly
160
+ """
161
+ try:
162
+ if not heartbeat_response:
163
+ # No response from registry (connection error, timeout, 5xx)
164
+ # → Skip entirely for resilience (keep existing dependencies)
165
+ self.logger.debug(
166
+ "No heartbeat response - skipping API rewiring for resilience"
167
+ )
168
+ return
169
+
170
+ # Extract current dependency state structure
171
+ current_state = self._extract_dependency_state(heartbeat_response)
172
+
173
+ # IMPORTANT: Empty state from successful response means "unwire everything"
174
+ # This is different from "no response" which means "keep existing for resilience"
175
+
176
+ # Hash the current state (including empty state)
177
+ current_hash = self._hash_dependency_state(current_state)
178
+
179
+ # Compare with previous state (use global variable with API-specific name)
180
+ global _last_api_dependency_hash
181
+ if current_hash == _last_api_dependency_hash:
182
+ self.logger.debug(
183
+ f"🔄 API dependency state unchanged (hash: {current_hash}), skipping rewiring"
184
+ )
185
+ return
186
+
187
+ # State changed - determine what changed
188
+ function_count = len(current_state)
189
+ total_deps = sum(len(deps) for deps in current_state.values())
190
+
191
+ if _last_api_dependency_hash is None:
192
+ if function_count > 0:
193
+ self.logger.info(
194
+ f"🔄 Initial API dependency state detected: {function_count} functions, {total_deps} dependencies"
195
+ )
196
+ else:
197
+ self.logger.info(
198
+ "🔄 Initial API dependency state detected: no dependencies"
199
+ )
200
+ else:
201
+ self.logger.info(
202
+ f"🔄 API dependency state changed (hash: {_last_api_dependency_hash} → {current_hash})"
203
+ )
204
+ if function_count > 0:
205
+ self.logger.info(
206
+ f"🔄 Updating API dependencies for {function_count} functions ({total_deps} total dependencies)"
207
+ )
208
+ else:
209
+ self.logger.info(
210
+ "🔄 Registry reports no API dependencies - unwiring all existing dependencies"
211
+ )
212
+
213
+ # Import here to avoid circular imports
214
+ from ...engine.dependency_injector import get_global_injector
215
+ from ...engine.full_mcp_proxy import EnhancedFullMCPProxy, FullMCPProxy
216
+ from ...engine.mcp_client_proxy import (
217
+ EnhancedMCPClientProxy,
218
+ MCPClientProxy,
219
+ )
220
+
221
+ injector = get_global_injector()
222
+
223
+ # Step 1: Collect all capabilities that should exist according to registry
224
+ target_capabilities = set()
225
+ for function_name, dependencies in current_state.items():
226
+ for capability in dependencies.keys():
227
+ target_capabilities.add(capability)
228
+
229
+ # Step 2: Find existing capabilities that need to be removed (unwired)
230
+ # This handles the case where registry stops reporting some dependencies
231
+ existing_capabilities = (
232
+ set(injector._dependencies.keys())
233
+ if hasattr(injector, "_dependencies")
234
+ else set()
235
+ )
236
+ capabilities_to_remove = existing_capabilities - target_capabilities
237
+
238
+ unwired_count = 0
239
+ for capability in capabilities_to_remove:
240
+ await injector.unregister_dependency(capability)
241
+ unwired_count += 1
242
+ self.logger.info(
243
+ f"🗑️ Unwired API dependency '{capability}' (no longer reported by registry)"
244
+ )
245
+
246
+ # Step 3: Apply all dependency updates for capabilities that should exist
247
+ updated_count = 0
248
+ for function_name, dependencies in current_state.items():
249
+ for capability, dep_info in dependencies.items():
250
+ status = dep_info["status"]
251
+ endpoint = dep_info["endpoint"]
252
+ dep_function_name = dep_info["function_name"]
253
+ kwargs_config = dep_info.get("kwargs", {}) # Extract kwargs config
254
+
255
+ if status == "available" and endpoint and dep_function_name:
256
+ # Import here to avoid circular imports
257
+ import os
258
+
259
+ from ...engine.full_mcp_proxy import (
260
+ EnhancedFullMCPProxy,
261
+ FullMCPProxy,
262
+ )
263
+ from ...engine.mcp_client_proxy import (
264
+ EnhancedMCPClientProxy,
265
+ MCPClientProxy,
266
+ )
267
+ from ...engine.self_dependency_proxy import SelfDependencyProxy
268
+
269
+ # Get current agent ID for self-dependency detection
270
+ current_agent_id = None
271
+ try:
272
+ from ...engine.decorator_registry import DecoratorRegistry
273
+
274
+ config = DecoratorRegistry.get_resolved_agent_config()
275
+ current_agent_id = config["agent_id"]
276
+ self.logger.debug(
277
+ f"🔍 Current API service ID from DecoratorRegistry: '{current_agent_id}'"
278
+ )
279
+ except Exception as e:
280
+ # For API services, try environment variable fallback
281
+ current_agent_id = os.getenv("MCP_MESH_AGENT_ID")
282
+ self.logger.debug(
283
+ f"🔍 Current API service ID from environment: '{current_agent_id}' (fallback due to: {e})"
284
+ )
285
+
286
+ target_agent_id = dep_info.get("agent_id")
287
+ self.logger.debug(
288
+ f"🔍 Target agent ID from registry: '{target_agent_id}'"
289
+ )
290
+
291
+ # Determine if this is a self-dependency (less common for API services)
292
+ is_self_dependency = (
293
+ current_agent_id
294
+ and target_agent_id
295
+ and current_agent_id == target_agent_id
296
+ )
297
+
298
+ self.logger.debug(
299
+ f"🔍 Self-dependency check for '{capability}': "
300
+ f"current='{current_agent_id}' vs target='{target_agent_id}' "
301
+ f"→ {'SELF' if is_self_dependency else 'CROSS'}-dependency"
302
+ )
303
+
304
+ if is_self_dependency:
305
+ # Note: Self-dependencies are unusual for API services but we handle them
306
+ self.logger.warning(
307
+ f"⚠️ API SELF-DEPENDENCY detected for '{capability}' - "
308
+ f"this is unusual for API services. Consider refactoring."
309
+ )
310
+ # For API services, we don't have access to original functions in the same way
311
+ # Fall back to HTTP proxy approach
312
+ proxy_type = self._determine_api_proxy_type_for_capability(
313
+ capability, injector
314
+ )
315
+ new_proxy = self._create_proxy_for_api(
316
+ proxy_type, endpoint, dep_function_name, kwargs_config
317
+ )
318
+ else:
319
+ # Create cross-service proxy (the normal case for API services)
320
+ proxy_type = self._determine_api_proxy_type_for_capability(
321
+ capability, injector
322
+ )
323
+ new_proxy = self._create_proxy_for_api(
324
+ proxy_type, endpoint, dep_function_name, kwargs_config
325
+ )
326
+
327
+ # Update in injector (this will update ALL route handlers that depend on this capability)
328
+ self.logger.debug(f"🔄 Before update: registering {capability} = {type(new_proxy).__name__}")
329
+ await injector.register_dependency(capability, new_proxy)
330
+ updated_count += 1
331
+
332
+ # Log which functions will be affected
333
+ affected_functions = injector._dependency_mapping.get(capability, set())
334
+ self.logger.debug(f"🎯 Functions affected by '{capability}' update: {list(affected_functions)}")
335
+
336
+ self.logger.info(
337
+ f"🔄 Updated API dependency '{capability}' → {endpoint}/{dep_function_name} "
338
+ f"(proxy: {proxy_type})"
339
+ )
340
+ else:
341
+ if status != "available":
342
+ self.logger.debug(
343
+ f"⚠️ API dependency '{capability}' not available: {status}"
344
+ )
345
+ else:
346
+ self.logger.warning(
347
+ f"⚠️ Cannot update API dependency '{capability}': missing endpoint or function_name"
348
+ )
349
+
350
+ # Store new hash for next comparison (use global variable)
351
+ _last_api_dependency_hash = current_hash
352
+
353
+ if unwired_count > 0 and updated_count > 0:
354
+ self.logger.info(
355
+ f"✅ Successfully unwired {unwired_count} and updated {updated_count} API dependencies (state hash: {current_hash})"
356
+ )
357
+ elif unwired_count > 0:
358
+ self.logger.info(
359
+ f"✅ Successfully unwired {unwired_count} API dependencies (state hash: {current_hash})"
360
+ )
361
+ elif updated_count > 0:
362
+ self.logger.info(
363
+ f"✅ Successfully updated {updated_count} API dependencies (state hash: {current_hash})"
364
+ )
365
+ else:
366
+ self.logger.info(
367
+ f"✅ API dependency state synchronized (state hash: {current_hash})"
368
+ )
369
+
370
+ except Exception as e:
371
+ self.logger.error(
372
+ f"❌ Failed to process API heartbeat response for rewiring: {e}"
373
+ )
374
+ # Don't raise - this should not break the heartbeat loop
375
+
376
+ def _determine_api_proxy_type_for_capability(self, capability: str, injector) -> str:
377
+ """
378
+ Determine which proxy type to use for API route handlers.
379
+
380
+ For API services, we need to check the parameter types used in FastAPI route handlers
381
+ that depend on this capability. This is different from MCP tools because route handlers
382
+ are wrapped differently.
383
+
384
+ Logic:
385
+ 1. Check if any API route handlers use McpAgent for this capability
386
+ 2. If yes → use FullMCPProxy
387
+ 3. Otherwise → use MCPClientProxy (for McpMeshAgent or untyped)
388
+
389
+ Args:
390
+ capability: The capability name to check
391
+ injector: The dependency injector instance
392
+
393
+ Returns:
394
+ "FullMCPProxy" or "MCPClientProxy"
395
+ """
396
+ try:
397
+ # Get functions that depend on this capability
398
+ if capability not in injector._dependency_mapping:
399
+ self.logger.debug(
400
+ f"🔍 No API route handlers depend on capability '{capability}', using MCPClientProxy"
401
+ )
402
+ return "MCPClientProxy"
403
+
404
+ affected_function_ids = injector._dependency_mapping[capability]
405
+
406
+ # Scan ALL route handlers to detect ANY McpAgent usage
407
+ mcpagent_functions = []
408
+ mcpmeshagent_functions = []
409
+
410
+ for func_id in affected_function_ids:
411
+ if func_id in injector._function_registry:
412
+ wrapper_func = injector._function_registry[func_id]
413
+
414
+ # Get stored parameter types from wrapper (same pattern as MCP)
415
+ if hasattr(wrapper_func, "_mesh_parameter_types") and hasattr(
416
+ wrapper_func, "_mesh_dependencies"
417
+ ):
418
+ parameter_types = wrapper_func._mesh_parameter_types
419
+ dependencies = wrapper_func._mesh_dependencies
420
+ mesh_positions = wrapper_func._mesh_positions
421
+
422
+ # Find which parameter position corresponds to this capability
423
+ for dep_index, dep_name in enumerate(dependencies):
424
+ if dep_name == capability and dep_index < len(
425
+ mesh_positions
426
+ ):
427
+ param_position = mesh_positions[dep_index]
428
+
429
+ # Check the parameter type at this position
430
+ if param_position in parameter_types:
431
+ param_type = parameter_types[param_position]
432
+ if param_type == "McpAgent":
433
+ mcpagent_functions.append(func_id)
434
+ elif param_type == "McpMeshAgent":
435
+ mcpmeshagent_functions.append(func_id)
436
+
437
+ # Make deterministic decision based on complete analysis
438
+ if mcpagent_functions:
439
+ self.logger.debug(
440
+ f"🔍 Found McpAgent in API route handlers {mcpagent_functions} for capability '{capability}' → using FullMCPProxy"
441
+ )
442
+ if mcpmeshagent_functions:
443
+ self.logger.info(
444
+ f"ℹ️ API capability '{capability}' used by both McpAgent {mcpagent_functions} and McpMeshAgent {mcpmeshagent_functions} → upgrading ALL to FullMCPProxy"
445
+ )
446
+ return "FullMCPProxy"
447
+ else:
448
+ # Only McpMeshAgent or untyped parameters
449
+ self.logger.debug(
450
+ f"🔍 Only McpMeshAgent/untyped API route handlers {mcpmeshagent_functions} for capability '{capability}' → using MCPClientProxy"
451
+ )
452
+ return "MCPClientProxy"
453
+
454
+ except Exception as e:
455
+ self.logger.warning(
456
+ f"⚠️ Failed to determine proxy type for API capability '{capability}': {e}"
457
+ )
458
+ return "MCPClientProxy" # Safe default
459
+
460
+ def _create_proxy_for_api(
461
+ self, proxy_type: str, endpoint: str, dep_function_name: str, kwargs_config: dict
462
+ ):
463
+ """
464
+ Create the appropriate proxy instance for API route handlers.
465
+
466
+ Args:
467
+ proxy_type: "FullMCPProxy" or "MCPClientProxy"
468
+ endpoint: Target endpoint URL
469
+ dep_function_name: Target function name
470
+ kwargs_config: Additional configuration (timeout, retry, etc.)
471
+
472
+ Returns:
473
+ Proxy instance
474
+ """
475
+ from ...engine.full_mcp_proxy import EnhancedFullMCPProxy, FullMCPProxy
476
+ from ...engine.mcp_client_proxy import EnhancedMCPClientProxy, MCPClientProxy
477
+
478
+ if proxy_type == "FullMCPProxy":
479
+ # Use enhanced proxy if kwargs available
480
+ if kwargs_config:
481
+ proxy = EnhancedFullMCPProxy(
482
+ endpoint,
483
+ dep_function_name,
484
+ kwargs_config=kwargs_config,
485
+ )
486
+ self.logger.debug(
487
+ f"🔧 Created EnhancedFullMCPProxy for API with kwargs: {kwargs_config}"
488
+ )
489
+ else:
490
+ proxy = FullMCPProxy(
491
+ endpoint,
492
+ dep_function_name,
493
+ kwargs_config=kwargs_config,
494
+ )
495
+ self.logger.debug("🔧 Created FullMCPProxy for API (no kwargs)")
496
+ return proxy
497
+ else:
498
+ # Use enhanced proxy if kwargs available
499
+ if kwargs_config:
500
+ proxy = EnhancedMCPClientProxy(
501
+ endpoint,
502
+ dep_function_name,
503
+ kwargs_config=kwargs_config,
504
+ )
505
+ self.logger.debug(
506
+ f"🔧 Created EnhancedMCPClientProxy for API with kwargs: {kwargs_config}"
507
+ )
508
+ else:
509
+ proxy = MCPClientProxy(
510
+ endpoint,
511
+ dep_function_name,
512
+ kwargs_config=kwargs_config,
513
+ )
514
+ self.logger.debug("🔧 Created MCPClientProxy for API (no kwargs)")
515
+ return proxy
@@ -0,0 +1,117 @@
1
+ """
2
+ Fast Heartbeat Check Step for API heartbeat pipeline.
3
+
4
+ Performs lightweight HEAD requests to registry for fast topology change detection
5
+ before expensive full POST heartbeat operations for FastAPI services.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any
10
+
11
+ from ...shared.fast_heartbeat_status import FastHeartbeatStatus, FastHeartbeatStatusUtil
12
+ from ..shared.base_step import PipelineStep
13
+ from ..shared.pipeline_types import PipelineResult
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class APIFastHeartbeatStep(PipelineStep):
19
+ """
20
+ Fast heartbeat check step for API services optimization and resilience.
21
+
22
+ Performs lightweight HEAD request to registry to check for topology changes
23
+ before deciding whether to execute expensive full POST heartbeat for API services.
24
+
25
+ Stores semantic status in context for pipeline conditional execution.
26
+ """
27
+
28
+ def __init__(self):
29
+ super().__init__(
30
+ name="api-fast-heartbeat-check",
31
+ required=True,
32
+ description="Lightweight HEAD request for fast topology change detection (API services)",
33
+ )
34
+
35
+ async def execute(self, context: dict[str, Any]) -> PipelineResult:
36
+ """
37
+ Execute fast heartbeat check for API service and store semantic status.
38
+
39
+ Args:
40
+ context: Pipeline context containing service_id/agent_id and registry_wrapper
41
+
42
+ Returns:
43
+ PipelineResult with fast_heartbeat_status in context
44
+ """
45
+ self.logger.debug("Starting API fast heartbeat check...")
46
+
47
+ result = PipelineResult(message="API fast heartbeat check completed")
48
+
49
+ try:
50
+ # Validate required context - API services use service_id or agent_id
51
+ service_id = context.get("service_id") or context.get("agent_id")
52
+ registry_wrapper = context.get("registry_wrapper")
53
+
54
+ if not service_id:
55
+ raise ValueError("service_id or agent_id is required in context")
56
+
57
+ if not registry_wrapper:
58
+ raise ValueError("registry_wrapper is required in context")
59
+
60
+ self.logger.debug(
61
+ f"🚀 Performing API fast heartbeat check for service '{service_id}'"
62
+ )
63
+
64
+ # Perform fast heartbeat check using same method as MCP agents
65
+ status = await registry_wrapper.check_fast_heartbeat(service_id)
66
+
67
+ # Store semantic status in context
68
+ result.add_context("fast_heartbeat_status", status)
69
+
70
+ # Set appropriate message based on status
71
+ action_description = FastHeartbeatStatusUtil.get_action_description(status)
72
+ result.message = f"API fast heartbeat check: {action_description}"
73
+
74
+ # Log status and action with API-specific messaging
75
+ if status == FastHeartbeatStatus.NO_CHANGES:
76
+ self.logger.info(
77
+ f"✅ API fast heartbeat: No changes detected for service '{service_id}'"
78
+ )
79
+ elif status == FastHeartbeatStatus.TOPOLOGY_CHANGED:
80
+ self.logger.info(
81
+ f"🔄 API fast heartbeat: Topology changed for service '{service_id}' - full refresh needed"
82
+ )
83
+ elif status == FastHeartbeatStatus.AGENT_UNKNOWN:
84
+ self.logger.info(
85
+ f"❓ API fast heartbeat: Service '{service_id}' unknown - re-registration needed"
86
+ )
87
+ elif status == FastHeartbeatStatus.REGISTRY_ERROR:
88
+ self.logger.warning(
89
+ f"⚠️ API fast heartbeat: Registry error for service '{service_id}' - skipping for resilience"
90
+ )
91
+ elif status == FastHeartbeatStatus.NETWORK_ERROR:
92
+ self.logger.warning(
93
+ f"⚠️ API fast heartbeat: Network error for service '{service_id}' - skipping for resilience"
94
+ )
95
+
96
+ except Exception as e:
97
+ # Convert any exception to NETWORK_ERROR for resilient handling
98
+ status = FastHeartbeatStatusUtil.from_exception(e)
99
+ result.add_context("fast_heartbeat_status", status)
100
+
101
+ action_description = FastHeartbeatStatusUtil.get_action_description(status)
102
+ result.message = f"API fast heartbeat check: {action_description}"
103
+
104
+ self.logger.warning(
105
+ f"⚠️ API fast heartbeat check failed for service '{service_id}': {e}"
106
+ )
107
+ self.logger.debug(f"Exception details: {e}", exc_info=True)
108
+
109
+ # Step succeeds but sets error status for pipeline decision
110
+ # This ensures pipeline can handle errors gracefully
111
+
112
+ # Always preserve existing context
113
+ for key, value in context.items():
114
+ if key not in result.context:
115
+ result.add_context(key, value)
116
+
117
+ return result