mcp-mesh 0.5.4__py3-none-any.whl → 0.5.6__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 +5 -2
- _mcp_mesh/engine/decorator_registry.py +95 -0
- _mcp_mesh/engine/mcp_client_proxy.py +17 -7
- _mcp_mesh/engine/unified_mcp_proxy.py +43 -40
- _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +4 -167
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +4 -0
- _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +13 -0
- _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -0
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +306 -163
- _mcp_mesh/pipeline/mcp_startup/server_discovery.py +164 -0
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +198 -160
- _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +7 -4
- _mcp_mesh/pipeline/shared/mesh_pipeline.py +4 -0
- _mcp_mesh/shared/server_discovery.py +312 -0
- _mcp_mesh/shared/simple_shutdown.py +217 -0
- {mcp_mesh-0.5.4.dist-info → mcp_mesh-0.5.6.dist-info}/METADATA +1 -1
- {mcp_mesh-0.5.4.dist-info → mcp_mesh-0.5.6.dist-info}/RECORD +20 -18
- mesh/decorators.py +303 -36
- _mcp_mesh/engine/threading_utils.py +0 -223
- {mcp_mesh-0.5.4.dist-info → mcp_mesh-0.5.6.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.5.4.dist-info → mcp_mesh-0.5.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utilities for discovering existing FastAPI and uvicorn server instances.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to discover running servers that have been started
|
|
5
|
+
outside the pipeline, such as by immediate uvicorn start in decorators.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import gc
|
|
9
|
+
import logging
|
|
10
|
+
import socket
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
12
|
+
import threading
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ServerDiscoveryUtil:
|
|
18
|
+
"""Utility class for discovering existing FastAPI apps and uvicorn servers."""
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def discover_fastapi_instances() -> Dict[str, Dict[str, Any]]:
|
|
22
|
+
"""
|
|
23
|
+
Discover FastAPI application instances in the Python runtime.
|
|
24
|
+
|
|
25
|
+
Uses intelligent deduplication to handle standard uvicorn patterns where
|
|
26
|
+
the same app might be imported multiple times (e.g., "module:app" pattern).
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dict mapping app_id -> app_info where app_info contains:
|
|
30
|
+
- 'instance': The FastAPI app instance
|
|
31
|
+
- 'title': App title from FastAPI
|
|
32
|
+
- 'routes': List of route information
|
|
33
|
+
- 'module': Module where app was found
|
|
34
|
+
"""
|
|
35
|
+
fastapi_apps = {}
|
|
36
|
+
seen_apps = {} # For deduplication: title -> app_info
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# Import FastAPI here to avoid dependency if not used
|
|
40
|
+
from fastapi import FastAPI
|
|
41
|
+
except ImportError:
|
|
42
|
+
logger.warning("FastAPI not installed - cannot discover FastAPI apps")
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
# Scan garbage collector for FastAPI instances
|
|
46
|
+
candidate_apps = []
|
|
47
|
+
for obj in gc.get_objects():
|
|
48
|
+
if isinstance(obj, FastAPI):
|
|
49
|
+
candidate_apps.append(obj)
|
|
50
|
+
|
|
51
|
+
# Deduplicate apps with identical configurations
|
|
52
|
+
for obj in candidate_apps:
|
|
53
|
+
try:
|
|
54
|
+
title = getattr(obj, "title", "FastAPI App")
|
|
55
|
+
version = getattr(obj, "version", "unknown")
|
|
56
|
+
routes = ServerDiscoveryUtil._extract_route_info(obj)
|
|
57
|
+
route_count = len(routes)
|
|
58
|
+
|
|
59
|
+
# Create a signature for deduplication
|
|
60
|
+
app_signature = (title, version, route_count)
|
|
61
|
+
|
|
62
|
+
# Check if we've seen an identical app
|
|
63
|
+
if app_signature in seen_apps:
|
|
64
|
+
existing_app = seen_apps[app_signature]
|
|
65
|
+
# Compare route details to ensure they're truly identical
|
|
66
|
+
existing_routes = existing_app["routes"]
|
|
67
|
+
|
|
68
|
+
if ServerDiscoveryUtil._routes_are_identical(routes, existing_routes):
|
|
69
|
+
logger.debug(
|
|
70
|
+
f"Skipping duplicate FastAPI app: '{title}' (same title, version, and routes)"
|
|
71
|
+
)
|
|
72
|
+
continue # Skip this duplicate
|
|
73
|
+
|
|
74
|
+
# This is a unique app, add it
|
|
75
|
+
app_id = f"app_{id(obj)}"
|
|
76
|
+
app_info = {
|
|
77
|
+
"instance": obj,
|
|
78
|
+
"title": title,
|
|
79
|
+
"version": version,
|
|
80
|
+
"routes": routes,
|
|
81
|
+
"module": ServerDiscoveryUtil._get_app_module(obj),
|
|
82
|
+
"object_id": id(obj),
|
|
83
|
+
"router_routes_count": len(obj.router.routes) if hasattr(obj, 'router') else 0,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fastapi_apps[app_id] = app_info
|
|
87
|
+
seen_apps[app_signature] = app_info
|
|
88
|
+
|
|
89
|
+
logger.debug(
|
|
90
|
+
f"Found FastAPI app: '{title}' (module: {app_info['module']}) with "
|
|
91
|
+
f"{len(routes)} routes"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.warning(f"Error analyzing FastAPI app: {e}")
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
return fastapi_apps
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def discover_running_servers() -> List[Dict[str, Any]]:
|
|
102
|
+
"""
|
|
103
|
+
Discover running uvicorn servers by scanning threads and checking port bindings.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List of server info dictionaries containing:
|
|
107
|
+
- 'type': 'uvicorn' or 'unknown'
|
|
108
|
+
- 'host': Server host
|
|
109
|
+
- 'port': Server port
|
|
110
|
+
- 'thread': Thread object if found
|
|
111
|
+
- 'app': FastAPI app if discoverable
|
|
112
|
+
"""
|
|
113
|
+
running_servers = []
|
|
114
|
+
|
|
115
|
+
# Look for uvicorn server threads
|
|
116
|
+
for thread in threading.enumerate():
|
|
117
|
+
if hasattr(thread, '_target'):
|
|
118
|
+
# Check if thread target looks like a uvicorn server
|
|
119
|
+
target_name = getattr(thread._target, '__name__', '') if thread._target else ''
|
|
120
|
+
if 'server' in target_name.lower() or 'uvicorn' in target_name.lower():
|
|
121
|
+
server_info = {
|
|
122
|
+
'type': 'uvicorn',
|
|
123
|
+
'thread': thread,
|
|
124
|
+
'target_name': target_name,
|
|
125
|
+
'daemon': thread.daemon,
|
|
126
|
+
'alive': thread.is_alive(),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Try to extract server details from thread
|
|
130
|
+
server_details = ServerDiscoveryUtil._extract_server_details_from_thread(thread)
|
|
131
|
+
server_info.update(server_details)
|
|
132
|
+
|
|
133
|
+
running_servers.append(server_info)
|
|
134
|
+
logger.debug(f"Found running server thread: {target_name} (daemon={thread.daemon})")
|
|
135
|
+
|
|
136
|
+
# Also check for bound ports that might indicate running servers
|
|
137
|
+
bound_ports = ServerDiscoveryUtil._discover_bound_ports()
|
|
138
|
+
for port_info in bound_ports:
|
|
139
|
+
# Only add if we haven't already found this port via thread discovery
|
|
140
|
+
existing_ports = [s.get('port') for s in running_servers if s.get('port')]
|
|
141
|
+
if port_info['port'] not in existing_ports:
|
|
142
|
+
port_info['type'] = 'unknown'
|
|
143
|
+
running_servers.append(port_info)
|
|
144
|
+
logger.debug(f"Found bound port: {port_info['host']}:{port_info['port']}")
|
|
145
|
+
|
|
146
|
+
return running_servers
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _extract_server_details_from_thread(thread) -> Dict[str, Any]:
|
|
150
|
+
"""Extract server details from a thread if possible."""
|
|
151
|
+
details = {}
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
# Try to access thread local variables or target args
|
|
155
|
+
if hasattr(thread, '_args') and thread._args:
|
|
156
|
+
# Some uvicorn servers might have args with host/port
|
|
157
|
+
args = thread._args
|
|
158
|
+
if len(args) >= 2:
|
|
159
|
+
# Common pattern: (app, host, port) or similar
|
|
160
|
+
if isinstance(args[0], str) and ':' in args[0]:
|
|
161
|
+
# Might be "host:port" format
|
|
162
|
+
try:
|
|
163
|
+
host, port = args[0].split(':')
|
|
164
|
+
details['host'] = host
|
|
165
|
+
details['port'] = int(port)
|
|
166
|
+
except (ValueError, IndexError):
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
# Try to find FastAPI app in thread target or args
|
|
170
|
+
if hasattr(thread, '_target') and thread._target:
|
|
171
|
+
# Check if target has app attribute or if it's in closure
|
|
172
|
+
target = thread._target
|
|
173
|
+
if hasattr(target, '__closure__') and target.__closure__:
|
|
174
|
+
for cell in target.__closure__:
|
|
175
|
+
try:
|
|
176
|
+
cell_contents = cell.cell_contents
|
|
177
|
+
from fastapi import FastAPI
|
|
178
|
+
if isinstance(cell_contents, FastAPI):
|
|
179
|
+
details['app'] = cell_contents
|
|
180
|
+
details['app_title'] = getattr(cell_contents, 'title', 'Unknown')
|
|
181
|
+
break
|
|
182
|
+
except (ImportError, AttributeError):
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.debug(f"Could not extract server details from thread: {e}")
|
|
187
|
+
|
|
188
|
+
return details
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _discover_bound_ports() -> List[Dict[str, Any]]:
|
|
192
|
+
"""Discover ports that are currently bound by this process."""
|
|
193
|
+
bound_ports = []
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Common port ranges for web servers
|
|
197
|
+
common_ports = [8000, 8080, 8090, 9090, 9091, 3000, 3001, 4000, 5000]
|
|
198
|
+
|
|
199
|
+
for port in common_ports:
|
|
200
|
+
for host in ['127.0.0.1', '0.0.0.0', 'localhost']:
|
|
201
|
+
try:
|
|
202
|
+
# Try to connect to see if port is bound
|
|
203
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
204
|
+
sock.settimeout(0.1) # Very short timeout
|
|
205
|
+
result = sock.connect_ex((host, port))
|
|
206
|
+
sock.close()
|
|
207
|
+
|
|
208
|
+
if result == 0: # Connection successful = port is bound
|
|
209
|
+
bound_ports.append({
|
|
210
|
+
'host': host,
|
|
211
|
+
'port': port,
|
|
212
|
+
'status': 'bound'
|
|
213
|
+
})
|
|
214
|
+
break # Don't check other hosts for same port
|
|
215
|
+
except Exception:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.debug(f"Error discovering bound ports: {e}")
|
|
220
|
+
|
|
221
|
+
return bound_ports
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def find_server_on_port(target_port: int, target_host: str = None) -> Optional[Dict[str, Any]]:
|
|
225
|
+
"""
|
|
226
|
+
Find if there's already a server running on the specified port.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
target_port: Port to check
|
|
230
|
+
target_host: Host to check (optional)
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Server info dict if found, None otherwise
|
|
234
|
+
"""
|
|
235
|
+
running_servers = ServerDiscoveryUtil.discover_running_servers()
|
|
236
|
+
|
|
237
|
+
for server in running_servers:
|
|
238
|
+
server_port = server.get('port')
|
|
239
|
+
server_host = server.get('host')
|
|
240
|
+
|
|
241
|
+
# Check port match
|
|
242
|
+
if server_port == target_port:
|
|
243
|
+
# If target_host is specified, check host match too
|
|
244
|
+
if target_host is None or server_host == target_host or server_host in ['0.0.0.0', '127.0.0.1']:
|
|
245
|
+
logger.info(f"🔍 DISCOVERY: Found existing server on {server_host}:{server_port}")
|
|
246
|
+
return server
|
|
247
|
+
|
|
248
|
+
logger.debug(f"🔍 DISCOVERY: No existing server found on port {target_port}")
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _routes_are_identical(routes1: List[Dict[str, Any]], routes2: List[Dict[str, Any]]) -> bool:
|
|
253
|
+
"""Compare two route lists to see if they're identical."""
|
|
254
|
+
if len(routes1) != len(routes2):
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
# Create comparable signatures for each route
|
|
258
|
+
def route_signature(route):
|
|
259
|
+
return (
|
|
260
|
+
tuple(sorted(route.get('methods', []))), # Sort methods for consistent comparison
|
|
261
|
+
route.get('path', ''),
|
|
262
|
+
route.get('endpoint_name', '')
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Sort routes by signature for consistent comparison
|
|
266
|
+
sig1 = sorted([route_signature(r) for r in routes1])
|
|
267
|
+
sig2 = sorted([route_signature(r) for r in routes2])
|
|
268
|
+
|
|
269
|
+
return sig1 == sig2
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _extract_route_info(app) -> List[Dict[str, Any]]:
|
|
273
|
+
"""Extract route information from FastAPI app without modifying it."""
|
|
274
|
+
routes = []
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
for route in app.router.routes:
|
|
278
|
+
if hasattr(route, 'endpoint') and hasattr(route, 'path'):
|
|
279
|
+
route_info = {
|
|
280
|
+
"path": route.path,
|
|
281
|
+
"methods": list(route.methods) if hasattr(route, 'methods') else [],
|
|
282
|
+
"endpoint": route.endpoint,
|
|
283
|
+
"endpoint_name": getattr(route.endpoint, '__name__', 'unknown'),
|
|
284
|
+
"has_mesh_route": hasattr(route.endpoint, '_mesh_route_metadata'),
|
|
285
|
+
}
|
|
286
|
+
routes.append(route_info)
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.warning(f"Error extracting route info: {e}")
|
|
290
|
+
|
|
291
|
+
return routes
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def _get_app_module(app) -> Optional[str]:
|
|
295
|
+
"""Try to determine which module the FastAPI app belongs to."""
|
|
296
|
+
try:
|
|
297
|
+
# Try to get module from the app's stack frame when it was created
|
|
298
|
+
# This is best-effort - may not always work
|
|
299
|
+
import inspect
|
|
300
|
+
|
|
301
|
+
frame = inspect.currentframe()
|
|
302
|
+
while frame:
|
|
303
|
+
frame_globals = frame.f_globals
|
|
304
|
+
for name, obj in frame_globals.items():
|
|
305
|
+
if obj is app:
|
|
306
|
+
return frame_globals.get('__name__', 'unknown')
|
|
307
|
+
frame = frame.f_back
|
|
308
|
+
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
return None
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple shutdown coordination for MCP Mesh agents.
|
|
3
|
+
|
|
4
|
+
Provides clean shutdown via FastAPI lifespan events and basic signal handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import signal
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SimpleShutdownCoordinator:
|
|
17
|
+
"""Lightweight shutdown coordination using FastAPI lifespan."""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self._shutdown_requested = False
|
|
21
|
+
self._registry_url: Optional[str] = None
|
|
22
|
+
self._agent_id: Optional[str] = None
|
|
23
|
+
self._shutdown_complete = False # Flag to prevent race conditions
|
|
24
|
+
|
|
25
|
+
def set_shutdown_context(self, registry_url: str, agent_id: str) -> None:
|
|
26
|
+
"""Set context for shutdown cleanup."""
|
|
27
|
+
self._registry_url = registry_url
|
|
28
|
+
self._agent_id = agent_id
|
|
29
|
+
logger.debug(
|
|
30
|
+
f"🔧 Shutdown context set: agent_id={agent_id}, registry_url={registry_url}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def install_signal_handlers(self) -> None:
|
|
34
|
+
"""Install minimal signal handlers as backup."""
|
|
35
|
+
|
|
36
|
+
def shutdown_signal_handler(signum, frame):
|
|
37
|
+
# Avoid logging in signal handler to prevent reentrant call issues
|
|
38
|
+
self._shutdown_requested = True
|
|
39
|
+
|
|
40
|
+
signal.signal(signal.SIGINT, shutdown_signal_handler)
|
|
41
|
+
signal.signal(signal.SIGTERM, shutdown_signal_handler)
|
|
42
|
+
logger.debug("📡 Signal handlers installed")
|
|
43
|
+
|
|
44
|
+
def is_shutdown_requested(self) -> bool:
|
|
45
|
+
"""Check if shutdown was requested via signal."""
|
|
46
|
+
return self._shutdown_requested
|
|
47
|
+
|
|
48
|
+
def is_shutdown_complete(self) -> bool:
|
|
49
|
+
"""Check if shutdown cleanup is complete."""
|
|
50
|
+
return self._shutdown_complete
|
|
51
|
+
|
|
52
|
+
def mark_shutdown_complete(self) -> None:
|
|
53
|
+
"""Mark shutdown cleanup as complete to prevent further operations."""
|
|
54
|
+
self._shutdown_complete = True
|
|
55
|
+
logger.debug("🏁 Shutdown marked as complete")
|
|
56
|
+
|
|
57
|
+
async def perform_registry_cleanup(self) -> None:
|
|
58
|
+
"""Perform registry cleanup by calling DELETE /agents/{agent_id}."""
|
|
59
|
+
# Try to get the actual agent_id from DecoratorRegistry if available
|
|
60
|
+
actual_agent_id = self._agent_id
|
|
61
|
+
try:
|
|
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
|
|
109
|
+
|
|
110
|
+
def create_shutdown_lifespan(self, original_lifespan=None):
|
|
111
|
+
"""Create lifespan function that includes registry cleanup."""
|
|
112
|
+
|
|
113
|
+
@asynccontextmanager
|
|
114
|
+
async def shutdown_lifespan(app):
|
|
115
|
+
# Startup phase
|
|
116
|
+
if original_lifespan:
|
|
117
|
+
# If user had a lifespan, run their startup code
|
|
118
|
+
async with original_lifespan(app):
|
|
119
|
+
yield
|
|
120
|
+
else:
|
|
121
|
+
yield
|
|
122
|
+
|
|
123
|
+
# Shutdown phase
|
|
124
|
+
logger.info("🔄 FastAPI shutdown initiated, performing registry cleanup...")
|
|
125
|
+
await self.perform_registry_cleanup()
|
|
126
|
+
logger.info("🏁 Registry cleanup completed")
|
|
127
|
+
|
|
128
|
+
return shutdown_lifespan
|
|
129
|
+
|
|
130
|
+
def inject_shutdown_lifespan(self, app, registry_url: str, agent_id: str) -> None:
|
|
131
|
+
"""Inject shutdown lifespan into FastAPI app."""
|
|
132
|
+
self.set_shutdown_context(registry_url, agent_id)
|
|
133
|
+
|
|
134
|
+
# Store original lifespan if it exists
|
|
135
|
+
original_lifespan = getattr(app, "router", {}).get("lifespan", None)
|
|
136
|
+
|
|
137
|
+
# Replace with our shutdown-aware lifespan
|
|
138
|
+
new_lifespan = self.create_shutdown_lifespan(original_lifespan)
|
|
139
|
+
app.router.lifespan = new_lifespan
|
|
140
|
+
|
|
141
|
+
logger.info(f"🔌 Shutdown lifespan injected for agent '{agent_id}'")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Global instance
|
|
145
|
+
_simple_shutdown_coordinator = SimpleShutdownCoordinator()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def inject_shutdown_lifespan(app, registry_url: str, agent_id: str) -> None:
|
|
149
|
+
"""Inject shutdown lifespan into FastAPI app (module-level function)."""
|
|
150
|
+
_simple_shutdown_coordinator.inject_shutdown_lifespan(app, registry_url, agent_id)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def install_signal_handlers() -> None:
|
|
154
|
+
"""Install signal handlers (module-level function)."""
|
|
155
|
+
_simple_shutdown_coordinator.install_signal_handlers()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def should_stop_heartbeat() -> bool:
|
|
159
|
+
"""Check if heartbeat should stop due to shutdown."""
|
|
160
|
+
return _simple_shutdown_coordinator.is_shutdown_complete()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def start_blocking_loop_with_shutdown_support(thread) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Keep main thread alive while uvicorn in the thread handles requests.
|
|
166
|
+
|
|
167
|
+
Install signal handlers in main thread for proper registry cleanup since
|
|
168
|
+
signals to threads can be unreliable for FastAPI lifespan shutdown.
|
|
169
|
+
"""
|
|
170
|
+
logger.info("🔒 MAIN THREAD: Installing signal handlers for registry cleanup")
|
|
171
|
+
|
|
172
|
+
# Install signal handlers for proper registry cleanup
|
|
173
|
+
_simple_shutdown_coordinator.install_signal_handlers()
|
|
174
|
+
|
|
175
|
+
logger.info(
|
|
176
|
+
"🔒 MAIN THREAD: Waiting for uvicorn thread - signals handled by main thread"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Wait for thread while handling signals in main thread
|
|
181
|
+
while thread.is_alive():
|
|
182
|
+
thread.join(timeout=1.0)
|
|
183
|
+
|
|
184
|
+
# Check if shutdown was requested via signal
|
|
185
|
+
if _simple_shutdown_coordinator.is_shutdown_requested():
|
|
186
|
+
logger.info(
|
|
187
|
+
"🔄 MAIN THREAD: Shutdown requested, performing registry cleanup..."
|
|
188
|
+
)
|
|
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
|
+
break
|
|
201
|
+
|
|
202
|
+
except KeyboardInterrupt:
|
|
203
|
+
logger.info(
|
|
204
|
+
"🔄 MAIN THREAD: KeyboardInterrupt received, performing registry cleanup..."
|
|
205
|
+
)
|
|
206
|
+
|
|
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
|
+
logger.info("🏁 MAIN THREAD: Uvicorn thread completed")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-mesh
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: Kubernetes-native platform for distributed MCP applications
|
|
5
5
|
Project-URL: Homepage, https://github.com/dhyansraj/mcp-mesh
|
|
6
6
|
Project-URL: Documentation, https://github.com/dhyansraj/mcp-mesh/tree/main/docs
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
_mcp_mesh/__init__.py,sha256=
|
|
1
|
+
_mcp_mesh/__init__.py,sha256=uw4Yj9HoBfp-_CyH8izKGYKrrCI-dF9uwMLMNgkIMN0,2719
|
|
2
2
|
_mcp_mesh/engine/__init__.py,sha256=2ennzbo7yJcpkXO9BqN69TruLjJfmJY4Y5VEsG644K4,3630
|
|
3
3
|
_mcp_mesh/engine/async_mcp_client.py,sha256=UcbQjxtgVfeRw6DHTZhAzN1gkcKlTg-lUPEePRPQWAU,6306
|
|
4
|
-
_mcp_mesh/engine/decorator_registry.py,sha256=
|
|
4
|
+
_mcp_mesh/engine/decorator_registry.py,sha256=0-GI7DILpbB6GRLIYhZu0DcdHEA62d89jDYxkK1ywiM,22422
|
|
5
5
|
_mcp_mesh/engine/dependency_injector.py,sha256=B1iRxtIXq_z4LnJ6NTs0ZPdEIlijrMJv5KbDpQbq9iw,23752
|
|
6
6
|
_mcp_mesh/engine/full_mcp_proxy.py,sha256=PlRv7GSKqn5riOCqeCVulVdtq3z1Ug76mOkwMsOFHXw,25297
|
|
7
7
|
_mcp_mesh/engine/http_wrapper.py,sha256=PeYR7HkJVX7Hb0W-uYSMO-Y14VqCyrPlWzA6vnwf1Tw,21640
|
|
8
|
-
_mcp_mesh/engine/mcp_client_proxy.py,sha256=
|
|
8
|
+
_mcp_mesh/engine/mcp_client_proxy.py,sha256=eJStwy_VQJexYYD8bOh_m4Ld3Bb8Ae_dt8N1CC41qBc,17625
|
|
9
9
|
_mcp_mesh/engine/self_dependency_proxy.py,sha256=OkKt0-B_ADnJlWtHiHItoZCBZ7Su0iz2unEPFfXvrs4,3302
|
|
10
10
|
_mcp_mesh/engine/session_aware_client.py,sha256=mc9eh-aCvUvfllORiXTf_X8_jPqV-32QdWKlr8tHLkU,10600
|
|
11
11
|
_mcp_mesh/engine/session_manager.py,sha256=MCr0_fXBaUjXM51WU5EhDkiGvBdfzYQFVNb9DCXXL0A,10418
|
|
12
12
|
_mcp_mesh/engine/signature_analyzer.py,sha256=AxBuFq8_TCT2afw_XDF4AjWPqdG6ST5jfbw2BDdp9Mo,7482
|
|
13
|
-
_mcp_mesh/engine/
|
|
14
|
-
_mcp_mesh/engine/unified_mcp_proxy.py,sha256=r1nbJ_adRAbblDK8f-4AffxszJqkqtwvEwRKw-tczPA,36095
|
|
13
|
+
_mcp_mesh/engine/unified_mcp_proxy.py,sha256=33xm-jABTYUwr_uIB6mx4yNlNJrsvstCHJOMTdcm-pg,36365
|
|
15
14
|
_mcp_mesh/generated/.openapi-generator-ignore,sha256=-d-Y-RVAZRrHw36jO0b79oDXpfA8rZdBGPCG4Vs_rUs,227
|
|
16
15
|
_mcp_mesh/generated/.openapi-generator/FILES,sha256=BXFXHe4FTV5GFUTNjMhmvOrVcYVlHJc5O-3UXyp8OCY,2169
|
|
17
16
|
_mcp_mesh/generated/.openapi-generator/VERSION,sha256=nMm490YXJUW3_vAdeAsg7E3yRgUqVwk5-50PuaFonM8,7
|
|
@@ -69,30 +68,31 @@ _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py,sha256=6N0JdXdnLkaXa
|
|
|
69
68
|
_mcp_mesh/pipeline/api_startup/__init__.py,sha256=eivolkSKot2bJTWP2BV8-RKRT1Zm7SGQYuEUiTxusOQ,577
|
|
70
69
|
_mcp_mesh/pipeline/api_startup/api_pipeline.py,sha256=w4m7LP7qnpLxYYWy4moa_8inAWFT0jWy9i7G9WGWOCM,2546
|
|
71
70
|
_mcp_mesh/pipeline/api_startup/api_server_setup.py,sha256=Qy0wbXyIWIQYA7CjiGVZwn0nWCKK85ZzFTRI2JDA9Aw,15099
|
|
72
|
-
_mcp_mesh/pipeline/api_startup/fastapi_discovery.py,sha256=
|
|
71
|
+
_mcp_mesh/pipeline/api_startup/fastapi_discovery.py,sha256=MV3hvDXvX7r1Mrn6LAReu9W3hKt-5-jDhPpPYwZXnco,5770
|
|
73
72
|
_mcp_mesh/pipeline/api_startup/middleware_integration.py,sha256=ybImXZlmIR6yA-wYg5Zy_ZMFF9YgToLkk4jnBeZJ7WY,6267
|
|
74
73
|
_mcp_mesh/pipeline/api_startup/route_collection.py,sha256=UjA-F5_RbGVU5TfDT19Np5_x2PtYkNn2mGFyivDsk24,2031
|
|
75
74
|
_mcp_mesh/pipeline/api_startup/route_integration.py,sha256=aMT7p7cwK8N3tZBRqeGQF8upc7tU-Exj6Dz0a4cSBhU,13441
|
|
76
75
|
_mcp_mesh/pipeline/mcp_heartbeat/__init__.py,sha256=nRNjZ3VD_9bPLQuJ6Nc02gE7KSLcMP7TMquB0hP6hHs,844
|
|
77
76
|
_mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py,sha256=nHjKSM4Yyn05wnfw1dI4VGroHZy8lRkDihnZfFHeF8U,16217
|
|
78
77
|
_mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py,sha256=QTzYdL81WERkOaBVOgNbFQh1ddTn70urNtyIgtFTudA,4465
|
|
79
|
-
_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py,sha256=
|
|
78
|
+
_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py,sha256=uB9o298X7GbOaKUw4ij_fUAOCpK0n2brx_oWqWGTXFY,11296
|
|
80
79
|
_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py,sha256=Jb7EVJO13trUVO3aCSgzGqAtoc4vie5kDrYLZtOkiXg,11601
|
|
81
80
|
_mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py,sha256=ydVx-Vb_RgW1WPCboHVdZfEwNbgDngFV6Y9elZIBrAw,3602
|
|
82
|
-
_mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py,sha256=
|
|
81
|
+
_mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py,sha256=4XPPlaJ6rz-FkDO3bxzVxHmVF-aq1CCaTW4vIBXrB0c,3016
|
|
83
82
|
_mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py,sha256=4abbOKN3echwX82PV0RvxF6cJZUu0pMgisOpILZ_ZzY,2875
|
|
84
|
-
_mcp_mesh/pipeline/mcp_startup/__init__.py,sha256=
|
|
83
|
+
_mcp_mesh/pipeline/mcp_startup/__init__.py,sha256=gS0xNmVx66bkLUMw64olMsN40ZLPH3ymwlLixZ4NuTs,1239
|
|
85
84
|
_mcp_mesh/pipeline/mcp_startup/configuration.py,sha256=6LRLIxrqFMU76qrBb6GjGknUlKPZZ9iqOlxE7F9ZhLs,2808
|
|
86
85
|
_mcp_mesh/pipeline/mcp_startup/decorator_collection.py,sha256=RHC6MHtfP9aP0hZ-IJjISZu72e0Pml3LU0qr7dc284w,2294
|
|
87
|
-
_mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py,sha256=
|
|
86
|
+
_mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py,sha256=ktCn1IB8J3Iz7T0iUJF_ytwwO_RRbJN3dNQ6qZLY6iI,40229
|
|
88
87
|
_mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py,sha256=ktsE9EZYdyZbCtCKB6HVdzGFMQ0E9n0-7I55LRO99sE,10270
|
|
89
88
|
_mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py,sha256=v85B0ynomvYu87eIvLe-aSZ7-Iwov2VtM4Fg3PkmrZs,3865
|
|
90
89
|
_mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py,sha256=v3Fl0PvW5s7Ib_Cy7WtXA7gDvsFGiz54a-IlQRTcLPg,10410
|
|
91
|
-
_mcp_mesh/pipeline/mcp_startup/
|
|
92
|
-
_mcp_mesh/pipeline/mcp_startup/
|
|
90
|
+
_mcp_mesh/pipeline/mcp_startup/server_discovery.py,sha256=i3t12Dd2nEg3nbifyMGvm2SYr3WYiYJbicBakS3ZeuM,8007
|
|
91
|
+
_mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py,sha256=KldSG3xfGNm0iexnCMPkDsi3nuIVXBwneDBuoT5gJO4,26756
|
|
92
|
+
_mcp_mesh/pipeline/mcp_startup/startup_pipeline.py,sha256=Y_VeZcvyT3y9phWtGD7cX92NzKZIzF2J6kRJUO8q-9U,2291
|
|
93
93
|
_mcp_mesh/pipeline/shared/__init__.py,sha256=s9xmdf6LkoetrVRGd7Zp3NUxcJCW6YZ_yNKzUBcnYys,352
|
|
94
94
|
_mcp_mesh/pipeline/shared/base_step.py,sha256=kyPbNUX79NyGrE_0Q-e-Aek7m1J0TW036njWfv0iZ0I,1080
|
|
95
|
-
_mcp_mesh/pipeline/shared/mesh_pipeline.py,sha256=
|
|
95
|
+
_mcp_mesh/pipeline/shared/mesh_pipeline.py,sha256=UlQBrPWqbruFiUdVYgFKgPOpp_sMVsO97nZsWX90k9U,6498
|
|
96
96
|
_mcp_mesh/pipeline/shared/pipeline_types.py,sha256=iKSgzCoYu3bpIwLViw6lE7T7irEzOm7gpie29lyy7SQ,1504
|
|
97
97
|
_mcp_mesh/pipeline/shared/registry_connection.py,sha256=jmlgULixRM1NMNyMc-8SJN3RpCtV9hUf76vn9nciAN4,2904
|
|
98
98
|
_mcp_mesh/shared/__init__.py,sha256=L0detgEWjnc_XfxA_5vIBcaGTuLcT6AARKkUpUBIaH4,1116
|
|
@@ -104,6 +104,8 @@ _mcp_mesh/shared/fastapi_middleware_manager.py,sha256=_h10dSL9mgQstpJW_ZM2cpkU6y
|
|
|
104
104
|
_mcp_mesh/shared/host_resolver.py,sha256=ycs6gXnI1zJX5KiqiLJPX5GkHX8r4j8NMHQOlG2J2X8,2964
|
|
105
105
|
_mcp_mesh/shared/logging_config.py,sha256=n9AqShZ5BZgyrkoTlvux6ECRVpM9dUYvmGB0NPMl-Ak,2477
|
|
106
106
|
_mcp_mesh/shared/registry_client_wrapper.py,sha256=d8yL-MiCrQr_WYdRFStOd531qaLv9kZjh0zJAmCJ-Cc,16976
|
|
107
|
+
_mcp_mesh/shared/server_discovery.py,sha256=W5nsN-GvEVFD-7XkbMTxh-9FUIEiyWOxP3GYr8GNi3E,13142
|
|
108
|
+
_mcp_mesh/shared/simple_shutdown.py,sha256=jnF1rTR2yR619LZnEjNlu-ZdKlB3PovxKqG0VZ3HDgE,8319
|
|
107
109
|
_mcp_mesh/shared/sse_parser.py,sha256=OEPnfL9xL3rsjQrbyvfUO82WljPSDeO6Z61uUwN1NAo,8035
|
|
108
110
|
_mcp_mesh/shared/support_types.py,sha256=k-ICF_UwDkHxQ1D5LwFZrp-UrNb4E5dzw02CRuLW9iI,7264
|
|
109
111
|
_mcp_mesh/tracing/agent_context_helper.py,sha256=BIJ3Kc4Znd6emMAu97aUhSoxSIza3qYUmObLgc9ONiA,4765
|
|
@@ -114,9 +116,9 @@ _mcp_mesh/tracing/redis_metadata_publisher.py,sha256=F78E34qnI3D0tOmbHUTBsLbDst2
|
|
|
114
116
|
_mcp_mesh/tracing/trace_context_helper.py,sha256=6tEkwjWFqMBe45zBlhacktmIpzJWTF950ph3bwL3cNc,5994
|
|
115
117
|
_mcp_mesh/tracing/utils.py,sha256=t9lJuTH7CeuzAiiAaD0WxsJMFJPdzZFR0w6-vyR9f2E,3849
|
|
116
118
|
mesh/__init__.py,sha256=l5RSMV8Kx0h7cvku8YkZPbTHjEPWciGT0bcEB2O_eNU,3242
|
|
117
|
-
mesh/decorators.py,sha256=
|
|
119
|
+
mesh/decorators.py,sha256=oBWoRE-FA3qUGygAUtk3-eAYBckwTGfTzvXOgCag4ys,36678
|
|
118
120
|
mesh/types.py,sha256=g37DXAzya-xGPa1_WKlW3T3_VqyTn8ZVepIDSrhBTkc,10815
|
|
119
|
-
mcp_mesh-0.5.
|
|
120
|
-
mcp_mesh-0.5.
|
|
121
|
-
mcp_mesh-0.5.
|
|
122
|
-
mcp_mesh-0.5.
|
|
121
|
+
mcp_mesh-0.5.6.dist-info/METADATA,sha256=Mh8nwL5wjm53WMFPOqcVHCRmcvbPa4sgMmX7bQV-yiw,4879
|
|
122
|
+
mcp_mesh-0.5.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
123
|
+
mcp_mesh-0.5.6.dist-info/licenses/LICENSE,sha256=_EBQHRQThv9FPOLc5eFOUdeeRO0mYwChC7cx60dM1tM,1078
|
|
124
|
+
mcp_mesh-0.5.6.dist-info/RECORD,,
|