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/server.py ADDED
@@ -0,0 +1,632 @@
1
+ """SSH MCP Server - Main entry point."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+ from typing import Any
7
+
8
+ import structlog
9
+ from mcp.server.fastmcp import FastMCP
10
+
11
+ from sshmcp.config import ConfigurationError, get_config, load_config
12
+ from sshmcp.prompts.backup import backup_database as _backup_database
13
+ from sshmcp.prompts.deploy import deploy_app as _deploy_app
14
+ from sshmcp.prompts.monitor import monitor_health as _monitor_health
15
+ from sshmcp.resources.logs import get_logs as _get_logs
16
+ from sshmcp.resources.metrics import get_metrics as _get_metrics
17
+ from sshmcp.resources.status import get_status as _get_status
18
+ from sshmcp.security.audit import init_audit_logger
19
+ from sshmcp.security.whitelist import init_whitelist
20
+ from sshmcp.ssh.pool import init_pool
21
+ from sshmcp.tools.commands import execute_command as _execute_command
22
+ from sshmcp.tools.commands import execute_on_multiple as _execute_on_multiple
23
+ from sshmcp.tools.files import list_files as _list_files
24
+ from sshmcp.tools.files import read_file as _read_file
25
+ from sshmcp.tools.files import upload_file as _upload_file
26
+ from sshmcp.tools.helpers import get_allowed_commands as _get_allowed_commands
27
+ from sshmcp.tools.helpers import get_help as _get_help
28
+ from sshmcp.tools.helpers import get_server_info as _get_server_info
29
+ from sshmcp.tools.processes import manage_process as _manage_process
30
+ from sshmcp.tools.servers import add_server as _add_server
31
+ from sshmcp.tools.servers import list_servers as _list_servers
32
+ from sshmcp.tools.servers import remove_server as _remove_server
33
+ from sshmcp.tools.servers import test_server_connection as _test_server_connection
34
+
35
+ # Configure structured logging to stderr (stdout is used for MCP JSON protocol)
36
+ structlog.configure(
37
+ processors=[
38
+ structlog.processors.TimeStamper(fmt="iso"),
39
+ structlog.processors.add_log_level,
40
+ structlog.processors.StackInfoRenderer(),
41
+ structlog.processors.format_exc_info,
42
+ structlog.dev.ConsoleRenderer(),
43
+ ],
44
+ wrapper_class=structlog.make_filtering_bound_logger(0),
45
+ context_class=dict,
46
+ logger_factory=structlog.PrintLoggerFactory(file=sys.stderr),
47
+ cache_logger_on_first_use=True,
48
+ )
49
+
50
+ logger = structlog.get_logger()
51
+
52
+ # Create MCP server instance
53
+ mcp = FastMCP(
54
+ "SSH VPS Manager",
55
+ json_response=True,
56
+ )
57
+
58
+ # ============================================================================
59
+ # MCP Tools
60
+ # ============================================================================
61
+
62
+
63
+ @mcp.tool()
64
+ def execute_command(
65
+ host: str,
66
+ command: str,
67
+ timeout: int | None = None,
68
+ ) -> dict[str, Any]:
69
+ """
70
+ Execute a command on remote VPS server via SSH.
71
+
72
+ This tool allows AI agents to execute commands on configured VPS servers
73
+ with security validation and timeout protection.
74
+
75
+ Args:
76
+ host: Name of the host from machines.json configuration.
77
+ command: Shell command to execute (must match whitelist patterns).
78
+ timeout: Maximum execution time in seconds (default: from config).
79
+
80
+ Returns:
81
+ Dictionary with exit_code, stdout, stderr, duration_ms, success.
82
+ """
83
+ return _execute_command(host, command, timeout)
84
+
85
+
86
+ @mcp.tool()
87
+ def read_file(
88
+ host: str,
89
+ path: str,
90
+ encoding: str = "utf-8",
91
+ max_size: int = 1024 * 1024,
92
+ ) -> dict[str, Any]:
93
+ """
94
+ Read file content from remote VPS server.
95
+
96
+ Args:
97
+ host: Name of the host from machines.json configuration.
98
+ path: Path to the file on remote server.
99
+ encoding: File encoding (default: utf-8).
100
+ max_size: Maximum file size to read in bytes (default: 1MB).
101
+
102
+ Returns:
103
+ Dictionary with content, path, size, encoding, truncated.
104
+ """
105
+ return _read_file(host, path, encoding, max_size)
106
+
107
+
108
+ @mcp.tool()
109
+ def upload_file(
110
+ host: str,
111
+ path: str,
112
+ content: str,
113
+ mode: str | None = None,
114
+ ) -> dict[str, Any]:
115
+ """
116
+ Upload file to remote VPS server.
117
+
118
+ Args:
119
+ host: Name of the host from machines.json configuration.
120
+ path: Destination path on remote server.
121
+ content: File content to write.
122
+ mode: Optional file permissions (e.g., "0644").
123
+
124
+ Returns:
125
+ Dictionary with success, path, size.
126
+ """
127
+ return _upload_file(host, path, content, mode)
128
+
129
+
130
+ @mcp.tool()
131
+ def list_files(
132
+ host: str,
133
+ directory: str,
134
+ recursive: bool = False,
135
+ ) -> dict[str, Any]:
136
+ """
137
+ List files in directory on remote VPS server.
138
+
139
+ Args:
140
+ host: Name of the host from machines.json configuration.
141
+ directory: Path to directory on remote server.
142
+ recursive: Whether to list files recursively (default: false).
143
+
144
+ Returns:
145
+ Dictionary with files list, directory, total_count.
146
+ """
147
+ return _list_files(host, directory, recursive)
148
+
149
+
150
+ @mcp.tool()
151
+ def manage_process(
152
+ host: str,
153
+ action: str,
154
+ process_name: str,
155
+ service_manager: str = "auto",
156
+ ) -> dict[str, Any]:
157
+ """
158
+ Manage processes on remote VPS server.
159
+
160
+ Supports systemd, pm2, and supervisor service managers.
161
+
162
+ Args:
163
+ host: Name of the host from machines.json configuration.
164
+ action: Action to perform (start, stop, restart, status).
165
+ process_name: Name of the process or service.
166
+ service_manager: Service manager to use (systemd, pm2, supervisor, auto).
167
+
168
+ Returns:
169
+ Dictionary with success, action, process, status.
170
+ """
171
+ return _manage_process(host, action, process_name, service_manager) # type: ignore
172
+
173
+
174
+ @mcp.tool()
175
+ def get_available_machines() -> dict[str, Any]:
176
+ """
177
+ Get list of available configured machines.
178
+
179
+ Returns:
180
+ Dictionary with list of machine names and their descriptions.
181
+ """
182
+ try:
183
+ config = get_config()
184
+ machines = []
185
+ for machine in config.machines:
186
+ machines.append(
187
+ {
188
+ "name": machine.name,
189
+ "host": machine.host,
190
+ "description": machine.description,
191
+ }
192
+ )
193
+ return {
194
+ "machines": machines,
195
+ "count": len(machines),
196
+ }
197
+ except ConfigurationError as e:
198
+ return {
199
+ "error": str(e),
200
+ "machines": [],
201
+ "count": 0,
202
+ }
203
+
204
+
205
+ # ============================================================================
206
+ # Server Management Tools
207
+ # ============================================================================
208
+
209
+
210
+ @mcp.tool()
211
+ def list_servers(tag: str | None = None) -> dict[str, Any]:
212
+ """
213
+ List all configured VPS servers.
214
+
215
+ Returns a list of all servers with their connection details.
216
+ Use this to see what servers are available.
217
+
218
+ Args:
219
+ tag: Optional tag to filter servers (e.g., "production", "web").
220
+
221
+ Returns:
222
+ Dictionary with servers list, count, and available tags.
223
+ """
224
+ return _list_servers(tag)
225
+
226
+
227
+ @mcp.tool()
228
+ def add_server(
229
+ name: str,
230
+ host: str,
231
+ user: str,
232
+ port: int = 22,
233
+ auth_type: str = "key",
234
+ key_path: str = "~/.ssh/id_rsa",
235
+ password: str | None = None,
236
+ description: str | None = None,
237
+ security_level: str = "full",
238
+ tags: list[str] | None = None,
239
+ ) -> dict[str, Any]:
240
+ """
241
+ Add a new VPS server to the configuration.
242
+
243
+ After adding, the server can be used with execute_command and other tools.
244
+
245
+ Args:
246
+ name: Unique name for the server (e.g., "production", "staging").
247
+ host: Server hostname or IP address.
248
+ user: SSH username.
249
+ port: SSH port (default: 22).
250
+ auth_type: Authentication type - "key" or "password".
251
+ key_path: Path to SSH private key (for key auth).
252
+ password: SSH password (for password auth).
253
+ description: Optional description of the server.
254
+ security_level: Security profile - "strict", "moderate", or "full" (default: full).
255
+ tags: Tags for grouping servers (e.g., ["production", "web"]).
256
+
257
+ Returns:
258
+ Dictionary with success status and server details.
259
+ """
260
+ return _add_server(
261
+ name=name,
262
+ host=host,
263
+ user=user,
264
+ port=port,
265
+ auth_type=auth_type,
266
+ key_path=key_path,
267
+ password=password,
268
+ description=description,
269
+ security_level=security_level,
270
+ tags=tags,
271
+ )
272
+
273
+
274
+ @mcp.tool()
275
+ def remove_server(name: str) -> dict[str, Any]:
276
+ """
277
+ Remove a VPS server from the configuration.
278
+
279
+ Args:
280
+ name: Name of the server to remove.
281
+
282
+ Returns:
283
+ Dictionary with success status.
284
+ """
285
+ return _remove_server(name)
286
+
287
+
288
+ @mcp.tool()
289
+ def test_connection(name: str) -> dict[str, Any]:
290
+ """
291
+ Test SSH connection to a server.
292
+
293
+ Attempts to connect and verify the server is accessible.
294
+
295
+ Args:
296
+ name: Name of the server to test.
297
+
298
+ Returns:
299
+ Dictionary with connection status and server info.
300
+ """
301
+ return _test_server_connection(name)
302
+
303
+
304
+ @mcp.tool()
305
+ def execute_on_multiple(
306
+ hosts: list[str],
307
+ command: str,
308
+ timeout: int | None = None,
309
+ stop_on_error: bool = False,
310
+ ) -> dict[str, Any]:
311
+ """
312
+ Execute a command on multiple VPS servers.
313
+
314
+ Run the same command on multiple servers simultaneously.
315
+ Useful for fleet management, deployments, and status checks.
316
+
317
+ Args:
318
+ hosts: List of server names. Use ["*"] for all servers,
319
+ or ["tag:production"] to filter by tag.
320
+ command: Shell command to execute.
321
+ timeout: Timeout per server in seconds.
322
+ stop_on_error: Stop on first error if True.
323
+
324
+ Returns:
325
+ Dictionary with results from each server.
326
+
327
+ Examples:
328
+ execute_on_multiple(["web1", "web2"], "uptime")
329
+ execute_on_multiple(["*"], "docker ps")
330
+ execute_on_multiple(["tag:production"], "systemctl status nginx")
331
+ """
332
+ return _execute_on_multiple(hosts, command, timeout, stop_on_error)
333
+
334
+
335
+ @mcp.tool()
336
+ def get_help(topic: str | None = None) -> dict[str, Any]:
337
+ """
338
+ Get help information about SSH MCP tools.
339
+
340
+ Provides documentation, examples, and usage information.
341
+
342
+ Args:
343
+ topic: Help topic - "tools", "security", "servers", "examples", or None for overview.
344
+
345
+ Returns:
346
+ Dictionary with help information.
347
+ """
348
+ return _get_help(topic)
349
+
350
+
351
+ @mcp.tool()
352
+ def get_allowed_commands(host: str) -> dict[str, Any]:
353
+ """
354
+ Get the list of allowed commands for a server.
355
+
356
+ Shows security configuration including allowed/forbidden patterns.
357
+
358
+ Args:
359
+ host: Name of the server.
360
+
361
+ Returns:
362
+ Dictionary with allowed commands, forbidden patterns, and timeouts.
363
+ """
364
+ return _get_allowed_commands(host)
365
+
366
+
367
+ @mcp.tool()
368
+ def get_server_info(host: str) -> dict[str, Any]:
369
+ """
370
+ Get detailed information about a server.
371
+
372
+ Shows configuration, security settings, and connection details.
373
+
374
+ Args:
375
+ host: Name of the server.
376
+
377
+ Returns:
378
+ Dictionary with server details.
379
+ """
380
+ return _get_server_info(host)
381
+
382
+
383
+ # ============================================================================
384
+ # MCP Resources
385
+ # ============================================================================
386
+
387
+
388
+ @mcp.resource("vps://{host}/logs/{log_path}")
389
+ def resource_logs(host: str, log_path: str) -> str:
390
+ """
391
+ Get logs from VPS server.
392
+
393
+ URI pattern: vps://{host}/logs/{log_path}
394
+ Example: vps://production-server/logs/var/log/app.log
395
+
396
+ Args:
397
+ host: Name of the host.
398
+ log_path: Path to log file (without leading slash).
399
+
400
+ Returns:
401
+ Log content as string.
402
+ """
403
+ return _get_logs(host, log_path)
404
+
405
+
406
+ @mcp.resource("vps://{host}/metrics")
407
+ def resource_metrics(host: str) -> str:
408
+ """
409
+ Get system metrics from VPS server.
410
+
411
+ URI pattern: vps://{host}/metrics
412
+ Example: vps://production-server/metrics
413
+
414
+ Args:
415
+ host: Name of the host.
416
+
417
+ Returns:
418
+ JSON string with CPU, memory, disk, uptime metrics.
419
+ """
420
+ import json
421
+
422
+ metrics = _get_metrics(host)
423
+ return json.dumps(metrics, indent=2)
424
+
425
+
426
+ @mcp.resource("vps://{host}/status")
427
+ def resource_status(host: str) -> str:
428
+ """
429
+ Get status of VPS server.
430
+
431
+ URI pattern: vps://{host}/status
432
+ Example: vps://production-server/status
433
+
434
+ Args:
435
+ host: Name of the host.
436
+
437
+ Returns:
438
+ JSON string with server status information.
439
+ """
440
+ import json
441
+
442
+ status = _get_status(host)
443
+ return json.dumps(status, indent=2)
444
+
445
+
446
+ # ============================================================================
447
+ # MCP Prompts
448
+ # ============================================================================
449
+
450
+
451
+ @mcp.prompt()
452
+ def deploy_app(
453
+ host: str,
454
+ branch: str = "main",
455
+ app_path: str = "/var/www/app",
456
+ package_manager: str = "npm",
457
+ process_manager: str = "pm2",
458
+ app_name: str = "app",
459
+ ) -> str:
460
+ """
461
+ Generate deployment prompt for application.
462
+
463
+ Creates a step-by-step deployment plan.
464
+
465
+ Args:
466
+ host: Target host name.
467
+ branch: Git branch to deploy (default: main).
468
+ app_path: Application directory path.
469
+ package_manager: Package manager (npm, yarn, pip).
470
+ process_manager: Process manager (pm2, systemd, supervisor).
471
+ app_name: Application name for process manager.
472
+
473
+ Returns:
474
+ Deployment instructions.
475
+ """
476
+ return _deploy_app(
477
+ host, branch, app_path, package_manager, process_manager, app_name
478
+ )
479
+
480
+
481
+ @mcp.prompt()
482
+ def backup_database(
483
+ host: str,
484
+ database_name: str,
485
+ database_type: str = "postgresql",
486
+ backup_path: str = "/var/backups",
487
+ compress: bool = True,
488
+ ) -> str:
489
+ """
490
+ Generate database backup prompt.
491
+
492
+ Creates a step-by-step backup plan.
493
+
494
+ Args:
495
+ host: Target host name.
496
+ database_name: Name of the database to backup.
497
+ database_type: Database type (postgresql, mysql, mongodb).
498
+ backup_path: Directory for backup files.
499
+ compress: Whether to compress the backup.
500
+
501
+ Returns:
502
+ Backup instructions.
503
+ """
504
+ return _backup_database(host, database_name, database_type, backup_path, compress)
505
+
506
+
507
+ @mcp.prompt()
508
+ def monitor_health(
509
+ host: str,
510
+ check_logs: bool = True,
511
+ check_services: bool = True,
512
+ ) -> str:
513
+ """
514
+ Generate health monitoring prompt.
515
+
516
+ Creates a comprehensive health check plan.
517
+
518
+ Args:
519
+ host: Target host name.
520
+ check_logs: Whether to check log files for errors.
521
+ check_services: Whether to check service statuses.
522
+
523
+ Returns:
524
+ Monitoring instructions.
525
+ """
526
+ return _monitor_health(host, check_logs, check_services)
527
+
528
+
529
+ # ============================================================================
530
+ # Server Initialization
531
+ # ============================================================================
532
+
533
+
534
+ def initialize_server(config_path: str | None = None) -> None:
535
+ """
536
+ Initialize the MCP server with configuration.
537
+
538
+ Args:
539
+ config_path: Optional path to configuration file.
540
+ """
541
+ try:
542
+ # Load configuration
543
+ config = load_config(config_path)
544
+
545
+ # Initialize connection pool
546
+ init_pool(config)
547
+
548
+ # Initialize whitelist
549
+ init_whitelist(config)
550
+
551
+ # Initialize audit logger
552
+ audit_log_path = os.environ.get("SSHMCP_AUDIT_LOG")
553
+ init_audit_logger(log_file=audit_log_path)
554
+
555
+ logger.info(
556
+ "server_initialized",
557
+ machines=config.get_machine_names(),
558
+ )
559
+
560
+ except ConfigurationError as e:
561
+ logger.error("server_init_failed", error=str(e))
562
+ raise
563
+
564
+
565
+ def main() -> None:
566
+ """Main entry point for the MCP server."""
567
+ parser = argparse.ArgumentParser(
568
+ description="SSH MCP Server - Manage VPS servers via MCP protocol"
569
+ )
570
+ parser.add_argument(
571
+ "--config",
572
+ "-c",
573
+ help="Path to machines.json configuration file",
574
+ default=None,
575
+ )
576
+ parser.add_argument(
577
+ "--transport",
578
+ "-t",
579
+ choices=["stdio", "streamable-http"],
580
+ default="stdio",
581
+ help="Transport type (default: stdio)",
582
+ )
583
+ parser.add_argument(
584
+ "--host",
585
+ default="0.0.0.0",
586
+ help="Host for HTTP transport (default: 0.0.0.0)",
587
+ )
588
+ parser.add_argument(
589
+ "--port",
590
+ "-p",
591
+ type=int,
592
+ default=8000,
593
+ help="Port for HTTP transport (default: 8000)",
594
+ )
595
+
596
+ args = parser.parse_args()
597
+
598
+ # Set config path from argument or environment
599
+ if args.config:
600
+ os.environ["SSHMCP_CONFIG_PATH"] = args.config
601
+
602
+ try:
603
+ # Initialize server
604
+ initialize_server(args.config)
605
+
606
+ # Run MCP server
607
+ logger.info(
608
+ "server_starting",
609
+ transport=args.transport,
610
+ )
611
+
612
+ if args.transport == "stdio":
613
+ mcp.run(transport="stdio")
614
+ else:
615
+ # Note: streamable-http uses default host/port
616
+ # Custom host/port require uvicorn configuration
617
+ mcp.run(transport="streamable-http")
618
+
619
+ except ConfigurationError as e:
620
+ logger.error("configuration_error", error=str(e))
621
+ print(f"Configuration error: {e}", file=sys.stderr)
622
+ sys.exit(1)
623
+ except KeyboardInterrupt:
624
+ logger.info("server_stopped", reason="keyboard_interrupt")
625
+ except Exception as e:
626
+ logger.error("server_error", error=str(e))
627
+ print(f"Server error: {e}", file=sys.stderr)
628
+ sys.exit(1)
629
+
630
+
631
+ if __name__ == "__main__":
632
+ main()
sshmcp/ssh/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """SSH client module for remote server connections."""
2
+
3
+ from sshmcp.ssh.client import SSHClient
4
+ from sshmcp.ssh.pool import SSHConnectionPool
5
+
6
+ __all__ = ["SSHClient", "SSHConnectionPool"]