Build and publish / Test and build (darwin) (push) Successful in 2m4s
Build and publish / Test and build (linux) (push) Successful in 3m3s
Build and publish / Test and build (windows) (push) Successful in 2m50s
Build and publish / Build and publish Docker image (push) Successful in 2m4s
A set-but-empty environment variable wiped the corresponding .env value, so an EMBY_SERVER_URL='' guard on CI broke the config tests. Empty variables now count as unset, and the tests clear the config env keys explicitly for hermeticity.
143 lines
4.6 KiB
Go
143 lines
4.6 KiB
Go
// Package config loads Emby.MCP settings from a .env file and/or the process
|
|
// environment, matching the semantics of the original Python implementation
|
|
// (python-dotenv's dotenv_values for our key set).
|
|
package config
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Transport names for MCP_TRANSPORT.
|
|
const (
|
|
TransportStdio = "stdio"
|
|
TransportHTTP = "http" // MCP streamable HTTP (SSE streaming), endpoint /mcp
|
|
)
|
|
|
|
// Config holds the Emby.MCP runtime configuration.
|
|
type Config struct {
|
|
ServerURL string // EMBY_SERVER_URL, e.g. "http://localhost:8096"
|
|
Username string // EMBY_USERNAME
|
|
Password string // EMBY_PASSWORD
|
|
APIKey string // EMBY_API_KEY (alternative to username/password)
|
|
UserID string // EMBY_USER_ID (required with API key auth)
|
|
VerifySSL bool // EMBY_VERIFY_SSL, default true
|
|
MaxChunkSize int // LLM_MAX_ITEMS; 0 or negative means no chunking limit
|
|
Transport string // MCP_TRANSPORT: "stdio" (default) or "http"
|
|
ListenAddr string // MCP_LISTEN_ADDR, default "127.0.0.1:8080"
|
|
SessionTimeout time.Duration // MCP_SESSION_TIMEOUT, default 30m; <=0 disables
|
|
}
|
|
|
|
// Load reads the .env file at path (if it exists), lets real environment
|
|
// variables override file values, and validates the result.
|
|
func Load(path string) (*Config, error) {
|
|
vals := map[string]string{}
|
|
if path != "" {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot find the .env file. Ensure that it exists in the same directory as the binary")
|
|
}
|
|
defer f.Close()
|
|
if err := parseDotEnv(f, vals); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
// Real environment overrides file values; empty variables are unset.
|
|
for _, k := range []string{
|
|
"EMBY_SERVER_URL", "EMBY_USERNAME", "EMBY_PASSWORD",
|
|
"EMBY_API_KEY", "EMBY_USER_ID", "EMBY_VERIFY_SSL", "LLM_MAX_ITEMS",
|
|
"MCP_TRANSPORT", "MCP_LISTEN_ADDR", "MCP_SESSION_TIMEOUT",
|
|
} {
|
|
if v, ok := os.LookupEnv(k); ok && v != "" {
|
|
vals[k] = v
|
|
}
|
|
}
|
|
|
|
cfg := &Config{
|
|
ServerURL: strings.TrimRight(vals["EMBY_SERVER_URL"], "/"),
|
|
Username: vals["EMBY_USERNAME"],
|
|
Password: vals["EMBY_PASSWORD"],
|
|
APIKey: vals["EMBY_API_KEY"],
|
|
UserID: vals["EMBY_USER_ID"],
|
|
VerifySSL: strToBool(defaultStr(vals["EMBY_VERIFY_SSL"], "True")),
|
|
}
|
|
if v, err := strconv.Atoi(strings.TrimSpace(vals["LLM_MAX_ITEMS"])); err == nil {
|
|
cfg.MaxChunkSize = v
|
|
}
|
|
cfg.Transport = strings.ToLower(defaultStr(vals["MCP_TRANSPORT"], TransportStdio))
|
|
if cfg.Transport != TransportStdio && cfg.Transport != TransportHTTP {
|
|
return nil, fmt.Errorf("invalid MCP_TRANSPORT %q: expected %q or %q", cfg.Transport, TransportStdio, TransportHTTP)
|
|
}
|
|
cfg.ListenAddr = defaultStr(vals["MCP_LISTEN_ADDR"], "127.0.0.1:8080")
|
|
cfg.SessionTimeout = 30 * time.Minute
|
|
if s := strings.TrimSpace(vals["MCP_SESSION_TIMEOUT"]); s != "" {
|
|
d, err := time.ParseDuration(s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid MCP_SESSION_TIMEOUT %q: %v", s, err)
|
|
}
|
|
cfg.SessionTimeout = d
|
|
}
|
|
|
|
if cfg.ServerURL == "" {
|
|
return nil, fmt.Errorf("missing required variable EMBY_SERVER_URL")
|
|
}
|
|
// HTTP mode authenticates each request against Emby; server-side
|
|
// credentials are only required for stdio mode.
|
|
if cfg.Transport == TransportHTTP {
|
|
return cfg, nil
|
|
}
|
|
if cfg.APIKey != "" {
|
|
if cfg.UserID == "" {
|
|
return nil, fmt.Errorf("EMBY_USER_ID is required when using EMBY_API_KEY")
|
|
}
|
|
} else if cfg.Username == "" || cfg.Password == "" {
|
|
return nil, fmt.Errorf("missing required variables. Ensure the .env file contains EMBY_SERVER_URL and either EMBY_API_KEY+EMBY_USER_ID or EMBY_USERNAME+EMBY_PASSWORD")
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func defaultStr(s, d string) string {
|
|
if strings.TrimSpace(s) == "" {
|
|
return d
|
|
}
|
|
return s
|
|
}
|
|
|
|
func strToBool(s string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "true", "1", "yes", "y", "on":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseDotEnv reads KEY=VALUE lines, tolerating surrounding whitespace,
|
|
// single/double quotes, blank lines and '#' comments.
|
|
func parseDotEnv(f *os.File, out map[string]string) error {
|
|
sc := bufio.NewScanner(f)
|
|
sc.Buffer(make([]byte, 64*1024), 1024*1024)
|
|
for sc.Scan() {
|
|
line := strings.TrimSpace(sc.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
eq := strings.IndexByte(line, '=')
|
|
if eq < 0 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(line[:eq])
|
|
val := strings.TrimSpace(line[eq+1:])
|
|
if len(val) >= 2 {
|
|
if (val[0] == '"' && val[len(val)-1] == '"') || (val[0] == '\'' && val[len(val)-1] == '\'') {
|
|
val = val[1 : len(val)-1]
|
|
}
|
|
}
|
|
out[key] = val
|
|
}
|
|
return sc.Err()
|
|
}
|