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
访问服务器

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

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选