Files
CHORUS/cmd/seqthink-wrapper/main.go

174 lines
4.2 KiB
Go

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
}