Secure VPS Operations MCP Server
# 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
Scored across 8 tools
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.
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.
8 tools is well-scoped for a VPS operations/monitoring server. Each tool addresses a distinct monitoring need without redundancy or bloat.
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.