Skip to main content
Glama
cygnussystems

cygnus-ssh-mcp

Official

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

TableJSON Schema
NameRequiredDescriptionDefault
cwdNoRun 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.
commandYesCommand to execute on remote host
use_sudoNoRun command with sudo
io_timeoutNoMax 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_timeoutNoMax 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_timeoutNoTotal 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

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed5 schema fields changedv1.6.1
    • addedInput schema / properties / cwd
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "string"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "description": "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."
      +}
    • changedInput schema / properties / io_timeout / description
      Previous value: -"I/O timeout in seconds"New value: +"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."
    • removedInput schema / properties / io_timeout / exclusiveMinimum
      Removed value: -0
    • changedInput schema / properties / runtime_timeout / description
      Previous value: -"Total runtime timeout in seconds"New value: +"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."
    • addedInput schema / properties / wait_timeout
      Added value: +{
      +  "anyOf": [
      +    {
      +      "exclusiveMinimum": 0,
      +      "type": "number"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "description": "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."
      +}
  2. First observedv1.3.0

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden and does so thoroughly. It discloses timeout semantics (io_timeout, wait_timeout, runtime_timeout), handoff behavior, background monitoring, connection lifetime, Windows cmd.exe behavior, working directory independence, output handling, and detailed status values. This goes far beyond what any annotation would typically provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but well-structured with clear headings (Timeout semantics, Windows targets, Working directory, Returns). Every section addresses a likely agent question. It is front-loaded with the core behavior, then structured details. While lengthy, it is not redundant and each part earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description is extremely complete for a complex tool with 6 parameters and an output schema. It covers timeout semantics, handoff behavior, status meanings, connection lifetime differences from ssh_task_launch, working directory independence, Windows command syntax, output truncation and stream separation, and all status values. Given the complexity, it provides everything an agent needs to call correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already provides detailed parameter descriptions. The description reiterates and expands on timeout parameters and cwd, but since the schema covers everything, the baseline 3 is appropriate. The description adds meaningful behavioral context for timeouts (e.g., io_timeout fires only on silence) that complements the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a precise verb (Execute) and resource (a command on the remote host) and immediately describes the blocking behavior. Clearly distinguises it from siblings like ssh_task_launch and ssh_cmd_check_status by explaining scope and blocking semantics.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly names when to use this tool vs alternatives: e.g., 'For anything that may take more than about a minute, ssh_task_launch is usually the better tool' and 'For commands that should survive you disconnecting/reconnecting entirely, use ssh_task_launch instead'. Also gives detailed timeout selection guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.