Ssh Live
FreeNot checkedInteractive, streaming SSH tool for LLM agents that enables spawning long-running remote commands with live line-by-line output, signal handling, stdin input, a
About
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.
README
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.pyon a remote box, get ajob_idin <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_signalwhen the user wants to stop. - Feed
sudopasswords or REPL input viassh_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-channelsend_signalis ignored by the server (which is the common case). - Ring-buffered output (default 10 000 lines per stream) with condition-variable-backed blocking
wait_mssossh_tailis 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>.logso 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
- Open the Agent Panel (right sidebar).
- Click
···(top-right of the panel) → Settings → MCP Servers. - Find
ssh-liveand toggle it ON. - The indicator next to
ssh-liveshould turn green within a few seconds.
If it stays red — check
Zed.log: Command Palette →zed: open logand look forssh-live. The most common fix: thecommandpath in settings is wrong. Runwhere python(Windows) orwhich 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
···. Runtail -f /var/log/syslogfor 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
···. UploadC:/Users/me/builds/app-1.0.zipto/opt/app/releases/app-1.0.zipon 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.jsto/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.gzto/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— userdeploy, password···. Save SSH connection to 5.6.7.8 asdeploy_production— userdeploy, 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
./deploydirectory to/srv/app/ondeploy_test. Run/srv/app/healthcheck.shafterwards 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
./deploydirectory to/srv/app/ondeploy_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 inps). - 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_hostsnever 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-liveruns 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
- Quick start — install, register, run in 4 steps (this file).
- SPEC.md — full technical specification, all tool contracts with JSON shapes.
- docs/CONFIG.md — complete CLI + TOML reference.
- docs/USAGE.md — real-world examples per tool.
- docs/TROUBLESHOOTING.md — common failure modes with fixes.
- docs/DEVELOPMENT.md — how to hack on the server itself.
- CHANGELOG.md — release history.
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:
pip install -e ".[dev]"pytest— all 68 tests must pass.ruff check .andblack --check .for style.- If the change touches a tool schema, update
SPEC.mdfirst 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 | ❌ |
Install Ssh Live in Claude Desktop, Claude Code & Cursor
unyly install mcp-ssh-liveInstalls into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.
First time? Get the CLI: curl -fsSL https://unyly.org/install | sh
Or configure manually
Run in your terminal:
claude mcp add mcp-ssh-live -- uvx mcp-ssh-liveFAQ
Is Ssh Live MCP free?
Yes, Ssh Live MCP is free — one-click install via Unyly at no cost.
Does Ssh Live need an API key?
No, Ssh Live runs without API keys or environment variables.
Is Ssh Live hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Ssh Live in Claude Desktop, Claude Code or Cursor?
Open Ssh Live on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
Fetch
Web content fetching and conversion for efficient LLM usage.
AWS KB Retrieval
Retrieval from AWS Knowledge Base using Bedrock Agent Runtime.
by modelcontextprotocolSpring AI MCP Server
Provides auto-configuration for setting up an MCP server in Spring Boot applications.
llm-analysis-assistant
A very streamlined mcp client that supports calling and monitoring stdio/sse/streamableHttp, and can also view request responses through the /logs page. It also
by xuzexin-hzCompare Ssh Live with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All ai MCPs
