package main import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" "chorus/pkg/seqthink/mcpclient" "chorus/pkg/seqthink/observability" "chorus/pkg/seqthink/proxy" "github.com/rs/zerolog/log" ) // Config holds the wrapper configuration type Config struct { Port string MCPLocalURL string LogLevel string MaxBodyMB int HealthTimeout time.Duration ShutdownTimeout time.Duration AgeIdentPath string AgeRecipsPath string KachingJWKSURL string RequiredScope string } func loadConfig() *Config { return &Config{ Port: getEnv("PORT", "8443"), MCPLocalURL: getEnv("MCP_LOCAL", "http://127.0.0.1:8000"), LogLevel: getEnv("LOG_LEVEL", "info"), MaxBodyMB: getEnvInt("MAX_BODY_MB", 4), HealthTimeout: 5 * time.Second, ShutdownTimeout: 30 * time.Second, AgeIdentPath: getEnv("AGE_IDENT_PATH", ""), AgeRecipsPath: getEnv("AGE_RECIPS_PATH", ""), KachingJWKSURL: getEnv("KACHING_JWKS_URL", ""), RequiredScope: getEnv("REQUIRED_SCOPE", "sequentialthinking.run"), } } func main() { cfg := loadConfig() // Initialize observability observability.InitLogger(cfg.LogLevel) metrics := observability.InitMetrics() log.Info(). Str("port", cfg.Port). Str("mcp_url", cfg.MCPLocalURL). Str("version", "0.1.0-beta2"). Msg("🚀 Starting Sequential Thinking Age Wrapper") // Create MCP client mcpClient := mcpclient.New(cfg.MCPLocalURL) // Wait for MCP server to be ready ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() log.Info().Msg("⏳ Waiting for MCP server...") if err := waitForMCP(ctx, mcpClient); err != nil { log.Fatal().Err(err).Msg("❌ MCP server not ready") } log.Info().Msg("✅ MCP server ready") // Create proxy server proxyServer, err := proxy.NewServer(proxy.ServerConfig{ MCPClient: mcpClient, Metrics: metrics, MaxBodyMB: cfg.MaxBodyMB, AgeIdentPath: cfg.AgeIdentPath, AgeRecipsPath: cfg.AgeRecipsPath, KachingJWKSURL: cfg.KachingJWKSURL, RequiredScope: cfg.RequiredScope, }) if err != nil { log.Fatal().Err(err).Msg("❌ Failed to create proxy server") } // Setup HTTP server srv := &http.Server{ Addr: ":" + cfg.Port, Handler: proxyServer.Handler(), ReadTimeout: 30 * time.Second, WriteTimeout: 90 * time.Second, IdleTimeout: 120 * time.Second, } // Start server in goroutine go func() { log.Info(). Str("addr", srv.Addr). Bool("encryption_enabled", cfg.AgeIdentPath != ""). Bool("policy_enabled", cfg.KachingJWKSURL != ""). Msg("🔐 Wrapper listening") if err := srv.ListenAndServe(); err != http.ErrServerClosed { log.Fatal().Err(err).Msg("❌ HTTP server failed") } }() // Wait for shutdown signal sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-sigChan log.Info().Msg("🛑 Shutting down gracefully...") // Graceful shutdown shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout) defer shutdownCancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Error().Err(err).Msg("⚠️ Shutdown error") } log.Info().Msg("✅ Shutdown complete") } // waitForMCP waits for MCP server to be ready func waitForMCP(ctx context.Context, client *mcpclient.Client) error { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return fmt.Errorf("timeout waiting for MCP server") case <-ticker.C: if err := client.Health(ctx); err == nil { return nil } log.Debug().Msg("Waiting for MCP server...") } } } // getEnv gets environment variable with default func getEnv(key, defaultVal string) string { if val := os.Getenv(key); val != "" { return val } return defaultVal } // getEnvInt gets environment variable as int with default func getEnvInt(key string, defaultVal int) int { val := os.Getenv(key) if val == "" { return defaultVal } var result int if _, err := fmt.Sscanf(val, "%d", &result); err != nil { log.Warn(). Str("key", key). Str("value", val). Int("default", defaultVal). Msg("Invalid integer env var, using default") return defaultVal } return result }