npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ssh-mcp-cli

v1.6.2

Published

Dual-mode SSH automation: MCP server and CLI (exec, transfers, port forwarding, background jobs)

Readme

SSH for AI agents and humans. Define your servers once — use them from any MCP client or directly from your terminal. One binary, one config, persistent shells, background jobs, tunnels, and it keeps itself updated.

CI npm License: MIT Node

$ ssh-mcp exec prod-web "uptime && df -h / | tail -1"
 21:42:07 up 34 days,  load average: 0.18 0.22 0.19
 /dev/vda1       79G   31G   45G  41% /

$ ssh-mcp exec prod-web "npm run build" --bg --stall-timeout 0
 Job started: job_m9x2k1
$ ssh-mcp job check job_m9x2k1 --json | jq -r '.result.exitCode'
 0

$ ssh-mcp forward prod-db localhost 5432 --local-port 15432 &
 127.0.0.1:15432 -> localhost:5432  (Ctrl-C to stop)

30-second quickstart

npm install -g ssh-mcp-cli          # or: bun add -g ssh-mcp-cli
ssh-mcp servers                     # first run creates ~/.ssh-mcp/config.json (chmod 600)
$EDITOR ~/.ssh-mcp/config.json      # add a server (see below)
ssh-mcp exec my-server uname -a     # done

And for your AI agent (any MCP client):

{ "mcpServers": { "ssh-mcp": { "command": "npx", "args": ["-y", "ssh-mcp-cli"] } } }

Features

  • Dual-mode: same binary runs as an MCP server (stdio) or a standalone CLI
  • Auto-connect: Tools automatically connect when needed - no manual connect calls required
  • Persistent SSH connections with keep-alive and auto-reconnection
  • Persistent shell sessions - working directory and environment variables persist across commands
  • Execute commands on remote servers
  • Background execution: Run long commands asynchronously with job tracking
  • Upload and download files via SFTP
  • Local and remote port forwarding (including through jump hosts)
  • Multi-server support with connection pooling
  • Secure credential storage with 0600 permission validation

Installation

npm install -g ssh-mcp-cli
ssh-mcp --help

Works with any package manager (bun add -g ssh-mcp-cli, pnpm add -g ssh-mcp-cli, yarn global add ssh-mcp-cli) and one-shot via npx ssh-mcp-cli servers.

Zero-dependency install: the npm package ships as a single prebuilt bundle — no dependency install scripts, nothing for npm 12's script policy to warn about, and installs take under a second.

Auto-update

The CLI keeps itself current automatically: at most once per 24h it checks the npm registry in the background and, when a newer version exists, installs it via a detached process. The running command is never blocked or modified - the next invocation uses the new version.

  • Opt out: export SSH_MCP_AUTO_UPDATE=0 (also false/no/off)
  • Never triggers in MCP server mode, background job runners, or --json invocations
  • Check state: ~/.ssh-mcp/update-state.json (next to your config)
  • Manual update anytime: ssh-mcp update

Alternative while offline from npm - install from GitHub (release tags ship prebuilt dist/, no build scripts run):

bun add -g github:ErcinDedeoglu/ssh-mcp

Developing locally: clone, bun install, bun run build, then npm link to expose the ssh-mcp command.

Requirements: Node.js 22+ locally. Remote servers: Linux, macOS, Windows (bash/zsh, PowerShell, cmd.exe). Windows local caveat: job cancel / forward-close can't deliver cross-process signals, so they hard-terminate the runner/forward instead of a graceful shutdown.

Configuration

By default, the config file is at ~/.ssh-mcp/config.json. You can override this with:

| Method | Example | | -------------------- | ----------------------------------------- | | Environment variable | SSH_MCP_CONFIG=~/.config/myapp/ssh.json | | CLI argument | --config ~/.config/myapp/ssh.json |

Priority: CLI arg > env var > default. The ~ expands to home directory on all platforms.

Project-level config (.ssh-mcp.json)

Per-project server overrides: drop a .ssh-mcp.json in your project root (discovered walking up from the current directory, git-style). It merges over your central config:

  • servers override by id — project entries win, new ids are appended
  • keys merge by name, project wins
  • defaults merge per-field
  • shell-type persistence writes back to whichever file owns the server
{ "servers": [ { "id": "staging", "host": "10.0.0.9", "port": 22, "username": "deploy", "auth": { "password": "..." } } ] }

Notes:

  • Same rules as the central file: JSON Schema validated, 0600 permissions enforcedchmod 600 .ssh-mcp.json
  • Add .ssh-mcp.json to .gitignore if it contains credentials
  • Explicit --config / SSH_MCP_CONFIG disables project discovery entirely
  • Runtime state (jobs, forwards, update state) always stays in ~/.ssh-mcp/ — project dirs never accumulate runtime files

Config file format

Authentication options per server (pick one):

| Auth | Config | Best for | |---|---|---| | Password | "auth": { "password": "..." } | Quick setups, throwaway boxes | | Inline key | "auth": { "privateKey": "main-key" } (alias into keys) | Portable config, one file | | Key from your machine | "auth": { "privateKey": "~/.ssh/id_ed25519" } | Keys stay where they are; ~ supported | | SSH agent | "auth": { "agent": true } | Zero secrets in config; works with ssh-agent, 1Password, Vault, gpg-agent |

macOS Keychain note: Apple's Secure Keychain agent (SSH_AUTH_SOCK via launchd) rejects signing requests from Node's ssh2 library — if that's your only agent, use the key-file option instead. Standard agents work fine.

{
  "keys": {
    "main-key": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNza...(key content)...\n-----END OPENSSH PRIVATE KEY-----"
  },
  "servers": [
    {
      "id": "prod-web",
      "host": "192.168.1.100",
      "port": 22,
      "username": "ubuntu",
      "auth": {
        "privateKey": "main-key"
      },
      "description": "Production web server (key alias)"
    },
    {
      "id": "prod-api",
      "host": "api.example.com",
      "port": 22,
      "username": "deploy",
      "auth": {
        "privateKey": "main-key"
      },
      "description": "Production API server (same key alias)"
    },
    {
      "id": "dev-db",
      "host": "dev.example.com",
      "port": 22,
      "username": "admin",
      "auth": {
        "privateKey": "~/.ssh/id_rsa"
      },
      "agentForward": true,
      "description": "Dev database (key from file, agent forwarding enabled)"
    },
    {
      "id": "bastion",
      "host": "10.0.0.1",
      "port": 22,
      "username": "ops",
      "auth": { "agent": true },
      "description": "SSH agent auth - no secrets in this file"
    }
  ],
  "defaults": {
    "timeouts": {
      "connection": 10,
      "command": 60,
      "idle": 900
    }
  }
}

privateKey formats

The privateKey field auto-detects format:

| Format | Detection | Example | | ---------- | -------------------------------- | -------------------------------------------- | | Inline PEM | Starts with -----BEGIN | "-----BEGIN OPENSSH PRIVATE KEY-----\n..." | | Key alias | Matches a name in keys section | "main-key" | | File path | Everything else | "~/.ssh/id_rsa" |

Use the keys section to define a key once and reference it by alias across multiple servers.

Aliases can point to files too — not just inline PEM. This keeps key material out of the config file entirely while still using the short name everywhere:

{
  "keys": {
    "dublok": "/Users/you/Keys/exported_id_rsa"
  },
  "servers": [
    { "id": "neo", "host": "10.0.0.1", "port": 22, "username": "root",
      "auth": { "privateKey": "dublok" } }
  ]
}

All three styles can be mixed in one config: aliases (to paths or PEM), direct paths, and inline PEM. This is especially useful for project-level .ssh-mcp.json in private repos — the config references keys that live on your machine, so no key material is committed.

SSH Agent Forwarding

Agent forwarding lets you use your local SSH keys on remote servers (e.g., for git with private repos).

Two-level control:

| Config agentForward | Tool agentForward | Result | | --------------------- | ------------------- | ------------------ | | true (default) | true | Enabled | | true (default) | false/omitted | Disabled | | false | any | Disabled (blocked) |

  • Config level: Permission gate. Set false to block agent forwarding entirely for a server.
  • Tool level: Request flag. Pass true to enable for that command.
{
  "id": "restricted-server",
  "host": "prod.example.com",
  "username": "admin",
  "auth": { "privateKey": "~/.ssh/id_rsa" },
  "agentForward": false
}
execute(serverId, "git clone [email protected]:private/repo.git", { agentForward: true })

Auto-recreate: If you request agentForward: true but the existing shell lacks it, the shell is automatically recreated. This loses cwd and env vars. Response includes a notice when this happens:

{ "stdout": "...", "exitCode": 0, "notice": "Shell recreated with agent forwarding enabled..." }

Requirements: SSH agent running with keys loaded (ssh-add -l to verify). The SSH_AUTH_SOCK environment variable must be set.

Important: Set file permissions to 0600:

chmod 600 ~/.ssh-mcp/config.json

CLI Usage

The same binary is a standalone CLI. Running with no arguments starts the MCP server (backwards compatible); pass any command to use the CLI:

ssh-mcp                        # MCP stdio server (no args = MCP mode)
ssh-mcp mcp                    # explicit MCP mode
ssh-mcp servers                # list configured servers
ssh-mcp exec prod-web uptime   # run a command, exit code propagates
ssh-mcp exec prod-web "systemctl status nginx" --json
ssh-mcp exec prod-web "npm install" --bg          # detached background job
ssh-mcp job check <jobId>                         # poll job status/output
ssh-mcp job cancel <jobId>
ssh-mcp upload prod-web ./app.tar.gz /tmp/
ssh-mcp download prod-web /var/log/app.log ./logs/
ssh-mcp status prod-web                           # connection health (auto-connects)
ssh-mcp jump bastion prod-db "hostname"            # run via jump host
ssh-mcp forward prod-db localhost 5432             # foreground tunnel, Ctrl-C stops
ssh-mcp rforward prod-web localhost 3000           # expose local service remotely
ssh-mcp forwards                                   # list CLI-managed forwards

Notes:

  • Every command supports --json for structured output (handy for scripts and agents).
  • exec joins arguments with spaces (like ssh); quote the command as one string when it contains shell operators: ssh-mcp exec srv "sh -c 'exit 7'".
  • Background jobs are persisted under <config-dir>/jobs/ and survive across invocations; job check streams their output.
  • MCP-only tools (disconnect, get_console_history) are session-scoped and have no CLI equivalent; CLI exec is one-shot per invocation.

MCP Client Integration

Claude Desktop

Add to ~/.config/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "ssh-mcp": {
      "command": "npx",
      "args": ["-y", "ssh-mcp-cli"]
    }
  }
}

With custom config path:

{
  "mcpServers": {
    "ssh-mcp": {
      "command": "npx",
      "args": ["-y", "ssh-mcp-cli"],
      "env": {
        "SSH_MCP_CONFIG": "~/.config/claude/ssh-mcp.json"
      }
    }
  }
}

OpenCode

Add to ~/.config/opencode/opencode.json:

{
  "mcp": {
    "ssh-mcp": {
      "type": "local",
      "command": ["npx", "-y", "ssh-mcp-cli"],
      "environment": {
        "SSH_MCP_CONFIG": "~/.config/opencode/ssh-mcp.json"
      }
    }
  }
}

Restart the MCP client after configuration.

Tools

execute is the primary tool. Use it for all shell operations: ls, cat, mkdir, rm, chmod, grep, ps, file I/O, etc. Other tools exist only for operations impossible via shell commands.

| Tool | Purpose | | ---------------------- | ------------------------------------------------------- | | list_servers | List configured servers (auto-reloads config) | | disconnect | Close SSH connection | | execute | Run any shell command (auto-connects) | | execute_background | Run command in background, returns job ID | | check_job | Check background job status and output | | cancel_job | Cancel a running background job (sends SIGINT) | | get_console_history | Retrieve previous command outputs | | upload | SFTP upload (binary-safe, up to 100MB, auto-connects) | | download | SFTP download (binary-safe, up to 100MB, auto-connects) | | connection_status | Check connection health (auto-connects) | | jump_connect | Connect through jump host (auto-connects jump host) | | forward_port | Local port forward (auto-connects) | | forward_remote_port | Remote port forward (auto-connects) | | list_forwards | List active port forwards | | close_forward | Close a local port forward | | close_remote_forward | Close a remote port forward |

execute

The core tool. Runs any shell command on the remote server using a persistent shell session. Automatically connects if not already connected.

Parameters: serverId, command, stdin (optional), timeout (optional), stallTimeout (optional),
            maxOutputLength (optional), agentForward (optional)
Returns: stdout, stderr, exitCode, truncated

Auto-connect: Just call execute - the server connects automatically on first use. No need to call connect first.

State persistence: Working directory (cd) and environment variables (export) persist across multiple execute calls on the same server. This allows natural workflows like:

execute("cd /var/log")
execute("grep error app.log")   # runs in /var/log
execute("export DEBUG=1")
execute("./run-tests.sh")       # sees DEBUG=1

Shell sessions are automatically destroyed on disconnect.

Stdin support: Provide content to write to the command's stdin. Use this instead of heredocs for commands that read from stdin:

# Create a config file (replaces heredoc syntax)
execute(serverId, "cat > /etc/app.conf", { stdin: "key1=value1\nkey2=value2" })

# Execute a bash script
execute(serverId, "bash -s", { stdin: "#!/bin/bash\necho 'Hello from script'" })

# Process data with grep/awk/etc
execute(serverId, "grep -c ERROR", { stdin: logFileContent })

Stall timeout: By default, commands that produce no output for 10 seconds are considered stalled and terminated. For long-running commands (builds, package installs), pass stallTimeout: 0 to disable stall detection:

execute(serverId, "apt upgrade -y", { stallTimeout: 0 })
execute(serverId, "npm install", { stallTimeout: 0 })

Output truncation: To prevent large outputs from overwhelming MCP clients, output is truncated to maxOutputLength characters (default: 10,000). When truncated, the response includes truncated: true and a notice showing total size:

execute(serverId, "cat large-file.log", { maxOutputLength: 50000 })

Agent forwarding: Pass agentForward: true to use your local SSH agent keys on the remote server:

execute(serverId, "git clone [email protected]:private/repo.git", { agentForward: true })

See SSH Agent Forwarding for the two-level control system. If you request forwarding but the shell lacks it, it's auto-recreated (cwd/env lost).

execute_background / check_job / cancel_job

For very long-running commands, use background execution to avoid blocking:

# Start command in background
execute_background(serverId, "npm run build")
→ { jobId: "job_abc123", status: "running" }

# Poll for status - output streams in real-time
check_job(jobId)
→ { status: "running", partialOutput: "Installing dependencies...",
    bytesReceived: 1024, elapsedMs: 5000, msSinceLastOutput: 200 }

check_job(jobId)
→ { status: "running", partialOutput: "Installing dependencies...\nBuilding...",
    bytesReceived: 2048, elapsedMs: 10000, msSinceLastOutput: 150 }

# Final result when complete
check_job(jobId)
→ { status: "completed", result: { stdout, exitCode }, durationMs: 15000 }

# Cancel if needed
cancel_job(jobId)
→ { status: "cancelled", interruptSent: true }

Real-time streaming: Output is available immediately as commands produce it - no waiting for completion. Progress indicators help track long-running jobs:

| Field | Description | | ------------------- | ---------------------------------------------- | | partialOutput | Output received so far (streams in) | | bytesReceived | Total bytes of output received | | elapsedMs | Time since job started | | msSinceLastOutput | Time since last output chunk (stall detection) |

Background jobs run independently. Poll check_job to monitor progress and retrieve output.

get_console_history

Retrieve previous command outputs from the shell session. Useful for reviewing what happened or checking outputs from earlier commands.

Parameters: serverId, limit (optional, default: all)
Returns: Array of { timestamp, command, stdout, exitCode, durationMs }

Limits: 100 entries max per server, 50KB max per output (truncated if larger). History is cleared on disconnect.

upload / download

Use only for binary files or files >10MB. For text files, prefer execute with cat/echo.

Parameters: serverId, localPath, remotePath
Limits: 100MB max

list_servers

Lists all configured servers with their connection status. Automatically reloads config from disk on each call, so you can edit ~/.ssh-mcp/config.json and see changes immediately without restarting.

Returns: Array of { id, host, port, username, connected, description? }

disconnect / connection_status

Connection lifecycle management. Protocol-level operations that can't be done via shell. Note that connection_status will auto-connect if not already connected.

Troubleshooting

"Config file not found"

A template config is auto-generated at the specified path. Edit it with your servers and restart.

"Insecure file permissions"

Run chmod 600 <config-path> to restrict access. This check is skipped on Windows.

"Authentication failed"

  • For key auth: Verify key path and permissions (0600)
  • For password auth: Check credentials
  • Verify the username is correct

"Connection timeout"

  • Check network connectivity to the host
  • Verify the host and port are correct
  • Check firewall rules

"Command timeout"

Increase the timeout in config or pass timeout parameter to execute.

"Command stalled"

Commands that produce no output for 10 seconds are terminated with a stall error. For slow commands (builds, package managers), disable stall detection:

execute(serverId, "npm install", { stallTimeout: 0 })

Or use background execution for very long commands:

execute_background(serverId, "npm run build")
check_job(jobId)  # Poll for completion

Connection drops frequently

The server uses keep-alive (30s interval) and auto-reconnection (5 attempts with exponential backoff). If issues persist, check network stability.

Security

See SECURITY.md for:

  • Threat model
  • Credential handling best practices
  • Error sanitization rules

Key points:

  • Config file requires 0600 permissions (Linux/macOS only)
  • Credentials never appear in logs or error messages
  • SSH keys are recommended over passwords

Development

# Install dependencies
npm install

# Run unit tests
npm test

# Run E2E tests (requires Docker - handles setup/cleanup automatically)
npm run test:e2e              # Parallel with 8 shards (default)
npm run test:e2e:sequential   # Single shard for debugging
SHARDS=4 npm run test:e2e     # Custom shard count

# Run all tests
npm run test:all

# Build
npm run build

# Lint & typecheck
npm run lint
npm run typecheck

Parallel E2E Tests

E2E tests run in parallel by default (8 shards). Each shard gets its own Docker Compose project with unique ports:

| Shard | ssh-server-1 | ssh-server-2 | ssh-server-key | | ----- | ------------ | ------------ | -------------- | | 0 | 2222 | 2223 | 2224 | | 1 | 3222 | 3223 | 3224 | | ... | ... | ... | ... |

| Shards | Docker | Health | Tests | Cleanup | Total | | ------ | ------ | ------ | ----- | ------- | ----- | | 1 | 0s | 0s | 103s | 4s | 107s | | 4 | 2s | 1s | 38s | 5s | 46s | | 8 | 5s | 2s | 27s | 8s | 42s | | 16 | 16s | 2s | 27s | 12s | 57s |

Customize shard count: SHARDS=4 npm run test:e2e or SHARDS=1 npm run test:e2e for debugging.

License

MIT