mcp-win-stdio-ssh 0.2.4__tar.gz → 0.2.5__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.
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/.gitignore +3 -0
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/PKG-INFO +2 -2
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/pyproject.toml +44 -44
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/src/mcp_win_stdio/ssh/__init__.py +5 -5
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/src/mcp_win_stdio/ssh/cli.py +9 -9
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/src/mcp_win_stdio/ssh/connection.py +13 -17
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/src/mcp_win_stdio/ssh/guide.py +1 -0
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/src/mcp_win_stdio/ssh/pty_session.py +8 -8
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/src/mcp_win_stdio/ssh/server.py +83 -62
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/src/mcp_win_stdio/ssh/sftp_ops.py +20 -16
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/src/mcp_win_stdio/ssh/tunnels.py +1 -6
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/README.md +0 -0
- {mcp_win_stdio_ssh-0.2.4 → mcp_win_stdio_ssh-0.2.5}/src/mcp_win_stdio/ssh/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-win-stdio-ssh
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Advanced Multi-SSH Connection & Remote Management Model Context Protocol (MCP) server for Windows & Claude: 25+ tools for multi-host pooling, ~/.ssh/config auto-discovery, PTY interactive shells, SFTP file management, systemd/docker services, background jobs, and local port forwarding tunnels.
|
|
5
5
|
Author: Mohan Kumar Indala
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
19
19
|
Classifier: Topic :: System :: Systems Administration
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Requires-Dist: cryptography>=3.3.0
|
|
22
|
-
Requires-Dist: mcp-win-stdio>=0.2.
|
|
22
|
+
Requires-Dist: mcp-win-stdio>=0.2.5
|
|
23
23
|
Requires-Dist: mcp>=1.2.0
|
|
24
24
|
Requires-Dist: paramiko>=3.0.0
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "mcp-win-stdio-ssh"
|
|
7
|
-
version = "0.2.
|
|
8
|
-
description = "Advanced Multi-SSH Connection & Remote Management Model Context Protocol (MCP) server for Windows & Claude: 25+ tools for multi-host pooling, ~/.ssh/config auto-discovery, PTY interactive shells, SFTP file management, systemd/docker services, background jobs, and local port forwarding tunnels."
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
|
|
11
|
-
requires-python = ">=3.10"
|
|
12
|
-
license = "MIT"
|
|
13
|
-
authors = [
|
|
14
|
-
{ name = "Mohan Kumar Indala" }
|
|
15
|
-
]
|
|
16
|
-
keywords = ["mcp", "claude", "ssh", "sftp", "pty", "tunnels", "port-forwarding", "remote-management", "systemd", "docker", "ai", "llm", "windows"]
|
|
17
|
-
classifiers = [
|
|
18
|
-
"Development Status :: 4 - Beta",
|
|
19
|
-
"Environment :: Win32 (MS Windows)",
|
|
20
|
-
"Intended Audience :: Developers",
|
|
21
|
-
"License :: OSI Approved :: MIT License",
|
|
22
|
-
"Operating System :: Microsoft :: Windows",
|
|
23
|
-
"Programming Language :: Python :: 3",
|
|
24
|
-
"Programming Language :: Python :: 3.10",
|
|
25
|
-
"Programming Language :: Python :: 3.11",
|
|
26
|
-
"Programming Language :: Python :: 3.12",
|
|
27
|
-
"Programming Language :: Python :: 3.13",
|
|
28
|
-
"Programming Language :: Python :: 3.14",
|
|
29
|
-
"Topic :: System :: Systems Administration",
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
dependencies = [
|
|
33
|
-
"mcp-win-stdio>=0.2.
|
|
34
|
-
"mcp>=1.2.0",
|
|
35
|
-
"paramiko>=3.0.0",
|
|
36
|
-
"cryptography>=3.3.0",
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
[project.scripts]
|
|
40
|
-
mcp-win-stdio-ssh = "mcp_win_stdio.ssh.cli:main"
|
|
41
|
-
mws-ssh = "mcp_win_stdio.ssh.cli:main"
|
|
42
|
-
|
|
43
|
-
[tool.hatch.build.targets.wheel]
|
|
44
|
-
packages = ["src/mcp_win_stdio"]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcp-win-stdio-ssh"
|
|
7
|
+
version = "0.2.5"
|
|
8
|
+
description = "Advanced Multi-SSH Connection & Remote Management Model Context Protocol (MCP) server for Windows & Claude: 25+ tools for multi-host pooling, ~/.ssh/config auto-discovery, PTY interactive shells, SFTP file management, systemd/docker services, background jobs, and local port forwarding tunnels."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Mohan Kumar Indala" }
|
|
15
|
+
]
|
|
16
|
+
keywords = ["mcp", "claude", "ssh", "sftp", "pty", "tunnels", "port-forwarding", "remote-management", "systemd", "docker", "ai", "llm", "windows"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Environment :: Win32 (MS Windows)",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: Microsoft :: Windows",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Programming Language :: Python :: 3.14",
|
|
29
|
+
"Topic :: System :: Systems Administration",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
dependencies = [
|
|
33
|
+
"mcp-win-stdio>=0.2.5",
|
|
34
|
+
"mcp>=1.2.0",
|
|
35
|
+
"paramiko>=3.0.0",
|
|
36
|
+
"cryptography>=3.3.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
mcp-win-stdio-ssh = "mcp_win_stdio.ssh.cli:main"
|
|
41
|
+
mws-ssh = "mcp_win_stdio.ssh.cli:main"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/mcp_win_stdio"]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Advanced Multi-SSH Connection & Remote Management MCP Server for mcp-win-stdio.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
__version__ = "0.2.
|
|
1
|
+
"""
|
|
2
|
+
Advanced Multi-SSH Connection & Remote Management MCP Server for mcp-win-stdio.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.5"
|
|
@@ -3,12 +3,10 @@ CLI entry point for mcp-win-stdio-ssh.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
-
import os
|
|
7
|
-
from pathlib import Path
|
|
8
6
|
import shutil
|
|
9
|
-
import subprocess
|
|
10
7
|
import sys
|
|
11
8
|
import time
|
|
9
|
+
from pathlib import Path
|
|
12
10
|
|
|
13
11
|
# Ensure Windows console uses UTF-8 without crashing on cp1252
|
|
14
12
|
if sys.platform == "win32":
|
|
@@ -21,10 +19,10 @@ if sys.platform == "win32":
|
|
|
21
19
|
from mcp_win_stdio.ssh import __version__
|
|
22
20
|
from mcp_win_stdio.ssh.connection import (
|
|
23
21
|
SYSTEM_SSH_CONFIG,
|
|
24
|
-
get_all_registered_hosts,
|
|
25
22
|
get_active_host_name,
|
|
26
|
-
|
|
23
|
+
get_all_registered_hosts,
|
|
27
24
|
get_cached_or_connect,
|
|
25
|
+
resolve_host_info,
|
|
28
26
|
)
|
|
29
27
|
from mcp_win_stdio.ssh.guide import print_ssh_guide
|
|
30
28
|
from mcp_win_stdio.ssh.server import mcp
|
|
@@ -54,7 +52,7 @@ def cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
54
52
|
if ssh_bin:
|
|
55
53
|
print(f"[OK] System SSH CLI: {ssh_bin}")
|
|
56
54
|
else:
|
|
57
|
-
print(
|
|
55
|
+
print("[INFO] System SSH CLI: Not on PATH (Optional: Paramiko native SSH client active)")
|
|
58
56
|
|
|
59
57
|
# 3. SSH Config & Keys
|
|
60
58
|
ssh_dir = Path.home() / ".ssh"
|
|
@@ -63,13 +61,13 @@ def cmd_doctor(args: argparse.Namespace) -> None:
|
|
|
63
61
|
if SYSTEM_SSH_CONFIG.exists():
|
|
64
62
|
print(f"[OK] SSH Config: {SYSTEM_SSH_CONFIG}")
|
|
65
63
|
else:
|
|
66
|
-
print(
|
|
64
|
+
print("[INFO] SSH Config: No ~/.ssh/config found (can use add_host or create config)")
|
|
67
65
|
|
|
68
66
|
keys = [f.name for f in ssh_dir.iterdir() if f.is_file() and not f.name.endswith(".pub") and "id_" in f.name]
|
|
69
67
|
if keys:
|
|
70
68
|
print(f"[OK] Found Keys: {', '.join(keys)}")
|
|
71
69
|
else:
|
|
72
|
-
print(
|
|
70
|
+
print("[INFO] Found Keys: No default id_* keys found in ~/.ssh/")
|
|
73
71
|
else:
|
|
74
72
|
print(f"[INFO] SSH Directory: {ssh_dir} not created yet.")
|
|
75
73
|
|
|
@@ -107,7 +105,9 @@ def cmd_test(args: argparse.Namespace) -> None:
|
|
|
107
105
|
stdin, stdout, stderr = client.exec_command("uname -srmo 2>/dev/null || ver", timeout=5)
|
|
108
106
|
os_info = stdout.read().decode("utf-8", errors="replace").strip()
|
|
109
107
|
|
|
110
|
-
print(
|
|
108
|
+
print(
|
|
109
|
+
f" [OK] {h_name:<16} -> {h_info.get('user')}@{h_info.get('hostname')}:{h_info.get('port')} ({lat_ms}ms) | {os_info}"
|
|
110
|
+
)
|
|
111
111
|
except Exception as e:
|
|
112
112
|
print(f" [FAIL] {h_name:<14} -> Error: {e}")
|
|
113
113
|
|
|
@@ -6,13 +6,10 @@ bastion/jump host tunneling, and resilient connection pooling.
|
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
import re
|
|
11
|
-
import socket
|
|
12
9
|
import sys
|
|
13
10
|
import threading
|
|
14
|
-
import
|
|
15
|
-
from typing import Any, Dict, List, Optional
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
16
13
|
|
|
17
14
|
import paramiko
|
|
18
15
|
from paramiko.config import SSHConfig
|
|
@@ -102,7 +99,7 @@ def parse_system_ssh_config() -> Dict[str, Dict[str, Any]]:
|
|
|
102
99
|
entry = ssh_cfg.lookup(host)
|
|
103
100
|
identity_files = entry.get("identityfile", [])
|
|
104
101
|
key_path = identity_files[0] if identity_files else None
|
|
105
|
-
|
|
102
|
+
|
|
106
103
|
# Resolve ~ in key path
|
|
107
104
|
if key_path:
|
|
108
105
|
key_path = str(Path(key_path).expanduser())
|
|
@@ -203,8 +200,7 @@ def resolve_host_info(target_name: Optional[str] = None) -> Dict[str, Any]:
|
|
|
203
200
|
set_active_host_name(first_name)
|
|
204
201
|
return registered[first_name]
|
|
205
202
|
raise ValueError(
|
|
206
|
-
"No SSH host specified and no active host is configured. "
|
|
207
|
-
"Use 'add_host' or provide 'host' parameter."
|
|
203
|
+
"No SSH host specified and no active host is configured. Use 'add_host' or provide 'host' parameter."
|
|
208
204
|
)
|
|
209
205
|
|
|
210
206
|
raise ValueError(f"SSH host '{host_name}' not found. Available hosts: {list(registered.keys())}")
|
|
@@ -314,9 +310,7 @@ def get_cached_or_connect(
|
|
|
314
310
|
pass
|
|
315
311
|
del _CLIENT_POOL[name]
|
|
316
312
|
|
|
317
|
-
client = create_ssh_client(
|
|
318
|
-
host_info, password=password, passphrase=passphrase, timeout=timeout
|
|
319
|
-
)
|
|
313
|
+
client = create_ssh_client(host_info, password=password, passphrase=passphrase, timeout=timeout)
|
|
320
314
|
_CLIENT_POOL[name] = client
|
|
321
315
|
return client
|
|
322
316
|
|
|
@@ -360,10 +354,12 @@ def get_pool_status() -> List[Dict[str, Any]]:
|
|
|
360
354
|
for name, client in _CLIENT_POOL.items():
|
|
361
355
|
transport = client.get_transport()
|
|
362
356
|
is_active = transport.is_active() if transport else False
|
|
363
|
-
status.append(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
357
|
+
status.append(
|
|
358
|
+
{
|
|
359
|
+
"name": name,
|
|
360
|
+
"isActive": is_active,
|
|
361
|
+
"remoteAddress": f"{transport.getpeername()}" if (transport and is_active) else "closed",
|
|
362
|
+
"isCurrentDefault": name == get_active_host_name(),
|
|
363
|
+
}
|
|
364
|
+
)
|
|
369
365
|
return status
|
|
@@ -4,13 +4,11 @@ Maintains long-lived interactive shell sessions for REPLs, prompts, and CLI wiza
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import re
|
|
7
|
-
import select
|
|
8
7
|
import threading
|
|
9
8
|
import time
|
|
10
9
|
from typing import Any, Dict, List, Optional
|
|
11
10
|
|
|
12
11
|
import paramiko
|
|
13
|
-
|
|
14
12
|
from mcp_win_stdio.ssh.connection import get_cached_or_connect, resolve_host_info
|
|
15
13
|
|
|
16
14
|
_PTY_SESSIONS: Dict[str, Dict[str, Any]] = {}
|
|
@@ -171,12 +169,14 @@ def list_pty_sessions() -> List[Dict[str, Any]]:
|
|
|
171
169
|
with _PTY_LOCK:
|
|
172
170
|
for name, sess in _PTY_SESSIONS.items():
|
|
173
171
|
chan = sess["channel"]
|
|
174
|
-
res.append(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
172
|
+
res.append(
|
|
173
|
+
{
|
|
174
|
+
"sessionName": name,
|
|
175
|
+
"host": sess["host"],
|
|
176
|
+
"createdAt": sess["createdAt"],
|
|
177
|
+
"isActive": not chan.closed,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
180
|
return res
|
|
181
181
|
|
|
182
182
|
|
|
@@ -7,13 +7,10 @@ service/process management, and local port forwarding tunnels.
|
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
-
from pathlib import Path
|
|
11
10
|
import re
|
|
12
11
|
import shlex
|
|
13
|
-
import sys
|
|
14
|
-
import tempfile
|
|
15
12
|
import time
|
|
16
|
-
from typing import Any, Dict,
|
|
13
|
+
from typing import Any, Dict, Literal, Optional, Union
|
|
17
14
|
|
|
18
15
|
try:
|
|
19
16
|
from mcp.server.mcpserver import MCPServer as FastMCP
|
|
@@ -21,10 +18,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
21
18
|
from mcp.server.fastmcp import FastMCP
|
|
22
19
|
|
|
23
20
|
import paramiko
|
|
24
|
-
|
|
25
21
|
from mcp_win_stdio.ssh.connection import (
|
|
26
|
-
_normalize_host_param,
|
|
27
|
-
close_all_connections,
|
|
28
22
|
close_connection,
|
|
29
23
|
get_active_host_name,
|
|
30
24
|
get_all_registered_hosts,
|
|
@@ -36,7 +30,6 @@ from mcp_win_stdio.ssh.connection import (
|
|
|
36
30
|
set_active_host_name,
|
|
37
31
|
)
|
|
38
32
|
from mcp_win_stdio.ssh.pty_session import (
|
|
39
|
-
close_all_pty_sessions,
|
|
40
33
|
close_pty_session,
|
|
41
34
|
list_pty_sessions,
|
|
42
35
|
read_pty_buffer,
|
|
@@ -54,7 +47,6 @@ from mcp_win_stdio.ssh.sftp_ops import (
|
|
|
54
47
|
write_remote_text_file,
|
|
55
48
|
)
|
|
56
49
|
from mcp_win_stdio.ssh.tunnels import (
|
|
57
|
-
close_all_tunnels,
|
|
58
50
|
close_tunnel,
|
|
59
51
|
list_active_tunnels,
|
|
60
52
|
open_local_tunnel,
|
|
@@ -67,6 +59,7 @@ mcp = FastMCP("ssh-mcp")
|
|
|
67
59
|
# Helper Functions
|
|
68
60
|
# ==============================================================================
|
|
69
61
|
|
|
62
|
+
|
|
70
63
|
def _format_ssh_error(e: Exception, host: Optional[str] = None, command: Optional[str] = None) -> Dict[str, Any]:
|
|
71
64
|
"""Format SSH exception into clean, structured diagnostics with helpful advice."""
|
|
72
65
|
error_payload = {
|
|
@@ -155,7 +148,7 @@ def _run_exec_channel(
|
|
|
155
148
|
|
|
156
149
|
start_t = time.time()
|
|
157
150
|
stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout, get_pty=False)
|
|
158
|
-
|
|
151
|
+
|
|
159
152
|
# Read output
|
|
160
153
|
out_str = stdout.read().decode("utf-8", errors="replace")
|
|
161
154
|
err_str = stderr.read().decode("utf-8", errors="replace")
|
|
@@ -183,6 +176,7 @@ def _run_exec_channel(
|
|
|
183
176
|
# 1. Host & Connection Management Tools
|
|
184
177
|
# ==============================================================================
|
|
185
178
|
|
|
179
|
+
|
|
186
180
|
@mcp.tool()
|
|
187
181
|
def list_hosts() -> Dict[str, Any]:
|
|
188
182
|
"""
|
|
@@ -196,21 +190,23 @@ def list_hosts() -> Dict[str, Any]:
|
|
|
196
190
|
|
|
197
191
|
results = []
|
|
198
192
|
for name, info in all_hosts.items():
|
|
199
|
-
is_active =
|
|
193
|
+
is_active = name == active_name
|
|
200
194
|
in_pool = pool_status.get(name, {})
|
|
201
195
|
|
|
202
|
-
results.append(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
196
|
+
results.append(
|
|
197
|
+
{
|
|
198
|
+
"name": name,
|
|
199
|
+
"hostname": info.get("hostname", "localhost"),
|
|
200
|
+
"user": info.get("user", "root"),
|
|
201
|
+
"port": info.get("port", 22),
|
|
202
|
+
"keyPath": info.get("key_path"),
|
|
203
|
+
"hasPassword": bool(info.get("password")),
|
|
204
|
+
"jumpHost": info.get("jump_host") or info.get("proxyjump"),
|
|
205
|
+
"source": info.get("source", "config"),
|
|
206
|
+
"isActiveDefault": is_active,
|
|
207
|
+
"poolConnection": "connected" if in_pool.get("isActive") else "idle",
|
|
208
|
+
}
|
|
209
|
+
)
|
|
214
210
|
|
|
215
211
|
return {
|
|
216
212
|
"activeHost": active_name,
|
|
@@ -224,7 +220,7 @@ def use_host(host: str) -> Dict[str, Any]:
|
|
|
224
220
|
"""
|
|
225
221
|
Switch the active default SSH host context. All subsequent SSH, SFTP, and diagnostic tool calls
|
|
226
222
|
will target this host automatically when host is not specified.
|
|
227
|
-
|
|
223
|
+
|
|
228
224
|
Args:
|
|
229
225
|
host: Host alias, hostname, or user@hostname:port to switch to.
|
|
230
226
|
"""
|
|
@@ -241,7 +237,7 @@ def use_host(host: str) -> Dict[str, Any]:
|
|
|
241
237
|
"user": info.get("user"),
|
|
242
238
|
"port": info.get("port"),
|
|
243
239
|
"source": info.get("source"),
|
|
244
|
-
}
|
|
240
|
+
},
|
|
245
241
|
}
|
|
246
242
|
except Exception as e:
|
|
247
243
|
return _format_ssh_error(e, host=host)
|
|
@@ -261,7 +257,7 @@ def add_host(
|
|
|
261
257
|
) -> Dict[str, Any]:
|
|
262
258
|
"""
|
|
263
259
|
Register and persist a new SSH host configuration to ~/.mcp-win-stdio/ssh_hosts.json.
|
|
264
|
-
|
|
260
|
+
|
|
265
261
|
Args:
|
|
266
262
|
name: Unique alias name for the host (e.g. 'prod-web-01', 'db-cluster', 'staging').
|
|
267
263
|
hostname: IP address or domain name of the remote server.
|
|
@@ -311,7 +307,7 @@ def add_host(
|
|
|
311
307
|
def remove_host(name: str) -> Dict[str, Any]:
|
|
312
308
|
"""
|
|
313
309
|
Remove a saved SSH host configuration from ~/.mcp-win-stdio/ssh_hosts.json.
|
|
314
|
-
|
|
310
|
+
|
|
315
311
|
Args:
|
|
316
312
|
name: Name of the host to delete.
|
|
317
313
|
"""
|
|
@@ -347,7 +343,7 @@ def remove_host(name: str) -> Dict[str, Any]:
|
|
|
347
343
|
def test_host(host: Optional[str] = None) -> Dict[str, Any]:
|
|
348
344
|
"""
|
|
349
345
|
Test SSH connectivity, authentication, latency, and retrieve remote OS info.
|
|
350
|
-
|
|
346
|
+
|
|
351
347
|
Args:
|
|
352
348
|
host: Host alias, hostname, or user@hostname:port (defaults to active host).
|
|
353
349
|
"""
|
|
@@ -388,7 +384,7 @@ def list_active_connections() -> Dict[str, Any]:
|
|
|
388
384
|
def disconnect_host(host: Optional[str] = None) -> Dict[str, Any]:
|
|
389
385
|
"""
|
|
390
386
|
Close and disconnect the SSH connection for a specific host from the connection pool.
|
|
391
|
-
|
|
387
|
+
|
|
392
388
|
Args:
|
|
393
389
|
host: Host name to disconnect (or active host if omitted).
|
|
394
390
|
"""
|
|
@@ -404,6 +400,7 @@ def disconnect_host(host: Optional[str] = None) -> Dict[str, Any]:
|
|
|
404
400
|
# 2. Remote Command & Script Execution Tools
|
|
405
401
|
# ==============================================================================
|
|
406
402
|
|
|
403
|
+
|
|
407
404
|
@mcp.tool()
|
|
408
405
|
def ssh_exec(
|
|
409
406
|
command: str,
|
|
@@ -415,7 +412,7 @@ def ssh_exec(
|
|
|
415
412
|
"""
|
|
416
413
|
Execute a non-interactive shell command on the remote SSH host.
|
|
417
414
|
Returns exit code, stdout, stderr, execution duration, and structured results.
|
|
418
|
-
|
|
415
|
+
|
|
419
416
|
Args:
|
|
420
417
|
command: The shell command line string to run.
|
|
421
418
|
host: Target SSH host (defaults to active host).
|
|
@@ -442,7 +439,7 @@ def ssh_exec_sudo(
|
|
|
442
439
|
) -> Dict[str, Any]:
|
|
443
440
|
"""
|
|
444
441
|
Execute a command with elevated sudo privileges, automatically handling the sudo password prompt if needed.
|
|
445
|
-
|
|
442
|
+
|
|
446
443
|
Args:
|
|
447
444
|
command: Command to execute with sudo (e.g. 'systemctl restart nginx', 'apt update').
|
|
448
445
|
sudo_password: Password for sudo prompt (if omitted, uses host password or passwordless sudo).
|
|
@@ -461,7 +458,9 @@ def ssh_exec_sudo(
|
|
|
461
458
|
|
|
462
459
|
start_t = time.time()
|
|
463
460
|
# Request PTY for sudo prompt handling
|
|
464
|
-
stdin, stdout, stderr = client.exec_command(
|
|
461
|
+
stdin, stdout, stderr = client.exec_command(
|
|
462
|
+
f"sudo -S -p '[SUDO_PROMPT]' {clean_cmd}", get_pty=True, timeout=timeout
|
|
463
|
+
)
|
|
465
464
|
|
|
466
465
|
if pwd:
|
|
467
466
|
# Send password when prompt requested
|
|
@@ -497,7 +496,7 @@ def ssh_exec_script(
|
|
|
497
496
|
) -> Dict[str, Any]:
|
|
498
497
|
"""
|
|
499
498
|
Upload and execute a multi-line script (bash, sh, python, node) on the remote server, returning execution results.
|
|
500
|
-
|
|
499
|
+
|
|
501
500
|
Args:
|
|
502
501
|
script_content: Full multi-line script code.
|
|
503
502
|
interpreter: Interpreter to run script with ('bash', 'sh', 'python3', 'node', 'pwsh').
|
|
@@ -526,7 +525,7 @@ def ssh_exec_script(
|
|
|
526
525
|
# Execute
|
|
527
526
|
run_cmd = f"{interpreter} {shlex.quote(remote_tmp)}"
|
|
528
527
|
res = _run_exec_channel(client, run_cmd, timeout=timeout)
|
|
529
|
-
|
|
528
|
+
|
|
530
529
|
# Cleanup
|
|
531
530
|
try:
|
|
532
531
|
client.exec_command(f"rm -f {shlex.quote(remote_tmp)}")
|
|
@@ -549,7 +548,7 @@ def ssh_exec_background(
|
|
|
549
548
|
) -> Dict[str, Any]:
|
|
550
549
|
"""
|
|
551
550
|
Launch a long-running process in the detached background (using nohup) and track its Process ID (PID).
|
|
552
|
-
|
|
551
|
+
|
|
553
552
|
Args:
|
|
554
553
|
command: Long-running command (e.g. 'npm run start', 'python train.py', 'backup.sh').
|
|
555
554
|
job_name: Optional label for the job.
|
|
@@ -590,7 +589,7 @@ def ssh_exec_background(
|
|
|
590
589
|
def ssh_check_job(job_id_or_pid: Union[int, str], host: Optional[str] = None) -> Dict[str, Any]:
|
|
591
590
|
"""
|
|
592
591
|
Check if a detached background job/PID is still running and read the latest log output.
|
|
593
|
-
|
|
592
|
+
|
|
594
593
|
Args:
|
|
595
594
|
job_id_or_pid: The PID or job name to check.
|
|
596
595
|
host: Target SSH host (defaults to active host).
|
|
@@ -627,7 +626,7 @@ def ssh_check_job(job_id_or_pid: Union[int, str], host: Optional[str] = None) ->
|
|
|
627
626
|
def ssh_kill_job(pid: int, signal: str = "SIGTERM", host: Optional[str] = None) -> Dict[str, Any]:
|
|
628
627
|
"""
|
|
629
628
|
Terminate a remote process by PID.
|
|
630
|
-
|
|
629
|
+
|
|
631
630
|
Args:
|
|
632
631
|
pid: Remote process ID.
|
|
633
632
|
signal: Signal name ('SIGTERM', 'SIGKILL', 'SIGHUP', 'SIGINT').
|
|
@@ -645,7 +644,9 @@ def ssh_kill_job(pid: int, signal: str = "SIGTERM", host: Optional[str] = None)
|
|
|
645
644
|
"host": host_info["name"],
|
|
646
645
|
"pid": pid,
|
|
647
646
|
"signal": signal,
|
|
648
|
-
"message": f"Sent {signal} to PID {pid}."
|
|
647
|
+
"message": f"Sent {signal} to PID {pid}."
|
|
648
|
+
if res["is_success"]
|
|
649
|
+
else f"Failed to kill PID {pid}: {res['stderr']}",
|
|
649
650
|
}
|
|
650
651
|
except Exception as e:
|
|
651
652
|
return _format_ssh_error(e, host=host)
|
|
@@ -655,6 +656,7 @@ def ssh_kill_job(pid: int, signal: str = "SIGTERM", host: Optional[str] = None)
|
|
|
655
656
|
# 3. Interactive PTY / Pseudo-Terminal Tools
|
|
656
657
|
# ==============================================================================
|
|
657
658
|
|
|
659
|
+
|
|
658
660
|
@mcp.tool()
|
|
659
661
|
def ssh_pty_start(
|
|
660
662
|
session_name: str,
|
|
@@ -663,7 +665,7 @@ def ssh_pty_start(
|
|
|
663
665
|
) -> Dict[str, Any]:
|
|
664
666
|
"""
|
|
665
667
|
Start an interactive pseudo-terminal (PTY) session for stateful interactions, REPLs, and prompts.
|
|
666
|
-
|
|
668
|
+
|
|
667
669
|
Args:
|
|
668
670
|
session_name: Unique identifier for this terminal session (e.g. 'wizard', 'python-repl').
|
|
669
671
|
host: Target SSH host (defaults to active host).
|
|
@@ -683,7 +685,7 @@ def ssh_pty_send(
|
|
|
683
685
|
) -> Dict[str, Any]:
|
|
684
686
|
"""
|
|
685
687
|
Send keystrokes, answers, or commands into an active interactive PTY session and collect output.
|
|
686
|
-
|
|
688
|
+
|
|
687
689
|
Args:
|
|
688
690
|
session_name: Name of active PTY session.
|
|
689
691
|
input_text: Text/command to send.
|
|
@@ -699,7 +701,7 @@ def ssh_pty_send(
|
|
|
699
701
|
def ssh_pty_read(session_name: str, max_chars: int = 4000) -> Dict[str, Any]:
|
|
700
702
|
"""
|
|
701
703
|
Read the output buffer of an active interactive PTY session.
|
|
702
|
-
|
|
704
|
+
|
|
703
705
|
Args:
|
|
704
706
|
session_name: Name of active PTY session.
|
|
705
707
|
max_chars: Maximum character limit for output.
|
|
@@ -724,7 +726,7 @@ def ssh_list_pty_sessions() -> Dict[str, Any]:
|
|
|
724
726
|
def ssh_pty_close(session_name: str) -> Dict[str, Any]:
|
|
725
727
|
"""
|
|
726
728
|
Close and terminate an interactive PTY session.
|
|
727
|
-
|
|
729
|
+
|
|
728
730
|
Args:
|
|
729
731
|
session_name: Name of the session to terminate.
|
|
730
732
|
"""
|
|
@@ -735,12 +737,13 @@ def ssh_pty_close(session_name: str) -> Dict[str, Any]:
|
|
|
735
737
|
# 4. Diagnostics & Remote Services Management
|
|
736
738
|
# ==============================================================================
|
|
737
739
|
|
|
740
|
+
|
|
738
741
|
@mcp.tool()
|
|
739
742
|
def ssh_system_overview(host: Optional[str] = None) -> Dict[str, Any]:
|
|
740
743
|
"""
|
|
741
744
|
Retrieve comprehensive system diagnostics: OS version, kernel, CPU count, RAM utilization,
|
|
742
745
|
load averages, uptime, and disk usage (df -h).
|
|
743
|
-
|
|
746
|
+
|
|
744
747
|
Args:
|
|
745
748
|
host: Target SSH host (defaults to active host).
|
|
746
749
|
"""
|
|
@@ -794,7 +797,9 @@ cat /etc/os-release 2>/dev/null || echo "N/A"
|
|
|
794
797
|
|
|
795
798
|
@mcp.tool()
|
|
796
799
|
def ssh_list_packages(
|
|
797
|
-
package_manager: Optional[
|
|
800
|
+
package_manager: Optional[
|
|
801
|
+
Literal["apt", "dpkg", "rpm", "dnf", "yum", "pip", "npm", "brew", "pacman", "apk", "winget"]
|
|
802
|
+
] = None,
|
|
798
803
|
filter: Optional[str] = None,
|
|
799
804
|
limit: int = 50,
|
|
800
805
|
offset: int = 0,
|
|
@@ -803,7 +808,7 @@ def ssh_list_packages(
|
|
|
803
808
|
"""
|
|
804
809
|
List installed software packages on the remote server across Linux/macOS/Windows package managers.
|
|
805
810
|
Features automatic package manager detection, token-safe pagination, and filtering to prevent context bloat.
|
|
806
|
-
|
|
811
|
+
|
|
807
812
|
Args:
|
|
808
813
|
package_manager: Package manager to query (auto-detected if None: 'dpkg'/'apt', 'rpm'/'dnf'/'yum', 'pip', 'npm', 'brew', 'pacman', 'apk', 'winget').
|
|
809
814
|
filter: Optional keyword or pattern filter on package name or description.
|
|
@@ -856,7 +861,7 @@ fi
|
|
|
856
861
|
query_cmd = "winget list 2>/dev/null"
|
|
857
862
|
elif pm == "npm":
|
|
858
863
|
query_cmd = "npm list -g --depth=0 --json 2>/dev/null || npm list -g --depth=0"
|
|
859
|
-
else:
|
|
864
|
+
else: # pip
|
|
860
865
|
query_cmd = "pip list --format=json 2>/dev/null || pip list"
|
|
861
866
|
|
|
862
867
|
stdin, stdout, stderr = client.exec_command(query_cmd, timeout=20)
|
|
@@ -881,7 +886,13 @@ fi
|
|
|
881
886
|
if not packages and raw_out:
|
|
882
887
|
for line in raw_out.splitlines():
|
|
883
888
|
line_str = line.strip()
|
|
884
|
-
if
|
|
889
|
+
if (
|
|
890
|
+
not line_str
|
|
891
|
+
or line_str.startswith("Desired=")
|
|
892
|
+
or line_str.startswith("|")
|
|
893
|
+
or line_str.startswith("Name ")
|
|
894
|
+
or line_str.startswith("---")
|
|
895
|
+
):
|
|
885
896
|
continue
|
|
886
897
|
|
|
887
898
|
parts = re.split(r"\t+|\s{2,}", line_str)
|
|
@@ -899,7 +910,11 @@ fi
|
|
|
899
910
|
pkg_ver = ""
|
|
900
911
|
pkg_summary = ""
|
|
901
912
|
|
|
902
|
-
if clean_filter and (
|
|
913
|
+
if clean_filter and (
|
|
914
|
+
clean_filter not in pkg_name.lower()
|
|
915
|
+
and clean_filter not in pkg_summary.lower()
|
|
916
|
+
and clean_filter not in pkg_ver.lower()
|
|
917
|
+
):
|
|
903
918
|
continue
|
|
904
919
|
|
|
905
920
|
entry = {"name": pkg_name, "version": pkg_ver}
|
|
@@ -948,7 +963,7 @@ def ssh_list_services(
|
|
|
948
963
|
) -> Dict[str, Any]:
|
|
949
964
|
"""
|
|
950
965
|
Inspect running remote services across systemd units, Docker containers, or PM2 node processes with pagination.
|
|
951
|
-
|
|
966
|
+
|
|
952
967
|
Args:
|
|
953
968
|
service_type: Service framework ('systemd', 'docker', 'pm2').
|
|
954
969
|
filter: Optional keyword or pattern filter.
|
|
@@ -1015,7 +1030,7 @@ def ssh_service_action(
|
|
|
1015
1030
|
) -> Dict[str, Any]:
|
|
1016
1031
|
"""
|
|
1017
1032
|
Manage a remote daemon or container (status, start, stop, restart, reload).
|
|
1018
|
-
|
|
1033
|
+
|
|
1019
1034
|
Args:
|
|
1020
1035
|
service_name: Name of the service unit (e.g. 'nginx', 'postgresql', 'my-container').
|
|
1021
1036
|
action: Lifecycle action to take.
|
|
@@ -1028,7 +1043,11 @@ def ssh_service_action(
|
|
|
1028
1043
|
|
|
1029
1044
|
s_name = shlex.quote(service_name)
|
|
1030
1045
|
if service_type == "systemd":
|
|
1031
|
-
cmd =
|
|
1046
|
+
cmd = (
|
|
1047
|
+
f"sudo systemctl {action} {s_name} --no-pager"
|
|
1048
|
+
if action != "status"
|
|
1049
|
+
else f"systemctl status {s_name} --no-pager"
|
|
1050
|
+
)
|
|
1032
1051
|
elif service_type == "docker":
|
|
1033
1052
|
cmd = f"docker {action} {s_name}"
|
|
1034
1053
|
else:
|
|
@@ -1057,7 +1076,7 @@ def ssh_tail_logs(
|
|
|
1057
1076
|
"""
|
|
1058
1077
|
Tail remote log files (e.g. /var/log/syslog, /var/log/nginx/error.log) or systemd journal logs.
|
|
1059
1078
|
Features safety caps on line count and character length to prevent context bloat.
|
|
1060
|
-
|
|
1079
|
+
|
|
1061
1080
|
Args:
|
|
1062
1081
|
target: Log file path (e.g. '/var/log/syslog') or systemd service unit name if is_journal=True.
|
|
1063
1082
|
lines: Number of trailing lines to return (default: 50, max: 200).
|
|
@@ -1099,7 +1118,7 @@ def ssh_list_processes(
|
|
|
1099
1118
|
) -> Dict[str, Any]:
|
|
1100
1119
|
"""
|
|
1101
1120
|
List top remote processes sorted by CPU or Memory usage with token-safe limits.
|
|
1102
|
-
|
|
1121
|
+
|
|
1103
1122
|
Args:
|
|
1104
1123
|
filter: Optional process name or command filter.
|
|
1105
1124
|
sort_by: Sort metric ('cpu' or 'mem').
|
|
@@ -1160,6 +1179,7 @@ def ssh_list_processes(
|
|
|
1160
1179
|
# 5. SFTP Remote File Operations Tools
|
|
1161
1180
|
# ==============================================================================
|
|
1162
1181
|
|
|
1182
|
+
|
|
1163
1183
|
@mcp.tool()
|
|
1164
1184
|
def sftp_list_dir(
|
|
1165
1185
|
remote_path: str = ".",
|
|
@@ -1170,7 +1190,7 @@ def sftp_list_dir(
|
|
|
1170
1190
|
) -> Dict[str, Any]:
|
|
1171
1191
|
"""
|
|
1172
1192
|
List contents of a remote directory with file types, sizes, permissions, timestamps, and context window protection.
|
|
1173
|
-
|
|
1193
|
+
|
|
1174
1194
|
Args:
|
|
1175
1195
|
remote_path: Remote directory path (default: current directory '.').
|
|
1176
1196
|
limit: Max items to return per batch (default 100, max 250).
|
|
@@ -1193,7 +1213,7 @@ def sftp_read_file(
|
|
|
1193
1213
|
) -> Dict[str, Any]:
|
|
1194
1214
|
"""
|
|
1195
1215
|
Read text/source code from a remote file with line offset support and token safety.
|
|
1196
|
-
|
|
1216
|
+
|
|
1197
1217
|
Args:
|
|
1198
1218
|
remote_path: Remote file path.
|
|
1199
1219
|
max_chars: Maximum characters to return (default: 15,000).
|
|
@@ -1220,7 +1240,7 @@ def sftp_write_file(
|
|
|
1220
1240
|
) -> Dict[str, Any]:
|
|
1221
1241
|
"""
|
|
1222
1242
|
Write or append text content to a remote file via SFTP.
|
|
1223
|
-
|
|
1243
|
+
|
|
1224
1244
|
Args:
|
|
1225
1245
|
remote_path: Remote destination path.
|
|
1226
1246
|
content: Text content to write.
|
|
@@ -1245,7 +1265,7 @@ def sftp_stat(
|
|
|
1245
1265
|
) -> Dict[str, Any]:
|
|
1246
1266
|
"""
|
|
1247
1267
|
Inspect detailed file/directory metadata, size, permissions, uid/gid, and timestamps.
|
|
1248
|
-
|
|
1268
|
+
|
|
1249
1269
|
Args:
|
|
1250
1270
|
remote_path: Remote file or directory path.
|
|
1251
1271
|
host: Target SSH host (defaults to active host).
|
|
@@ -1264,7 +1284,7 @@ def sftp_upload(
|
|
|
1264
1284
|
) -> Dict[str, Any]:
|
|
1265
1285
|
"""
|
|
1266
1286
|
Upload a local file or entire folder to the remote host.
|
|
1267
|
-
|
|
1287
|
+
|
|
1268
1288
|
Args:
|
|
1269
1289
|
local_path: Local path on machine (file or folder).
|
|
1270
1290
|
remote_path: Remote destination path.
|
|
@@ -1284,7 +1304,7 @@ def sftp_download(
|
|
|
1284
1304
|
) -> Dict[str, Any]:
|
|
1285
1305
|
"""
|
|
1286
1306
|
Download a remote file or folder to the local machine.
|
|
1287
|
-
|
|
1307
|
+
|
|
1288
1308
|
Args:
|
|
1289
1309
|
remote_path: Remote source path.
|
|
1290
1310
|
local_path: Local destination path.
|
|
@@ -1304,7 +1324,7 @@ def sftp_remove(
|
|
|
1304
1324
|
) -> Dict[str, Any]:
|
|
1305
1325
|
"""
|
|
1306
1326
|
Delete a remote file or directory.
|
|
1307
|
-
|
|
1327
|
+
|
|
1308
1328
|
Args:
|
|
1309
1329
|
remote_path: Remote path to remove.
|
|
1310
1330
|
recursive: If True, recursively deletes non-empty directories.
|
|
@@ -1320,6 +1340,7 @@ def sftp_remove(
|
|
|
1320
1340
|
# 6. Port Forwarding & Tunnels Tools
|
|
1321
1341
|
# ==============================================================================
|
|
1322
1342
|
|
|
1343
|
+
|
|
1323
1344
|
@mcp.tool()
|
|
1324
1345
|
def ssh_tunnel_open(
|
|
1325
1346
|
local_port: int,
|
|
@@ -1331,7 +1352,7 @@ def ssh_tunnel_open(
|
|
|
1331
1352
|
"""
|
|
1332
1353
|
Establish a local-to-remote SSH port forwarding tunnel in the background.
|
|
1333
1354
|
Allows accessing remote databases, APIs, or services via 127.0.0.1:<local_port>.
|
|
1334
|
-
|
|
1355
|
+
|
|
1335
1356
|
Args:
|
|
1336
1357
|
local_port: Local port to bind on machine (e.g. 15432, 13306, 8080).
|
|
1337
1358
|
remote_port: Remote target port on the server (e.g. 5432, 3306, 80).
|
|
@@ -1365,7 +1386,7 @@ def ssh_tunnel_list() -> Dict[str, Any]:
|
|
|
1365
1386
|
def ssh_tunnel_close(tunnel_id_or_name: str) -> Dict[str, Any]:
|
|
1366
1387
|
"""
|
|
1367
1388
|
Close and terminate an active SSH port forwarding tunnel.
|
|
1368
|
-
|
|
1389
|
+
|
|
1369
1390
|
Args:
|
|
1370
1391
|
tunnel_id_or_name: Tunnel name or local port number.
|
|
1371
1392
|
"""
|
|
@@ -3,16 +3,14 @@ SFTP File Operations and Directory Traversal for mcp-win-stdio-ssh.
|
|
|
3
3
|
Provides token-safe remote file reading, streaming writing, metadata inspection, and sync operations.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from datetime import datetime
|
|
7
6
|
import os
|
|
8
|
-
from pathlib import Path
|
|
9
7
|
import stat
|
|
10
8
|
import threading
|
|
11
|
-
import
|
|
12
|
-
from
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, Optional, Tuple
|
|
13
12
|
|
|
14
13
|
import paramiko
|
|
15
|
-
|
|
16
14
|
from mcp_win_stdio.ssh.connection import get_cached_or_connect, resolve_host_info
|
|
17
15
|
|
|
18
16
|
_SFTP_CLIENTS: Dict[str, paramiko.SFTPClient] = {}
|
|
@@ -56,7 +54,7 @@ def list_remote_directory(
|
|
|
56
54
|
) -> Dict[str, Any]:
|
|
57
55
|
"""
|
|
58
56
|
List contents of a remote directory with detailed metadata and context window protection.
|
|
59
|
-
|
|
57
|
+
|
|
60
58
|
Args:
|
|
61
59
|
remote_path: Target directory path on remote server.
|
|
62
60
|
limit: Max items to return per batch (default 100, max 250).
|
|
@@ -85,16 +83,20 @@ def list_remote_directory(
|
|
|
85
83
|
is_symlink = stat.S_ISLNK(attr.st_mode)
|
|
86
84
|
mtime_dt = datetime.fromtimestamp(attr.st_mtime) if attr.st_mtime else None
|
|
87
85
|
|
|
88
|
-
items.append(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
items.append(
|
|
87
|
+
{
|
|
88
|
+
"name": attr.filename,
|
|
89
|
+
"type": "directory" if is_dir else ("symlink" if is_symlink else "file"),
|
|
90
|
+
"sizeBytes": attr.st_size,
|
|
91
|
+
"sizeFormatted": f"{attr.st_size / 1024:.1f} KB"
|
|
92
|
+
if attr.st_size < 1024 * 1024
|
|
93
|
+
else f"{attr.st_size / (1024 * 1024):.2f} MB",
|
|
94
|
+
"permissions": format_file_mode(attr.st_mode),
|
|
95
|
+
"modified": mtime_dt.isoformat() if mtime_dt else None,
|
|
96
|
+
"uid": attr.st_uid,
|
|
97
|
+
"gid": attr.st_gid,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
98
100
|
|
|
99
101
|
safe_limit = min(max(1, limit), 250)
|
|
100
102
|
safe_offset = max(0, offset)
|
|
@@ -399,6 +401,7 @@ def remove_remote_path(
|
|
|
399
401
|
st = sftp.stat(remote_path)
|
|
400
402
|
if stat.S_ISDIR(st.st_mode):
|
|
401
403
|
if recursive:
|
|
404
|
+
|
|
402
405
|
def _rm_recursive(rem_dir: str):
|
|
403
406
|
for attr in sftp.listdir_attr(rem_dir):
|
|
404
407
|
item = f"{rem_dir.rstrip('/')}/{attr.filename}"
|
|
@@ -407,6 +410,7 @@ def remove_remote_path(
|
|
|
407
410
|
else:
|
|
408
411
|
sftp.remove(item)
|
|
409
412
|
sftp.rmdir(rem_dir)
|
|
413
|
+
|
|
410
414
|
_rm_recursive(remote_path)
|
|
411
415
|
else:
|
|
412
416
|
sftp.rmdir(remote_path)
|
|
@@ -4,14 +4,11 @@ Allows creating background local-to-remote tunnels using paramiko direct-tcpip c
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import select
|
|
7
|
-
import socket
|
|
8
7
|
import socketserver
|
|
9
8
|
import threading
|
|
10
9
|
import time
|
|
11
10
|
from typing import Any, Dict, List, Optional
|
|
12
11
|
|
|
13
|
-
import paramiko
|
|
14
|
-
|
|
15
12
|
from mcp_win_stdio.ssh.connection import get_cached_or_connect, resolve_host_info
|
|
16
13
|
|
|
17
14
|
_ACTIVE_TUNNELS: Dict[str, Dict[str, Any]] = {}
|
|
@@ -89,9 +86,7 @@ def open_local_tunnel(
|
|
|
89
86
|
except Exception as e:
|
|
90
87
|
raise OSError(f"Failed to bind local port {local_port}: {e}")
|
|
91
88
|
|
|
92
|
-
server_thread = threading.Thread(
|
|
93
|
-
target=server.serve_forever, daemon=True, name=f"SSHTunnel-{name}"
|
|
94
|
-
)
|
|
89
|
+
server_thread = threading.Thread(target=server.serve_forever, daemon=True, name=f"SSHTunnel-{name}")
|
|
95
90
|
server_thread.start()
|
|
96
91
|
|
|
97
92
|
info = {
|
|
File without changes
|
|
File without changes
|