# SSH MCP Server for AI Assistants - Secure Remote Server Management A secure, production-ready **Model Context Protocol (MCP)** server that empowers AI assistants to manage remote infrastructure via SSH. [![npm version](https://badge.fury.io/js/@uarlouski%2Fssh-mcp-server.svg)](https://www.npmjs.com/package/@uarlouski/ssh-mcp-server) [![npm downloads](https://img.shields.io/npm/dm/@uarlouski/ssh-mcp-server.svg)](https://www.npmjs.com/package/@uarlouski/ssh-mcp-server) [![CI](https://github.com/uarlouski/ssh-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/uarlouski/ssh-mcp-server/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Node.js Version](https://img.shields.io/node/v/@uarlouski/ssh-mcp-server.svg)](https://nodejs.org) [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue.svg)](https://www.typescriptlang.org/) [![GitHub stars](https://img.shields.io/github/stars/uarlouski/ssh-mcp-server.svg?style=social&label=Star)](https://github.com/uarlouski/ssh-mcp-server) The **SSH MCP Server** bridges the gap between Large Language Models (LLMs) and your infrastructure. It enables AI assistants like **Claude Desktop**, **Cursor**, and **GitHub Copilot** to securely execute commands, transfer files via SFTP, and manage port forwarding tunnels. Turn your AI into a capable **DevOps assistant** that can troubleshoot, deploy, and monitor your servers with strict security boundaries. ## Table of Contents - [Why use SSH MCP Server?](#why-use-ssh-mcp-server) - [Compatibility](#compatibility) - [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) - [Configuration](#configuration) - [Available Tools](#available-tools) - [Security](#security) - [Examples](#examples) - [License](#license) ## Why use SSH MCP Server? - **DevOps Automation**: Let your AI assistant check Docker container status, read Kubernetes pod logs, or restart systemd services. - **Infrastructure Debugging**: Quickly analyze log files, check disk usage, and investigate network issues using natural language. - **Secure File Management**: Safely read configuration files, update scripts, or retrieve logs via SFTP without leaving your chat interface. - **Complex Workflows**: Chain multiple commands and tools to perform complex maintenance tasks with a single prompt. ## Compatibility This MCP server is designed to work with any client supporting the [Model Context Protocol](https://modelcontextprotocol.io/), including: - **Anthropic Claude Desktop** - **Cursor Editor** - **GitHub Copilot** ## Features - 🔐 **Secure SSH Command Execution** - Run shell commands on remote servers with strict validation and timeouts. - 🛡️ **Granular Access Control** - Whitelist specific commands and arguments to prevent unauthorized actions (e.g., allow `docker ps` but block `docker stop`). - 📁 **SFTP Integration** - Full support for reading, writing, and listing files on remote filesystems. - 🌉 **Smart Port Forwarding** - Automatically manage SSH tunnels to access internal services like databases or dashboards. - 🔄 **Robust Connection Pooling** - Efficiently manages multiple persistent SSH connections for high performance. - 🔑 **Key-Based Authentication** - Supports standard SSH keys (Ed25519, RSA) for secure, password-less authentication. - ⚙️ **SSH Config Import** - Seamlessly imports hosts from your existing `~/.ssh/config` file. - 🎯 **Parameterized Templates** - Define safe, reusable command templates (macros) for common tasks. - 📝 **Audit Logging** - Complete JSONL audit trails of all executed commands for compliance and security reviews. ## Installation ### As an MCP Server The recommended way to use this package is as an MCP server with AI assistants like GitHub Copilot. **Prerequisites:** - Node.js 18.0.0 or higher - SSH access to target servers - SSH private keys configured **Using npx (recommended):** No installation required! Add to your MCP client configuration: ```json { "mcpServers": { "ssh": { "command": "npx", "args": [ "@uarlouski/ssh-mcp-server@latest", "--configPath=/path/to/your/ssh-mcp-config.json" ] } } } ``` **Global installation:** ```bash npm install -g @uarlouski/ssh-mcp-server ``` Then configure with: ```json { "mcpServers": { "ssh": { "command": "ssh-mcp-server", "args": ["--configPath=/path/to/your/ssh-mcp-config.json"] } } } ``` ## Quick Start ### 1. Create SSH Keys If you don't already have SSH keys for your servers: ```bash ssh-keygen -t ed25519 -f ~/.ssh/deploy_key -C "deploy@example.com" ssh-copy-id -i ~/.ssh/deploy_key.pub user@your-server.com ``` ### 2. Create Configuration File Create a `ssh-mcp-config.json` file: ```json { "allowedCommands": ["ls", "cat", "grep", "docker", "kubectl"], "servers": { "my-server": { "host": "example.com", "username": "deploy", "privateKeyPath": "~/.ssh/deploy_key" } } } ``` ### 3. Configure Your MCP Client Add the server to your MCP client (e.g., GitHub Copilot): ```json { "mcpServers": { "ssh": { "command": "npx", "args": [ "@uarlouski/ssh-mcp-server@latest", "--configPath=/Users/yourname/ssh-mcp-config.json" ] } } } ``` ### 4. Restart Your MCP Client Restart your AI assistant to load the new server configuration. ## Configuration ### Basic Configuration The configuration file supports the following options: ```json { "allowedCommands": ["ls", "pwd", "cat", "grep", "docker", "kubectl"], "servers": { "server-name": { "host": "hostname-or-ip", "port": 22, "username": "username", "privateKeyPath": "~/.ssh/private_key" } }, "portForwardingServices": { "service-name": { "connectionName": "server-name", "localPort": 8080, "remoteHost": "localhost", "remotePort": 80, "description": "Optional description" } }, "commandTemplates": { "k8s-pod-logs": { "command": "kubectl logs -n {{namespace}} {{pod}} --tail={{lines:100}}", "description": "Fetch Kubernetes pod logs with configurable tail size" }, "app-deploy": { "command": "cd /var/www/{{app}} && git pull origin {{branch:main}} && npm install && pm2 restart {{app}}", "description": "Deploy application with git pull, npm install, and pm2 restart" }, "docker-stats": "docker stats {{container:--all}} --no-stream --format 'table {{.Name}}\\t{{.CPUPerc}}'" }, "commandTimeout": 30000, "maxConnections": 10 } ``` ### Configuration Options #### `allowedCommands` (optional) Array of base command names that are permitted for execution. - **If specified (non-empty)**: Enables strict validation - Only listed commands can be executed - Validates complex commands including pipes (`|`), chains (`&&`, `||`, `;`), and substitutions (`$()`) - Blocks bypass attempts like `ls | rm -rf /` - **If omitted or empty**: Disables validation (all commands allowed - use with caution!) **Example:** ```json "allowedCommands": ["ls", "cat", "grep", "docker", "kubectl", "systemctl"] ``` #### `servers` (required) Named SSH server configurations. Each server must have: - `host` (required): Hostname or IP address - `username` (required): SSH username - `privateKeyPath` (required): Path to SSH private key (supports `~` expansion) - `port` (optional): SSH port (default: 22) **Example:** ```json "servers": { "staging-api": { "host": "api-staging-01.example.com", "username": "deploy", "privateKeyPath": "~/.ssh/staging_deploy_key" }, "staging-db": { "host": "db-staging-master.example.com", "port": 2222, "username": "dbadmin", "privateKeyPath": "~/.ssh/db_admin_key" } } ``` #### `sshConfigImport` (optional) Import server configurations from your existing SSH config file (e.g. `~/.ssh/config`). This feature allows you to reuse your existing SSH configurations without duplicating them in the MCP config file. **Benefits:** - 🔄 Reuse existing SSH configurations - 🎯 Import specific hosts using pattern matching - 🔒 Works cross-platform (macOS, Linux, Windows) **Configuration:** - `path` (string, optional): Path to SSH config file (default: `~/.ssh/config`) - `hosts` (array of strings, optional): Host patterns to import (e.g., `["prod-*", "staging-*"]`) - Supports wildcards: `*` (matches any characters), `?` (matches single character) - If omitted, imports all valid hosts (excluding wildcard-only hosts like `*`) **Note:** Simply defining `sshConfigImport` in your config enables SSH config import. To disable it, remove the `sshConfigImport` field entirely. **Example - Import specific hosts with pattern matching:** ```json { "sshConfigImport": { "path": "/custom/path/to/ssh_config", "hosts": ["prod-*", "staging-*"] } } ``` **Notes:** - Only hosts with all required fields (`HostName`, `User`, `IdentityFile`) are imported - Wildcard SSH config entries (`Host *`) apply their settings to specific hosts but aren't imported as standalone servers - If the SSH config file doesn't exist, the server continues with manually defined servers - **Important**: If a server name exists in both manual configuration and SSH config import, the server will fail to start with a clear error message. Use the `hosts` pattern filter to avoid conflicts, or rename servers in one of the configurations. #### `portForwardingServices` (optional) Pre-configured named port forwarding services for common use cases. - `connectionName` (required): Name of the server from `servers` config - `remoteHost` (required): Remote host to forward to - `remotePort` (required): Remote port to forward to - `localPort` (optional): Local port to bind to (random if omitted) - `description` (optional): Human-readable description **Example:** ```json "portForwardingServices": { "pg-staging-database": { "connectionName": "staging-db", "remoteHost": "db-internal-01.example.com", "remotePort": 5432, "description": "PostgreSQL database access" } } ``` #### `commandTemplates` (optional) Reusable parameterized command templates with variable substitution. Templates can be defined in two formats: - **String format**: `"template-name": "command with {{variables}}"` - **Object format**: `"template-name": { "command": "...", "description": "..." }` **Variable syntax:** - `{{variable}}` - Required variable - `{{variable:default}}` - Optional variable with default value - `{{.field}}` - Preserved for Docker/Go templates (not substituted) **Example:** ```json "commandTemplates": { "k8s-pod-logs": { "command": "kubectl logs -n {{namespace}} {{pod}} --tail={{lines:100}}", "description": "Fetch Kubernetes pod logs with configurable tail size" }, "app-deploy": { "command": "cd /var/www/{{app}} && git pull origin {{branch:main}} && npm install && pm2 restart {{app}}", "description": "Deploy application" }, "docker-stats": "docker stats {{container:--all}} --no-stream --format 'table {{.Name}}\\t{{.CPUPerc}}'", "nginx-reload": "sudo nginx -t && sudo systemctl reload nginx" } ``` **Usage with AI:** ``` "Get logs from the api-7d8f9 pod in the staging namespace" ``` The AI will recognize this matches the `k8s-pod-logs` template and execute it with the appropriate variables. #### `commandTimeout` (optional) Command execution timeout in milliseconds. Default: `30000` (30 seconds). #### `maxConnections` (optional) Maximum number of concurrent SSH connections. Default: `5`. #### `auditLog` (optional) Configure audit logging for SSH sessions. - `enabled` (boolean, optional): Enable audit logging (default: false) - `folder` (string, optional): Path to store audit logs (defaults to current directory) **Example:** ```json "auditLog": { "enabled": true, "folder": "~/ssh-audit-logs" } ``` ### Complete Example See [config.example.json](config.example.json) for a complete configuration example. ## Available Tools ### `ssh_execute_command` Execute commands on remote servers. **Parameters:** - `connectionName` (string, required): Name of the server from your config - `command` (string, required): Command to execute - `commandTimeout` (number, optional): Command execution timeout in milliseconds (overrides global `commandTimeout`) **Example:** ```json { "connectionName": "staging-api", "command": "docker ps -a" } ``` **Response:** ```json { "stdout": "CONTAINER ID IMAGE COMMAND ...", "stderr": "", "exitCode": 0, "timedOut": false } ``` ### `ssh_list_servers` List all available SSH servers configured in your config file. **Parameters:** None **Example:** ```json {} ``` **Response:** ```json { "success": true, "servers": [ { "name": "production-api", "host": "api-prod-01.example.com", "port": 22, "username": "deploy" }, { "name": "staging-db", "host": "db-staging.example.com", "port": 2222, "username": "admin" } ], "count": 2 } ``` This tool helps AI assistants discover what servers are available for SSH operations without needing to see the full configuration file. ### `ssh_port_forward` Set up SSH port forwarding to access remote services. **Parameters:** - `connectionName` (string, required): Name of the server from your config - `remoteHost` (string, required): Remote host to forward to - `remotePort` (number, required): Remote port to forward to - `localPort` (number, optional): Local port to bind to (random if omitted) **Example with specific local port:** ```json { "connectionName": "staging-db", "localPort": 8080, "remoteHost": "internal-db.cluster.local", "remotePort": 5432 } ``` **Example with automatic port assignment:** ```json { "connectionName": "staging-db", "remoteHost": "internal-db.cluster.local", "remotePort": 5432 } ``` **Response:** ```json { "id": "a1b2c3d4", "localPort": 8080, "remoteHost": "internal-db.cluster.local", "remotePort": 5432, "status": "active" } ``` ### `ssh_close_port_forward` Close an active port forward. **Parameters:** - `id` (string, required): Unique ID of the active port forward to close **Example:** ```json { "id": "a1b2c3d4" } ``` ### `ssh_restart_port_forward` Restart an active port forward using its unique ID. **Parameters:** - `id` (string, required): Unique ID of the active port forward to restart **Example:** ```json { "id": "a1b2c3d4" } ``` ### `ssh_list_port_forwards` List all active port forwards across all connections. **Parameters:** None **Response:** ```json { "forwards": [ { "id": "a1b2c3d4", "sshConnection": "admin@db-staging.example.com:2222", "tunnel": "localhost:8080 -> internal-db.cluster.local:5432", "status": "active" } ] } ``` ### `ssh_port_forward_service` Start a pre-configured named port forwarding service from your config. **Parameters:** - `serviceName` (string, required): Name of the service from `portForwardingServices` config **Example:** ```json { "serviceName": "pg-staging-database" } ``` This is equivalent to calling `ssh_port_forward` with the pre-configured parameters (the returned result will include the generated unique `id`). ### `ssh_upload_file` Upload a file from local system to remote server via SFTP. **Parameters:** - `connectionName` (string, required): Name of the server from your config - `localPath` (string, required): Local file path to upload - `remotePath` (string, required): Remote destination path - `permissions` (string, optional): File permissions in octal format (e.g., "0644", "0755") **Example:** ```json { "connectionName": "app-server", "localPath": "~/configs/app.json", "remotePath": "/var/www/app/config.json", "permissions": "0644" } ``` **Response:** ```json { "success": true, "bytesTransferred": 1024, "message": "Successfully uploaded ~/configs/app.json to /var/www/app/config.json", "localPath": "~/configs/app.json", "remotePath": "/var/www/app/config.json" } ``` ### `ssh_download_file` Download a file from remote server to local system via SFTP. **Parameters:** - `connectionName` (string, required): Name of the server from your config - `remotePath` (string, required): Remote file path to download - `localPath` (string, required): Local destination path **Example:** ```json { "connectionName": "app-server", "remotePath": "/var/log/app/error.log", "localPath": "~/downloads/error.log" } ``` **Response:** ```json { "success": true, "bytesTransferred": 2048, "message": "Successfully downloaded /var/log/app/error.log to ~/downloads/error.log", "remotePath": "/var/log/app/error.log", "localPath": "~/downloads/error.log" } ``` ### `ssh_list_remote_files` List files in a remote directory via SFTP. **Parameters:** - `connectionName` (string, required): Name of the server from your config - `remotePath` (string, required): Remote directory path to list - `pattern` (string, optional): Glob pattern to filter files (e.g., "*.log", "*.json") **Example:** ```json { "connectionName": "app-server", "remotePath": "/var/log/app", "pattern": ".*\\.log$" } ``` **Response:** ```json { "remotePath": "/var/log/app", "pattern": ".*\\.log$", "totalCount": 3, "files": [ { "name": "error.log", "size": 10485760, "modified": "2024-12-04T12:00:00.000Z", "permissions": "100644", "isDirectory": false, "isFile": true } ] } ``` ### `ssh_delete_remote_file` Delete a file on the remote server via SFTP. **Parameters:** - `connectionName` (string, required): Name of the server from your config - `remotePath` (string, required): Remote file path to delete **Example:** ```json { "connectionName": "app-server", "remotePath": "/tmp/old-backup.tar.gz" } ``` **Response:** ```json { "success": true, "message": "Successfully deleted /tmp/old-backup.tar.gz", "remotePath": "/tmp/old-backup.tar.gz" } ``` ### `ssh_execute_template` Execute a pre-configured command template with variable substitution. **Parameters:** - `connectionName` (string, required): Name of the server from your config - `templateName` (string, required): Name of the command template - `variables` (object, optional): Key-value pairs for variable substitution - `commandTimeout` (number, optional): Command execution timeout in milliseconds (overrides global `commandTimeout`) **Example:** ```json { "connectionName": "kubernetes-bastion", "templateName": "k8s-pod-logs", "variables": { "namespace": "staging", "pod": "api-7d8f9", "lines": "50" } } ``` **Response:** ```json { "success": true, "templateName": "k8s-pod-logs", "expandedCommand": "kubectl logs -n production api-7d8f9 --tail=50", "variables": { "namespace": "staging", "pod": "api-7d8f9", "lines": "50" }, "result": { "stdout": "...", "stderr": "", "exitCode": 0, "timedOut": false } } ``` ### `ssh_list_templates` List all available command templates with their descriptions and variables. **Parameters:** None **Example:** ```json {} ``` **Response:** ```json { "success": true, "templates": [ { "name": "k8s-pod-logs", "command": "kubectl logs -n {{namespace}} {{pod}} --tail={{lines:100}}", "description": "Fetch Kubernetes pod logs with configurable tail size", "variables": [ { "name": "namespace", "required": true }, { "name": "pod", "required": true }, { "name": "lines", "required": false, "defaultValue": "100" } ] } ], "count": 1 } ``` ## Security ### Built-in Security Features 1. **Command Validation** - Automatically enabled when `allowedCommands` is specified - Validates all commands including pipes (`|`), chains (`&&`, `||`, `;`), and command substitutions (`$()`, backticks) - Blocks bypass attempts like `ls | rm -rf /` or `cat $(whoami)` - Uses robust parsing to prevent command injection 2. **Server Allowlist** - Only pre-configured servers in `ssh-mcp-config.json` can be accessed - No dynamic server connections allowed - Prevents unauthorized access to infrastructure 3. **SSH Key Authentication Only** - Only SSH key-based authentication is supported - No password authentication - Follows security best practices 4. **Connection Pooling** - Limits concurrent connections via `maxConnections` - Prevents resource exhaustion - Automatic cleanup of idle connections 5. **Template Validation** - Command templates are validated at config load time - Expanded template commands are subject to `allowedCommands` validation - Template syntax prevents conflicts with shell variable substitution - Docker/Go template patterns (`{{.Field}}`) are preserved and not substituted ### Security Considerations ⚠️ **Warning:** This server provides powerful capabilities. Consider the following: - AI assistants will have the ability to execute commands and create tunnels on configured servers - Ensure you understand the capabilities you're granting - Start with restrictive `allowedCommands` and expand as needed - Use separate SSH keys for MCP access - Regularly audit command execution logs ## Examples ### Example 1: Kubernetes Management **Config:** ```json { "allowedCommands": ["kubectl", "helm", "docker"], "servers": { "k8s-bastion": { "host": "k8s-bastion.example.com", "username": "k8s-operator", "privateKeyPath": "~/.ssh/kubernetes_operator_key" } } } ``` **Usage:** Ask your AI assistant: "Check the status of pods in the staging namespace" The assistant will execute: ```json { "connectionName": "k8s-bastion", "command": "kubectl get pods -n staging" } ``` ### Example 2: Database Access via Port Forwarding **Config:** ```json { "servers": { "db-bastion": { "host": "bastion.example.com", "username": "dbadmin", "privateKeyPath": "~/.ssh/db_key" } }, "portForwardingServices": { "staging-db": { "connectionName": "db-bastion", "localPort": 5432, "remoteHost": "db-internal.example.com", "remotePort": 5432, "description": "Staging PostgreSQL database" } } } ``` **Usage:** Ask your AI assistant: "Start the staging database tunnel" The assistant will execute: ```json { "serviceName": "staging-db" } ``` Then you can connect locally: `psql -h localhost -p 5432 -U dbuser` ### Example 3: Docker Container Management **Config:** ```json { "allowedCommands": ["docker", "docker-compose"], "servers": { "app-server": { "host": "app-01.example.com", "username": "deploy", "privateKeyPath": "~/.ssh/deploy_key" } } } ``` **Usage:** Ask your AI assistant: "Restart the nginx container on app-server" The assistant will execute: ```json { "connectionName": "app-server", "command": "docker restart nginx" } ``` ### Example 4: File Management and Log Analysis **Config:** ```json { "servers": { "app-server": { "host": "app-01.example.com", "username": "deploy", "privateKeyPath": "~/.ssh/deploy_key" } } } ``` **Usage:** **Upload a configuration file:** ``` Ask your AI assistant: "Upload my local config.json to /var/www/app/config.json on app-server with 644 permissions" ``` The assistant will execute: ```json { "connectionName": "app-server", "localPath": "~/config.json", "remotePath": "/var/www/app/config.json", "permissions": "0644" } ``` **Download logs for analysis:** ``` Ask your AI assistant: "Download the error log from /var/log/app/error.log on app-server" ``` The assistant will execute: ```json { "connectionName": "app-server", "remotePath": "/var/log/app/error.log", "localPath": "~/downloads/error.log" } ``` **List and filter log files:** ``` Ask your AI assistant: "Show me all .log files in /var/log/app on app-server" ``` The assistant will execute: ```json { "connectionName": "app-server", "remotePath": "/var/log/app", "pattern": ".*\\.log$" } ``` **Clean up old backups:** ``` Ask your AI assistant: "Delete the old backup at /tmp/backup-2024-01-01.tar.gz on app-server" ``` The assistant will execute: ```json { "connectionName": "app-server", "remotePath": "/tmp/backup-2024-01-01.tar.gz" } ``` ## License Apache 2.0 - see [LICENSE](LICENSE) file for details. --- **Repository:** [github.com/uarlouski/ssh-mcp-server](https://github.com/uarlouski/ssh-mcp-server) **Issues:** [github.com/uarlouski/ssh-mcp-server/issues](https://github.com/uarlouski/ssh-mcp-server/issues) **npm Package:** [@uarlouski/ssh-mcp-server](https://www.npmjs.com/package/@uarlouski/ssh-mcp-server)