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
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
"""Threading utilities for sync-to-async bridging with atexit bug fixes.
|
|
2
|
-
|
|
3
|
-
Provides a consolidated implementation for running async operations from sync contexts
|
|
4
|
-
while avoiding the Python 3.8+ atexit bug that occurs in daemon thread contexts.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
import logging
|
|
9
|
-
import queue
|
|
10
|
-
import threading
|
|
11
|
-
from collections.abc import Callable
|
|
12
|
-
from typing import Any, Union
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ThreadingUtils:
|
|
18
|
-
"""Utilities for safe sync-to-async bridging avoiding Python 3.8+ atexit issues."""
|
|
19
|
-
|
|
20
|
-
@staticmethod
|
|
21
|
-
def run_sync_from_async(
|
|
22
|
-
coro_or_func: Union[Any, Callable],
|
|
23
|
-
timeout: float = 60.0,
|
|
24
|
-
context_name: str = "operation",
|
|
25
|
-
) -> Any:
|
|
26
|
-
"""Convert async coroutine to sync call avoiding atexit bug.
|
|
27
|
-
|
|
28
|
-
Handles both coroutines and coroutine creation functions safely.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
coro_or_func: Either a coroutine object or a function that returns a coroutine
|
|
32
|
-
timeout: Operation timeout in seconds
|
|
33
|
-
context_name: Name for logging/debugging context
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
The result of the async operation
|
|
37
|
-
|
|
38
|
-
Raises:
|
|
39
|
-
TimeoutError: If operation exceeds timeout
|
|
40
|
-
RuntimeError: If operation fails or returns no result
|
|
41
|
-
"""
|
|
42
|
-
import inspect
|
|
43
|
-
|
|
44
|
-
# If it's a function, call it to get the coroutine
|
|
45
|
-
if callable(coro_or_func) and not inspect.iscoroutine(coro_or_func):
|
|
46
|
-
try:
|
|
47
|
-
loop = asyncio.get_event_loop()
|
|
48
|
-
if loop.is_running():
|
|
49
|
-
# In running loop context, use thread-safe approach
|
|
50
|
-
return ThreadingUtils._run_in_thread_safe(
|
|
51
|
-
coro_or_func, timeout, context_name
|
|
52
|
-
)
|
|
53
|
-
else:
|
|
54
|
-
# No running loop, create coroutine and run directly
|
|
55
|
-
coro = coro_or_func()
|
|
56
|
-
return loop.run_until_complete(coro)
|
|
57
|
-
except RuntimeError:
|
|
58
|
-
# No event loop, use thread-safe approach
|
|
59
|
-
return ThreadingUtils._run_in_thread_safe(
|
|
60
|
-
coro_or_func, timeout, context_name
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
# It's already a coroutine, handle it
|
|
64
|
-
coro = coro_or_func
|
|
65
|
-
|
|
66
|
-
try:
|
|
67
|
-
loop = asyncio.get_event_loop()
|
|
68
|
-
if loop.is_running():
|
|
69
|
-
# Use thread-safe approach for running loops
|
|
70
|
-
return ThreadingUtils._run_coroutine_in_thread(
|
|
71
|
-
coro, timeout, context_name
|
|
72
|
-
)
|
|
73
|
-
else:
|
|
74
|
-
# No running loop, safe to use directly
|
|
75
|
-
return loop.run_until_complete(coro)
|
|
76
|
-
except RuntimeError:
|
|
77
|
-
# No event loop exists, use thread-safe approach
|
|
78
|
-
return ThreadingUtils._run_coroutine_in_thread(coro, timeout, context_name)
|
|
79
|
-
|
|
80
|
-
@staticmethod
|
|
81
|
-
def _run_in_thread_safe(
|
|
82
|
-
coro_func: Callable, timeout: float, context_name: str
|
|
83
|
-
) -> Any:
|
|
84
|
-
"""Run coroutine creation function in thread-safe manner."""
|
|
85
|
-
result_queue: queue.Queue = queue.Queue()
|
|
86
|
-
|
|
87
|
-
def _thread_runner():
|
|
88
|
-
"""Execute coroutine function in isolated thread context."""
|
|
89
|
-
try:
|
|
90
|
-
# Apply atexit bypass for this thread
|
|
91
|
-
ThreadingUtils._apply_atexit_bypass()
|
|
92
|
-
|
|
93
|
-
# Create fresh event loop
|
|
94
|
-
new_loop = asyncio.new_event_loop()
|
|
95
|
-
asyncio.set_event_loop(new_loop)
|
|
96
|
-
|
|
97
|
-
try:
|
|
98
|
-
# Create coroutine in this thread's context
|
|
99
|
-
coro = coro_func()
|
|
100
|
-
result = new_loop.run_until_complete(coro)
|
|
101
|
-
result_queue.put(("success", result))
|
|
102
|
-
except Exception as e:
|
|
103
|
-
logger.error(
|
|
104
|
-
f"❌ {context_name} failed in thread: {e}", exc_info=True
|
|
105
|
-
)
|
|
106
|
-
result_queue.put(("error", e))
|
|
107
|
-
finally:
|
|
108
|
-
# Manual cleanup without relying on atexit
|
|
109
|
-
try:
|
|
110
|
-
new_loop.close()
|
|
111
|
-
finally:
|
|
112
|
-
asyncio.set_event_loop(None)
|
|
113
|
-
|
|
114
|
-
except Exception as e:
|
|
115
|
-
logger.error(
|
|
116
|
-
f"❌ {context_name} thread setup failed: {e}", exc_info=True
|
|
117
|
-
)
|
|
118
|
-
result_queue.put(("error", e))
|
|
119
|
-
|
|
120
|
-
# Use non-daemon thread to avoid atexit issues
|
|
121
|
-
thread = threading.Thread(
|
|
122
|
-
target=_thread_runner, daemon=False, name=f"MCPMesh-{context_name}"
|
|
123
|
-
)
|
|
124
|
-
thread.start()
|
|
125
|
-
thread.join(timeout=timeout)
|
|
126
|
-
|
|
127
|
-
if thread.is_alive():
|
|
128
|
-
logger.error(f"⏰ {context_name} timed out after {timeout}s")
|
|
129
|
-
raise TimeoutError(f"{context_name} timed out after {timeout}s")
|
|
130
|
-
|
|
131
|
-
if result_queue.empty():
|
|
132
|
-
raise RuntimeError(f"No result from {context_name}")
|
|
133
|
-
|
|
134
|
-
status, result = result_queue.get()
|
|
135
|
-
if status == "error":
|
|
136
|
-
raise result
|
|
137
|
-
|
|
138
|
-
logger.debug(f"✅ {context_name} completed successfully in thread")
|
|
139
|
-
return result
|
|
140
|
-
|
|
141
|
-
@staticmethod
|
|
142
|
-
def _run_coroutine_in_thread(coro: Any, timeout: float, context_name: str) -> Any:
|
|
143
|
-
"""Run existing coroutine in thread-safe manner."""
|
|
144
|
-
result_queue: queue.Queue = queue.Queue()
|
|
145
|
-
|
|
146
|
-
def _thread_runner():
|
|
147
|
-
"""Execute coroutine in isolated thread context."""
|
|
148
|
-
try:
|
|
149
|
-
# Apply atexit bypass for this thread
|
|
150
|
-
ThreadingUtils._apply_atexit_bypass()
|
|
151
|
-
|
|
152
|
-
# Create fresh event loop
|
|
153
|
-
new_loop = asyncio.new_event_loop()
|
|
154
|
-
asyncio.set_event_loop(new_loop)
|
|
155
|
-
|
|
156
|
-
try:
|
|
157
|
-
result = new_loop.run_until_complete(coro)
|
|
158
|
-
result_queue.put(("success", result))
|
|
159
|
-
except Exception as e:
|
|
160
|
-
logger.error(
|
|
161
|
-
f"❌ {context_name} coroutine failed: {e}", exc_info=True
|
|
162
|
-
)
|
|
163
|
-
result_queue.put(("error", e))
|
|
164
|
-
finally:
|
|
165
|
-
# Manual cleanup without relying on atexit
|
|
166
|
-
try:
|
|
167
|
-
new_loop.close()
|
|
168
|
-
finally:
|
|
169
|
-
asyncio.set_event_loop(None)
|
|
170
|
-
|
|
171
|
-
except Exception as e:
|
|
172
|
-
logger.error(
|
|
173
|
-
f"❌ {context_name} thread setup failed: {e}", exc_info=True
|
|
174
|
-
)
|
|
175
|
-
result_queue.put(("error", e))
|
|
176
|
-
|
|
177
|
-
# Use non-daemon thread to avoid atexit issues
|
|
178
|
-
thread = threading.Thread(
|
|
179
|
-
target=_thread_runner, daemon=False, name=f"MCPMesh-{context_name}"
|
|
180
|
-
)
|
|
181
|
-
thread.start()
|
|
182
|
-
thread.join(timeout=timeout)
|
|
183
|
-
|
|
184
|
-
if thread.is_alive():
|
|
185
|
-
logger.error(f"⏰ {context_name} timed out after {timeout}s")
|
|
186
|
-
raise TimeoutError(f"{context_name} timed out after {timeout}s")
|
|
187
|
-
|
|
188
|
-
if result_queue.empty():
|
|
189
|
-
raise RuntimeError(f"No result from {context_name}")
|
|
190
|
-
|
|
191
|
-
status, result = result_queue.get()
|
|
192
|
-
if status == "error":
|
|
193
|
-
raise result
|
|
194
|
-
|
|
195
|
-
logger.debug(f"✅ {context_name} completed successfully in thread")
|
|
196
|
-
return result
|
|
197
|
-
|
|
198
|
-
@staticmethod
|
|
199
|
-
def _apply_atexit_bypass():
|
|
200
|
-
"""Apply atexit bypass for current thread to prevent registration errors."""
|
|
201
|
-
try:
|
|
202
|
-
import atexit
|
|
203
|
-
import threading
|
|
204
|
-
|
|
205
|
-
# Store originals
|
|
206
|
-
original_atexit_register = atexit.register
|
|
207
|
-
original_thread_register = getattr(threading, "_register_atexit", None)
|
|
208
|
-
|
|
209
|
-
# Apply temporary bypass
|
|
210
|
-
def _noop_register(*args, **kwargs):
|
|
211
|
-
"""No-op atexit registration to prevent threading issues."""
|
|
212
|
-
pass
|
|
213
|
-
|
|
214
|
-
atexit.register = _noop_register
|
|
215
|
-
if original_thread_register:
|
|
216
|
-
threading._register_atexit = _noop_register
|
|
217
|
-
|
|
218
|
-
# Store cleanup function for potential restoration
|
|
219
|
-
# (In practice, these threads are short-lived so restoration isn't critical)
|
|
220
|
-
|
|
221
|
-
except Exception as e:
|
|
222
|
-
# If atexit bypass fails, log but continue
|
|
223
|
-
logger.warning(f"⚠️ Failed to apply atexit bypass: {e}")
|
|
File without changes
|
|
File without changes
|