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
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rust-backed heartbeat implementation for API services.
|
|
3
|
+
|
|
4
|
+
Replaces the Python API heartbeat pipeline with the Rust core runtime.
|
|
5
|
+
The Rust core handles:
|
|
6
|
+
- Registry communication (HEAD/POST heartbeats)
|
|
7
|
+
- Topology change detection
|
|
8
|
+
- Event emission
|
|
9
|
+
|
|
10
|
+
Python handles:
|
|
11
|
+
- DI updates when topology changes (route wrapper updates)
|
|
12
|
+
- FastAPI app health status
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Lazy import to avoid ImportError if Rust core not built
|
|
23
|
+
_rust_core = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_rust_core():
|
|
27
|
+
"""Lazy import of Rust core module."""
|
|
28
|
+
global _rust_core
|
|
29
|
+
if _rust_core is None:
|
|
30
|
+
try:
|
|
31
|
+
import mcp_mesh_core
|
|
32
|
+
|
|
33
|
+
_rust_core = mcp_mesh_core
|
|
34
|
+
logger.debug("Rust core module loaded successfully for API heartbeat")
|
|
35
|
+
except ImportError as e:
|
|
36
|
+
logger.warning(f"Rust core not available for API heartbeat: {e}")
|
|
37
|
+
raise
|
|
38
|
+
return _rust_core
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _build_api_agent_spec(context: dict[str, Any], service_id: str = None) -> Any:
|
|
42
|
+
"""
|
|
43
|
+
Build AgentSpec from API service context.
|
|
44
|
+
|
|
45
|
+
Converts the API service metadata and route wrappers into a Rust AgentSpec.
|
|
46
|
+
For API services:
|
|
47
|
+
- function_name = METHOD:path (e.g., "GET:/api/v1/data")
|
|
48
|
+
- capability = "" (API routes don't provide capabilities, they consume them)
|
|
49
|
+
- dependencies = list of capabilities the route depends on
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
context: Pipeline context containing display_config, agent_config, etc.
|
|
53
|
+
service_id: Service ID from heartbeat config (passed explicitly)
|
|
54
|
+
"""
|
|
55
|
+
core = _get_rust_core()
|
|
56
|
+
|
|
57
|
+
# Get service ID - prefer explicit parameter, fallback to context
|
|
58
|
+
if not service_id:
|
|
59
|
+
service_id = context.get("service_id") or context.get(
|
|
60
|
+
"agent_id", "unknown-api-service"
|
|
61
|
+
)
|
|
62
|
+
display_config = context.get("display_config", {})
|
|
63
|
+
agent_config = context.get("agent_config", {})
|
|
64
|
+
|
|
65
|
+
# Get registry URL
|
|
66
|
+
from ...shared.config_resolver import get_config_value
|
|
67
|
+
|
|
68
|
+
# Default is handled by Rust core
|
|
69
|
+
registry_url = get_config_value(
|
|
70
|
+
"MCP_MESH_REGISTRY_URL",
|
|
71
|
+
override=agent_config.get("registry_url"),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Get heartbeat interval
|
|
75
|
+
from ...shared.defaults import MeshDefaults
|
|
76
|
+
|
|
77
|
+
heartbeat_interval = int(
|
|
78
|
+
get_config_value(
|
|
79
|
+
"MCP_MESH_HEALTH_INTERVAL",
|
|
80
|
+
override=agent_config.get("health_interval"),
|
|
81
|
+
default=MeshDefaults.HEALTH_INTERVAL,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Get HTTP config from display_config
|
|
86
|
+
http_host = display_config.get("display_host", "127.0.0.1")
|
|
87
|
+
http_port = display_config.get("display_port", 8080)
|
|
88
|
+
namespace = agent_config.get("namespace", "default")
|
|
89
|
+
version = agent_config.get("version", "1.0.0")
|
|
90
|
+
|
|
91
|
+
# Build tool specs from route wrappers
|
|
92
|
+
from ...engine.decorator_registry import DecoratorRegistry
|
|
93
|
+
|
|
94
|
+
tools = []
|
|
95
|
+
route_wrappers = DecoratorRegistry.get_all_route_wrappers()
|
|
96
|
+
|
|
97
|
+
for route_id, route_info in route_wrappers.items():
|
|
98
|
+
dependencies = route_info.get("dependencies", [])
|
|
99
|
+
|
|
100
|
+
# Only include routes with dependencies (routes without deps don't need registry)
|
|
101
|
+
if not dependencies:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# Build dependency specs
|
|
105
|
+
deps = []
|
|
106
|
+
for dep_cap in dependencies:
|
|
107
|
+
# Tags must be serialized to JSON string (Rust core expects string, not list)
|
|
108
|
+
dep_spec = core.DependencySpec(
|
|
109
|
+
capability=dep_cap,
|
|
110
|
+
tags=json.dumps([]),
|
|
111
|
+
version=None,
|
|
112
|
+
)
|
|
113
|
+
deps.append(dep_spec)
|
|
114
|
+
|
|
115
|
+
# Create ToolSpec for this route
|
|
116
|
+
# For API routes: function_name is the route_id (METHOD:path)
|
|
117
|
+
# capability is empty since routes consume, not provide capabilities
|
|
118
|
+
tool_spec = core.ToolSpec(
|
|
119
|
+
function_name=route_id, # e.g., "GET:/api/v1/benchmark-services"
|
|
120
|
+
capability="", # API routes don't provide capabilities
|
|
121
|
+
version="1.0.0",
|
|
122
|
+
description="",
|
|
123
|
+
tags=[],
|
|
124
|
+
dependencies=deps if deps else None,
|
|
125
|
+
input_schema=None,
|
|
126
|
+
llm_filter=None,
|
|
127
|
+
llm_provider=None,
|
|
128
|
+
)
|
|
129
|
+
tools.append(tool_spec)
|
|
130
|
+
|
|
131
|
+
# Create AgentSpec
|
|
132
|
+
spec = core.AgentSpec(
|
|
133
|
+
name=service_id,
|
|
134
|
+
registry_url=registry_url,
|
|
135
|
+
version=version,
|
|
136
|
+
description="",
|
|
137
|
+
http_port=http_port,
|
|
138
|
+
http_host=http_host,
|
|
139
|
+
namespace=namespace,
|
|
140
|
+
agent_type="api", # API services only consume capabilities, not provide them
|
|
141
|
+
tools=tools if tools else None,
|
|
142
|
+
llm_agents=None, # API services don't have LLM agents
|
|
143
|
+
heartbeat_interval=heartbeat_interval,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
logger.info(
|
|
147
|
+
f"Built API AgentSpec: name={service_id}, routes_with_deps={len(tools)}, "
|
|
148
|
+
f"registry={registry_url}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return spec
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def _handle_api_mesh_event(event: Any, context: dict[str, Any]) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Handle a mesh event from the Rust core for API services.
|
|
157
|
+
|
|
158
|
+
Dispatches to appropriate handler based on event type.
|
|
159
|
+
"""
|
|
160
|
+
event_type = event.event_type
|
|
161
|
+
|
|
162
|
+
if event_type == "agent_registered":
|
|
163
|
+
logger.info(f"API service registered with ID: {event.agent_id}")
|
|
164
|
+
|
|
165
|
+
elif event_type == "registration_failed":
|
|
166
|
+
logger.error(f"API service registration failed: {event.error}")
|
|
167
|
+
|
|
168
|
+
elif event_type == "dependency_available":
|
|
169
|
+
await _handle_api_dependency_change(
|
|
170
|
+
capability=event.capability,
|
|
171
|
+
endpoint=event.endpoint,
|
|
172
|
+
function_name=event.function_name,
|
|
173
|
+
agent_id=event.agent_id,
|
|
174
|
+
available=True,
|
|
175
|
+
context=context,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
elif event_type == "dependency_changed":
|
|
179
|
+
await _handle_api_dependency_change(
|
|
180
|
+
capability=event.capability,
|
|
181
|
+
endpoint=event.endpoint,
|
|
182
|
+
function_name=event.function_name,
|
|
183
|
+
agent_id=event.agent_id,
|
|
184
|
+
available=True,
|
|
185
|
+
context=context,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
elif event_type == "dependency_unavailable":
|
|
189
|
+
await _handle_api_dependency_change(
|
|
190
|
+
capability=event.capability,
|
|
191
|
+
endpoint=None,
|
|
192
|
+
function_name=None,
|
|
193
|
+
agent_id=None,
|
|
194
|
+
available=False,
|
|
195
|
+
context=context,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
elif event_type == "llm_tools_updated":
|
|
199
|
+
# API services typically don't use LLM tools, but handle gracefully
|
|
200
|
+
logger.debug(f"LLM tools update for API service (ignored): {event.function_id}")
|
|
201
|
+
|
|
202
|
+
elif event_type == "health_check_due":
|
|
203
|
+
logger.debug("Health check due for API service (not implemented yet)")
|
|
204
|
+
|
|
205
|
+
elif event_type == "registry_disconnected":
|
|
206
|
+
logger.warning(f"Registry disconnected for API service: {event.reason}")
|
|
207
|
+
|
|
208
|
+
elif event_type == "shutdown":
|
|
209
|
+
logger.info("Rust core shutdown event received for API service")
|
|
210
|
+
|
|
211
|
+
else:
|
|
212
|
+
logger.debug(f"Unhandled event type for API service: {event_type}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def _handle_api_dependency_change(
|
|
216
|
+
capability: str,
|
|
217
|
+
endpoint: Optional[str],
|
|
218
|
+
function_name: Optional[str],
|
|
219
|
+
agent_id: Optional[str],
|
|
220
|
+
available: bool,
|
|
221
|
+
context: dict[str, Any],
|
|
222
|
+
) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Handle dependency availability change for API services.
|
|
225
|
+
|
|
226
|
+
Updates route wrappers with new/changed/removed dependencies.
|
|
227
|
+
API services use route wrappers which have direct _mesh_update_dependency methods.
|
|
228
|
+
"""
|
|
229
|
+
logger.info(
|
|
230
|
+
f"API dependency change: {capability} -> "
|
|
231
|
+
f"{'available' if available else 'unavailable'} "
|
|
232
|
+
f"at {endpoint}/{function_name}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
from ...engine.decorator_registry import DecoratorRegistry
|
|
236
|
+
from ...engine.unified_mcp_proxy import EnhancedUnifiedMCPProxy
|
|
237
|
+
|
|
238
|
+
route_wrappers = DecoratorRegistry.get_all_route_wrappers()
|
|
239
|
+
|
|
240
|
+
if not available:
|
|
241
|
+
# Dependency became unavailable - clear it from all route wrappers
|
|
242
|
+
for route_id, route_info in route_wrappers.items():
|
|
243
|
+
dependencies = route_info.get("dependencies", [])
|
|
244
|
+
wrapper = route_info.get("wrapper")
|
|
245
|
+
|
|
246
|
+
if not wrapper or not hasattr(wrapper, "_mesh_update_dependency"):
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# Find which dependency index(es) match this capability
|
|
250
|
+
for dep_index, dep_cap in enumerate(dependencies):
|
|
251
|
+
if dep_cap == capability:
|
|
252
|
+
# Set to None to indicate unavailable
|
|
253
|
+
wrapper._mesh_update_dependency(dep_index, None)
|
|
254
|
+
logger.info(
|
|
255
|
+
f"Cleared dependency '{capability}' at index {dep_index} "
|
|
256
|
+
f"for route '{route_id}'"
|
|
257
|
+
)
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
# Dependency is available - update all route wrappers that need it
|
|
261
|
+
for route_id, route_info in route_wrappers.items():
|
|
262
|
+
dependencies = route_info.get("dependencies", [])
|
|
263
|
+
wrapper = route_info.get("wrapper")
|
|
264
|
+
|
|
265
|
+
if not wrapper or not hasattr(wrapper, "_mesh_update_dependency"):
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
# Find which dependency index(es) match this capability
|
|
269
|
+
for dep_index, dep_cap in enumerate(dependencies):
|
|
270
|
+
if dep_cap == capability:
|
|
271
|
+
# Check for self-dependency (rare for API services but handle it)
|
|
272
|
+
current_service_id = context.get("service_id") or context.get(
|
|
273
|
+
"agent_id"
|
|
274
|
+
)
|
|
275
|
+
if not current_service_id:
|
|
276
|
+
# Use config resolver for consistent env var handling
|
|
277
|
+
from ...shared.config_resolver import get_config_value
|
|
278
|
+
|
|
279
|
+
current_service_id = get_config_value("MCP_MESH_AGENT_ID")
|
|
280
|
+
|
|
281
|
+
is_self_dependency = (
|
|
282
|
+
current_service_id and agent_id and current_service_id == agent_id
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if is_self_dependency:
|
|
286
|
+
# Self-dependency for API services - use SelfDependencyProxy
|
|
287
|
+
from ...engine.self_dependency_proxy import SelfDependencyProxy
|
|
288
|
+
|
|
289
|
+
# For API services, try to find the function in mesh tools
|
|
290
|
+
mesh_tools = DecoratorRegistry.get_mesh_tools()
|
|
291
|
+
wrapper_func = mesh_tools.get(function_name)
|
|
292
|
+
|
|
293
|
+
if wrapper_func:
|
|
294
|
+
proxy = SelfDependencyProxy(
|
|
295
|
+
wrapper_func.function, function_name
|
|
296
|
+
)
|
|
297
|
+
logger.debug(
|
|
298
|
+
f"Created SelfDependencyProxy for API route '{route_id}' "
|
|
299
|
+
f"dependency '{capability}'"
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
# Fallback to HTTP proxy
|
|
303
|
+
proxy = EnhancedUnifiedMCPProxy(endpoint, function_name)
|
|
304
|
+
logger.debug(
|
|
305
|
+
f"Created EnhancedUnifiedMCPProxy (fallback) for API route "
|
|
306
|
+
f"'{route_id}' dependency '{capability}'"
|
|
307
|
+
)
|
|
308
|
+
else:
|
|
309
|
+
# Cross-service dependency - create HTTP proxy
|
|
310
|
+
proxy = EnhancedUnifiedMCPProxy(endpoint, function_name)
|
|
311
|
+
logger.debug(
|
|
312
|
+
f"Created EnhancedUnifiedMCPProxy for API route '{route_id}' "
|
|
313
|
+
f"dependency '{capability}' -> {endpoint}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Update the route wrapper
|
|
317
|
+
wrapper._mesh_update_dependency(dep_index, proxy)
|
|
318
|
+
logger.info(
|
|
319
|
+
f"Updated dependency '{capability}' at index {dep_index} "
|
|
320
|
+
f"for route '{route_id}' -> {endpoint}/{function_name}"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
async def rust_api_heartbeat_task(heartbeat_config: dict[str, Any]) -> None:
|
|
325
|
+
"""
|
|
326
|
+
Rust-backed heartbeat task for API services that runs in FastAPI lifespan.
|
|
327
|
+
|
|
328
|
+
This is a drop-in replacement for api_heartbeat_lifespan_task.
|
|
329
|
+
Instead of running Python heartbeat pipeline, it starts the Rust core
|
|
330
|
+
and listens for events.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
heartbeat_config: Configuration containing service_id, interval, context
|
|
334
|
+
"""
|
|
335
|
+
service_id = heartbeat_config.get("service_id", "unknown-api-service")
|
|
336
|
+
context = heartbeat_config.get("context", {})
|
|
337
|
+
standalone_mode = heartbeat_config.get("standalone_mode", False)
|
|
338
|
+
|
|
339
|
+
if standalone_mode:
|
|
340
|
+
logger.info(
|
|
341
|
+
f"Rust API heartbeat in standalone mode for service '{service_id}' "
|
|
342
|
+
"(no registry communication)"
|
|
343
|
+
)
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
core = _get_rust_core()
|
|
348
|
+
except ImportError as e:
|
|
349
|
+
logger.error(
|
|
350
|
+
f"Rust core not available for API service '{service_id}': {e}. "
|
|
351
|
+
"The mcp_mesh_core module must be built and installed."
|
|
352
|
+
)
|
|
353
|
+
raise RuntimeError(
|
|
354
|
+
f"Rust core (mcp_mesh_core) is required but not available: {e}"
|
|
355
|
+
) from e
|
|
356
|
+
|
|
357
|
+
logger.info(f"Starting Rust-backed heartbeat for API service '{service_id}'")
|
|
358
|
+
|
|
359
|
+
handle = None
|
|
360
|
+
try:
|
|
361
|
+
# Build AgentSpec from API service context, passing service_id explicitly
|
|
362
|
+
spec = _build_api_agent_spec(context, service_id=service_id)
|
|
363
|
+
|
|
364
|
+
# Start Rust core runtime
|
|
365
|
+
handle = core.start_agent(spec)
|
|
366
|
+
logger.info(f"Rust core started for API service '{service_id}'")
|
|
367
|
+
|
|
368
|
+
# Event loop - process events from Rust core
|
|
369
|
+
while True:
|
|
370
|
+
# Check for Python shutdown signal
|
|
371
|
+
try:
|
|
372
|
+
from ...shared.simple_shutdown import should_stop_heartbeat
|
|
373
|
+
|
|
374
|
+
if should_stop_heartbeat():
|
|
375
|
+
logger.info(
|
|
376
|
+
f"Stopping Rust API heartbeat for service '{service_id}' due to shutdown"
|
|
377
|
+
)
|
|
378
|
+
handle.shutdown()
|
|
379
|
+
break
|
|
380
|
+
except ImportError:
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
# Wait for next event from Rust core with timeout
|
|
385
|
+
# Timeout allows periodic shutdown checks
|
|
386
|
+
try:
|
|
387
|
+
event = await asyncio.wait_for(handle.next_event(), timeout=1.0)
|
|
388
|
+
except TimeoutError:
|
|
389
|
+
# No event in 1 second, loop back to check shutdown signal
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
if event.event_type == "shutdown":
|
|
393
|
+
logger.info(f"Rust core shutdown for API service '{service_id}'")
|
|
394
|
+
break
|
|
395
|
+
|
|
396
|
+
# Handle the event
|
|
397
|
+
await _handle_api_mesh_event(event, context)
|
|
398
|
+
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.error(f"Error handling Rust event for API service: {e}")
|
|
401
|
+
# Continue processing events
|
|
402
|
+
|
|
403
|
+
except asyncio.CancelledError:
|
|
404
|
+
logger.info(f"Rust API heartbeat task cancelled for service '{service_id}'")
|
|
405
|
+
raise
|
|
406
|
+
except Exception as e:
|
|
407
|
+
logger.error(f"Rust API heartbeat failed for service '{service_id}': {e}")
|
|
408
|
+
raise
|
|
409
|
+
finally:
|
|
410
|
+
# Always ensure graceful shutdown of Rust core to prevent daemon thread issues
|
|
411
|
+
# This is critical: without shutdown(), Rust background threads may try to
|
|
412
|
+
# write to stdout via tracing after Python's stdout is finalized
|
|
413
|
+
if handle is not None:
|
|
414
|
+
try:
|
|
415
|
+
handle.shutdown()
|
|
416
|
+
# Give Rust core a moment to clean up before Python exits
|
|
417
|
+
# Use time.sleep as fallback if asyncio is shutting down
|
|
418
|
+
try:
|
|
419
|
+
await asyncio.sleep(0.2)
|
|
420
|
+
except (asyncio.CancelledError, RuntimeError):
|
|
421
|
+
# Event loop might be shutting down, use blocking sleep
|
|
422
|
+
import time
|
|
423
|
+
|
|
424
|
+
time.sleep(0.2)
|
|
425
|
+
logger.debug(
|
|
426
|
+
f"Rust core shutdown complete for API service '{service_id}'"
|
|
427
|
+
)
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.warning(f"Error during Rust core shutdown for API service: {e}")
|
|
@@ -43,22 +43,20 @@ class APIPipeline(MeshPipeline):
|
|
|
43
43
|
"""Setup the API pipeline steps."""
|
|
44
44
|
# Essential API integration steps
|
|
45
45
|
steps = [
|
|
46
|
-
RouteCollectionStep(),
|
|
47
|
-
FastAPIAppDiscoveryStep(),
|
|
48
|
-
RouteIntegrationStep(),
|
|
49
|
-
TracingMiddlewareIntegrationStep(),
|
|
50
|
-
APIServerSetupStep(),
|
|
46
|
+
RouteCollectionStep(), # Collect @mesh.route decorators
|
|
47
|
+
FastAPIAppDiscoveryStep(), # Find user's FastAPI app instances
|
|
48
|
+
RouteIntegrationStep(), # Apply dependency injection to routes
|
|
49
|
+
TracingMiddlewareIntegrationStep(), # Add tracing middleware to FastAPI apps
|
|
50
|
+
APIServerSetupStep(), # Prepare service registration metadata
|
|
51
51
|
# Note: Heartbeat integration will be added in next phase
|
|
52
52
|
# Note: User controls FastAPI server startup (uvicorn/gunicorn)
|
|
53
53
|
]
|
|
54
54
|
|
|
55
55
|
self.add_steps(steps)
|
|
56
56
|
self.logger.debug(f"API pipeline configured with {len(steps)} steps")
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
# Log the pipeline strategy
|
|
59
59
|
self.logger.info(
|
|
60
60
|
f"🌐 [DEBUG] API Pipeline initialized: dependency injection for @mesh.route decorators"
|
|
61
61
|
)
|
|
62
|
-
self.logger.debug(
|
|
63
|
-
f"📋 Pipeline steps: {[step.name for step in steps]}"
|
|
64
|
-
)
|
|
62
|
+
self.logger.debug(f"📋 Pipeline steps: {[step.name for step in steps]}")
|