Skip to main content
Glama
kunwarmahen

SSH Read-Only MCP Server

by kunwarmahen

ssh_execute

Run read-only commands on a remote machine via SSH, blocking write operations. Enables safe remote troubleshooting, monitoring, and auditing without risk of unintended changes.

Instructions

Execute a read-only command on the connected remote machine.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
hostYesRemote host (must be connected first)
usernameYesSSH username
commandYesThe read-only command to execute (e.g., 'ls -la', 'cat /etc/hostname')
portNoSSH port (default: 22)

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • The ssh_execute handler function implementing execution of read-only commands via SSH. It checks if connected, validates command safety via is_command_safe, executes via paramiko, and returns stdout/stderr.
    @mcp.tool()
    def ssh_execute(
        host: str,
        username: str,
        command: str,
        port: int = 22
    ) -> str:
        """
        Execute a read-only command on the connected remote machine.
        
        Args:
            host: Remote host (must be connected first)
            username: SSH username
            command: The read-only command to execute (e.g., 'ls -la', 'cat /etc/hostname')
            port: SSH port (default: 22)
        
        Returns:
            Command output or error message
        """
        connection_id = f"{username}@{host}:{port}"
        
        # Check if connected
        if connection_id not in ssh_clients:
            return f"Error: Not connected to {connection_id}. Please connect first using ssh_connect."
        
        # Validate command safety
        if not is_command_safe(command):
            return f"Error: Command not allowed for security reasons.\nOnly read-only commands are permitted.\nBlocked command: {command}"
        
        try:
            client = ssh_clients[connection_id]['client']
            stdin, stdout, stderr = client.exec_command(command, timeout=30)
            
            output = stdout.read().decode('utf-8')
            error = stderr.read().decode('utf-8')
            exit_code = stdout.channel.recv_exit_status()
            
            if exit_code == 0:
                return output if output else "(command executed successfully with no output)"
            else:
                return f"Command failed with exit code {exit_code}\nError: {error if error else 'No error message'}"
        
        except paramiko.SSHException as e:
            return f"SSH error: {str(e)}"
        except Exception as e:
            return f"Command execution failed: {str(e)}"
  • The ssh_execute handler function in the multicast version of the server. Same logic as the base version: checks connection, validates command safety, executes via paramiko, and returns output.
    @mcp.tool()
    def ssh_execute(
        host: str,
        username: str,
        command: str,
        port: int = 22
    ) -> str:
        """
        Execute a read-only command on the connected remote machine.
        
        Args:
            host: Remote host (must be connected first)
            username: SSH username
            command: The read-only command to execute (e.g., 'ls -la', 'cat /etc/hostname')
            port: SSH port (default: 22)
        
        Returns:
            Command output or error message
        """
        connection_id = f"{username}@{host}:{port}"
        
        # Check if connected
        if connection_id not in ssh_clients:
            return f"Error: Not connected to {connection_id}. Please connect first using ssh_connect."
        
        # Validate command safety
        if not is_command_safe(command):
            return f"Error: Command not allowed for security reasons.\nOnly read-only commands are permitted.\nBlocked command: {command}"
        
        try:
            client = ssh_clients[connection_id]['client']
            stdin, stdout, stderr = client.exec_command(command, timeout=30)
            
            output = stdout.read().decode('utf-8')
            error = stderr.read().decode('utf-8')
            exit_code = stdout.channel.recv_exit_status()
            
            if exit_code == 0:
                return output if output else "(command executed successfully with no output)"
            else:
                return f"Command failed with exit code {exit_code}\nError: {error if error else 'No error message'}"
        
        except paramiko.SSHException as e:
            return f"SSH error: {str(e)}"
        except Exception as e:
            return f"Command execution failed: {str(e)}"
  • The ALLOWED_COMMANDS set defining the read-only commands permitted for use with ssh_execute.
    # List of allowed read-only commands
    ALLOWED_COMMANDS = {
        'cat', 'ls', 'pwd', 'whoami', 'id', 'date', 'uptime', 'ps', 'top',
        'df', 'du', 'free', 'netstat', 'ss', 'ifconfig', 'ip', 'hostname',
        'uname', 'lsb_release', 'file', 'head', 'tail', 'wc', 'grep',
        'find', 'locate', 'which', 'whereis', 'stat', 'lsof', 'mount',
        'dmidecode', 'lscpu', 'lsblk', 'fdisk', 'blkid', 'journalctl',
        'systemctl', 'service', 'curl', 'wget', 'dig', 'nslookup', 'ping',
        'traceroute', 'mtr', 'iptables', 'firewall-cmd', 'ufw', 'awk', 'sed'
    }
  • The is_command_safe helper function used by ssh_execute to validate that a command is read-only safe and in the allowed list.
    def is_command_safe(command: str) -> bool:
        """Verify that the command is read-only safe."""
        cmd_parts = command.strip().split()
        if not cmd_parts:
            return False
        
        base_cmd = cmd_parts[0].split('/')[-1]  # Get the command name without path
        
        # Block dangerous patterns
        dangerous_patterns = [
            '>', '<', '|', '&', ';', '$(', '`', 'rm', 'dd', 'mkfs',
            'chmod', 'chown', 'mv', 'cp', 'touch', 'mkdir', 'rmdir',
            'kill', 'shutdown', 'reboot', 'halt', 'poweroff',
            'su', 'sudo', 'passwd', 'usermod', 'useradd', 'userdel',
            'nano', 'vi', 'vim', 'ed'
        ]
        
        # Check for dangerous patterns in command
        for pattern in dangerous_patterns:
            if pattern in command:
                return False
        
        # Check if base command is in allowed list
        return base_cmd in ALLOWED_COMMANDS
  • The @mcp.tool() decorator on line 104 registers ssh_execute as an MCP tool. The FastMCP instance is created on this line.
    mcp = FastMCP("ssh-readonly")
Behavior4/5

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

No annotations are provided, so the description carries the full burden. It correctly marks the tool as read-only, which is a critical behavioral trait. However, it does not disclose other traits like authentication requirements or error handling, but the read-only aspect is well conveyed.

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

Conciseness5/5

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

Single sentence, no wasted words, front-loads the purpose. Excellent conciseness.

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?

For a simple read-only command execution tool, the description covers purpose and safety. The output schema likely handles return values, so no further explanation needed. Complete given the complexity.

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 coverage is 100%, so the baseline is 3. The description does not add any additional meaning beyond the schema's parameter descriptions. No extra semantics provided.

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?

Description clearly states 'Execute a read-only command on the connected remote machine,' specifying the verb (execute), resource (command on remote machine), and a distinguishing trait (read-only). This differentiates from sibling tools like ssh_connect and ssh_disconnect.

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

Usage Guidelines4/5

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

The description explicitly says 'read-only command,' guiding the agent to use this tool for read operations. While it doesn't name alternatives, the context of sibling tools and the schema's command description imply when to use versus write operations.

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

Install Server

Other Tools

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/kunwarmahen/ssh-mcp-server'

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