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
|
@@ -8,8 +8,8 @@ outside the pipeline, such as by immediate uvicorn start in decorators.
|
|
|
8
8
|
import gc
|
|
9
9
|
import logging
|
|
10
10
|
import socket
|
|
11
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
12
11
|
import threading
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
@@ -21,10 +21,10 @@ class ServerDiscoveryUtil:
|
|
|
21
21
|
def discover_fastapi_instances() -> Dict[str, Dict[str, Any]]:
|
|
22
22
|
"""
|
|
23
23
|
Discover FastAPI application instances in the Python runtime.
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
Uses intelligent deduplication to handle standard uvicorn patterns where
|
|
26
26
|
the same app might be imported multiple times (e.g., "module:app" pattern).
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
Returns:
|
|
29
29
|
Dict mapping app_id -> app_info where app_info contains:
|
|
30
30
|
- 'instance': The FastAPI app instance
|
|
@@ -34,7 +34,7 @@ class ServerDiscoveryUtil:
|
|
|
34
34
|
"""
|
|
35
35
|
fastapi_apps = {}
|
|
36
36
|
seen_apps = {} # For deduplication: title -> app_info
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
try:
|
|
39
39
|
# Import FastAPI here to avoid dependency if not used
|
|
40
40
|
from fastapi import FastAPI
|
|
@@ -55,22 +55,24 @@ class ServerDiscoveryUtil:
|
|
|
55
55
|
version = getattr(obj, "version", "unknown")
|
|
56
56
|
routes = ServerDiscoveryUtil._extract_route_info(obj)
|
|
57
57
|
route_count = len(routes)
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
# Create a signature for deduplication
|
|
60
60
|
app_signature = (title, version, route_count)
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
# Check if we've seen an identical app
|
|
63
63
|
if app_signature in seen_apps:
|
|
64
64
|
existing_app = seen_apps[app_signature]
|
|
65
65
|
# Compare route details to ensure they're truly identical
|
|
66
66
|
existing_routes = existing_app["routes"]
|
|
67
|
-
|
|
68
|
-
if ServerDiscoveryUtil._routes_are_identical(
|
|
67
|
+
|
|
68
|
+
if ServerDiscoveryUtil._routes_are_identical(
|
|
69
|
+
routes, existing_routes
|
|
70
|
+
):
|
|
69
71
|
logger.debug(
|
|
70
72
|
f"Skipping duplicate FastAPI app: '{title}' (same title, version, and routes)"
|
|
71
73
|
)
|
|
72
74
|
continue # Skip this duplicate
|
|
73
|
-
|
|
75
|
+
|
|
74
76
|
# This is a unique app, add it
|
|
75
77
|
app_id = f"app_{id(obj)}"
|
|
76
78
|
app_info = {
|
|
@@ -80,17 +82,19 @@ class ServerDiscoveryUtil:
|
|
|
80
82
|
"routes": routes,
|
|
81
83
|
"module": ServerDiscoveryUtil._get_app_module(obj),
|
|
82
84
|
"object_id": id(obj),
|
|
83
|
-
"router_routes_count":
|
|
85
|
+
"router_routes_count": (
|
|
86
|
+
len(obj.router.routes) if hasattr(obj, "router") else 0
|
|
87
|
+
),
|
|
84
88
|
}
|
|
85
|
-
|
|
89
|
+
|
|
86
90
|
fastapi_apps[app_id] = app_info
|
|
87
91
|
seen_apps[app_signature] = app_info
|
|
88
|
-
|
|
92
|
+
|
|
89
93
|
logger.debug(
|
|
90
94
|
f"Found FastAPI app: '{title}' (module: {app_info['module']}) with "
|
|
91
95
|
f"{len(routes)} routes"
|
|
92
96
|
)
|
|
93
|
-
|
|
97
|
+
|
|
94
98
|
except Exception as e:
|
|
95
99
|
logger.warning(f"Error analyzing FastAPI app: {e}")
|
|
96
100
|
continue
|
|
@@ -101,7 +105,7 @@ class ServerDiscoveryUtil:
|
|
|
101
105
|
def discover_running_servers() -> List[Dict[str, Any]]:
|
|
102
106
|
"""
|
|
103
107
|
Discover running uvicorn servers by scanning threads and checking port bindings.
|
|
104
|
-
|
|
108
|
+
|
|
105
109
|
Returns:
|
|
106
110
|
List of server info dictionaries containing:
|
|
107
111
|
- 'type': 'uvicorn' or 'unknown'
|
|
@@ -111,183 +115,208 @@ class ServerDiscoveryUtil:
|
|
|
111
115
|
- 'app': FastAPI app if discoverable
|
|
112
116
|
"""
|
|
113
117
|
running_servers = []
|
|
114
|
-
|
|
118
|
+
|
|
115
119
|
# Look for uvicorn server threads
|
|
116
120
|
for thread in threading.enumerate():
|
|
117
|
-
if hasattr(thread,
|
|
121
|
+
if hasattr(thread, "_target"):
|
|
118
122
|
# Check if thread target looks like a uvicorn server
|
|
119
|
-
target_name =
|
|
120
|
-
|
|
123
|
+
target_name = (
|
|
124
|
+
getattr(thread._target, "__name__", "") if thread._target else ""
|
|
125
|
+
)
|
|
126
|
+
if "server" in target_name.lower() or "uvicorn" in target_name.lower():
|
|
121
127
|
server_info = {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
"type": "uvicorn",
|
|
129
|
+
"thread": thread,
|
|
130
|
+
"target_name": target_name,
|
|
131
|
+
"daemon": thread.daemon,
|
|
132
|
+
"alive": thread.is_alive(),
|
|
127
133
|
}
|
|
128
|
-
|
|
134
|
+
|
|
129
135
|
# Try to extract server details from thread
|
|
130
|
-
server_details =
|
|
136
|
+
server_details = (
|
|
137
|
+
ServerDiscoveryUtil._extract_server_details_from_thread(thread)
|
|
138
|
+
)
|
|
131
139
|
server_info.update(server_details)
|
|
132
|
-
|
|
140
|
+
|
|
133
141
|
running_servers.append(server_info)
|
|
134
|
-
logger.debug(
|
|
135
|
-
|
|
142
|
+
logger.debug(
|
|
143
|
+
f"Found running server thread: {target_name} (daemon={thread.daemon})"
|
|
144
|
+
)
|
|
145
|
+
|
|
136
146
|
# Also check for bound ports that might indicate running servers
|
|
137
147
|
bound_ports = ServerDiscoveryUtil._discover_bound_ports()
|
|
138
148
|
for port_info in bound_ports:
|
|
139
149
|
# Only add if we haven't already found this port via thread discovery
|
|
140
|
-
existing_ports = [s.get(
|
|
141
|
-
if port_info[
|
|
142
|
-
port_info[
|
|
150
|
+
existing_ports = [s.get("port") for s in running_servers if s.get("port")]
|
|
151
|
+
if port_info["port"] not in existing_ports:
|
|
152
|
+
port_info["type"] = "unknown"
|
|
143
153
|
running_servers.append(port_info)
|
|
144
|
-
logger.debug(
|
|
145
|
-
|
|
154
|
+
logger.debug(
|
|
155
|
+
f"Found bound port: {port_info['host']}:{port_info['port']}"
|
|
156
|
+
)
|
|
157
|
+
|
|
146
158
|
return running_servers
|
|
147
159
|
|
|
148
160
|
@staticmethod
|
|
149
161
|
def _extract_server_details_from_thread(thread) -> Dict[str, Any]:
|
|
150
162
|
"""Extract server details from a thread if possible."""
|
|
151
163
|
details = {}
|
|
152
|
-
|
|
164
|
+
|
|
153
165
|
try:
|
|
154
166
|
# Try to access thread local variables or target args
|
|
155
|
-
if hasattr(thread,
|
|
167
|
+
if hasattr(thread, "_args") and thread._args:
|
|
156
168
|
# Some uvicorn servers might have args with host/port
|
|
157
169
|
args = thread._args
|
|
158
170
|
if len(args) >= 2:
|
|
159
171
|
# Common pattern: (app, host, port) or similar
|
|
160
|
-
if isinstance(args[0], str) and
|
|
172
|
+
if isinstance(args[0], str) and ":" in args[0]:
|
|
161
173
|
# Might be "host:port" format
|
|
162
174
|
try:
|
|
163
|
-
host, port = args[0].split(
|
|
164
|
-
details[
|
|
165
|
-
details[
|
|
175
|
+
host, port = args[0].split(":")
|
|
176
|
+
details["host"] = host
|
|
177
|
+
details["port"] = int(port)
|
|
166
178
|
except (ValueError, IndexError):
|
|
167
179
|
pass
|
|
168
|
-
|
|
180
|
+
|
|
169
181
|
# Try to find FastAPI app in thread target or args
|
|
170
|
-
if hasattr(thread,
|
|
182
|
+
if hasattr(thread, "_target") and thread._target:
|
|
171
183
|
# Check if target has app attribute or if it's in closure
|
|
172
184
|
target = thread._target
|
|
173
|
-
if hasattr(target,
|
|
185
|
+
if hasattr(target, "__closure__") and target.__closure__:
|
|
174
186
|
for cell in target.__closure__:
|
|
175
187
|
try:
|
|
176
188
|
cell_contents = cell.cell_contents
|
|
177
189
|
from fastapi import FastAPI
|
|
190
|
+
|
|
178
191
|
if isinstance(cell_contents, FastAPI):
|
|
179
|
-
details[
|
|
180
|
-
details[
|
|
192
|
+
details["app"] = cell_contents
|
|
193
|
+
details["app_title"] = getattr(
|
|
194
|
+
cell_contents, "title", "Unknown"
|
|
195
|
+
)
|
|
181
196
|
break
|
|
182
197
|
except (ImportError, AttributeError):
|
|
183
198
|
continue
|
|
184
|
-
|
|
199
|
+
|
|
185
200
|
except Exception as e:
|
|
186
201
|
logger.debug(f"Could not extract server details from thread: {e}")
|
|
187
|
-
|
|
202
|
+
|
|
188
203
|
return details
|
|
189
204
|
|
|
190
205
|
@staticmethod
|
|
191
206
|
def _discover_bound_ports() -> List[Dict[str, Any]]:
|
|
192
207
|
"""Discover ports that are currently bound by this process."""
|
|
193
208
|
bound_ports = []
|
|
194
|
-
|
|
209
|
+
|
|
195
210
|
try:
|
|
196
211
|
# Common port ranges for web servers
|
|
197
212
|
common_ports = [8000, 8080, 8090, 9090, 9091, 3000, 3001, 4000, 5000]
|
|
198
|
-
|
|
213
|
+
|
|
199
214
|
for port in common_ports:
|
|
200
|
-
for host in [
|
|
215
|
+
for host in ["127.0.0.1", "0.0.0.0", "localhost"]:
|
|
201
216
|
try:
|
|
202
217
|
# Try to connect to see if port is bound
|
|
203
218
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
204
219
|
sock.settimeout(0.1) # Very short timeout
|
|
205
220
|
result = sock.connect_ex((host, port))
|
|
206
221
|
sock.close()
|
|
207
|
-
|
|
222
|
+
|
|
208
223
|
if result == 0: # Connection successful = port is bound
|
|
209
|
-
bound_ports.append(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
'status': 'bound'
|
|
213
|
-
})
|
|
224
|
+
bound_ports.append(
|
|
225
|
+
{"host": host, "port": port, "status": "bound"}
|
|
226
|
+
)
|
|
214
227
|
break # Don't check other hosts for same port
|
|
215
228
|
except Exception:
|
|
216
229
|
continue
|
|
217
|
-
|
|
230
|
+
|
|
218
231
|
except Exception as e:
|
|
219
232
|
logger.debug(f"Error discovering bound ports: {e}")
|
|
220
|
-
|
|
233
|
+
|
|
221
234
|
return bound_ports
|
|
222
235
|
|
|
223
236
|
@staticmethod
|
|
224
|
-
def find_server_on_port(
|
|
237
|
+
def find_server_on_port(
|
|
238
|
+
target_port: int, target_host: str = None
|
|
239
|
+
) -> Optional[Dict[str, Any]]:
|
|
225
240
|
"""
|
|
226
241
|
Find if there's already a server running on the specified port.
|
|
227
|
-
|
|
242
|
+
|
|
228
243
|
Args:
|
|
229
244
|
target_port: Port to check
|
|
230
245
|
target_host: Host to check (optional)
|
|
231
|
-
|
|
246
|
+
|
|
232
247
|
Returns:
|
|
233
248
|
Server info dict if found, None otherwise
|
|
234
249
|
"""
|
|
235
250
|
running_servers = ServerDiscoveryUtil.discover_running_servers()
|
|
236
|
-
|
|
251
|
+
|
|
237
252
|
for server in running_servers:
|
|
238
|
-
server_port = server.get(
|
|
239
|
-
server_host = server.get(
|
|
240
|
-
|
|
253
|
+
server_port = server.get("port")
|
|
254
|
+
server_host = server.get("host")
|
|
255
|
+
|
|
241
256
|
# Check port match
|
|
242
257
|
if server_port == target_port:
|
|
243
258
|
# If target_host is specified, check host match too
|
|
244
|
-
if
|
|
245
|
-
|
|
259
|
+
if (
|
|
260
|
+
target_host is None
|
|
261
|
+
or server_host == target_host
|
|
262
|
+
or server_host in ["0.0.0.0", "127.0.0.1"]
|
|
263
|
+
):
|
|
264
|
+
logger.info(
|
|
265
|
+
f"🔍 DISCOVERY: Found existing server on {server_host}:{server_port}"
|
|
266
|
+
)
|
|
246
267
|
return server
|
|
247
|
-
|
|
268
|
+
|
|
248
269
|
logger.debug(f"🔍 DISCOVERY: No existing server found on port {target_port}")
|
|
249
270
|
return None
|
|
250
271
|
|
|
251
272
|
@staticmethod
|
|
252
|
-
def _routes_are_identical(
|
|
273
|
+
def _routes_are_identical(
|
|
274
|
+
routes1: List[Dict[str, Any]], routes2: List[Dict[str, Any]]
|
|
275
|
+
) -> bool:
|
|
253
276
|
"""Compare two route lists to see if they're identical."""
|
|
254
277
|
if len(routes1) != len(routes2):
|
|
255
278
|
return False
|
|
256
|
-
|
|
279
|
+
|
|
257
280
|
# Create comparable signatures for each route
|
|
258
281
|
def route_signature(route):
|
|
259
282
|
return (
|
|
260
|
-
tuple(
|
|
261
|
-
|
|
262
|
-
|
|
283
|
+
tuple(
|
|
284
|
+
sorted(route.get("methods", []))
|
|
285
|
+
), # Sort methods for consistent comparison
|
|
286
|
+
route.get("path", ""),
|
|
287
|
+
route.get("endpoint_name", ""),
|
|
263
288
|
)
|
|
264
|
-
|
|
289
|
+
|
|
265
290
|
# Sort routes by signature for consistent comparison
|
|
266
291
|
sig1 = sorted([route_signature(r) for r in routes1])
|
|
267
292
|
sig2 = sorted([route_signature(r) for r in routes2])
|
|
268
|
-
|
|
293
|
+
|
|
269
294
|
return sig1 == sig2
|
|
270
295
|
|
|
271
296
|
@staticmethod
|
|
272
297
|
def _extract_route_info(app) -> List[Dict[str, Any]]:
|
|
273
298
|
"""Extract route information from FastAPI app without modifying it."""
|
|
274
299
|
routes = []
|
|
275
|
-
|
|
300
|
+
|
|
276
301
|
try:
|
|
277
302
|
for route in app.router.routes:
|
|
278
|
-
if hasattr(route,
|
|
303
|
+
if hasattr(route, "endpoint") and hasattr(route, "path"):
|
|
279
304
|
route_info = {
|
|
280
305
|
"path": route.path,
|
|
281
|
-
"methods":
|
|
306
|
+
"methods": (
|
|
307
|
+
list(route.methods) if hasattr(route, "methods") else []
|
|
308
|
+
),
|
|
282
309
|
"endpoint": route.endpoint,
|
|
283
|
-
"endpoint_name": getattr(route.endpoint,
|
|
284
|
-
"has_mesh_route": hasattr(
|
|
310
|
+
"endpoint_name": getattr(route.endpoint, "__name__", "unknown"),
|
|
311
|
+
"has_mesh_route": hasattr(
|
|
312
|
+
route.endpoint, "_mesh_route_metadata"
|
|
313
|
+
),
|
|
285
314
|
}
|
|
286
315
|
routes.append(route_info)
|
|
287
|
-
|
|
316
|
+
|
|
288
317
|
except Exception as e:
|
|
289
318
|
logger.warning(f"Error extracting route info: {e}")
|
|
290
|
-
|
|
319
|
+
|
|
291
320
|
return routes
|
|
292
321
|
|
|
293
322
|
@staticmethod
|
|
@@ -297,16 +326,16 @@ class ServerDiscoveryUtil:
|
|
|
297
326
|
# Try to get module from the app's stack frame when it was created
|
|
298
327
|
# This is best-effort - may not always work
|
|
299
328
|
import inspect
|
|
300
|
-
|
|
329
|
+
|
|
301
330
|
frame = inspect.currentframe()
|
|
302
331
|
while frame:
|
|
303
332
|
frame_globals = frame.f_globals
|
|
304
333
|
for name, obj in frame_globals.items():
|
|
305
334
|
if obj is app:
|
|
306
|
-
return frame_globals.get(
|
|
335
|
+
return frame_globals.get("__name__", "unknown")
|
|
307
336
|
frame = frame.f_back
|
|
308
|
-
|
|
337
|
+
|
|
309
338
|
except Exception:
|
|
310
339
|
pass
|
|
311
|
-
|
|
312
|
-
return None
|
|
340
|
+
|
|
341
|
+
return None
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Simple shutdown coordination for MCP Mesh agents.
|
|
3
3
|
|
|
4
4
|
Provides clean shutdown via FastAPI lifespan events and basic signal handling.
|
|
5
|
+
The Rust core handles actual deregistration from the registry.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
|
-
import asyncio
|
|
8
8
|
import logging
|
|
9
9
|
import signal
|
|
10
10
|
from contextlib import asynccontextmanager
|
|
@@ -14,7 +14,12 @@ logger = logging.getLogger(__name__)
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class SimpleShutdownCoordinator:
|
|
17
|
-
"""Lightweight shutdown coordination using FastAPI lifespan.
|
|
17
|
+
"""Lightweight shutdown coordination using FastAPI lifespan.
|
|
18
|
+
|
|
19
|
+
The Rust core handles registry deregistration automatically when
|
|
20
|
+
handle.shutdown() is called. This coordinator just manages the
|
|
21
|
+
shutdown signal flow between Python and Rust.
|
|
22
|
+
"""
|
|
18
23
|
|
|
19
24
|
def __init__(self):
|
|
20
25
|
self._shutdown_requested = False
|
|
@@ -23,7 +28,7 @@ class SimpleShutdownCoordinator:
|
|
|
23
28
|
self._shutdown_complete = False # Flag to prevent race conditions
|
|
24
29
|
|
|
25
30
|
def set_shutdown_context(self, registry_url: str, agent_id: str) -> None:
|
|
26
|
-
"""Set context for shutdown
|
|
31
|
+
"""Set context for shutdown (used for logging)."""
|
|
27
32
|
self._registry_url = registry_url
|
|
28
33
|
self._agent_id = agent_id
|
|
29
34
|
logger.debug(
|
|
@@ -54,61 +59,20 @@ class SimpleShutdownCoordinator:
|
|
|
54
59
|
self._shutdown_complete = True
|
|
55
60
|
logger.debug("🏁 Shutdown marked as complete")
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
from _mcp_mesh.engine.decorator_registry import DecoratorRegistry
|
|
63
|
-
|
|
64
|
-
agent_config = DecoratorRegistry.get_resolved_agent_config()
|
|
65
|
-
if agent_config and "agent_id" in agent_config:
|
|
66
|
-
resolved_agent_id = agent_config["agent_id"]
|
|
67
|
-
if resolved_agent_id and resolved_agent_id != "unknown":
|
|
68
|
-
actual_agent_id = resolved_agent_id
|
|
69
|
-
logger.debug(
|
|
70
|
-
f"🔧 Using resolved agent_id from DecoratorRegistry: {actual_agent_id}"
|
|
71
|
-
)
|
|
72
|
-
except Exception as e:
|
|
73
|
-
logger.debug(f"Could not get agent_id from DecoratorRegistry: {e}")
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
not self._registry_url
|
|
77
|
-
or not actual_agent_id
|
|
78
|
-
or actual_agent_id == "unknown"
|
|
79
|
-
):
|
|
80
|
-
logger.warning(
|
|
81
|
-
f"⚠️ Missing registry URL or agent ID for cleanup: registry_url={self._registry_url}, agent_id={actual_agent_id}"
|
|
82
|
-
)
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
try:
|
|
86
|
-
from _mcp_mesh.generated.mcp_mesh_registry_client.api_client import (
|
|
87
|
-
ApiClient,
|
|
88
|
-
)
|
|
89
|
-
from _mcp_mesh.generated.mcp_mesh_registry_client.configuration import (
|
|
90
|
-
Configuration,
|
|
91
|
-
)
|
|
92
|
-
from _mcp_mesh.shared.registry_client_wrapper import RegistryClientWrapper
|
|
93
|
-
|
|
94
|
-
config = Configuration(host=self._registry_url)
|
|
95
|
-
api_client = ApiClient(configuration=config)
|
|
96
|
-
registry_wrapper = RegistryClientWrapper(api_client)
|
|
97
|
-
|
|
98
|
-
success = await registry_wrapper.unregister_agent(actual_agent_id)
|
|
99
|
-
if success:
|
|
100
|
-
logger.info(f"✅ Agent '{actual_agent_id}' unregistered from registry")
|
|
101
|
-
self.mark_shutdown_complete()
|
|
102
|
-
else:
|
|
103
|
-
logger.warning(f"⚠️ Failed to unregister agent '{actual_agent_id}'")
|
|
104
|
-
self.mark_shutdown_complete() # Mark complete even on failure to prevent loops
|
|
105
|
-
|
|
106
|
-
except Exception as e:
|
|
107
|
-
logger.error(f"❌ Registry cleanup error: {e}")
|
|
108
|
-
self.mark_shutdown_complete() # Mark complete even on error to prevent loops
|
|
62
|
+
def request_shutdown(self) -> None:
|
|
63
|
+
"""Request shutdown (called when lifespan exits)."""
|
|
64
|
+
self._shutdown_requested = True
|
|
65
|
+
agent_id = self._agent_id or "<unknown>"
|
|
66
|
+
logger.info(f"🔄 Shutdown requested for agent '{agent_id}'")
|
|
109
67
|
|
|
110
68
|
def create_shutdown_lifespan(self, original_lifespan=None):
|
|
111
|
-
"""Create lifespan function that
|
|
69
|
+
"""Create lifespan function that signals shutdown on exit.
|
|
70
|
+
|
|
71
|
+
The Rust core will handle actual deregistration when it receives
|
|
72
|
+
the shutdown signal via handle.shutdown().
|
|
73
|
+
"""
|
|
74
|
+
# Capture agent_id at creation time with fallback for None
|
|
75
|
+
agent_id = self._agent_id or "<unknown>"
|
|
112
76
|
|
|
113
77
|
@asynccontextmanager
|
|
114
78
|
async def shutdown_lifespan(app):
|
|
@@ -120,10 +84,14 @@ class SimpleShutdownCoordinator:
|
|
|
120
84
|
else:
|
|
121
85
|
yield
|
|
122
86
|
|
|
123
|
-
# Shutdown phase
|
|
124
|
-
logger.info(
|
|
125
|
-
|
|
126
|
-
|
|
87
|
+
# Shutdown phase - just signal, Rust handles deregistration
|
|
88
|
+
logger.info(
|
|
89
|
+
f"🔄 FastAPI shutdown initiated for agent '{agent_id}', "
|
|
90
|
+
"Rust core will handle deregistration"
|
|
91
|
+
)
|
|
92
|
+
self.request_shutdown()
|
|
93
|
+
self.mark_shutdown_complete()
|
|
94
|
+
logger.info("🏁 Shutdown signaled")
|
|
127
95
|
|
|
128
96
|
return shutdown_lifespan
|
|
129
97
|
|
|
@@ -164,12 +132,15 @@ def start_blocking_loop_with_shutdown_support(thread) -> None:
|
|
|
164
132
|
"""
|
|
165
133
|
Keep main thread alive while uvicorn in the thread handles requests.
|
|
166
134
|
|
|
167
|
-
Install signal handlers in main thread for proper
|
|
135
|
+
Install signal handlers in main thread for proper shutdown signaling since
|
|
168
136
|
signals to threads can be unreliable for FastAPI lifespan shutdown.
|
|
137
|
+
|
|
138
|
+
Note: The Rust core handles registry deregistration automatically when
|
|
139
|
+
handle.shutdown() is called from the heartbeat task.
|
|
169
140
|
"""
|
|
170
|
-
logger.info("🔒 MAIN THREAD: Installing signal handlers
|
|
141
|
+
logger.info("🔒 MAIN THREAD: Installing signal handlers")
|
|
171
142
|
|
|
172
|
-
# Install signal handlers
|
|
143
|
+
# Install signal handlers
|
|
173
144
|
_simple_shutdown_coordinator.install_signal_handlers()
|
|
174
145
|
|
|
175
146
|
logger.info(
|
|
@@ -184,34 +155,21 @@ def start_blocking_loop_with_shutdown_support(thread) -> None:
|
|
|
184
155
|
# Check if shutdown was requested via signal
|
|
185
156
|
if _simple_shutdown_coordinator.is_shutdown_requested():
|
|
186
157
|
logger.info(
|
|
187
|
-
"🔄 MAIN THREAD: Shutdown requested,
|
|
158
|
+
"🔄 MAIN THREAD: Shutdown requested, signaling heartbeat to stop..."
|
|
159
|
+
)
|
|
160
|
+
# Mark shutdown complete so heartbeat task will call handle.shutdown()
|
|
161
|
+
# which triggers Rust core to deregister from registry
|
|
162
|
+
_simple_shutdown_coordinator.mark_shutdown_complete()
|
|
163
|
+
logger.info(
|
|
164
|
+
"🏁 MAIN THREAD: Shutdown signaled, Rust core will handle deregistration"
|
|
188
165
|
)
|
|
189
|
-
|
|
190
|
-
# Perform registry cleanup in main thread
|
|
191
|
-
import asyncio
|
|
192
|
-
|
|
193
|
-
try:
|
|
194
|
-
# Run cleanup in main thread
|
|
195
|
-
asyncio.run(_simple_shutdown_coordinator.perform_registry_cleanup())
|
|
196
|
-
except Exception as e:
|
|
197
|
-
logger.error(f"❌ Registry cleanup error: {e}")
|
|
198
|
-
|
|
199
|
-
logger.info("🏁 MAIN THREAD: Registry cleanup completed, exiting")
|
|
200
166
|
break
|
|
201
167
|
|
|
202
168
|
except KeyboardInterrupt:
|
|
169
|
+
logger.info("🔄 MAIN THREAD: KeyboardInterrupt received, signaling shutdown...")
|
|
170
|
+
_simple_shutdown_coordinator.mark_shutdown_complete()
|
|
203
171
|
logger.info(
|
|
204
|
-
"
|
|
172
|
+
"🏁 MAIN THREAD: Shutdown signaled, Rust core will handle deregistration"
|
|
205
173
|
)
|
|
206
174
|
|
|
207
|
-
# Perform registry cleanup on Ctrl+C
|
|
208
|
-
import asyncio
|
|
209
|
-
|
|
210
|
-
try:
|
|
211
|
-
asyncio.run(_simple_shutdown_coordinator.perform_registry_cleanup())
|
|
212
|
-
except Exception as e:
|
|
213
|
-
logger.error(f"❌ Registry cleanup error: {e}")
|
|
214
|
-
|
|
215
|
-
logger.info("🏁 MAIN THREAD: Registry cleanup completed")
|
|
216
|
-
|
|
217
175
|
logger.info("🏁 MAIN THREAD: Uvicorn thread completed")
|
|
@@ -10,12 +10,8 @@ from collections.abc import Callable
|
|
|
10
10
|
from typing import Any, Optional
|
|
11
11
|
|
|
12
12
|
# Import shared utilities at module level to avoid circular imports during execution
|
|
13
|
-
from .utils import (
|
|
14
|
-
|
|
15
|
-
get_agent_metadata_with_fallback,
|
|
16
|
-
is_tracing_enabled,
|
|
17
|
-
publish_trace_with_fallback,
|
|
18
|
-
)
|
|
13
|
+
from .utils import (generate_span_id, get_agent_metadata_with_fallback,
|
|
14
|
+
is_tracing_enabled, publish_trace_with_fallback)
|
|
19
15
|
|
|
20
16
|
logger = logging.getLogger(__name__)
|
|
21
17
|
|