Skip to main content
Glama

vm_exec

Execute shell commands on remote virtual machines via SSH to manage Incus VMs, capturing output and exit codes for automation tasks.

Instructions

Execute a command on a remote host via SSH.

Runs the command via `sh -c` so shell features (pipes, redirects, etc.)
work as expected. Captures both stdout and stderr.

Args:
    vm: Name of the host (as configured in hosts.toml).
    command: Shell command to execute.
    workdir: Working directory on the remote host (default: /).
    timeout: Maximum seconds to wait (default: 120).

Returns:
    Command output with stdout, stderr, and exit code.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
vmYes
commandYes
workdirNo/
timeoutNo

Implementation Reference

  • MCP tool registration and handler for vm_exec. This is decorated with @mcp.tool() and serves as the entry point for the tool. It validates parameters, calls the underlying implementation, formats the result, and handles exceptions.
    @mcp.tool()
    async def vm_exec(
        vm: str,
        command: str,
        workdir: str = "/",
        timeout: int = 120,
    ) -> str:
        """Execute a command on a remote host via SSH.
    
        Runs the command via `sh -c` so shell features (pipes, redirects, etc.)
        work as expected. Captures both stdout and stderr.
    
        Args:
            vm: Name of the host (as configured in hosts.toml).
            command: Shell command to execute.
            workdir: Working directory on the remote host (default: /).
            timeout: Maximum seconds to wait (default: 120).
    
        Returns:
            Command output with stdout, stderr, and exit code.
        """
        try:
            result = await _vm_exec(vm, command, workdir=workdir, timeout=timeout)
            return _format_exec_result(result)
        except (ValueError, KeyError, TimeoutError, RuntimeError, OSError) as e:
            return f"ERROR: {e}"
  • Core implementation of vm_exec that performs SSH command execution. Resolves the host config, validates the command, wraps it with cd to workdir, and executes via SSH using sh -c for shell features.
    async def vm_exec(
        vm: str,
        command: str,
        workdir: str = "/",
        timeout: int = 120,
    ) -> ExecResult:
        """Execute a command on a remote host via SSH.
    
        Uses `ssh user@host sh -c <command>` to run the command in a shell
        so pipes, redirects, etc. work as expected.
    
        Args:
            vm: Name of the host (as configured in hosts.toml).
            command: Shell command to execute.
            workdir: Working directory on the remote host (default: /).
            timeout: Maximum seconds to wait (default: 120).
    
        Returns:
            ExecResult with stdout, stderr, and exit code.
        """
        host = _resolve_host(vm)
        if not command or not command.strip():
            raise ValueError("Command cannot be empty")
    
        # Wrap the command with cd to workdir.
        # SSH concatenates remote args into a single string, so we pass
        # the entire wrapped command as one argument to avoid splitting.
        wrapped = f"cd {workdir} && {command}"
        return await _run_ssh(
            host,
            [wrapped],
            timeout=timeout,
        )
  • Low-level SSH execution helper that runs commands on remote hosts using asyncio.create_subprocess_exec. Handles timeout, stdin piping, and returns an ExecResult with stdout, stderr, and exit code.
    async def _run_ssh(
        host_config: HostConfig,
        remote_command: list[str],
        timeout: int = 120,
        stdin_data: bytes | None = None,
    ) -> ExecResult:
        """Run a command on a remote host via SSH.
    
        Args:
            host_config: SSH connection details for the target host.
            remote_command: Command and arguments to run remotely.
            timeout: Maximum seconds to wait.
            stdin_data: Optional bytes to pipe to stdin.
    
        Returns:
            ExecResult with stdout, stderr, and exit code.
    
        Raises:
            TimeoutError: If the command exceeds the timeout.
            OSError: If the ssh binary is not found.
        """
        ssh_args = host_config.ssh_args()
        full_cmd = ["ssh"] + ssh_args + remote_command
    
        proc = await asyncio.create_subprocess_exec(
            *full_cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            stdin=asyncio.subprocess.PIPE if stdin_data else asyncio.subprocess.DEVNULL,
        )
    
        try:
            stdout_bytes, stderr_bytes = await asyncio.wait_for(
                proc.communicate(input=stdin_data),
                timeout=timeout,
            )
        except asyncio.TimeoutError:
            proc.kill()
            await proc.wait()
            raise TimeoutError(
                f"SSH command timed out after {timeout}s on {host_config.name}"
            )
    
        return ExecResult(
            stdout=stdout_bytes.decode("utf-8", errors="replace"),
            stderr=stderr_bytes.decode("utf-8", errors="replace"),
            exit_code=proc.returncode or 0,
        )
  • ExecResult dataclass schema that defines the return type for command execution, containing stdout, stderr, and exit_code fields.
    @dataclass
    class ExecResult:
        """Result of running a command on a remote host."""
    
        stdout: str
        stderr: str
        exit_code: int
  • Helper function that formats an ExecResult into a readable string for MCP responses, combining stdout, stderr, and exit code into a single formatted output.
    def _format_exec_result(result: ExecResult) -> str:
        """Format an ExecResult as a readable string for MCP response."""
        parts = []
        if result.stdout:
            parts.append(f"STDOUT:\n{result.stdout}")
        if result.stderr:
            parts.append(f"STDERR:\n{result.stderr}")
        parts.append(f"EXIT CODE: {result.exit_code}")
        return "\n".join(parts)

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/bobbyhiddn/Sympathy-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server