Final phase-1 step: the broker now starts. run() parses config, opens the store, builds the httpserver, and blocks on signal.NotifyContext until SIGTERM/SIGINT fires, at which point it drains through httpserver.Run's graceful-shutdown path and closes the store. --version is handled before config.Load so operators can inspect a binary without providing the rest of the config. flag.ErrHelp is passed through so -h exits 0. Config failure exits 2; runtime failure exits 1. Integration tests build the binary once in TestMain and exercise three acceptance scenarios against it: - --version: prints build info, exits 0 - no config: exits nonzero with stderr listing every missing field - full startup: /healthz returns 200 with correct JSON; SIGTERM triggers clean exit within 2s Closes forgejo-mcp-broker-t37. Phase 1 complete. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
2.6 KiB
Go
96 lines
2.6 KiB
Go
// 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 (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"kode.naiv.no/olemd/forgejo-mcp-broker/internal/buildinfo"
|
|
"kode.naiv.no/olemd/forgejo-mcp-broker/internal/config"
|
|
"kode.naiv.no/olemd/forgejo-mcp-broker/internal/httpserver"
|
|
brokerlog "kode.naiv.no/olemd/forgejo-mcp-broker/internal/log"
|
|
"kode.naiv.no/olemd/forgejo-mcp-broker/internal/store"
|
|
)
|
|
|
|
// Exit codes follow the usual convention: 0 success, 2 config/usage, 1 runtime.
|
|
const (
|
|
exitSuccess = 0
|
|
exitRuntime = 1
|
|
exitConfig = 2
|
|
)
|
|
|
|
func main() {
|
|
os.Exit(run(os.Args[1:], os.Stderr))
|
|
}
|
|
|
|
// run is the testable entry point. Parses config, wires dependencies, and
|
|
// blocks until the HTTP server exits or a shutdown signal arrives. Returns
|
|
// an OS exit code.
|
|
func run(args []string, out io.Writer) int {
|
|
// --version is handled before config.Load so operators can inspect a
|
|
// binary without providing the rest of the required config.
|
|
for _, a := range args {
|
|
if a == "--version" || a == "-version" {
|
|
fmt.Fprintf(out, "fjmcp-broker %s (rev %s, built %s)\n",
|
|
buildinfo.Version, buildinfo.GitRevision, buildinfo.BuildDate)
|
|
return exitSuccess
|
|
}
|
|
}
|
|
|
|
cfg, err := config.Load(args, out)
|
|
switch {
|
|
case errors.Is(err, flag.ErrHelp):
|
|
return exitSuccess
|
|
case err != nil:
|
|
fmt.Fprintln(out, "fjmcp-broker: config error:")
|
|
fmt.Fprintln(out, err.Error())
|
|
return exitConfig
|
|
}
|
|
|
|
logger := brokerlog.New(out, cfg.Debug)
|
|
logger.Info("starting broker",
|
|
"listen", cfg.Listen,
|
|
"public_url", cfg.PublicURL,
|
|
"forgejo_url", cfg.ForgejoURL,
|
|
"store_path", cfg.StorePath,
|
|
"max_sessions", cfg.MaxSessions,
|
|
"idle_timeout", cfg.SessionIdleTimeout.String(),
|
|
)
|
|
|
|
// Signal handling is owned here; the HTTP server just responds to ctx
|
|
// cancellation. This keeps internal/httpserver free of signal coupling
|
|
// and makes it testable without any OS-level wiring.
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
st, err := store.Open(ctx, cfg.StorePath)
|
|
if err != nil {
|
|
logger.Error("open store", "err", err.Error())
|
|
return exitRuntime
|
|
}
|
|
defer func() {
|
|
if err := st.Close(); err != nil {
|
|
logger.Error("close store", "err", err.Error())
|
|
}
|
|
}()
|
|
|
|
srv := &httpserver.Server{
|
|
Addr: cfg.Listen,
|
|
Log: logger,
|
|
Store: st,
|
|
}
|
|
if err := srv.Run(ctx); err != nil {
|
|
logger.Error("server exit", "err", err.Error())
|
|
return exitRuntime
|
|
}
|
|
logger.Info("broker stopped")
|
|
return exitSuccess
|
|
}
|