@neoline/hostbridge 2.0.2 → 2.0.4
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 +15 -8
- package/bin/hostbridge.js +80 -9
- package/examples/claude_desktop_config.json +1 -1
- package/examples/codex.config.toml +1 -1
- package/examples/hosts.example.json +1 -0
- package/package.json +1 -1
- package/pyproject.toml +5 -4
- package/src/server_control_mcp/__init__.py +58 -23
- package/src/server_control_mcp/cli.py +1 -1
- package/src/server_control_mcp/client.py +99 -7
- package/src/server_control_mcp/config.py +16 -5
- package/src/server_control_mcp/daemon.py +239 -49
- package/src/server_control_mcp/doctor.py +8 -4
- package/src/server_control_mcp/hosts.py +118 -24
- package/src/server_control_mcp/mock_ssh.py +62 -605
- package/src/server_control_mcp/mock_ssh_exec.py +210 -0
- package/src/server_control_mcp/mock_ssh_session.py +68 -0
- package/src/server_control_mcp/mock_ssh_sftp.py +506 -0
- package/src/server_control_mcp/mock_ssh_tunnel.py +592 -0
- package/src/server_control_mcp/policy.py +86 -14
- package/src/server_control_mcp/protocol.py +18 -17
- package/src/server_control_mcp/remote_task_agent.py +102 -0
- package/src/server_control_mcp/remote_tunnel_agent.py +143 -0
- package/src/server_control_mcp/runtime.py +3 -2
- package/src/server_control_mcp/secrets.py +6 -6
- package/src/server_control_mcp/secure_files.py +121 -0
- package/src/server_control_mcp/server.py +30 -12
- package/src/server_control_mcp/services.py +363 -165
- package/src/server_control_mcp/transports/pty.py +114 -33
- package/src/server_control_mcp/transports/ssh.py +308 -24
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
import sys
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from dataclasses import asdict
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
from typing import Any, TypeVar
|
|
8
9
|
|
|
9
10
|
from mcp.server.fastmcp import FastMCP
|
|
@@ -21,6 +22,8 @@ T = TypeVar("T")
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def _client() -> HostBridgeClient:
|
|
25
|
+
if daemon.start_background(quiet=True) != 0:
|
|
26
|
+
raise RuntimeError(f"hostbridge daemon failed to start; see {daemon.log_file()}")
|
|
24
27
|
return HostBridgeClient()
|
|
25
28
|
|
|
26
29
|
|
|
@@ -48,14 +51,18 @@ def _request(
|
|
|
48
51
|
owner = _owner()
|
|
49
52
|
if owner is not None:
|
|
50
53
|
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:
|
|
51
58
|
try:
|
|
52
|
-
return
|
|
59
|
+
return operation()
|
|
53
60
|
except HostBridgeError as exc:
|
|
54
|
-
if exc.code != "connection_lost"
|
|
61
|
+
if exc.code != "connection_lost":
|
|
55
62
|
raise
|
|
56
63
|
if daemon.start_background(quiet=True) != 0:
|
|
57
64
|
raise RuntimeError(f"hostbridge daemon failed to start; see {daemon.log_file()}") from exc
|
|
58
|
-
return
|
|
65
|
+
return operation()
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
def _mode(value: str | int) -> int:
|
|
@@ -78,9 +85,11 @@ def hosts_list() -> dict[str, object]:
|
|
|
78
85
|
@mcp.tool()
|
|
79
86
|
def session_open(host_id: str, connect_timeout: int = DEFAULT_CONNECT_TIMEOUT) -> dict[str, object]:
|
|
80
87
|
"""Open a persistent session for an allowlisted host."""
|
|
81
|
-
|
|
88
|
+
safe_timeout = max(1, min(int(connect_timeout), 180))
|
|
82
89
|
return _call(
|
|
83
|
-
lambda:
|
|
90
|
+
lambda: _after_daemon_start(
|
|
91
|
+
lambda: _client().open_session(host_id, owner=_owner(), connect_timeout=safe_timeout)
|
|
92
|
+
),
|
|
84
93
|
lambda value: {"ok": True, "session": asdict(value)},
|
|
85
94
|
)
|
|
86
95
|
|
|
@@ -92,10 +101,10 @@ def sessions_list() -> dict[str, object]:
|
|
|
92
101
|
|
|
93
102
|
|
|
94
103
|
@mcp.tool()
|
|
95
|
-
def session_close(session_id: str
|
|
104
|
+
def session_close(session_id: str) -> dict[str, object]:
|
|
96
105
|
"""Close a session."""
|
|
97
106
|
return _call(
|
|
98
|
-
lambda: _client().close_session(session_id, owner=_owner()
|
|
107
|
+
lambda: _client().close_session(session_id, owner=_owner()),
|
|
99
108
|
lambda _: {"ok": True, "session_id": session_id, "closed": True},
|
|
100
109
|
)
|
|
101
110
|
|
|
@@ -204,9 +213,18 @@ def file_upload(
|
|
|
204
213
|
session_id: str, local_path: str, remote_path: str, mode: str = "0644", max_bytes: int = DEFAULT_MAX_TRANSFER_BYTES
|
|
205
214
|
) -> dict[str, object]:
|
|
206
215
|
"""Stream a local file to the remote host."""
|
|
207
|
-
|
|
216
|
+
safe_limit = _limit(max_bytes)
|
|
217
|
+
source = Path(local_path)
|
|
218
|
+
try:
|
|
219
|
+
source_size = source.stat().st_size
|
|
220
|
+
except OSError as exc:
|
|
221
|
+
return _error(exc)
|
|
222
|
+
if source_size > safe_limit:
|
|
223
|
+
return _error(HostBridgeError("resource_limit", f"upload exceeds {safe_limit} bytes"))
|
|
208
224
|
return _call(
|
|
209
|
-
lambda: _client().upload_file(
|
|
225
|
+
lambda: _client().upload_file(
|
|
226
|
+
session_id, source, remote_path, mode=_mode(mode), owner=_owner(), max_bytes=safe_limit
|
|
227
|
+
),
|
|
210
228
|
lambda value: {"ok": True, "transfer": asdict(value)},
|
|
211
229
|
)
|
|
212
230
|
|
|
@@ -216,16 +234,16 @@ def file_download(
|
|
|
216
234
|
session_id: str, remote_path: str, local_path: str, max_bytes: int = DEFAULT_MAX_TRANSFER_BYTES
|
|
217
235
|
) -> dict[str, object]:
|
|
218
236
|
"""Stream a remote file to a local path."""
|
|
219
|
-
|
|
237
|
+
safe_limit = _limit(max_bytes)
|
|
220
238
|
return _call(
|
|
221
|
-
lambda: _client().download_file(session_id, remote_path, local_path, owner=_owner()),
|
|
239
|
+
lambda: _client().download_file(session_id, remote_path, local_path, owner=_owner(), max_bytes=safe_limit),
|
|
222
240
|
lambda value: {"ok": True, "transfer": asdict(value)},
|
|
223
241
|
)
|
|
224
242
|
|
|
225
243
|
|
|
226
244
|
def main(argv: list[str] | None = None) -> Any:
|
|
227
245
|
args = sys.argv[1:] if argv is None else argv
|
|
228
|
-
if args
|
|
246
|
+
if args:
|
|
229
247
|
return cli.main(args)
|
|
230
248
|
return mcp.run(transport="stdio")
|
|
231
249
|
|