mathias 6f3467fa47
CI / Lint / Test / Vet (push) Successful in 7s
ci: add CI — this repo verified nothing on push
Until now no .gitea/workflows step ran anything here: no build, no test, no
vet, no lint. A push could break the repo and nothing would say so. Found by
infra scripts/check-task-properties.py, a gate that asserts what CI RUNS
rather than what a repo contains (infra v1.6.0).

Runs lint, vet and test. setup-go carries cache: false, matching the rest of
the estate -- this runner keeps GOMODCACHE warm on its own volume, so
setup-go cache restore can only collide with it (infra#360).

Verified before adding, so switching verification on could not turn the repo
red: go build, go vet, go test -race and golangci-lint against the canonical
.golangci.yml all pass at 0 issues. Each command was run exactly as the
workflow spells it, not reasoned about.

Worth noting for this repo in particular: mcp-chassis is a shared library --
gitea-mcp imports it for auth -- so it was the dependency with the least
verification and the widest blast radius.
2026-09-05 16:49:39 +02:00

mcp-chassis

Shared Go library for Mathias-owned MCP servers. Provides the auth + middleware primitives that every MCP server needs.

Why

By 2026-05-22 there were three+ MCP servers (gitea-mcp, brain-mcp / ingestion, future ones from template-go-agent) each carrying their own near-identical:

  • Dex JWT validator (~80 LOC, identical jwx/v2 plumbing)
  • Bearer middleware (~50 LOC, dual-mode static + JWT)
  • RFC 9728 protected-resource metadata handler (~25 LOC)

The homelab architecture review's spike S3 (see gitea.d-ma.be/mathias/infra/docs/superpowers/handoffs/2026-05-22-mcp-chassis-spike.md) concluded a thin shared lib pays for itself within the first migration. This is that lib.

Non-goals

  • Replacing each MCP's tool registration / handler logic — that is per-domain.
  • Solving HTTP routing — consumers keep their own http.ServeMux.
  • Solving observability — see gitea.d-ma.be/mathias/hyperguild/ingestion/internal/metrics for the hand-rolled Prometheus pattern. May absorb a metrics subpackage here later, once a second consumer needs it.

Packages

auth

  • JWTValidator — Dex OIDC JWT validation. nil is a valid value meaning "JWT auth disabled".
  • BearerMiddleware — static-Bearer-or-Dex-JWT gate. Static wins first; only emits WWW-Authenticate: Bearer ... resource_metadata=... on 401 when resourceMetadataURL is non-empty (claude.ai OAuth discovery).
  • ProtectedResourceHandler — RFC 9728 metadata document for GET /.well-known/oauth-protected-resource.

Usage

package main

import (
	"context"
	"net/http"
	"os"

	"git.d-ma.be/mathias/mcp-chassis/auth"
)

func main() {
	staticToken := os.Getenv("BRAIN_MCP_TOKEN")
	dexIssuer := os.Getenv("DEX_ISSUER_URL")
	audience := os.Getenv("MCP_AUDIENCE")
	resourceURL := os.Getenv("MCP_RESOURCE_URL")

	validator, err := auth.NewJWTValidator(context.Background(), dexIssuer, audience)
	if err != nil {
		panic(err)
	}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /.well-known/oauth-protected-resource",
		auth.ProtectedResourceHandler(resourceURL, dexIssuer))
	mux.Handle("/mcp", auth.BearerMiddleware(
		staticToken,
		validator,
		"brain",
		resourceURL+"/.well-known/oauth-protected-resource",
		mcpHandler(),
	))

	_ = http.ListenAndServe(":3300", mux)
}

func mcpHandler() http.Handler { /* per-domain */ return nil }

Versioning

Trunk-based development on main. Tagged with semver. Consumers pin specific tags (go.mod require git.d-ma.be/mathias/mcp-chassis v0.x.y) and bump deliberately.

Migrations are documented per-consumer in the consumer's CHANGELOG / commits.

Dependencies

  • github.com/lestrrat-go/jwx/v2 — JWKS cache + JWT parsing. Same dep every MCP already had; no new transitive cost when adopting the chassis.
  • github.com/stretchr/testify — tests only.

stdlib otherwise.

S
Description
Shared Go library for Mathias-owned MCP servers — Dex JWT validator + Bearer middleware + RFC 9728 protected-resource metadata. Hand-rolled (stdlib + lestrrat-go/jwx) per the homelab architecture review.
Readme
101 KiB
Languages
Go 100%