174 lines
4.2 KiB
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
|
|
}
|