Command Execution Guide¶
Overview¶
The SSH MCP Server provides robust command execution capabilities with timeout handling, output management, and process control. This guide covers the command execution tools and their usage patterns.
Core Tools¶
ssh_cmd_run (Primary Tool)¶
The main tool for executing commands on the remote host.
Features: - Executes commands with configurable timeouts - Captures output with status metadata - Supports sudo elevation - Tracks execution in command history
Example:
ssh_cmd_run(
command="ls -la /var/log",
io_timeout=30.0,
runtime_timeout=60.0,
use_sudo=False
)
Three independent timeout knobs (see Timeout Management
below): io_timeout (silence-based), wait_timeout (elapsed-based, regardless of
activity), and runtime_timeout (the only one that kills). All three are checked
concurrently - whichever of io_timeout/wait_timeout fires first hands back
control without touching the remote command; runtime_timeout always wins outright
if it fires, and kills.
Supporting Tools¶
| Tool | Purpose |
|---|---|
ssh_cmd_check_status |
Monitor running command status |
ssh_cmd_kill |
Terminate running commands |
ssh_cmd_output |
Retrieve command output |
ssh_cmd_history |
Access execution records |
ssh_cmd_clear_history |
Clear command history |
Timeout Management¶
Three independent knobs, all checked concurrently on every poll iteration. Two are "soft" (never kill the remote command, just hand back control); one is "hard" (the only one that ever kills).
I/O Timeout (io_timeout) - soft, silence-based¶
- An inactivity timeout, not a total command timeout - it only measures silence since the last output
- Triggers when no output is received within the timeout period. A command that
keeps producing output, however slowly, never triggers this - see
wait_timeoutbelow if you want a cap that applies regardless of activity - Does NOT kill the remote command. Monitoring is handed off to a background
thread that keeps collecting output and watching for the real exit code, so the
command genuinely keeps running and finishing normally is still fully observable
later. The response has
status='io_timeout',still_running=True, and anid/pidto check back with viassh_cmd_check_status/ssh_cmd_output- or to end it early withssh_cmd_killif you decide you don't want to wait for it - Default: 60 seconds
- Use for: Commands that should produce regular output
Wait Timeout (wait_timeout) - soft, elapsed-based¶
- A total elapsed wait cap, regardless of output activity - unlike
io_timeout, this fires even while the command is actively producing output - Same non-killing handoff behavior as
io_timeout: the command keeps running in the background, andssh_cmd_check_status/ssh_cmd_output/ssh_cmd_killall work on it exactly the same way afterward - Default:
None(disabled) - onlyio_timeout/runtime_timeoutapply unless you set this explicitly - Use for: Checking in periodically on a command that's chatty but long-running
(e.g. a
docker pullwith a constantly-updating progress bar, which would never go quiet enough to triggerio_timeout) - lets you decide whether to keep waiting, do something else, or kill it, rather than being blocked until it actually finishes
Runtime Timeout (runtime_timeout) - hard, the only one that kills¶
- Limits total execution time regardless of output activity
- Hard stop - firing this DOES attempt to kill the remote command, on Linux,
macOS, and Windows (non-sudo commands; sudo'd commands are the one remaining gap -
see
docs_internal/CMD-EXECUTION-MODEL.mdfor current coverage details). This still applies even to a command that's already been handed off to background monitoring after anio_timeout/wait_timeout- it's enforced there too, with an internal safety ceiling applied automatically if you never set one, so a background-monitored command can never run completely unbounded - Default: None (no limit) - set this generously (much longer than the command should ever realistically take); it's a safety net, not a UX mechanism
- Use for: Preventing runaway processes
Timeout Priority¶
- All three are checked every poll iteration; whichever of
io_timeout/wait_timeouttriggers first ends the local wait and hands off to background monitoring - neither kills anything runtime_timeoutis the one to rely on for an actual cap, since it's the only one that kills the remote process - this holds true both before and after a handoff to background monitoring- A background thread continues monitoring wall-clock duration and output after a
soft timeout fires, so
ssh_cmd_check_status/ssh_cmd_outputcontinue to reflect reality (including the real exit code, once the command actually finishes) rather than going stale the moment this tool call returns
Execution Flow¶
┌─────────────────┐
│ ssh_cmd_run() │
└────────┬────────┘
│
┌────────▼────────┐
│ Command starts │
│ (Handle ID │
│ assigned) │
└────────┬────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌───────▼───────┐ ┌──────▼──────┐ ┌───────▼───────┐
│ Success │ │ Timeout │ │ Error │
│ (exit code 0) │ │ (I/O, wait, │ │ (non-zero │
│ │ │ or runtime)│ │ exit code) │
└───────┬───────┘ └──────┬──────┘ └───────┬───────┘
│ │ │
└────────────────┼────────────────┘
│
┌────────▼────────┐
│ Store in │
│ command history │
└────────┬────────┘
│
┌────────▼────────┐
│ Return result │
│ dictionary │
└─────────────────┘
Status Codes¶
ssh_cmd_run response status field¶
| Status | Description | Exit Code |
|---|---|---|
success |
Command completed | 0 |
command_failed |
Command exited with error | Non-zero |
cwd_not_found |
The cwd parameter didn't exist on the remote host - the command was not executed |
N/A |
io_timeout |
No output within io_timeout - remote command was not killed, genuinely still running (background monitoring keeps collecting it) |
N/A |
wait_timeout |
wait_timeout elapsed regardless of activity - remote command was not killed, genuinely still running (same as io_timeout) |
N/A |
runtime_timeout |
runtime_timeout exceeded - an attempt was made to kill the remote command |
N/A |
sudo_required |
Sudo access needed but not available | N/A |
busy |
Another ssh_cmd_run is already in flight on this connection |
N/A |
error |
Unexpected failure (e.g. connection dropped) | N/A |
ssh_cmd_check_status response status field¶
| Status | Description |
|---|---|
completed |
Confirmed finished, exit_code is populated - including for a command that survived an io_timeout/wait_timeout, since background monitoring keeps watching for the real exit code |
running |
Still being actively monitored, including a command that's past an io_timeout/wait_timeout but hasn't finished yet - background monitoring has definite knowledge it's still going, not just a guess |
killed |
The remote process was confirmed terminated (e.g. runtime_timeout killed it, or a prior ssh_cmd_kill call found it already gone) - exit_code is not known, but treat this as terminal, same as completed |
completed_exit_code_unknown |
Rare fallback: monitoring stopped without a confirmed exit code (e.g. an unexpected error/lost connection during background monitoring) and a live check now confirms the remote process is no longer running - terminal, but the real exit code was never observed and cannot be recovered |
unknown_still_running |
Rare fallback (same caveat as completed_exit_code_unknown) - a live check confirms the remote command is still actually running - not a failure, call this tool again to keep checking |
not_found |
The handle_id doesn't exist (handles don't survive reconnects) |
ssh_cmd_kill response result field¶
Note this is a separate field (result, not status) on the ssh_cmd_kill response.
| Result | Description |
|---|---|
killed |
Process confirmed exited after the signal (or force-kill fallback) |
not_running |
Command was already not running when kill was attempted |
already_exited |
Process had already exited before the signal was sent |
failed_to_kill |
Process still running after signal (and force-kill, if attempted) |
invalid_pid |
The tracked PID was not a real value |
error |
Unexpected failure while attempting the kill |
Output Management¶
Circular Buffer¶
- stdout and stderr are captured into separate in-memory buffers with tail preservation - never interleaved into one combined stream
- Default: 100 lines retained per stream
- Streaming capture with line normalization
ssh_cmd_run's own response only ever includesoutput(stdout) andstderras two distinct fields - a command that succeeds can still have written to stderr (warnings, progress meters, non-fatal messages), so checkstderreven onstatus='success'
Retrieving Output¶
# Get last N lines of stdout (default stream)
ssh_cmd_output(handle_id=1001, lines=50)
# Get all captured stdout
ssh_cmd_output(handle_id=1001)
# Get stderr instead
ssh_cmd_output(handle_id=1001, stream="stderr")
Output in History¶
ssh_cmd_history(
include_output=True,
output_lines=5 # Lines per entry
)
ssh_cmd_history's output snippets are stdout only.
Long-Running Commands¶
For commands that may run for extended periods:
Strategy 1: Increase Timeouts¶
ssh_cmd_run(
command="apt-get update",
io_timeout=120.0, # 2 minutes between outputs
runtime_timeout=600.0, # 10 minutes total
use_sudo=True
)
Strategy 2: Background Task¶
For very long operations, use background tasks instead:
ssh_task_launch(
command="long-running-script.sh",
use_sudo=True
)
# Returns immediately with PID
# Check status later with ssh_task_status(pid=...)
Strategy 3: Check and Wait¶
# Start command
result = ssh_cmd_run(command="make", io_timeout=5.0)
if result['status'] == 'io_timeout':
handle_id = result['id'] # ssh_cmd_run returns 'id'; check_status/kill/output take it as 'handle_id'
# Check periodically
while True:
status = ssh_cmd_check_status(
handle_id=handle_id,
wait_seconds=5.0 # Wait then check
)
if status['status'] == 'completed':
break
Note: runtime_timeout firing already attempts to kill the remote command (Linux, macOS,
and Windows; non-sudo), so there's nothing left to poll for in that case. Polling to wait
out a long command is the io_timeout pattern, shown above.
Strategy 4: Periodic Check-Ins on a Chatty Command¶
io_timeout only fires on silence - a command that keeps producing output (a
verbose docker pull, a build with a constant progress bar) never triggers it, so
ssh_cmd_run would otherwise block for the command's entire duration. Use
wait_timeout to get control back periodically regardless of activity:
result = ssh_cmd_run(
command="docker pull large/image:latest",
io_timeout=120.0, # still catch genuine hangs/silence
wait_timeout=20.0, # but also check in every 20s even while it's chatty
runtime_timeout=1800.0 # hard safety cap
)
if result['status'] in ('io_timeout', 'wait_timeout'):
handle_id = result['id']
# Decide here: keep polling, do something else, or ssh_cmd_kill(handle_id) to
# end it early - the command is still running either way.
Process Termination¶
Graceful Kill (SIGTERM)¶
ssh_cmd_kill(
handle_id=1001,
signal=15, # SIGTERM
force=True, # Use SIGKILL if needed
wait_seconds=2.0
)
Immediate Kill (SIGKILL)¶
ssh_cmd_kill(
handle_id=1001,
signal=9, # SIGKILL
force=False # Already using force signal
)
Concurrency¶
Single Connection Behavior¶
- Only one command can execute at a time per connection
- Attempting concurrent execution returns
busystatus - Use the execution lock to prevent conflicts
Parallel Execution Options¶
- Multiple connections - Each handles one command
- Pipeline commands - Single command with pipes
- Background tasks - Non-blocking execution
Pipeline Example¶
# Execute as single command
ssh_cmd_run(
command="find /var/log -name '*.log' | xargs grep ERROR | head -20"
)
Sudo Operations¶
Basic Sudo Usage¶
ssh_cmd_run(
command="cat /etc/shadow",
use_sudo=True
)
How Sudo Works¶
- Server retrieves sudo password from host configuration
- Command wrapped with
sudo -S -p ''prefix - Password piped to sudo via stdin
- Output captured normally
Verify Sudo Access¶
# Check before running privileged commands
ssh_conn_verify_sudo()
# Returns: {"available": true, "passwordless": false, "requires_password": true}
Command History¶
View Recent Commands¶
ssh_cmd_history(
limit=10,
reverse=True # Most recent first
)
Search History¶
ssh_cmd_history(
pattern="apt", # Filter by command text
include_output=True
)
History Entry Structure¶
{
"id": 1001,
"command": "ls -la",
"exit_code": 0,
"start_time": "2024-01-15T10:30:00Z",
"end_time": "2024-01-15T10:30:01Z",
"pid": 4821,
"output": ["file1.txt", "file2.txt", "..."]
}
Note: there is no status field here - exit_code is null if the command hasn't
completed (e.g. it's still running or hit an io_timeout). output is only present
when ssh_cmd_history(include_output=True) is used.
Best Practices¶
- Set appropriate timeouts based on expected command duration
- Use runtime_timeout for commands with unpredictable output timing - it's the only knob that actually kills the remote command, so it's your real safety net
- Use wait_timeout instead of (or alongside)
io_timeoutfor commands that are chatty but long-running, so you still get periodic control back even though the command never goes quiet - Check status codes before assuming success
- Use background tasks (
ssh_task_launch) for operations that should survive you disconnecting/reconnecting, not just this call returning - Verify sudo access before running privileged commands
- Clear history periodically if storing sensitive commands