ssh_cmd_run
Execute a command on a remote SSH host and block until it completes or a timeout fires. On timeout, keep monitoring in the background instead of rerunning the command.
Instructions
Execute a command on the remote host and BLOCK until it completes, an io_timeout (silence), a wait_timeout (elapsed cap), or a runtime_timeout (hard cap) occurs.
Timeout semantics (read this before choosing values):
io_timeout and wait_timeout firing do NOT mean the remote command stopped - it genuinely keeps running. Monitoring is handed off to a background thread so output/exit code continue to be collected; the response has status='io_timeout'/'wait_timeout', still_running=true, and an id/pid you can use with ssh_cmd_check_status(handle_id=...) to poll again later, or ssh_cmd_output(handle_id=...) to read output collected so far (including output produced after this call returned). Do not rerun the command. You can also still decide to end it early with ssh_cmd_kill(handle_id=...) at any point after either of these fires.
io_timeout fires only on SILENCE (no output for N seconds) - a command that keeps producing output never triggers it, however long it runs.
wait_timeout fires after N seconds of TOTAL elapsed wait, regardless of activity - use this if you want to check in periodically on a long-running command even while it's actively producing output, rather than being blocked until completion.
runtime_timeout is the only knob that DOES attempt to terminate the remote command - a hard safety ceiling, not a UX mechanism. Set it generously (much longer than the command should ever realistically take).
A single call never blocks longer than the server's wait cap (default 50s, configurable via --max-wait / MCP_SSH_MAX_WAIT), because most MCP clients abort a tool call at ~60s and the id/pid handoff would be lost. Longer waits are clamped: the response is a normal status='wait_timeout' handoff with wait_capped=true. For anything that may take more than about a minute, ssh_task_launch is usually the better tool.
For commands that should survive you disconnecting/reconnecting entirely (not just this call returning), use ssh_task_launch instead - it runs fully detached from this SSH session, whereas a command started here (even after surviving io_timeout/wait_timeout) is still tied to the current connection's lifetime.
You can access command history using 'ssh_cmd_history' to see previous commands and their output. Commands run this way always appear there with origin='user' - pass include_internal=False to ssh_cmd_history to hide unrelated internal plumbing from other tools (e.g. ssh_file_write's sudo dance).
Windows targets: commands run under cmd.exe (CMD syntax), not PowerShell. For PowerShell, run it explicitly: powershell -NoProfile -Command "...".
Working directory: each call is an independent remote process (like a GitHub Actions step or Ansible task, not a continuous shell) - nothing is remembered between calls, including 'cd'. Running ssh_cmd_run("cd /var/log") does NOT affect a later ssh_cmd_run("ls"); it will still list the login directory. Use absolute paths, chain "cd dir && command" within one call, or pass the cwd parameter to run this specific call in a specific directory (Linux/macOS only for now).
Returns:
Dictionary containing command output, status, and metadata. The handle
identifier is returned here as 'id', but every other tool that accepts it
(ssh_cmd_check_status, ssh_cmd_kill, ssh_cmd_output) names the same parameter
handle_id - pass this value there. output (stdout) and stderr are always
two SEPARATE fields, never interleaved into one combined stream - a command
that succeeds can still have written to stderr (warnings, progress meters,
non-fatal messages), so check stderr even on status='success'.
Each stream is returned inline up to its most recent ~32 KB. `output_truncated`
and `stderr_truncated` are always present; when true, the response also has
line counts and an `output_note` saying how to read the rest with
ssh_cmd_output(handle_id=..., start_line=...), or that the earliest lines were
dropped (the server keeps ~2 MB per stream per command). For very large output,
redirect it to a file and read that instead.
`status` is one of:
- 'success': command completed with exit code 0. `exit_code`, `output`, `stderr` populated.
- 'command_failed': completed with a non-zero exit code. `exit_code`, `output`, `stderr` populated.
- 'cwd_not_found': the `cwd` parameter didn't exist on the remote host - the
command was NOT executed at all (fails closed).
- 'io_timeout': no output within `io_timeout` seconds - remote command was NOT
killed, still_running=true. Poll with ssh_cmd_check_status(handle_id=...).
- 'wait_timeout': `wait_timeout` elapsed regardless of activity - remote command was
NOT killed, still_running=true. Poll with ssh_cmd_check_status(handle_id=...).
- 'runtime_timeout': `runtime_timeout` exceeded - an attempt was made to kill
the remote command (see ssh_cmd_check_status's `'killed'` status to confirm).
- 'sudo_required': `use_sudo=True` but elevation isn't available (see
ssh_conn_verify_sudo before retrying).
- 'busy': another ssh_cmd_run is still WAITING in the foreground on this
connection - only one call can wait at a time. A command that was already
handed off (io_timeout/wait_timeout) keeps running in the background and does
NOT block new ssh_cmd_run calls; several remote commands can be running at once.
- 'error': unexpected failure (e.g. connection dropped).Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| cwd | No | Run the command in this directory, for this call only (Linux/macOS only - not yet supported on Windows). Nothing is remembered between calls: each ssh_cmd_run is an independent process, so pass cwd again on every call where it matters, or chain 'cd dir && command' yourself. Fails closed - if the directory doesn't exist, the command is never executed at all (status='cwd_not_found'), so there's no ambiguity about where anything ran. | |
| command | Yes | Command to execute on remote host | |
| use_sudo | No | Run command with sudo | |
| io_timeout | No | Max seconds of SILENCE (no output) before giving up on waiting. Does NOT kill the remote command - hands off to background monitoring and returns control to you. Note a single call never blocks longer than the server's wait cap (default 50s, since most MCP clients abort tool calls at ~60s) - raising this above that only matters for how long silence is tolerated within the cap. For package installs, Docker pulls, large downloads or compilation, prefer ssh_task_launch. If hit, call ssh_cmd_check_status or ssh_cmd_output to check back rather than rerunning. | |
| wait_timeout | No | Max seconds to wait in THIS call, regardless of output activity - unlike io_timeout, fires even if the command is actively producing output. Does NOT kill the remote command, same non-destructive handoff as io_timeout. Use this when you want to check in periodically on a command that's chatty but long-running (e.g. a verbose Docker pull), rather than being blocked until it finishes or goes quiet. Values above the server's wait cap (default 50s) are clamped to it - the response then has wait_capped=true. | |
| runtime_timeout | No | Total wall-clock cap in seconds, regardless of output activity. Unlike io_timeout/wait_timeout, hitting this DOES attempt to kill the remote command - it's the only hard ceiling. Set generously for long operations (installs/downloads can need 10-60+ minutes) - this should be a safety net, not a UX mechanism. |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||