mcp-win-stdio-ssh 0.2.4__py3-none-any.whl

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.
@@ -0,0 +1,5 @@
1
+ """
2
+ Advanced Multi-SSH Connection & Remote Management MCP Server for mcp-win-stdio.
3
+ """
4
+
5
+ __version__ = "0.2.4"
@@ -0,0 +1,8 @@
1
+ """
2
+ Direct execution entrypoint: python -m mcp_win_stdio.ssh
3
+ """
4
+
5
+ from mcp_win_stdio.ssh.server import mcp
6
+
7
+ if __name__ == "__main__":
8
+ mcp.run(transport="stdio")
@@ -0,0 +1,152 @@
1
+ """
2
+ CLI entry point for mcp-win-stdio-ssh.
3
+ """
4
+
5
+ import argparse
6
+ import os
7
+ from pathlib import Path
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import time
12
+
13
+ # Ensure Windows console uses UTF-8 without crashing on cp1252
14
+ if sys.platform == "win32":
15
+ try:
16
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
17
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
18
+ except Exception:
19
+ pass
20
+
21
+ from mcp_win_stdio.ssh import __version__
22
+ from mcp_win_stdio.ssh.connection import (
23
+ SYSTEM_SSH_CONFIG,
24
+ get_all_registered_hosts,
25
+ get_active_host_name,
26
+ resolve_host_info,
27
+ get_cached_or_connect,
28
+ )
29
+ from mcp_win_stdio.ssh.guide import print_ssh_guide
30
+ from mcp_win_stdio.ssh.server import mcp
31
+
32
+
33
+ def cmd_run(args: argparse.Namespace) -> None:
34
+ """Run SSH MCP server over stdio."""
35
+ mcp.run()
36
+
37
+
38
+ def cmd_doctor(args: argparse.Namespace) -> None:
39
+ """Run diagnostic checks on SSH drivers, Windows OpenSSH service, keys, and configs."""
40
+ print(f"\n=== mcp-win-stdio-ssh Doctor Diagnostic (v{__version__}) ===\n")
41
+ print(f"[OK] Python Runtime: {sys.version.split()[0]} ({sys.executable})")
42
+
43
+ # 1. Driver Checks
44
+ for dep in ("mcp", "paramiko", "cryptography"):
45
+ try:
46
+ mod = __import__(dep)
47
+ ver = getattr(mod, "__version__", "installed")
48
+ print(f"[OK] Python Module: {dep:<14} (v{ver})")
49
+ except ImportError:
50
+ print(f"[FAIL] Python Module: {dep:<14} (MISSING - run `pip install {dep}`)")
51
+
52
+ # 2. OpenSSH Windows client check
53
+ ssh_bin = shutil.which("ssh")
54
+ if ssh_bin:
55
+ print(f"[OK] System SSH CLI: {ssh_bin}")
56
+ else:
57
+ print(f"[INFO] System SSH CLI: Not on PATH (Optional: Paramiko native SSH client active)")
58
+
59
+ # 3. SSH Config & Keys
60
+ ssh_dir = Path.home() / ".ssh"
61
+ if ssh_dir.is_dir():
62
+ print(f"[OK] SSH Directory: {ssh_dir}")
63
+ if SYSTEM_SSH_CONFIG.exists():
64
+ print(f"[OK] SSH Config: {SYSTEM_SSH_CONFIG}")
65
+ else:
66
+ print(f"[INFO] SSH Config: No ~/.ssh/config found (can use add_host or create config)")
67
+
68
+ 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
+ if keys:
70
+ print(f"[OK] Found Keys: {', '.join(keys)}")
71
+ else:
72
+ print(f"[INFO] Found Keys: No default id_* keys found in ~/.ssh/")
73
+ else:
74
+ print(f"[INFO] SSH Directory: {ssh_dir} not created yet.")
75
+
76
+ # 4. Registered Hosts
77
+ hosts = get_all_registered_hosts()
78
+ active = get_active_host_name()
79
+ print(f"\n--- Registered Hosts ({len(hosts)}) ---")
80
+ for name, h in hosts.items():
81
+ is_act = " (ACTIVE DEFAULT)" if name == active else ""
82
+ print(f" • {name:<16} -> {h.get('user')}@{h.get('hostname')}:{h.get('port')} [{h.get('source')}]{is_act}")
83
+
84
+ print("\nDiagnostic complete.\n")
85
+
86
+
87
+ def cmd_test(args: argparse.Namespace) -> None:
88
+ """Test SSH connectivity and latency to configured hosts."""
89
+ target = args.host
90
+ hosts = get_all_registered_hosts()
91
+
92
+ if not hosts and not target:
93
+ print("\n[!] No SSH hosts configured. Use 'add_host' tool or configure ~/.ssh/config.\n")
94
+ return
95
+
96
+ targets = [target] if target else list(hosts.keys())
97
+
98
+ print(f"\n=== Testing SSH Connectivity ({len(targets)} host{'s' if len(targets) > 1 else ''}) ===\n")
99
+
100
+ for h_name in targets:
101
+ try:
102
+ h_info = resolve_host_info(h_name)
103
+ t_start = time.time()
104
+ client = get_cached_or_connect(h_info["name"], timeout=10)
105
+ lat_ms = round((time.time() - t_start) * 1000, 2)
106
+
107
+ stdin, stdout, stderr = client.exec_command("uname -srmo 2>/dev/null || ver", timeout=5)
108
+ os_info = stdout.read().decode("utf-8", errors="replace").strip()
109
+
110
+ print(f" [OK] {h_name:<16} -> {h_info.get('user')}@{h_info.get('hostname')}:{h_info.get('port')} ({lat_ms}ms) | {os_info}")
111
+ except Exception as e:
112
+ print(f" [FAIL] {h_name:<14} -> Error: {e}")
113
+
114
+ print("\nTest complete.\n")
115
+
116
+
117
+ def cmd_guide(args: argparse.Namespace) -> None:
118
+ """Print guide and prompt recipes."""
119
+ print_ssh_guide()
120
+
121
+
122
+ def main() -> None:
123
+ parser = argparse.ArgumentParser(
124
+ prog="mcp-win-stdio-ssh",
125
+ description="Unified Multi-SSH Connection & Remote Management MCP Server CLI",
126
+ )
127
+ parser.add_argument("--version", "-v", action="version", version=f"%(prog)s {__version__}")
128
+ subparsers = parser.add_subparsers(dest="command", help="Command to execute")
129
+
130
+ sub_guide = subparsers.add_parser("guide", help="View usage guide and recipes")
131
+ sub_guide.set_defaults(func=cmd_guide)
132
+
133
+ sub_doctor = subparsers.add_parser("doctor", help="Check SSH configuration and drivers")
134
+ sub_doctor.set_defaults(func=cmd_doctor)
135
+
136
+ sub_test = subparsers.add_parser("test", help="Test connectivity to configured hosts")
137
+ sub_test.add_argument("host", nargs="?", help="Specific host name to test")
138
+ sub_test.set_defaults(func=cmd_test)
139
+
140
+ sub_run = subparsers.add_parser("run", help="Run SSH MCP server over stdio")
141
+ sub_run.set_defaults(func=cmd_run)
142
+
143
+ args = parser.parse_args()
144
+ if hasattr(args, "func"):
145
+ args.func(args)
146
+ else:
147
+ # Default action is run
148
+ cmd_run(args)
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
@@ -0,0 +1,369 @@
1
+ """
2
+ SSH Connection Pool and Host Configuration Manager for mcp-win-stdio-ssh.
3
+ Handles ~/.ssh/config auto-discovery, persistent host bookmarks, authentication resolution,
4
+ bastion/jump host tunneling, and resilient connection pooling.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ import re
11
+ import socket
12
+ import sys
13
+ import threading
14
+ import time
15
+ from typing import Any, Dict, List, Optional, Tuple, Union
16
+
17
+ import paramiko
18
+ from paramiko.config import SSHConfig
19
+
20
+ from mcp_win_stdio.core.config import INDALA_DIR, ensure_workspace_dirs
21
+
22
+ SSH_CONFIG_DIR = INDALA_DIR
23
+ HOSTS_CONFIG_FILE = SSH_CONFIG_DIR / "ssh_hosts.json"
24
+ SSH_USER_DIR = Path.home() / ".ssh"
25
+ SYSTEM_SSH_CONFIG = SSH_USER_DIR / "config"
26
+
27
+ _ACTIVE_HOST: Optional[str] = None
28
+ _CLIENT_POOL: Dict[str, paramiko.SSHClient] = {}
29
+ _POOL_LOCK = threading.RLock()
30
+
31
+
32
+ def _normalize_host_param(host: Any) -> Optional[str]:
33
+ """Normalize host parameter from LLM tool call, filtering out null/empty strings."""
34
+ if host is None:
35
+ return None
36
+ s = str(host).strip()
37
+ if s.lower() in ("", "null", "none", "undefined", "default"):
38
+ return None
39
+ return s
40
+
41
+
42
+ def load_ssh_hosts() -> Dict[str, Any]:
43
+ """Load persistent SSH hosts from ~/.mcp-win-stdio/ssh_hosts.json."""
44
+ ensure_workspace_dirs()
45
+ if not HOSTS_CONFIG_FILE.exists():
46
+ return {}
47
+ try:
48
+ with open(HOSTS_CONFIG_FILE, "r", encoding="utf-8") as f:
49
+ data = json.load(f)
50
+ return data if isinstance(data, dict) else {}
51
+ except Exception:
52
+ return {}
53
+
54
+
55
+ def save_ssh_hosts(data: Dict[str, Any]) -> None:
56
+ """Save persistent SSH hosts to ~/.mcp-win-stdio/ssh_hosts.json."""
57
+ ensure_workspace_dirs()
58
+ try:
59
+ with open(HOSTS_CONFIG_FILE, "w", encoding="utf-8") as f:
60
+ json.dump(data, f, indent=2)
61
+ except Exception as e:
62
+ sys.stderr.write(f"Warning: Failed to save ssh hosts: {e}\n")
63
+
64
+
65
+ def get_active_host_name() -> Optional[str]:
66
+ """Get the currently active host name (memory or persistent)."""
67
+ global _ACTIVE_HOST
68
+ if _ACTIVE_HOST:
69
+ return _ACTIVE_HOST
70
+ data = load_ssh_hosts()
71
+ active = data.get("active_host")
72
+ if active:
73
+ _ACTIVE_HOST = active
74
+ return active
75
+ return None
76
+
77
+
78
+ def set_active_host_name(name: Optional[str]) -> None:
79
+ """Set and persist the active host name."""
80
+ global _ACTIVE_HOST
81
+ _ACTIVE_HOST = name
82
+ data = load_ssh_hosts()
83
+ data["active_host"] = name
84
+ save_ssh_hosts(data)
85
+
86
+
87
+ def parse_system_ssh_config() -> Dict[str, Dict[str, Any]]:
88
+ """Parse ~/.ssh/config and return a mapping of host aliases to connection configs."""
89
+ configs: Dict[str, Dict[str, Any]] = {}
90
+ if not SYSTEM_SSH_CONFIG.exists():
91
+ return configs
92
+
93
+ try:
94
+ ssh_cfg = SSHConfig()
95
+ with open(SYSTEM_SSH_CONFIG, "r", encoding="utf-8", errors="replace") as f:
96
+ ssh_cfg.parse(f)
97
+
98
+ # Iterate through host entries
99
+ for host in ssh_cfg.get_hostnames():
100
+ if host == "*":
101
+ continue
102
+ entry = ssh_cfg.lookup(host)
103
+ identity_files = entry.get("identityfile", [])
104
+ key_path = identity_files[0] if identity_files else None
105
+
106
+ # Resolve ~ in key path
107
+ if key_path:
108
+ key_path = str(Path(key_path).expanduser())
109
+
110
+ configs[host] = {
111
+ "name": host,
112
+ "hostname": entry.get("hostname", host),
113
+ "user": entry.get("user", os.environ.get("USERNAME", "root")),
114
+ "port": int(entry.get("port", 22)),
115
+ "key_path": key_path,
116
+ "proxyjump": entry.get("proxyjump"),
117
+ "proxycommand": entry.get("proxycommand"),
118
+ "source": "ssh_config",
119
+ }
120
+ except Exception as e:
121
+ sys.stderr.write(f"Warning: Failed to parse system SSH config: {e}\n")
122
+
123
+ return configs
124
+
125
+
126
+ def get_all_registered_hosts() -> Dict[str, Dict[str, Any]]:
127
+ """Merge SSH config hosts, persistent custom hosts, and env variables."""
128
+ all_hosts: Dict[str, Dict[str, Any]] = {}
129
+
130
+ # 1. System ~/.ssh/config
131
+ system_hosts = parse_system_ssh_config()
132
+ all_hosts.update(system_hosts)
133
+
134
+ # 2. Persistent ~/.mcp-win-stdio/ssh_hosts.json
135
+ saved_data = load_ssh_hosts()
136
+ hosts_dict = saved_data.get("hosts", {})
137
+ for name, host_data in hosts_dict.items():
138
+ if isinstance(host_data, dict):
139
+ entry = dict(host_data)
140
+ entry["name"] = name
141
+ entry["source"] = "saved_config"
142
+ all_hosts[name] = entry
143
+
144
+ # 3. Environment variable SSH_SERVERS (JSON format)
145
+ env_servers = os.environ.get("SSH_SERVERS")
146
+ if env_servers:
147
+ try:
148
+ parsed = json.loads(env_servers)
149
+ if isinstance(parsed, dict):
150
+ for name, cfg in parsed.items():
151
+ if isinstance(cfg, dict):
152
+ entry = dict(cfg)
153
+ entry["name"] = name
154
+ entry["source"] = "env"
155
+ all_hosts[name] = entry
156
+ except Exception:
157
+ pass
158
+
159
+ return all_hosts
160
+
161
+
162
+ def resolve_host_info(target_name: Optional[str] = None) -> Dict[str, Any]:
163
+ """
164
+ Resolve connection parameters for a host by name or active default.
165
+ Supports user@host:port strings or registered aliases.
166
+ """
167
+ norm = _normalize_host_param(target_name)
168
+ host_name = norm or get_active_host_name()
169
+
170
+ registered = get_all_registered_hosts()
171
+
172
+ if host_name and host_name in registered:
173
+ return registered[host_name]
174
+
175
+ # Check if target is a direct user@hostname:port connection string
176
+ if host_name and ("@" in host_name or "." in host_name):
177
+ user = os.environ.get("USERNAME", "root")
178
+ host_str = host_name
179
+ port = 22
180
+
181
+ if "@" in host_str:
182
+ user, host_str = host_str.split("@", 1)
183
+
184
+ if ":" in host_str:
185
+ host_str, port_str = host_str.rsplit(":", 1)
186
+ try:
187
+ port = int(port_str)
188
+ except ValueError:
189
+ port = 22
190
+
191
+ return {
192
+ "name": host_name,
193
+ "hostname": host_str,
194
+ "user": user,
195
+ "port": port,
196
+ "key_path": None,
197
+ "source": "ad_hoc",
198
+ }
199
+
200
+ if not host_name:
201
+ if registered:
202
+ first_name = next(iter(registered.keys()))
203
+ set_active_host_name(first_name)
204
+ return registered[first_name]
205
+ raise ValueError(
206
+ "No SSH host specified and no active host is configured. "
207
+ "Use 'add_host' or provide 'host' parameter."
208
+ )
209
+
210
+ raise ValueError(f"SSH host '{host_name}' not found. Available hosts: {list(registered.keys())}")
211
+
212
+
213
+ def _find_default_keys() -> List[str]:
214
+ """Find default SSH keys in ~/.ssh/ directory."""
215
+ keys = []
216
+ if SSH_USER_DIR.is_dir():
217
+ for key_name in ["id_ed25519", "id_rsa", "id_ecdsa", "id_dsa"]:
218
+ kp = SSH_USER_DIR / key_name
219
+ if kp.exists():
220
+ keys.append(str(kp))
221
+ return keys
222
+
223
+
224
+ def create_ssh_client(
225
+ host_info: Dict[str, Any],
226
+ password: Optional[str] = None,
227
+ passphrase: Optional[str] = None,
228
+ timeout: int = 15,
229
+ ) -> paramiko.SSHClient:
230
+ """Instantiate and connect a paramiko.SSHClient based on host info."""
231
+ client = paramiko.SSHClient()
232
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
233
+
234
+ hostname = host_info.get("hostname", "localhost")
235
+ port = int(host_info.get("port", 22))
236
+ username = host_info.get("user") or os.environ.get("USERNAME", "root")
237
+ key_path = host_info.get("key_path")
238
+ pwd = password or host_info.get("password")
239
+ pass_phrase = passphrase or host_info.get("passphrase")
240
+
241
+ key_filename = None
242
+ if key_path and Path(key_path).expanduser().exists():
243
+ key_filename = str(Path(key_path).expanduser())
244
+ else:
245
+ # Check default keys
246
+ default_keys = _find_default_keys()
247
+ if default_keys:
248
+ key_filename = default_keys[0]
249
+
250
+ # Support JumpHost / Bastion if specified
251
+ sock = None
252
+ jump_host = host_info.get("jump_host") or host_info.get("proxyjump")
253
+ if jump_host:
254
+ jump_info = resolve_host_info(jump_host)
255
+ jump_client = get_cached_or_connect(jump_info["name"])
256
+ transport = jump_client.get_transport()
257
+ if transport and transport.is_active():
258
+ dest_addr = (hostname, port)
259
+ local_addr = ("127.0.0.1", 0)
260
+ sock = transport.open_channel("direct-tcpip", dest_addr, local_addr)
261
+
262
+ connect_kwargs: Dict[str, Any] = {
263
+ "hostname": hostname,
264
+ "port": port,
265
+ "username": username,
266
+ "timeout": timeout,
267
+ "allow_agent": True,
268
+ "look_for_keys": True,
269
+ }
270
+
271
+ if sock:
272
+ connect_kwargs["sock"] = sock
273
+ if pwd:
274
+ connect_kwargs["password"] = pwd
275
+ if key_filename:
276
+ connect_kwargs["key_filename"] = key_filename
277
+ if pass_phrase:
278
+ connect_kwargs["passphrase"] = pass_phrase
279
+
280
+ try:
281
+ client.connect(**connect_kwargs)
282
+ # Enable keepalive
283
+ transport = client.get_transport()
284
+ if transport:
285
+ transport.set_keepalive(30)
286
+ return client
287
+ except Exception as e:
288
+ client.close()
289
+ raise ConnectionError(
290
+ f"Failed to connect to SSH host '{host_info.get('name', hostname)}' ({username}@{hostname}:{port}): {e}"
291
+ )
292
+
293
+
294
+ def get_cached_or_connect(
295
+ target_name: Optional[str] = None,
296
+ password: Optional[str] = None,
297
+ passphrase: Optional[str] = None,
298
+ timeout: int = 15,
299
+ ) -> paramiko.SSHClient:
300
+ """Retrieve an active SSH client from the pool or establish a new connection."""
301
+ host_info = resolve_host_info(target_name)
302
+ name = host_info["name"]
303
+
304
+ with _POOL_LOCK:
305
+ if name in _CLIENT_POOL:
306
+ client = _CLIENT_POOL[name]
307
+ transport = client.get_transport()
308
+ if transport and transport.is_active():
309
+ return client
310
+ # Drop dead connection
311
+ try:
312
+ client.close()
313
+ except Exception:
314
+ pass
315
+ del _CLIENT_POOL[name]
316
+
317
+ client = create_ssh_client(
318
+ host_info, password=password, passphrase=passphrase, timeout=timeout
319
+ )
320
+ _CLIENT_POOL[name] = client
321
+ return client
322
+
323
+
324
+ def close_connection(target_name: Optional[str] = None) -> bool:
325
+ """Close connection for a specific host in the pool."""
326
+ norm = _normalize_host_param(target_name)
327
+ name = norm or get_active_host_name()
328
+ if not name:
329
+ return False
330
+
331
+ with _POOL_LOCK:
332
+ if name in _CLIENT_POOL:
333
+ try:
334
+ _CLIENT_POOL[name].close()
335
+ except Exception:
336
+ pass
337
+ del _CLIENT_POOL[name]
338
+ return True
339
+ return False
340
+
341
+
342
+ def close_all_connections() -> int:
343
+ """Close all open SSH connections in the pool."""
344
+ closed = 0
345
+ with _POOL_LOCK:
346
+ for name, client in list(_CLIENT_POOL.items()):
347
+ try:
348
+ client.close()
349
+ closed += 1
350
+ except Exception:
351
+ pass
352
+ _CLIENT_POOL.clear()
353
+ return closed
354
+
355
+
356
+ def get_pool_status() -> List[Dict[str, Any]]:
357
+ """Return status of all live connections in the pool."""
358
+ status = []
359
+ with _POOL_LOCK:
360
+ for name, client in _CLIENT_POOL.items():
361
+ transport = client.get_transport()
362
+ is_active = transport.is_active() if transport else False
363
+ status.append({
364
+ "name": name,
365
+ "isActive": is_active,
366
+ "remoteAddress": f"{transport.getpeername()}" if (transport and is_active) else "closed",
367
+ "isCurrentDefault": name == get_active_host_name(),
368
+ })
369
+ return status
@@ -0,0 +1,94 @@
1
+ """
2
+ Comprehensive guide, tool reference, and Claude prompt recipes for mcp-win-stdio-ssh.
3
+ """
4
+
5
+ def print_ssh_guide() -> None:
6
+ """Print complete SSH MCP tool reference and workflow recipes."""
7
+ guide_text = """
8
+ ================================================================================
9
+ 🚀 mcp-win-stdio-ssh — Advanced Multi-SSH Connection & Remote Management
10
+ ================================================================================
11
+
12
+ The SSH MCP server connects AI models directly to remote Linux, macOS, and Windows
13
+ servers over standard SSH, SFTP, and interactive PTY channels.
14
+
15
+ --------------------------------------------------------------------------------
16
+ 🌟 Key Architecture & Capabilities
17
+ --------------------------------------------------------------------------------
18
+ • Multi-Host Pooling: Connect to multiple servers concurrently with connection reuse.
19
+ • Auto-Discovery: Automatically reads ~/.ssh/config aliases, identity files & proxy jumps.
20
+ • Sticky Context: 'use_host' sets the default target host across subsequent tool calls.
21
+ • Interactive PTY: Stateful terminal sessions for long-running processes, REPLs & prompts.
22
+ • Service & Health: Fast system metrics, systemd/docker/pm2 inspection, and log tailing.
23
+ • Full SFTP: Remote file reading/writing with token protection, directory recursion.
24
+ • Local Tunnels: Port forwarding to expose remote MySQL/Postgres/Web services locally.
25
+
26
+ --------------------------------------------------------------------------------
27
+ 🛠️ Complete Tool Reference (27 Tools)
28
+ --------------------------------------------------------------------------------
29
+
30
+ 1. Host & Connection Management
31
+ • list_hosts() -> List all discovered/saved SSH hosts & status
32
+ • use_host(host) -> Switch active sticky host context
33
+ • add_host(name, hostname, ...) -> Save new host to ~/.mcp-win-stdio/ssh_hosts.json
34
+ • remove_host(name) -> Delete saved host configuration
35
+ • test_host(host=None) -> Test SSH handshake, auth, latency, and remote OS
36
+ • list_active_connections() -> Show live connections in the connection pool
37
+ • disconnect_host(host=None) -> Close connection for specific or active host
38
+
39
+ 2. Command Execution & Background Jobs
40
+ • ssh_exec(command, cwd, env, ...) -> Execute non-interactive command & return exit code
41
+ • ssh_exec_sudo(command, password) -> Run command with sudo handling password prompt
42
+ • ssh_exec_script(script_content) -> Upload and execute bash/python/sh script
43
+ • ssh_exec_background(command) -> Run detached background job (nohup) & get PID
44
+ • ssh_check_job(job_id_or_pid) -> Inspect status and logs of background job
45
+ • ssh_kill_job(pid, signal) -> Terminate remote process by PID
46
+
47
+ 3. Interactive PTY / Shell
48
+ • ssh_pty_start(session_name) -> Spawn interactive terminal session
49
+ • ssh_pty_send(session_name, text) -> Send command/keystroke and read buffer
50
+ • ssh_pty_read(session_name) -> Read output buffer from PTY session
51
+ • ssh_list_pty_sessions() -> List open interactive PTY sessions
52
+ • ssh_pty_close(session_name) -> Terminate PTY session
53
+
54
+ 4. Diagnostics & Remote Services
55
+ • ssh_system_overview() -> CPU/RAM/Disk stats, OS distro, kernel, uptime
56
+ • ssh_list_services(type, filter) -> List systemd units, docker containers, or pm2 apps
57
+ • ssh_service_action(name, action) -> Manage service (status/start/stop/restart/reload)
58
+ • ssh_tail_logs(target, lines) -> Tail log files (/var/log/...) or journalctl
59
+ • ssh_list_processes(sort_by) -> Top processes by CPU or Memory
60
+ • ssh_list_packages(manager, ...) -> Paginated installed packages (apt, dnf, pacman, pip, npm)
61
+
62
+ 5. SFTP File Management
63
+ • sftp_list_dir(remote_path) -> List directory with permissions & sizes
64
+ • sftp_read_file(remote_path) -> Read remote text file with line offset
65
+ • sftp_write_file(remote_path, ...) -> Write or append remote file
66
+ • sftp_stat(remote_path) -> Get file/folder permissions & metadata
67
+ • sftp_mkdir(remote_path) -> Create remote directory
68
+ • sftp_remove(remote_path) -> Delete remote file or directory
69
+ • sftp_upload(local_path, remote) -> Upload file or folder recursively
70
+ • sftp_download(remote, local_path) -> Download file or folder recursively
71
+
72
+ 6. Port Forwarding & Tunnels
73
+ • ssh_tunnel_open(local, remote) -> Forward remote port to 127.0.0.1:<local_port>
74
+ • ssh_tunnel_list() -> List active local tunnels
75
+ • ssh_tunnel_close(tunnel_id) -> Terminate active tunnel
76
+
77
+ --------------------------------------------------------------------------------
78
+ 💡 Example Claude Workflows & Prompts
79
+ --------------------------------------------------------------------------------
80
+
81
+ 1. Remote Diagnostics:
82
+ "Check the health and disk usage of production-server, then list any failed systemd services."
83
+
84
+ 2. Deploy Application & Check Logs:
85
+ "Upload ./dist to /var/www/my-app on web-01, restart the nginx service, and tail the error log."
86
+
87
+ 3. Database Tunneling:
88
+ "Open a local tunnel from port 15432 to remote Postgres port 5432 on db-cluster, then check table counts."
89
+
90
+ 4. Interactive Configuration:
91
+ "Start an interactive PTY session on staging-server and run the configuration script."
92
+ ================================================================================
93
+ """
94
+ print(guide_text)