MCP gateway for controlled SSH access with per-client auth, command policies, and audit logging.
A centralized MCP gateway that gives AI agents controlled SSH access over Streamable HTTP.
Most MCP SSH servers run as local stdio processes — one per client, with no shared state, no centralized authorization, and no audit trail. When multiple AI agents need SSH access, each manages its own SSH keys and runs its own process. This creates:
ssh-mcp solves this by deploying a single HTTP gateway. All clients connect to it; it connects to your SSH targets. Authorization, rate limiting, connection pooling, and audit logging happen in one place.
docker compose pull
docker compose up -d
The image is published at ghcr.io/gelse/ssh-mcp:latest.
The compose file maps host port 9080 to container port 8080.
Verify the server is running:
curl http://localhost:9080/health
# → {"status": "ok"}
Create a minimal config in config/ssh-mcp-config.json:
{
"version": 1,
"ssh_targets": {
"my-server": {
"host": "10.0.1.10",
"username": "deploy"
}
},
"allowed_commands": {
"default": [
{
"targets": ["*"],
"commands": ["hostname", "uptime", "free", "df"]
}
]
}
}
Generate an API key hash and add it to your config or
secrets.json (see Configuration).
make build
docker compose up -d --build
Six MCP tools are available over Streamable HTTP:
| Tool | Description |
|---|---|
ssh_list_servers | List configured SSH targets |
ssh_list_allowed_commands | Show allowed commands for a target |
ssh_execute_command | Execute a command on a remote server |
ssh_check_connection | Test SSH connectivity to a target |
ssh_download_file | Download a file via SFTP |
ssh_upload_file | Upload a file via SFTP |
All tools follow the pattern ssh_<verb>_<noun>:
ssh_list_servers — list resourcesssh_list_allowed_commands — list permissionsssh_execute_command — perform an actionssh_check_connection — verify connectivityssh_download_file / ssh_upload_file — file transferSee examples/README.md for usage examples
including curl commands and Python client code.
ssh-mcp is configured via JSON files with hot-reload (15 s poll, 2 s debounce). Key areas:
| Area | Details |
|---|---|
| SSH targets | Host, port, username, key, password |
| Command policies | Block patterns, per-key/network allowlists |
| Connection pool | Max connections, idle timeout, concurrency |
| Rate limiting | Per-IP sliding window (default 60 req/min) |
| Logging | JSONL with rotation, gzip, multiple targets |
| SFTP | Sandbox root, path length limits |
Full reference: docs/CONFIGURATION.md
| File | Purpose |
|---|---|
ssh-mcp-config.json | Main config |
config.schema.json | JSON Schema for validation |
secrets.json | Passwords and API key hashes |
MCP_SSH_* env vars | Overrides for any setting |
ssh-mcp adds a layered authorization chain (9 ordered layers) between every client request and every SSH command. Per-API-key and per-network rules let different agents get different permissions on different servers — without touching the underlying SSH accounts.
Additional protections:
Architecture: ARCHITECTURE.md
Security model: docs/SECURITY.md
Fixable with contribution:
Architectural:
GET /health — returns {"status": "ok"}GET /metrics — Prometheus exposition formatFull reference: docs/OBSERVABILITY.md
An optional web dashboard for managing configuration without
editing JSON files. Enable with CONFIG_API_ENABLED=true.
Full reference: docs/CONFIG-API.md
The session cookie defaults to Secure (HTTPS only). For local
HTTP testing, set:
environment:
- CONFIG_API_SESSION_COOKIE_SECURE=false
Then restart the container.
Connect to http://host:9080/mcp using the Streamable HTTP
transport. Pass your API key via X-API-Key or
Authorization: Bearer header.
docker compose exec mcp-ssh python -c \
"from lib.crypto import hash_api_key; print(hash_api_key('your-key'))"
# → pbkdf2:sha256:100000$<salt>$<hash>
Or use the hash utility in the Config API dashboard.
The config file is polled every 15 seconds with a 2-second debounce. Changes to targets, commands, and settings take effect without restart. Rate limiter and log target settings require a restart.
Commands are evaluated through a 9-layer authorization chain.
The matched_via field in logs shows which layer denied.
See Security Model for the full chain.
Per-IP sliding window, default 60 requests per 60 seconds.
Exceeding the limit returns HTTP 503. Configure via
settings.rate_limit in the config file.
Logs are written to the /logs volume (mapped from ./logs).
The active log file is ssh-mcp.log in JSONL format with
optional gzip rotation.
# Check server health
curl http://localhost:9080/health
# Validate config
make config-test
# Check logs
docker compose logs mcp-ssh
| Document | Description |
|---|---|
ARCHITECTURE.md | System design and data flow |
docs/SECURITY.md | Security model and threat analysis |
docs/CONFIGURATION.md | Full config reference |
docs/CONFIG-API.md | Config API & dashboard |
docs/OBSERVABILITY.md | Health, metrics, logging |
CONTRIBUTING.md | Development and contribution guide |
CHANGELOG.md | Release history |
examples/ | Config examples and client code |
# Unit tests
make test
# Integration tests (builds Docker image)
make integrationtest
See CONTRIBUTING.md for the full development
guide, coding conventions, and PR workflow.
No public roadmap. See Not yet / known gaps for current limitations and opportunities.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
docker run -i --rm ghcr.io/gelse/ssh-mcp:0.3.0Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-gelse-ssh-mcp": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/gelse/ssh-mcp:0.3.0"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceghcr.io/gelse/ssh-mcp:0.3.0dockerssh-mcp works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.