mcp-ssh-live

mcp-ssh-live

Interactive, streaming SSH tool for LLM agents that enables spawning long-running remote commands with live line-by-line output, signal handling, stdin input, and SFTP file transfer.

Category
Visit Server

README

mcp-ssh-live

Interactive, streaming SSH tool for LLM agents via MCP (Model Context Protocol).

Lets LLM agents (Claude, Cursor, Zed) spawn long-running remote commands and watch their output arrive line-by-line in chat — instead of blocking for hours waiting for a ssh_exec to return.

┌─────────────┐   JSON-RPC/stdio    ┌──────────────────┐        SSH        ┌─────────────┐
│  Zed /      │ ────────────────── │  mcp-ssh-live    │ ───────────────── │ your remote │
│  Claude /   │                     │  (Python)        │                   │  server(s)  │
│  Cursor     │                     │                  │                   │             │
└─────────────┘                     └──────────────────┘                   └─────────────┘
                                      ^               ^
                                      |               |
                                 ring buffers     SFTP + exec
                                 + reader threads

Why this exists

Other SSH-MCP servers (tufantunc/ssh-mcp, classfang/ssh-mcp-server, AiondaDotCom/mcp-ssh, …) are all request/response: send a command, wait for it to finish, get the full output. Long jobs — parsers, builds, deploys, log tails — are unusable. The LLM either blocks past the MCP client's 60 s timeout, or falls back to nohup … & + tail -n 10 log polling that loses lines and has no kill-signal story.

mcp-ssh-live splits the one SSH primitive into spawn + tail + signal + stdin, so an agent can:

  • Start python parser.py on a remote box, get a job_id in <100 ms.
  • Poll ssh_tail(job_id, since_line_no, wait_ms=5000) in a loop and stream output into the chat live.
  • Send SIGTERM / SIGKILL / SIGINT via ssh_signal when the user wants to stop.
  • Feed sudo passwords or REPL input via ssh_send_stdin.
  • Upload / download files via SFTP (ssh_upload / ssh_download) — binary-safe, with sha256 on both sides.
  • Manage jobs across multiple hosts simultaneously.

Features

  • 10 MCP tools covering synchronous exec, streaming spawn+tail, signals, stdin, SFTP, host/job registry management.
  • OpenSSH-compatible signal delivery. Captures the remote PID at spawn time, falls back to kill -SIG <pid> on a fresh exec channel when paramiko's in-channel send_signal is ignored by the server (which is the common case).
  • Ring-buffered output (default 10 000 lines per stream) with condition-variable-backed blocking wait_ms so ssh_tail is near-live without busy-polling.
  • Multi-host: one process manages several SSH targets; each tool call can pick a host by alias.
  • Auto-reconnect on transient network / sshd hiccups (configurable retries + delay).
  • Graceful shutdown: TERM → 5 s grace → KILL for every running remote process when the server exits.
  • Opt-in disk log mirror (--log-dir) — every streamed line also written to <log_dir>/<job_id>.log so full output survives ring-buffer eviction and server restarts.
  • Secrets never leak into MCP responses: passwords live in env vars, only variable names appear in ssh_list_hosts.

Quick start

1. Install

pipx install mcp-ssh-live

Or, for a project-local editable install:

python -m venv .venv
source .venv/bin/activate         # Windows: .venv\Scripts\activate
pip install -e .

Requires Python 3.10+.

2. Try it from the command line

# Start the MCP server — no SSH credentials needed at startup
mcp-ssh-live

The server starts empty and waits for an MCP client to connect. Credentials are provided at runtime by the agent via ssh_connect (see step 4). If you prefer to pre-configure a host at startup, you can still pass --host, --user, --password-env flags — see Configuration.

3. Register with an MCP client

Zed

Edit ~/.config/zed/settings.json (or .zed/settings.json for per-project).

Windows (use the Python executable directly — Zed doesn't pick up PATH the same way as a terminal):

Find your Python path: open a terminal and run where python.

{
  "context_servers": {
    "ssh-live": {
      "enabled": true,
      "command": "C:/path/to/python.exe",
      "args": ["-m", "mcp_ssh_live"],
      "env": {
        "FASTMCP_DISABLE_VERSION_CHECK": "1",
        "PYTHONUNBUFFERED": "1"
      },
      "settings": {}
    }
  },
  "agent": {
    "always_allow_tool_actions": true,
    "always_allowed_tools": {
      "ssh_connect": true, "ssh_disconnect": true,
      "ssh_exec": true, "ssh_spawn": true, "ssh_tail": true,
      "ssh_signal": true, "ssh_send_stdin": true,
      "ssh_list_jobs": true, "ssh_remove_job": true,
      "ssh_upload": true, "ssh_download": true, "ssh_list_hosts": true
    }
  }
}

macOS / Linux (if installed via pipx — binary is in PATH):

{
  "context_servers": {
    "ssh-live": {
      "source": "custom",
      "enabled": true,
      "command": "mcp-ssh-live",
      "args": [],
      "env": {
        "FASTMCP_DISABLE_VERSION_CHECK": "1",
        "PYTHONUNBUFFERED": "1"
      }
    }
  },
  "agent": {
    "always_allow_tool_actions": true,
    "always_allowed_tools": {
      "ssh_connect": true, "ssh_disconnect": true,
      "ssh_exec": true, "ssh_spawn": true, "ssh_tail": true,
      "ssh_signal": true, "ssh_send_stdin": true,
      "ssh_list_jobs": true, "ssh_remove_job": true,
      "ssh_upload": true, "ssh_download": true, "ssh_list_hosts": true
    }
  }
}

Save the file. No restart needed in most cases, but if the indicator stays red — close and reopen Zed.

3b. Enable in the Agent Panel

  1. Open the Agent Panel (right sidebar).
  2. Click ··· (top-right of the panel) → SettingsMCP Servers.
  3. Find ssh-live and toggle it ON.
  4. The indicator next to ssh-live should turn green within a few seconds.

If it stays red — check Zed.log: Command Palettezed: open log and look for ssh-live. The most common fix: the command path in settings is wrong. Run where python (Windows) or which python (macOS/Linux) to get the correct path.

There's a full config template at .zed/settings.json.example.

Claude Desktop

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

{
  "mcpServers": {
    "ssh-live": {
      "command": "mcp-ssh-live",
      "args": [],
      "env": { "FASTMCP_DISABLE_VERSION_CHECK": "1" }
    }
  }
}

Cursor

~/.cursor/mcp.json — same shape as Claude Desktop.

4. Ask your agent

Once registered, just tell the agent your SSH details in chat. It calls ssh_connect(...) automatically — credentials never touch the config file.

Stream a live log:

Connect to 1.2.3.4 as root, password is ···. Run tail -f /var/log/syslog for 30 seconds, then stop.

The agent picks ssh_spawn → loops ssh_tail(wait_ms=5000)ssh_signal("TERM")ssh_remove_job, streaming lines into the chat the whole time.

Upload a local file to the server:

Connect to 1.2.3.4 as root, password is ···. Upload C:/Users/me/builds/app-1.0.zip to /opt/app/releases/app-1.0.zip on the server.

The agent calls ssh_connect(...) then ssh_upload(local_path="C:/Users/me/builds/app-1.0.zip", remote_path="/opt/app/releases/app-1.0.zip") — binary-safe, atomic, with sha256 verification printed in the reply.

Key auth instead of password:

Connect to 1.2.3.4 as deploy using key ~/.ssh/id_ed25519. Upload ./dist/bundle.js to /var/www/html/bundle.js.

Multiple servers at once:

Connect to prod at 1.2.3.4 and stage at 10.0.0.5 (both as root, password ···). Deploy ./release.tar.gz to /srv/app/ on both.

Staged deploy — save credentials once, deploy step by step:

First, register both servers by alias so you don't repeat credentials every time:

Save SSH connection to 1.2.3.4 as deploy_test — user deploy, password ···. Save SSH connection to 5.6.7.8 as deploy_production — user deploy, password ···.

The agent calls ssh_connect(host="1.2.3.4", user="deploy", password="...", alias="deploy_test") and the same for deploy_production. Both aliases stay active for the rest of the session.

Then deploy to test first:

Upload everything from the local ./deploy directory to /srv/app/ on deploy_test. Run /srv/app/healthcheck.sh afterwards and show me the output.

The agent calls ssh_upload for each file in ./deploy, then ssh_exec(command="/srv/app/healthcheck.sh", host="deploy_test") and streams the result.

If the output looks good, deploy to production:

Looks good. Now upload the same ./deploy directory to /srv/app/ on deploy_production.

The agent reuses the saved deploy_production alias — no need to re-enter credentials — and repeats the upload.


Tools

Tool What it does
ssh_connect(host, user?, password?, key_path?, port?, alias?, insecure_auto_add?) Add and open an SSH connection at runtime. Returns an alias used by all other tools. Credentials never go into config files.
ssh_disconnect(alias) Close a connection and remove it from the registry.
ssh_exec(command, host?, timeout_ms?, cwd?, env?) Run a short command synchronously; returns full stdout/stderr/exit_status. Timeout triggers SIGTERM → SIGKILL.
ssh_spawn(command, host?, pty?, label?, cwd?, env?) Start a background job, return job_id in <100 ms.
ssh_tail(job_id, since_line_no, wait_ms, max_lines, stream) Stream new lines incrementally. Blocks up to wait_ms for new output. Returns still_running, exit_status, buffer_truncated.
ssh_signal(job_id, signal, wait_ms?) Send TERM/KILL/INT/… via paramiko + kill -SIG <pid> fallback. Optionally waits for exit.
ssh_send_stdin(job_id, data, newline?, close_stdin?) Write to a running job's stdin — sudo passwords, REPL input.
ssh_list_jobs(host?, include_finished?) List every job the server knows about: id, cmd, label, status, exit_status, line counts.
ssh_remove_job(job_id, force?) Drop a job from the registry. Running jobs need force=True (use ssh_signal first for a clean kill).
ssh_run_persistent(command, host?, cwd?, env?, label?, work_dir?) Run a command via nohup — survives MCP disconnect. Returns PID + log paths.
ssh_persistent_status(pid, out_log, err_log, host?, tail_lines?) Check status and tail output of a persistent job.
ssh_upload(local_path, remote_path, host?, mode?, max_bytes?) SFTP upload, atomic .partial + rename, auto-mkdir parents, scp-style dir target, sha256 on both sides.
ssh_download(remote_path, local_path, host?, max_bytes?) SFTP download, atomic .partial + rename, size cap enforced pre- and during-transfer.
ssh_list_hosts() Enumerate configured hosts (alias, address, auth method, connected, active_jobs, is_default). Never leaks passwords.

Full JSON schemas and examples: SPEC.md.


Configuration

Credential-free mode (recommended)

Start the server with no SSH arguments. The agent receives credentials from the user in chat and connects via ssh_connect:

mcp-ssh-live

No credentials in any config file. The agent connects on demand:

> "Connect to 1.2.3.4 as root, password is …"
Agent → ssh_connect(host="1.2.3.4", user="root", password="…")
Agent → ssh_exec(command="hostname")

Pre-configured hosts (optional)

If you prefer hosts to be available immediately without an ssh_connect call, pass them via CLI flags or a TOML file. Credentials go into env vars — never on the command line.

export SSH_PASSWORD='your-password'
mcp-ssh-live \
  --host prod=1.2.3.4:22 \
  --host stage=10.0.0.5 \
  --user root \
  --password-env SSH_PASSWORD \
  --default-host prod \
  --insecure-auto-add \
  --log-dir ~/.cache/mcp-ssh-live/logs \
  --log-level INFO

Run mcp-ssh-live --help for the full flag list.

TOML config file

~/.config/mcp-ssh-live/config.toml:

default_host = "prod"

[hosts.prod]
host = "1.2.3.4"
port = 22
user = "root"
password_env = "PROD_PASS"

[hosts.stage]
host = "10.0.0.5"
user = "deploy"
key = "~/.ssh/stage_key"

[limits]
ring_buffer_lines = 10000
max_jobs_per_host = 50
reap_finished_after_sec = 600
reconnect_retries = 3
reconnect_delay_sec = 2.0

[server]
insecure_auto_add = false
known_hosts = "~/.ssh/known_hosts"
log_dir = "~/.cache/mcp-ssh-live/logs"

Full reference: docs/CONFIG.md.

Auth

  • Password: --password-env SSH_PASSWORD — name of the env var. Never pass passwords on the command line (visible in ps).
  • Key: --key ~/.ssh/id_ed25519. Optional passphrase via --key-passphrase-env NAME.
  • Agent: omit both — paramiko will try the SSH agent and default key locations.

Common gotchas

"I ran python parser.py and see no output for minutes"

Python (and many other programs) block-buffer stdout when stdin is not a TTY. The output is fine — it's sitting in a buffer on the remote side. Three fixes, pick one:

# Option 1: pty=True
ssh_spawn(command="python parser.py", pty=True)

# Option 2: unbuffered Python
ssh_spawn(command="python -u parser.py")

# Option 3: env var
ssh_spawn(command="python parser.py", env={"PYTHONUNBUFFERED": "1"})

For non-Python tools: stdbuf -oL <cmd> or unbuffer <cmd> (from expect).

"ssh_signal returns sent=True but the job keeps running"

OpenSSH's sshd typically ignores channel.send_signal (the RFC 4254 in-channel way). mcp-ssh-live falls back to kill -<SIG> <pid> on a fresh exec channel, using the PID captured from the PID=<n> wrapper at spawn time. The response tells you which path worked:

{"sent_via_channel": false, "sent_via_pid": true, ...}

If both are false, the spawn wrapper couldn't capture a PID (extremely rare — only happens if capture_pid=False was forced or the remote shell is very non-standard).

"Host key verification failed"

By default mcp-ssh-live respects ~/.ssh/known_hosts and refuses unknown keys. For the first connection either:

  • SSH into the host once by hand so the key is trusted, or
  • Pass --insecure-auto-add (INSECURE — vulnerable to MITM on first connect), or
  • Point at a custom file via --known-hosts /path/to/file.

"Tool calls hang for 60 seconds then error out"

If you see csp request task for "initialize" took over 60s in Zed's log: FastMCP tries to fetch its latest version from PyPI on startup. On air-gapped networks or slow DNS, this can time out. Fix with the env var:

FASTMCP_DISABLE_VERSION_CHECK=1

More in docs/TROUBLESHOOTING.md.


Security

  • Passwords pass through env vars only, never command-line arguments (which would show up in ps).
  • ssh_list_hosts never returns the password itself — only the env var name and the key file path.
  • Known-hosts is enforced by default.
  • Sandboxing is at the MCP client level: whichever client you use (Zed, Claude Desktop, Cursor) is the thing asking the user to approve each tool call. Once approved, mcp-ssh-live runs whatever the LLM supplied verbatim — there is no command-level sanitization. This is the whole point of the tool.
  • The LLM gets a shell on your server. If that bothers you, use a dedicated restricted user, jailed with ForceCommand / rbash / containers.

Documentation


Project status

v0.1.0 — phases 1-5 complete.

Phase Feature Status
1 Skeleton + ssh_exec
2 Jobs + streaming tail
3 Signals + stdin
4 SFTP + multi-host
5 Auto-reconnect + graceful shutdown + disk log mirror
6 Docs + CI + PyPI publish in progress

68 unit tests. Acceptance-tested end-to-end against a real Ubuntu 24.04 sshd: streaming, signals, SFTP round-trip, disk mirror, multi-host.


Contributing

Design feedback on SPEC.md and docs/DEVELOPMENT.md is always welcome. Before opening a code PR:

  1. pip install -e ".[dev]"
  2. pytest — all 68 tests must pass.
  3. ruff check . and black --check . for style.
  4. If the change touches a tool schema, update SPEC.md first and explain why in the PR.

License

MIT. See LICENSE.


Related projects

Reviewed before building this; all of them are request/response only, which is why this project exists:

Project Stars Language Streaming?
tufantunc/ssh-mcp 392 TS
classfang/ssh-mcp-server 332 TS
bvisible/mcp-ssh-manager 159 JS
AiondaDotCom/mcp-ssh 63 JS

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured