cervo
Manages Caddy configuration for routing and serving static sites, re-rendering the Caddyfile so each created site is available at its own hostname.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@cervoDeploy my static site to the VPS and configure HTTPS for it"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
cervo
A demo app for managing static website hosting on a shared VPS. It's built as an MCP server (FastMCP, served over HTTP), so the hosting is managed by chatting with an AI: in development, Claude Code acts as the chat interface for exercising and testing the server's tools.
Quick start
Prerequisite: Docker (with Compose).
bin/dev # docker compose up -d: app, worker, caddy, mailCaddy fronts everything on port 80: the MCP server at http://localhost/mcp, and each created site at http://{slug}.localhost. It boots with no config file — on a fresh checkout it serves nothing for a few seconds, until the worker writes its config over the admin API.
Then open Claude Code in this repo — the server is pre-registered in .mcp.json, so its tools become available directly in the chat. Connecting runs cervo's OAuth sign-in in the browser: enter an email and type back the code that lands in mailcatcher (no real mail is sent in development). Start the stack before opening the Claude Code session (connections are made at startup), and run /mcp to reconnect whenever you change the MCP server code (docker compose restart app) — Claude Code doesn't reconnect automatically.
Development works with zero configuration; settings can be overridden via a .env file (see the configuration table).
Related MCP server: StaticX MCP Server
Connecting from claude.ai
Once deployed (see below), add cervo as a custom connector:
On claude.ai, open Settings → Connectors and click Add custom connector.
Name it (e.g.
cervo) and set the remote MCP server URL tohttps://{your-domain}/mcp.Under advanced settings, keep Use Anthropic's hosted client metadata selected — the recommended option; cervo advertises CIMD support so it works out of the box (plain dynamic client registration works too) — and set authentication to always required.
Click Connect: your browser opens cervo's sign-in page. Enter your email and type back the six-digit code from your inbox. The connection then stays signed in on its own.
The same four steps, written for users and illustrated, are on the deployed
instance's /docs page.
Deploying
Production is the same image on a VPS, run by rootful podman
quadlets
(podman ≥ 4.4 — Debian 13). Deploys run from your machine: bin/deploy
builds the image for linux/amd64, pushes it to Docker Hub tagged with the
git sha, and runs the ansible playbook in deploy/, which writes the
quadlet units and environment, pulls the image, and restarts the services —
caddy first (it resumes what it was serving), then the worker, whose startup
sync rewrites caddy's whole config from the database, so config changes
always land (see Reconciling caddy). Secrets never
live in the repo: the Docker Hub
token and SMTP password are read from 1Password by the op CLI at deploy
time.
One-time setup:
DNS: an
Arecord for the apex and a wildcardA *record, both to the server's IP — sites live athttps://{slug}.{domain}.1Password items (vault
cervo):docker-hubwith atokenfield, andsmtpwith apasswordfield (theop://paths are inventory vars, so any layout works).Create
deploy/inventory.yml(gitignored — every deploy setting lives here, nothing is hardcoded):cervo: hosts: cervo-vps: ansible_host: your.server.ip ansible_port: 22 ansible_user: debian vars: image_repo: docker.io/you/cervo dockerhub_user: you op_dockerhub_token: op://cervo/docker-hub/token domain: example.com acme_email: you@example.com email_host: smtp.example.com # port-587 STARTTLS provider email_port: 587 email_user: your-smtp-user email_from: cervo@example.com # a sender your provider verified op_smtp_password: op://cervo/smtp/password # Optional — with it, errors, request/job telemetry, and container # logs flow to Honeybadger, and every deploy is reported there: honeybadger_api_key: your-project-api-keyOn your machine:
ansibleandopinstalled,opsigned in, docker logged out is fine —bin/deploylogs in itself.
Then every deploy — first and later alike — is:
bin/deployWith SCHEME=https (set by the playbook) caddy obtains a certificate per
hostname from Let's Encrypt and redirects plain http; the first request to
a fresh site waits a few seconds while its certificate is issued.
Certificates persist in the caddy-data volume, so redeploys never
re-issue them.
With honeybadger_api_key set, the playbook also installs
vector to forward the services' journald output to
Honeybadger Insights, and reports each deploy (with its exact commit sha)
to Honeybadger — errors, request and job telemetry, logs, and deploys all
land in one project.
Reconciling caddy
There is no Caddyfile anywhere. Caddy runs caddy run --resume, so it comes
back holding the config it last saved (in the caddy-config volume), and
everything it serves — cervo's own reverse proxy and every hosted site — is
written into its running config by the worker over the admin API. The
database is the source of truth; caddy's config is a cache of it.
That needs no operator either way: the worker rewrites caddy's whole config from the database at startup and every five minutes, so a caddy that lost its autosave is serving again by the next sync — right away when the worker restarted with it, within five minutes when caddy bounced alone — and one that resumed a stale config has it replaced wholesale. A sync that finds nothing listening spends its attempts and fails; the worker sees that on its next poll and asks for another, so a slow caddy costs seconds, not the whole interval.
To force a reconciliation now, run the cervo-sync command inside the
worker container:
podman exec worker uv run cervo-sync # on the VPS
docker compose exec worker uv run cervo-sync # in developmentIt only queues the job (deduped — a sync already waiting is reused and said so); the worker runs it within a poll or two.
The same property sets the deploy order — caddy, then worker, then app: the
restart costs caddy a second or two of refused connections and no unrouted
site (--resume), and the worker's startup sync brings its config back in
step right after.
Documentation
Detailed information lives in CLAUDE.md (also loaded by Claude Code as project context):
Configuration — all settings and their defaults
Layout — where things live in the codebase
Testing with Claude Code — the development/testing workflow
The look of every cervo-served page — tokens, components, and the brand
assets in src/cervo/brand/ — is written down in
design-system/DESIGN_SYSTEM.md.
This server cannot be deployed
Maintenance
Related MCP Connectors
Publish a site your AI built to paid hosting, then read it back, change a page or restore a version.
Build, edit and run real hosted websites from your AI - content, SEO, menus, store, rollback.
Host AI-generated HTML/CSS/JS instantly. Files, zips, or clone an existing page. Live in seconds.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to deploy and manage static websites directly through EdgeOne Pages. This self-hosted solution allows for direct file management and website hosting using EdgeOne's infrastructure and KV storage.-
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to deploy static websites to StaticX, including creating sites, uploading builds, publishing releases, and managing domains.31 npmMIT
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to edit and serve a static website via natural language, providing file management tools over MCP and HTTP hosting.-
- FlicenseNot gradedqualityAmaintenanceEnables AI clients and users to deploy static sites, Node.js services, and Python web applications without Docker, with tools for project management, deployment, rollback, and domain configuration.-