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,695 @@
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
+ namespace = agent_config.get("namespace", "default")
77
+ version = agent_config.get("version", "1.0.0")
78
+ description = agent_config.get("description", "")
79
+
80
+ # Build tool specs from decorator registry
81
+ from ...engine.decorator_registry import DecoratorRegistry
82
+
83
+ tools = []
84
+ mesh_tools = DecoratorRegistry.get_mesh_tools()
85
+ mesh_llm_agents = DecoratorRegistry.get_mesh_llm_agents()
86
+
87
+ # Import FastMCP schema extractor for input schema extraction
88
+ from ...utils.fastmcp_schema_extractor import FastMCPSchemaExtractor
89
+
90
+ # Get FastMCP server info from context (set by fastmcp-server-discovery step)
91
+ # Convert to dict format expected by extract_from_fastmcp_servers
92
+ fastmcp_server_info = context.get("fastmcp_server_info", [])
93
+ fastmcp_servers = {}
94
+ for server_info in fastmcp_server_info:
95
+ server_name = server_info.get("server_name", "unknown")
96
+ fastmcp_servers[server_name] = server_info
97
+ logger.debug(
98
+ f"FastMCP servers for schema extraction: {list(fastmcp_servers.keys())}"
99
+ )
100
+
101
+ for tool_name, decorated_func in mesh_tools.items():
102
+ tool_metadata = decorated_func.metadata or {}
103
+ current_function = decorated_func.function
104
+
105
+ # Build dependency specs
106
+ deps = []
107
+ for dep_info in tool_metadata.get("dependencies", []):
108
+ dep_spec = core.DependencySpec(
109
+ capability=dep_info.get("capability", ""),
110
+ tags=dep_info.get("tags", []),
111
+ version=dep_info.get("version"),
112
+ )
113
+ deps.append(dep_spec)
114
+
115
+ # Extract input schema from FastMCP tool (like heartbeat_preparation.py)
116
+ # This is critical for LLM tool filtering - registry requires inputSchema
117
+ input_schema = tool_metadata.get("input_schema")
118
+ if input_schema is None:
119
+ # Primary method: Extract from FastMCP server tool managers
120
+ input_schema = FastMCPSchemaExtractor.extract_from_fastmcp_servers(
121
+ current_function, fastmcp_servers
122
+ )
123
+ if input_schema:
124
+ logger.debug(
125
+ f"📋 Extracted inputSchema for {tool_name} from FastMCP servers: {list(input_schema.get('properties', {}).keys())}"
126
+ )
127
+ else:
128
+ # Fallback: Try direct _fastmcp_tool attribute
129
+ input_schema = FastMCPSchemaExtractor.extract_input_schema(
130
+ current_function
131
+ )
132
+ if input_schema:
133
+ logger.debug(
134
+ f"📋 Extracted inputSchema for {tool_name} from _fastmcp_tool: {list(input_schema.get('properties', {}).keys())}"
135
+ )
136
+ else:
137
+ logger.warning(f"⚠️ No inputSchema found for {tool_name}")
138
+ input_schema_json = json.dumps(input_schema) if input_schema else None
139
+
140
+ # Get LLM filter/provider from mesh_llm_agents by matching function name
141
+ # (The @mesh.llm decorator stores these, not @mesh.tool)
142
+ llm_filter_json = None
143
+ llm_provider_json = None
144
+
145
+ func_name = decorated_func.function.__name__
146
+ for llm_agent_id, llm_metadata in mesh_llm_agents.items():
147
+ if llm_metadata.function.__name__ == func_name:
148
+ # Found matching LLM agent - extract filter config
149
+ raw_filter = llm_metadata.config.get("filter")
150
+ filter_mode = llm_metadata.config.get("filter_mode", "all")
151
+
152
+ # Normalize filter to array format
153
+ if raw_filter is None:
154
+ normalized_filter = []
155
+ elif isinstance(raw_filter, list):
156
+ normalized_filter = raw_filter
157
+ elif isinstance(raw_filter, dict):
158
+ normalized_filter = [raw_filter]
159
+ elif isinstance(raw_filter, str):
160
+ normalized_filter = [raw_filter] if raw_filter else []
161
+ else:
162
+ normalized_filter = []
163
+
164
+ if normalized_filter:
165
+ llm_filter_data = {
166
+ "filter": normalized_filter,
167
+ "filter_mode": filter_mode,
168
+ }
169
+ llm_filter_json = json.dumps(llm_filter_data)
170
+ logger.debug(
171
+ f"🤖 Extracted llm_filter for {func_name}: {len(normalized_filter)} filters, mode={filter_mode}"
172
+ )
173
+
174
+ # Extract llm_provider (v0.6.1: LLM Mesh Delegation)
175
+ provider = llm_metadata.config.get("provider")
176
+ if isinstance(provider, dict):
177
+ llm_provider_data = {
178
+ "capability": provider.get("capability", "llm"),
179
+ "tags": provider.get("tags", []),
180
+ "version": provider.get("version", ""),
181
+ "namespace": provider.get("namespace", "default"),
182
+ }
183
+ llm_provider_json = json.dumps(llm_provider_data)
184
+ logger.debug(
185
+ f"🔌 Extracted llm_provider for {func_name}: {llm_provider_data}"
186
+ )
187
+ break
188
+
189
+ tool_spec = core.ToolSpec(
190
+ function_name=tool_name,
191
+ capability=tool_metadata.get("capability", tool_name),
192
+ version=tool_metadata.get("version", "1.0.0"),
193
+ description=tool_metadata.get("description", ""),
194
+ tags=tool_metadata.get("tags", []),
195
+ dependencies=deps if deps else None,
196
+ input_schema=input_schema_json,
197
+ llm_filter=llm_filter_json,
198
+ llm_provider=llm_provider_json,
199
+ )
200
+ tools.append(tool_spec)
201
+ logger.info(
202
+ f"📤 Tool '{tool_name}': llm_filter={llm_filter_json}, llm_provider={llm_provider_json}"
203
+ )
204
+
205
+ # Build LLM agent specs
206
+ llm_agents = []
207
+
208
+ for func_id, llm_metadata in mesh_llm_agents.items():
209
+ # LLMAgentMetadata is a dataclass with .config dict
210
+ config = llm_metadata.config if hasattr(llm_metadata, "config") else {}
211
+
212
+ provider = config.get("provider", {})
213
+ provider_json = json.dumps(provider) if provider else "{}"
214
+
215
+ filter_spec = config.get("filter")
216
+ filter_json = json.dumps(filter_spec) if filter_spec else None
217
+
218
+ llm_spec = core.LlmAgentSpec(
219
+ function_id=func_id,
220
+ provider=provider_json,
221
+ filter=filter_json,
222
+ filter_mode=config.get("filter_mode", "all"),
223
+ max_iterations=config.get("max_iterations", 1),
224
+ )
225
+ llm_agents.append(llm_spec)
226
+
227
+ # Create AgentSpec
228
+ spec = core.AgentSpec(
229
+ name=agent_id,
230
+ registry_url=registry_url,
231
+ version=version,
232
+ description=description,
233
+ http_port=http_port,
234
+ http_host=http_host,
235
+ namespace=namespace,
236
+ tools=tools if tools else None,
237
+ llm_agents=llm_agents if llm_agents else None,
238
+ heartbeat_interval=heartbeat_interval,
239
+ )
240
+
241
+ logger.info(
242
+ f"Built AgentSpec: name={agent_id}, tools={len(tools)}, "
243
+ f"llm_agents={len(llm_agents)}, registry={registry_url}"
244
+ )
245
+
246
+ return spec
247
+
248
+
249
+ async def _handle_mesh_event(event: Any, context: dict[str, Any]) -> None:
250
+ """
251
+ Handle a mesh event from the Rust core.
252
+
253
+ Dispatches to appropriate handler based on event type.
254
+ """
255
+ event_type = event.event_type
256
+
257
+ if event_type == "agent_registered":
258
+ logger.info(f"Agent registered with ID: {event.agent_id}")
259
+
260
+ elif event_type == "registration_failed":
261
+ logger.error(f"Agent registration failed: {event.error}")
262
+
263
+ elif event_type == "dependency_available":
264
+ await _handle_dependency_change(
265
+ capability=event.capability,
266
+ endpoint=event.endpoint,
267
+ function_name=event.function_name,
268
+ agent_id=event.agent_id,
269
+ available=True,
270
+ context=context,
271
+ requesting_function=getattr(event, "requesting_function", None),
272
+ dep_index=getattr(event, "dep_index", None),
273
+ )
274
+
275
+ elif event_type == "dependency_changed":
276
+ await _handle_dependency_change(
277
+ capability=event.capability,
278
+ endpoint=event.endpoint,
279
+ function_name=event.function_name,
280
+ agent_id=event.agent_id,
281
+ available=True,
282
+ context=context,
283
+ requesting_function=getattr(event, "requesting_function", None),
284
+ dep_index=getattr(event, "dep_index", None),
285
+ )
286
+
287
+ elif event_type == "dependency_unavailable":
288
+ await _handle_dependency_change(
289
+ capability=event.capability,
290
+ endpoint=None,
291
+ function_name=None,
292
+ agent_id=None,
293
+ available=False,
294
+ context=context,
295
+ requesting_function=getattr(event, "requesting_function", None),
296
+ dep_index=getattr(event, "dep_index", None),
297
+ )
298
+
299
+ elif event_type == "llm_tools_updated":
300
+ if event.tools is None:
301
+ logger.warning(
302
+ f"llm_tools_updated event for '{event.function_id}' has no tools data, skipping"
303
+ )
304
+ else:
305
+ await _handle_llm_tools_update(
306
+ function_id=event.function_id,
307
+ tools=event.tools,
308
+ context=context,
309
+ )
310
+
311
+ elif event_type == "llm_provider_available":
312
+ if event.provider_info is None:
313
+ logger.warning(
314
+ "llm_provider_available event has no provider_info, skipping"
315
+ )
316
+ else:
317
+ await _handle_llm_provider_update(
318
+ provider_info=event.provider_info,
319
+ context=context,
320
+ )
321
+
322
+ elif event_type == "health_check_due":
323
+ # Python can perform health check and report back
324
+ logger.debug("Health check due (not implemented yet)")
325
+
326
+ elif event_type == "registry_disconnected":
327
+ logger.warning(f"Registry disconnected: {event.reason}")
328
+
329
+ elif event_type == "shutdown":
330
+ logger.info("Rust core shutdown event received")
331
+
332
+ else:
333
+ logger.debug(f"Unhandled event type: {event_type}")
334
+
335
+
336
+ async def _handle_dependency_change(
337
+ capability: str,
338
+ endpoint: Optional[str],
339
+ function_name: Optional[str],
340
+ agent_id: Optional[str],
341
+ available: bool,
342
+ context: dict[str, Any],
343
+ requesting_function: Optional[str] = None,
344
+ dep_index: Optional[int] = None,
345
+ ) -> None:
346
+ """
347
+ Handle dependency availability change.
348
+
349
+ Updates the DI system with new/changed/removed dependencies.
350
+
351
+ If requesting_function and dep_index are provided (new behavior from Rust core),
352
+ we can directly register/unregister at the exact position. Otherwise, we fall
353
+ back to capability-based matching (backward compatibility).
354
+ """
355
+ logger.info(
356
+ f"Dependency change: {capability} -> "
357
+ f"{'available' if available else 'unavailable'} "
358
+ f"at {endpoint}/{function_name}"
359
+ + (
360
+ f" (func: {requesting_function}, idx: {dep_index})"
361
+ if requesting_function
362
+ else ""
363
+ )
364
+ )
365
+
366
+ # Import DI components
367
+ from ...engine.decorator_registry import DecoratorRegistry
368
+ from ...engine.dependency_injector import get_global_injector
369
+ from ...engine.unified_mcp_proxy import EnhancedUnifiedMCPProxy
370
+ from ...shared.config_resolver import get_config_value
371
+
372
+ injector = get_global_injector()
373
+ mesh_tools = DecoratorRegistry.get_mesh_tools()
374
+
375
+ # If we have position info, use it directly (new behavior)
376
+ if requesting_function is not None and dep_index is not None:
377
+ # Build dep_key - requesting_function is the function_name from registry
378
+ # We need to find the corresponding func_id
379
+ func_id = requesting_function
380
+ for tool_name, decorated_func in mesh_tools.items():
381
+ if tool_name == requesting_function:
382
+ func = decorated_func.function
383
+ func_id = f"{func.__module__}.{func.__qualname__}"
384
+ break
385
+
386
+ dep_key = f"{func_id}:dep_{dep_index}"
387
+
388
+ if not available:
389
+ await injector.unregister_dependency(dep_key)
390
+ logger.info(f"Unregistered dependency: {dep_key}")
391
+ return
392
+
393
+ # Get kwargs from the tool metadata
394
+ kwargs_config = {}
395
+ for tool_name, decorated_func in mesh_tools.items():
396
+ if tool_name == requesting_function:
397
+ tool_metadata = decorated_func.metadata or {}
398
+ dependencies = tool_metadata.get("dependencies", [])
399
+ if dep_index < len(dependencies):
400
+ kwargs_config = dependencies[dep_index].get("kwargs", {})
401
+ break
402
+
403
+ # Check for self-dependency
404
+ current_agent_id = None
405
+ try:
406
+ config = DecoratorRegistry.get_resolved_agent_config()
407
+ current_agent_id = config.get("agent_id")
408
+ except Exception:
409
+ # Use config resolver for consistent env var handling
410
+ current_agent_id = get_config_value("MCP_MESH_AGENT_ID")
411
+
412
+ is_self_dependency = (
413
+ current_agent_id and agent_id and current_agent_id == agent_id
414
+ )
415
+
416
+ if is_self_dependency:
417
+ from ...engine.self_dependency_proxy import SelfDependencyProxy
418
+
419
+ wrapper_func = mesh_tools.get(function_name)
420
+ if wrapper_func:
421
+ proxy = SelfDependencyProxy(wrapper_func.function, function_name)
422
+ logger.debug(f"Created SelfDependencyProxy for {capability}")
423
+ else:
424
+ proxy = EnhancedUnifiedMCPProxy(endpoint, function_name)
425
+ logger.debug(
426
+ f"Created EnhancedUnifiedMCPProxy (fallback) for {capability}"
427
+ )
428
+ else:
429
+ proxy = EnhancedUnifiedMCPProxy(
430
+ endpoint, function_name, kwargs_config=kwargs_config
431
+ )
432
+ logger.debug(
433
+ f"Created EnhancedUnifiedMCPProxy for {capability} -> {endpoint}"
434
+ )
435
+
436
+ await injector.register_dependency(dep_key, proxy)
437
+ logger.info(f"Registered dependency: {dep_key}")
438
+ return
439
+
440
+ # Fallback: capability-based matching (backward compatibility)
441
+ if not available:
442
+ # Dependency became unavailable - unregister it
443
+ if hasattr(injector, "_dependencies"):
444
+ keys_to_remove = [
445
+ key for key in injector._dependencies.keys() if capability in key
446
+ ]
447
+ for dep_key in keys_to_remove:
448
+ await injector.unregister_dependency(dep_key)
449
+ logger.info(f"Unregistered dependency: {dep_key}")
450
+ return
451
+
452
+ # Dependency is available - create proxy and register
453
+ # Map tool names to func_ids
454
+ tool_name_to_func_id = {}
455
+ for tool_name, decorated_func in mesh_tools.items():
456
+ func = decorated_func.function
457
+ func_id = f"{func.__module__}.{func.__qualname__}"
458
+ tool_name_to_func_id[tool_name] = func_id
459
+
460
+ # Find which functions depend on this capability
461
+ for tool_name, decorated_func in mesh_tools.items():
462
+ tool_metadata = decorated_func.metadata or {}
463
+ dependencies = tool_metadata.get("dependencies", [])
464
+
465
+ for idx, dep_info in enumerate(dependencies):
466
+ if dep_info.get("capability") == capability:
467
+ func_id = tool_name_to_func_id.get(tool_name, tool_name)
468
+ dep_key = f"{func_id}:dep_{idx}"
469
+
470
+ # Check for self-dependency
471
+ current_agent_id = None
472
+ try:
473
+ config = DecoratorRegistry.get_resolved_agent_config()
474
+ current_agent_id = config.get("agent_id")
475
+ except Exception:
476
+ # Use config resolver for consistent env var handling
477
+ current_agent_id = get_config_value("MCP_MESH_AGENT_ID")
478
+
479
+ is_self_dependency = (
480
+ current_agent_id and agent_id and current_agent_id == agent_id
481
+ )
482
+
483
+ if is_self_dependency:
484
+ # Create self-dependency proxy
485
+ from ...engine.self_dependency_proxy import SelfDependencyProxy
486
+
487
+ wrapper_func = mesh_tools.get(function_name)
488
+ if wrapper_func:
489
+ proxy = SelfDependencyProxy(
490
+ wrapper_func.function, function_name
491
+ )
492
+ logger.debug(f"Created SelfDependencyProxy for {capability}")
493
+ else:
494
+ # Fallback to HTTP proxy
495
+ proxy = EnhancedUnifiedMCPProxy(endpoint, function_name)
496
+ logger.debug(
497
+ f"Created EnhancedUnifiedMCPProxy (fallback) for {capability}"
498
+ )
499
+ else:
500
+ # Create cross-service proxy
501
+ kwargs_config = dep_info.get("kwargs", {})
502
+ proxy = EnhancedUnifiedMCPProxy(
503
+ endpoint, function_name, kwargs_config=kwargs_config
504
+ )
505
+ logger.debug(
506
+ f"Created EnhancedUnifiedMCPProxy for {capability} -> {endpoint}"
507
+ )
508
+
509
+ await injector.register_dependency(dep_key, proxy)
510
+ logger.info(f"Registered dependency: {dep_key}")
511
+
512
+
513
+ async def _handle_llm_tools_update(
514
+ function_id: str,
515
+ tools: list,
516
+ context: dict[str, Any],
517
+ ) -> None:
518
+ """
519
+ Handle LLM tools update event.
520
+
521
+ Updates the LLM tools registry for the given function via the DI system.
522
+ """
523
+ logger.info(f"LLM tools update for {function_id}: {len(tools)} tools")
524
+
525
+ # Import injector
526
+ from ...engine.dependency_injector import get_global_injector
527
+
528
+ # Convert tools to the expected format (using "name" for OpenAPI contract)
529
+ tool_list = []
530
+ for tool in tools:
531
+ tool_info = {
532
+ "name": tool.function_name, # OpenAPI contract uses "name" not "function_name"
533
+ "capability": tool.capability,
534
+ "endpoint": tool.endpoint,
535
+ "agent_id": tool.agent_id,
536
+ "input_schema": (
537
+ json.loads(tool.input_schema) if tool.input_schema else None
538
+ ),
539
+ }
540
+ tool_list.append(tool_info)
541
+
542
+ # Update LLM tools via the dependency injector
543
+ injector = get_global_injector()
544
+ llm_tools = {function_id: tool_list}
545
+ injector.update_llm_tools(llm_tools)
546
+ logger.debug(f"Updated {len(tool_list)} LLM tools for {function_id}")
547
+
548
+
549
+ async def _handle_llm_provider_update(
550
+ provider_info: Any,
551
+ context: dict[str, Any],
552
+ ) -> None:
553
+ """
554
+ Handle LLM provider resolution event.
555
+
556
+ Updates the LLM provider for the given function via the DI system.
557
+ """
558
+ function_id = provider_info.function_id
559
+ logger.info(
560
+ f"LLM provider resolved for {function_id}: "
561
+ f"{provider_info.function_name} at {provider_info.endpoint}"
562
+ )
563
+
564
+ # Import injector
565
+ from ...engine.dependency_injector import get_global_injector
566
+ from ...engine.unified_mcp_proxy import EnhancedUnifiedMCPProxy
567
+
568
+ # Create proxy for the LLM provider
569
+ proxy = EnhancedUnifiedMCPProxy(
570
+ provider_info.endpoint,
571
+ provider_info.function_name,
572
+ )
573
+
574
+ # Register as the LLM provider for this function
575
+ injector = get_global_injector()
576
+ provider_key = f"{function_id}:llm_provider"
577
+ await injector.register_dependency(provider_key, proxy)
578
+
579
+ # Also store provider metadata for the mesh agent to use (using "name" for OpenAPI contract)
580
+ llm_providers = {
581
+ function_id: {
582
+ "agent_id": provider_info.agent_id,
583
+ "endpoint": provider_info.endpoint,
584
+ "name": provider_info.function_name, # OpenAPI contract uses "name"
585
+ "model": provider_info.model,
586
+ }
587
+ }
588
+ injector.process_llm_providers(llm_providers)
589
+ logger.debug(f"Registered LLM provider for {function_id}")
590
+
591
+
592
+ async def rust_heartbeat_task(heartbeat_config: dict[str, Any]) -> None:
593
+ """
594
+ Rust-backed heartbeat task that runs in FastAPI lifespan.
595
+
596
+ This is a drop-in replacement for heartbeat_lifespan_task.
597
+ Instead of running Python heartbeat loop, it starts the Rust core
598
+ and listens for events.
599
+
600
+ Args:
601
+ heartbeat_config: Configuration containing agent_id, interval, context
602
+ """
603
+ agent_id = heartbeat_config["agent_id"]
604
+ context = heartbeat_config["context"]
605
+ standalone_mode = heartbeat_config.get("standalone_mode", False)
606
+
607
+ if standalone_mode:
608
+ logger.info(
609
+ f"Rust heartbeat in standalone mode for agent '{agent_id}' "
610
+ "(no registry communication)"
611
+ )
612
+ return
613
+
614
+ try:
615
+ core = _get_rust_core()
616
+ except ImportError as e:
617
+ logger.error(
618
+ f"Rust core not available for agent '{agent_id}': {e}. "
619
+ "The mcp_mesh_core module must be built and installed."
620
+ )
621
+ raise RuntimeError(
622
+ f"Rust core (mcp_mesh_core) is required but not available: {e}"
623
+ ) from e
624
+
625
+ logger.info(f"Starting Rust-backed heartbeat for agent '{agent_id}'")
626
+
627
+ handle = None
628
+ try:
629
+ # Build AgentSpec from context
630
+ spec = _build_agent_spec(context)
631
+
632
+ # Start Rust core runtime
633
+ handle = core.start_agent(spec)
634
+ logger.info(f"Rust core started for agent '{agent_id}'")
635
+
636
+ # Event loop - process events from Rust core
637
+ while True:
638
+ # Check for Python shutdown signal
639
+ try:
640
+ from ...shared.simple_shutdown import should_stop_heartbeat
641
+
642
+ if should_stop_heartbeat():
643
+ logger.info(
644
+ f"Stopping Rust heartbeat for agent '{agent_id}' due to shutdown"
645
+ )
646
+ handle.shutdown()
647
+ break
648
+ except ImportError:
649
+ pass
650
+
651
+ try:
652
+ # Wait for next event from Rust core with timeout
653
+ # Timeout allows periodic shutdown checks
654
+ try:
655
+ event = await asyncio.wait_for(handle.next_event(), timeout=1.0)
656
+ except TimeoutError:
657
+ # No event in 1 second, loop back to check shutdown signal
658
+ continue
659
+
660
+ if event.event_type == "shutdown":
661
+ logger.info(f"Rust core shutdown for agent '{agent_id}'")
662
+ break
663
+
664
+ # Handle the event
665
+ await _handle_mesh_event(event, context)
666
+
667
+ except Exception as e:
668
+ logger.error(f"Error handling Rust event: {e}")
669
+ # Continue processing events
670
+
671
+ except asyncio.CancelledError:
672
+ logger.info(f"Rust heartbeat task cancelled for agent '{agent_id}'")
673
+ raise
674
+ except Exception as e:
675
+ logger.error(f"Rust heartbeat failed for agent '{agent_id}': {e}")
676
+ raise
677
+ finally:
678
+ # Always ensure graceful shutdown of Rust core to prevent daemon thread issues
679
+ # This is critical: without shutdown(), Rust background threads may try to
680
+ # write to stdout via tracing after Python's stdout is finalized
681
+ if handle is not None:
682
+ try:
683
+ handle.shutdown()
684
+ # Give Rust core a moment to clean up before Python exits
685
+ # Use time.sleep as fallback if asyncio is shutting down
686
+ try:
687
+ await asyncio.sleep(0.2)
688
+ except (asyncio.CancelledError, RuntimeError):
689
+ # Event loop might be shutting down, use blocking sleep
690
+ import time
691
+
692
+ time.sleep(0.2)
693
+ logger.debug(f"Rust core shutdown complete for agent '{agent_id}'")
694
+ except Exception as e:
695
+ logger.warning(f"Error during Rust core shutdown: {e}")