SSH MCP Server
by uarlouski
README.md
# 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.
[](https://www.npmjs.com/package/@uarlouski/ssh-mcp-server)
[](https://www.npmjs.com/package/@uarlouski/ssh-mcp-server)
[](https://github.com/uarlouski/ssh-mcp-server/actions/workflows/ci.yml)
[](https://opensource.org/licenses/Apache-2.0)
[](https://nodejs.org)
[](https://www.typescriptlang.org/)
[](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)
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues