package mcp import ( "fmt" "os" "strconv" "strings" "gopkg.in/yaml.v3" ) // Load reads MCP server configuration from the process environment. It returns // every configured server; callers filter on Enabled. func Load() (*Config, error) { return FromEnv(os.Environ()) } // FromEnv builds a Config from a KEY=value pair slice such as os.Environ() or a // test fixture. Named servers are read from MCP__* for indexes; the // unnumbered MCP_* variables describe a single default server. When N is a // positive index, one server is read per group and scanning stops at the first // group with no NAME/COMMAND/URL set. func FromEnv(pairs []string) (*Config, error) { env := make(map[string]string, len(pairs)) for _, p := range pairs { k, v, ok := strings.Cut(p, "=") if ok { env[k] = v } } return &Config{Servers: fromEnvMap(env)}, nil } // FromYAML parses a YAML document describing MCP servers, for example: // // servers: // - name: calc // command: mcp-calc // args: [--stdio] // enabled: true func FromYAML(data []byte) (*Config, error) { var doc struct { Servers []MCPConfig `yaml:"servers"` } if err := yaml.Unmarshal(data, &doc); err != nil { return nil, fmt.Errorf("mcp config: %w", err) } return &Config{Servers: doc.Servers}, nil } // fromEnvMap builds the server list from a resolved environment map. func fromEnvMap(env map[string]string) []MCPConfig { var servers []MCPConfig if cfg, ok := readOne(env, "MCP"); ok { servers = append(servers, cfg) } for i := 0; ; i++ { cfg, ok := readOne(env, "MCP_"+strconv.Itoa(i)) if !ok { break } servers = append(servers, cfg) } return servers } // readOne reads a single MCPConfig from env using the given variable prefix // (e.g. "MCP" or "MCP_0"). It reports ok=false when no identifying field is set. func readOne(env map[string]string, prefix string) (MCPConfig, bool) { p := prefix if p == "" { p = "MCP" } name := env[p+"_NAME"] command := env[p+"_COMMAND"] url := env[p+"_URL"] transp := env[p+"_TRANSPORT"] if name == "" && command == "" && url == "" { return MCPConfig{}, false } return MCPConfig{ Name: name, Command: command, Args: splitArgs(env[p+"_ARGS"]), URL: url, TransportType: transp, Enabled: parseBool(env[p+"_ENABLED"], false), }, true } // splitArgs splits a comma-separated args value, trimming spaces and empty // entries. func splitArgs(raw string) []string { raw = strings.TrimSpace(raw) if raw == "" { return nil } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) for _, p := range parts { if t := strings.TrimSpace(p); t != "" { out = append(out, t) } } return out } // parseBool parses a boolean value, defaulting to def on empty or invalid input. func parseBool(raw string, def bool) bool { raw = strings.TrimSpace(raw) if raw == "" { return def } b, err := strconv.ParseBool(strings.ToLower(raw)) if err != nil { return def } return b }