Skip to main content
Glama
kunwarmahen

SSH Read-Only MCP Server

by kunwarmahen
README.md
# SSH Read-Only MCP Server

A secure Model Context Protocol (MCP) server that enables remote SSH command execution with strict read-only enforcement. Perfect for safely delegating SSH access to Claude while preventing accidental or malicious write operations.

## Features

✅ **Read-Only Command Enforcement** – Only allows safe, read-only commands  
✅ **SSH Connection Pooling** – Support multiple simultaneous connections  
✅ **Command Validation** – Blocks dangerous patterns and write operations  
✅ **Multicast Discovery** – Auto-announces on network for easy discovery  
✅ **Flexible Transport** – Stdio, HTTP, or Streamable-HTTP modes  
✅ **Comprehensive Logging** – Full audit trail in `ssh_mcp.log`  
✅ **Environment Configuration** – Fully configurable via .env

## Installation

### Prerequisites

- Python 3.8+
- `uv` package manager

### Setup

```bash
# Clone or create project directory
mkdir ssh-mcp-server
cd ssh-mcp-server

# Initialize uv project (if needed)
uv init

# Install dependencies
uv pip install fastmcp paramiko python-dotenv
```

Or use `pyproject.toml`:

```toml
[project]
name = "ssh-mcp-server"
version = "0.1.0"
dependencies = [
    "fastmcp>=0.1.0",
    "paramiko>=3.0.0",
    "python-dotenv>=1.0.0",
]
```

Then install:

```bash
uv sync
```

## Configuration

Create a `.env` file in the project root:

```env
# Transport mode: stdio (default), http, or streamable-http
MCP_TRANSPORT=stdio

# Server identification
MCP_SERVER_NAME=SSH Read-Only MCP Server

# HTTP mode settings (if using http/streamable-http)
MCP_HOST=0.0.0.0
MCP_PORT=3000

# Multicast discovery
MCP_ENABLE_BROADCAST=true
MCP_BROADCAST_INTERVAL=30
```

### Environment Variables

| Variable                 | Default                    | Description                                                    |
| ------------------------ | -------------------------- | -------------------------------------------------------------- |
| `MCP_TRANSPORT`          | `stdio`                    | Communication transport: `stdio`, `http`, or `streamable-http` |
| `MCP_SERVER_NAME`        | `SSH Read-Only MCP Server` | Display name for the server                                    |
| `MCP_HOST`               | `0.0.0.0`                  | Bind address for HTTP mode                                     |
| `MCP_PORT`               | `3000`                     | Port for HTTP mode                                             |
| `MCP_ENABLE_BROADCAST`   | `true`                     | Enable multicast discovery announcements                       |
| `MCP_BROADCAST_INTERVAL` | `30`                       | Seconds between broadcast announcements                        |

## Usage

### Start the Server

**Stdio mode (default):**

```bash
uv run ssh_readonly_fastmcp_mcast.py
```

**HTTP mode with multicast discovery:**

```bash
MCP_TRANSPORT=http MCP_PORT=3000 uv run ssh_readonly_fastmcp_mcast.py
```

**HTTP mode without broadcasting:**

```bash
MCP_ENABLE_BROADCAST=false MCP_TRANSPORT=http MCP_PORT=3000 uv run ssh_readonly_fastmcp_mcast.py
```

### Available Tools

#### 1. `ssh_connect`

Establish an SSH connection to a remote machine.

**Parameters:**

- `host` (required) – Remote host IP or hostname
- `username` (required) – SSH username
- `port` (optional, default: 22) – SSH port
- `key_filename` (optional) – Path to private key file (recommended)
- `password` (optional) – SSH password (fallback)

**Example:**

```
Connect to 192.168.1.100 as user 'admin' with private key
host: 192.168.1.100
username: admin
key_filename: /home/user/.ssh/id_rsa
```

#### 2. `ssh_execute`

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

**Parameters:**

- `host` (required) – Remote host (must be connected first)
- `username` (required) – SSH username
- `command` (required) – Read-only command to execute
- `port` (optional, default: 22) – SSH port

**Example:**

```
Run 'ls -la /home' on connected server
host: 192.168.1.100
username: admin
command: ls -la /home
```

#### 3. `ssh_disconnect`

Close an SSH connection.

**Parameters:**

- `host` (required) – Remote host
- `username` (required) – SSH username
- `port` (optional, default: 22) – SSH port

#### 4. `ssh_list_connections`

View all active SSH connections.

**Parameters:** None

#### 5. `ssh_get_allowed_commands`

Retrieve the list of allowed read-only commands.

**Parameters:** None

## Allowed Commands

The server permits the following read-only operations:

- **File operations:** `cat`, `ls`, `file`, `head`, `tail`, `find`, `locate`
- **System info:** `ps`, `top`, `df`, `du`, `free`, `uname`, `hostname`, `uptime`
- **Network:** `netstat`, `ss`, `ifconfig`, `ip`, `curl`, `wget`, `dig`, `nslookup`, `ping`, `traceroute`
- **Process management:** `lsof`, `systemctl`, `service`
- **Text processing:** `grep`, `awk`, `sed`, `wc`
- And many more read-only utilities

**Blocked operations:** `rm`, `mv`, `cp`, `chmod`, `chown`, `mkdir`, `touch`, `kill`, `shutdown`, `reboot`, `sudo`, and any write/modify commands.

## Multicast Discovery

When running in HTTP mode with broadcasting enabled, the server announces itself on the multicast group:

- **Address:** `239.255.255.250`
- **Port:** `5353`
- **Interval:** Configurable (default: 30 seconds)

Discovery announcement includes:

- Server UUID
- Server name
- Local IP and port
- Transport type
- Protocol version

## Logging

All activity is logged to `ssh_mcp.log`:

```
2025-10-17 10:30:45,123 [INFO] ssh_mcp - Starting MCP server with transport=http
2025-10-17 10:30:46,456 [INFO] ssh_mcp - Starting multicast broadcaster on 239.255.255.250:5353
2025-10-17 10:30:47,789 [INFO] ssh_mcp - Successfully connected to admin@192.168.1.100:22
```

## Security Considerations

🔒 **Read-Only Enforcement:**

- Only whitelisted commands are allowed
- Dangerous patterns (pipes, redirects, subshells) are blocked
- Write operations are prevented at the command level

⏱️ **Timeouts:**

- 30-second execution timeout per command
- Prevents hanging commands from blocking the server

🔐 **Authentication:**

- SSH key authentication recommended over passwords
- Passwords stored in memory only, never persisted

📋 **Audit Trail:**

- All connections and commands are logged
- Review `ssh_mcp.log` for security audits

## Troubleshooting

### Connection Refused

```
Error: Connection failed: [Errno 111] Connection refused
```

- Verify the remote host is reachable: `ping <host>`
- Check SSH is running on the remote machine
- Verify port number (default 22)

### Authentication Failed

```
Error: Connection failed: Authentication failed
```

- Verify username is correct
- For key auth: check key file path and permissions (`chmod 600`)
- For password auth: verify credentials
- Ensure SSH public key is authorized on remote (`~/.ssh/authorized_keys`)

### Command Not Allowed

```
Error: Command not allowed for security reasons
```

- The command contains a blocked pattern or is not in the allowed list
- Use `ssh_get_allowed_commands` to see permitted commands
- For write operations, use SSH directly instead

### Broadcast Not Working

- Verify `MCP_ENABLE_BROADCAST=true`
- Check network supports multicast (most corporate networks block it)
- Verify firewall allows UDP on port 5353
- Check `ssh_mcp.log` for broadcast errors

## Development

### Running in Debug Mode

```bash
DEBUG=true uv run ssh_readonly_fastmcp_mcast.py
```

### Testing

```bash
# Test connection
uv run -c "from ssh_readonly_fastmcp_mcast import is_command_safe; print(is_command_safe('ls -la'))"

# Should print: True

# Test blocked command
uv run -c "from ssh_readonly_fastmcp_mcast import is_command_safe; print(is_command_safe('rm -rf /'))"

# Should print: False
```

## Project Structure

```
ssh-mcp-server/
├── ssh_readonly_fastmcp_mcast.py    # Main server implementation
├── .env                        # Configuration file
├── .env.example               # Configuration template
├── ssh_mcp.log                # Server logs (auto-generated)
├── pyproject.toml             # Project metadata
└── README.md                  # This file
```

## API Response Format

All tools return consistent JSON responses:

**Success:**

```json
{
  "status": "success",
  "host": "192.168.1.100",
  "command": "ls -la /home",
  "exit_code": 0,
  "output": "total 24\ndrwxr-xr-x 3 root root 4096...",
  "error": null
}
```

**Error:**

```json
{
  "status": "error",
  "message": "Command not allowed for security reasons",
  "reason": "Only read-only commands are permitted"
}
```

## License

MIT

## Contributing

Contributions welcome! Please ensure:

- All changes maintain read-only enforcement
- Code is logged appropriately
- Tests pass for security validations

## Support

For issues or questions:

1. Check `ssh_mcp.log` for error details
2. Review the Troubleshooting section
3. Verify environment configuration
4. Check network connectivity to remote hosts

TDQS

A3.8/5.0

Scored across 5 tools

Disambiguation5/5

Each tool has a distinct purpose: connecting, disconnecting, executing commands, listing allowed commands, and listing connections. No overlap, clear differentiation.

Naming Consistency5/5

All tools follow the ssh_verb_noun pattern consistently, with verbs like connect, disconnect, execute, get_allowed_commands, and list_connections. Minor variation between get and list is acceptable as both are retrieval verbs.

Tool Count5/5

5 tools is well-scoped for a read-only SSH server. Each tool covers a necessary operation without redundancy or missing essentials.

Completeness5/5

The tool surface covers the full lifecycle of a read-only SSH session: connect, execute (read-only commands), disconnect, and utilities like listing allowed commands and connections. No obvious gaps.

Maintenance

ActivityInactive
ResponsivenessNo issues