feat(cmd/broker): wire config → log → store → httpserver (forgejo-mcp-broker-t37)
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>
This commit is contained in:
parent
36722940eb
commit
09fcdc5af4
3 changed files with 265 additions and 18 deletions
|
|
@ -1,31 +1,96 @@
|
|||
// 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.
|
||||
// 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() {
|
||||
var showVersion bool
|
||||
flag.BoolVar(&showVersion, "version", false, "print build info and exit")
|
||||
flag.Parse()
|
||||
os.Exit(run(os.Args[1:], os.Stderr))
|
||||
}
|
||||
|
||||
if showVersion {
|
||||
fmt.Printf("fjmcp-broker %s (rev %s, built %s)\n",
|
||||
buildinfo.Version, buildinfo.GitRevision, buildinfo.BuildDate)
|
||||
return
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue