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
@@ -0,0 +1,484 @@
1
+ """MCP Tools for server management."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import structlog
9
+
10
+ from sshmcp.config import reload_config as reload_global_config
11
+ from sshmcp.models.machine import (
12
+ AuthConfig,
13
+ MachineConfig,
14
+ MachinesConfig,
15
+ SecurityConfig,
16
+ )
17
+ from sshmcp.security.whitelist import init_whitelist
18
+ from sshmcp.ssh.pool import get_pool
19
+
20
+ logger = structlog.get_logger()
21
+
22
+ DEFAULT_CONFIG_PATH = Path.home() / ".sshmcp" / "machines.json"
23
+
24
+
25
+ def _get_config_path() -> Path:
26
+ """Get configuration file path."""
27
+ env_path = os.environ.get("SSHMCP_CONFIG_PATH")
28
+ if env_path:
29
+ return Path(env_path).expanduser()
30
+ return DEFAULT_CONFIG_PATH
31
+
32
+
33
+ def _load_config() -> MachinesConfig:
34
+ """Load machines configuration."""
35
+ config_path = _get_config_path()
36
+ if not config_path.exists():
37
+ return MachinesConfig(machines=[])
38
+
39
+ with open(config_path, "r") as f:
40
+ data = json.load(f)
41
+ return MachinesConfig.model_validate(data)
42
+
43
+
44
+ def _save_config(config: MachinesConfig) -> None:
45
+ """Save machines configuration."""
46
+ config_path = _get_config_path()
47
+ config_path.parent.mkdir(parents=True, exist_ok=True)
48
+
49
+ with open(config_path, "w") as f:
50
+ json.dump(config.model_dump(), f, indent=2)
51
+
52
+
53
+ def list_servers(tag: str | None = None) -> dict[str, Any]:
54
+ """
55
+ List all configured VPS servers.
56
+
57
+ Returns a list of all servers with their basic information.
58
+ Use this to see what servers are available for commands.
59
+
60
+ Args:
61
+ tag: Optional tag to filter servers. Only servers with this tag will be returned.
62
+
63
+ Returns:
64
+ Dictionary with:
65
+ - servers: List of server info (name, host, user, description, tags)
66
+ - count: Total number of servers (after filtering)
67
+ - all_tags: List of all available tags
68
+ - config_path: Path to configuration file
69
+
70
+ Example:
71
+ >>> list_servers()
72
+ {"servers": [{"name": "prod", "host": "1.2.3.4", ...}], "count": 1}
73
+
74
+ >>> list_servers(tag="production")
75
+ {"servers": [...], "count": 2, "filter": "production"}
76
+ """
77
+ config = _load_config()
78
+
79
+ # Collect all tags
80
+ all_tags = set()
81
+ for machine in config.machines:
82
+ if machine.tags:
83
+ all_tags.update(machine.tags)
84
+
85
+ servers = []
86
+ for machine in config.machines:
87
+ # Filter by tag if specified
88
+ if tag and tag not in (machine.tags or []):
89
+ continue
90
+
91
+ servers.append(
92
+ {
93
+ "name": machine.name,
94
+ "host": machine.host,
95
+ "port": machine.port,
96
+ "user": machine.user,
97
+ "description": machine.description,
98
+ "auth_type": machine.auth.type,
99
+ "tags": machine.tags or [],
100
+ "security_level": "full"
101
+ if ".*" in machine.security.allowed_commands
102
+ else "restricted",
103
+ }
104
+ )
105
+
106
+ result = {
107
+ "servers": servers,
108
+ "count": len(servers),
109
+ "all_tags": sorted(all_tags),
110
+ "config_path": str(_get_config_path()),
111
+ }
112
+
113
+ if tag:
114
+ result["filter"] = f"tag:{tag}"
115
+
116
+ return result
117
+
118
+
119
+ def add_server(
120
+ name: str,
121
+ host: str,
122
+ user: str,
123
+ port: int = 22,
124
+ auth_type: str = "key",
125
+ key_path: str = "~/.ssh/id_rsa",
126
+ password: str | None = None,
127
+ description: str | None = None,
128
+ security_level: str = "full",
129
+ tags: list[str] | None = None,
130
+ ) -> dict[str, Any]:
131
+ """
132
+ Add a new VPS server to the configuration.
133
+
134
+ Adds a new server that can be used with execute_command and other tools.
135
+
136
+ Args:
137
+ name: Unique name for the server (e.g., "production", "staging").
138
+ host: Server hostname or IP address.
139
+ user: SSH username.
140
+ port: SSH port (default: 22).
141
+ auth_type: Authentication type - "key" or "password".
142
+ key_path: Path to SSH private key (for key auth).
143
+ password: SSH password (for password auth).
144
+ description: Optional description of the server.
145
+ security_level: Security profile - "strict", "moderate", or "full" (default: full).
146
+ tags: List of tags for grouping servers (e.g., ["production", "web"]).
147
+
148
+ Returns:
149
+ Dictionary with success status and server details.
150
+
151
+ Example:
152
+ >>> add_server("prod", "192.168.1.100", "deploy", tags=["production"])
153
+ {"success": true, "name": "prod", "message": "Server added"}
154
+ """
155
+ config = _load_config()
156
+
157
+ # Check if already exists
158
+ if config.has_machine(name):
159
+ return {
160
+ "success": False,
161
+ "error": f"Server '{name}' already exists",
162
+ "hint": "Use a different name or remove the existing server first",
163
+ }
164
+
165
+ # Validate auth
166
+ if auth_type == "key":
167
+ auth = AuthConfig(type="key", key_path=key_path)
168
+ elif auth_type == "password":
169
+ if not password:
170
+ return {
171
+ "success": False,
172
+ "error": "Password required for password authentication",
173
+ }
174
+ auth = AuthConfig(type="password", password=password)
175
+ else:
176
+ return {
177
+ "success": False,
178
+ "error": f"Invalid auth_type: {auth_type}. Use 'key' or 'password'",
179
+ }
180
+
181
+ # Security profiles
182
+ security_profiles = {
183
+ "strict": SecurityConfig(
184
+ allowed_commands=[
185
+ r"^git (pull|status|log|diff).*",
186
+ r"^ls .*",
187
+ r"^cat .*",
188
+ r"^tail .*",
189
+ r"^head .*",
190
+ r"^pwd$",
191
+ r"^whoami$",
192
+ r"^df -h$",
193
+ r"^free -m$",
194
+ r"^uptime$",
195
+ ],
196
+ forbidden_commands=[
197
+ r".*rm\s+-rf.*",
198
+ r".*sudo.*",
199
+ r".*su\s+-.*",
200
+ ],
201
+ timeout_seconds=30,
202
+ ),
203
+ "moderate": SecurityConfig(
204
+ allowed_commands=[
205
+ r"^git .*",
206
+ r"^npm .*",
207
+ r"^yarn .*",
208
+ r"^pip .*",
209
+ r"^pm2 .*",
210
+ r"^systemctl .*",
211
+ r"^docker .*",
212
+ r"^ls .*",
213
+ r"^cat .*",
214
+ r"^tail .*",
215
+ r"^head .*",
216
+ r"^pwd$",
217
+ r"^whoami$",
218
+ r"^df -h$",
219
+ r"^free -m$",
220
+ r"^uptime$",
221
+ r"^ps aux$",
222
+ r"^top -bn1$",
223
+ ],
224
+ forbidden_commands=[
225
+ r".*rm\s+-rf\s+/.*",
226
+ r".*sudo\s+rm.*",
227
+ ],
228
+ timeout_seconds=60,
229
+ ),
230
+ "full": SecurityConfig(
231
+ allowed_commands=[r".*"],
232
+ forbidden_commands=[r".*rm\s+-rf\s+/$"],
233
+ timeout_seconds=120,
234
+ ),
235
+ }
236
+
237
+ if security_level not in security_profiles:
238
+ return {
239
+ "success": False,
240
+ "error": f"Invalid security_level: {security_level}",
241
+ "valid_options": list(security_profiles.keys()),
242
+ }
243
+
244
+ security = security_profiles[security_level]
245
+
246
+ try:
247
+ machine = MachineConfig(
248
+ name=name,
249
+ host=host,
250
+ port=port,
251
+ user=user,
252
+ auth=auth,
253
+ security=security,
254
+ description=description,
255
+ tags=tags or [],
256
+ )
257
+
258
+ config.machines.append(machine)
259
+ _save_config(config)
260
+
261
+ # Reload global config cache so get_machine() finds the new server
262
+ reload_global_config()
263
+
264
+ # Register in connection pool immediately
265
+ pool = get_pool()
266
+ pool.register_machine(machine)
267
+ init_whitelist(MachinesConfig(machines=[machine]))
268
+
269
+ logger.info("server_added", name=name, host=host)
270
+
271
+ return {
272
+ "success": True,
273
+ "name": name,
274
+ "host": host,
275
+ "user": user,
276
+ "port": port,
277
+ "auth_type": auth_type,
278
+ "security_level": security_level,
279
+ "tags": tags or [],
280
+ "message": f"Server '{name}' added successfully",
281
+ }
282
+
283
+ except Exception as e:
284
+ return {
285
+ "success": False,
286
+ "error": str(e),
287
+ }
288
+
289
+
290
+ def remove_server(name: str) -> dict[str, Any]:
291
+ """
292
+ Remove a VPS server from the configuration.
293
+
294
+ Args:
295
+ name: Name of the server to remove.
296
+
297
+ Returns:
298
+ Dictionary with success status.
299
+
300
+ Example:
301
+ >>> remove_server("old-server")
302
+ {"success": true, "message": "Server removed"}
303
+ """
304
+ config = _load_config()
305
+
306
+ if not config.has_machine(name):
307
+ return {
308
+ "success": False,
309
+ "error": f"Server '{name}' not found",
310
+ "available_servers": config.get_machine_names(),
311
+ }
312
+
313
+ config.machines = [m for m in config.machines if m.name != name]
314
+ _save_config(config)
315
+ reload_global_config()
316
+
317
+ logger.info("server_removed", name=name)
318
+
319
+ return {
320
+ "success": True,
321
+ "name": name,
322
+ "message": f"Server '{name}' removed successfully",
323
+ "remaining_servers": config.get_machine_names(),
324
+ }
325
+
326
+
327
+ def test_server_connection(name: str) -> dict[str, Any]:
328
+ """
329
+ Test SSH connection to a server.
330
+
331
+ Attempts to connect and run a simple command to verify the server is accessible.
332
+
333
+ Args:
334
+ name: Name of the server to test.
335
+
336
+ Returns:
337
+ Dictionary with connection status and details.
338
+
339
+ Example:
340
+ >>> test_server_connection("prod")
341
+ {"success": true, "hostname": "server1", "message": "Connection OK"}
342
+ """
343
+ config = _load_config()
344
+
345
+ machine = config.get_machine(name)
346
+ if not machine:
347
+ return {
348
+ "success": False,
349
+ "error": f"Server '{name}' not found",
350
+ "available_servers": config.get_machine_names(),
351
+ }
352
+
353
+ try:
354
+ from sshmcp.ssh.client import SSHClient
355
+
356
+ client = SSHClient(machine)
357
+ client.connect()
358
+
359
+ # Run test commands
360
+ result = client.execute("hostname && uptime")
361
+ client.disconnect()
362
+
363
+ if result.exit_code == 0:
364
+ lines = result.stdout.strip().split("\n")
365
+ hostname = lines[0] if lines else "unknown"
366
+
367
+ return {
368
+ "success": True,
369
+ "name": name,
370
+ "hostname": hostname,
371
+ "host": machine.host,
372
+ "output": result.stdout,
373
+ "message": "Connection successful",
374
+ }
375
+ else:
376
+ return {
377
+ "success": False,
378
+ "name": name,
379
+ "error": "Command failed",
380
+ "stderr": result.stderr,
381
+ }
382
+
383
+ except Exception as e:
384
+ return {
385
+ "success": False,
386
+ "name": name,
387
+ "error": str(e),
388
+ "message": "Connection failed",
389
+ }
390
+
391
+
392
+ def update_server(
393
+ name: str,
394
+ host: str | None = None,
395
+ user: str | None = None,
396
+ port: int | None = None,
397
+ description: str | None = None,
398
+ security_level: str | None = None,
399
+ ) -> dict[str, Any]:
400
+ """
401
+ Update an existing server's configuration.
402
+
403
+ Only specified fields will be updated.
404
+
405
+ Args:
406
+ name: Name of the server to update.
407
+ host: New hostname or IP (optional).
408
+ user: New SSH username (optional).
409
+ port: New SSH port (optional).
410
+ description: New description (optional).
411
+ security_level: New security level (optional).
412
+
413
+ Returns:
414
+ Dictionary with update status.
415
+
416
+ Example:
417
+ >>> update_server("prod", host="10.0.0.1")
418
+ {"success": true, "message": "Server updated"}
419
+ """
420
+ config = _load_config()
421
+
422
+ machine = config.get_machine(name)
423
+ if not machine:
424
+ return {
425
+ "success": False,
426
+ "error": f"Server '{name}' not found",
427
+ }
428
+
429
+ # Find and update
430
+ for i, m in enumerate(config.machines):
431
+ if m.name == name:
432
+ if host:
433
+ config.machines[i].host = host
434
+ if user:
435
+ config.machines[i].user = user
436
+ if port:
437
+ config.machines[i].port = port
438
+ if description is not None:
439
+ config.machines[i].description = description
440
+
441
+ if security_level:
442
+ security_profiles = {
443
+ "strict": SecurityConfig(
444
+ allowed_commands=[r"^git.*", r"^ls.*", r"^cat.*"],
445
+ forbidden_commands=[r".*rm\s+-rf.*", r".*sudo.*"],
446
+ timeout_seconds=30,
447
+ ),
448
+ "moderate": SecurityConfig(
449
+ allowed_commands=[
450
+ r"^git.*",
451
+ r"^npm.*",
452
+ r"^pm2.*",
453
+ r"^docker.*",
454
+ ],
455
+ forbidden_commands=[r".*rm\s+-rf\s+/.*"],
456
+ timeout_seconds=60,
457
+ ),
458
+ "full": SecurityConfig(
459
+ allowed_commands=[r".*"],
460
+ forbidden_commands=[r".*rm\s+-rf\s+/$"],
461
+ timeout_seconds=120,
462
+ ),
463
+ }
464
+ if security_level in security_profiles:
465
+ config.machines[i].security = security_profiles[security_level]
466
+
467
+ break
468
+
469
+ _save_config(config)
470
+ reload_global_config()
471
+
472
+ # Re-register in pool with updated config
473
+ pool = get_pool()
474
+ for m in config.machines:
475
+ if m.name == name:
476
+ pool.register_machine(m)
477
+ init_whitelist(MachinesConfig(machines=[m]))
478
+ break
479
+
480
+ return {
481
+ "success": True,
482
+ "name": name,
483
+ "message": f"Server '{name}' updated",
484
+ }