mcp-mesh 0.7.21__py3-none-any.whl → 0.8.0b1__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 +4 -6
- _mcp_mesh/engine/http_wrapper.py +69 -10
- _mcp_mesh/engine/mesh_llm_agent.py +4 -7
- _mcp_mesh/engine/mesh_llm_agent_injector.py +2 -1
- _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/unified_mcp_proxy.py +18 -34
- _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 +425 -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 +695 -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 +5 -6
- _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +21 -9
- _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 +2 -1
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/METADATA +2 -1
- mcp_mesh-0.8.0b1.dist-info/RECORD +85 -0
- mesh/__init__.py +2 -1
- mesh/decorators.py +89 -5
- _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.0b1.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,418 +0,0 @@
|
|
|
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.debug(
|
|
58
|
-
"No heartbeat response to process - this is normal for API services"
|
|
59
|
-
)
|
|
60
|
-
return result
|
|
61
|
-
|
|
62
|
-
# Use the same hash-based change detection pattern as MCP
|
|
63
|
-
await self.process_heartbeat_response_for_api_rewiring(heartbeat_response)
|
|
64
|
-
|
|
65
|
-
# For context consistency, extract dependency count
|
|
66
|
-
dependencies_resolved = registry_wrapper.parse_tool_dependencies(
|
|
67
|
-
heartbeat_response
|
|
68
|
-
)
|
|
69
|
-
dependency_count = sum(
|
|
70
|
-
len(deps) if isinstance(deps, list) else 0
|
|
71
|
-
for deps in dependencies_resolved.values()
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
# Store processed dependencies info for context
|
|
75
|
-
result.add_context("dependency_count", dependency_count)
|
|
76
|
-
result.add_context("dependencies_resolved", dependencies_resolved)
|
|
77
|
-
|
|
78
|
-
result.message = (
|
|
79
|
-
"API dependency resolution completed (efficient hash-based)"
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
if dependency_count > 0:
|
|
83
|
-
self.logger.info(f"🔗 Dependencies resolved: {dependency_count} items")
|
|
84
|
-
|
|
85
|
-
# Log function registry status for debugging
|
|
86
|
-
injector = get_global_injector()
|
|
87
|
-
function_count = len(injector._function_registry)
|
|
88
|
-
self.logger.debug(f"Function registry contains {function_count} functions")
|
|
89
|
-
|
|
90
|
-
except Exception as e:
|
|
91
|
-
result.status = PipelineStatus.FAILED
|
|
92
|
-
result.message = f"API dependency resolution failed: {e}"
|
|
93
|
-
result.add_error(str(e))
|
|
94
|
-
self.logger.error(f"❌ API dependency resolution failed: {e}")
|
|
95
|
-
|
|
96
|
-
return result
|
|
97
|
-
|
|
98
|
-
def _extract_dependency_state(
|
|
99
|
-
self, heartbeat_response: dict[str, Any]
|
|
100
|
-
) -> dict[str, list[dict[str, Any]]]:
|
|
101
|
-
"""Extract dependency state structure from heartbeat response.
|
|
102
|
-
|
|
103
|
-
Preserves array structure and order from registry to support multiple
|
|
104
|
-
dependencies with the same capability name (e.g., different versions/tags).
|
|
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, agent_id, kwargs}, ...]}
|
|
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
|
-
# Preserve array structure to maintain order and support duplicate capabilities
|
|
128
|
-
state[function_name].append(
|
|
129
|
-
{
|
|
130
|
-
"capability": dep_resolution["capability"],
|
|
131
|
-
"endpoint": dep_resolution.get("endpoint", ""),
|
|
132
|
-
"function_name": dep_resolution.get("function_name", ""),
|
|
133
|
-
"status": dep_resolution.get("status", ""),
|
|
134
|
-
"agent_id": dep_resolution.get("agent_id", ""),
|
|
135
|
-
"kwargs": dep_resolution.get("kwargs", {}),
|
|
136
|
-
}
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
return state
|
|
140
|
-
|
|
141
|
-
def _hash_dependency_state(self, state: dict) -> str:
|
|
142
|
-
"""Create hash of dependency state structure."""
|
|
143
|
-
import hashlib
|
|
144
|
-
|
|
145
|
-
# Convert to sorted JSON string for consistent hashing
|
|
146
|
-
state_json = json.dumps(state, sort_keys=True)
|
|
147
|
-
return hashlib.sha256(state_json.encode()).hexdigest()[
|
|
148
|
-
:16
|
|
149
|
-
] # First 16 chars for readability
|
|
150
|
-
|
|
151
|
-
async def process_heartbeat_response_for_api_rewiring(
|
|
152
|
-
self, heartbeat_response: dict[str, Any]
|
|
153
|
-
) -> None:
|
|
154
|
-
"""Process heartbeat response to update API route dependency injection.
|
|
155
|
-
|
|
156
|
-
Uses hash-based comparison to efficiently detect when ANY dependency changes
|
|
157
|
-
and then updates ALL affected route handlers in one operation.
|
|
158
|
-
|
|
159
|
-
Resilience logic (same as MCP):
|
|
160
|
-
- No response (connection error, 5xx) → Skip entirely (keep existing wiring)
|
|
161
|
-
- 2xx response with empty dependencies → Unwire all dependencies
|
|
162
|
-
- 2xx response with partial dependencies → Update to match registry exactly
|
|
163
|
-
"""
|
|
164
|
-
try:
|
|
165
|
-
if not heartbeat_response:
|
|
166
|
-
# No response from registry (connection error, timeout, 5xx)
|
|
167
|
-
# → Skip entirely for resilience (keep existing dependencies)
|
|
168
|
-
self.logger.debug(
|
|
169
|
-
"No heartbeat response - skipping API rewiring for resilience"
|
|
170
|
-
)
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
# Extract current dependency state structure
|
|
174
|
-
current_state = self._extract_dependency_state(heartbeat_response)
|
|
175
|
-
|
|
176
|
-
# IMPORTANT: Empty state from successful response means "unwire everything"
|
|
177
|
-
# This is different from "no response" which means "keep existing for resilience"
|
|
178
|
-
|
|
179
|
-
# Hash the current state (including empty state)
|
|
180
|
-
current_hash = self._hash_dependency_state(current_state)
|
|
181
|
-
|
|
182
|
-
# Compare with previous state (use global variable with API-specific name)
|
|
183
|
-
global _last_api_dependency_hash
|
|
184
|
-
if current_hash == _last_api_dependency_hash:
|
|
185
|
-
self.logger.debug(
|
|
186
|
-
f"🔄 API dependency state unchanged (hash: {current_hash}), skipping rewiring"
|
|
187
|
-
)
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
# State changed - determine what changed
|
|
191
|
-
function_count = len(current_state)
|
|
192
|
-
total_deps = sum(len(deps) for deps in current_state.values())
|
|
193
|
-
|
|
194
|
-
if _last_api_dependency_hash is None:
|
|
195
|
-
if function_count > 0:
|
|
196
|
-
self.logger.debug(
|
|
197
|
-
f"Initial API dependency state detected: {function_count} functions, {total_deps} dependencies"
|
|
198
|
-
)
|
|
199
|
-
else:
|
|
200
|
-
self.logger.debug(
|
|
201
|
-
"Initial API dependency state detected: no dependencies"
|
|
202
|
-
)
|
|
203
|
-
else:
|
|
204
|
-
self.logger.debug(
|
|
205
|
-
f"API dependency state changed (hash: {_last_api_dependency_hash} → {current_hash})"
|
|
206
|
-
)
|
|
207
|
-
if function_count > 0:
|
|
208
|
-
self.logger.debug(
|
|
209
|
-
f"Updating API dependencies for {function_count} functions ({total_deps} total dependencies)"
|
|
210
|
-
)
|
|
211
|
-
else:
|
|
212
|
-
self.logger.debug(
|
|
213
|
-
"Registry reports no API dependencies - unwiring all existing dependencies"
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
# Import here to avoid circular imports
|
|
217
|
-
from ...engine.dependency_injector import get_global_injector
|
|
218
|
-
|
|
219
|
-
injector = get_global_injector()
|
|
220
|
-
|
|
221
|
-
# Step 1: Collect all dependency keys (func_id:dep_index) that should exist
|
|
222
|
-
# Map tool names to func_ids first
|
|
223
|
-
from ...engine.decorator_registry import DecoratorRegistry
|
|
224
|
-
|
|
225
|
-
tool_name_to_func_id = {}
|
|
226
|
-
mesh_tools = DecoratorRegistry.get_mesh_tools()
|
|
227
|
-
for tool_name, decorated_func in mesh_tools.items():
|
|
228
|
-
func = decorated_func.function
|
|
229
|
-
func_id = f"{func.__module__}.{func.__qualname__}"
|
|
230
|
-
tool_name_to_func_id[tool_name] = func_id
|
|
231
|
-
|
|
232
|
-
target_dependency_keys = set()
|
|
233
|
-
for function_name, dependency_list in current_state.items():
|
|
234
|
-
# Map tool name to func_id
|
|
235
|
-
func_id = tool_name_to_func_id.get(function_name, function_name)
|
|
236
|
-
for dep_index in range(len(dependency_list)):
|
|
237
|
-
dep_key = f"{func_id}:dep_{dep_index}"
|
|
238
|
-
target_dependency_keys.add(dep_key)
|
|
239
|
-
|
|
240
|
-
# Step 2: Find existing dependency keys that need to be removed (unwired)
|
|
241
|
-
# This handles the case where registry stops reporting some dependencies
|
|
242
|
-
existing_dependency_keys = (
|
|
243
|
-
set(injector._dependencies.keys())
|
|
244
|
-
if hasattr(injector, "_dependencies")
|
|
245
|
-
else set()
|
|
246
|
-
)
|
|
247
|
-
keys_to_remove = existing_dependency_keys - target_dependency_keys
|
|
248
|
-
|
|
249
|
-
unwired_count = 0
|
|
250
|
-
for dep_key in keys_to_remove:
|
|
251
|
-
await injector.unregister_dependency(dep_key)
|
|
252
|
-
unwired_count += 1
|
|
253
|
-
self.logger.debug(
|
|
254
|
-
f"Unwired API dependency '{dep_key}' (no longer reported by registry)"
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
# Step 3: Apply all dependency updates using positional indexing
|
|
258
|
-
updated_count = 0
|
|
259
|
-
for function_name, dependency_list in current_state.items():
|
|
260
|
-
# Check if function_name is a route path (METHOD:path format)
|
|
261
|
-
# Route paths contain "/" and look like "GET:/api/v1/benchmark-services"
|
|
262
|
-
is_route_path = "/" in function_name
|
|
263
|
-
|
|
264
|
-
# Map tool name to func_id (using mapping from Step 1)
|
|
265
|
-
# For route paths, use the route_id directly as it won't be in tool_name_to_func_id
|
|
266
|
-
func_id = tool_name_to_func_id.get(function_name, function_name)
|
|
267
|
-
|
|
268
|
-
# Get route wrapper if this is a route path
|
|
269
|
-
route_wrapper_info = None
|
|
270
|
-
if is_route_path:
|
|
271
|
-
route_wrapper_info = DecoratorRegistry.get_route_wrapper(
|
|
272
|
-
function_name
|
|
273
|
-
)
|
|
274
|
-
if not route_wrapper_info:
|
|
275
|
-
self.logger.warning(
|
|
276
|
-
f"No route wrapper found for '{function_name}' - dependency injection may fail"
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
for dep_index, dep_info in enumerate(dependency_list):
|
|
280
|
-
status = dep_info["status"]
|
|
281
|
-
endpoint = dep_info["endpoint"]
|
|
282
|
-
dep_function_name = dep_info["function_name"]
|
|
283
|
-
capability = dep_info["capability"]
|
|
284
|
-
kwargs_config = dep_info.get("kwargs", {})
|
|
285
|
-
|
|
286
|
-
if status == "available" and endpoint and dep_function_name:
|
|
287
|
-
# Import here to avoid circular imports
|
|
288
|
-
import os
|
|
289
|
-
|
|
290
|
-
from ...engine.self_dependency_proxy import SelfDependencyProxy
|
|
291
|
-
from ...engine.unified_mcp_proxy import EnhancedUnifiedMCPProxy
|
|
292
|
-
|
|
293
|
-
# Get current agent ID for self-dependency detection
|
|
294
|
-
current_agent_id = None
|
|
295
|
-
try:
|
|
296
|
-
from ...engine.decorator_registry import DecoratorRegistry
|
|
297
|
-
|
|
298
|
-
config = DecoratorRegistry.get_resolved_agent_config()
|
|
299
|
-
current_agent_id = config["agent_id"]
|
|
300
|
-
except Exception:
|
|
301
|
-
# For API services, try environment variable fallback
|
|
302
|
-
current_agent_id = os.getenv("MCP_MESH_AGENT_ID")
|
|
303
|
-
|
|
304
|
-
target_agent_id = dep_info.get("agent_id")
|
|
305
|
-
|
|
306
|
-
# Determine if this is a self-dependency (less common for API services)
|
|
307
|
-
is_self_dependency = (
|
|
308
|
-
current_agent_id
|
|
309
|
-
and target_agent_id
|
|
310
|
-
and current_agent_id == target_agent_id
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
if is_self_dependency:
|
|
314
|
-
# Create self-dependency proxy with WRAPPER function (not original)
|
|
315
|
-
# The wrapper has dependency injection logic, so calling it ensures
|
|
316
|
-
# the target function's dependencies are also injected properly.
|
|
317
|
-
wrapper_func = None
|
|
318
|
-
if dep_function_name in mesh_tools:
|
|
319
|
-
wrapper_func = mesh_tools[dep_function_name].function
|
|
320
|
-
|
|
321
|
-
if wrapper_func:
|
|
322
|
-
new_proxy = SelfDependencyProxy(
|
|
323
|
-
wrapper_func, dep_function_name
|
|
324
|
-
)
|
|
325
|
-
self.logger.debug(
|
|
326
|
-
f"API SELF-DEPENDENCY: Using wrapper for '{capability}' "
|
|
327
|
-
f"(local call with full DI support)"
|
|
328
|
-
)
|
|
329
|
-
else:
|
|
330
|
-
# Fallback to original function if wrapper not found
|
|
331
|
-
original_func = injector.find_original_function(
|
|
332
|
-
dep_function_name
|
|
333
|
-
)
|
|
334
|
-
if original_func:
|
|
335
|
-
new_proxy = SelfDependencyProxy(
|
|
336
|
-
original_func, dep_function_name
|
|
337
|
-
)
|
|
338
|
-
self.logger.warning(
|
|
339
|
-
f"⚠️ API SELF-DEPENDENCY: Using original function for '{capability}' "
|
|
340
|
-
f"(wrapper not found, DI may not work for nested deps)"
|
|
341
|
-
)
|
|
342
|
-
else:
|
|
343
|
-
self.logger.warning(
|
|
344
|
-
f"⚠️ API SELF-DEPENDENCY: Cannot create SelfDependencyProxy for '{capability}', "
|
|
345
|
-
f"falling back to HTTP (may cause issues)"
|
|
346
|
-
)
|
|
347
|
-
# Fall back to unified proxy (same as cross-service)
|
|
348
|
-
new_proxy = EnhancedUnifiedMCPProxy(
|
|
349
|
-
endpoint,
|
|
350
|
-
dep_function_name,
|
|
351
|
-
kwargs_config=kwargs_config,
|
|
352
|
-
)
|
|
353
|
-
else:
|
|
354
|
-
# Create cross-service proxy using unified proxy (same as MCP pipeline)
|
|
355
|
-
new_proxy = EnhancedUnifiedMCPProxy(
|
|
356
|
-
endpoint,
|
|
357
|
-
dep_function_name,
|
|
358
|
-
kwargs_config=kwargs_config,
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
# For route paths, directly update the wrapper's dependencies
|
|
362
|
-
# This bypasses the injector key-based lookup which doesn't work for routes
|
|
363
|
-
if route_wrapper_info:
|
|
364
|
-
wrapper = route_wrapper_info.get("wrapper")
|
|
365
|
-
if wrapper and hasattr(wrapper, "_mesh_update_dependency"):
|
|
366
|
-
wrapper._mesh_update_dependency(dep_index, new_proxy)
|
|
367
|
-
updated_count += 1
|
|
368
|
-
self.logger.debug(
|
|
369
|
-
f"Updated route dependency '{capability}' at position {dep_index} "
|
|
370
|
-
f"→ {endpoint}/{dep_function_name} for route '{function_name}'"
|
|
371
|
-
)
|
|
372
|
-
else:
|
|
373
|
-
self.logger.warning(
|
|
374
|
-
f"Route wrapper for '{function_name}' doesn't have _mesh_update_dependency method"
|
|
375
|
-
)
|
|
376
|
-
else:
|
|
377
|
-
# Fallback: Register with composite key using func_id for MCP tools
|
|
378
|
-
dep_key = f"{func_id}:dep_{dep_index}"
|
|
379
|
-
await injector.register_dependency(dep_key, new_proxy)
|
|
380
|
-
updated_count += 1
|
|
381
|
-
|
|
382
|
-
# Log which functions will be affected
|
|
383
|
-
affected_functions = injector._dependency_mapping.get(
|
|
384
|
-
dep_key, set()
|
|
385
|
-
)
|
|
386
|
-
self.logger.debug(
|
|
387
|
-
f"Functions affected by '{capability}' at position {dep_index}: {list(affected_functions)}"
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
self.logger.debug(
|
|
391
|
-
f"Updated API dependency '{capability}' at position {dep_index} → {endpoint}/{dep_function_name}"
|
|
392
|
-
)
|
|
393
|
-
self.logger.debug(
|
|
394
|
-
f"Registered dependency '{capability}' at position {dep_index} with key '{dep_key}' (func_id: {func_id})"
|
|
395
|
-
)
|
|
396
|
-
else:
|
|
397
|
-
if status != "available":
|
|
398
|
-
self.logger.debug(
|
|
399
|
-
f"API dependency '{capability}' at position {dep_index} not available: {status}"
|
|
400
|
-
)
|
|
401
|
-
else:
|
|
402
|
-
self.logger.warning(
|
|
403
|
-
f"Cannot update API dependency '{capability}' at position {dep_index}: missing endpoint or function_name"
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
# Store new hash for next comparison (use global variable)
|
|
407
|
-
_last_api_dependency_hash = current_hash
|
|
408
|
-
|
|
409
|
-
if unwired_count > 0 or updated_count > 0:
|
|
410
|
-
self.logger.debug(
|
|
411
|
-
f"API dependency sync: unwired={unwired_count}, updated={updated_count} (hash: {current_hash})"
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
except Exception as e:
|
|
415
|
-
self.logger.error(
|
|
416
|
-
f"❌ Failed to process API heartbeat response for rewiring: {e}"
|
|
417
|
-
)
|
|
418
|
-
# Don't raise - this should not break the heartbeat loop
|
|
@@ -1,117 +0,0 @@
|
|
|
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.trace("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.trace(
|
|
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.trace(
|
|
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
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
API health check step for API heartbeat pipeline.
|
|
3
|
-
|
|
4
|
-
Validates FastAPI application health status and endpoint availability
|
|
5
|
-
for heartbeat reporting to the registry.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import logging
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
from ..shared.base_step import PipelineStep
|
|
12
|
-
from ..shared.pipeline_types import PipelineResult
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class APIHealthCheckStep(PipelineStep):
|
|
18
|
-
"""
|
|
19
|
-
Check FastAPI application health status.
|
|
20
|
-
|
|
21
|
-
Validates that the FastAPI application is running properly
|
|
22
|
-
and endpoints are accessible for dependency injection.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
def __init__(self, required: bool = True):
|
|
26
|
-
super().__init__(
|
|
27
|
-
name="api-health-check",
|
|
28
|
-
required=required,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
async def execute(self, context: dict[str, Any]) -> PipelineResult:
|
|
32
|
-
"""
|
|
33
|
-
Check FastAPI application health status.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
context: Pipeline context containing fastapi_app and service info
|
|
37
|
-
|
|
38
|
-
Returns:
|
|
39
|
-
PipelineResult with health_status in context
|
|
40
|
-
"""
|
|
41
|
-
self.logger.trace("🏥 Checking FastAPI application health status")
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
# Get FastAPI app from context
|
|
45
|
-
fastapi_app = context.get("fastapi_app")
|
|
46
|
-
service_id = context.get("service_id") or context.get("agent_id", "unknown")
|
|
47
|
-
|
|
48
|
-
if not fastapi_app:
|
|
49
|
-
error_msg = "No FastAPI application found in context for health check"
|
|
50
|
-
self.logger.error(f"❌ {error_msg}")
|
|
51
|
-
|
|
52
|
-
from ..shared.pipeline_types import PipelineStatus
|
|
53
|
-
result = PipelineResult(
|
|
54
|
-
status=PipelineStatus.FAILED,
|
|
55
|
-
message=error_msg,
|
|
56
|
-
context=context
|
|
57
|
-
)
|
|
58
|
-
result.add_error(error_msg)
|
|
59
|
-
return result
|
|
60
|
-
|
|
61
|
-
# Check FastAPI app basic properties
|
|
62
|
-
app_title = getattr(fastapi_app, "title", "Unknown API")
|
|
63
|
-
app_version = getattr(fastapi_app, "version", "1.0.0")
|
|
64
|
-
|
|
65
|
-
# Count available routes with dependency injection
|
|
66
|
-
routes_with_mesh = self._count_mesh_routes(fastapi_app)
|
|
67
|
-
total_routes = len(getattr(fastapi_app, "routes", []))
|
|
68
|
-
|
|
69
|
-
self.logger.trace(
|
|
70
|
-
f"🔍 FastAPI app health: {app_title} v{app_version}, "
|
|
71
|
-
f"{routes_with_mesh}/{total_routes} routes with mesh injection"
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
# Build health status for API service
|
|
75
|
-
# For API services, we create a simplified health status dict instead of using
|
|
76
|
-
# the strict HealthStatus model which requires capabilities (designed for MCP agents)
|
|
77
|
-
from datetime import UTC, datetime
|
|
78
|
-
|
|
79
|
-
health_status_dict = {
|
|
80
|
-
"agent_name": service_id,
|
|
81
|
-
"status": "healthy",
|
|
82
|
-
"timestamp": datetime.now(UTC).isoformat(),
|
|
83
|
-
"version": app_version,
|
|
84
|
-
"metadata": {
|
|
85
|
-
"service_type": "api",
|
|
86
|
-
"app_title": app_title,
|
|
87
|
-
"app_version": app_version,
|
|
88
|
-
"routes_total": total_routes,
|
|
89
|
-
"routes_with_mesh": routes_with_mesh,
|
|
90
|
-
"health_check_timestamp": datetime.now(UTC).isoformat(),
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
self.logger.trace(
|
|
95
|
-
f"🏥 API health check passed: {app_title} v{app_version} "
|
|
96
|
-
f"({routes_with_mesh} mesh routes)"
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
return PipelineResult(
|
|
100
|
-
message=f"API health check passed for {app_title}",
|
|
101
|
-
context={
|
|
102
|
-
"health_status": health_status_dict,
|
|
103
|
-
"app_title": app_title,
|
|
104
|
-
"app_version": app_version,
|
|
105
|
-
"routes_total": total_routes,
|
|
106
|
-
"routes_with_mesh": routes_with_mesh,
|
|
107
|
-
}
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
except Exception as e:
|
|
111
|
-
error_msg = f"API health check failed: {e}"
|
|
112
|
-
self.logger.error(f"❌ {error_msg}")
|
|
113
|
-
|
|
114
|
-
from ..shared.pipeline_types import PipelineStatus
|
|
115
|
-
result = PipelineResult(
|
|
116
|
-
status=PipelineStatus.FAILED,
|
|
117
|
-
message=error_msg,
|
|
118
|
-
context=context
|
|
119
|
-
)
|
|
120
|
-
result.add_error(str(e))
|
|
121
|
-
return result
|
|
122
|
-
|
|
123
|
-
def _count_mesh_routes(self, fastapi_app: Any) -> int:
|
|
124
|
-
"""Count routes that have mesh dependency injection applied."""
|
|
125
|
-
try:
|
|
126
|
-
mesh_routes = 0
|
|
127
|
-
routes = getattr(fastapi_app, "routes", [])
|
|
128
|
-
|
|
129
|
-
for route in routes:
|
|
130
|
-
# Check if route has dependency injection wrapper
|
|
131
|
-
endpoint = getattr(route, "endpoint", None)
|
|
132
|
-
if endpoint and hasattr(endpoint, "__wrapped__"):
|
|
133
|
-
# This indicates our dependency injection wrapper
|
|
134
|
-
mesh_routes += 1
|
|
135
|
-
|
|
136
|
-
return mesh_routes
|
|
137
|
-
|
|
138
|
-
except Exception as e:
|
|
139
|
-
self.logger.warning(f"⚠️ Could not count mesh routes: {e}")
|
|
140
|
-
return 0
|