mcp-remote-ssh
# mcp-remote-ssh
[](https://pypi.org/project/mcp-remote-ssh/)
[](https://pypi.org/project/mcp-remote-ssh/)
[](https://opensource.org/licenses/MIT)
MCP server giving AI agents full SSH access -- persistent sessions, structured command output, SFTP file transfer, port forwarding, session transcript recording, and **secret-safe environment variable injection with automatic output redaction**.
## Why this exists
Every other SSH MCP server is missing something: no password auth, no persistent sessions, no SFTP, no port forwarding, or no structured exit codes. This one has all of them -- plus the only MCP-level secret management that prevents AI agents from ever seeing your credentials.
## Secret-Safe Environment Variables
**The problem:** When an AI agent needs to use API tokens, passwords, or keys on a remote server, the standard approach exposes secrets in the LLM's context window. The agent either reads the secret file (now it's in the conversation) or runs `echo $TOKEN` and sees the value in the output.
**The solution:** `ssh_load_env_file` reads secrets from a local file on your machine, injects them into the remote SSH session, and registers them for **automatic output redaction**. The AI agent can use the variables freely -- every tool response is scrubbed before it reaches the LLM.
```text
# Agent calls this -- file is read from YOUR machine, not the remote host
ssh_load_env_file(session_id="abc", file_path="~/.secrets/prod.env")
→ "Loaded 3 variables from local:~/.secrets/prod.env: API_TOKEN, DB_PASS, SECRET_KEY"
# Agent tries to echo the value -- redacted automatically
ssh_execute(session_id="abc", command="echo $API_TOKEN")
→ {"stdout": "***\n", "exit_code": 0}
# Agent dumps the environment -- all secret values scrubbed
ssh_execute(session_id="abc", command="env | grep API_TOKEN")
→ {"stdout": "API_TOKEN=***\n", "exit_code": 0}
# Agent reads a file containing a secret -- also redacted
ssh_read_remote_file(session_id="abc", remote_path="/etc/app/config")
→ "db_password=***\ndb_host=localhost\n"
# Normal commands work perfectly -- no over-redaction
ssh_execute(session_id="abc", command="uname -a")
→ {"stdout": "Linux server 6.1.0 ...", "exit_code": 0}
```
### How it works
```
┌─────────┐ ┌──────────────────────────────┐ ┌─────────────┐
│ LLM │ ←─JSON─ │ MCP Server (your machine) │ ──SSH─→ │ Remote Host │
│ (Agent) │ │ │ │ │
└─────────┘ │ 1. Reads ~/.secrets/prod.env│ └─────────────┘
│ 2. Parses KEY=VALUE pairs │
│ 3. Stores values in memory │
│ 4. Injects into SSH session │
│ 5. Redacts ALL tool output │
└──────────────────────────────┘
```
1. **Local file read** -- the env file lives on your machine, never on the remote host
2. **Shell injection via builtins** -- uses `read -r VAR <<< 'value' && export VAR` (no process tree exposure)
3. **Stdin-based exec injection** -- `ssh_execute` feeds secrets via stdin to a bash wrapper, so they never appear in `/proc/*/cmdline`
4. **Automatic redaction** -- every tool response (`ssh_execute`, `ssh_shell_send`, `ssh_shell_read`, `ssh_read_remote_file`) is scrubbed before reaching the LLM
5. **Longest-first matching** -- prevents partial-match corruption (e.g., `abc123` is replaced before `abc`)
### Security properties
| Threat | Mitigated? | How |
|--------|-----------|-----|
| Secret in LLM context window | Yes | Output redaction replaces values with `***` |
| Secret in remote process tree (shell) | Yes | Shell builtins (`read`/`export`) don't fork |
| Secret in remote process tree (exec) | Yes | Secrets fed via stdin, never in `/proc/*/cmdline` |
| LLM tries `cat` on the env file | N/A | File is local-only, doesn't exist on remote |
| LLM tries `echo $VAR` | Yes | Output is redacted |
| Encoded/transformed secret (base64) | No | Only literal matches are redacted |
| MITM on first SSH connection | Accepted | `AutoAddPolicy` used — see note below |
### Host key policy
This server uses Paramiko's `AutoAddPolicy` — unknown host keys are accepted without prompting. This is intentional for QE/lab environments where hosts are ephemeral (Beaker, cloud instances, CI machines). The trade-off:
- **Pro:** Zero-friction connections to newly provisioned machines
- **Con:** Vulnerable to MITM on the very first connection to an unknown host
If you operate on untrusted networks, consider wrapping connections through a VPN or SSH bastion with pre-distributed host keys. A `host_key_policy` parameter may be added in a future release for strict environments.
### Env file format
Standard `.env` format:
```bash
# Comments are ignored
API_TOKEN=your-secret-token
DB_PASSWORD="quoted values work"
SECRET_KEY='single quotes too'
export ALSO_WORKS=yes
```
## Session transcripts
Recording is **off by default**. Enable it per session when you need an audit log of what the agent actually ran — useful for bug reproductions and test campaigns.
```text
# Start recording on connect
ssh_connect(host="lab.example.com", username="root", password="...", record=True)
# Or toggle later
ssh_start_recording(session_id="a1b2c3d4")
ssh_execute(session_id="a1b2c3d4", command="uname -a")
ssh_get_transcript(session_id="a1b2c3d4")
→ {"recording": true, "total_entries": 2, "transcript": "[12:01:02] --- connect: lab.example.com ---\n[12:01:05] $ uname -a\nLinux ...\n[exit 0]"}
ssh_save_transcript(session_id="a1b2c3d4", path="/tmp/lab-session.log")
ssh_stop_recording(session_id="a1b2c3d4")
```
- Execute/sudo **output** and shell send/read I/O are secret-redacted before they are recorded
- Closing the session discards the in-memory transcript — `ssh_save_transcript` or `ssh_get_transcript` first
## Installation
```bash
uvx mcp-remote-ssh # or: pip install mcp-remote-ssh
```
## Configuration
```json
{
"mcpServers": {
"remote-ssh": {
"command": "uvx",
"args": ["mcp-remote-ssh"]
}
}
}
```
## Tools (24)
### Connection
| Tool | Description |
|------|-------------|
| `ssh_connect` | Connect with password, key, or agent auth. Optional `record=True` to start a transcript immediately. Returns `session_id` |
| `ssh_list_sessions` | List active sessions |
| `ssh_close_session` | Close a session and release resources |
### Execution
| Tool | Description |
|------|-------------|
| `ssh_execute` | Run a command, returns `{stdout, stderr, exit_code}` |
| `ssh_sudo_execute` | Run with sudo elevation |
### Interactive Shell
| Tool | Description |
|------|-------------|
| `ssh_shell_open` | Open persistent shell (preserves cwd, env, processes) |
| `ssh_shell_send` | Send text (with optional Enter) |
| `ssh_shell_read` | Read current output buffer |
| `ssh_shell_send_control` | Send Ctrl+C, Ctrl+D, etc. |
| `ssh_shell_wait` | Wait for a pattern or output to stabilize |
### Secrets Management
| Tool | Description |
|------|-------------|
| `ssh_load_env_file` | Load secrets from a local env file; values never returned to the LLM |
| `ssh_clear_secrets` | Clear redaction registry (values become visible again) |
### Transcripts
| Tool | Description |
|------|-------------|
| `ssh_start_recording` | Start recording execute/sudo/shell I/O for this session |
| `ssh_stop_recording` | Stop recording; transcript stays available until the session is closed |
| `ssh_get_transcript` | Return the transcript as text or JSONL (`last_n` optional) |
| `ssh_save_transcript` | Write the transcript to a **local** file on the MCP host |
### SFTP
| Tool | Description |
|------|-------------|
| `ssh_upload_file` | Upload local file to remote host |
| `ssh_download_file` | Download remote file to local machine |
| `ssh_read_remote_file` | Read a remote text file |
| `ssh_write_remote_file` | Write/append to a remote file |
| `ssh_list_remote_dir` | List directory with metadata |
### Port Forwarding
| Tool | Description |
|------|-------------|
| `ssh_forward_port` | Create SSH tunnel (local -> remote) |
| `ssh_list_forwards` | List active tunnels |
| `ssh_close_forward` | Close a tunnel |
## Quick start
```text
ssh_connect(host="server.example.com", username="admin", password="secret", record=True)
→ {"session_id": "a1b2c3d4", "connected": true, "recording": true}
ssh_load_env_file(session_id="a1b2c3d4", file_path="~/.secrets/prod.env")
→ "Loaded 2 variables: API_TOKEN, DB_PASS"
ssh_execute(session_id="a1b2c3d4", command="curl -H \"Authorization: Bearer $API_TOKEN\" https://api.example.com")
→ {"stdout": "{\"status\": \"ok\"}", "exit_code": 0} # token used but never visible
ssh_shell_open(session_id="a1b2c3d4")
ssh_shell_send(session_id="a1b2c3d4", data="cd /opt && make -j$(nproc)")
ssh_shell_wait(session_id="a1b2c3d4", pattern="$ ", timeout=600)
ssh_upload_file(session_id="a1b2c3d4", local_path="config.yaml", remote_path="/etc/app/config.yaml")
ssh_forward_port(session_id="a1b2c3d4", remote_port=5432, local_port=15432)
```
## Design
Built on **[Paramiko](https://www.paramiko.org/)** (SSH) + **[FastMCP](https://github.com/PrefectHQ/fastmcp)** (MCP protocol).
- `ssh_execute` uses `exec_command()` for clean structured output with real exit codes
- When secrets are loaded, `ssh_execute` feeds exports via stdin to a `bash` wrapper, then `exec`s the actual command -- secrets never appear in the process tree
- `ssh_shell_*` uses `invoke_shell()` for persistent interactive sessions
- All blocking Paramiko calls run in `run_in_executor` to stay async
- Shell keeps a 500KB rolling buffer for `shell_read` polling
- Secret redaction uses longest-first string replacement across all output paths
- Session transcripts are in-memory, off by default, and discarded when the session is closed
## License
MIT
TDQS
Scored across 18 tools
Each tool targets a distinct operation: connection, execution, file transfer, shell interaction, port forwarding, and session management. No overlaps; even similar tools like ssh_execute and ssh_sudo_execute are clearly differentiated by the sudo aspect.
All tools follow the pattern `ssh_<verb>_<noun>`, with shell tools using `ssh_shell_<verb>` as a consistent sub-pattern. No mixed conventions or atypical naming.
18 tools cover the full scope of SSH remote management: connection, stateless and persistent execution, file operations, port forwarding, and session control. Each tool feels justified and the count is appropriate for the domain.
Core workflows are well covered (connect, execute, file transfer, shell, port forwarding). Minor gaps exist: no direct file delete/rename or directory creation tool, but these can be achieved via command execution. Overall, the surface is nearly complete.