Skip to main content
Glama
ojoilesanmi

Secure VPS Operations MCP Server

by ojoilesanmi
README.md
# Secure VPS Operations MCP Server

A secure, read-only Model Context Protocol (MCP) server for inspecting a VPS over SSH without giving an AI assistant unrestricted shell access.

The core idea is simple: the AI selects approved operations; it never constructs or executes arbitrary shell commands.

## What This Project Does

This project exposes a small set of MCP tools for safe VPS inspection:

- `get_system_health`
- `get_disk_usage`
- `list_containers`
- `get_container_logs`
- `get_service_status`
- `check_nginx_configuration`
- `check_ssl_expiry`
- `check_database_health`

Each MCP tool validates input locally, checks configured allowlists, then calls a fixed SSH gateway command on the VPS. The gateway maps operation names to root-owned scripts under `/usr/local/lib/mcp/scripts`.

There is intentionally no generic `execute_shell` tool.

## Architecture

```text
AI Client
    |
    v
MCP Client
    |
    v
Local Python MCP Server
    |
    v
SSH key authentication
    |
    v
Dedicated VPS user
    |
    v
/usr/local/bin/mcp-command-gateway
    |
    v
Root-owned allowlisted scripts
```

## Why Python

This first version uses Python 3.11+ because the official MCP Python SDK, `asyncssh`, and Pydantic make a small control-plane server straightforward to build and audit.

Java would also be viable, especially inside an existing Spring-based internal platform, but Python keeps this MCP adapter smaller and simpler for the first release.

## Security Model

The security boundary is based on constraints rather than prompting:

- No unrestricted shell tool
- Dedicated SSH user, not root
- SSH key authentication only
- Host-key verification through `known_hosts`
- Fixed gateway command on the VPS
- Operation allowlist in the gateway
- Resource allowlists for containers, services, domains, and databases
- Root-owned scripts that the SSH user cannot edit
- Narrow sudo rules only where required
- Output sanitisation before returning data to the model
- Append-only JSONL audit logs

## Local Setup

Create a virtual environment:

```bash
python3.11 -m venv .venv
source .venv/bin/activate
pip install -e '.[test]'
```

Create local configuration:

```bash
cp .env.example .env
```

Edit `.env`:

```text
SSH_HOST=your-vps-host-or-ip
SSH_PORT=22
SSH_USERNAME=mcp-operator
SSH_PRIVATE_KEY_FILE=/path/to/private/key
SSH_KNOWN_HOSTS_FILE=/path/to/known_hosts

ALLOWED_CONTAINERS=grafana,prometheus,opensearch
ALLOWED_SERVICES=app.service,docker.service,nginx.service
ALLOWED_DOMAINS=api-dev.example.com,api-staging.example.com
ALLOWED_DATABASES=appdb
```

Pin the VPS host key:

```bash
ssh-keyscan -H your-vps-host-or-ip >> ~/.ssh/known_hosts
```

Run tests:

```bash
python -m pytest
```

Run the MCP server locally:

```bash
contabo-ops-mcp
```

For stdio MCP usage, the server will appear to wait for input. That is expected. An MCP client starts the process and communicates with it over stdin/stdout.

## VPS Setup

Create a dedicated user on the VPS:

```bash
sudo useradd --create-home --shell /bin/bash mcp-operator
sudo mkdir -p /home/mcp-operator/.ssh
sudo chmod 700 /home/mcp-operator/.ssh
sudo chown -R mcp-operator:mcp-operator /home/mcp-operator
```

Create an SSH key locally:

```bash
ssh-keygen -t ed25519 -f ~/.ssh/vps_mcp -C "vps-operations-mcp"
```

Add the public key to the VPS:

```bash
sudo tee /home/mcp-operator/.ssh/authorized_keys > /dev/null <<'EOF'
PASTE_PUBLIC_KEY_HERE
EOF

sudo chown -R mcp-operator:mcp-operator /home/mcp-operator/.ssh
sudo chmod 700 /home/mcp-operator/.ssh
sudo chmod 600 /home/mcp-operator/.ssh/authorized_keys
sudo chmod 755 /home/mcp-operator
```

Test SSH from your local machine:

```bash
ssh -i ~/.ssh/vps_mcp mcp-operator@your-vps-host-or-ip
```

## Gateway Installation

Copy the gateway to the VPS:

```bash
scp -i ~/.ssh/vps_mcp vps/mcp-command-gateway mcp-operator@your-vps-host-or-ip:/tmp/mcp-command-gateway
```

Install it on the VPS:

```bash
sudo install -o root -g root -m 755 /tmp/mcp-command-gateway /usr/local/bin/mcp-command-gateway
sudo mkdir -p /usr/local/lib/mcp/scripts
sudo chown -R root:root /usr/local/lib/mcp
sudo chmod -R 755 /usr/local/lib/mcp
```

The gateway accepts structured JSON like this:

```json
{
  "operation": "system.health",
  "arguments": {}
}
```

It rejects unknown operations and never accepts raw shell commands.

## Example Script

Create `/usr/local/lib/mcp/scripts/system-health` on the VPS:

```bash
sudo tee /usr/local/lib/mcp/scripts/system-health > /dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail

load_average="$(cut -d ' ' -f1-3 /proc/loadavg)"
uptime_seconds="$(cut -d. -f1 /proc/uptime)"
hostname="$(hostname)"

printf '{"success":true,"hostname":"%s","uptimeSeconds":%s,"loadAverage":"%s"}\n' \
  "$hostname" \
  "$uptime_seconds" \
  "$load_average"
EOF

sudo chown root:root /usr/local/lib/mcp/scripts/system-health
sudo chmod 755 /usr/local/lib/mcp/scripts/system-health
```

Test directly on the VPS:

```bash
printf '{"operation":"system.health","arguments":{}}' | /usr/local/bin/mcp-command-gateway
```

Test from your local machine over SSH:

```bash
ssh -i ~/.ssh/vps_mcp mcp-operator@your-vps-host-or-ip \
  'printf '\''{"operation":"system.health","arguments":{}}'\'' | /usr/local/bin/mcp-command-gateway'
```

## MCP Client Configuration

For an MCP client that supports local stdio servers, configure the command as:

```json
{
  "mcp": {
    "vps-ops": {
      "type": "local",
      "command": [
        "/absolute/path/to/project/.venv/bin/python",
        "/absolute/path/to/project/src/contabo_mcp/main.py"
      ],
      "enabled": true,
      "env": {
        "PYTHONPATH": "/absolute/path/to/project/src"
      }
    }
  }
}
```

Restart the MCP client after changing its config.

Example prompts:

- "Check system health on the VPS."
- "Show disk usage on the VPS."
- "Show the last 20 logs for the Grafana container."
- "Check SSL expiry for `api-dev.example.com`."
- "Check status of `nginx.service`."

## Development

Run tests:

```bash
source .venv/bin/activate
python -m pytest
```

Check config loading:

```bash
python -c "from contabo_mcp.config import get_settings; print(get_settings().ssh_host)"
```

Check the SSH gateway from Python:

```bash
python - <<'PY'
import asyncio
from contabo_mcp.config import get_settings
from contabo_mcp.ssh_gateway import SshGateway

async def main():
    gateway = SshGateway(get_settings())
    result = await gateway.execute("system.health", {})
    print(result.exit_code)
    print(result.stdout)
    print(result.stderr)

asyncio.run(main())
PY
```

## Current Scope

This repository is intentionally read-only for the first release.

Write operations such as service restarts, Nginx reloads, certificate renewal, backups, deployments, and rollbacks should be added only after implementing:

- Explicit approval handling
- Risk classification
- Narrow sudo rules
- Idempotency checks
- Post-action health verification
- Stronger audit records
- Failure and rollback behavior

## License

This project is licensed under the MIT License. See `LICENSE` for details.

TDQS

A3.7/5.0

Scored across 8 tools

Disambiguation4/5

Most tools have clearly distinct purposes (system health, disk usage, containers, logs, services, nginx, SSL, database). There's minor overlap between get_system_health and get_disk_usage/get_service_status, but descriptions clarify the scope.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (get_*, list_*, check_*) with snake_case throughout. The verbs are predictable and appropriate for the actions.

Tool Count5/5

8 tools is well-scoped for a VPS operations/monitoring server. Each tool addresses a distinct monitoring need without redundancy or bloat.

Completeness4/5

The server covers core VPS health checks (system, disk, containers, services, nginx, SSL, database) well. Minor gaps exist (e.g., no network health or process-level details), but the surface is sufficient for typical operational monitoring.

Maintenance

ActivitySlowing
ResponsivenessNo issues