vssh 3.3.1__tar.gz → 3.3.4__tar.gz
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.
- {vssh-3.3.1/vssh.egg-info → vssh-3.3.4}/PKG-INFO +3 -3
- {vssh-3.3.1 → vssh-3.3.4}/README.md +2 -2
- {vssh-3.3.1 → vssh-3.3.4}/pyproject.toml +5 -2
- {vssh-3.3.1 → vssh-3.3.4/vssh.egg-info}/PKG-INFO +3 -3
- {vssh-3.3.1 → vssh-3.3.4}/vssh.egg-info/SOURCES.txt +0 -2
- {vssh-3.3.1 → vssh-3.3.4}/vssh.egg-info/entry_points.txt +3 -0
- {vssh-3.3.1 → vssh-3.3.4}/vssh.egg-info/top_level.txt +0 -2
- {vssh-3.3.1 → vssh-3.3.4}/vssh.py +21 -13
- {vssh-3.3.1 → vssh-3.3.4}/vssh_mcp_server.py +140 -35
- {vssh-3.3.1 → vssh-3.3.4}/vssh_p2p.py +2 -2
- vssh-3.3.1/vssh_p2p_fast.py +0 -500
- vssh-3.3.1/vssh_quic.py +0 -436
- {vssh-3.3.1 → vssh-3.3.4}/LICENSE +0 -0
- {vssh-3.3.1 → vssh-3.3.4}/setup.cfg +0 -0
- {vssh-3.3.1 → vssh-3.3.4}/vssh.egg-info/dependency_links.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vssh
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.4
|
|
4
4
|
Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
|
|
5
5
|
Author-email: MeshPOP <mpop@mpop.dev>
|
|
6
6
|
License: MIT
|
|
@@ -41,10 +41,10 @@ pip install vssh
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
# Execute remote command
|
|
44
|
-
vssh exec
|
|
44
|
+
vssh exec relay1 "uptime"
|
|
45
45
|
|
|
46
46
|
# Transfer file at 50+ MB/s
|
|
47
|
-
vssh put
|
|
47
|
+
vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
|
|
48
48
|
|
|
49
49
|
# Check connection status
|
|
50
50
|
vssh status
|
|
@@ -18,10 +18,10 @@ pip install vssh
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
# Execute remote command
|
|
21
|
-
vssh exec
|
|
21
|
+
vssh exec relay1 "uptime"
|
|
22
22
|
|
|
23
23
|
# Transfer file at 50+ MB/s
|
|
24
|
-
vssh put
|
|
24
|
+
vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
|
|
25
25
|
|
|
26
26
|
# Check connection status
|
|
27
27
|
vssh status
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vssh"
|
|
7
|
-
version = "3.3.
|
|
7
|
+
version = "3.3.4"
|
|
8
8
|
description = "Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -34,5 +34,8 @@ vssh-mcp = "vssh_mcp_server:main"
|
|
|
34
34
|
Homepage = "https://github.com/meshpop/vssh"
|
|
35
35
|
Repository = "https://github.com/meshpop/vssh"
|
|
36
36
|
|
|
37
|
+
[project.entry-points."meshpop.mcp"]
|
|
38
|
+
vssh = "vssh_mcp_server"
|
|
39
|
+
|
|
37
40
|
[tool.setuptools]
|
|
38
|
-
py-modules = ["vssh", "vssh_p2p", "
|
|
41
|
+
py-modules = ["vssh", "vssh_p2p", "vssh_mcp_server"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vssh
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.4
|
|
4
4
|
Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
|
|
5
5
|
Author-email: MeshPOP <mpop@mpop.dev>
|
|
6
6
|
License: MIT
|
|
@@ -41,10 +41,10 @@ pip install vssh
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
# Execute remote command
|
|
44
|
-
vssh exec
|
|
44
|
+
vssh exec relay1 "uptime"
|
|
45
45
|
|
|
46
46
|
# Transfer file at 50+ MB/s
|
|
47
|
-
vssh put
|
|
47
|
+
vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
|
|
48
48
|
|
|
49
49
|
# Check connection status
|
|
50
50
|
vssh status
|
|
@@ -68,7 +68,7 @@ def _load_tailscale_map() -> dict:
|
|
|
68
68
|
if len(parts) >= 2:
|
|
69
69
|
ts_map[parts[0]] = parts[1] # wire_ip tailscale_ip
|
|
70
70
|
except (OSError, ValueError) as e:
|
|
71
|
-
pass #
|
|
71
|
+
pass # e silenced
|
|
72
72
|
|
|
73
73
|
return ts_map
|
|
74
74
|
|
|
@@ -103,7 +103,7 @@ def vssh_connect(host: str, timeout: float = 5.0) -> socket.socket:
|
|
|
103
103
|
sock.connect((host, PORT))
|
|
104
104
|
return sock
|
|
105
105
|
except (ConnectionRefusedError, ConnectionError, OSError, socket.timeout) as wire_err:
|
|
106
|
-
pass #
|
|
106
|
+
pass # wire_err silenced
|
|
107
107
|
|
|
108
108
|
# Fallback: try Tailscale IP
|
|
109
109
|
ts_ip = TAILSCALE_MAP.get(host)
|
|
@@ -380,7 +380,7 @@ def get_public_ip() -> str:
|
|
|
380
380
|
except (urllib.error.URLError, OSError):
|
|
381
381
|
continue
|
|
382
382
|
except Exception as e:
|
|
383
|
-
pass #
|
|
383
|
+
pass # e silenced
|
|
384
384
|
return ''
|
|
385
385
|
|
|
386
386
|
def get_network_info_local() -> dict:
|
|
@@ -435,7 +435,7 @@ def is_safe_network(host: str) -> Tuple[bool, str]:
|
|
|
435
435
|
if 16 <= second <= 31:
|
|
436
436
|
return True, "lan"
|
|
437
437
|
except (ValueError, IndexError) as e:
|
|
438
|
-
pass #
|
|
438
|
+
pass # e silenced
|
|
439
439
|
|
|
440
440
|
# Everything else is public IP - UNSAFE!
|
|
441
441
|
return False, "public_ip_unencrypted"
|
|
@@ -499,7 +499,7 @@ def resolve_name(host: str) -> str:
|
|
|
499
499
|
if host in _name_cache:
|
|
500
500
|
return _name_cache[host]
|
|
501
501
|
break # got peers, just no match
|
|
502
|
-
except (OSError,
|
|
502
|
+
except (OSError, ValueError, TimeoutError):
|
|
503
503
|
continue
|
|
504
504
|
return host
|
|
505
505
|
|
|
@@ -748,7 +748,7 @@ def _rpc_get_logs(payload: dict) -> dict:
|
|
|
748
748
|
)
|
|
749
749
|
return {'service': service, 'lines': r.stdout.strip().split('\n')}
|
|
750
750
|
except (subprocess.SubprocessError, OSError) as e:
|
|
751
|
-
pass #
|
|
751
|
+
pass # e silenced
|
|
752
752
|
|
|
753
753
|
path = log_paths.get(service)
|
|
754
754
|
if path and os.path.exists(path):
|
|
@@ -2421,11 +2421,11 @@ def pty_session(host: str, name: str = ''):
|
|
|
2421
2421
|
sys.stdout.buffer.write(data)
|
|
2422
2422
|
sys.stdout.buffer.flush()
|
|
2423
2423
|
except (BlockingIOError) as e:
|
|
2424
|
-
pass #
|
|
2424
|
+
pass # e silenced
|
|
2425
2425
|
except (EOFError, ConnectionError):
|
|
2426
2426
|
raise EOFError
|
|
2427
2427
|
except (EOFError, KeyboardInterrupt, ConnectionError, BrokenPipeError) as e:
|
|
2428
|
-
pass #
|
|
2428
|
+
pass # e silenced
|
|
2429
2429
|
finally:
|
|
2430
2430
|
# Restore terminal
|
|
2431
2431
|
if old_settings:
|
|
@@ -2588,7 +2588,7 @@ class VsshSession:
|
|
|
2588
2588
|
self.sock.sendall(f"CLOSE:{self.session_id}\n".encode())
|
|
2589
2589
|
self.sock.recv(32) # CLOSE_OK
|
|
2590
2590
|
except OSError as e:
|
|
2591
|
-
pass #
|
|
2591
|
+
pass # e silenced
|
|
2592
2592
|
finally:
|
|
2593
2593
|
self.sock.close()
|
|
2594
2594
|
self.sock = None
|
|
@@ -3213,7 +3213,7 @@ def server():
|
|
|
3213
3213
|
try:
|
|
3214
3214
|
payload = json.loads(payload_data.decode())
|
|
3215
3215
|
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
3216
|
-
pass #
|
|
3216
|
+
pass # e silenced
|
|
3217
3217
|
|
|
3218
3218
|
result = handle_rpc(method, payload, sender_ip=addr[0])
|
|
3219
3219
|
conn.sendall(f'RPC_RESULT:{len(result)}\n'.encode())
|
|
@@ -3281,6 +3281,14 @@ def server():
|
|
|
3281
3281
|
|
|
3282
3282
|
def main():
|
|
3283
3283
|
args = sys.argv[1:]
|
|
3284
|
+
# Handle --version and --help flags
|
|
3285
|
+
if args and args[0] in ("--version", "-v", "-V"):
|
|
3286
|
+
try:
|
|
3287
|
+
from importlib.metadata import version as _v
|
|
3288
|
+
print(f"vssh v{_v('vssh')}")
|
|
3289
|
+
except Exception:
|
|
3290
|
+
print("vssh v3.3.2")
|
|
3291
|
+
return
|
|
3284
3292
|
if not args:
|
|
3285
3293
|
print('''vssh v3 - Distributed command & file transport daemon for server meshes
|
|
3286
3294
|
|
|
@@ -3469,7 +3477,7 @@ Env: VSSH_SECRET
|
|
|
3469
3477
|
try:
|
|
3470
3478
|
days = int(args[1])
|
|
3471
3479
|
except (ValueError, TypeError) as e:
|
|
3472
|
-
pass #
|
|
3480
|
+
pass # e silenced
|
|
3473
3481
|
stats = get_transfer_stats(days)
|
|
3474
3482
|
print(f'Transfer Statistics (last {days} days)')
|
|
3475
3483
|
print(f' Total transfers: {stats["total"]}')
|
|
@@ -3634,7 +3642,7 @@ Env: VSSH_SECRET
|
|
|
3634
3642
|
try:
|
|
3635
3643
|
result['info'] = json.loads(buf.decode())
|
|
3636
3644
|
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
3637
|
-
pass #
|
|
3645
|
+
pass # e silenced
|
|
3638
3646
|
s.close()
|
|
3639
3647
|
except OSError:
|
|
3640
3648
|
pass # safe to ignore
|
|
@@ -3723,7 +3731,7 @@ Env: VSSH_SECRET
|
|
|
3723
3731
|
print(f" {r['name']:6s} {r['ip']:18s} ○ offline")
|
|
3724
3732
|
print(f"\nTotal: {online}/{total} online")
|
|
3725
3733
|
else:
|
|
3726
|
-
# Unknown command -> try as PTY session shortcut (vssh
|
|
3734
|
+
# Unknown command -> try as PTY session shortcut (vssh node3 = vssh session node3)
|
|
3727
3735
|
host = resolve_name(cmd)
|
|
3728
3736
|
rc = pty_session(host, cmd)
|
|
3729
3737
|
if rc != 0:
|
|
@@ -237,9 +237,12 @@ def vssh_download(ip: str, remote_path: str, local_path: str) -> dict:
|
|
|
237
237
|
if not resp.startswith(b'OK:'):
|
|
238
238
|
return {"error": resp.decode()}
|
|
239
239
|
|
|
240
|
-
# Parse size
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
# Parse size — format: OK:<size>\n or OK:<size>:<checksum>\n
|
|
241
|
+
try:
|
|
242
|
+
parts = resp.decode().split(':')
|
|
243
|
+
size = int(parts[1].strip())
|
|
244
|
+
except (IndexError, ValueError) as parse_err:
|
|
245
|
+
return {"error": f"Unexpected server response: {resp[:64]!r} ({parse_err})"}
|
|
243
246
|
|
|
244
247
|
# Receive file data
|
|
245
248
|
start = time.time()
|
|
@@ -253,8 +256,9 @@ def vssh_download(ip: str, remote_path: str, local_path: str) -> dict:
|
|
|
253
256
|
elapsed = time.time() - start
|
|
254
257
|
sock.close()
|
|
255
258
|
|
|
256
|
-
# Save file
|
|
257
|
-
os.
|
|
259
|
+
# Save file — ensure parent dir exists for bare filenames too
|
|
260
|
+
local_dir = os.path.dirname(os.path.abspath(local_path))
|
|
261
|
+
os.makedirs(local_dir, exist_ok=True)
|
|
258
262
|
with open(local_path, 'wb') as f:
|
|
259
263
|
f.write(data)
|
|
260
264
|
|
|
@@ -342,28 +346,57 @@ def tool_vssh_get(server: str, remote_path: str, local_path: str):
|
|
|
342
346
|
return vssh_download(ip, remote_path, local_path)
|
|
343
347
|
|
|
344
348
|
def tool_vssh_sync(source: str, dest: str, path: str):
|
|
345
|
-
"""Sync directory
|
|
349
|
+
"""Sync directory from source server to dest server via local relay"""
|
|
346
350
|
servers = get_servers()
|
|
347
351
|
|
|
348
|
-
if source not in servers
|
|
349
|
-
return {"error": "Unknown server
|
|
352
|
+
if source not in servers:
|
|
353
|
+
return {"error": f"Unknown source server: {source}"}
|
|
354
|
+
if dest not in servers:
|
|
355
|
+
return {"error": f"Unknown dest server: {dest}"}
|
|
356
|
+
|
|
357
|
+
src_ip = servers[source].get("ip", "").strip()
|
|
358
|
+
dst_ip = servers[dest].get("ip", "").strip()
|
|
359
|
+
|
|
360
|
+
if not src_ip:
|
|
361
|
+
return {"error": f"No IP configured for source server: {source}"}
|
|
362
|
+
if not dst_ip:
|
|
363
|
+
return {"error": f"No IP configured for dest server: {dest}"}
|
|
350
364
|
|
|
351
365
|
# Get file list from source
|
|
352
|
-
src_ip = servers[source].get("ip", "")
|
|
353
366
|
files_output = vssh_exec_cmd(src_ip, f"find {path} -type f 2>/dev/null | head -100")
|
|
354
|
-
|
|
355
367
|
if files_output.startswith("Error"):
|
|
356
|
-
return {"error": files_output}
|
|
357
|
-
|
|
358
|
-
files = [f for f in files_output.split(
|
|
368
|
+
return {"error": f"Cannot list files on {source}: {files_output}"}
|
|
369
|
+
|
|
370
|
+
files = [f.strip() for f in files_output.split("\n") if f.strip()]
|
|
371
|
+
if not files:
|
|
372
|
+
return {"source": source, "dest": dest, "path": path,
|
|
373
|
+
"synced": 0, "message": "No files found at path"}
|
|
374
|
+
|
|
375
|
+
# Relay: download each file from source, upload to dest
|
|
376
|
+
import tempfile
|
|
377
|
+
synced, failed = [], []
|
|
378
|
+
with tempfile.TemporaryDirectory(prefix="vssh_sync_") as tmpdir:
|
|
379
|
+
for remote_file in files:
|
|
380
|
+
fname = os.path.basename(remote_file)
|
|
381
|
+
local_tmp = os.path.join(tmpdir, fname)
|
|
382
|
+
dl = vssh_download(src_ip, remote_file, local_tmp)
|
|
383
|
+
if "error" in dl:
|
|
384
|
+
failed.append({"file": remote_file, "error": dl["error"]})
|
|
385
|
+
continue
|
|
386
|
+
ul = vssh_upload(dst_ip, local_tmp, remote_file)
|
|
387
|
+
if "error" in ul:
|
|
388
|
+
failed.append({"file": remote_file, "error": ul["error"]})
|
|
389
|
+
else:
|
|
390
|
+
synced.append(remote_file)
|
|
359
391
|
|
|
360
392
|
return {
|
|
361
393
|
"source": source,
|
|
362
394
|
"dest": dest,
|
|
363
395
|
"path": path,
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
396
|
+
"total_files": len(files),
|
|
397
|
+
"synced": len(synced),
|
|
398
|
+
"failed": len(failed),
|
|
399
|
+
"errors": failed[:10] if failed else []
|
|
367
400
|
}
|
|
368
401
|
|
|
369
402
|
def tool_vssh_speed_test(server: str, size_mb: int = 10):
|
|
@@ -433,41 +466,97 @@ def tool_vssh_p2p_status():
|
|
|
433
466
|
}
|
|
434
467
|
|
|
435
468
|
def tool_vssh_tunnel(server: str, local_port: int, remote_port: int, remote_host: str = "localhost"):
|
|
436
|
-
"""Create SSH tunnel (port forwarding)"""
|
|
469
|
+
"""Create SSH tunnel (port forwarding). Launches tunnel in background and returns PID."""
|
|
437
470
|
servers = get_servers()
|
|
438
471
|
|
|
439
472
|
if server not in servers:
|
|
440
473
|
return {"error": f"Unknown server: {server}"}
|
|
441
474
|
|
|
442
|
-
ip = servers[server].get("ip", "")
|
|
475
|
+
ip = servers[server].get("ip", "").strip()
|
|
476
|
+
if not ip:
|
|
477
|
+
return {"error": f"No IP configured for server: {server}"}
|
|
443
478
|
|
|
444
|
-
|
|
479
|
+
user = servers[server].get("user", "root")
|
|
480
|
+
|
|
481
|
+
# Check if local port is already in use
|
|
445
482
|
import subprocess
|
|
446
483
|
check = subprocess.run(
|
|
447
484
|
f"lsof -i :{local_port} 2>/dev/null | grep LISTEN",
|
|
448
485
|
shell=True, capture_output=True, text=True
|
|
449
486
|
)
|
|
450
|
-
|
|
451
487
|
if check.stdout.strip():
|
|
488
|
+
# Check if it's already our tunnel
|
|
489
|
+
if ip in check.stdout or server in check.stdout:
|
|
490
|
+
return {
|
|
491
|
+
"status": "already_running",
|
|
492
|
+
"local_port": local_port,
|
|
493
|
+
"message": f"Tunnel to {server}:{remote_port} already running on localhost:{local_port}"
|
|
494
|
+
}
|
|
452
495
|
return {
|
|
453
496
|
"status": "port_in_use",
|
|
454
497
|
"local_port": local_port,
|
|
455
|
-
"message": f"Port {local_port} is already in use"
|
|
498
|
+
"message": f"Port {local_port} is already in use by another process"
|
|
456
499
|
}
|
|
457
500
|
|
|
458
|
-
#
|
|
459
|
-
|
|
501
|
+
# Build SSH tunnel command
|
|
502
|
+
ssh_args = [
|
|
503
|
+
"ssh", "-f", "-N",
|
|
504
|
+
"-o", "StrictHostKeyChecking=no",
|
|
505
|
+
"-o", "ConnectTimeout=10",
|
|
506
|
+
"-o", "BatchMode=yes",
|
|
507
|
+
"-L", f"{local_port}:{remote_host}:{remote_port}",
|
|
508
|
+
f"{user}@{ip}"
|
|
509
|
+
]
|
|
510
|
+
tunnel_cmd = " ".join(ssh_args)
|
|
511
|
+
|
|
512
|
+
# Launch tunnel as background process
|
|
513
|
+
try:
|
|
514
|
+
proc = subprocess.Popen(
|
|
515
|
+
ssh_args,
|
|
516
|
+
stdout=subprocess.DEVNULL,
|
|
517
|
+
stderr=subprocess.PIPE,
|
|
518
|
+
start_new_session=True
|
|
519
|
+
)
|
|
520
|
+
# Wait briefly to detect immediate failures
|
|
521
|
+
import time
|
|
522
|
+
time.sleep(1.5)
|
|
523
|
+
if proc.poll() is not None:
|
|
524
|
+
# Process already exited — SSH failed
|
|
525
|
+
err = proc.stderr.read().decode(errors="replace").strip()
|
|
526
|
+
return {
|
|
527
|
+
"status": "failed",
|
|
528
|
+
"error": f"SSH tunnel failed: {err or 'exited immediately'}",
|
|
529
|
+
"command": tunnel_cmd,
|
|
530
|
+
"hint": "Check SSH key auth: ssh-copy-id " + user + "@" + ip
|
|
531
|
+
}
|
|
460
532
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
533
|
+
# Verify port is now listening
|
|
534
|
+
verify = subprocess.run(
|
|
535
|
+
f"lsof -i :{local_port} 2>/dev/null | grep LISTEN",
|
|
536
|
+
shell=True, capture_output=True, text=True
|
|
537
|
+
)
|
|
538
|
+
listening = bool(verify.stdout.strip())
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
"status": "running" if listening else "started",
|
|
542
|
+
"pid": proc.pid,
|
|
543
|
+
"server": server,
|
|
544
|
+
"local_port": local_port,
|
|
545
|
+
"remote_host": remote_host,
|
|
546
|
+
"remote_port": remote_port,
|
|
547
|
+
"access": f"localhost:{local_port}",
|
|
548
|
+
"usage": f"Connect to {remote_host}:{remote_port} on {server} via localhost:{local_port}",
|
|
549
|
+
"close_cmd": f"kill {proc.pid} # or: pkill -f \'ssh.*-L {local_port}\'"
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
except FileNotFoundError:
|
|
553
|
+
return {
|
|
554
|
+
"status": "no_ssh",
|
|
555
|
+
"error": "ssh not found — install OpenSSH",
|
|
556
|
+
"command": tunnel_cmd
|
|
557
|
+
}
|
|
558
|
+
except Exception as e:
|
|
559
|
+
return {"status": "error", "error": str(e), "command": tunnel_cmd}
|
|
471
560
|
|
|
472
561
|
def tool_vssh_keys():
|
|
473
562
|
"""Manage vssh keys and secrets"""
|
|
@@ -493,7 +582,7 @@ def tool_vssh_keys():
|
|
|
493
582
|
"permissions": oct(os.stat(fpath).st_mode)[-3:]
|
|
494
583
|
})
|
|
495
584
|
|
|
496
|
-
# Check servers with vssh
|
|
585
|
+
# Check servers with vssh server running
|
|
497
586
|
servers = get_servers()
|
|
498
587
|
keys["servers_with_vssh"] = []
|
|
499
588
|
|
|
@@ -634,7 +723,7 @@ TOOLS = [
|
|
|
634
723
|
},
|
|
635
724
|
{
|
|
636
725
|
"name": "vssh_tunnel",
|
|
637
|
-
"description": "Create SSH tunnel (port forwarding) to access remote services locally.",
|
|
726
|
+
"description": "Create SSH tunnel (port forwarding) to access remote services locally. Launches tunnel in background, returns PID and access URL.",
|
|
638
727
|
"inputSchema": {
|
|
639
728
|
"type": "object",
|
|
640
729
|
"properties": {
|
|
@@ -758,5 +847,21 @@ def main():
|
|
|
758
847
|
sys.stderr.write(f"Error: {e}\n")
|
|
759
848
|
break
|
|
760
849
|
|
|
850
|
+
|
|
851
|
+
def handle_tool(name: str, arguments: dict) -> str:
|
|
852
|
+
"""Unified MCP compatibility wrapper."""
|
|
853
|
+
resp = handle_request({
|
|
854
|
+
"method": "tools/call",
|
|
855
|
+
"id": 1,
|
|
856
|
+
"params": {"name": name, "arguments": arguments},
|
|
857
|
+
})
|
|
858
|
+
if resp and "result" in resp:
|
|
859
|
+
content = resp["result"].get("content", [])
|
|
860
|
+
if content:
|
|
861
|
+
return content[0].get("text", "{}")
|
|
862
|
+
if resp and "error" in resp:
|
|
863
|
+
return json.dumps({"error": resp["error"]})
|
|
864
|
+
return json.dumps({"error": "no result"})
|
|
865
|
+
|
|
761
866
|
if __name__ == "__main__":
|
|
762
867
|
main()
|
|
@@ -519,9 +519,9 @@ def p2p_tcp_connect(peer_ip: str, peer_port: int, local_port: int,
|
|
|
519
519
|
return None
|
|
520
520
|
|
|
521
521
|
def signal_exchange(peer_vpn_ip: str, my_external: tuple, relay_ip: str = None, timeout: int = 30) -> tuple:
|
|
522
|
-
"""Exchange external addresses via relay server (
|
|
522
|
+
"""Exchange external addresses via relay server (relay1)
|
|
523
523
|
|
|
524
|
-
Both peers register with
|
|
524
|
+
Both peers register with relay1, then query for each other.
|
|
525
525
|
Returns peer's (external_ip, external_port, local_port) or (None, None, None)
|
|
526
526
|
"""
|
|
527
527
|
import subprocess
|