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.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/engine/dependency_injector.py +13 -15
- _mcp_mesh/engine/http_wrapper.py +69 -10
- _mcp_mesh/engine/mesh_llm_agent.py +29 -10
- _mcp_mesh/engine/mesh_llm_agent_injector.py +77 -41
- _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
- _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
- _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
- _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
- _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
- _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
- _mcp_mesh/engine/response_parser.py +61 -15
- _mcp_mesh/engine/signature_analyzer.py +58 -68
- _mcp_mesh/engine/unified_mcp_proxy.py +19 -35
- _mcp_mesh/pipeline/__init__.py +9 -20
- _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
- _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
- _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +429 -0
- _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
- _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
- _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
- _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
- _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
- _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
- _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +710 -0
- _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
- _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +31 -8
- _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +23 -11
- _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
- _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
- _mcp_mesh/reload.py +1 -3
- _mcp_mesh/shared/__init__.py +2 -8
- _mcp_mesh/shared/config_resolver.py +124 -80
- _mcp_mesh/shared/defaults.py +89 -14
- _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
- _mcp_mesh/shared/host_resolver.py +8 -46
- _mcp_mesh/shared/server_discovery.py +115 -86
- _mcp_mesh/shared/simple_shutdown.py +44 -86
- _mcp_mesh/tracing/execution_tracer.py +2 -6
- _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
- _mcp_mesh/tracing/trace_context_helper.py +3 -13
- _mcp_mesh/tracing/utils.py +29 -15
- _mcp_mesh/utils/fastmcp_schema_extractor.py +5 -4
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0.dist-info}/METADATA +7 -5
- mcp_mesh-0.8.0.dist-info/RECORD +85 -0
- mesh/__init__.py +12 -1
- mesh/decorators.py +248 -33
- mesh/helpers.py +52 -0
- mesh/types.py +40 -13
- _mcp_mesh/generated/.openapi-generator/FILES +0 -50
- _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
- _mcp_mesh/generated/.openapi-generator-ignore +0 -15
- _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
- _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
- _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
- _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
- _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
- _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
- _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
- _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
- _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
- _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
- _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
- _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
- _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
- _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
- _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
- _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
- _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
- _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
- _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
- _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
- _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
- _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
- _mcp_mesh/shared/registry_client_wrapper.py +0 -515
- mcp_mesh-0.7.21.dist-info/RECORD +0 -152
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0.dist-info}/WHEEL +0 -0
- {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
|