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.
Files changed (121) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/dependency_injector.py +4 -6
  3. _mcp_mesh/engine/http_wrapper.py +69 -10
  4. _mcp_mesh/engine/mesh_llm_agent.py +4 -7
  5. _mcp_mesh/engine/mesh_llm_agent_injector.py +2 -1
  6. _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
  7. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
  8. _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
  9. _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
  10. _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
  11. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
  12. _mcp_mesh/engine/response_parser.py +61 -15
  13. _mcp_mesh/engine/unified_mcp_proxy.py +18 -34
  14. _mcp_mesh/pipeline/__init__.py +9 -20
  15. _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
  16. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
  17. _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +425 -0
  18. _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
  19. _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
  20. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
  21. _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
  22. _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
  23. _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
  24. _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +695 -0
  25. _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
  26. _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
  27. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +5 -6
  28. _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
  29. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +21 -9
  30. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
  31. _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
  32. _mcp_mesh/reload.py +1 -3
  33. _mcp_mesh/shared/__init__.py +2 -8
  34. _mcp_mesh/shared/config_resolver.py +124 -80
  35. _mcp_mesh/shared/defaults.py +89 -14
  36. _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
  37. _mcp_mesh/shared/host_resolver.py +8 -46
  38. _mcp_mesh/shared/server_discovery.py +115 -86
  39. _mcp_mesh/shared/simple_shutdown.py +44 -86
  40. _mcp_mesh/tracing/execution_tracer.py +2 -6
  41. _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
  42. _mcp_mesh/tracing/trace_context_helper.py +3 -13
  43. _mcp_mesh/tracing/utils.py +29 -15
  44. _mcp_mesh/utils/fastmcp_schema_extractor.py +2 -1
  45. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/METADATA +2 -1
  46. mcp_mesh-0.8.0b1.dist-info/RECORD +85 -0
  47. mesh/__init__.py +2 -1
  48. mesh/decorators.py +89 -5
  49. _mcp_mesh/generated/.openapi-generator/FILES +0 -50
  50. _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
  51. _mcp_mesh/generated/.openapi-generator-ignore +0 -15
  52. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
  53. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
  54. _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
  55. _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
  56. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
  57. _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
  58. _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
  59. _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
  60. _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
  61. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
  62. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
  63. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
  64. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
  65. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
  66. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
  67. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
  68. _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
  69. _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
  70. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
  71. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
  72. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
  73. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
  74. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
  75. _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
  76. _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
  77. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
  78. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
  79. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
  80. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
  81. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
  82. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
  83. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
  84. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
  85. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
  86. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
  87. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
  88. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
  89. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
  90. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
  91. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
  92. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
  93. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
  94. _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
  95. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
  96. _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
  97. _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
  98. _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
  99. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
  100. _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
  101. _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
  102. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
  103. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
  104. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
  105. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
  106. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
  107. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
  108. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
  109. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
  110. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
  111. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
  112. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
  113. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
  114. _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
  115. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
  116. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
  117. _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
  118. _mcp_mesh/shared/registry_client_wrapper.py +0 -515
  119. mcp_mesh-0.7.21.dist-info/RECORD +0 -152
  120. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/WHEEL +0 -0
  121. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,425 @@
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
+ dep_spec = core.DependencySpec(
108
+ capability=dep_cap,
109
+ tags=[],
110
+ version=None,
111
+ )
112
+ deps.append(dep_spec)
113
+
114
+ # Create ToolSpec for this route
115
+ # For API routes: function_name is the route_id (METHOD:path)
116
+ # capability is empty since routes consume, not provide capabilities
117
+ tool_spec = core.ToolSpec(
118
+ function_name=route_id, # e.g., "GET:/api/v1/benchmark-services"
119
+ capability="", # API routes don't provide capabilities
120
+ version="1.0.0",
121
+ description="",
122
+ tags=[],
123
+ dependencies=deps if deps else None,
124
+ input_schema=None,
125
+ llm_filter=None,
126
+ llm_provider=None,
127
+ )
128
+ tools.append(tool_spec)
129
+
130
+ # Create AgentSpec
131
+ spec = core.AgentSpec(
132
+ name=service_id,
133
+ registry_url=registry_url,
134
+ version=version,
135
+ description="",
136
+ http_port=http_port,
137
+ http_host=http_host,
138
+ namespace=namespace,
139
+ tools=tools if tools else None,
140
+ llm_agents=None, # API services don't have LLM agents
141
+ heartbeat_interval=heartbeat_interval,
142
+ )
143
+
144
+ logger.info(
145
+ f"Built API AgentSpec: name={service_id}, routes_with_deps={len(tools)}, "
146
+ f"registry={registry_url}"
147
+ )
148
+
149
+ return spec
150
+
151
+
152
+ async def _handle_api_mesh_event(event: Any, context: dict[str, Any]) -> None:
153
+ """
154
+ Handle a mesh event from the Rust core for API services.
155
+
156
+ Dispatches to appropriate handler based on event type.
157
+ """
158
+ event_type = event.event_type
159
+
160
+ if event_type == "agent_registered":
161
+ logger.info(f"API service registered with ID: {event.agent_id}")
162
+
163
+ elif event_type == "registration_failed":
164
+ logger.error(f"API service registration failed: {event.error}")
165
+
166
+ elif event_type == "dependency_available":
167
+ await _handle_api_dependency_change(
168
+ capability=event.capability,
169
+ endpoint=event.endpoint,
170
+ function_name=event.function_name,
171
+ agent_id=event.agent_id,
172
+ available=True,
173
+ context=context,
174
+ )
175
+
176
+ elif event_type == "dependency_changed":
177
+ await _handle_api_dependency_change(
178
+ capability=event.capability,
179
+ endpoint=event.endpoint,
180
+ function_name=event.function_name,
181
+ agent_id=event.agent_id,
182
+ available=True,
183
+ context=context,
184
+ )
185
+
186
+ elif event_type == "dependency_unavailable":
187
+ await _handle_api_dependency_change(
188
+ capability=event.capability,
189
+ endpoint=None,
190
+ function_name=None,
191
+ agent_id=None,
192
+ available=False,
193
+ context=context,
194
+ )
195
+
196
+ elif event_type == "llm_tools_updated":
197
+ # API services typically don't use LLM tools, but handle gracefully
198
+ logger.debug(f"LLM tools update for API service (ignored): {event.function_id}")
199
+
200
+ elif event_type == "health_check_due":
201
+ logger.debug("Health check due for API service (not implemented yet)")
202
+
203
+ elif event_type == "registry_disconnected":
204
+ logger.warning(f"Registry disconnected for API service: {event.reason}")
205
+
206
+ elif event_type == "shutdown":
207
+ logger.info("Rust core shutdown event received for API service")
208
+
209
+ else:
210
+ logger.debug(f"Unhandled event type for API service: {event_type}")
211
+
212
+
213
+ async def _handle_api_dependency_change(
214
+ capability: str,
215
+ endpoint: Optional[str],
216
+ function_name: Optional[str],
217
+ agent_id: Optional[str],
218
+ available: bool,
219
+ context: dict[str, Any],
220
+ ) -> None:
221
+ """
222
+ Handle dependency availability change for API services.
223
+
224
+ Updates route wrappers with new/changed/removed dependencies.
225
+ API services use route wrappers which have direct _mesh_update_dependency methods.
226
+ """
227
+ logger.info(
228
+ f"API dependency change: {capability} -> "
229
+ f"{'available' if available else 'unavailable'} "
230
+ f"at {endpoint}/{function_name}"
231
+ )
232
+
233
+ from ...engine.decorator_registry import DecoratorRegistry
234
+ from ...engine.unified_mcp_proxy import EnhancedUnifiedMCPProxy
235
+
236
+ route_wrappers = DecoratorRegistry.get_all_route_wrappers()
237
+
238
+ if not available:
239
+ # Dependency became unavailable - clear it from all route wrappers
240
+ for route_id, route_info in route_wrappers.items():
241
+ dependencies = route_info.get("dependencies", [])
242
+ wrapper = route_info.get("wrapper")
243
+
244
+ if not wrapper or not hasattr(wrapper, "_mesh_update_dependency"):
245
+ continue
246
+
247
+ # Find which dependency index(es) match this capability
248
+ for dep_index, dep_cap in enumerate(dependencies):
249
+ if dep_cap == capability:
250
+ # Set to None to indicate unavailable
251
+ wrapper._mesh_update_dependency(dep_index, None)
252
+ logger.info(
253
+ f"Cleared dependency '{capability}' at index {dep_index} "
254
+ f"for route '{route_id}'"
255
+ )
256
+ return
257
+
258
+ # Dependency is available - update all route wrappers that need it
259
+ for route_id, route_info in route_wrappers.items():
260
+ dependencies = route_info.get("dependencies", [])
261
+ wrapper = route_info.get("wrapper")
262
+
263
+ if not wrapper or not hasattr(wrapper, "_mesh_update_dependency"):
264
+ continue
265
+
266
+ # Find which dependency index(es) match this capability
267
+ for dep_index, dep_cap in enumerate(dependencies):
268
+ if dep_cap == capability:
269
+ # Check for self-dependency (rare for API services but handle it)
270
+ current_service_id = context.get("service_id") or context.get(
271
+ "agent_id"
272
+ )
273
+ if not current_service_id:
274
+ # Use config resolver for consistent env var handling
275
+ current_service_id = get_config_value("MCP_MESH_AGENT_ID")
276
+
277
+ is_self_dependency = (
278
+ current_service_id and agent_id and current_service_id == agent_id
279
+ )
280
+
281
+ if is_self_dependency:
282
+ # Self-dependency for API services - use SelfDependencyProxy
283
+ from ...engine.self_dependency_proxy import SelfDependencyProxy
284
+
285
+ # For API services, try to find the function in mesh tools
286
+ mesh_tools = DecoratorRegistry.get_mesh_tools()
287
+ wrapper_func = mesh_tools.get(function_name)
288
+
289
+ if wrapper_func:
290
+ proxy = SelfDependencyProxy(
291
+ wrapper_func.function, function_name
292
+ )
293
+ logger.debug(
294
+ f"Created SelfDependencyProxy for API route '{route_id}' "
295
+ f"dependency '{capability}'"
296
+ )
297
+ else:
298
+ # Fallback to HTTP proxy
299
+ proxy = EnhancedUnifiedMCPProxy(endpoint, function_name)
300
+ logger.debug(
301
+ f"Created EnhancedUnifiedMCPProxy (fallback) for API route "
302
+ f"'{route_id}' dependency '{capability}'"
303
+ )
304
+ else:
305
+ # Cross-service dependency - create HTTP proxy
306
+ proxy = EnhancedUnifiedMCPProxy(endpoint, function_name)
307
+ logger.debug(
308
+ f"Created EnhancedUnifiedMCPProxy for API route '{route_id}' "
309
+ f"dependency '{capability}' -> {endpoint}"
310
+ )
311
+
312
+ # Update the route wrapper
313
+ wrapper._mesh_update_dependency(dep_index, proxy)
314
+ logger.info(
315
+ f"Updated dependency '{capability}' at index {dep_index} "
316
+ f"for route '{route_id}' -> {endpoint}/{function_name}"
317
+ )
318
+
319
+
320
+ async def rust_api_heartbeat_task(heartbeat_config: dict[str, Any]) -> None:
321
+ """
322
+ Rust-backed heartbeat task for API services that runs in FastAPI lifespan.
323
+
324
+ This is a drop-in replacement for api_heartbeat_lifespan_task.
325
+ Instead of running Python heartbeat pipeline, it starts the Rust core
326
+ and listens for events.
327
+
328
+ Args:
329
+ heartbeat_config: Configuration containing service_id, interval, context
330
+ """
331
+ service_id = heartbeat_config.get("service_id", "unknown-api-service")
332
+ context = heartbeat_config.get("context", {})
333
+ standalone_mode = heartbeat_config.get("standalone_mode", False)
334
+
335
+ if standalone_mode:
336
+ logger.info(
337
+ f"Rust API heartbeat in standalone mode for service '{service_id}' "
338
+ "(no registry communication)"
339
+ )
340
+ return
341
+
342
+ try:
343
+ core = _get_rust_core()
344
+ except ImportError as e:
345
+ logger.error(
346
+ f"Rust core not available for API service '{service_id}': {e}. "
347
+ "The mcp_mesh_core module must be built and installed."
348
+ )
349
+ raise RuntimeError(
350
+ f"Rust core (mcp_mesh_core) is required but not available: {e}"
351
+ ) from e
352
+
353
+ logger.info(f"Starting Rust-backed heartbeat for API service '{service_id}'")
354
+
355
+ handle = None
356
+ try:
357
+ # Build AgentSpec from API service context, passing service_id explicitly
358
+ spec = _build_api_agent_spec(context, service_id=service_id)
359
+
360
+ # Start Rust core runtime
361
+ handle = core.start_agent(spec)
362
+ logger.info(f"Rust core started for API service '{service_id}'")
363
+
364
+ # Event loop - process events from Rust core
365
+ while True:
366
+ # Check for Python shutdown signal
367
+ try:
368
+ from ...shared.simple_shutdown import should_stop_heartbeat
369
+
370
+ if should_stop_heartbeat():
371
+ logger.info(
372
+ f"Stopping Rust API heartbeat for service '{service_id}' due to shutdown"
373
+ )
374
+ handle.shutdown()
375
+ break
376
+ except ImportError:
377
+ pass
378
+
379
+ try:
380
+ # Wait for next event from Rust core with timeout
381
+ # Timeout allows periodic shutdown checks
382
+ try:
383
+ event = await asyncio.wait_for(handle.next_event(), timeout=1.0)
384
+ except TimeoutError:
385
+ # No event in 1 second, loop back to check shutdown signal
386
+ continue
387
+
388
+ if event.event_type == "shutdown":
389
+ logger.info(f"Rust core shutdown for API service '{service_id}'")
390
+ break
391
+
392
+ # Handle the event
393
+ await _handle_api_mesh_event(event, context)
394
+
395
+ except Exception as e:
396
+ logger.error(f"Error handling Rust event for API service: {e}")
397
+ # Continue processing events
398
+
399
+ except asyncio.CancelledError:
400
+ logger.info(f"Rust API heartbeat task cancelled for service '{service_id}'")
401
+ raise
402
+ except Exception as e:
403
+ logger.error(f"Rust API heartbeat failed for service '{service_id}': {e}")
404
+ raise
405
+ finally:
406
+ # Always ensure graceful shutdown of Rust core to prevent daemon thread issues
407
+ # This is critical: without shutdown(), Rust background threads may try to
408
+ # write to stdout via tracing after Python's stdout is finalized
409
+ if handle is not None:
410
+ try:
411
+ handle.shutdown()
412
+ # Give Rust core a moment to clean up before Python exits
413
+ # Use time.sleep as fallback if asyncio is shutting down
414
+ try:
415
+ await asyncio.sleep(0.2)
416
+ except (asyncio.CancelledError, RuntimeError):
417
+ # Event loop might be shutting down, use blocking sleep
418
+ import time
419
+
420
+ time.sleep(0.2)
421
+ logger.debug(
422
+ f"Rust core shutdown complete for API service '{service_id}'"
423
+ )
424
+ except Exception as e:
425
+ 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(), # 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
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]}")