MCP SSH Gateway
by d00mus
README.md
<!-- mcp-name: io.github.d00mus/mcp-ssh-gateway -->
# MCP SSH Gateway
**Use your MCP client to work with several SSH hosts through one local connection.** Run diagnostics on a VPS, inspect a NAS or query a Keenetic router by name. Sessions preserve terminal state between calls; long output can be read a page at a time.
For people who already use SSH and want an assistant to help with routine diagnostics and administration. It is not an SSH daemon, a hosted proxy or a replacement for access controls on your servers.
[](https://github.com/d00mus/MCP-SSH/actions/workflows/ci.yml) [](https://www.python.org/downloads/) [](https://pypi.org/project/mcp-ssh-gateway/) [](https://registry.modelcontextprotocol.io/) [](LICENSE)
## Try it with one host
You need Python 3.11+, an SSH account on a host you control and an MCP client that can launch a local stdio server.
1. Create a working directory for the gateway configuration and install the published package into an isolated environment. The virtual environment keeps this install separate from other Python tools:
```bash
mkdir ssh-gateway && cd ssh-gateway
python -m venv .venv
# Windows PowerShell: .venv\Scripts\Activate.ps1
# macOS/Linux: source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install mcp-ssh-gateway
mcp-ssh-gateway --help
```
This installs the current release from PyPI and checks that the command is available. Create servers.json in this directory before starting the gateway; without configuration, startup exits with an error. For a disposable launch without a persistent install, use `uvx --from mcp-ssh-gateway mcp-ssh-gateway --help`; to use a config file, replace `--help` with `--servers-config /absolute/path/to/servers.json`.
To work on the project source instead, clone it and install development dependencies: `git clone https://github.com/d00mus/MCP-SSH.git && cd MCP-SSH && python -m pip install -r requirements.txt`
2. Create a `servers.json` in this working directory (replace the address, user and key path with your own):
```json
{
"servers": {
"lab": {
"host": "192.168.1.10",
"user": "your-ssh-user",
"key_path": "~/.ssh/id_ed25519"
}
}
}
```
Host-key verification is enabled by default and uses the machine’s system host-key store. Ensure the host key is already trusted there, and verify its fingerprint independently before adding it. For password authentication, use `"password": "${LAB_SSH_PASSWORD}"` and provide `LAB_SSH_PASSWORD` to the MCP server process. Do not commit real credentials or your `servers.json`. See the [security policy](SECURITY.md).
3. Add this to a client that uses the `mcpServers` config format. **Replace the absolute path**: clients do not necessarily start in your working directory.
```json
{
"mcpServers": {
"ssh-gateway": {
"command": "mcp-ssh-gateway",
"args": ["--servers-config", "/absolute/path/to/servers.json"]
}
}
}
```
On Windows, point `command` at `mcp-ssh-gateway.exe` in your Python `Scripts` directory if the client does not resolve it from `PATH`, and use escaped backslashes in JSON paths (for example `C:\\Users\\you\\servers.json`).
Running the command directly is not an interactive SSH terminal: it communicates with the client over stdio. Restart the MCP client after updating its config.
4. In the client, ask: **“List my SSH hosts, then run `uname -a` on lab.”** If the host is missing, check the config path and the client's MCP server logs. If SSH fails, check credentials and host-key verification.
Add more hosts under `servers` in the same file. [servers.json.example](servers.json.example) shows a multi-host configuration; check its host-key and credential choices before copying it. For a clean-directory installation check that does not depend on the repository clone, follow the [PyPI smoke-test steps](PYPI_SMOKE_TEST.md).
## What using it looks like
A Linux host and a router can share one MCP connection. Your client makes calls like these (they are not terminal commands):
```text
server_list() # find configured hosts
run(server="lab", command="df -h") # inspect disk space
run(server="keenetic", command="show interface", shell=false) # router CLI
```
`run` returns a `session_id`; pass it to later calls if you need the same terminal state. Without it an idle session may be reused with unknown state; `new_session: true` forces a clean session. A command still running after the initial wait (5 seconds by default) reports `still_running: true`. Use `read(session_id="...")` for later output, or whenever `has_more` indicates unread lines. `signal(action="ctrl_c")` interrupts a stuck command. Non-zero exits report `completed_nonzero` and `exit_status`, not silent success.
The `file` tool can inspect and edit remote files through SFTP (with shell fallback). Review edits and give an assistant only the SSH permissions it needs.
## When to use it
- **Multiple hosts:** one MCP server configuration routes calls to named targets. For just one host, this matters less.
- **Multi-step troubleshooting:** persistent sessions keep shell state, while line-based output windows avoid dumping an entire log into the conversation at once.
- **A Keenetic alongside Linux hosts:** `shell: false` sends device CLI commands without a POSIX shell; common pagers such as `--More--` are handled. Keenetic NDM is a supported use case, but other vendor CLIs are not guaranteed. Keep NDM CLI and Linux shell operations in separate sessions.
**Security boundary:** `read_only` and command blacklists are best-effort guardrails against mistakes, not a sandbox. Shell expansion and interpreters can bypass checks on command text. Use restricted SSH users and server-side permissions for sensitive hosts. Host-key verification is on by default; avoid turning it off casually.
The SSH connection originates from the machine running the gateway. This project works with MCP clients that can start a stdio server; it does not add SSH access to a chat app without MCP integration.
## Other ways to run it
**Docker (build from this clone):**
```bash
docker build -t mcp-ssh-server .
docker run -i --rm \
-v /absolute/path/to/servers.json:/app/servers.json:ro \
-v /absolute/path/to/your/.ssh:/root/.ssh:ro \
mcp-ssh-server --servers-config /app/servers.json
```
Use absolute mount paths and pass required environment variables with `-e NAME`. This example exposes SSH keys to the container; mount only what it needs. For an MCP client using Docker, set `command` to `docker` and put the same run arguments in `args`.
**PyPI / MCP Registry:** The package is published as [`mcp-ssh-gateway`](https://pypi.org/project/mcp-ssh-gateway/) and listed in the [MCP Registry](https://registry.modelcontextprotocol.io/) as `io.github.d00mus/mcp-ssh-gateway`, so `pip install mcp-ssh-gateway` and `uvx --from mcp-ssh-gateway ...` work. See the [release process](docs/PUBLISHING.md).
## Configuration and tools
- Each target has an alias, `host`, `user`, optional `port` (default 22) and a `key_path` or `password`. `verify_host` defaults to `true`. `password` and `key_passphrase` support environment references (`${NAME}`); missing references fail at startup.
- The default full profile exposes `server_list`, `server_add`, `run`, `read`, `signal`, `file`, `session_list`, `session_update`, `session_close` and `last_command_details`. `server_add` accepts an `alias` and only appends new targets. `--tool-profile lean` exposes six everyday tools for a smaller catalog.
- Changes to `servers.json` are checked periodically (every 30 seconds); `server_list(reload=true)` checks immediately. Unchanged hosts keep their sessions; removing a host or changing its address, login or host-key settings closes that host’s active sessions.
- `--log-output meta` (the default) records lifecycle information and command text. `full` also records raw output; `off` disables logging. Consider what secrets might appear in commands and output.
For contributions or vulnerabilities, see [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).
## Development
```bash
python -m unittest discover -s tests -t .
```
See the [changelog](CHANGELOG.md). MIT-licensed; see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessUnresponsive