@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.
- package/README.md +11 -9
- package/docs/ARCHITECTURE.md +62 -0
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/server_control_mcp/__init__.py +4 -3
- package/src/server_control_mcp/async_lifecycle.py +41 -0
- package/src/server_control_mcp/cli.py +1 -1
- package/src/server_control_mcp/client.py +302 -287
- package/src/server_control_mcp/config.py +2 -1
- package/src/server_control_mcp/daemon.py +233 -526
- package/src/server_control_mcp/doctor.py +34 -2
- package/src/server_control_mcp/hosts.py +136 -2
- package/src/server_control_mcp/mock_ssh.py +52 -14
- package/src/server_control_mcp/mock_ssh_exec.py +147 -58
- package/src/server_control_mcp/mock_ssh_session.py +3 -2
- package/src/server_control_mcp/mock_ssh_sftp.py +126 -28
- package/src/server_control_mcp/mock_ssh_tunnel.py +141 -444
- package/src/server_control_mcp/mux_connection.py +326 -0
- package/src/server_control_mcp/mux_daemon.py +186 -0
- package/src/server_control_mcp/mux_protocol.py +279 -0
- package/src/server_control_mcp/mux_records.py +71 -0
- package/src/server_control_mcp/mux_rpc.py +1779 -0
- package/src/server_control_mcp/mux_service.py +506 -0
- package/src/server_control_mcp/mux_stream.py +283 -0
- package/src/server_control_mcp/mux_sync_client.py +224 -0
- package/src/server_control_mcp/remote_agent_bundle.py +56 -0
- package/src/server_control_mcp/remote_mux_agent.py +769 -0
- package/src/server_control_mcp/runtime_services.py +862 -0
- package/src/server_control_mcp/server.py +33 -18
- package/src/server_control_mcp/services.py +2 -666
- package/src/server_control_mcp/transports/__init__.py +4 -8
- package/src/server_control_mcp/transports/base.py +60 -38
- package/src/server_control_mcp/transports/ssh.py +206 -259
- package/src/server_control_mcp/tunnel_manager.py +1245 -0
- package/src/server_control_mcp/tunnel_native_ssh.py +454 -0
- package/src/server_control_mcp/tunnel_providers.py +20 -0
- package/src/server_control_mcp/tunnel_pty_agent.py +909 -0
- package/src/server_control_mcp/protocol.py +0 -363
- package/src/server_control_mcp/remote_tunnel_agent.py +0 -143
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|