mcp-ssh-vps 0.4.1__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.
Files changed (47) hide show
  1. mcp_ssh_vps-0.4.1.dist-info/METADATA +482 -0
  2. mcp_ssh_vps-0.4.1.dist-info/RECORD +47 -0
  3. mcp_ssh_vps-0.4.1.dist-info/WHEEL +5 -0
  4. mcp_ssh_vps-0.4.1.dist-info/entry_points.txt +4 -0
  5. mcp_ssh_vps-0.4.1.dist-info/licenses/LICENSE +21 -0
  6. mcp_ssh_vps-0.4.1.dist-info/top_level.txt +1 -0
  7. sshmcp/__init__.py +3 -0
  8. sshmcp/cli.py +473 -0
  9. sshmcp/config.py +155 -0
  10. sshmcp/core/__init__.py +5 -0
  11. sshmcp/core/container.py +291 -0
  12. sshmcp/models/__init__.py +15 -0
  13. sshmcp/models/command.py +69 -0
  14. sshmcp/models/file.py +102 -0
  15. sshmcp/models/machine.py +139 -0
  16. sshmcp/monitoring/__init__.py +0 -0
  17. sshmcp/monitoring/alerts.py +464 -0
  18. sshmcp/prompts/__init__.py +7 -0
  19. sshmcp/prompts/backup.py +151 -0
  20. sshmcp/prompts/deploy.py +115 -0
  21. sshmcp/prompts/monitor.py +146 -0
  22. sshmcp/resources/__init__.py +7 -0
  23. sshmcp/resources/logs.py +99 -0
  24. sshmcp/resources/metrics.py +204 -0
  25. sshmcp/resources/status.py +160 -0
  26. sshmcp/security/__init__.py +7 -0
  27. sshmcp/security/audit.py +314 -0
  28. sshmcp/security/rate_limiter.py +221 -0
  29. sshmcp/security/totp.py +392 -0
  30. sshmcp/security/validator.py +234 -0
  31. sshmcp/security/whitelist.py +169 -0
  32. sshmcp/server.py +632 -0
  33. sshmcp/ssh/__init__.py +6 -0
  34. sshmcp/ssh/async_client.py +247 -0
  35. sshmcp/ssh/client.py +464 -0
  36. sshmcp/ssh/executor.py +79 -0
  37. sshmcp/ssh/forwarding.py +368 -0
  38. sshmcp/ssh/pool.py +343 -0
  39. sshmcp/ssh/shell.py +518 -0
  40. sshmcp/ssh/transfer.py +461 -0
  41. sshmcp/tools/__init__.py +13 -0
  42. sshmcp/tools/commands.py +226 -0
  43. sshmcp/tools/files.py +220 -0
  44. sshmcp/tools/helpers.py +321 -0
  45. sshmcp/tools/history.py +372 -0
  46. sshmcp/tools/processes.py +214 -0
  47. sshmcp/tools/servers.py +484 -0
sshmcp/tools/files.py ADDED
@@ -0,0 +1,220 @@
1
+ """MCP Tools for file operations."""
2
+
3
+ from typing import Any
4
+
5
+ import structlog
6
+
7
+ from sshmcp.config import get_machine
8
+ from sshmcp.security.audit import get_audit_logger
9
+ from sshmcp.security.validator import validate_path
10
+ from sshmcp.ssh.client import SSHExecutionError
11
+ from sshmcp.ssh.pool import get_pool
12
+
13
+ logger = structlog.get_logger()
14
+
15
+
16
+ def read_file(
17
+ host: str,
18
+ path: str,
19
+ encoding: str = "utf-8",
20
+ max_size: int = 1024 * 1024,
21
+ ) -> dict[str, Any]:
22
+ """
23
+ Read file content from remote VPS server.
24
+
25
+ Args:
26
+ host: Name of the host from machines.json configuration.
27
+ path: Path to the file on remote server.
28
+ encoding: File encoding (default: utf-8).
29
+ max_size: Maximum file size to read in bytes (default: 1MB).
30
+
31
+ Returns:
32
+ Dictionary with:
33
+ - content: File content as string
34
+ - path: Full path to file
35
+ - size: File size in bytes
36
+ - encoding: File encoding used
37
+ - truncated: Whether content was truncated
38
+ - host: Host where file is located
39
+
40
+ Raises:
41
+ ValueError: If host not found or path not allowed.
42
+ RuntimeError: If file cannot be read.
43
+
44
+ Example:
45
+ >>> read_file("production-server", "/var/log/app.log")
46
+ {"content": "log content...", "size": 1024, ...}
47
+ """
48
+ audit = get_audit_logger()
49
+
50
+ # Get machine configuration
51
+ try:
52
+ machine = get_machine(host)
53
+ except Exception as e:
54
+ raise ValueError(f"Host not found: {host}") from e
55
+
56
+ # Validate path
57
+ is_valid, error_msg = validate_path(path, machine.security, "read")
58
+ if not is_valid:
59
+ audit.log_path_rejected(host, path, error_msg or "Path not allowed")
60
+ raise ValueError(f"Path not allowed: {error_msg}")
61
+
62
+ # Read file
63
+ pool = get_pool()
64
+ pool.register_machine(machine)
65
+
66
+ try:
67
+ client = pool.get_client(host)
68
+ try:
69
+ result = client.read_file(path, encoding=encoding, max_size=max_size)
70
+
71
+ audit.log_file_read(host, path, result.size)
72
+
73
+ return result.to_dict()
74
+
75
+ finally:
76
+ pool.release_client(client)
77
+
78
+ except SSHExecutionError as e:
79
+ raise RuntimeError(f"Failed to read file: {e}") from e
80
+ except Exception as e:
81
+ raise RuntimeError(f"SSH error: {e}") from e
82
+
83
+
84
+ def upload_file(
85
+ host: str,
86
+ path: str,
87
+ content: str,
88
+ mode: str | None = None,
89
+ ) -> dict[str, Any]:
90
+ """
91
+ Upload file to remote VPS server.
92
+
93
+ Args:
94
+ host: Name of the host from machines.json configuration.
95
+ path: Destination path on remote server.
96
+ content: File content to write.
97
+ mode: Optional file permissions (e.g., "0644").
98
+
99
+ Returns:
100
+ Dictionary with:
101
+ - success: Whether upload was successful
102
+ - path: Path where file was uploaded
103
+ - size: Uploaded file size in bytes
104
+ - host: Host where file was uploaded
105
+
106
+ Raises:
107
+ ValueError: If host not found or path not allowed.
108
+ RuntimeError: If file cannot be written.
109
+
110
+ Example:
111
+ >>> upload_file("production-server", "/opt/app/config.json", '{"key": "value"}')
112
+ {"success": true, "path": "/opt/app/config.json", "size": 16, ...}
113
+ """
114
+ audit = get_audit_logger()
115
+
116
+ # Get machine configuration
117
+ try:
118
+ machine = get_machine(host)
119
+ except Exception as e:
120
+ raise ValueError(f"Host not found: {host}") from e
121
+
122
+ # Validate path
123
+ is_valid, error_msg = validate_path(path, machine.security, "write")
124
+ if not is_valid:
125
+ audit.log_path_rejected(host, path, error_msg or "Path not allowed")
126
+ raise ValueError(f"Path not allowed: {error_msg}")
127
+
128
+ # Write file
129
+ pool = get_pool()
130
+ pool.register_machine(machine)
131
+
132
+ try:
133
+ client = pool.get_client(host)
134
+ try:
135
+ result = client.write_file(path, content, mode=mode)
136
+
137
+ audit.log_file_write(host, path, result.size)
138
+
139
+ return result.to_dict()
140
+
141
+ finally:
142
+ pool.release_client(client)
143
+
144
+ except SSHExecutionError as e:
145
+ raise RuntimeError(f"Failed to write file: {e}") from e
146
+ except Exception as e:
147
+ raise RuntimeError(f"SSH error: {e}") from e
148
+
149
+
150
+ def list_files(
151
+ host: str,
152
+ directory: str,
153
+ recursive: bool = False,
154
+ ) -> dict[str, Any]:
155
+ """
156
+ List files in directory on remote VPS server.
157
+
158
+ Args:
159
+ host: Name of the host from machines.json configuration.
160
+ directory: Path to directory on remote server.
161
+ recursive: Whether to list files recursively (default: false).
162
+
163
+ Returns:
164
+ Dictionary with:
165
+ - files: List of file information objects
166
+ - directory: Listed directory path
167
+ - host: Host where files are located
168
+ - total_count: Total number of files
169
+
170
+ Raises:
171
+ ValueError: If host not found or path not allowed.
172
+ RuntimeError: If directory cannot be listed.
173
+
174
+ Example:
175
+ >>> list_files("production-server", "/var/www")
176
+ {"files": [...], "directory": "/var/www", "total_count": 10, ...}
177
+ """
178
+ audit = get_audit_logger()
179
+
180
+ # Get machine configuration
181
+ try:
182
+ machine = get_machine(host)
183
+ except Exception as e:
184
+ raise ValueError(f"Host not found: {host}") from e
185
+
186
+ # Validate path
187
+ is_valid, error_msg = validate_path(directory, machine.security, "read")
188
+ if not is_valid:
189
+ audit.log_path_rejected(host, directory, error_msg or "Path not allowed")
190
+ raise ValueError(f"Path not allowed: {error_msg}")
191
+
192
+ # List files
193
+ pool = get_pool()
194
+ pool.register_machine(machine)
195
+
196
+ try:
197
+ client = pool.get_client(host)
198
+ try:
199
+ files = client.list_files(directory, recursive=recursive)
200
+
201
+ audit.log(
202
+ event="directory_listed",
203
+ host=host,
204
+ result={"path": directory, "count": len(files)},
205
+ )
206
+
207
+ return {
208
+ "files": [f.to_dict() for f in files],
209
+ "directory": directory,
210
+ "host": host,
211
+ "total_count": len(files),
212
+ }
213
+
214
+ finally:
215
+ pool.release_client(client)
216
+
217
+ except SSHExecutionError as e:
218
+ raise RuntimeError(f"Failed to list directory: {e}") from e
219
+ except Exception as e:
220
+ raise RuntimeError(f"SSH error: {e}") from e
@@ -0,0 +1,321 @@
1
+ """MCP Tools for help and information."""
2
+
3
+ from typing import Any
4
+
5
+ from sshmcp.config import get_machine, list_machines
6
+
7
+
8
+ def get_help(topic: str | None = None) -> dict[str, Any]:
9
+ """
10
+ Get help information about available commands and features.
11
+
12
+ Provides documentation about tools, security profiles, and usage examples.
13
+
14
+ Args:
15
+ topic: Specific topic to get help for. Options:
16
+ - "tools" - list all available MCP tools
17
+ - "security" - security profiles and allowed commands
18
+ - "servers" - server management commands
19
+ - "examples" - usage examples
20
+ - None - general overview
21
+
22
+ Returns:
23
+ Dictionary with help information.
24
+
25
+ Example:
26
+ >>> get_help("tools")
27
+ {"topic": "tools", "content": "Available tools: ..."}
28
+ """
29
+ help_topics = {
30
+ "tools": {
31
+ "topic": "Available MCP Tools",
32
+ "tools": [
33
+ {
34
+ "name": "execute_command",
35
+ "description": "Execute shell command on remote server",
36
+ "args": "host (str), command (str), timeout (int, optional)",
37
+ "example": 'execute_command("prod", "docker ps -a")',
38
+ },
39
+ {
40
+ "name": "execute_on_multiple",
41
+ "description": "Execute command on multiple servers",
42
+ "args": "hosts (list[str]), command (str)",
43
+ "example": 'execute_on_multiple(["prod", "staging"], "uptime")',
44
+ },
45
+ {
46
+ "name": "list_servers",
47
+ "description": "List all configured servers",
48
+ "args": "tag (str, optional) - filter by tag",
49
+ "example": "list_servers() or list_servers(tag='production')",
50
+ },
51
+ {
52
+ "name": "add_server",
53
+ "description": "Add new VPS server",
54
+ "args": "name, host, user, port, auth_type, key_path/password, tags",
55
+ "example": 'add_server("web1", "1.2.3.4", "root", tags=["production"])',
56
+ },
57
+ {
58
+ "name": "remove_server",
59
+ "description": "Remove server from configuration",
60
+ "args": "name (str)",
61
+ "example": 'remove_server("old-server")',
62
+ },
63
+ {
64
+ "name": "test_connection",
65
+ "description": "Test SSH connection to server",
66
+ "args": "name (str)",
67
+ "example": 'test_connection("prod")',
68
+ },
69
+ {
70
+ "name": "get_allowed_commands",
71
+ "description": "Get list of allowed commands for a server",
72
+ "args": "host (str)",
73
+ "example": 'get_allowed_commands("prod")',
74
+ },
75
+ {
76
+ "name": "read_file",
77
+ "description": "Read file content from remote server",
78
+ "args": "host (str), path (str), max_size (int, optional)",
79
+ "example": 'read_file("prod", "/var/log/app.log")',
80
+ },
81
+ {
82
+ "name": "upload_file",
83
+ "description": "Upload file to remote server",
84
+ "args": "host (str), remote_path (str), content (str)",
85
+ "example": 'upload_file("prod", "/tmp/script.sh", "#!/bin/bash\\necho hi")',
86
+ },
87
+ {
88
+ "name": "list_files",
89
+ "description": "List files in directory on remote server",
90
+ "args": "host (str), directory (str), recursive (bool)",
91
+ "example": 'list_files("prod", "/var/log")',
92
+ },
93
+ {
94
+ "name": "manage_process",
95
+ "description": "Manage processes (systemd/pm2/supervisor)",
96
+ "args": "host (str), action (start/stop/restart/status), process_name",
97
+ "example": 'manage_process("prod", "restart", "nginx")',
98
+ },
99
+ ],
100
+ },
101
+ "security": {
102
+ "topic": "Security Profiles",
103
+ "profiles": [
104
+ {
105
+ "name": "strict",
106
+ "description": "Only safe read-only commands",
107
+ "allowed": [
108
+ "git pull/status",
109
+ "ls",
110
+ "cat",
111
+ "tail",
112
+ "df",
113
+ "free",
114
+ "uptime",
115
+ ],
116
+ "forbidden": ["rm -rf", "sudo", "su -"],
117
+ "timeout": 30,
118
+ },
119
+ {
120
+ "name": "moderate",
121
+ "description": "Standard DevOps commands",
122
+ "allowed": [
123
+ "git",
124
+ "npm",
125
+ "yarn",
126
+ "pip",
127
+ "pm2",
128
+ "systemctl",
129
+ "docker",
130
+ "ls",
131
+ "cat",
132
+ "ps",
133
+ "top",
134
+ ],
135
+ "forbidden": ["rm -rf /", "sudo rm"],
136
+ "timeout": 60,
137
+ },
138
+ {
139
+ "name": "full",
140
+ "description": "All commands allowed",
141
+ "allowed": ["ALL commands"],
142
+ "forbidden": ["rm -rf /"],
143
+ "timeout": 120,
144
+ },
145
+ ],
146
+ "note": "Use security_level parameter when adding servers: add_server(..., security_level='full')",
147
+ },
148
+ "servers": {
149
+ "topic": "Server Management",
150
+ "commands": [
151
+ "list_servers() - List all servers",
152
+ "list_servers(tag='prod') - Filter by tag",
153
+ "add_server(name, host, user, ...) - Add new server",
154
+ "remove_server(name) - Remove server",
155
+ "update_server(name, ...) - Update server settings",
156
+ "test_connection(name) - Test SSH connection",
157
+ ],
158
+ "tips": [
159
+ "Use tags to organize servers: tags=['production', 'web']",
160
+ "Test connection after adding: test_connection('server-name')",
161
+ "Use security_level='full' for unrestricted access",
162
+ ],
163
+ },
164
+ "examples": {
165
+ "topic": "Usage Examples",
166
+ "examples": [
167
+ {
168
+ "task": "Check server status",
169
+ "commands": [
170
+ 'execute_command("prod", "uptime")',
171
+ 'execute_command("prod", "df -h")',
172
+ 'execute_command("prod", "free -m")',
173
+ ],
174
+ },
175
+ {
176
+ "task": "Docker management",
177
+ "commands": [
178
+ 'execute_command("prod", "docker ps -a")',
179
+ 'execute_command("prod", "docker logs nginx --tail 100")',
180
+ 'execute_command("prod", "docker restart nginx")',
181
+ ],
182
+ },
183
+ {
184
+ "task": "Deploy application",
185
+ "commands": [
186
+ 'execute_command("prod", "cd /app && git pull")',
187
+ 'execute_command("prod", "cd /app && npm install")',
188
+ 'execute_command("prod", "pm2 restart all")',
189
+ ],
190
+ },
191
+ {
192
+ "task": "Check logs",
193
+ "commands": [
194
+ 'execute_command("prod", "tail -100 /var/log/nginx/error.log")',
195
+ 'execute_command("prod", "journalctl -u nginx --since \'1 hour ago\'")',
196
+ ],
197
+ },
198
+ {
199
+ "task": "Run on multiple servers",
200
+ "commands": [
201
+ 'execute_on_multiple(["web1", "web2", "web3"], "uptime")',
202
+ 'execute_on_multiple(["prod", "staging"], "docker ps")',
203
+ ],
204
+ },
205
+ ],
206
+ },
207
+ }
208
+
209
+ if topic is None:
210
+ return {
211
+ "topic": "SSH MCP Server Help",
212
+ "description": "MCP server for managing VPS via SSH",
213
+ "available_topics": list(help_topics.keys()),
214
+ "usage": "Call get_help(topic) for detailed information",
215
+ "quick_start": [
216
+ "1. list_servers() - See available servers",
217
+ "2. test_connection('server-name') - Test connection",
218
+ "3. execute_command('server-name', 'uptime') - Run command",
219
+ ],
220
+ }
221
+
222
+ if topic not in help_topics:
223
+ return {
224
+ "error": f"Unknown topic: {topic}",
225
+ "available_topics": list(help_topics.keys()),
226
+ }
227
+
228
+ return help_topics[topic]
229
+
230
+
231
+ def get_allowed_commands(host: str) -> dict[str, Any]:
232
+ """
233
+ Get the list of allowed and forbidden commands for a server.
234
+
235
+ Shows the security configuration including command patterns,
236
+ timeout settings, and path restrictions.
237
+
238
+ Args:
239
+ host: Name of the server to check.
240
+
241
+ Returns:
242
+ Dictionary with security configuration details.
243
+
244
+ Example:
245
+ >>> get_allowed_commands("production")
246
+ {"host": "production", "allowed": [".*"], "forbidden": ["rm -rf /"]}
247
+ """
248
+ try:
249
+ machine = get_machine(host)
250
+ except Exception:
251
+ available = list_machines()
252
+ return {
253
+ "success": False,
254
+ "error": f"Server '{host}' not found",
255
+ "available_servers": available,
256
+ }
257
+
258
+ security = machine.security
259
+
260
+ # Determine security level based on patterns
261
+ if security.allowed_commands == [r".*"] or ".*" in security.allowed_commands:
262
+ security_level = "full"
263
+ elif len(security.allowed_commands) > 10:
264
+ security_level = "moderate"
265
+ else:
266
+ security_level = "strict"
267
+
268
+ return {
269
+ "success": True,
270
+ "host": host,
271
+ "security_level": security_level,
272
+ "allowed_commands": security.allowed_commands,
273
+ "forbidden_commands": security.forbidden_commands,
274
+ "timeout_seconds": security.timeout_seconds,
275
+ "max_concurrent_commands": security.max_concurrent_commands,
276
+ "allowed_paths": security.allowed_paths or ["all paths allowed"],
277
+ "forbidden_paths": security.forbidden_paths or [],
278
+ "tip": "Use update_server(name, security_level='full') to allow all commands",
279
+ }
280
+
281
+
282
+ def get_server_info(host: str) -> dict[str, Any]:
283
+ """
284
+ Get detailed information about a server.
285
+
286
+ Returns configuration details, security settings, and connection info.
287
+
288
+ Args:
289
+ host: Name of the server.
290
+
291
+ Returns:
292
+ Dictionary with server details.
293
+
294
+ Example:
295
+ >>> get_server_info("production")
296
+ {"name": "production", "host": "1.2.3.4", "user": "deploy", ...}
297
+ """
298
+ try:
299
+ machine = get_machine(host)
300
+ except Exception:
301
+ available = list_machines()
302
+ return {
303
+ "success": False,
304
+ "error": f"Server '{host}' not found",
305
+ "available_servers": available,
306
+ }
307
+
308
+ return {
309
+ "success": True,
310
+ "name": machine.name,
311
+ "host": machine.host,
312
+ "port": machine.port,
313
+ "user": machine.user,
314
+ "description": machine.description,
315
+ "tags": getattr(machine, "tags", []),
316
+ "auth_type": machine.auth.type,
317
+ "security_level": "full"
318
+ if ".*" in machine.security.allowed_commands
319
+ else "restricted",
320
+ "timeout_seconds": machine.security.timeout_seconds,
321
+ }