Files
mcp/internal/config/agent.go
Kyle Isom 84c487e7f8 Phase B: Agent registers routes with mc-proxy on deploy
The agent connects to mc-proxy via Unix socket and automatically
registers/removes routes during deploy and stop. This eliminates
manual mcproxyctl usage or TOML editing.

- New ProxyRouter abstraction wraps mc-proxy client library
- Deploy: after container starts, registers routes with mc-proxy
  using host ports from the registry
- Stop: removes routes from mc-proxy before stopping container
- Config: [mcproxy] section with socket path and cert_dir
- Nil-safe: if mc-proxy socket not configured, route registration
  is silently skipped (backward compatible)
- L7 routes use certs from convention path (<cert_dir>/<service>.pem)
- L4 routes use TLS passthrough (backend_tls=true)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:35:06 -07:00

209 lines
5.6 KiB
Go

package config
import (
"fmt"
"os"
"time"
toml "github.com/pelletier/go-toml/v2"
)
// AgentConfig is the configuration for the mcp-agent daemon.
type AgentConfig struct {
Server ServerConfig `toml:"server"`
Database DatabaseConfig `toml:"database"`
MCIAS MCIASConfig `toml:"mcias"`
Agent AgentSettings `toml:"agent"`
MCProxy MCProxyConfig `toml:"mcproxy"`
Monitor MonitorConfig `toml:"monitor"`
Log LogConfig `toml:"log"`
}
// MCProxyConfig holds the mc-proxy connection settings.
type MCProxyConfig struct {
// Socket is the path to the mc-proxy gRPC admin API Unix socket.
// If empty, route registration is disabled.
Socket string `toml:"socket"`
// CertDir is the directory containing TLS certificates for routes.
// Convention: <service>.pem and <service>.key per service.
// Defaults to /srv/mc-proxy/certs.
CertDir string `toml:"cert_dir"`
}
// ServerConfig holds gRPC server listen address and TLS paths.
type ServerConfig struct {
GRPCAddr string `toml:"grpc_addr"`
TLSCert string `toml:"tls_cert"`
TLSKey string `toml:"tls_key"`
}
// DatabaseConfig holds the SQLite database path.
type DatabaseConfig struct {
Path string `toml:"path"`
}
// AgentSettings holds agent-specific settings.
type AgentSettings struct {
NodeName string `toml:"node_name"`
ContainerRuntime string `toml:"container_runtime"`
}
// MonitorConfig holds monitoring loop parameters.
type MonitorConfig struct {
Interval Duration `toml:"interval"`
AlertCommand []string `toml:"alert_command"`
Cooldown Duration `toml:"cooldown"`
FlapThreshold int `toml:"flap_threshold"`
FlapWindow Duration `toml:"flap_window"`
Retention Duration `toml:"retention"`
}
// LogConfig holds logging settings.
type LogConfig struct {
Level string `toml:"level"`
}
// Duration wraps time.Duration to support TOML string unmarshaling.
// It accepts Go duration strings (e.g. "60s", "15m", "24h") plus a
// "d" suffix for days (e.g. "30d" becomes 30*24h).
type Duration struct {
time.Duration
}
// UnmarshalText implements encoding.TextUnmarshaler for TOML parsing.
func (d *Duration) UnmarshalText(text []byte) error {
s := string(text)
if s == "" {
d.Duration = 0
return nil
}
// Handle "d" suffix for days.
if len(s) > 1 && s[len(s)-1] == 'd' {
var days float64
if _, err := fmt.Sscanf(s[:len(s)-1], "%f", &days); err != nil {
return fmt.Errorf("parse duration %q: %w", s, err)
}
d.Duration = time.Duration(days * float64(24*time.Hour))
return nil
}
dur, err := time.ParseDuration(s)
if err != nil {
return fmt.Errorf("parse duration %q: %w", s, err)
}
d.Duration = dur
return nil
}
// MarshalText implements encoding.TextMarshaler for TOML serialization.
func (d Duration) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
// LoadAgentConfig reads and validates an agent configuration file.
// Environment variables override file values for select fields.
func LoadAgentConfig(path string) (*AgentConfig, error) {
data, err := os.ReadFile(path) //nolint:gosec // config path from trusted CLI flag
if err != nil {
return nil, fmt.Errorf("read config %q: %w", path, err)
}
var cfg AgentConfig
if err := toml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %q: %w", path, err)
}
applyAgentDefaults(&cfg)
applyAgentEnvOverrides(&cfg)
if err := validateAgentConfig(&cfg); err != nil {
return nil, fmt.Errorf("validate config: %w", err)
}
return &cfg, nil
}
func applyAgentDefaults(cfg *AgentConfig) {
if cfg.Monitor.Interval.Duration == 0 {
cfg.Monitor.Interval.Duration = 60 * time.Second
}
if cfg.Monitor.Cooldown.Duration == 0 {
cfg.Monitor.Cooldown.Duration = 15 * time.Minute
}
if cfg.Monitor.FlapThreshold == 0 {
cfg.Monitor.FlapThreshold = 3
}
if cfg.Monitor.FlapWindow.Duration == 0 {
cfg.Monitor.FlapWindow.Duration = 10 * time.Minute
}
if cfg.Monitor.Retention.Duration == 0 {
cfg.Monitor.Retention.Duration = 30 * 24 * time.Hour // 30 days
}
if cfg.Log.Level == "" {
cfg.Log.Level = "info"
}
if cfg.Agent.ContainerRuntime == "" {
cfg.Agent.ContainerRuntime = "podman"
}
if cfg.MCProxy.CertDir == "" {
cfg.MCProxy.CertDir = "/srv/mc-proxy/certs"
}
}
func applyAgentEnvOverrides(cfg *AgentConfig) {
if v := os.Getenv("MCP_AGENT_SERVER_GRPC_ADDR"); v != "" {
cfg.Server.GRPCAddr = v
}
if v := os.Getenv("MCP_AGENT_SERVER_TLS_CERT"); v != "" {
cfg.Server.TLSCert = v
}
if v := os.Getenv("MCP_AGENT_SERVER_TLS_KEY"); v != "" {
cfg.Server.TLSKey = v
}
if v := os.Getenv("MCP_AGENT_DATABASE_PATH"); v != "" {
cfg.Database.Path = v
}
if v := os.Getenv("MCP_AGENT_NODE_NAME"); v != "" {
cfg.Agent.NodeName = v
}
if v := os.Getenv("MCP_AGENT_CONTAINER_RUNTIME"); v != "" {
cfg.Agent.ContainerRuntime = v
}
if v := os.Getenv("MCP_AGENT_LOG_LEVEL"); v != "" {
cfg.Log.Level = v
}
if v := os.Getenv("MCP_AGENT_MCPROXY_SOCKET"); v != "" {
cfg.MCProxy.Socket = v
}
if v := os.Getenv("MCP_AGENT_MCPROXY_CERT_DIR"); v != "" {
cfg.MCProxy.CertDir = v
}
}
func validateAgentConfig(cfg *AgentConfig) error {
if cfg.Server.GRPCAddr == "" {
return fmt.Errorf("server.grpc_addr is required")
}
if cfg.Server.TLSCert == "" {
return fmt.Errorf("server.tls_cert is required")
}
if cfg.Server.TLSKey == "" {
return fmt.Errorf("server.tls_key is required")
}
if cfg.Database.Path == "" {
return fmt.Errorf("database.path is required")
}
if cfg.MCIAS.ServerURL == "" {
return fmt.Errorf("mcias.server_url is required")
}
if cfg.MCIAS.ServiceName == "" {
return fmt.Errorf("mcias.service_name is required")
}
if cfg.Agent.NodeName == "" {
return fmt.Errorf("agent.node_name is required")
}
return nil
}