diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl new file mode 100644 index 0000000..427883a --- /dev/null +++ b/.beads/issues.jsonl @@ -0,0 +1,5 @@ +{"id":"forgejo-mcp-broker-8ei","title":"Phase 1: internal/httpserver with /healthz and graceful shutdown","description":"Implement internal/httpserver: constructs a *http.Server bound to cfg.Listen, mounts GET /healthz (returns 200 with JSON build-info from the build-info package, including version, git revision, build date, and current store status), handles SIGTERM/SIGINT by initiating graceful shutdown with a configurable deadline (default 10s). Uses log/slog for structured JSON logs. Exposes a Run(ctx) error method that blocks until shutdown completes.","acceptance_criteria":"go test ./internal/httpserver passes; GET /healthz returns expected JSON; sending SIGTERM causes Run to return nil within 2 seconds after in-flight requests complete; slow in-flight request is allowed to finish within the deadline, then forcibly closed.","status":"open","priority":1,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-24T14:46:20Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-24T14:46:20Z","dependencies":[{"issue_id":"forgejo-mcp-broker-8ei","depends_on_id":"forgejo-mcp-broker-n84","type":"blocks","created_at":"2026-04-24T16:46:19Z","created_by":"Ole-Morten Duesund","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"forgejo-mcp-broker-t37","title":"Phase 1: wire cmd/broker/main.go and integration test","description":"Final phase 1 task: wire config → log → store → httpserver in cmd/broker/main.go. Parse config, init slog, open store, start httpserver, wait for shutdown signal, close store, exit. Add an integration test under cmd/broker/ that builds the binary, runs it with a valid env + temp store path, curls /healthz, sends SIGTERM, verifies clean exit within 2s. This is the acceptance gate for phase 1.","acceptance_criteria":"make build; make test (incl. integration) pass; running the binary with missing config fails with a clear error; running with valid config serves /healthz; SIGTERM shuts down cleanly within 2s; /healthz JSON includes version, git revision, build date, and store OK status.","status":"open","priority":1,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-24T14:46:20Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-24T14:46:20Z","dependencies":[{"issue_id":"forgejo-mcp-broker-t37","depends_on_id":"forgejo-mcp-broker-8ei","type":"blocks","created_at":"2026-04-24T16:48:29Z","created_by":"Ole-Morten Duesund","metadata":"{}"},{"issue_id":"forgejo-mcp-broker-t37","depends_on_id":"forgejo-mcp-broker-9jh","type":"blocks","created_at":"2026-04-24T16:48:29Z","created_by":"Ole-Morten Duesund","metadata":"{}"},{"issue_id":"forgejo-mcp-broker-t37","depends_on_id":"forgejo-mcp-broker-9nq","type":"blocks","created_at":"2026-04-24T16:48:28Z","created_by":"Ole-Morten Duesund","metadata":"{}"},{"issue_id":"forgejo-mcp-broker-t37","depends_on_id":"forgejo-mcp-broker-n84","type":"blocks","created_at":"2026-04-24T16:48:28Z","created_by":"Ole-Morten Duesund","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} +{"id":"forgejo-mcp-broker-9jh","title":"Phase 1: internal/store with SQLite open and embedded schema migrations","description":"Implement internal/store: wraps a modernc.org/sqlite connection, applies embedded schema migrations in order via a schema_migrations table, exposes a *sql.DB and a Close method. Phase 1 schema is just the migrations table itself plus a health_check row — real tables (clients, auth_codes, access_tokens, refresh_tokens) ship in phase 2. Store_path from config; creates parent dirs if missing; fails fast on unwritable path. Migrations embedded via embed.FS under internal/store/migrations/.","acceptance_criteria":"go test ./internal/store passes; opening a fresh db file applies migrations; re-opening is idempotent (no re-application, no errors); corrupt/locked files yield a clear error; Close() leaves no file handles open.","status":"open","priority":1,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-24T14:46:19Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-24T14:46:19Z","dependencies":[{"issue_id":"forgejo-mcp-broker-9jh","depends_on_id":"forgejo-mcp-broker-n84","type":"blocks","created_at":"2026-04-24T16:46:19Z","created_by":"Ole-Morten Duesund","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"forgejo-mcp-broker-9nq","title":"Phase 1: internal/config package with flag + env parsing and validation","description":"Implement internal/config: a Config struct populated from CLI flags and environment variables (flags win), with validation at startup. Parse public-url, listen addr, forgejo-url, forgejo-oauth-client-id/secret/scopes, forgejo-mcp-binary, store-path, max-sessions, session-idle-timeout, debug. Validation: required fields present and non-empty; public-url parses as an https URL; store-path writable; idle-timeout positive; max-sessions positive. Unit tests cover happy path + every validation error branch.","acceptance_criteria":"go test ./internal/config passes with \u003e=90% coverage; missing required env produces a clear error message listing all missing fields; flag values override env; --help prints all config options.","status":"open","priority":1,"issue_type":"task","owner":"olemd@glemt.net","created_at":"2026-04-24T14:46:19Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-24T14:46:19Z","dependencies":[{"issue_id":"forgejo-mcp-broker-9nq","depends_on_id":"forgejo-mcp-broker-n84","type":"blocks","created_at":"2026-04-24T16:46:18Z","created_by":"Ole-Morten Duesund","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"forgejo-mcp-broker-n84","title":"Phase 1: bootstrap Go project layout","description":"Set up the Go project skeleton so all subsequent phase 1 packages have somewhere to live. Initialize go.mod with module path kode.naiv.no/olemd/forgejo-mcp-broker, create the directory layout (cmd/broker, internal/config, internal/log, internal/store, internal/httpserver), add a Makefile with build/test/lint targets, and wire build-info injection (version, git revision, build date) via -ldflags.","acceptance_criteria":"go.mod present with correct module path; make build produces ./fjmcp-broker binary; make test and make lint targets exist and pass against an empty codebase; binary prints --version with injected build info.","status":"in_progress","priority":1,"issue_type":"task","assignee":"Ole-Morten Duesund","owner":"olemd@glemt.net","created_at":"2026-04-24T14:45:44Z","created_by":"Ole-Morten Duesund","updated_at":"2026-04-24T14:50:57Z","started_at":"2026-04-24T14:50:57Z","dependency_count":0,"dependent_count":4,"comment_count":0} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..56f8e15 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +# forgejo-mcp-broker Makefile + +BINARY := fjmcp-broker +CMD_PKG := ./cmd/broker +MODULE := kode.naiv.no/olemd/forgejo-mcp-broker +VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +GIT_REV := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) +BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) +LDFLAGS := -s -w \ + -X $(MODULE)/internal/buildinfo.Version=$(VERSION) \ + -X $(MODULE)/internal/buildinfo.GitRevision=$(GIT_REV) \ + -X $(MODULE)/internal/buildinfo.BuildDate=$(BUILD_DATE) + +.PHONY: all build test lint tidy clean help + +all: build + +build: ## Build the broker binary + go build -trimpath -ldflags '$(LDFLAGS)' -o $(BINARY) $(CMD_PKG) + +test: ## Run tests with the race detector + go test -race ./... + +lint: ## Static analysis (go vet; golangci-lint if installed) + go vet ./... + @if command -v golangci-lint >/dev/null 2>&1; then \ + golangci-lint run; \ + else \ + echo "golangci-lint not installed; skipping (go vet already ran)"; \ + fi + +tidy: ## Tidy go.mod / go.sum + go mod tidy + +clean: ## Remove build artefacts + rm -f $(BINARY) + +help: ## Show available targets + @awk 'BEGIN {FS = ":.*##"; print "Targets:"} /^[a-zA-Z_-]+:.*?##/ { printf " %-8s %s\n", $$1, $$2 }' $(MAKEFILE_LIST) diff --git a/cmd/broker/main.go b/cmd/broker/main.go new file mode 100644 index 0000000..2bdb185 --- /dev/null +++ b/cmd/broker/main.go @@ -0,0 +1,31 @@ +// Command fjmcp-broker is an OAuth 2.1 authorization server and MCP session +// broker that fronts forgejo-mcp. See ../../README.md and ../../docs/ for the +// design. +package main + +import ( + "flag" + "fmt" + "os" + + "kode.naiv.no/olemd/forgejo-mcp-broker/internal/buildinfo" +) + +func main() { + var showVersion bool + flag.BoolVar(&showVersion, "version", false, "print build info and exit") + flag.Parse() + + if showVersion { + fmt.Printf("fjmcp-broker %s (rev %s, built %s)\n", + buildinfo.Version, buildinfo.GitRevision, buildinfo.BuildDate) + return + } + + // Full startup wiring (config → log → store → httpserver) lands in + // forgejo-mcp-broker-t37. Until then this binary only serves --version + // so the bootstrap acceptance criteria can be exercised. + fmt.Fprintln(os.Stderr, "fjmcp-broker: runtime wiring not yet implemented (phase 1 in progress)") + fmt.Fprintln(os.Stderr, "Use --version to print build info.") + os.Exit(2) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..31500ab --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module kode.naiv.no/olemd/forgejo-mcp-broker + +go 1.26 diff --git a/internal/buildinfo/buildinfo.go b/internal/buildinfo/buildinfo.go new file mode 100644 index 0000000..e9a5d19 --- /dev/null +++ b/internal/buildinfo/buildinfo.go @@ -0,0 +1,12 @@ +// Package buildinfo exposes compile-time build metadata. +// +// Values are injected at link time via -ldflags -X (see the Makefile). When +// the binary is built without those flags (e.g. go run, go test), the +// placeholder defaults below are used. +package buildinfo + +var ( + Version = "dev" + GitRevision = "unknown" + BuildDate = "unknown" +) diff --git a/internal/buildinfo/buildinfo_test.go b/internal/buildinfo/buildinfo_test.go new file mode 100644 index 0000000..6e5cd99 --- /dev/null +++ b/internal/buildinfo/buildinfo_test.go @@ -0,0 +1,21 @@ +package buildinfo_test + +import ( + "testing" + + "kode.naiv.no/olemd/forgejo-mcp-broker/internal/buildinfo" +) + +func TestDefaultsArePopulated(t *testing.T) { + // Build-info placeholders must never be empty: they are exposed on /healthz + // and an empty value would be observable to operators as a broken build. + if buildinfo.Version == "" { + t.Error("Version is empty") + } + if buildinfo.GitRevision == "" { + t.Error("GitRevision is empty") + } + if buildinfo.BuildDate == "" { + t.Error("BuildDate is empty") + } +} diff --git a/internal/config/doc.go b/internal/config/doc.go new file mode 100644 index 0000000..90f26ef --- /dev/null +++ b/internal/config/doc.go @@ -0,0 +1,5 @@ +// Package config loads broker configuration from flags and environment +// variables, applies defaults, and validates the result. +// +// Implementation lands in forgejo-mcp-broker-9nq. +package config diff --git a/internal/httpserver/doc.go b/internal/httpserver/doc.go new file mode 100644 index 0000000..90d2c2d --- /dev/null +++ b/internal/httpserver/doc.go @@ -0,0 +1,6 @@ +// Package httpserver hosts the broker's HTTP surface: OAuth endpoints, the +// gated MCP endpoint, and /healthz. Owns an *http.Server with graceful +// shutdown on SIGTERM / SIGINT. +// +// Implementation lands in forgejo-mcp-broker-8ei. +package httpserver diff --git a/internal/log/doc.go b/internal/log/doc.go new file mode 100644 index 0000000..da7abee --- /dev/null +++ b/internal/log/doc.go @@ -0,0 +1,6 @@ +// Package log constructs the process-wide structured logger (log/slog, JSON +// handler to stderr) and provides small helpers for attaching build-info and +// request-scoped fields. +// +// Implementation lands alongside forgejo-mcp-broker-8ei / t37. +package log diff --git a/internal/store/doc.go b/internal/store/doc.go new file mode 100644 index 0000000..82162b8 --- /dev/null +++ b/internal/store/doc.go @@ -0,0 +1,6 @@ +// Package store opens the SQLite-backed persistence layer used by the broker +// for OAuth clients, authorization codes, access tokens, and refresh tokens. +// Migrations are embedded via embed.FS and applied on open. +// +// Implementation lands in forgejo-mcp-broker-9jh. +package store