@neoline/hostbridge 2.0.4 → 2.0.5

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.
Files changed (40) hide show
  1. package/README.md +11 -9
  2. package/docs/ARCHITECTURE.md +62 -0
  3. package/package.json +2 -1
  4. package/pyproject.toml +1 -1
  5. package/src/server_control_mcp/__init__.py +4 -3
  6. package/src/server_control_mcp/async_lifecycle.py +41 -0
  7. package/src/server_control_mcp/cli.py +1 -1
  8. package/src/server_control_mcp/client.py +302 -287
  9. package/src/server_control_mcp/config.py +2 -1
  10. package/src/server_control_mcp/daemon.py +233 -526
  11. package/src/server_control_mcp/doctor.py +34 -2
  12. package/src/server_control_mcp/hosts.py +136 -2
  13. package/src/server_control_mcp/mock_ssh.py +52 -14
  14. package/src/server_control_mcp/mock_ssh_exec.py +147 -58
  15. package/src/server_control_mcp/mock_ssh_session.py +3 -2
  16. package/src/server_control_mcp/mock_ssh_sftp.py +126 -28
  17. package/src/server_control_mcp/mock_ssh_tunnel.py +141 -444
  18. package/src/server_control_mcp/mux_connection.py +326 -0
  19. package/src/server_control_mcp/mux_daemon.py +186 -0
  20. package/src/server_control_mcp/mux_protocol.py +279 -0
  21. package/src/server_control_mcp/mux_records.py +71 -0
  22. package/src/server_control_mcp/mux_rpc.py +1779 -0
  23. package/src/server_control_mcp/mux_service.py +506 -0
  24. package/src/server_control_mcp/mux_stream.py +283 -0
  25. package/src/server_control_mcp/mux_sync_client.py +224 -0
  26. package/src/server_control_mcp/remote_agent_bundle.py +56 -0
  27. package/src/server_control_mcp/remote_mux_agent.py +769 -0
  28. package/src/server_control_mcp/runtime_services.py +862 -0
  29. package/src/server_control_mcp/server.py +33 -18
  30. package/src/server_control_mcp/services.py +2 -666
  31. package/src/server_control_mcp/transports/__init__.py +4 -8
  32. package/src/server_control_mcp/transports/base.py +60 -38
  33. package/src/server_control_mcp/transports/ssh.py +206 -259
  34. package/src/server_control_mcp/tunnel_manager.py +1245 -0
  35. package/src/server_control_mcp/tunnel_native_ssh.py +454 -0
  36. package/src/server_control_mcp/tunnel_providers.py +20 -0
  37. package/src/server_control_mcp/tunnel_pty_agent.py +909 -0
  38. package/src/server_control_mcp/protocol.py +0 -363
  39. package/src/server_control_mcp/remote_tunnel_agent.py +0 -143
  40. package/src/server_control_mcp/transports/pty.py +0 -515
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import os
4
5
  import sys
5
- from collections.abc import Callable
6
+ import threading
7
+ from collections.abc import AsyncIterator, Callable
8
+ from contextlib import asynccontextmanager
6
9
  from dataclasses import asdict
7
10
  from pathlib import Path
8
11
  from typing import Any, TypeVar
@@ -17,14 +20,39 @@ DEFAULT_CONNECT_TIMEOUT = 30
17
20
  DEFAULT_MAX_TRANSFER_BYTES = 100 * 1024 * 1024
18
21
  MAX_TRANSFER_BYTES = 1024 * 1024 * 1024
19
22
 
20
- mcp = FastMCP("hostbridge")
23
+ _client_instance: HostBridgeClient | None = None
24
+ _client_lock = threading.Lock()
25
+
26
+
27
+ @asynccontextmanager
28
+ async def _mcp_lifespan(_server: FastMCP[Any]) -> AsyncIterator[None]:
29
+ try:
30
+ yield None
31
+ finally:
32
+ await asyncio.to_thread(_close_client)
33
+
34
+
35
+ mcp = FastMCP("hostbridge", lifespan=_mcp_lifespan)
21
36
  T = TypeVar("T")
22
37
 
23
38
 
24
39
  def _client() -> HostBridgeClient:
40
+ global _client_instance
25
41
  if daemon.start_background(quiet=True) != 0:
26
42
  raise RuntimeError(f"hostbridge daemon failed to start; see {daemon.log_file()}")
27
- return HostBridgeClient()
43
+ with _client_lock:
44
+ if _client_instance is None or _client_instance.closed:
45
+ _client_instance = HostBridgeClient()
46
+ return _client_instance
47
+
48
+
49
+ def _close_client() -> None:
50
+ global _client_instance
51
+ with _client_lock:
52
+ client = _client_instance
53
+ _client_instance = None
54
+ if client is not None:
55
+ client.close()
28
56
 
29
57
 
30
58
  def _owner() -> str | None:
@@ -51,18 +79,7 @@ def _request(
51
79
  owner = _owner()
52
80
  if owner is not None:
53
81
  payload.setdefault("owner", owner)
54
- return _after_daemon_start(lambda: _client().request(method, payload, timeout=timeout))
55
-
56
-
57
- def _after_daemon_start(operation: Callable[[], T]) -> T:
58
- try:
59
- return operation()
60
- except HostBridgeError as exc:
61
- if exc.code != "connection_lost":
62
- raise
63
- if daemon.start_background(quiet=True) != 0:
64
- raise RuntimeError(f"hostbridge daemon failed to start; see {daemon.log_file()}") from exc
65
- return operation()
82
+ return _client().request(method, payload, timeout=timeout)
66
83
 
67
84
 
68
85
  def _mode(value: str | int) -> int:
@@ -87,9 +104,7 @@ def session_open(host_id: str, connect_timeout: int = DEFAULT_CONNECT_TIMEOUT) -
87
104
  """Open a persistent session for an allowlisted host."""
88
105
  safe_timeout = max(1, min(int(connect_timeout), 180))
89
106
  return _call(
90
- lambda: _after_daemon_start(
91
- lambda: _client().open_session(host_id, owner=_owner(), connect_timeout=safe_timeout)
92
- ),
107
+ lambda: _client().open_session(host_id, owner=_owner(), connect_timeout=safe_timeout),
93
108
  lambda value: {"ok": True, "session": asdict(value)},
94
109
  )
95
110