TOR-themed dice roller — Go server with embedded Svelte SPA. Calls rpg-dice-mcp over MCP for the actual roll resolution.
One binary, one container, one pod. The browser only ever talks to this server (same-origin, no CORS). Inside the Go process, an MCP client speaks Streamable HTTP to rpg-dice-mcp for roll, roll_tor_check, and roll_tor_combat.
Tracked: dvystrcil/homelab#262.
┌─────────────────────────────────────┐
│ tor-dice (this repo) │
│ │
│ GET / embed.FS │ ← Svelte index.html + js/css
│ GET /assets/* embed.FS │
│ POST /api/roll ──┐ │ ← REST endpoints translate to
│ POST /api/roll_tor_check ──┼─→ │ MCP CallTool() over Streamable
│ POST /api/roll_tor_combat ──┘ │ HTTP (one long-lived session)
└─────────────────────────────────────┘
│
▼ MCP / HTTP
┌─────────────────────────────────────┐
│ rpg-dice-mcp (separate service) │
│ POST /mcp │
└─────────────────────────────────────┘
REST endpoints default format=html_tor so the response includes a ready-to-render formatted field with <span class="fdie7">7</span>-style dice spans. The Svelte SPA uses {@html result.formatted} for the rendered dice faces, with .fdie*/.sdie* CSS classes loading background images from un-tor.github.io.
.
├── main.go Go entrypoint + //go:embed all:web/dist
├── internal/
│ ├── server/server.go HTTP server (REST + embed.FS)
│ ├── server/server_test.go
│ ├── mcpclient/client.go MCP client wrapper (Streamable HTTP)
│ └── mcpclient/client_test.go
├── web/ Svelte SPA source
│ ├── package.json
│ ├── vite.config.js
│ ├── index.html
│ └── src/
│ ├── App.svelte
│ ├── main.js
│ ├── app.css
│ ├── lib/
│ │ ├── api.js fetch() wrappers
│ │ └── history.js localStorage
│ └── components/
│ ├── SkillCheck.svelte
│ ├── CombatRoll.svelte
│ ├── GenericRoll.svelte
│ └── RollHistory.svelte
├── Dockerfile Multi-stage: vite build → go build → scratch
└── .github/workflows/
├── build.yaml Tests + :dev image push on push to main
└── docker-release.yaml Promote :dev to :v* on GH release
The Svelte SPA can run against the Go server via Vite's proxy (defined in vite.config.js). One terminal runs Go, the other runs Vite:
# Terminal 1 — Go server (listens on :8080)
TOR_DICE_MCP_URL=http://localhost:8081/mcp go run .
# Terminal 2 — Svelte dev (listens on :5173, proxies /api → :8080)
cd web && npm install && npm run dev
For an end-to-end smoke test without an in-cluster rpg-dice-mcp, run a local one:
# Terminal 3 — rpg-dice-mcp
cd ../rpg-dice-mcp-docker
go run . --http=:8081
docker build -t tor-dice:dev .
docker run --rm -p 8080:8080 \
-e TOR_DICE_MCP_URL=http://rpg-dice-mcp.example.com/mcp \
tor-dice:dev
| Env var | Default | Purpose |
|---|---|---|
TOR_DICE_LISTEN |
:8080 |
Bind address |
TOR_DICE_MCP_URL |
http://rpg-dice-mcp.rpg-dice-mcp.svc.cluster.local/mcp |
rpg-dice-mcp endpoint |
go test -cover ./...
internal/server: HTTP handlers tested against a fake DiceClient interface (89% coverage)internal/mcpclient: round-trip tests against an in-memory MCP server using the SDK's own NewStreamableHTTPHandler (74% coverage)MIT. Dice-face PNGs and Aniron / InitialRing fonts © their respective creators, sourced from un-tor.github.io.