diff --git a/internal/config/config_core.go b/internal/config/config_core.go index 2356bff5..1aa72d58 100644 --- a/internal/config/config_core.go +++ b/internal/config/config_core.go @@ -198,6 +198,19 @@ func applyGatewayDefaults(cfg *GatewayConfig) { } } +// EnsureGatewayDefaults guarantees that cfg.Gateway is non-nil and that all +// gateway-level fields have sensible defaults applied. This matches the +// invariants enforced by the standard loaders (LoadFromFile, LoadFromStdin), +// and can be used by callers that construct Config values manually (e.g. in +// tests) to avoid nil-pointer panics and ensure consistent defaults. +func (cfg *Config) EnsureGatewayDefaults() { + if cfg.Gateway == nil { + cfg.Gateway = &GatewayConfig{} + } + applyGatewayDefaults(cfg.Gateway) + applyDefaults(cfg) +} + // LoadFromFile loads configuration from a TOML file. // // This function uses the BurntSushi/toml v1.6.0+ parser with TOML 1.1 support, diff --git a/internal/launcher/launcher.go b/internal/launcher/launcher.go index 6336459a..3285fc10 100644 --- a/internal/launcher/launcher.go +++ b/internal/launcher/launcher.go @@ -55,14 +55,13 @@ func New(ctx context.Context, cfg *config.Config) *Launcher { log.Println("[LAUNCHER] Detected running inside a container") } - // Get startup timeout from config, default to config.DefaultStartupTimeout seconds - startupTimeout := time.Duration(config.DefaultStartupTimeout) * time.Second - if cfg.Gateway != nil && cfg.Gateway.StartupTimeout > 0 { - startupTimeout = time.Duration(cfg.Gateway.StartupTimeout) * time.Second - logLauncher.Printf("Using configured startup timeout: %v", startupTimeout) - } else { - logLauncher.Printf("Using default startup timeout: %v", startupTimeout) - } + // Guarantee cfg.Gateway is non-nil with defaults applied. + // LoadFromFile/LoadFromStdin already ensure this, but tests may + // construct configs manually without going through the load path. + cfg.EnsureGatewayDefaults() + + startupTimeout := time.Duration(cfg.Gateway.StartupTimeout) * time.Second + logLauncher.Printf("Using startup timeout: %v", startupTimeout) // Initialize OIDC provider from environment if available var oidcProvider *oidc.Provider diff --git a/internal/launcher/launcher_test.go b/internal/launcher/launcher_test.go index 7d0717fe..c3016e28 100644 --- a/internal/launcher/launcher_test.go +++ b/internal/launcher/launcher_test.go @@ -527,6 +527,7 @@ func TestGetOrLaunchForSession_SessionReuse(t *testing.T) { ctx := context.Background() cfg := &config.Config{ Servers: map[string]*config.ServerConfig{}, + Gateway: &config.GatewayConfig{StartupTimeout: config.DefaultStartupTimeout}, } l := New(ctx, cfg) defer l.Close() @@ -606,7 +607,8 @@ func TestLauncher_StartupTimeout(t *testing.T) { } func TestLauncher_TimeoutWithNilGateway(t *testing.T) { - // Test that launcher uses default timeout when Gateway config is nil + // Test that launcher uses default timeout when Gateway config is nil. + // EnsureGatewayDefaults is called by launcher.New, so nil Gateway is safe. ctx := context.Background() cfg := &config.Config{ Servers: map[string]*config.ServerConfig{ @@ -615,7 +617,7 @@ func TestLauncher_TimeoutWithNilGateway(t *testing.T) { URL: "http://example.com", }, }, - Gateway: nil, // No gateway config + Gateway: nil, // No gateway config — EnsureGatewayDefaults fills defaults } l := New(ctx, cfg) diff --git a/internal/server/unified.go b/internal/server/unified.go index a515197b..1df1d201 100644 --- a/internal/server/unified.go +++ b/internal/server/unified.go @@ -110,25 +110,14 @@ type UnifiedServer struct { // NewUnified creates a new unified MCP server func NewUnified(ctx context.Context, cfg *config.Config) (*UnifiedServer, error) { logUnified.Printf("Creating new unified server: sequentialLaunch=%v, servers=%d", cfg.SequentialLaunch, len(cfg.Servers)) - l := launcher.New(ctx, cfg) - - // Get payload directory from config, with fallback to default - payloadDir := config.DefaultPayloadDir - if cfg.Gateway != nil && cfg.Gateway.PayloadDir != "" { - payloadDir = cfg.Gateway.PayloadDir - } - // Get payload path prefix from config (empty by default) - payloadPathPrefix := "" - if cfg.Gateway != nil && cfg.Gateway.PayloadPathPrefix != "" { - payloadPathPrefix = cfg.Gateway.PayloadPathPrefix - } + l := launcher.New(ctx, cfg) - // Get payload size threshold from config, with fallback to default - payloadSizeThreshold := config.DefaultPayloadSizeThreshold - if cfg.Gateway != nil && cfg.Gateway.PayloadSizeThreshold > 0 { - payloadSizeThreshold = cfg.Gateway.PayloadSizeThreshold - } + // Config loading guarantees cfg.Gateway is non-nil and all fields + // have defaults applied via applyGatewayDefaults/applyDefaults. + payloadDir := cfg.Gateway.PayloadDir + payloadPathPrefix := cfg.Gateway.PayloadPathPrefix + payloadSizeThreshold := cfg.Gateway.PayloadSizeThreshold logUnified.Printf("Payload configuration: dir=%s, pathPrefix=%s, sizeThreshold=%d bytes (%.2f KB)", payloadDir, payloadPathPrefix, payloadSizeThreshold, float64(payloadSizeThreshold)/1024)