Files
ulicraft-server-v1/plan/15-mc-status.md
Oier Bravo Urtasun d21a7f0e92 feat(landing): live server card backed by self-hosted mc-status
Add a featured live ServerCard to the landing (replaces the static
ServerListPanel in hero + status sections): server favicon, color MOTD,
online/offline pill, players online/max with fill bar, and a player-head
row rendered by our own NMSR from Drasl skins. Progressive enhancement —
SSG skeleton degrades gracefully when JS or the API is unavailable.

Back it with a self-hosted pinger instead of the public api.mcstatus.io:
mcstatus.io's API service is closed-source (only the mcutil library is
open), so docker/mc-status wraps mcutil and re-emits its v2 JSON shape,
keeping the frontend unchanged. The service ignores the path address and
only pings MC_STATUS_TARGET (no SSRF relay), with a 30s TTL cache.

Exposed same-origin via caddy at the apex /api/mcstatus/* path (no new DNS
subdomain or LE cert change, no CORS). Uptime Kuma stays the uptime
history + alerting backend; see plan/15-mc-status.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:08:30 +02:00

3.7 KiB

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 <addr> segment in /v2/status/java/<addr> 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:

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/<uuid>?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.