2026-04-24 16:54:27 +02:00
|
|
|
// Command fjmcp-broker is an OAuth 2.1 authorization server and MCP session
|
2026-04-24 17:29:37 +02:00
|
|
|
// broker that fronts forgejo-mcp. See ../../README.md and ../../docs/ for
|
|
|
|
|
// the design.
|
2026-04-24 16:54:27 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-24 17:29:37 +02:00
|
|
|
"context"
|
|
|
|
|
"errors"
|
2026-04-24 16:54:27 +02:00
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
2026-04-24 17:29:37 +02:00
|
|
|
"io"
|
2026-04-24 16:54:27 +02:00
|
|
|
"os"
|
2026-04-24 17:29:37 +02:00
|
|
|
"os/signal"
|
|
|
|
|
"syscall"
|
2026-04-24 16:54:27 +02:00
|
|
|
|
|
|
|
|
"kode.naiv.no/olemd/forgejo-mcp-broker/internal/buildinfo"
|
2026-04-24 17:29:37 +02:00
|
|
|
"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
|
2026-04-24 16:54:27 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func main() {
|
2026-04-24 17:29:37 +02:00
|
|
|
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
|
2026-04-24 16:54:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 17:29:37 +02:00
|
|
|
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
|
2026-04-24 16:54:27 +02:00
|
|
|
}
|