mcp-mesh 0.7.15__py3-none-any.whl → 0.7.17__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 CHANGED
@@ -31,7 +31,7 @@ from .engine.decorator_registry import (
31
31
  get_decorator_stats,
32
32
  )
33
33
 
34
- __version__ = "0.7.15"
34
+ __version__ = "0.7.17"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -553,6 +553,7 @@ mcp_mesh_up{{agent="{agent_name}"}} 1
553
553
  port=bind_port,
554
554
  log_level="info",
555
555
  access_log=False, # Reduce noise
556
+ ws="websockets-sansio", # Use modern websockets API (avoids deprecation warnings)
556
557
  )
557
558
 
558
559
  # Create and start server
@@ -319,6 +319,7 @@ class DebounceCoordinator:
319
319
  port=bind_port,
320
320
  log_level="info",
321
321
  access_log=False, # Reduce noise
322
+ ws="websockets-sansio", # Use modern websockets API (avoids deprecation warnings)
322
323
  )
323
324
 
324
325
  except KeyboardInterrupt:
_mcp_mesh/reload.py ADDED
@@ -0,0 +1,206 @@
1
+ """
2
+ File watch and reload functionality for MCP Mesh agents.
3
+
4
+ Provides automatic restart of agent processes when source files change.
5
+ Used by `meshctl start --watch` for development workflows.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ import signal
11
+ import subprocess
12
+ import sys
13
+ import threading
14
+ import time
15
+ from pathlib import Path
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Debounce delay to batch rapid file changes (seconds)
20
+ DEBOUNCE_DELAY = float(os.getenv("MCP_MESH_RELOAD_DEBOUNCE", "0.5"))
21
+
22
+ # Delay after stopping process to allow port release (seconds)
23
+ PORT_RELEASE_DELAY = float(os.getenv("MCP_MESH_RELOAD_PORT_DELAY", "0.5"))
24
+
25
+
26
+ def get_watch_paths(script_path: str) -> list[Path]:
27
+ """
28
+ Determine which paths to watch for changes.
29
+
30
+ Watches the directory containing the agent script and all subdirectories.
31
+ """
32
+ script_dir = Path(script_path).parent.absolute()
33
+ return [script_dir]
34
+
35
+
36
+ def should_watch_file(path: str) -> bool:
37
+ """
38
+ Filter function to determine which files to watch.
39
+
40
+ Only watches:
41
+ - Python files (.py)
42
+ - Jinja2 templates (.jinja2, .j2)
43
+ - YAML config files (.yaml, .yml)
44
+
45
+ Excludes common non-source directories like __pycache__, .git, .venv, etc.
46
+ Also excludes hidden files and editor temporary files.
47
+ """
48
+ path_obj = Path(path)
49
+ filename = path_obj.name
50
+
51
+ # Skip hidden files and editor temp files (e.g., .!12345!main.py from sed -i)
52
+ if filename.startswith("."):
53
+ return False
54
+
55
+ # Skip common editor backup/temp files
56
+ if filename.endswith("~") or filename.endswith(".swp") or filename.endswith(".tmp"):
57
+ return False
58
+
59
+ path_parts = {p.lower() for p in path_obj.parts}
60
+
61
+ # Skip common non-source directories (case-insensitive, exact component match)
62
+ skip_patterns = [
63
+ "__pycache__",
64
+ ".git",
65
+ ".venv",
66
+ "venv",
67
+ ".pytest_cache",
68
+ ".mypy_cache",
69
+ "node_modules",
70
+ ".eggs",
71
+ ".egg-info",
72
+ ]
73
+
74
+ for pattern in skip_patterns:
75
+ if pattern.lower() in path_parts:
76
+ return False
77
+
78
+ # Only watch specific file types
79
+ watch_extensions = [".py", ".jinja2", ".j2", ".yaml", ".yml"]
80
+ return path_obj.suffix.lower() in watch_extensions
81
+
82
+
83
+ def terminate_process(process: subprocess.Popen, timeout: int = 3) -> None:
84
+ """
85
+ Gracefully terminate a process and all its children, force kill if needed.
86
+
87
+ Args:
88
+ process: The subprocess to terminate
89
+ timeout: Seconds to wait before force killing
90
+ """
91
+ if process is None or process.poll() is not None:
92
+ return
93
+
94
+ pid = process.pid
95
+
96
+ # Try graceful termination first - send to process group to include children
97
+ try:
98
+ os.killpg(pid, signal.SIGTERM)
99
+ except (ProcessLookupError, PermissionError):
100
+ # Process group doesn't exist or we can't signal it, try direct
101
+ process.terminate()
102
+
103
+ try:
104
+ process.wait(timeout=timeout)
105
+ except subprocess.TimeoutExpired:
106
+ logger.warning(
107
+ f"Process {pid} didn't terminate gracefully, force killing..."
108
+ )
109
+ # Force kill the entire process group
110
+ try:
111
+ os.killpg(pid, signal.SIGKILL)
112
+ except (ProcessLookupError, PermissionError):
113
+ process.kill()
114
+ process.wait()
115
+
116
+
117
+ def run_with_reload(script_path: str) -> None:
118
+ """
119
+ Run the agent script with file watching and auto-reload.
120
+
121
+ Uses watchfiles to monitor the agent directory for changes.
122
+ When files change, the agent process is terminated and restarted.
123
+
124
+ Args:
125
+ script_path: Path to the agent's main.py script
126
+ """
127
+ try:
128
+ import watchfiles
129
+ except ImportError:
130
+ logger.error(
131
+ "watchfiles is required for reload mode. "
132
+ "Install with: pip install watchfiles"
133
+ )
134
+ sys.exit(1)
135
+
136
+ script_path = os.path.abspath(script_path)
137
+ watch_paths = get_watch_paths(script_path)
138
+ python_exec = sys.executable
139
+
140
+ logger.info("🔄 Starting agent with file watching enabled")
141
+ logger.info(f"📁 Watching: {watch_paths[0]}")
142
+ logger.info("📝 File types: .py, .jinja2, .j2, .yaml, .yml")
143
+ logger.info("🔃 Agent will restart automatically when files change")
144
+
145
+ process = None
146
+ stop_event = threading.Event()
147
+
148
+ def signal_handler(signum, frame):
149
+ stop_event.set()
150
+ logger.info("🛑 Stopping agent...")
151
+ terminate_process(process)
152
+
153
+ signal.signal(signal.SIGINT, signal_handler)
154
+ signal.signal(signal.SIGTERM, signal_handler)
155
+
156
+ def start_agent() -> subprocess.Popen:
157
+ """Start the agent subprocess in a new process group."""
158
+ return subprocess.Popen(
159
+ [python_exec, script_path],
160
+ stdout=sys.stdout,
161
+ stderr=sys.stderr,
162
+ stdin=sys.stdin,
163
+ start_new_session=True, # Create new process group for clean shutdown
164
+ )
165
+
166
+ # Start initial process
167
+ process = start_agent()
168
+ logger.info(f"✅ Agent started (PID: {process.pid})")
169
+
170
+ # Watch for file changes
171
+ try:
172
+ for changes in watchfiles.watch(
173
+ *watch_paths,
174
+ watch_filter=lambda ct, p: should_watch_file(p),
175
+ debounce=int(DEBOUNCE_DELAY * 1000), # Convert to milliseconds
176
+ stop_event=stop_event,
177
+ ):
178
+ if stop_event.is_set():
179
+ break
180
+
181
+ # Check if process crashed
182
+ if process.poll() is not None:
183
+ logger.warning(f"⚠️ Agent process exited with code {process.returncode}")
184
+
185
+ # Log changes
186
+ for change_type, path in changes:
187
+ rel_path = os.path.relpath(path, watch_paths[0])
188
+ logger.info(f"🔄 Detected {change_type.name}: {rel_path}")
189
+
190
+ logger.info("🔃 Restarting agent...")
191
+
192
+ # Stop current process
193
+ terminate_process(process)
194
+
195
+ # Delay to allow ports to be released
196
+ time.sleep(PORT_RELEASE_DELAY)
197
+
198
+ # Start new process
199
+ process = start_agent()
200
+ logger.info(f"✅ Agent restarted (PID: {process.pid})")
201
+
202
+ except KeyboardInterrupt:
203
+ pass
204
+ finally:
205
+ terminate_process(process)
206
+ logger.info("👋 File watcher stopped")
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Mesh Reload Runner - Entry point for file watching mode.
4
+
5
+ Usage:
6
+ python -m _mcp_mesh.reload_runner <agent_script.py>
7
+
8
+ This module is invoked by `meshctl start --watch` to wrap agent execution
9
+ with file watching. When source files change, the agent process is
10
+ automatically restarted.
11
+
12
+ Environment variables:
13
+ MCP_MESH_RELOAD_DEBOUNCE: Debounce delay in seconds (default: 0.5)
14
+ MCP_MESH_RELOAD_PORT_DELAY: Port release delay in seconds (default: 0.5)
15
+ """
16
+
17
+ import logging
18
+ import os
19
+ import sys
20
+
21
+ # Configure logging before imports
22
+ logging.basicConfig(
23
+ level=logging.INFO,
24
+ format="%(levelname)-8s %(message)s",
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def main():
30
+ """Main entry point for reload runner."""
31
+ if len(sys.argv) < 2:
32
+ print("Usage: python -m _mcp_mesh.reload_runner <agent_script.py>")
33
+ sys.exit(1)
34
+
35
+ script_path = sys.argv[1]
36
+
37
+ if not os.path.exists(script_path):
38
+ print(f"Error: Script not found: {script_path}")
39
+ sys.exit(1)
40
+
41
+ if not script_path.endswith(".py"):
42
+ print(f"Error: Expected a Python script (.py), got: {script_path}")
43
+ sys.exit(1)
44
+
45
+ # Import and run with reload
46
+ from .reload import run_with_reload
47
+
48
+ run_with_reload(script_path)
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-mesh
3
- Version: 0.7.15
3
+ Version: 0.7.17
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,4 +1,6 @@
1
- _mcp_mesh/__init__.py,sha256=xx8r72WkHqUhfJc2KwE6aOwUNPw5F5OuDJs4znH8B3g,2720
1
+ _mcp_mesh/__init__.py,sha256=QiGicZib2awgedpv_mY7bUW8ymNN5gvbxWgypsdI-7c,2720
2
+ _mcp_mesh/reload.py,sha256=IqZeS7lsFw7bwOzDPE0LJLPkY5nR68BKc8C4srSCX1o,6239
3
+ _mcp_mesh/reload_runner.py,sha256=SgQKzzO2yHfSUBq8s3SpAnovWA0rveimVNaxeLCEo_0,1310
2
4
  _mcp_mesh/engine/__init__.py,sha256=U_6Kw3vA_3RiNK0Oln5c5C7WvA9lSONV22wWzfxYHNw,2975
3
5
  _mcp_mesh/engine/async_mcp_client.py,sha256=Sz-rXTkb1Mng_f0SpLqLuOdPJ8vZjv3DFy0i8yYOqYk,8792
4
6
  _mcp_mesh/engine/base_injector.py,sha256=qzRLZqFP2VvEFagVovkpdldvDmm3VwPHm6tHwV58a2k,5648
@@ -105,13 +107,13 @@ _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py,sha256=kL7k_yx2zafH3HO4s
105
107
  _mcp_mesh/pipeline/mcp_startup/__init__.py,sha256=gS0xNmVx66bkLUMw64olMsN40ZLPH3ymwlLixZ4NuTs,1239
106
108
  _mcp_mesh/pipeline/mcp_startup/configuration.py,sha256=6LRLIxrqFMU76qrBb6GjGknUlKPZZ9iqOlxE7F9ZhLs,2808
107
109
  _mcp_mesh/pipeline/mcp_startup/decorator_collection.py,sha256=RHC6MHtfP9aP0hZ-IJjISZu72e0Pml3LU0qr7dc284w,2294
108
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py,sha256=si_HkHkpBbJDUIciDOA9-tppjnutGVg4K6Qcm6-gCKg,33888
110
+ _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py,sha256=SYTMVRtR3qYoWaeIit88DnAyWhAxsG--ZatDtFvTkrc,33987
109
111
  _mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py,sha256=Pm24wrSuRGsgeUrHvMPDnNh6RhIZoznnMAUwAkllohk,10661
110
112
  _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py,sha256=v85B0ynomvYu87eIvLe-aSZ7-Iwov2VtM4Fg3PkmrZs,3865
111
113
  _mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py,sha256=sOpzxRc0kYiXwSW9lvv8DSjliT85oZCWPODeJRuiqgg,15635
112
114
  _mcp_mesh/pipeline/mcp_startup/lifespan_factory.py,sha256=Hu7IvrhVH9sM7-XQDyWAGA3rgOnNIRyWFBtobkUQ5Es,4404
113
115
  _mcp_mesh/pipeline/mcp_startup/server_discovery.py,sha256=VuqqaBE00h6AerPjk-Ab-g51x6jODCbMX4nemLRQIIQ,8375
114
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py,sha256=dT96P2f6Ci0H8sDe-6_Uro88Y0lZiyjPanN2bPsrTyk,25066
116
+ _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py,sha256=JuEP4JOCBNG1qhaS2KaT1cFhhAb_NTuF46saJapHjeY,25165
115
117
  _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py,sha256=cAAbqioYRswf-P25OpZFX2yL_qN2sl_bVk-kcynFxPw,2347
116
118
  _mcp_mesh/pipeline/shared/__init__.py,sha256=s9xmdf6LkoetrVRGd7Zp3NUxcJCW6YZ_yNKzUBcnYys,352
117
119
  _mcp_mesh/pipeline/shared/base_step.py,sha256=kyPbNUX79NyGrE_0Q-e-Aek7m1J0TW036njWfv0iZ0I,1080
@@ -141,10 +143,10 @@ _mcp_mesh/tracing/trace_context_helper.py,sha256=3XWVU_cnsqrfjgAGRJaRK0Uvkns363s
141
143
  _mcp_mesh/tracing/utils.py,sha256=t9lJuTH7CeuzAiiAaD0WxsJMFJPdzZFR0w6-vyR9f2E,3849
142
144
  _mcp_mesh/utils/fastmcp_schema_extractor.py,sha256=M54ffesC-56zl_fNJHj9dZxElDQaWFf1MXdSLCuFStg,17253
143
145
  mesh/__init__.py,sha256=Va5XRBWgejQurad7Maz3E-zPY7vu431B2_4sAdCu1zk,3868
144
- mesh/decorators.py,sha256=v0l_0oAS_UZBQLC1WwhJQTil9AvGMCFL9kYS-N7febk,59627
146
+ mesh/decorators.py,sha256=sFTLngsIo_2uBb-Fvuh9K1S8e2Kzq_70eWT4Z2FA83E,59730
145
147
  mesh/helpers.py,sha256=ITua2zdxbXeBLF5qS46A6A1P2GQNbfL_2_BMCKazQ4U,12575
146
148
  mesh/types.py,sha256=n0MxrBYZJ84xyQWGf_X2ZbVWSAaIcEBkRV7qaCmX6Ac,17008
147
- mcp_mesh-0.7.15.dist-info/METADATA,sha256=pV9YaeDfY1G07nCjfD-s24ggiQcKZ3-9zk75uev9VAc,4973
148
- mcp_mesh-0.7.15.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
149
- mcp_mesh-0.7.15.dist-info/licenses/LICENSE,sha256=_EBQHRQThv9FPOLc5eFOUdeeRO0mYwChC7cx60dM1tM,1078
150
- mcp_mesh-0.7.15.dist-info/RECORD,,
149
+ mcp_mesh-0.7.17.dist-info/METADATA,sha256=PISE6mS316f6UKCehxZyTLi4cneVYPYW8ybg213Ol38,4973
150
+ mcp_mesh-0.7.17.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
151
+ mcp_mesh-0.7.17.dist-info/licenses/LICENSE,sha256=_EBQHRQThv9FPOLc5eFOUdeeRO0mYwChC7cx60dM1tM,1078
152
+ mcp_mesh-0.7.17.dist-info/RECORD,,
mesh/decorators.py CHANGED
@@ -242,6 +242,7 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
242
242
  log_level="info",
243
243
  timeout_graceful_shutdown=30, # Allow time for registry cleanup
244
244
  access_log=False, # Reduce noise
245
+ ws="websockets-sansio", # Use modern websockets API (avoids deprecation warnings)
245
246
  )
246
247
  except Exception as e:
247
248
  logger.error(f"❌ IMMEDIATE UVICORN: Server failed: {e}")