mcp-mesh 0.7.21__py3-none-any.whl → 0.8.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 (124) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/dependency_injector.py +13 -15
  3. _mcp_mesh/engine/http_wrapper.py +69 -10
  4. _mcp_mesh/engine/mesh_llm_agent.py +29 -10
  5. _mcp_mesh/engine/mesh_llm_agent_injector.py +77 -41
  6. _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
  7. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
  8. _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
  9. _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
  10. _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
  11. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
  12. _mcp_mesh/engine/response_parser.py +61 -15
  13. _mcp_mesh/engine/signature_analyzer.py +58 -68
  14. _mcp_mesh/engine/unified_mcp_proxy.py +19 -35
  15. _mcp_mesh/pipeline/__init__.py +9 -20
  16. _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
  17. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
  18. _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +429 -0
  19. _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
  20. _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
  21. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
  22. _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
  23. _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
  24. _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
  25. _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +710 -0
  26. _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
  27. _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
  28. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +31 -8
  29. _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
  30. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +23 -11
  31. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
  32. _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
  33. _mcp_mesh/reload.py +1 -3
  34. _mcp_mesh/shared/__init__.py +2 -8
  35. _mcp_mesh/shared/config_resolver.py +124 -80
  36. _mcp_mesh/shared/defaults.py +89 -14
  37. _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
  38. _mcp_mesh/shared/host_resolver.py +8 -46
  39. _mcp_mesh/shared/server_discovery.py +115 -86
  40. _mcp_mesh/shared/simple_shutdown.py +44 -86
  41. _mcp_mesh/tracing/execution_tracer.py +2 -6
  42. _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
  43. _mcp_mesh/tracing/trace_context_helper.py +3 -13
  44. _mcp_mesh/tracing/utils.py +29 -15
  45. _mcp_mesh/utils/fastmcp_schema_extractor.py +5 -4
  46. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0.dist-info}/METADATA +7 -5
  47. mcp_mesh-0.8.0.dist-info/RECORD +85 -0
  48. mesh/__init__.py +12 -1
  49. mesh/decorators.py +248 -33
  50. mesh/helpers.py +52 -0
  51. mesh/types.py +40 -13
  52. _mcp_mesh/generated/.openapi-generator/FILES +0 -50
  53. _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
  54. _mcp_mesh/generated/.openapi-generator-ignore +0 -15
  55. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
  56. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
  57. _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
  58. _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
  59. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
  60. _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
  61. _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
  62. _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
  63. _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
  64. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
  65. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
  66. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
  67. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
  68. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
  69. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
  70. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
  71. _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
  72. _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
  73. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
  74. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
  75. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
  76. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
  77. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
  78. _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
  79. _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
  80. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
  81. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
  82. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
  83. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
  84. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
  85. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
  86. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
  87. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
  88. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
  89. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
  90. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
  91. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
  92. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
  93. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
  94. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
  95. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
  96. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
  97. _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
  98. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
  99. _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
  100. _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
  101. _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
  102. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
  103. _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
  104. _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
  105. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
  106. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
  107. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
  108. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
  109. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
  110. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
  111. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
  112. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
  113. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
  114. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
  115. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
  116. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
  117. _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
  118. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
  119. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
  120. _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
  121. _mcp_mesh/shared/registry_client_wrapper.py +0 -515
  122. mcp_mesh-0.7.21.dist-info/RECORD +0 -152
  123. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0.dist-info}/WHEEL +0 -0
  124. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,396 +0,0 @@
1
- """
2
- Dependency resolution step for MCP Mesh pipeline.
3
-
4
- Handles processing dependency resolution from registry response and
5
- updating the dependency injection system.
6
- """
7
-
8
- import json
9
- import logging
10
- from typing import Any
11
-
12
- from ..shared import PipelineResult, PipelineStatus, PipelineStep
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
- # Global state for dependency hash tracking across heartbeat cycles
17
- _last_dependency_hash = None
18
-
19
-
20
- class DependencyResolutionStep(PipelineStep):
21
- """
22
- Processes dependency resolution from registry response.
23
-
24
- Takes the dependencies_resolved data from the heartbeat response
25
- and prepares it for dependency injection (simplified for now).
26
- """
27
-
28
- def __init__(self):
29
- super().__init__(
30
- name="dependency-resolution",
31
- required=False, # Optional - can work without dependencies
32
- description="Process dependency resolution from registry",
33
- )
34
-
35
- async def execute(self, context: dict[str, Any]) -> PipelineResult:
36
- """Process dependency resolution with hash-based change detection."""
37
- self.logger.trace("Processing dependency resolution...")
38
-
39
- result = PipelineResult(message="Dependency resolution processed")
40
-
41
- try:
42
- # Get heartbeat response and registry wrapper
43
- heartbeat_response = context.get("heartbeat_response", {})
44
- registry_wrapper = context.get("registry_wrapper")
45
-
46
- if not heartbeat_response or not registry_wrapper:
47
- result.status = PipelineStatus.SUCCESS
48
- result.message = (
49
- "No heartbeat response or registry wrapper - completed successfully"
50
- )
51
- self.logger.info("ℹ️ No heartbeat response to process - this is normal")
52
- return result
53
-
54
- # Use the existing hash-based change detection and rewiring logic
55
- await self.process_heartbeat_response_for_rewiring(heartbeat_response)
56
-
57
- # For context consistency, also extract dependency count
58
- dependencies_resolved = registry_wrapper.parse_tool_dependencies(
59
- heartbeat_response
60
- )
61
- dependency_count = sum(
62
- len(deps) if isinstance(deps, list) else 0
63
- for deps in dependencies_resolved.values()
64
- )
65
-
66
- # Store processed dependencies info for context
67
- result.add_context("dependency_count", dependency_count)
68
- result.add_context("dependencies_resolved", dependencies_resolved)
69
-
70
- result.message = "Dependency resolution completed (efficient hash-based)"
71
- self.logger.trace(
72
- "🔗 Dependency resolution step completed using hash-based change detection"
73
- )
74
-
75
- except Exception as e:
76
- result.status = PipelineStatus.FAILED
77
- result.message = f"Dependency resolution failed: {e}"
78
- result.add_error(str(e))
79
- self.logger.error(f"❌ Dependency resolution failed: {e}")
80
-
81
- return result
82
-
83
- def _extract_dependency_state(
84
- self, heartbeat_response: dict[str, Any]
85
- ) -> dict[str, list[dict[str, Any]]]:
86
- """Extract dependency state structure from heartbeat response.
87
-
88
- Preserves array structure and order from registry to support multiple
89
- dependencies with the same capability name (e.g., different versions/tags).
90
-
91
- Returns:
92
- {function_name: [{capability, endpoint, function_name, status, agent_id, kwargs}, ...]}
93
- """
94
- state = {}
95
- dependencies_resolved = heartbeat_response.get("dependencies_resolved", {})
96
-
97
- for function_name, dependency_list in dependencies_resolved.items():
98
- if not isinstance(dependency_list, list):
99
- continue
100
-
101
- state[function_name] = []
102
- for dep_resolution in dependency_list:
103
- if (
104
- not isinstance(dep_resolution, dict)
105
- or "capability" not in dep_resolution
106
- ):
107
- continue
108
-
109
- # Preserve array structure to maintain order and support duplicate capabilities
110
- state[function_name].append(
111
- {
112
- "capability": dep_resolution["capability"],
113
- "endpoint": dep_resolution.get("endpoint", ""),
114
- "function_name": dep_resolution.get("function_name", ""),
115
- "status": dep_resolution.get("status", ""),
116
- "agent_id": dep_resolution.get("agent_id", ""),
117
- "kwargs": dep_resolution.get("kwargs", {}),
118
- }
119
- )
120
-
121
- return state
122
-
123
- def _hash_dependency_state(self, state: dict) -> str:
124
- """Create hash of dependency state structure."""
125
- import hashlib
126
-
127
- # Convert to sorted JSON string for consistent hashing
128
- state_json = json.dumps(state, sort_keys=True)
129
- return hashlib.sha256(state_json.encode()).hexdigest()[
130
- :16
131
- ] # First 16 chars for readability
132
-
133
- async def process_heartbeat_response_for_rewiring(
134
- self, heartbeat_response: dict[str, Any]
135
- ) -> None:
136
- """Process heartbeat response to update existing dependency injection.
137
-
138
- Uses hash-based comparison to efficiently detect when ANY dependency changes
139
- and then updates ALL affected functions in one operation.
140
-
141
- Resilience logic:
142
- - No response (connection error, 5xx) → Skip entirely (keep existing wiring)
143
- - 2xx response with empty dependencies → Unwire all dependencies
144
- - 2xx response with partial dependencies → Update to match registry exactly
145
- """
146
- try:
147
- if not heartbeat_response:
148
- # No response from registry (connection error, timeout, 5xx)
149
- # → Skip entirely for resilience (keep existing dependencies)
150
- self.logger.trace(
151
- "No heartbeat response - skipping rewiring for resilience"
152
- )
153
- return
154
-
155
- # Extract current dependency state structure
156
- current_state = self._extract_dependency_state(heartbeat_response)
157
-
158
- # IMPORTANT: Empty state from successful response means "unwire everything"
159
- # This is different from "no response" which means "keep existing for resilience"
160
-
161
- # Hash the current state (including empty state)
162
- current_hash = self._hash_dependency_state(current_state)
163
-
164
- # Compare with previous state (use global variable)
165
- global _last_dependency_hash
166
- if current_hash == _last_dependency_hash:
167
- self.logger.trace(
168
- f"🔄 Dependency state unchanged (hash: {current_hash}), skipping rewiring"
169
- )
170
- return
171
-
172
- # State changed - determine what changed
173
- function_count = len(current_state)
174
- total_deps = sum(len(deps) for deps in current_state.values())
175
-
176
- if _last_dependency_hash is None:
177
- if function_count > 0:
178
- self.logger.info(
179
- f"🔄 Initial dependency state detected: {function_count} functions, {total_deps} dependencies"
180
- )
181
- else:
182
- self.logger.info(
183
- "🔄 Initial dependency state detected: no dependencies"
184
- )
185
- else:
186
- self.logger.info(
187
- f"🔄 Dependency state changed (hash: {_last_dependency_hash} → {current_hash})"
188
- )
189
- if function_count > 0:
190
- self.logger.info(
191
- f"🔄 Updating dependencies for {function_count} functions ({total_deps} total dependencies)"
192
- )
193
- else:
194
- self.logger.info(
195
- "🔄 Registry reports no dependencies - unwiring all existing dependencies"
196
- )
197
-
198
- # Import here to avoid circular imports
199
- from ...engine.dependency_injector import get_global_injector
200
- from ...engine.unified_mcp_proxy import EnhancedUnifiedMCPProxy
201
-
202
- injector = get_global_injector()
203
-
204
- # Step 1: Collect all dependency keys (func_id:dep_index) that should exist
205
- # Map tool names to func_ids first
206
- from ...engine.decorator_registry import DecoratorRegistry
207
-
208
- tool_name_to_func_id = {}
209
- mesh_tools = DecoratorRegistry.get_mesh_tools()
210
- for tool_name, decorated_func in mesh_tools.items():
211
- func = decorated_func.function
212
- func_id = f"{func.__module__}.{func.__qualname__}"
213
- tool_name_to_func_id[tool_name] = func_id
214
-
215
- target_dependency_keys = set()
216
- for function_name, dependency_list in current_state.items():
217
- # Map tool name to func_id
218
- func_id = tool_name_to_func_id.get(function_name, function_name)
219
- for dep_index in range(len(dependency_list)):
220
- dep_key = f"{func_id}:dep_{dep_index}"
221
- target_dependency_keys.add(dep_key)
222
-
223
- # Step 2: Find existing dependency keys that need to be removed (unwired)
224
- # This handles the case where registry stops reporting some dependencies
225
- existing_dependency_keys = (
226
- set(injector._dependencies.keys())
227
- if hasattr(injector, "_dependencies")
228
- else set()
229
- )
230
- keys_to_remove = existing_dependency_keys - target_dependency_keys
231
-
232
- unwired_count = 0
233
- for dep_key in keys_to_remove:
234
- await injector.unregister_dependency(dep_key)
235
- unwired_count += 1
236
- self.logger.info(
237
- f"🗑️ Unwired dependency '{dep_key}' (no longer reported by registry)"
238
- )
239
-
240
- # Step 3: Apply all dependency updates using positional indexing
241
- updated_count = 0
242
- for function_name, dependency_list in current_state.items():
243
- # Map tool name to func_id (using mapping from Step 1)
244
- func_id = tool_name_to_func_id.get(function_name, function_name)
245
-
246
- for dep_index, dep_info in enumerate(dependency_list):
247
- status = dep_info["status"]
248
- endpoint = dep_info["endpoint"]
249
- dep_function_name = dep_info["function_name"]
250
- capability = dep_info["capability"]
251
- kwargs_config = dep_info.get("kwargs", {})
252
-
253
- if status == "available" and endpoint and dep_function_name:
254
- # Import here to avoid circular imports
255
- # Get current agent ID for self-dependency detection
256
- import os
257
-
258
- from ...engine.self_dependency_proxy import SelfDependencyProxy
259
-
260
- # Get current agent ID from DecoratorRegistry (single source of truth)
261
- current_agent_id = None
262
- try:
263
- from ...engine.decorator_registry import DecoratorRegistry
264
-
265
- config = DecoratorRegistry.get_resolved_agent_config()
266
- current_agent_id = config["agent_id"]
267
- self.logger.trace(
268
- f"🔍 Current agent ID from DecoratorRegistry: '{current_agent_id}'"
269
- )
270
- except Exception as e:
271
- # Fallback to environment variable
272
- current_agent_id = os.getenv("MCP_MESH_AGENT_ID")
273
- self.logger.trace(
274
- f"🔍 Current agent ID from environment: '{current_agent_id}' (fallback due to: {e})"
275
- )
276
-
277
- target_agent_id = dep_info.get("agent_id")
278
- self.logger.trace(
279
- f"🔍 Target agent ID from registry: '{target_agent_id}'"
280
- )
281
-
282
- # Determine if this is a self-dependency
283
- is_self_dependency = (
284
- current_agent_id
285
- and target_agent_id
286
- and current_agent_id == target_agent_id
287
- )
288
-
289
- self.logger.trace(
290
- f"🔍 Self-dependency check for '{capability}': "
291
- f"current='{current_agent_id}' vs target='{target_agent_id}' "
292
- f"→ {'SELF' if is_self_dependency else 'CROSS'}-dependency"
293
- )
294
-
295
- if is_self_dependency:
296
- # Create self-dependency proxy with WRAPPER function (not original)
297
- # The wrapper has dependency injection logic, so calling it ensures
298
- # the target function's dependencies are also injected properly.
299
- wrapper_func = None
300
- if dep_function_name in mesh_tools:
301
- wrapper_func = mesh_tools[dep_function_name].function
302
- self.logger.trace(
303
- f"🔍 Found wrapper for '{dep_function_name}' in DecoratorRegistry"
304
- )
305
-
306
- if wrapper_func:
307
- new_proxy = SelfDependencyProxy(
308
- wrapper_func, dep_function_name
309
- )
310
- self.logger.debug(
311
- f"🔄 SELF-DEPENDENCY: Using wrapper for '{capability}' "
312
- f"(local call with full DI support)"
313
- )
314
- else:
315
- # Fallback to original function if wrapper not found
316
- original_func = injector.find_original_function(
317
- dep_function_name
318
- )
319
- if original_func:
320
- new_proxy = SelfDependencyProxy(
321
- original_func, dep_function_name
322
- )
323
- self.logger.warning(
324
- f"⚠️ SELF-DEPENDENCY: Using original function for '{capability}' "
325
- f"(wrapper not found, DI may not work for nested deps)"
326
- )
327
- else:
328
- self.logger.error(
329
- f"❌ Cannot create SelfDependencyProxy for '{capability}': "
330
- f"neither wrapper nor original function '{dep_function_name}' found, falling back to HTTP"
331
- )
332
- # Use unified proxy for fallback
333
- new_proxy = EnhancedUnifiedMCPProxy(
334
- endpoint,
335
- dep_function_name,
336
- kwargs_config=kwargs_config,
337
- )
338
- self.logger.trace(
339
- f"🔧 Created EnhancedUnifiedMCPProxy (fallback): {kwargs_config}"
340
- )
341
- else:
342
- # Create cross-service proxy using unified proxy
343
- new_proxy = EnhancedUnifiedMCPProxy(
344
- endpoint,
345
- dep_function_name,
346
- kwargs_config=kwargs_config,
347
- )
348
- self.logger.debug(
349
- f"🔄 Updated to EnhancedUnifiedMCPProxy: '{capability}' -> {endpoint}/{dep_function_name}, "
350
- f"timeout={kwargs_config.get('timeout', 30)}s, streaming={kwargs_config.get('streaming', False)}"
351
- )
352
-
353
- # Register with composite key using func_id (not tool name) to match injector lookup
354
- dep_key = f"{func_id}:dep_{dep_index}"
355
- await injector.register_dependency(dep_key, new_proxy)
356
- updated_count += 1
357
- self.logger.trace(
358
- f"🔗 Registered dependency '{capability}' at position {dep_index} with key '{dep_key}' (func_id: {func_id})"
359
- )
360
- else:
361
- if status != "available":
362
- self.logger.trace(
363
- f"⚠️ Dependency '{capability}' at position {dep_index} not available: {status}"
364
- )
365
- else:
366
- self.logger.warning(
367
- f"⚠️ Cannot update dependency '{capability}' at position {dep_index}: missing endpoint or function_name"
368
- )
369
-
370
- # Store new hash for next comparison (use global variable)
371
- _last_dependency_hash = current_hash
372
-
373
- if unwired_count > 0 and updated_count > 0:
374
- self.logger.info(
375
- f"✅ Successfully unwired {unwired_count} and updated {updated_count} dependencies (state hash: {current_hash})"
376
- )
377
- elif unwired_count > 0:
378
- self.logger.info(
379
- f"✅ Successfully unwired {unwired_count} dependencies (state hash: {current_hash})"
380
- )
381
- elif updated_count > 0:
382
- self.logger.info(
383
- f"✅ Successfully updated {updated_count} dependencies (state hash: {current_hash})"
384
- )
385
- else:
386
- self.logger.info(
387
- f"✅ Dependency state synchronized (state hash: {current_hash})"
388
- )
389
-
390
- except Exception as e:
391
- self.logger.error(
392
- f"❌ Failed to process heartbeat response for rewiring: {e}"
393
- )
394
- # Don't raise - this should not break the heartbeat loop
395
-
396
- # Proxy type determination method removed - now using unified proxy for all dependencies
@@ -1,116 +0,0 @@
1
- """
2
- Fast Heartbeat Check Step for MCP Mesh pipeline.
3
-
4
- Performs lightweight HEAD requests to registry for fast topology change detection
5
- before expensive full POST heartbeat operations.
6
- """
7
-
8
- import logging
9
- from typing import Any
10
-
11
- from ...shared.fast_heartbeat_status import FastHeartbeatStatus, FastHeartbeatStatusUtil
12
- from ..shared import PipelineResult, PipelineStatus, PipelineStep
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- class FastHeartbeatStep(PipelineStep):
18
- """
19
- Fast heartbeat check step for optimization and resilience.
20
-
21
- Performs lightweight HEAD request to registry to check for topology changes
22
- before deciding whether to execute expensive full POST heartbeat.
23
-
24
- Stores semantic status in context for pipeline conditional execution.
25
- """
26
-
27
- def __init__(self):
28
- super().__init__(
29
- name="fast-heartbeat-check",
30
- required=True,
31
- description="Lightweight HEAD request for fast topology change detection",
32
- )
33
-
34
- async def execute(self, context: dict[str, Any]) -> PipelineResult:
35
- """
36
- Execute fast heartbeat check and store semantic status.
37
-
38
- Args:
39
- context: Pipeline context containing agent_id and registry_wrapper
40
-
41
- Returns:
42
- PipelineResult with fast_heartbeat_status in context
43
- """
44
- self.logger.trace("Starting fast heartbeat check...")
45
-
46
- result = PipelineResult(message="Fast heartbeat check completed")
47
-
48
- try:
49
- # Validate required context
50
- agent_id = context.get("agent_id")
51
- registry_wrapper = context.get("registry_wrapper")
52
-
53
- if not agent_id:
54
- raise ValueError("agent_id is required in context")
55
-
56
- if not registry_wrapper:
57
- raise ValueError("registry_wrapper is required in context")
58
-
59
- self.logger.trace(
60
- f"🚀 Performing fast heartbeat check for agent '{agent_id}'"
61
- )
62
-
63
- # Perform fast heartbeat check
64
- status = await registry_wrapper.check_fast_heartbeat(agent_id)
65
-
66
- # Store semantic status in context
67
- result.add_context("fast_heartbeat_status", status)
68
-
69
- # Set appropriate message based on status
70
- action_description = FastHeartbeatStatusUtil.get_action_description(status)
71
- result.message = f"Fast heartbeat check: {action_description}"
72
-
73
- # Log status and action
74
- if status == FastHeartbeatStatus.NO_CHANGES:
75
- self.logger.trace(
76
- f"✅ Fast heartbeat: No changes detected for agent '{agent_id}'"
77
- )
78
- elif status == FastHeartbeatStatus.TOPOLOGY_CHANGED:
79
- self.logger.trace(
80
- f"🔄 Fast heartbeat: Topology changed for agent '{agent_id}' - full refresh needed"
81
- )
82
- elif status == FastHeartbeatStatus.AGENT_UNKNOWN:
83
- self.logger.trace(
84
- f"❓ Fast heartbeat: Agent '{agent_id}' unknown - re-registration needed"
85
- )
86
- elif status == FastHeartbeatStatus.REGISTRY_ERROR:
87
- self.logger.warning(
88
- f"⚠️ Fast heartbeat: Registry error for agent '{agent_id}' - skipping for resilience"
89
- )
90
- elif status == FastHeartbeatStatus.NETWORK_ERROR:
91
- self.logger.warning(
92
- f"⚠️ Fast heartbeat: Network error for agent '{agent_id}' - skipping for resilience"
93
- )
94
-
95
- except Exception as e:
96
- # Convert any exception to NETWORK_ERROR for resilient handling
97
- status = FastHeartbeatStatusUtil.from_exception(e)
98
- result.add_context("fast_heartbeat_status", status)
99
-
100
- action_description = FastHeartbeatStatusUtil.get_action_description(status)
101
- result.message = f"Fast heartbeat check: {action_description}"
102
-
103
- self.logger.warning(
104
- f"⚠️ Fast heartbeat check failed for agent '{agent_id}': {e}"
105
- )
106
- self.logger.debug(f"Exception details: {e}", exc_info=True)
107
-
108
- # Step succeeds but sets error status for pipeline decision
109
- # This ensures pipeline can handle errors gracefully
110
-
111
- # Always preserve existing context
112
- for key, value in context.items():
113
- if key not in result.context:
114
- result.add_context(key, value)
115
-
116
- return result