← All projects
liveInternal Toollaunched May 2026

VPS Manager MCP

AI-operable cloud infrastructure for a 35-container production fleet

A pair of custom MCP (Model Context Protocol) servers that let Claude operate my Hostinger VPS directly — read logs, audit ports, restart services, deploy containers, inspect the filesystem — through HTTPS + OAuth, with hard auth boundaries. Built so AI agents could safely help me run the koralktech.io fleet instead of me SSH-ing for every check.

problem

I run 12 SaaS products across 35+ containers on a single Hostinger VPS as a solo operator. Every health check, log dive, restart, or audit means opening a terminal and SSH-ing in. AI assistants could help — but only if they could actually see and act on the system safely. No off-the-shelf MCP server exposes a multi-tenant Linux host with the right auth boundary.

solution

Built and deployed two MCP servers behind the existing Hetzner Caddy → Hostinger Traefik edge. VPS Manager MCP (vps-mcp.koralktech.io) — FastMCP + uvicorn, systemd-managed, bearer-token-auth — exposes file read/write, command exec, systemctl, docker, journalctl. Bound to 127.0.0.1, only reachable through the Traefik routing layer over a UFW-restricted Docker network gateway. Claude-VPS Bridge (vps-bridge.koralktech.io) — FastMCP 3.3 + OAuth 2.1 + Dynamic Client Registration, proxied to GitHub OAuth for user identity, with a single-user GitHub allowlist. Disk-backed client-storage survives restarts. Runs as a hardened host-systemd unit (deliberately not containerized — see lessons). Together they let Claude (Desktop, web, mobile) treat the VPS like a structured API: "list containers", "check listening ports", "tail the nginx log", "restart the trading bot" — natural language in, real ops out.

architecture

HTTPSTLSbearerOAuthDCR proxyexec/readclaude CLIClaude (Deskto…GitHub OAuthCaddy (Hetzner…Traefik (Hosti…vps-mcp (syste…vps-bridge (sy…VPS host (Dock…

external → compute → store → ui

outcome

The system pays for itself the first time it surfaces something a human eye would miss. Concrete win on 2026-05-28: I asked Claude to "list any ports open to 0.0.0.0" — it flagged five Postgres / Redis / app ports that Docker had quietly bypassed past UFW. All five were closed inside ten minutes. The MCP fleet now handles routine audits, log triage, certificate renewal runbooks, and deploy-and-verify loops without me touching a terminal.

stack

FastMCP 3.3Python 3.12systemdDockerTraefikCaddyOAuth 2.1 + DCRGitHub OAuthLet's EncryptUFWLinux

capabilities

  • File read/write, command exec, systemctl, docker, journalctl exposed as MCP tools
  • OAuth 2.1 + Dynamic Client Registration, GitHub-backed user identity
  • Single-user GitHub allowlist with disk-backed token storage (survives restart)
  • Bearer-token defense-in-depth on the inner MCP, behind Caddy + Traefik edge
  • Inner service bound to 127.0.0.1; only Traefik's Docker network can reach it
  • systemd Restart=always with hardened unit (NoNewPrivileges, ProtectSystem)
  • Smoke-testable via public /healthz and /version endpoints
  • Same control plane works from Claude Desktop, claude.ai web, and mobile

lessons learned

  • 01Docker bypasses UFW. Default-deny incoming meant nothing for any `ports:` block bound to 0.0.0.0 — docker-proxy inserts iptables rules before the UFW chains. Audit your published ports with `ss -tlnp`; prefix `127.0.0.1:` on everything except real public services.
  • 02Containerizing a bridge that's supposed to expose "the host" defeats the entire point. The original Docker-deployed Claude-VPS Bridge had an empty /root/apps, no docker socket, no systemctl. Migrated to host-systemd — bridge instantly saw all 38 containers as intended.
  • 03FastMCP's OAuth client storage is in-memory by default. Every restart invalidates the connector token and the user has to re-auth. Fix: pass `client_storage=DiskStore(...)`. Survives restart, reboot, redeploy.
  • 04Caddy: editing the Caddyfile alone does nothing — `systemctl reload caddy` is mandatory for the new block to activate. Easy bug to chase when a brand-new subdomain "won't resolve".
  • 05For a multi-cloud edge (Caddy on Hetzner → Traefik on Hostinger), the inner Traefik's ACME attempt for the same host harmlessly fails because the validator hits the outer Caddy first. Let Caddy hold the public cert and `tls_insecure_skip_verify` on the hop.
← All projects