Skip to main content
Glama
README.md
# mcp-ssh-gateway

`mcp-ssh-gateway` is an MCP-native boundary agent for remote operational capability.

It gives LLMs and automation systems controlled arms and legs into selected remote machines through SSH-based connections. Those machines may provide shell access, specialized hardware, local tools, storage, network adjacency, compute capacity, or access to isolated infrastructure.

The gateway is useful for work such as systems automation, remote administration, diagnostics, troubleshooting, penetration testing, offensive security research, and AI-assisted operational workflows.

The project is not a general orchestration platform, not a replacement for configuration management systems, and not an unrestricted remote shell broker. It is a narrow operational boundary between orchestration systems and real environments.

## Core Idea

LLMs are good at reasoning, planning, and interpreting complex output. Real systems require tools, hardware, shell access, network locality, and execution environments.

`mcp-ssh-gateway` bridges those worlds.

The gateway owns SSH identities, connection configuration, capability discovery, capability caching, execution logging, and transport mechanics. The orchestrator receives operational capability through MCP tools, but does not directly manage passwords, private keys, target addresses, or network topology.

The orchestrator gets capability, not custody.

## Architectural Shape

```text
LLM / MCP client
        ↓
FastMCP tool surface
        ↓
mcp-ssh-gateway
    ├── node registry
    ├── connection pool
    ├── execution logging
    └── transport orchestration
            ├── direct SSH
            └── reverse tunnel SSH
                    ↓
          remote node environments
```

Each configured node represents a trusted operational arm into a remote environment. The gateway manages SSH identities, connections, and execution, and exposes those capabilities back through MCP tools.

This allows an LLM to inspect available nodes, select the most appropriate machine for a task, and coordinate workflows across multiple remote systems.

## MCP Tools

| Tool | Description |
|---|---|
| `get_node_status` | List all nodes and their connection states |
| `get_node_info` | Get detailed node facts (use `refresh=true` for explicit manual refresh) |
| `get_agent_public_key` | Retrieve the agent's SSH public key for node enrollment |
| `add_node` | **Deprecated.** Enroll a new direct-mode node via password bootstrap. Kept only for existing test harnesses — do not call it from an MCP client or LLM workflow, since that would mean typing a real SSH password into a model prompt. Use the [CLI](#provisioning-a-node-cli) instead. |
| `enable_node` | Enable a node (use `validate=true` to probe connectivity) |
| `disable_node` | Disable a node and close its connection |
| `remove_node` | Remove a node from the pool and registry |
| `run_command_on_node` | Execute a command on a named node |
| `upload_file_to_node` | Upload a file to a named node via SFTP |
| `download_file_from_node` | Download a file from a named node via SFTP |

## Connection Modes

### Direct Mode

Direct mode is used when the remote machine is reachable from the gateway.

The gateway opens outbound SSH connections directly to the target. This is the simpler model and works well for internal infrastructure, VPN-connected environments, trusted networks, and static lab environments.

### Reverse Tunnel Mode

Reverse tunnel mode is used when the remote environment is not directly reachable from the gateway.

In this model, the remote machine initiates connectivity toward the gateway, exposes its local SSH service through a reverse tunnel, and the gateway connects back through the exposed local port.

This is useful for NATed environments, outbound-only infrastructure, headless devices, remote labs, and restricted networks.

The current configuration value is still:

```json
{
  "mode": "tunnel"
}
```

The documentation uses “reverse tunnel mode” to describe the operational model clearly.

## Runbooks and Skills

The gateway is intended to work together with runbooks, skills, procedures, and higher-level orchestration systems such as Open WebUI, n8n, OpenClaw, or other MCP-compatible clients.

The gateway provides operational reach. The orchestrator provides reasoning and workflow composition.

Paired with runbooks and skills, an LLM can plan larger workflows and execute them through real environments while keeping actions visible and attributable.

## Current Implementation Status

The project is in an active implementation phase.

Implemented:

- MCP startup and full node-lifecycle tool surface (see MCP Tools above)
- node registry and connection pool with direct SSH via Paramiko
- node enrollment via password bootstrap (`add_node`)
- agent SSH identity generation and key retrieval (`get_agent_public_key`)
- node handshake — facts collection via `resources/node/handshake.sh`
- structured command execution, SFTP upload, and SFTP download
- reverse tunnel probing through already-exposed local ports

Evolving:

- capability discovery and normalized capability cache
- execution history model
- task routing support

Not yet implemented:

- full agent-side reverse tunnel SSH listener
- end-to-end reverse tunnel establishment lifecycle
- advanced capability orchestration

Documentation must continue to distinguish implemented behavior from intended architecture.

## Build and Test

```bash
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install pytest
pytest
```

## Run

The current startup path expects a valid connection configuration file.

```bash
python app.py --connection-config ./connections.json
```

## Provisioning a Node (CLI)

**Why a CLI, and not the `add_node` MCP tool.** `add_node` takes an SSH
password as a tool argument. For that to work, the password would have to
pass through whatever is calling the tool — typically an LLM. No operator
should type a real credential into a model prompt, and no model should be the
thing asking for one. The CLI below is the channel where a human enters
credentials directly, in their own terminal: the password never becomes part
of any conversation, log line, or file on disk.

### Prerequisites

- Python with the project's dependencies installed (`pip install -e .`; see
  [Build and Test](#build-and-test) above). `paramiko` and `cryptography` are
  required.
- Network reachability from this machine to the target node's SSH port.
- The target's **current** SSH password for the account you want the agent
  to use (e.g. `pi`, `root`, a dedicated service account). This password is
  used exactly once, to install a public key — it is not the credential the
  gateway will use going forward.

### Run it

```bash
python -m agent.cli add-node
```

Run it from the repository root (or point `--agent-key-dir` /
`--connection-config` at absolute paths — see below). With no flags, it asks
you for everything it needs, one prompt at a time:

1. **Node name** — a unique identifier for this node inside the gateway
   (e.g. `pi-lab`, `build-box-3`). This is what you'll refer to it as in
   `run_command_on_node` and friends.
2. **Host** — hostname or IP address.
3. **Port** — SSH port (blank defaults to `22`).
4. **Username** — the SSH account on the target (blank defaults to `pi`).
5. **SSH password** — read with `getpass`, so it is never echoed to the
   terminal and never lands in shell history. This is the only prompt that
   cannot be skipped with a flag.

Every field except the password can also be passed as a flag, so the
non-secret parts can be scripted or come from a checklist:

```bash
python -m agent.cli add-node --name pi-lab --host 10.0.0.5 --port 22 --user pi
```

(You'll still be prompted for the password — there is no `--password` flag,
by design.) Add `--non-interactive` to make the command fail fast instead of
prompting if `--name`/`--host`/`--port`/`--user` are missing — useful in
scripts that want to fail loudly rather than hang waiting for input.

### What it does, step by step

1. Creates the agent key directory and the connections-config directory if
   they don't already exist.
2. Generates the agent's ed25519 SSH keypair in the key directory, if one
   isn't already there (reused across all nodes — this is the gateway's own
   identity, not a per-node key). On Windows, file permissions cannot be
   restricted to POSIX `0600`; the CLI reports this plainly and continues —
   it is a platform limitation, not a failure.
3. Connects to the target once, using the password you entered, and installs
   the agent's public key into that account's `~/.ssh/authorized_keys`
   (idempotent — safe to re-run against a host that already has the key).
4. Opens a fresh, **key-based** connection to prove the installed key
   actually works, then runs a trivial remote command (`echo ...`) over it
   and checks the output — so "provisioned" means "confirmed working",
   not just "we didn't get an error."
5. Appends the new node to `connections.json` (or wherever
   `--connection-config` points), in the exact shape the gateway already
   reads — `{name, user, id_file, mode, port, host}`. If that file already
   existed, it's backed up first, next to itself, as
   `connections.json.bak-<timestamp>`.
6. Prints the exact MCP client config block for this gateway, using the
   real, resolved paths on your machine — ready to paste into your MCP
   client's config (e.g. Claude Desktop's `claude_desktop_config.json`, or
   an equivalent `mcpServers` block).

### What to paste, and where

The command prints something like:

```json
{
  "mcpServers": {
    "ssh-gateway": {
      "command": "C:\\Python312\\python.exe",
      "args": [
        "C:\\mcp-ssh-gateway\\app.py",
        "--transport", "stdio",
        "--agent-key-dir", "C:\\mcp-ssh-gateway\\keys",
        "--connection-config", "C:\\mcp-ssh-gateway\\connections.json"
      ]
    }
  }
}
```

Paste the `"ssh-gateway": {...}` block (or the whole thing, if this is your
first server) into your MCP client's server-config file, under its
`mcpServers` key.

**If the gateway is already running:** it loaded `connections.json` at
startup and will not see the newly added node until it is restarted. The
CLI does not restart it for you — restart the process yourself once you're
ready to pick up the new node.

### How to verify it worked

The CLI itself already does the real verification for you — step 4 above
opens a genuine key-based SSH connection and runs a command through it, and
the final line of output says either:

```text
Result: NODE PROVISIONED AND VERIFIED.
```

or a clearly-labeled warning if the follow-up command check was
inconclusive (this can still mean the node is fine — e.g. an unusual shell
on the target — but it's called out rather than silently reported as
success).

To double-check independently after restarting the gateway, use the
`get_node_status` or `get_node_info` MCP tools (or `run_command_on_node`
against the new node's name) once the client is connected.

### Node parameters, in short

| Field | Meaning |
|---|---|
| `--name` | Unique identifier for the node inside the gateway. |
| `--host` | Hostname or IP address of the target. |
| `--port` | SSH port (default `22`). |
| `--user` | SSH username on the target. |
| `--mode` | Connection mode. Only `direct` is currently supported (matches the gateway's own `add_node` bootstrap logic). |
| `--agent-key-dir` | Where the agent's SSH keypair lives. Defaults to `keys/` under the repo root. |
| `--connection-config` | Path to the `connections.json`-shaped file to update. Defaults to `connections.json` under the repo root. |

## Documentation

- Documentation writing/governance standard: [`docs/DOCUMENTATION_GUIDE.md`](docs/DOCUMENTATION_GUIDE.md)
- Architecture boundaries and system model: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
- Testing intent and boundary enforcement: [`docs/TESTING_STRATEGY.md`](docs/TESTING_STRATEGY.md)
- Architectural decisions record: [`docs/ARCHITECTURAL_DECISIONS.md`](docs/ARCHITECTURAL_DECISIONS.md)
- Edge connectivity guidance: [`docs/EDGE.md`](docs/EDGE.md)
- Developer workflow: [`docs/DEVELOPER.md`](docs/DEVELOPER.md)
- Security and trust model: [`docs/SECURITY.md`](docs/SECURITY.md)
- Contribution guide: [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md)

## License

Apache 2.0.