// Package config loads MCP Nexus's declarative YAML configuration. Per // ARCHITECTURE tenet #2, configuration is the primary way to express desired // state; in later phases discovery/recipes will also feed it. package config import ( "fmt" "os" "strings" "gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/domain" "gopkg.in/yaml.v3" ) // Config is the top-level configuration document. type Config struct { Server ServerConfig `yaml:"server"` Gateway GatewayConfig `yaml:"gateway"` Upstreams []UpstreamConfig `yaml:"upstreams"` } // ServerConfig configures the HTTP listener. type ServerConfig struct { Address string `yaml:"address"` } // GatewayConfig configures the agent-facing MCP server identity. type GatewayConfig struct { Name string `yaml:"name"` Version string `yaml:"version"` // MCPPath is the HTTP path the aggregated MCP endpoint is served on. MCPPath string `yaml:"mcp_path"` } // UpstreamConfig statically declares an upstream MCP server (Phase 1). Later // phases populate instances via discovery + installer instead. type UpstreamConfig struct { Namespace string `yaml:"namespace"` Transport string `yaml:"transport"` // "stdio" | "http" Command string `yaml:"command,omitempty"` Args []string `yaml:"args,omitempty"` Env []string `yaml:"env,omitempty"` Dir string `yaml:"dir,omitempty"` Endpoint string `yaml:"endpoint,omitempty"` Headers map[string]string `yaml:"headers,omitempty"` } // Default returns a config populated with sensible defaults. func Default() *Config { return &Config{ Server: ServerConfig{Address: ":8080"}, Gateway: GatewayConfig{Name: "MCP Nexus", Version: "0.1.0-dev", MCPPath: "/mcp"}, } } // Load reads, expands environment variables in, and validates a config file. func Load(path string) (*Config, error) { raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config: %w", err) } expanded := os.ExpandEnv(string(raw)) cfg := Default() if err := yaml.Unmarshal([]byte(expanded), cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } cfg.applyDefaults() if err := cfg.Validate(); err != nil { return nil, err } return cfg, nil } func (c *Config) applyDefaults() { if c.Server.Address == "" { c.Server.Address = ":8080" } if c.Gateway.Name == "" { c.Gateway.Name = "MCP Nexus" } if c.Gateway.MCPPath == "" { c.Gateway.MCPPath = "/mcp" } if !strings.HasPrefix(c.Gateway.MCPPath, "/") { c.Gateway.MCPPath = "/" + c.Gateway.MCPPath } } // Validate checks the config for internal consistency. func (c *Config) Validate() error { seen := map[string]bool{} for i, u := range c.Upstreams { if u.Namespace == "" { return fmt.Errorf("upstreams[%d]: namespace is required", i) } if seen[u.Namespace] { return fmt.Errorf("upstreams[%d]: duplicate namespace %q", i, u.Namespace) } seen[u.Namespace] = true switch domain.TransportKind(u.Transport) { case domain.TransportStdio: if u.Command == "" { return fmt.Errorf("upstreams[%d] (%s): stdio transport requires command", i, u.Namespace) } case domain.TransportHTTP: if u.Endpoint == "" { return fmt.Errorf("upstreams[%d] (%s): http transport requires endpoint", i, u.Namespace) } default: return fmt.Errorf("upstreams[%d] (%s): unknown transport %q (want stdio|http)", i, u.Namespace, u.Transport) } } return nil }