# 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 ```go package main import ( "context" "net/http" "os" "gitea.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 gitea.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.