@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.
@@ -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 _client().request(method, payload, timeout=timeout)
59
+ return operation()
53
60
  except HostBridgeError as exc:
54
- if exc.code != "connection_lost" or os.environ.get("HOSTBRIDGE_NPM_LAUNCHER"):
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 _client().request(method, payload, timeout=timeout)
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
- _ = max(1, min(int(connect_timeout), 180))
88
+ safe_timeout = max(1, min(int(connect_timeout), 180))
82
89
  return _call(
83
- lambda: _client().open_session(host_id, owner=_owner()),
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, force: bool = False) -> dict[str, object]:
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(), force=bool(force)),
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
- del max_bytes
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(session_id, local_path, remote_path, mode=_mode(mode), owner=_owner()),
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
- del max_bytes
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 and (args[0] in cli.SETUP_COMMANDS or args[0] in {"-h", "--help"}):
246
+ if args:
229
247
  return cli.main(args)
230
248
  return mcp.run(transport="stdio")
231
249