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,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}")
|