MCP server for remote SSH operations -- persistent sessions, structured command execution, SFTP file transfer, and port forwarding for AI agents.
Project description
mcp-remote-ssh
MCP server giving AI agents full SSH access -- persistent sessions, structured command output, SFTP file transfer, port forwarding, 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.
# 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 │
└──────────────────────────────┘
- Local file read -- the env file lives on your machine, never on the remote host
- Shell injection via builtins -- uses
read -r VAR <<< 'value' && export VAR(no process tree exposure) - Automatic redaction -- every tool response (
ssh_execute,ssh_shell_send,ssh_shell_read,ssh_read_remote_file) is scrubbed before reaching the LLM - Longest-first matching -- prevents partial-match corruption (e.g.,
abc123is replaced beforeabc) - Works with exec channels -- secrets are prepended as exports to
ssh_executecommands so they're available in stateless channels too
Security properties
| Threat | Mitigated? | How |
|---|---|---|
| Secret in LLM context window | Yes | Output redaction replaces values with *** |
| Secret in remote process tree | Yes | Shell builtins (read/export) don't fork |
Secret in ssh_execute process tree |
Partial | Single short-lived process; use shell for zero-exposure |
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 |
Env file format
Standard .env format:
# Comments are ignored
API_TOKEN=your-secret-token
DB_PASSWORD="quoted values work"
SECRET_KEY='single quotes too'
export ALSO_WORKS=yes
Installation
uvx mcp-remote-ssh # or: pip install mcp-remote-ssh
Configuration
{
"mcpServers": {
"remote-ssh": {
"command": "uvx",
"args": ["mcp-remote-ssh"]
}
}
}
Tools (20)
Connection
| Tool | Description |
|---|---|
ssh_connect |
Connect with password, key, or agent auth. 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) |
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
ssh_connect(host="server.example.com", username="admin", password="secret")
→ {"session_id": "a1b2c3d4", "connected": 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 (SSH) + FastMCP (MCP protocol).
ssh_executeusesexec_command()for clean structured output with real exit codesssh_shell_*usesinvoke_shell()for persistent interactive sessions- All blocking Paramiko calls run in
run_in_executorto stay async - Shell keeps a 500KB rolling buffer for
shell_readpolling - Secret redaction uses longest-first string replacement across all output paths
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_remote_ssh-0.2.0.tar.gz.
File metadata
- Download URL: mcp_remote_ssh-0.2.0.tar.gz
- Upload date:
- Size: 23.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51b88afdc28c18e59dd314ceedf7f6b9e38548ac80f1c8cb1d5ec811a89a70a9
|
|
| MD5 |
19fcd073c36014214b4b07ca6f65dc5d
|
|
| BLAKE2b-256 |
919d8be1c8e5dddce64377f375fbe674a8688b40e3442c7e460f6b472fac2013
|
Provenance
The following attestation bundles were made for mcp_remote_ssh-0.2.0.tar.gz:
Publisher:
python-publish.yml on faizbawa/mcp-remote-ssh
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_remote_ssh-0.2.0.tar.gz -
Subject digest:
51b88afdc28c18e59dd314ceedf7f6b9e38548ac80f1c8cb1d5ec811a89a70a9 - Sigstore transparency entry: 1923864170
- Sigstore integration time:
-
Permalink:
faizbawa/mcp-remote-ssh@dfdc0e9607856c5ae6825b3074bf76074f78d491 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/faizbawa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@dfdc0e9607856c5ae6825b3074bf76074f78d491 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file mcp_remote_ssh-0.2.0-py3-none-any.whl.
File metadata
- Download URL: mcp_remote_ssh-0.2.0-py3-none-any.whl
- Upload date:
- Size: 20.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4be3522c869a9f36773c260ca1574ccc7597b749632b1a40b3e79aceb7246fed
|
|
| MD5 |
ab8c1bf5e8654f2c7b034cb051c42b40
|
|
| BLAKE2b-256 |
ea85726269a115078233449c1a1698dc91504b4b1b23479fe69721444efaad3f
|
Provenance
The following attestation bundles were made for mcp_remote_ssh-0.2.0-py3-none-any.whl:
Publisher:
python-publish.yml on faizbawa/mcp-remote-ssh
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_remote_ssh-0.2.0-py3-none-any.whl -
Subject digest:
4be3522c869a9f36773c260ca1574ccc7597b749632b1a40b3e79aceb7246fed - Sigstore transparency entry: 1923864306
- Sigstore integration time:
-
Permalink:
faizbawa/mcp-remote-ssh@dfdc0e9607856c5ae6825b3074bf76074f78d491 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/faizbawa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@dfdc0e9607856c5ae6825b3074bf76074f78d491 -
Trigger Event:
workflow_dispatch
-
Statement type: