mcp-mesh 0.7.21__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/dependency_injector.py +13 -15
  3. _mcp_mesh/engine/http_wrapper.py +69 -10
  4. _mcp_mesh/engine/mesh_llm_agent.py +29 -10
  5. _mcp_mesh/engine/mesh_llm_agent_injector.py +77 -41
  6. _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
  7. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
  8. _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
  9. _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
  10. _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
  11. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
  12. _mcp_mesh/engine/response_parser.py +61 -15
  13. _mcp_mesh/engine/signature_analyzer.py +58 -68
  14. _mcp_mesh/engine/unified_mcp_proxy.py +19 -35
  15. _mcp_mesh/pipeline/__init__.py +9 -20
  16. _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
  17. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
  18. _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +429 -0
  19. _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
  20. _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
  21. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
  22. _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
  23. _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
  24. _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
  25. _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +710 -0
  26. _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
  27. _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
  28. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +31 -8
  29. _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
  30. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +23 -11
  31. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
  32. _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
  33. _mcp_mesh/reload.py +1 -3
  34. _mcp_mesh/shared/__init__.py +2 -8
  35. _mcp_mesh/shared/config_resolver.py +124 -80
  36. _mcp_mesh/shared/defaults.py +89 -14
  37. _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
  38. _mcp_mesh/shared/host_resolver.py +8 -46
  39. _mcp_mesh/shared/server_discovery.py +115 -86
  40. _mcp_mesh/shared/simple_shutdown.py +44 -86
  41. _mcp_mesh/tracing/execution_tracer.py +2 -6
  42. _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
  43. _mcp_mesh/tracing/trace_context_helper.py +3 -13
  44. _mcp_mesh/tracing/utils.py +29 -15
  45. _mcp_mesh/utils/fastmcp_schema_extractor.py +5 -4
  46. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0.dist-info}/METADATA +7 -5
  47. mcp_mesh-0.8.0.dist-info/RECORD +85 -0
  48. mesh/__init__.py +12 -1
  49. mesh/decorators.py +248 -33
  50. mesh/helpers.py +52 -0
  51. mesh/types.py +40 -13
  52. _mcp_mesh/generated/.openapi-generator/FILES +0 -50
  53. _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
  54. _mcp_mesh/generated/.openapi-generator-ignore +0 -15
  55. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
  56. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
  57. _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
  58. _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
  59. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
  60. _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
  61. _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
  62. _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
  63. _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
  64. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
  65. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
  66. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
  67. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
  68. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
  69. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
  70. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
  71. _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
  72. _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
  73. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
  74. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
  75. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
  76. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
  77. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
  78. _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
  79. _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
  80. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
  81. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
  82. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
  83. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
  84. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
  85. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
  86. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
  87. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
  88. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
  89. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
  90. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
  91. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
  92. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
  93. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
  94. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
  95. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
  96. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
  97. _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
  98. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
  99. _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
  100. _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
  101. _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
  102. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
  103. _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
  104. _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
  105. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
  106. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
  107. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
  108. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
  109. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
  110. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
  111. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
  112. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
  113. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
  114. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
  115. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
  116. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
  117. _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
  118. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
  119. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
  120. _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
  121. _mcp_mesh/shared/registry_client_wrapper.py +0 -515
  122. mcp_mesh-0.7.21.dist-info/RECORD +0 -152
  123. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0.dist-info}/WHEEL +0 -0
  124. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,710 @@
1
+ """
2
+ Rust-backed heartbeat implementation.
3
+
4
+ Replaces the Python heartbeat loop 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
12
+ - LLM tools updates
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")
35
+ except ImportError as e:
36
+ logger.warning(f"Rust core not available: {e}")
37
+ raise
38
+ return _rust_core
39
+
40
+
41
+ def _build_agent_spec(context: dict[str, Any]) -> Any:
42
+ """
43
+ Build AgentSpec from Python context.
44
+
45
+ Converts the Python decorator registry state into a Rust AgentSpec.
46
+ """
47
+ core = _get_rust_core()
48
+
49
+ # Get agent config from context
50
+ agent_config = context.get("agent_config", {})
51
+ agent_id = context.get("agent_id", "unknown-agent")
52
+
53
+ # Get registry URL
54
+ from ...shared.config_resolver import get_config_value
55
+
56
+ # Default is handled by Rust core
57
+ registry_url = get_config_value(
58
+ "MCP_MESH_REGISTRY_URL",
59
+ override=agent_config.get("registry_url"),
60
+ )
61
+
62
+ # Get heartbeat interval
63
+ from ...shared.defaults import MeshDefaults
64
+
65
+ heartbeat_interval = int(
66
+ get_config_value(
67
+ "MCP_MESH_HEALTH_INTERVAL",
68
+ override=agent_config.get("health_interval"),
69
+ default=MeshDefaults.HEALTH_INTERVAL,
70
+ )
71
+ )
72
+
73
+ # Get HTTP config
74
+ http_host = agent_config.get("http_host", "localhost")
75
+ http_port = agent_config.get("http_port", 0)
76
+
77
+ # If port=0 (auto-assign), check for detected port from server discovery
78
+ if http_port == 0:
79
+ existing_server = context.get("existing_server")
80
+ if existing_server:
81
+ detected_port = existing_server.get("port", 0)
82
+ if detected_port > 0:
83
+ logger.info(
84
+ f"Using detected port {detected_port} (agent_config had port=0)"
85
+ )
86
+ http_port = detected_port
87
+
88
+ namespace = agent_config.get("namespace", "default")
89
+ version = agent_config.get("version", "1.0.0")
90
+ description = agent_config.get("description", "")
91
+
92
+ # Build tool specs from decorator registry
93
+ from ...engine.decorator_registry import DecoratorRegistry
94
+
95
+ tools = []
96
+ mesh_tools = DecoratorRegistry.get_mesh_tools()
97
+ mesh_llm_agents = DecoratorRegistry.get_mesh_llm_agents()
98
+
99
+ # Import FastMCP schema extractor for input schema extraction
100
+ from ...utils.fastmcp_schema_extractor import FastMCPSchemaExtractor
101
+
102
+ # Get FastMCP server info from context (set by fastmcp-server-discovery step)
103
+ # Convert to dict format expected by extract_from_fastmcp_servers
104
+ fastmcp_server_info = context.get("fastmcp_server_info", [])
105
+ fastmcp_servers = {}
106
+ for server_info in fastmcp_server_info:
107
+ server_name = server_info.get("server_name", "unknown")
108
+ fastmcp_servers[server_name] = server_info
109
+ logger.debug(
110
+ f"FastMCP servers for schema extraction: {list(fastmcp_servers.keys())}"
111
+ )
112
+
113
+ for tool_name, decorated_func in mesh_tools.items():
114
+ tool_metadata = decorated_func.metadata or {}
115
+ current_function = decorated_func.function
116
+
117
+ # Build dependency specs
118
+ deps = []
119
+ for dep_info in tool_metadata.get("dependencies", []):
120
+ # Serialize tags to JSON to support nested arrays for OR alternatives
121
+ # e.g., ["addition", ["python", "typescript"]] -> addition AND (python OR typescript)
122
+ tags_json = json.dumps(dep_info.get("tags", []))
123
+ dep_spec = core.DependencySpec(
124
+ capability=dep_info.get("capability", ""),
125
+ tags=tags_json,
126
+ version=dep_info.get("version"),
127
+ )
128
+ deps.append(dep_spec)
129
+
130
+ # Extract input schema from FastMCP tool (like heartbeat_preparation.py)
131
+ # This is critical for LLM tool filtering - registry requires inputSchema
132
+ input_schema = tool_metadata.get("input_schema")
133
+ if input_schema is None:
134
+ # Primary method: Extract from FastMCP server tool managers
135
+ input_schema = FastMCPSchemaExtractor.extract_from_fastmcp_servers(
136
+ current_function, fastmcp_servers
137
+ )
138
+ if input_schema:
139
+ logger.debug(
140
+ f"📋 Extracted inputSchema for {tool_name} from FastMCP servers: {list(input_schema.get('properties', {}).keys())}"
141
+ )
142
+ else:
143
+ # Fallback: Try direct _fastmcp_tool attribute
144
+ input_schema = FastMCPSchemaExtractor.extract_input_schema(
145
+ current_function
146
+ )
147
+ if input_schema:
148
+ logger.debug(
149
+ f"📋 Extracted inputSchema for {tool_name} from _fastmcp_tool: {list(input_schema.get('properties', {}).keys())}"
150
+ )
151
+ else:
152
+ logger.warning(f"⚠️ No inputSchema found for {tool_name}")
153
+ input_schema_json = json.dumps(input_schema) if input_schema else None
154
+
155
+ # Get LLM filter/provider from mesh_llm_agents by matching function name
156
+ # (The @mesh.llm decorator stores these, not @mesh.tool)
157
+ llm_filter_json = None
158
+ llm_provider_json = None
159
+
160
+ func_name = decorated_func.function.__name__
161
+ for llm_agent_id, llm_metadata in mesh_llm_agents.items():
162
+ if llm_metadata.function.__name__ == func_name:
163
+ # Found matching LLM agent - extract filter config
164
+ raw_filter = llm_metadata.config.get("filter")
165
+ filter_mode = llm_metadata.config.get("filter_mode", "all")
166
+
167
+ # Normalize filter to array format
168
+ if raw_filter is None:
169
+ normalized_filter = []
170
+ elif isinstance(raw_filter, list):
171
+ normalized_filter = raw_filter
172
+ elif isinstance(raw_filter, dict):
173
+ normalized_filter = [raw_filter]
174
+ elif isinstance(raw_filter, str):
175
+ normalized_filter = [raw_filter] if raw_filter else []
176
+ else:
177
+ normalized_filter = []
178
+
179
+ if normalized_filter:
180
+ llm_filter_data = {
181
+ "filter": normalized_filter,
182
+ "filter_mode": filter_mode,
183
+ }
184
+ llm_filter_json = json.dumps(llm_filter_data)
185
+ logger.debug(
186
+ f"🤖 Extracted llm_filter for {func_name}: {len(normalized_filter)} filters, mode={filter_mode}"
187
+ )
188
+
189
+ # Extract llm_provider (v0.6.1: LLM Mesh Delegation)
190
+ provider = llm_metadata.config.get("provider")
191
+ if isinstance(provider, dict):
192
+ llm_provider_data = {
193
+ "capability": provider.get("capability", "llm"),
194
+ "tags": provider.get("tags", []),
195
+ "version": provider.get("version", ""),
196
+ "namespace": provider.get("namespace", "default"),
197
+ }
198
+ llm_provider_json = json.dumps(llm_provider_data)
199
+ logger.debug(
200
+ f"🔌 Extracted llm_provider for {func_name}: {llm_provider_data}"
201
+ )
202
+ break
203
+
204
+ tool_spec = core.ToolSpec(
205
+ function_name=tool_name,
206
+ capability=tool_metadata.get("capability", tool_name),
207
+ version=tool_metadata.get("version", "1.0.0"),
208
+ description=tool_metadata.get("description", ""),
209
+ tags=tool_metadata.get("tags", []),
210
+ dependencies=deps if deps else None,
211
+ input_schema=input_schema_json,
212
+ llm_filter=llm_filter_json,
213
+ llm_provider=llm_provider_json,
214
+ )
215
+ tools.append(tool_spec)
216
+ logger.info(
217
+ f"📤 Tool '{tool_name}': llm_filter={llm_filter_json}, llm_provider={llm_provider_json}"
218
+ )
219
+
220
+ # Build LLM agent specs
221
+ llm_agents = []
222
+
223
+ for func_id, llm_metadata in mesh_llm_agents.items():
224
+ # LLMAgentMetadata is a dataclass with .config dict
225
+ config = llm_metadata.config if hasattr(llm_metadata, "config") else {}
226
+
227
+ provider = config.get("provider", {})
228
+ provider_json = json.dumps(provider) if provider else "{}"
229
+
230
+ filter_spec = config.get("filter")
231
+ filter_json = json.dumps(filter_spec) if filter_spec else None
232
+
233
+ llm_spec = core.LlmAgentSpec(
234
+ function_id=func_id,
235
+ provider=provider_json,
236
+ filter=filter_json,
237
+ filter_mode=config.get("filter_mode", "all"),
238
+ max_iterations=config.get("max_iterations", 1),
239
+ )
240
+ llm_agents.append(llm_spec)
241
+
242
+ # Create AgentSpec
243
+ spec = core.AgentSpec(
244
+ name=agent_id,
245
+ registry_url=registry_url,
246
+ version=version,
247
+ description=description,
248
+ http_port=http_port,
249
+ http_host=http_host,
250
+ namespace=namespace,
251
+ tools=tools if tools else None,
252
+ llm_agents=llm_agents if llm_agents else None,
253
+ heartbeat_interval=heartbeat_interval,
254
+ )
255
+
256
+ logger.info(
257
+ f"Built AgentSpec: name={agent_id}, tools={len(tools)}, "
258
+ f"llm_agents={len(llm_agents)}, registry={registry_url}"
259
+ )
260
+
261
+ return spec
262
+
263
+
264
+ async def _handle_mesh_event(event: Any, context: dict[str, Any]) -> None:
265
+ """
266
+ Handle a mesh event from the Rust core.
267
+
268
+ Dispatches to appropriate handler based on event type.
269
+ """
270
+ event_type = event.event_type
271
+
272
+ if event_type == "agent_registered":
273
+ logger.info(f"Agent registered with ID: {event.agent_id}")
274
+
275
+ elif event_type == "registration_failed":
276
+ logger.error(f"Agent registration failed: {event.error}")
277
+
278
+ elif event_type == "dependency_available":
279
+ await _handle_dependency_change(
280
+ capability=event.capability,
281
+ endpoint=event.endpoint,
282
+ function_name=event.function_name,
283
+ agent_id=event.agent_id,
284
+ available=True,
285
+ context=context,
286
+ requesting_function=getattr(event, "requesting_function", None),
287
+ dep_index=getattr(event, "dep_index", None),
288
+ )
289
+
290
+ elif event_type == "dependency_changed":
291
+ await _handle_dependency_change(
292
+ capability=event.capability,
293
+ endpoint=event.endpoint,
294
+ function_name=event.function_name,
295
+ agent_id=event.agent_id,
296
+ available=True,
297
+ context=context,
298
+ requesting_function=getattr(event, "requesting_function", None),
299
+ dep_index=getattr(event, "dep_index", None),
300
+ )
301
+
302
+ elif event_type == "dependency_unavailable":
303
+ await _handle_dependency_change(
304
+ capability=event.capability,
305
+ endpoint=None,
306
+ function_name=None,
307
+ agent_id=None,
308
+ available=False,
309
+ context=context,
310
+ requesting_function=getattr(event, "requesting_function", None),
311
+ dep_index=getattr(event, "dep_index", None),
312
+ )
313
+
314
+ elif event_type == "llm_tools_updated":
315
+ if event.tools is None:
316
+ logger.warning(
317
+ f"llm_tools_updated event for '{event.function_id}' has no tools data, skipping"
318
+ )
319
+ else:
320
+ await _handle_llm_tools_update(
321
+ function_id=event.function_id,
322
+ tools=event.tools,
323
+ context=context,
324
+ )
325
+
326
+ elif event_type == "llm_provider_available":
327
+ if event.provider_info is None:
328
+ logger.warning(
329
+ "llm_provider_available event has no provider_info, skipping"
330
+ )
331
+ else:
332
+ await _handle_llm_provider_update(
333
+ provider_info=event.provider_info,
334
+ context=context,
335
+ )
336
+
337
+ elif event_type == "health_check_due":
338
+ # Python can perform health check and report back
339
+ logger.debug("Health check due (not implemented yet)")
340
+
341
+ elif event_type == "registry_disconnected":
342
+ logger.warning(f"Registry disconnected: {event.reason}")
343
+
344
+ elif event_type == "shutdown":
345
+ logger.info("Rust core shutdown event received")
346
+
347
+ else:
348
+ logger.debug(f"Unhandled event type: {event_type}")
349
+
350
+
351
+ async def _handle_dependency_change(
352
+ capability: str,
353
+ endpoint: str | None,
354
+ function_name: str | None,
355
+ agent_id: str | None,
356
+ available: bool,
357
+ context: dict[str, Any],
358
+ requesting_function: str | None = None,
359
+ dep_index: int | None = None,
360
+ ) -> None:
361
+ """
362
+ Handle dependency availability change.
363
+
364
+ Updates the DI system with new/changed/removed dependencies.
365
+
366
+ If requesting_function and dep_index are provided (new behavior from Rust core),
367
+ we can directly register/unregister at the exact position. Otherwise, we fall
368
+ back to capability-based matching (backward compatibility).
369
+ """
370
+ logger.info(
371
+ f"Dependency change: {capability} -> "
372
+ f"{'available' if available else 'unavailable'} "
373
+ f"at {endpoint}/{function_name}"
374
+ + (
375
+ f" (func: {requesting_function}, idx: {dep_index})"
376
+ if requesting_function
377
+ else ""
378
+ )
379
+ )
380
+
381
+ # Import DI components
382
+ from ...engine.decorator_registry import DecoratorRegistry
383
+ from ...engine.dependency_injector import get_global_injector
384
+ from ...engine.unified_mcp_proxy import EnhancedUnifiedMCPProxy
385
+ from ...shared.config_resolver import get_config_value
386
+
387
+ injector = get_global_injector()
388
+ mesh_tools = DecoratorRegistry.get_mesh_tools()
389
+
390
+ # If we have position info, use it directly (new behavior)
391
+ if requesting_function is not None and dep_index is not None:
392
+ # Build dep_key - requesting_function is the function_name from registry
393
+ # We need to find the corresponding func_id
394
+ func_id = requesting_function
395
+ for tool_name, decorated_func in mesh_tools.items():
396
+ if tool_name == requesting_function:
397
+ func = decorated_func.function
398
+ func_id = f"{func.__module__}.{func.__qualname__}"
399
+ break
400
+
401
+ dep_key = f"{func_id}:dep_{dep_index}"
402
+
403
+ if not available:
404
+ await injector.unregister_dependency(dep_key)
405
+ logger.info(f"Unregistered dependency: {dep_key}")
406
+ return
407
+
408
+ # Get kwargs from the tool metadata
409
+ kwargs_config = {}
410
+ for tool_name, decorated_func in mesh_tools.items():
411
+ if tool_name == requesting_function:
412
+ tool_metadata = decorated_func.metadata or {}
413
+ dependencies = tool_metadata.get("dependencies", [])
414
+ if dep_index < len(dependencies):
415
+ kwargs_config = dependencies[dep_index].get("kwargs", {})
416
+ break
417
+
418
+ # Check for self-dependency
419
+ current_agent_id = None
420
+ try:
421
+ config = DecoratorRegistry.get_resolved_agent_config()
422
+ current_agent_id = config.get("agent_id")
423
+ except Exception:
424
+ # Use config resolver for consistent env var handling
425
+ current_agent_id = get_config_value("MCP_MESH_AGENT_ID")
426
+
427
+ is_self_dependency = (
428
+ current_agent_id and agent_id and current_agent_id == agent_id
429
+ )
430
+
431
+ if is_self_dependency:
432
+ from ...engine.self_dependency_proxy import SelfDependencyProxy
433
+
434
+ wrapper_func = mesh_tools.get(function_name)
435
+ if wrapper_func:
436
+ proxy = SelfDependencyProxy(wrapper_func.function, function_name)
437
+ logger.debug(f"Created SelfDependencyProxy for {capability}")
438
+ else:
439
+ proxy = EnhancedUnifiedMCPProxy(endpoint, function_name)
440
+ logger.debug(
441
+ f"Created EnhancedUnifiedMCPProxy (fallback) for {capability}"
442
+ )
443
+ else:
444
+ proxy = EnhancedUnifiedMCPProxy(
445
+ endpoint, function_name, kwargs_config=kwargs_config
446
+ )
447
+ logger.debug(
448
+ f"Created EnhancedUnifiedMCPProxy for {capability} -> {endpoint}"
449
+ )
450
+
451
+ await injector.register_dependency(dep_key, proxy)
452
+ logger.info(f"Registered dependency: {dep_key}")
453
+ return
454
+
455
+ # Fallback: capability-based matching (backward compatibility)
456
+ if not available:
457
+ # Dependency became unavailable - unregister it
458
+ if hasattr(injector, "_dependencies"):
459
+ keys_to_remove = [
460
+ key for key in injector._dependencies.keys() if capability in key
461
+ ]
462
+ for dep_key in keys_to_remove:
463
+ await injector.unregister_dependency(dep_key)
464
+ logger.info(f"Unregistered dependency: {dep_key}")
465
+ return
466
+
467
+ # Dependency is available - create proxy and register
468
+ # Map tool names to func_ids
469
+ tool_name_to_func_id = {}
470
+ for tool_name, decorated_func in mesh_tools.items():
471
+ func = decorated_func.function
472
+ func_id = f"{func.__module__}.{func.__qualname__}"
473
+ tool_name_to_func_id[tool_name] = func_id
474
+
475
+ # Find which functions depend on this capability
476
+ for tool_name, decorated_func in mesh_tools.items():
477
+ tool_metadata = decorated_func.metadata or {}
478
+ dependencies = tool_metadata.get("dependencies", [])
479
+
480
+ for idx, dep_info in enumerate(dependencies):
481
+ if dep_info.get("capability") == capability:
482
+ func_id = tool_name_to_func_id.get(tool_name, tool_name)
483
+ dep_key = f"{func_id}:dep_{idx}"
484
+
485
+ # Check for self-dependency
486
+ current_agent_id = None
487
+ try:
488
+ config = DecoratorRegistry.get_resolved_agent_config()
489
+ current_agent_id = config.get("agent_id")
490
+ except Exception:
491
+ # Use config resolver for consistent env var handling
492
+ current_agent_id = get_config_value("MCP_MESH_AGENT_ID")
493
+
494
+ is_self_dependency = (
495
+ current_agent_id and agent_id and current_agent_id == agent_id
496
+ )
497
+
498
+ if is_self_dependency:
499
+ # Create self-dependency proxy
500
+ from ...engine.self_dependency_proxy import SelfDependencyProxy
501
+
502
+ wrapper_func = mesh_tools.get(function_name)
503
+ if wrapper_func:
504
+ proxy = SelfDependencyProxy(
505
+ wrapper_func.function, function_name
506
+ )
507
+ logger.debug(f"Created SelfDependencyProxy for {capability}")
508
+ else:
509
+ # Fallback to HTTP proxy
510
+ proxy = EnhancedUnifiedMCPProxy(endpoint, function_name)
511
+ logger.debug(
512
+ f"Created EnhancedUnifiedMCPProxy (fallback) for {capability}"
513
+ )
514
+ else:
515
+ # Create cross-service proxy
516
+ kwargs_config = dep_info.get("kwargs", {})
517
+ proxy = EnhancedUnifiedMCPProxy(
518
+ endpoint, function_name, kwargs_config=kwargs_config
519
+ )
520
+ logger.debug(
521
+ f"Created EnhancedUnifiedMCPProxy for {capability} -> {endpoint}"
522
+ )
523
+
524
+ await injector.register_dependency(dep_key, proxy)
525
+ logger.info(f"Registered dependency: {dep_key}")
526
+
527
+
528
+ async def _handle_llm_tools_update(
529
+ function_id: str,
530
+ tools: list,
531
+ context: dict[str, Any],
532
+ ) -> None:
533
+ """
534
+ Handle LLM tools update event.
535
+
536
+ Updates the LLM tools registry for the given function via the DI system.
537
+ """
538
+ logger.info(f"LLM tools update for {function_id}: {len(tools)} tools")
539
+
540
+ # Import injector
541
+ from ...engine.dependency_injector import get_global_injector
542
+
543
+ # Convert tools to the expected format (using "name" for OpenAPI contract)
544
+ tool_list = []
545
+ for tool in tools:
546
+ tool_info = {
547
+ "name": tool.function_name, # OpenAPI contract uses "name" not "function_name"
548
+ "capability": tool.capability,
549
+ "endpoint": tool.endpoint,
550
+ "agent_id": tool.agent_id,
551
+ "input_schema": (
552
+ json.loads(tool.input_schema) if tool.input_schema else None
553
+ ),
554
+ }
555
+ tool_list.append(tool_info)
556
+
557
+ # Update LLM tools via the dependency injector
558
+ injector = get_global_injector()
559
+ llm_tools = {function_id: tool_list}
560
+ injector.update_llm_tools(llm_tools)
561
+ logger.debug(f"Updated {len(tool_list)} LLM tools for {function_id}")
562
+
563
+
564
+ async def _handle_llm_provider_update(
565
+ provider_info: Any,
566
+ context: dict[str, Any],
567
+ ) -> None:
568
+ """
569
+ Handle LLM provider resolution event.
570
+
571
+ Updates the LLM provider for the given function via the DI system.
572
+ """
573
+ function_id = provider_info.function_id
574
+ logger.info(
575
+ f"LLM provider resolved for {function_id}: "
576
+ f"{provider_info.function_name} at {provider_info.endpoint}"
577
+ )
578
+
579
+ # Import injector
580
+ from ...engine.dependency_injector import get_global_injector
581
+ from ...engine.unified_mcp_proxy import EnhancedUnifiedMCPProxy
582
+
583
+ # Create proxy for the LLM provider
584
+ proxy = EnhancedUnifiedMCPProxy(
585
+ provider_info.endpoint,
586
+ provider_info.function_name,
587
+ )
588
+
589
+ # Register as the LLM provider for this function
590
+ injector = get_global_injector()
591
+ provider_key = f"{function_id}:llm_provider"
592
+ await injector.register_dependency(provider_key, proxy)
593
+
594
+ # Also store provider metadata for the mesh agent to use (using "name" for OpenAPI contract)
595
+ llm_providers = {
596
+ function_id: {
597
+ "agent_id": provider_info.agent_id,
598
+ "endpoint": provider_info.endpoint,
599
+ "name": provider_info.function_name, # OpenAPI contract uses "name"
600
+ "model": provider_info.model,
601
+ }
602
+ }
603
+ injector.process_llm_providers(llm_providers)
604
+ logger.debug(f"Registered LLM provider for {function_id}")
605
+
606
+
607
+ async def rust_heartbeat_task(heartbeat_config: dict[str, Any]) -> None:
608
+ """
609
+ Rust-backed heartbeat task that runs in FastAPI lifespan.
610
+
611
+ This is a drop-in replacement for heartbeat_lifespan_task.
612
+ Instead of running Python heartbeat loop, it starts the Rust core
613
+ and listens for events.
614
+
615
+ Args:
616
+ heartbeat_config: Configuration containing agent_id, interval, context
617
+ """
618
+ agent_id = heartbeat_config["agent_id"]
619
+ context = heartbeat_config["context"]
620
+ standalone_mode = heartbeat_config.get("standalone_mode", False)
621
+
622
+ if standalone_mode:
623
+ logger.info(
624
+ f"Rust heartbeat in standalone mode for agent '{agent_id}' "
625
+ "(no registry communication)"
626
+ )
627
+ return
628
+
629
+ try:
630
+ core = _get_rust_core()
631
+ except ImportError as e:
632
+ logger.error(
633
+ f"Rust core not available for agent '{agent_id}': {e}. "
634
+ "The mcp_mesh_core module must be built and installed."
635
+ )
636
+ raise RuntimeError(
637
+ f"Rust core (mcp_mesh_core) is required but not available: {e}"
638
+ ) from e
639
+
640
+ logger.info(f"Starting Rust-backed heartbeat for agent '{agent_id}'")
641
+
642
+ handle = None
643
+ try:
644
+ # Build AgentSpec from context
645
+ spec = _build_agent_spec(context)
646
+
647
+ # Start Rust core runtime
648
+ handle = core.start_agent(spec)
649
+ logger.info(f"Rust core started for agent '{agent_id}'")
650
+
651
+ # Event loop - process events from Rust core
652
+ while True:
653
+ # Check for Python shutdown signal
654
+ try:
655
+ from ...shared.simple_shutdown import should_stop_heartbeat
656
+
657
+ if should_stop_heartbeat():
658
+ logger.info(
659
+ f"Stopping Rust heartbeat for agent '{agent_id}' due to shutdown"
660
+ )
661
+ handle.shutdown()
662
+ break
663
+ except ImportError:
664
+ pass
665
+
666
+ try:
667
+ # Wait for next event from Rust core with timeout
668
+ # Timeout allows periodic shutdown checks
669
+ try:
670
+ event = await asyncio.wait_for(handle.next_event(), timeout=1.0)
671
+ except TimeoutError:
672
+ # No event in 1 second, loop back to check shutdown signal
673
+ continue
674
+
675
+ if event.event_type == "shutdown":
676
+ logger.info(f"Rust core shutdown for agent '{agent_id}'")
677
+ break
678
+
679
+ # Handle the event
680
+ await _handle_mesh_event(event, context)
681
+
682
+ except Exception as e:
683
+ logger.error(f"Error handling Rust event: {e}")
684
+ # Continue processing events
685
+
686
+ except asyncio.CancelledError:
687
+ logger.info(f"Rust heartbeat task cancelled for agent '{agent_id}'")
688
+ raise
689
+ except Exception as e:
690
+ logger.error(f"Rust heartbeat failed for agent '{agent_id}': {e}")
691
+ raise
692
+ finally:
693
+ # Always ensure graceful shutdown of Rust core to prevent daemon thread issues
694
+ # This is critical: without shutdown(), Rust background threads may try to
695
+ # write to stdout via tracing after Python's stdout is finalized
696
+ if handle is not None:
697
+ try:
698
+ handle.shutdown()
699
+ # Give Rust core a moment to clean up before Python exits
700
+ # Use time.sleep as fallback if asyncio is shutting down
701
+ try:
702
+ await asyncio.sleep(0.2)
703
+ except (asyncio.CancelledError, RuntimeError):
704
+ # Event loop might be shutting down, use blocking sleep
705
+ import time
706
+
707
+ time.sleep(0.2)
708
+ logger.debug(f"Rust core shutdown complete for agent '{agent_id}'")
709
+ except Exception as e:
710
+ logger.warning(f"Error during Rust core shutdown: {e}")