Skip to main content
Glama

SSHand

An open MCP server that gives any AI agent SSH access to remote Linux/Unix machines — shell commands, file read/write, and SFTP transfers.

Works with Claude.ai, Claude Desktop, ChatGPT, Cursor, VS Code Copilot, OpenAI Agents SDK, and any other MCP-compatible client.


Quick start

# 1. Install
pip install sshand

# 2. First-run setup: add a host, test it, print client config
sshand setup

# 3. Manage hosts any time (add / test / remove + reprint config)
sshand manage

Both commands open the same clean interactive terminal UI — use the arrow keys to move and Enter to select. setup runs the guided first-run flow; manage is the ongoing host manager.


Related MCP server: mcp-remote-ssh

Setup guide

sshand setup walks you through three steps:

1. Add a host. You'll be asked for:

  • Alias — a short nickname like webserver or db-prod.

  • Hostname / IP, port (default 22), and username.

  • Authentication method:

    • SSH key file (recommended) — path to your private key, plus an optional passphrase.

    • Password — convenient for dev/test, avoid on internet-facing hosts.

    • SSH agent — delegates to your running ssh-agent; no credentials stored. On Windows the wizard also checks whether the OpenSSH Authentication Agent service is running and offers to start it.

The host is saved to hosts.yaml (or the path in SSH_MCP_HOSTS_FILE). An existing host is never overwritten without confirmation.

2. Test the connection. SSHand immediately connects and runs a no-op command, so you find out right away if something is wrong (bad key path, wrong port, unreachable host) instead of mid-conversation later. The host is saved either way — if the test fails, fix the issue and run sshand setup again.

3. Print client config. Pick a client (or All of them) and SSHand prints a ready-to-paste config snippet with the correct absolute paths already filled in:

Claude Desktop · Cursor · VS Code (GitHub Copilot Chat) · OpenAI (Agents SDK / ChatGPT Desktop) · Hermes Agent · OpenClaw (via MCPorter) · Other (HTTP)

Full per-client instructions are in Connecting to your AI client below.

Run sshand setup again any time to add another host or reprint a snippet — it won't touch your existing hosts.


Managing your hosts

sshand manage opens the interactive host manager — a live table of your configured hosts plus a menu:

  • List hosts — refresh the table.

  • Add a host — the same flow as setup, with an optional connection test afterwards.

  • Test a host — connect and report success or failure.

  • Remove a host — delete an entry (with confirmation).

  • Client config snippets — reprint the paste-ready config for any client, prefaced with the list of hosts the agent will be able to reach.

The screen refreshes in place after each action, with the result of your last action shown above the menu. Press q / choose Quit to exit.


Installation options

pip install sshand        # from PyPI
pip install -e .            # from source, editable install

Option B — uvx (zero-install, no venv needed)

uv is the modern Python package manager. With it installed you can run SSHand without any manual install step:

uvx sshand                # run server directly
uvx sshand setup          # run setup wizard

This is the cleanest option to recommend to non-technical users.

Option C — plain Python (no install)

git clone https://github.com/muradmalik23/sshand
cd sshand
pip install -r requirements.txt
python server.py            # start server
python manage.py setup      # run the setup wizard
python manage.py            # interactive host manager

Connecting to your AI client

Claude.ai and ChatGPT (web + desktop)

For Claude.ai web and ChatGPT, SSHand runs as an HTTP server and you connect it through each app's native integrations UI.

→ See INTEGRATIONS.md for the full step-by-step guide, including how to expose the server publicly using ngrok or Cloudflare Tunnel, and how to add it to Claude.ai Integrations and ChatGPT Desktop.


Claude Desktop

Add to your claude_desktop_config.json:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "ssh": {
      "command": "uvx",
      "args": ["sshand"]
    }
  }
}

Or with plain Python:

{
  "mcpServers": {
    "ssh": {
      "command": "python",
      "args": ["/absolute/path/to/sshand/server.py"]
    }
  }
}

No env var needed for either option — hosts are stored at ~/.sshand/hosts.yaml automatically. See Host inventory below if you want a different location.

Restart Claude Desktop after saving.


Cursor

Create or update .cursor/mcp.json in your project (or the global Cursor MCP settings):

{
  "mcpServers": {
    "ssh": {
      "command": "uvx",
      "args": ["sshand"]
    }
  }
}

No env var needed — hosts are stored at ~/.sshand/hosts.yaml automatically.


VS Code (GitHub Copilot Chat)

Add to .vscode/mcp.json or your workspace settings.json:

{
  "mcp": {
    "servers": {
      "ssh": {
        "type": "stdio",
        "command": "uvx",
        "args": ["sshand"]
      }
    }
  }
}

No env var needed — hosts are stored at ~/.sshand/hosts.yaml automatically.


OpenAI Agents SDK

# Terminal 1 — keep this running
sshand --transport http --port 8000
from agents import Agent
from agents.mcp import MCPServerStreamableHttp

ssh_server = MCPServerStreamableHttp(url="http://localhost:8000/mcp")
agent = Agent(name="ops-agent", mcp_servers=[ssh_server])

Hermes Agent

Hermes Agent (Nous Research) reads MCP server config from ~/.hermes/config.yaml under the mcp_servers key — same command/args/env shape as everywhere else:

mcp_servers:
  ssh:
    command: "uvx"
    args: ["sshand"]

If you installed SSHand from source instead of via uvx, point command at python and add ["/absolute/path/to/sshand/server.py"] as args, same as the Claude Desktop snippet above.

No env var needed either way — hosts are stored at ~/.sshand/hosts.yaml automatically.

Start (or reload) Hermes to pick it up:

hermes chat          # fresh start
/reload-mcp          # or, from inside a running session

Hermes prefixes every tool with mcp_<server_name>_, so e.g. ssh_run_command shows up as mcp_ssh_ssh_run_command — you won't normally need the prefixed name, Hermes picks the right tool from your prompt on its own.


OpenClaw

OpenClaw doesn't take MCP servers directly — it calls them through MCPorter, a separate CLI that OpenClaw shells out to for schema discovery and tool calls. Install MCPorter first:

npm install -g mcporter

Then register SSHand with it:

mcporter config add ssh --command uvx --args sshand

That writes an entry to config/mcporter.json (or ~/.mcporter/mcporter.json for a machine-wide install) in the same mcpServers shape used everywhere else:

{
  "mcpServers": {
    "ssh": {
      "command": "uvx",
      "args": ["sshand"]
    }
  }
}

No env var needed — hosts are stored at ~/.sshand/hosts.yaml automatically.

Confirm MCPorter can see it and list the tools:

mcporter list ssh --schema

No further OpenClaw-side config is needed — just ask it to do something that needs SSH (e.g. "check disk usage on webserver") and OpenClaw will invoke mcporter call ssh.ssh_run_command ... on its own.


Any other MCP client (HTTP)

sshand --transport http --port 8000

MCP endpoint: http://localhost:8000/mcp

For remote access, put a reverse proxy (nginx / Caddy) in front with TLS. Never expose port 8000 directly on a public interface.


Host inventory

Hosts are stored in ~/.sshand/hosts.yaml, created automatically on startup regardless of how SSHand was installed (pip, uvx, or from source) — no env var or extra config required. You can edit the file directly or let your agent call ssh_add_host.

Want a different location instead? Set SSH_MCP_HOSTS_FILE to override it, e.g. "env": { "SSH_MCP_HOSTS_FILE": "/absolute/path/to/hosts.yaml" } in any of the MCP configs above.

Running from a git clone and want the inventory to live next to the source instead? Point SSH_MCP_HOSTS_FILE at a repo-local copy:

cp hosts.yaml.example hosts.yaml
export SSH_MCP_HOSTS_FILE="$PWD/hosts.yaml"

Auth types

Key file — most common, most secure:

auth:
  type: key
  key_path: ~/.ssh/id_rsa       # ~ is expanded automatically
  passphrase: null              # set if the key is encrypted

Password — convenient for dev/test, avoid on internet-facing servers:

auth:
  type: password
  password: s3cr3t

SSH agent — no credentials stored at all, delegates to the running ssh-agent:

auth:
  type: agent

Available tools

Tool

Description

Read-only?

ssh_list_hosts

List all configured SSH targets

✅

ssh_add_host

Register a new SSH host

❌

ssh_remove_host

Remove a host from inventory

❌

ssh_test_connection

Verify auth works for a host

✅

ssh_run_command

Execute a shell command + capture output

❌

ssh_read_file

Read a remote file's contents

✅

ssh_write_file

Create or overwrite a remote file

❌

ssh_list_directory

Browse a remote directory

✅

ssh_upload_file

Push a local file to the remote host (SFTP)

❌

ssh_download_file

Pull a remote file to this machine (SFTP)

✅

ssh_get_local_info

Return OS and path style of the MCP server host

✅


Example conversations

"What servers do you have access to?" → ssh_list_hosts

"Check disk usage on webserver" → ssh_run_command(alias='webserver', command='df -h')

"Read the nginx config on the web box" → ssh_read_file(alias='webserver', remote_path='/etc/nginx/nginx.conf')

"Tail the last 100 lines of syslog on bastion" → ssh_run_command(alias='bastion', command='tail -n 100 /var/log/syslog')

"Deploy this config file to devbox" → ssh_write_file(alias='devbox', remote_path='/etc/myapp/config.yaml', content='...')

"Download today's DB backup from webserver" → ssh_download_file(alias='webserver', remote_path='/backups/db-today.sql.gz', local_path='/tmp/db-today.sql.gz')


CLI reference

sshand [subcommand] [options]

Subcommands:
  setup                  Interactive first-run wizard (add a host, test, print config)
  manage                 Interactive host manager (add / test / remove + config snippets)

Options:
  --transport {stdio,http}   Transport (default: stdio)
  --port INT                 HTTP port (default: 8000)
  --host STR                 HTTP bind address (default: 127.0.0.1)

Examples:
  sshand                    # start stdio server
  sshand setup              # first-run wizard
  sshand manage             # interactive host manager
  sshand --transport http   # start HTTP server on :8000

Security notes

  • By default your inventory lives outside the repo at ~/.sshand/hosts.yaml, so it can't accidentally land in version control. If you've pointed SSH_MCP_HOSTS_FILE at a repo-local hosts.yaml, keep it out of git — it's already excluded by the included .gitignore.

  • Prefer key-based or agent auth over password auth for any internet-facing host.

  • The HTTP transport binds to 127.0.0.1 by default.

  • When exposing the HTTP server publicly (for Claude.ai / ChatGPT web), use TLS and consider adding authentication via a reverse proxy.

  • ssh_run_command is marked destructiveHint: true — MCP clients that respect annotations will prompt before running potentially dangerous commands.


Project structure

sshand/
├── server.py           # FastMCP server — all 11 tools + CLI entry point
├── ssh_client.py       # Async paramiko wrapper (command exec + SFTP)
├── host_config.py      # YAML host inventory manager
├── manage.py           # Interactive TUI — setup wizard + host manager (rich + questionary)
├── setup_wizard.py     # Client-config snippet builders + Windows agent helpers (used by manage.py)
├── platform_utils.py   # Windows SSH agent helpers
├── hosts.yaml.example  # Safe template — for repo-local SSH_MCP_HOSTS_FILE setups
├── hosts.yaml          # Optional: only present if SSH_MCP_HOSTS_FILE points here (gitignored)
├── INTEGRATIONS.md     # Guide: Claude.ai and ChatGPT native extensions
├── pyproject.toml      # Package metadata + pip/uvx install config
├── requirements.txt    # Plain pip install fallback
└── README.md

Available Tools

11 tools
ssh_add_hostA

Register a new SSH host in the inventory.

Saves the host details to hosts.yaml so it can be referenced by alias in all other ssh_* tools. Supports key, password, and agent authentication.

Args: alias: Short nickname for the host (e.g., 'webserver', 'db-prod'). Only letters, digits, underscores, and hyphens are allowed. hostname: IP address or FQDN (e.g., '192.168.1.10', 'db.example.com'). username: SSH login username (e.g., 'ubuntu', 'ec2-user'). auth_type: Authentication method — 'key' (private key file), 'password', or 'agent' (ssh-agent). port: SSH port (default 22). key_path: Required when auth_type='key'. Path to the private key file (e.g., '~/.ssh/id_rsa'). key_passphrase: Optional passphrase to decrypt an encrypted private key. password: Required when auth_type='password'. description: Optional human-readable note about this server. overwrite: Set True to replace an existing host with the same alias.

Returns: Success message or error description.

Examples: - Add a key-based host: alias='webserver', hostname='10.0.0.5', username='ubuntu', auth_type='key', key_path='~/.ssh/id_rsa' - Add a password host: alias='legacy', hostname='old.corp.net', username='admin', auth_type='password', password='s3cr3t' - Add an agent-auth host: alias='jump', hostname='bastion.example.com', username='ops', auth_type='agent'

Inputs: alias (required), hostname (required), username (required), auth_type (required), +6 more. Returns: result

ssh_download_fileA
Idempotent

Download a file from a remote SSH host to the local machine via SFTP.

The local parent directory is created automatically if needed.

Args: alias: Source host alias. remote_path: Absolute POSIX path of the file on the remote host. local_path: Absolute destination path on the local machine, in the format native to the machine running the MCP server (e.g. 'C:\Users\me\downloads\file.txt' on Windows, '/home/me/downloads/file.txt' on Linux/macOS). Call ssh_get_local_info first if you are unsure which format to use.

Returns: Success message with byte count, or an error description.

Examples: - Fetch a log: remote_path='/var/log/app.log', local_path='/tmp/app.log' - Pull a DB dump: remote_path='/backups/db.sql.gz', local_path='/tmp/db.sql.gz'

Inputs: alias (required), local_path (required), remote_path (required). Returns: result

ssh_get_local_infoA
Read-onlyIdempotent

Return OS, home directory, cwd, and path style of the machine running the MCP server.

Call this before ssh_upload_file or ssh_download_file to discover the correct local_path format (Windows backslash vs POSIX forward-slash).

Args: response_format: 'json' (default) or 'markdown'.

Returns: OS info including: os, home, cwd, path_style ('windows' or 'posix').

Examples: - Before uploading: ssh_get_local_info() → learn that local_path needs Windows format - Debugging path errors: confirm the server's working directory

Inputs: response_format. Returns: result

ssh_list_directoryA
Read-onlyIdempotent

List the contents of a directory on a remote SSH host.

Returns file names, types, sizes, permissions, and last-modified timestamps. Directories are listed before files, both sorted alphabetically.

Args: alias: Host alias. remote_path: Directory path to list (default '~' = home directory). limit: Maximum number of entries to return. Useful for large directories like /proc or /var/log. Omit to return all entries. response_format: 'markdown' (default) or 'json'.

Returns: Directory listing with columns: Type | Name | Size | Permissions | Modified. JSON format: {path, total, count, truncated, entries: [{name, type, size, permissions, modified}]}

Examples: - Browse home dir: ssh_list_directory(alias='web') - List /var/log: remote_path='/var/log' - List /etc/nginx: remote_path='/etc/nginx'

Inputs: alias (required), limit, remote_path, response_format. Returns: result

ssh_list_hostsA
Read-onlyIdempotent

List all SSH hosts registered in the inventory.

Returns the alias, hostname, port, username, and auth type for every configured host. Call this first when you don't know what machines are available.

Args: response_format: 'markdown' (default) for a readable table, or 'json' for structured data.

Returns: Formatted list of hosts, or a message if the inventory is empty.

Examples: - "What servers can I connect to?" -> ssh_list_hosts() - "Show me all SSH targets as JSON" -> ssh_list_hosts(response_format='json')

Inputs: response_format. Returns: result

ssh_read_fileA
Read-onlyIdempotent

Read the contents of a file on a remote SSH host.

Uses SFTP to transfer the file. Suitable for config files, logs, scripts, etc. For binary files set encoding='raw' to get a base64-encoded string.

Args: alias: Host alias. remote_path: Absolute path of the file to read on the remote host (e.g., '/etc/nginx/nginx.conf'). encoding: Text encoding to decode the file (default 'utf-8'). Use 'raw' to get a base64-encoded string for binary files.

Returns: File contents as text, or base64-encoded bytes for encoding='raw'. Returns an error string on failure.

Examples: - Read nginx config: alias='web', remote_path='/etc/nginx/nginx.conf' - Read /etc/hosts: remote_path='/etc/hosts' - Read binary file: remote_path='/usr/bin/ls', encoding='raw'

Inputs: alias (required), remote_path (required), encoding. Returns: result

ssh_remove_hostA
Destructive

Remove a host from the SSH inventory permanently.

This only removes the local config entry — it does NOT touch the remote server in any way.

Args: alias: The host alias to remove.

Returns: Confirmation or error message.

Inputs: alias (required). Returns: result

ssh_run_commandA
Destructive

Execute a shell command on a remote SSH host and return the output.

The command runs in the login user's default shell. Both stdout and stderr are captured and returned. The exit code is included so you can detect failures.

Args: alias: Target host alias. command: Shell command to execute (e.g., 'ls -la /var/log', 'systemctl status nginx', 'df -h'). timeout: Maximum seconds to wait (default 60, max 3600). env: Optional extra environment variables dict for this command. cwd: Remote working directory. When set the effective invocation becomes 'cd && '. Use absolute POSIX paths (e.g. '/var/www/html'). State is NOT persisted between calls. sudo_password: When set, the command runs via sudo. The password is never echoed back in output. response_format: 'markdown' (default, human-readable) or 'json'.

Returns: Command output (stdout + stderr) with exit code.

Examples: - Inspect disk usage: command='df -h' - Check a service: command='systemctl status nginx' - Tail logs: command='tail -n 50 /var/log/syslog' - Install a package: command='sudo apt-get install -y htop'

Inputs: alias (required), command (required), cwd, env, +3 more. Returns: result

ssh_test_connectionA
Read-onlyIdempotent

Verify that a host is reachable and authentication succeeds.

Attempts to open an SSH session and run a trivial echo command. Use this immediately after ssh_add_host to confirm your credentials are correct.

Args: alias: Host alias to test (must exist in inventory).

Returns: '✓ Connected …' on success, or an actionable error on failure.

Examples: - After adding a new host: ssh_test_connection(alias='webserver') - Debugging a connection issue: ssh_test_connection(alias='db-prod')

Inputs: alias (required). Returns: result

ssh_upload_fileA
DestructiveIdempotent

Upload a local file to a remote SSH host via SFTP.

The remote parent directory is created automatically if it doesn't exist. Use this to deploy configs, binaries, or any large file.

Args: alias: Target host alias. local_path: Absolute path to the local file to upload, in the format native to the machine running the MCP server (e.g. 'C:\Users\me\file.txt' on Windows, '/home/me/file.txt' on Linux/macOS). Call ssh_get_local_info first if you are unsure which format to use. remote_path: Absolute POSIX destination path on the remote host (e.g. '/home/user/file.txt').

Returns: Success message with byte count, or an error description.

Examples: - Deploy an app binary: local_path='/dist/myapp', remote_path='/opt/myapp/bin/myapp' - Upload a TLS cert: local_path='/tmp/server.crt', remote_path='/etc/ssl/certs/server.crt'

Inputs: alias (required), local_path (required), remote_path (required). Returns: result

ssh_write_fileA
DestructiveIdempotent

Write content to a file on a remote SSH host (create or overwrite).

Uses SFTP to transfer the content. Missing parent directories are created automatically. For binary content, base64-encode it and set encoding='base64'.

Args: alias: Host alias. remote_path: Absolute destination path on the remote host. content: Text to write. For binary content, encode as base64 and set encoding='base64'. encoding: 'utf-8' to write the content as text (default), 'base64' to decode first.

Returns: Success message with byte count, or an error description.

Examples: - Write a cron job: remote_path='/etc/cron.d/myjob', content='...' - Deploy a config: remote_path='/etc/myapp/config.yaml', content='...' - Write a script: remote_path='/usr/local/bin/deploy.sh', content='...'

Inputs: alias (required), content (required), remote_path (required), encoding. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.6/5.0

    Scored across 11 tools

    Disambiguation5/5

    Each tool targets a distinct operation: host inventory management, file transfer, command execution, directory listing, and connection testing. There is no functional overlap between any two tools.

    Naming Consistency5/5

    All tools follow the 'ssh_verb_noun' pattern consistently (e.g., ssh_add_host, ssh_list_hosts, ssh_run_command). The naming is uniform and predictable.

    Tool Count5/5

    With 11 tools, the set covers all essential SSH operations without being excessive. The count is well within the typical 3-15 range for a focused server.

    Completeness4/5

    The tool surface provides CRUD for hosts, remote command execution, file read/write/upload/download, directory listing, and connection testing. The only minor gap is the lack of an update host tool, but this can be worked around.

    Maintenance

    ActivityNo data
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      An MCP server that gives AI assistants full SSH/SFTP remote operations — session management, command execution, interactive shells, file transfers, port forwarding, and system diagnostics.
      2
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      MCP server giving AI agents full SSH access with persistent sessions, structured command output, SFTP file transfer, and port forwarding.
      18
      59 PyPI
      12
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      An MCP server that gives AI agents SSH access to remote machines through your local OpenSSH client, enabling remote command execution, file transfer, persistent shell sessions, and port forwarding.
      17
      319 PyPI
      38
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      An MCP server that gives AI agents SSH capabilities to execute commands, transfer files, and inspect remote systems through a preconfigured host list.
      78 npm
      MIT