| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- // Package config loads and validates the server configuration from the
- // environment.
- package config
- import (
- "errors"
- "fmt"
- "log/slog"
- "net"
- "net/url"
- "os"
- "strconv"
- "strings"
- "time"
- )
- // Supported MCP transports.
- const (
- TransportStdio = "stdio"
- TransportHTTP = "http"
- )
- // Defaults for optional settings.
- const (
- DefaultAddr = "127.0.0.1:8080"
- DefaultPath = "/mcp"
- DefaultTimeout = 30 * time.Second
- DefaultLogLevel = slog.LevelInfo
- )
- // Config holds the complete runtime configuration.
- type Config struct {
- // WikiJS connection.
- WikiJSURL string
- WikiJSAPIKey string
- // EnableEdit exposes the create_page and edit_page tools.
- EnableEdit bool
- // Timeout applies to a single WikiJS GraphQL request.
- Timeout time.Duration
- // Transport is either TransportStdio or TransportHTTP.
- Transport string
- // Addr is the listen address for the HTTP transport.
- Addr string
- // Path is the HTTP path that serves the MCP endpoint.
- Path string
- // Stateless disables MCP sessions on the HTTP transport.
- Stateless bool
- // AuthToken protects the HTTP transport with a bearer token. Empty means
- // no authentication.
- AuthToken string
- // LogLevel controls the stderr logger.
- LogLevel slog.Level
- }
- // Load reads the configuration from the environment and applies defaults.
- // Required values and transport settings are validated by [Config.Validate] so
- // that flags such as -version and -help work without credentials present.
- func Load() (Config, error) {
- cfg := Config{
- WikiJSURL: strings.TrimSpace(os.Getenv("WIKIJS_URL")),
- WikiJSAPIKey: strings.TrimSpace(os.Getenv("WIKIJS_API_KEY")),
- Transport: strings.ToLower(strings.TrimSpace(os.Getenv("MCP_TRANSPORT"))),
- Addr: strings.TrimSpace(os.Getenv("MCP_ADDR")),
- Path: strings.TrimSpace(os.Getenv("MCP_PATH")),
- AuthToken: strings.TrimSpace(os.Getenv("MCP_AUTH_TOKEN")),
- Timeout: DefaultTimeout,
- LogLevel: DefaultLogLevel,
- }
- if cfg.Transport == "" {
- cfg.Transport = TransportStdio
- }
- if cfg.Addr == "" {
- cfg.Addr = DefaultAddr
- }
- if cfg.Path == "" {
- cfg.Path = DefaultPath
- }
- var err error
- if cfg.EnableEdit, err = boolEnv("WIKIJS_ENABLE_EDIT", false); err != nil {
- return Config{}, err
- }
- if cfg.Stateless, err = boolEnv("MCP_STATELESS", false); err != nil {
- return Config{}, err
- }
- if raw := strings.TrimSpace(os.Getenv("WIKIJS_TIMEOUT")); raw != "" {
- cfg.Timeout, err = time.ParseDuration(raw)
- if err != nil {
- return Config{}, fmt.Errorf("WIKIJS_TIMEOUT: %w", err)
- }
- }
- if raw := strings.TrimSpace(os.Getenv("LOG_LEVEL")); raw != "" {
- if cfg.LogLevel, err = ParseLogLevel(raw); err != nil {
- return Config{}, err
- }
- }
- return cfg, nil
- }
- // Validate checks required values and the settings that can be overridden by
- // command line flags.
- func (c *Config) Validate() error {
- if c.WikiJSURL == "" {
- return errors.New("WIKIJS_URL is required")
- }
- if c.WikiJSAPIKey == "" {
- return errors.New("WIKIJS_API_KEY is required")
- }
- u, err := url.Parse(c.WikiJSURL)
- if err != nil {
- return fmt.Errorf("WIKIJS_URL: %w", err)
- }
- if (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
- return fmt.Errorf("WIKIJS_URL must be an absolute http(s) URL, got %q", c.WikiJSURL)
- }
- if u.RawQuery != "" || u.Fragment != "" {
- return fmt.Errorf("WIKIJS_URL must not contain a query or fragment, got %q", c.WikiJSURL)
- }
- switch c.Transport {
- case TransportStdio, TransportHTTP:
- default:
- return fmt.Errorf("MCP_TRANSPORT must be %q or %q, got %q", TransportStdio, TransportHTTP, c.Transport)
- }
- if c.Addr == "" {
- return errors.New("MCP_ADDR must not be empty")
- }
- if c.Transport == TransportHTTP {
- host, port, err := net.SplitHostPort(c.Addr)
- if err != nil || port == "" {
- return fmt.Errorf("MCP_ADDR must be a host:port address, got %q", c.Addr)
- }
- if c.AuthToken == "" && !isLoopbackHost(host) {
- return fmt.Errorf("MCP_ADDR %q is not loopback; set MCP_AUTH_TOKEN or bind to 127.0.0.1", c.Addr)
- }
- }
- if !strings.HasPrefix(c.Path, "/") {
- return fmt.Errorf("MCP_PATH must start with %q, got %q", "/", c.Path)
- }
- if c.Path == "/healthz" {
- return errors.New(`MCP_PATH must not be "/healthz"`)
- }
- if c.Timeout <= 0 {
- return errors.New("WIKIJS_TIMEOUT must be positive")
- }
- return nil
- }
- func isLoopbackHost(host string) bool {
- if strings.EqualFold(host, "localhost") {
- return true
- }
- ip := net.ParseIP(host)
- return ip != nil && ip.IsLoopback()
- }
- func boolEnv(name string, fallback bool) (bool, error) {
- raw := strings.TrimSpace(os.Getenv(name))
- if raw == "" {
- return fallback, nil
- }
- v, err := strconv.ParseBool(raw)
- if err != nil {
- return false, fmt.Errorf("%s must be a boolean: %w", name, err)
- }
- return v, nil
- }
- // ParseLogLevel turns a human-readable level name into a [slog.Level].
- func ParseLogLevel(raw string) (slog.Level, error) {
- switch strings.ToLower(raw) {
- case "debug":
- return slog.LevelDebug, nil
- case "info":
- return slog.LevelInfo, nil
- case "warn", "warning":
- return slog.LevelWarn, nil
- case "error":
- return slog.LevelError, nil
- default:
- return 0, fmt.Errorf("LOG_LEVEL must be one of debug, info, warn, error; got %q", raw)
- }
- }
|