mcp-ssh-live
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-ssh-livetail the nginx access log on production"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 threadsWhy 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-liveOr, 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-liveThe 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 |
| Add and open an SSH connection at runtime. Returns an |
| Close a connection and remove it from the registry. |
| Run a short command synchronously; returns full stdout/stderr/exit_status. Timeout triggers SIGTERM → SIGKILL. |
| Start a background job, return |
| Stream new lines incrementally. Blocks up to |
| Send TERM/KILL/INT/… via paramiko + |
| Write to a running job's stdin — sudo passwords, REPL input. |
| List every job the server knows about: id, cmd, label, status, exit_status, line counts. |
| Drop a job from the registry. Running jobs need |
| Run a command via |
| Check status and tail output of a persistent job. |
| SFTP upload, atomic |
| SFTP download, atomic |
| 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-liveNo 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 INFORun 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), orPoint 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=1More 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 + | ✅ |
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? |
392 | TS | ❌ | |
332 | TS | ❌ | |
159 | JS | ❌ | |
63 | JS | ❌ |
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/pmboxbiz/mcp-ssh-live'
If you have feedback or need assistance with the MCP directory API, please join our Discord server