# 15 — mc-status (self-hosted live server card backend) Self-hosted Minecraft Server List Ping → JSON service that feeds the landing's live **ServerCard**. Replaces the third-party `api.mcstatus.io` dependency. ## Why not just use api.mcstatus.io The public API works (CORS `*`, no key), but mcstatus.io's **API service is not open source** — only its underlying Go library, `mcutil`, is. To self-host we run the *same library* behind our own thin HTTP shell that re-emits mcstatus.io's **v2 JSON shape**, so the frontend (`ServerCard.astro`) stays byte-identical — only the URL changes. itzg/mc-status is deprecated; mc-monitor only exports Prometheus metrics (no icon/MOTD/player sample), so neither fits the card. ## Service `docker/mc-status/` — Go, built from source (multi-stage → scratch), image `ulicraft/mc-status:local`. Wraps `github.com/mcstatus-io/mcutil/v4` `status.Modern()` and maps `StatusModern` → v2 JSON: | mcutil field | v2 field ServerCard reads | |---|---| | `Players.Online/Max` | `players.online` / `players.max` | | `Players.Sample[].ID` | `players.list[].uuid` | | `Players.Sample[].Name` (`formatting.Result`) | `players.list[].name_raw/clean/html` | | `MOTD` (`formatting.Result`) | `motd.raw/clean/html` | | `Favicon` (`data:image/png;base64,…`) | `icon` | On any ping failure it returns `{"online": false}` so the card shows an offline state without erroring. ### Hardening - **No open SSRF relay.** The `` segment in `/v2/status/java/` is **ignored** — the service only ever pings `MC_STATUS_TARGET`. The segment exists only to keep the URL drop-in compatible with the public API. - **TTL cache** (`MC_STATUS_CACHE_TTL`, default `30s`) so a busy landing page can't ping the Minecraft server once per visitor. ### Env | Var | Default | Notes | |---|---|---| | `MC_STATUS_TARGET` | `minecraft:25565` | pinged internally over `mcnet` | | `MC_STATUS_CACHE_TTL` | `30s` | in-memory cache window | | `MC_STATUS_LISTEN` | `:8080` | bind address | ## Ingress Path-on-apex (no new DNS subdomain, no Let's Encrypt cert change — DNS/TLS are managed outside this repo). `caddy/conf.d/10-static.caddy`: ```caddy handle /api/mcstatus/* { uri strip_prefix /api/mcstatus reverse_proxy mc-status:8080 } ``` Same-origin → the ServerCard fetch needs no CORS. `site.ts`: `statusApi: \`/api/mcstatus/v2/status/java/${BASE_DOMAIN}\``. ## Data flow ``` guest browser ─ GET /api/mcstatus/v2/status/java/${BASE_DOMAIN} └► caddy (apex) ─ strip_prefix /api/mcstatus ─► mc-status:8080 └► mcutil SLP ping ─► minecraft:25565 (internal, mcnet) ◄── v2 JSON {online, players.list[uuid,name_*], motd.html, icon} ServerCard → player heads via NMSR: avatar.${BASE_DOMAIN}/headiso/?size=64 ``` ## Relationship to Uptime Kuma (plan/10) Distinct roles. **Kuma** = uptime *history* + alerting backend (Minecraft Server monitor, status page). **mc-status** = live, public-facing data for the landing card (richer: icon/MOTD/player heads). Both ping `minecraft` over `mcnet`. ## Validation notes Built + `go vet`/`gofmt` clean. The live ping path can't be exercised from the dev sandbox (egress proxy allows TCP connect but filters the raw SLP handshake → `context deadline exceeded`); it resolves on the prod host where `minecraft` is reachable over `mcnet`. After deploy, confirm: ``` curl -s https://${BASE_DOMAIN}/api/mcstatus/v2/status/java/${BASE_DOMAIN} | jq . ``` expecting `online:true`, a populated `players.list`, and `motd.html`. ## Tasks - [ ] `docker compose up -d --build mc-status` on prod, verify the curl above. - [ ] Confirm the SLP player sample carries real (Drasl) UUIDs so NMSR heads render; if empty, the card falls back to the count-only / empty state.