A Model Context Protocol (MCP) server that provides secure SSH capabilities for AI assistants, enabling remote command execution, SFTP file transfers, and port forwarding with comprehensive security controls.
- 🔐 Secure SSH Command Execution - Execute commands on remote servers with granular security controls
- 🛡️ Host Allowlisting - Only connect to pre-configured, trusted servers
- 📁 SFTP File Operations - Upload, download, list, and delete files on remote servers
- 🌉 SSH Port Forwarding - Create secure tunnels to access remote services
- 🔄 Connection Pooling - Persistent connections with automatic management
- 🔑 SSH Key Authentication - Secure authentication using SSH private keys
- ⚙️ SSH Config Integration - Import servers from your existing SSJ config file
- ✅ Command Allowlisting - Restrict which commands can be executed
- 📦 Named Services - Pre-configured port forwarding services for common use cases
- 🎯 Command Templates - Reusable parameterized commands with variable substitution
- 📝 Audit Logging - Comprehensive JSONL-based audit logging for compliance and tracking
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:
{
"mcpServers": {
"ssh": {
"command": "npx",
"args": [
"@uarlouski/ssh-mcp-server@latest",
"--configPath=/path/to/your/ssh-mcp-config.json"
]
}
}
}Global installation:
npm install -g @uarlouski/ssh-mcp-serverThen configure with:
{
"mcpServers": {
"ssh": {
"command": "ssh-mcp-server",
"args": ["--configPath=/path/to/your/ssh-mcp-config.json"]
}
}
}If you don't already have SSH keys for your servers:
ssh-keygen -t ed25519 -f ~/.ssh/deploy_key -C "deploy@example.com"
ssh-copy-id -i ~/.ssh/deploy_key.pub user@your-server.comCreate a ssh-mcp-config.json file:
{
"allowedCommands": ["ls", "cat", "grep", "docker", "kubectl"],
"servers": {
"my-server": {
"host": "example.com",
"username": "deploy",
"privateKeyPath": "~/.ssh/deploy_key"
}
}
}Add the server to your MCP client (e.g., GitHub Copilot):
{
"mcpServers": {
"ssh": {
"command": "npx",
"args": [
"@uarlouski/ssh-mcp-server@latest",
"--configPath=/Users/yourname/ssh-mcp-config.json"
]
}
}
}Restart your AI assistant to load the new server configuration.
The configuration file supports the following options:
{
"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
}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:
"allowedCommands": ["ls", "cat", "grep", "docker", "kubectl", "systemctl"]Named SSH server configurations. Each server must have:
host(required): Hostname or IP addressusername(required): SSH usernameprivateKeyPath(required): Path to SSH private key (supports~expansion)port(optional): SSH port (default: 22)
Example:
"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"
}
}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
*)
- Supports wildcards:
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:
{
"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
hostspattern filter to avoid conflicts, or rename servers in one of the configurations.
Pre-configured named port forwarding services for common use cases.
connectionName(required): Name of the server fromserversconfigremoteHost(required): Remote host to forward toremotePort(required): Remote port to forward tolocalPort(optional): Local port to bind to (random if omitted)description(optional): Human-readable description
Example:
"portForwardingServices": {
"pg-staging-database": {
"connectionName": "staging-db",
"remoteHost": "db-internal-01.example.com",
"remotePort": 5432,
"description": "PostgreSQL database access"
}
}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:
"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.
Command execution timeout in milliseconds. Default: 30000 (30 seconds).
Maximum number of concurrent SSH connections. Default: 5.
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:
"auditLog": {
"enabled": true,
"folder": "~/ssh-audit-logs"
}See config.example.json for a complete configuration example.
Execute commands on remote servers.
Parameters:
connectionName(string, required): Name of the server from your configcommand(string, required): Command to executecommandTimeout(number, optional): Command execution timeout in milliseconds (overrides globalcommandTimeout)
Example:
{
"connectionName": "staging-api",
"command": "docker ps -a"
}Response:
{
"stdout": "CONTAINER ID IMAGE COMMAND ...",
"stderr": "",
"exitCode": 0,
"timedOut": false
}List all available SSH servers configured in your config file.
Parameters: None
Example:
{}Response:
{
"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.
Set up SSH port forwarding to access remote services.
Parameters:
connectionName(string, required): Name of the server from your configremoteHost(string, required): Remote host to forward toremotePort(number, required): Remote port to forward tolocalPort(number, optional): Local port to bind to (random if omitted)
Example with specific local port:
{
"connectionName": "staging-db",
"localPort": 8080,
"remoteHost": "internal-db.cluster.local",
"remotePort": 5432
}Example with automatic port assignment:
{
"connectionName": "staging-db",
"remoteHost": "internal-db.cluster.local",
"remotePort": 5432
}Response:
{
"localPort": 8080,
"remoteHost": "internal-db.cluster.local",
"remotePort": 5432,
"status": "active"
}Close an active port forward.
Parameters:
connectionName(string, required): Name of the serverlocalPort(number, required): Local port to close
Example:
{
"connectionName": "staging-db",
"localPort": 8080
}List all active port forwards across all connections.
Parameters: None
Response:
{
"forwards": [
{
"connectionName": "staging-db",
"localPort": 8080,
"remoteHost": "internal-db.cluster.local",
"remotePort": 5432
}
]
}Start a pre-configured named port forwarding service from your config.
Parameters:
serviceName(string, required): Name of the service fromportForwardingServicesconfig
Example:
{
"serviceName": "pg-staging-database"
}This is equivalent to calling ssh_port_forward with the pre-configured parameters.
Upload a file from local system to remote server via SFTP.
Parameters:
connectionName(string, required): Name of the server from your configlocalPath(string, required): Local file path to uploadremotePath(string, required): Remote destination pathpermissions(string, optional): File permissions in octal format (e.g., "0644", "0755")
Example:
{
"connectionName": "app-server",
"localPath": "~/configs/app.json",
"remotePath": "/var/www/app/config.json",
"permissions": "0644"
}Response:
{
"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"
}Download a file from remote server to local system via SFTP.
Parameters:
connectionName(string, required): Name of the server from your configremotePath(string, required): Remote file path to downloadlocalPath(string, required): Local destination path
Example:
{
"connectionName": "app-server",
"remotePath": "/var/log/app/error.log",
"localPath": "~/downloads/error.log"
}Response:
{
"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"
}List files in a remote directory via SFTP.
Parameters:
connectionName(string, required): Name of the server from your configremotePath(string, required): Remote directory path to listpattern(string, optional): Glob pattern to filter files (e.g., ".log", ".json")
Example:
{
"connectionName": "app-server",
"remotePath": "/var/log/app",
"pattern": ".*\\.log$"
}Response:
{
"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
}
]
}Delete a file on the remote server via SFTP.
Parameters:
connectionName(string, required): Name of the server from your configremotePath(string, required): Remote file path to delete
Example:
{
"connectionName": "app-server",
"remotePath": "/tmp/old-backup.tar.gz"
}Response:
{
"success": true,
"message": "Successfully deleted /tmp/old-backup.tar.gz",
"remotePath": "/tmp/old-backup.tar.gz"
}Execute a pre-configured command template with variable substitution.
Parameters:
connectionName(string, required): Name of the server from your configtemplateName(string, required): Name of the command templatevariables(object, optional): Key-value pairs for variable substitutioncommandTimeout(number, optional): Command execution timeout in milliseconds (overrides globalcommandTimeout)
Example:
{
"connectionName": "kubernetes-bastion",
"templateName": "k8s-pod-logs",
"variables": {
"namespace": "staging",
"pod": "api-7d8f9",
"lines": "50"
}
}Response:
{
"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
}
}List all available command templates with their descriptions and variables.
Parameters: None
Example:
{}Response:
{
"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
}-
Command Validation
- Automatically enabled when
allowedCommandsis specified - Validates all commands including pipes (
|), chains (&&,||,;), and command substitutions ($(), backticks) - Blocks bypass attempts like
ls | rm -rf /orcat $(whoami) - Uses robust parsing to prevent command injection
- Automatically enabled when
-
Server Allowlist
- Only pre-configured servers in
ssh-mcp-config.jsoncan be accessed - No dynamic server connections allowed
- Prevents unauthorized access to infrastructure
- Only pre-configured servers in
-
SSH Key Authentication Only
- Only SSH key-based authentication is supported
- No password authentication
- Follows security best practices
-
Connection Pooling
- Limits concurrent connections via
maxConnections - Prevents resource exhaustion
- Automatic cleanup of idle connections
- Limits concurrent connections via
-
Template Validation
- Command templates are validated at config load time
- Expanded template commands are subject to
allowedCommandsvalidation - Template syntax prevents conflicts with shell variable substitution
- Docker/Go template patterns (
{{.Field}}) are preserved and not substituted
- 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
allowedCommandsand expand as needed - Use separate SSH keys for MCP access
- Regularly audit command execution logs
Config:
{
"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:
{
"connectionName": "k8s-bastion",
"command": "kubectl get pods -n staging"
}Config:
{
"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:
{
"serviceName": "staging-db"
}Then you can connect locally: psql -h localhost -p 5432 -U dbuser
Config:
{
"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:
{
"connectionName": "app-server",
"command": "docker restart nginx"
}Config:
{
"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:
{
"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:
{
"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:
{
"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:
{
"connectionName": "app-server",
"remotePath": "/tmp/backup-2024-01-01.tar.gz"
}Apache 2.0 - see LICENSE file for details.
Repository: github.com/uarlouski/ssh-mcp-server
Issues: github.com/uarlouski/ssh-mcp-server/issues
npm Package: @uarlouski/ssh-mcp-server