SSH MCP Server
<p align="center">
<img src="ssh-mcp-server-logo.png" alt="SSH MCP Server" width="200">
</p>
<p align="center">
<a href="https://www.python.org/downloads/">
<img src="https://img.shields.io/badge/python-3.11%2B-blue.svg" alt="Python 3.11+">
</a>
<a href="https://opensource.org/licenses/MIT">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT">
</a>
<a href="https://github.com/pycqa/pylint">
<img src="https://img.shields.io/badge/lint-pylint-green.svg" alt="Pylint">
</a>
<a href="https://github.com/pyupio/safety">
<img src="https://img.shields.io/badge/security-safety-important.svg" alt="Safety Security Scan">
</a>
<a href="https://github.com/pyupio/safety-action">
<img src="https://img.shields.io/badge/dependency%20security-passed-brightgreen.svg" alt="Dependency Security Check">
</a>
</p>
<h1 align="center">SSH MCP Server</h1>
<p align="center">
<strong>Hardened SSH operations for VS Code Copilot Chat via the Model Context Protocol</strong>
</p>
<p align="center">
<a href="#features">Features</a> •
<a href="#quick-start">Quick Start</a> •
<a href="#tools">Tools</a> •
<a href="#configuration">Configuration</a> •
<a href="#security">Security</a> •
<a href="#testing">Testing</a> •
<a href="#contributing">Contributing</a>
</p>
---
## What Is This?
SSH MCP Server lets you manage remote Linux servers through **natural language** in VS Code Copilot Chat. Instead of switching to a terminal and remembering SSH commands, you just ask:
> "Check disk usage on production-web"
> "Show me the last 200 lines of /var/log/nginx/error.log on staging"
> "Download /var/log/syslog from web-server-01 for incident INC-2026-0309"
The server enforces **strict security policies** — no raw shell access, all commands go through pre-approved templates, parameters are regex-validated, secrets in output are auto-redacted, and privileged operations require an approval workflow.
## Features
- **23 MCP tools** — host discovery, session management, command execution (sync + background), file transfer, SFTP operations, SSH key management, certificate lifecycle, approval workflows
- **Template-only execution** — no raw shell; every command matches a registered template with regex-validated parameters
- **3-tier security model** — read-only (Tier 0), controlled mutation with confirmation (Tier 1), privileged with approval workflow (Tier 2)
- **Automatic secret redaction** — AWS keys, bearer tokens, passwords, private keys are scrubbed from output
- **Tamper-evident audit log** — every operation is logged with hash-chain integrity verification
- **Short-lived SSH certificates** — issue and revoke certificates with TTL enforcement via a local CA
- **Persistent SSH sessions** — connection pooling with keepalive probes and configurable idle timeout
- **Background command execution** — run long-running template commands asynchronously with output polling
- **Path traversal protection** — `..` sequences blocked in all path parameters and file transfers
- **Transfer policy** — allowed paths, blocked extensions, size limits, mandatory justification for downloads
## Quick Start
### Prerequisites
- **Python 3.11+**
- **VS Code** with **GitHub Copilot** extension
- SSH access to at least one remote Linux host
---
### Install
#### As vscode plugin
#### Follow instructions on link:
https://marketplace.visualstudio.com/items?itemName=bhayanak.ssh-mcp-server-secure
---
Python package:
https://pypi.org/project/ssh-mcp-server-copilot/
---
### Install from Source (for development / contributing)
```bash
git clone https://github.com/bhayanak/ssh-mcp-server.git
cd ssh-mcp-server
python -m venv .venv
source .venv/bin/activate # macOS/Linux
# .venv\Scripts\activate # Windows
pip install -e ".[dev]"
```
For local development, create `.vscode/mcp.json` pointing to the local source:
```json
{
"servers": {
"ssh-mcp": {
"type": "stdio",
"command": "${workspaceFolder}/.venv/bin/python",
"args": ["-m", "ssh_mcp.server"],
"env": {
"SSH_MCP_CONFIG_DIR": "${workspaceFolder}/config",
"SSH_MCP_HOSTS_FILE": "${workspaceFolder}/config/hosts.json",
"SSH_MCP_TEMPLATES_FILE": "${workspaceFolder}/config/templates.json",
"SSH_MCP_AUDIT_LOG_DIR": "${workspaceFolder}/audit_logs",
"SSH_MCP_CERT_DATA_DIR": "${workspaceFolder}/cert_data",
"SSH_MCP_APPROVAL_DATA_DIR": "${workspaceFolder}/approval_data",
"SSH_MCP_SSH_KNOWN_HOSTS_FILE": "~/.ssh/known_hosts"
}
}
}
}
```
---
### Configure Your Hosts
Edit the hosts file with your actual servers. If you used `ssh-mcp-server-copilot init`, edit `~/.ssh-mcp/hosts.json`. If developing from source, edit `config/hosts.json`.
```json
[
{
"host_id": "my-server",
"hostname": "192.168.1.10",
"port": 22,
"ssh_user": "deploy",
"labels": {"env": "production", "role": "web"},
"description": "Production web server",
"allowed_roles": ["operator", "admin"]
}
]
```
| Field | Required | Description |
|-------|----------|-------------|
| `host_id` | Yes | Unique identifier (alphanumeric, dots, dashes) |
| `hostname` | Yes | IP address or FQDN |
| `port` | No | SSH port (default: 22) |
| `ssh_user` | Yes* | Remote SSH username. If empty, uses your OS username |
| `labels` | No | Key-value tags for organization |
| `description` | No | Human-readable description |
| `allowed_roles` | No | Which roles can access this host (default: operator, admin) |
### Set Up SSH Access
Your SSH key must be authorized on each host:
```bash
# Generate a key if you don't have one
ssh-keygen -t ed25519 -C "your-email@example.com"
# Copy to each host
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@192.168.1.10
# Load into ssh-agent (required — the MCP server uses the agent)
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
# Verify access
ssh deploy@192.168.1.10 "echo OK"
```
Add host keys to known_hosts:
```bash
ssh-keyscan -H 192.168.1.10 >> ~/.ssh/known_hosts
```
### Start Using
1. Open the workspace in VS Code
2. The MCP server auto-starts from `.vscode/mcp.json`
3. Open **Copilot Chat** (Cmd+Shift+I / Ctrl+Shift+I)
4. **Switch to "Agent" mode** (critical — only Agent mode can invoke MCP tools)
5. Verify tools are loaded — click the tools icon in the chat input bar, you should see **23 tools** from `ssh-mcp`
Now just ask in natural language:
```
> List all my SSH hosts
> Check disk usage on my-server
> Show me the last 100 lines of /var/log/syslog on my-server
> What's the status of nginx on my-server?
```
## Tools (23)
### Tier 0 — Read-Only (No Confirmation)
| Tool | Description |
|------|-------------|
| `list_hosts` | List all configured SSH hosts with labels and metadata |
| `get_host_facts` | Get OS, uptime, kernel info from a host |
| `get_audit_logs` | View the tamper-evident audit trail |
| `list_templates` | List available command templates |
| `list_pending_approvals` | View pending approval requests |
### Session Management
| Tool | Description |
|------|-------------|
| `ssh_connect` | Open a persistent SSH session (returns session_id for reuse) |
| `ssh_disconnect` | Close a persistent session |
| `ssh_list_sessions` | List active sessions and remaining connection slots |
| `ssh_session_ping` | Health-check a session (liveness, idle time, uptime) |
### Tier 1 — Controlled Mutation (Confirmation Required)
| Tool | Description |
|------|-------------|
| `run_ssh_command` | Execute a template command on a host (supports `session_id` for persistent connections) |
| `transfer_file` | Download/upload files via SFTP with path and extension policies (supports `session_id`) |
### Background Command Execution
| Tool | Description |
|------|-------------|
| `run_ssh_command_background` | Start a template command in the background (returns job_id) |
| `poll_background_job` | Read accumulated stdout/stderr of a background job (redacted) |
| `list_background_jobs` | List all background jobs (running + completed) |
| `cancel_background_job` | Cancel a running background job |
### Enhanced SFTP
| Tool | Description |
|------|-------------|
| `sftp_list_directory` | List files in a remote directory (within allowed paths) |
| `sftp_delete` | Delete a remote file (within allowed paths, requires justification) |
### Tier 2 — Privileged (Approval Required)
| Tool | Description |
|------|-------------|
| `add_ssh_key` | Register a public SSH key with TTL enforcement |
| `remove_ssh_key` | Revoke a registered SSH key |
| `issue_cert` | Issue a short-lived SSH certificate via the local CA |
| `revoke_cert` | Revoke a certificate and delete its PEM |
| `request_approval` / `approve_request` | Approval workflow for privileged ops |
## Configuration
### Command Templates
Templates define which commands can be executed. Edit `config/templates.json`:
```json
[
{
"template_id": "disk_usage",
"description": "Show disk usage summary",
"command": "df -h",
"allowed_params": {},
"allowed_roles": ["developer", "operator", "admin"],
"timeout_seconds": 10,
"risk_level": "low"
},
{
"template_id": "tail_log",
"description": "Tail the last N lines of a log file",
"command": "tail -n {lines} {log_path}",
"allowed_params": {
"lines": "^[0-9]{1,5}$",
"log_path": "^/var/log/[a-zA-Z0-9_./-]+$"
},
"allowed_roles": ["operator", "admin"],
"timeout_seconds": 15,
"risk_level": "low"
}
]
```
Each parameter is validated against a regex pattern before substitution. Path traversal (`..`) is blocked automatically.
### Environment Variables
All configuration is via environment variables with the `SSH_MCP_` prefix:
| Variable | Default | Description |
|----------|---------|-------------|
| `SSH_MCP_CONFIG_DIR` | `~/.ssh-mcp` | Base config directory (all other paths derive from this) |
| `SSH_MCP_HOSTS_FILE` | `{config_dir}/hosts.json` | Path to hosts configuration |
| `SSH_MCP_TEMPLATES_FILE` | `{config_dir}/templates.json` | Path to command templates |
| `SSH_MCP_AUDIT_LOG_DIR` | `{config_dir}/audit_logs` | Audit log directory |
| `SSH_MCP_CERT_DATA_DIR` | `{config_dir}/cert_data` | Certificate storage directory |
| `SSH_MCP_APPROVAL_DATA_DIR` | `{config_dir}/approval_data` | Approval data directory |
| `SSH_MCP_MAX_SESSIONS` | `10` | Max simultaneous SSH sessions |
| `SSH_MCP_SESSION_IDLE_TIMEOUT` | `300` | Idle session timeout (seconds) |
| `SSH_MCP_KEEPALIVE_INTERVAL` | `15` | SSH keepalive interval (seconds) |
| `SSH_MCP_KEEPALIVE_COUNT_MAX` | `3` | Max failed keepalive probes before disconnect |
| `SSH_MCP_MAX_BACKGROUND_JOBS` | `10` | Max concurrent background jobs |
| `SSH_MCP_JOB_OUTPUT_MAX_BYTES` | `1048576` | Max output buffer per background job (1 MB) |
| `SSH_MCP_JOB_TTL_SECONDS` | `3600` | Background job auto-expiry (1 hour) |
| `SSH_MCP_SSH_KNOWN_HOSTS_FILE` | *(none)* | Path to SSH known_hosts file |
| `SSH_MCP_REQUIRE_TWO_PARTY_APPROVAL` | `true` | Require different user as approver |
| `SSH_MCP_AUTH_TOKEN` | *(none)* | Bearer token (empty = dev mode) |
| `SSH_MCP_SSH_TIMEOUT_SECONDS` | `30` | SSH connection timeout |
### Transfer Policy (Defaults)
| Setting | Default |
|---------|---------|
| Allowed paths | `/tmp/*`, `/var/log/*` |
| Blocked extensions | `.exe`, `.sh`, `.bat`, `.ps1`, `.dll`, `.so` |
| Max file size | 50 MB |
| Require justification for downloads | Yes |
### Roles
| Role | Access |
|------|--------|
| `developer` | Read-only tools, low-risk commands |
| `operator` | All Tier 0 + Tier 1 tools |
| `admin` | All tools including Tier 2 (key/cert management) |
| `auditor` | Audit log access |
## Security
### Design Principles
1. **No raw shell** — all commands go through registered templates
2. **Parameter validation** — every parameter is regex-validated before substitution
3. **Path traversal blocking** — `..` sequences rejected in all parameters and file paths
4. **Secret redaction** — AWS keys, bearer tokens, passwords, private keys automatically scrubbed from output
5. **Approval workflow** — privileged operations (key/cert management) require explicit approval with HMAC-verified tokens
6. **Tamper-evident audit** — hash-chained audit log for forensic analysis
7. **Strict host validation** — paramiko `RejectPolicy` by default (no auto-accepting unknown hosts)
8. **TTL enforcement** — SSH certificates and keys have configurable maximum lifetimes
### Approval Workflow
Tier 2 operations follow a 3-step flow:
```
1. request_approval → returns request_id + one-time approval_token
2. approve_request → verifies HMAC token, marks approved
3. call the tool → pass approval_request_id, consumed after use
```
With `REQUIRE_TWO_PARTY_APPROVAL=true` (production), a different user must approve.
### Audit Log Verification
```bash
python -c "
from ssh_mcp.audit import AuditLogger
from pathlib import Path
logger = AuditLogger(Path('audit_logs'))
ok, msg = logger.verify_chain()
print(f'Chain integrity: {ok} — {msg}')
"
```
## Testing
### Unit Tests
```bash
# Run all tests
pytest tests/ -v
# With coverage
pytest tests/ -v --cov=ssh_mcp
```
### Runtime Directories (git-ignored, auto-created)
| Directory | Purpose |
|-----------|---------|
| `audit_logs/` | Tamper-evident audit log (`audit.jsonl`) |
| `cert_data/` | CA keys, issued certificates, revocation list |
| `approval_data/` | Pending and consumed approval requests |
## CLI Reference
```bash
ssh-mcp-server-copilot --version # Print version
ssh-mcp-server-copilot init # Create ~/.ssh-mcp with default configs
ssh-mcp-server-copilot # Start MCP server (stdio transport)
ssh-mcp-server-copilot --config-dir PATH # Use custom config directory
ssh-mcp-server-copilot --config-dir PATH init # Init a custom config directory
```
## Contributing
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/my-feature`
3. Make your changes
4. Run tests: `pytest tests/ -v`
5. Lint: `ruff check src/ tests/`
6. Submit a pull request
## License
[MIT](LICENSE)
TDQS
Scored across 23 tools
Tools are generally well-differentiated by resource and action (e.g., request_approval vs approve_request vs list_pending_approvals form a clear workflow). However, with 23 granular tools, some operational overlap exists between transfer_file and sftp_* operations, and the background job suite requires careful reading to distinguish from standard command execution.
While all tools use snake_case with clear verbs, the prefix conventions are inconsistent: SSH session tools use ssh_* (ssh_connect, ssh_list_sessions) but host operations use bare verbs (list_hosts, get_host_facts); SFTP operations use sftp_* while file transfer uses unprefixed transfer_file. This mixed convention forces agents to read carefully.
At 23 tools, this falls into the borderline-heavy range (16-25). While the server covers a complex domain (governed SSH access with approval workflows, background jobs, and certificate management), the surface feels granular—session management alone uses four distinct tools, suggesting potential for consolidation (e.g., combining list/poll/cancel into a job management tool).
Core workflows for approvals, background jobs, and sessions are complete, but notable gaps exist: there are tools to add/remove SSH keys and issue/revoke certificates, but no corresponding list_ssh_keys or list_certs to inspect current state. Similarly, SFTP supports delete and list but lacks explicit upload/download (relying on the separate transfer_file tool).