Skip to main content
Glama
config.go12.2 kB
package config import ( "bytes" "context" "fmt" "os" "path/filepath" "sort" "strings" "github.com/BurntSushi/toml" "github.com/containers/kubernetes-mcp-server/pkg/api" "k8s.io/klog/v2" ) const ( DefaultDropInConfigDir = "conf.d" ) // StaticConfig is the configuration for the server. // It allows to configure server specific settings and tools to be enabled or disabled. type StaticConfig struct { DeniedResources []api.GroupVersionKind `toml:"denied_resources"` LogLevel int `toml:"log_level,omitzero"` Port string `toml:"port,omitempty"` SSEBaseURL string `toml:"sse_base_url,omitempty"` KubeConfig string `toml:"kubeconfig,omitempty"` ListOutput string `toml:"list_output,omitempty"` // When true, expose only tools annotated with readOnlyHint=true ReadOnly bool `toml:"read_only,omitempty"` // When true, disable tools annotated with destructiveHint=true DisableDestructive bool `toml:"disable_destructive,omitempty"` Toolsets []string `toml:"toolsets,omitempty"` EnabledTools []string `toml:"enabled_tools,omitempty"` DisabledTools []string `toml:"disabled_tools,omitempty"` // Authorization-related fields // RequireOAuth indicates whether the server requires OAuth for authentication. RequireOAuth bool `toml:"require_oauth,omitempty"` // OAuthAudience is the valid audience for the OAuth tokens, used for offline JWT claim validation. OAuthAudience string `toml:"oauth_audience,omitempty"` // ValidateToken indicates whether the server should validate the token against the Kubernetes API Server using TokenReview. ValidateToken bool `toml:"validate_token,omitempty"` // AuthorizationURL is the URL of the OIDC authorization server. // It is used for token validation and for STS token exchange. AuthorizationURL string `toml:"authorization_url,omitempty"` // DisableDynamicClientRegistration indicates whether dynamic client registration is disabled. // If true, the .well-known endpoints will not expose the registration endpoint. DisableDynamicClientRegistration bool `toml:"disable_dynamic_client_registration,omitempty"` // OAuthScopes are the supported **client** scopes requested during the **client/frontend** OAuth flow. OAuthScopes []string `toml:"oauth_scopes,omitempty"` // StsClientId is the OAuth client ID used for backend token exchange StsClientId string `toml:"sts_client_id,omitempty"` // StsClientSecret is the OAuth client secret used for backend token exchange StsClientSecret string `toml:"sts_client_secret,omitempty"` // StsAudience is the audience for the STS token exchange. StsAudience string `toml:"sts_audience,omitempty"` // StsScopes is the scopes for the STS token exchange. StsScopes []string `toml:"sts_scopes,omitempty"` CertificateAuthority string `toml:"certificate_authority,omitempty"` ServerURL string `toml:"server_url,omitempty"` // ClusterProviderStrategy is how the server finds clusters. // If set to "kubeconfig", the clusters will be loaded from those in the kubeconfig. // If set to "in-cluster", the server will use the in cluster config ClusterProviderStrategy string `toml:"cluster_provider_strategy,omitempty"` // ClusterProvider-specific configurations // This map holds raw TOML primitives that will be parsed by registered provider parsers ClusterProviderConfigs map[string]toml.Primitive `toml:"cluster_provider_configs,omitempty"` // Toolset-specific configurations // This map holds raw TOML primitives that will be parsed by registered toolset parsers ToolsetConfigs map[string]toml.Primitive `toml:"toolset_configs,omitempty"` // Prompt configuration // Raw TOML primitive for prompt definitions, parsed later // Note: Uses toml:"-" because Primitive can't be encoded, only decoded Prompts toml.Primitive `toml:"-"` promptsDefined bool // Internal: tracks if prompts were defined in config promptsMetadata toml.MetaData // Internal: metadata for prompts decoding // Server instructions to be provided by the MCP server to the MCP client // This can be used to provide specific instructions on how the client should use the server ServerInstructions string `toml:"server_instructions,omitempty"` // Internal: parsed provider configs (not exposed to TOML package) parsedClusterProviderConfigs map[string]api.ExtendedConfig // Internal: parsed toolset configs (not exposed to TOML package) parsedToolsetConfigs map[string]api.ExtendedConfig // Internal: the config.toml directory, to help resolve relative file paths configDirPath string } var _ api.BaseConfig = (*StaticConfig)(nil) type ReadConfigOpt func(cfg *StaticConfig) // WithDirPath returns a ReadConfigOpt that sets the config directory path. func WithDirPath(path string) ReadConfigOpt { return func(cfg *StaticConfig) { cfg.configDirPath = path } } // Read reads the toml file, applies drop-in configs from configDir (if provided), // and returns the StaticConfig with any opts applied. // Loading order: defaults → main config file → drop-in files (lexically sorted) func Read(configPath, dropInConfigDir string) (*StaticConfig, error) { var configFiles []string var configDir string // Main config file if configPath != "" { klog.V(2).Infof("Loading main config from: %s", configPath) configFiles = append(configFiles, configPath) // get and save the absolute dir path to the config file, so that other config parsers can use it absPath, err := filepath.Abs(configPath) if err != nil { return nil, fmt.Errorf("failed to resolve absolute path to config file: %w", err) } configDir = filepath.Dir(absPath) } // Drop-in config files if dropInConfigDir == "" { dropInConfigDir = DefaultDropInConfigDir } // Resolve drop-in config directory path (relative paths are resolved against config directory) if configDir != "" && !filepath.IsAbs(dropInConfigDir) { dropInConfigDir = filepath.Join(configDir, dropInConfigDir) } if configDir == "" { configDir = dropInConfigDir } dropInFiles, err := loadDropInConfigs(dropInConfigDir) if err != nil { return nil, fmt.Errorf("failed to load drop-in configs from %s: %w", dropInConfigDir, err) } if len(dropInFiles) == 0 { klog.V(2).Infof("No drop-in config files found in: %s", dropInConfigDir) } else { klog.V(2).Infof("Loading %d drop-in config file(s) from: %s", len(dropInFiles), dropInConfigDir) } configFiles = append(configFiles, dropInFiles...) // Read and merge all config files configData, err := readAndMergeFiles(configFiles) if err != nil { return nil, fmt.Errorf("failed to read and merge config files: %w", err) } return ReadToml(configData, WithDirPath(configDir)) } // loadDropInConfigs loads and merges config files from a drop-in directory. // Files are processed in lexical (alphabetical) order. // Only files with .toml extension are processed; dotfiles are ignored. func loadDropInConfigs(dropInConfigDir string) ([]string, error) { // Check if directory exists info, err := os.Stat(dropInConfigDir) if err != nil { if os.IsNotExist(err) { klog.V(2).Infof("Drop-in config directory does not exist, skipping: %s", dropInConfigDir) return nil, nil } return nil, fmt.Errorf("failed to stat drop-in directory: %w", err) } if !info.IsDir() { return nil, fmt.Errorf("drop-in config path is not a directory: %s", dropInConfigDir) } // Get all .toml files in the directory return getSortedConfigFiles(dropInConfigDir) } // getSortedConfigFiles returns a sorted list of .toml files in the specified directory. // Dotfiles (starting with '.') and non-.toml files are ignored. // Files are sorted lexically (alphabetically) by filename. func getSortedConfigFiles(dir string) ([]string, error) { entries, err := os.ReadDir(dir) if err != nil { return nil, fmt.Errorf("failed to read directory: %w", err) } var files []string for _, entry := range entries { // Skip directories if entry.IsDir() { continue } name := entry.Name() // Skip dotfiles if strings.HasPrefix(name, ".") { klog.V(4).Infof("Skipping dotfile: %s", name) continue } // Only process .toml files if !strings.HasSuffix(name, ".toml") { klog.V(4).Infof("Skipping non-.toml file: %s", name) continue } files = append(files, filepath.Join(dir, name)) } // Sort lexically sort.Strings(files) return files, nil } // readAndMergeFiles reads and merges multiple TOML config files into a single byte slice. // Files are merged in the order provided, with later files overriding earlier ones. func readAndMergeFiles(files []string) ([]byte, error) { rawConfig := map[string]interface{}{} // Merge each file in order using deep merge for _, file := range files { klog.V(3).Infof(" - Merging config: %s", filepath.Base(file)) configData, err := os.ReadFile(file) if err != nil { return nil, fmt.Errorf("failed to read config %s: %w", file, err) } dropInConfig := make(map[string]interface{}) if _, err = toml.NewDecoder(bytes.NewReader(configData)).Decode(&dropInConfig); err != nil { return nil, fmt.Errorf("failed to decode config %s: %w", file, err) } deepMerge(rawConfig, dropInConfig) } bufferedConfig := new(bytes.Buffer) if err := toml.NewEncoder(bufferedConfig).Encode(rawConfig); err != nil { return nil, fmt.Errorf("failed to encode merged config: %w", err) } return bufferedConfig.Bytes(), nil } // deepMerge recursively merges src into dst. // For nested maps, it merges recursively. For other types, src overwrites dst. func deepMerge(dst, src map[string]interface{}) { for key, srcVal := range src { if dstVal, exists := dst[key]; exists { // Both have this key - check if both are maps for recursive merge srcMap, srcIsMap := srcVal.(map[string]interface{}) dstMap, dstIsMap := dstVal.(map[string]interface{}) if srcIsMap && dstIsMap { deepMerge(dstMap, srcMap) continue } } // Either key doesn't exist in dst, or values aren't both maps - overwrite dst[key] = srcVal } } // ReadToml reads the toml data, loads and applies drop-in configs from configDir (if provided), // and returns the StaticConfig with any opts applied. // Loading order: defaults → main config file → drop-in files (lexically sorted) func ReadToml(configData []byte, opts ...ReadConfigOpt) (*StaticConfig, error) { config := Default() md, err := toml.NewDecoder(bytes.NewReader(configData)).Decode(config) if err != nil { return nil, err } for _, opt := range opts { opt(config) } ctx := withConfigDirPath(context.Background(), config.configDirPath) config.parsedClusterProviderConfigs, err = providerConfigRegistry.parse(ctx, md, config.ClusterProviderConfigs) if err != nil { return nil, err } config.parsedToolsetConfigs, err = toolsetConfigRegistry.parse(ctx, md, config.ToolsetConfigs) if err != nil { return nil, err } // Store prompts primitive if defined if md.IsDefined("prompts") { var temp struct { Prompts toml.Primitive `toml:"prompts"` } // Re-decode to get the primitive tempMd, _ := toml.NewDecoder(bytes.NewReader(configData)).Decode(&temp) config.Prompts = temp.Prompts config.promptsDefined = true config.promptsMetadata = tempMd } return config, nil } func (c *StaticConfig) GetClusterProviderStrategy() string { return c.ClusterProviderStrategy } func (c *StaticConfig) GetDeniedResources() []api.GroupVersionKind { return c.DeniedResources } func (c *StaticConfig) GetKubeConfigPath() string { return c.KubeConfig } func (c *StaticConfig) GetProviderConfig(strategy string) (api.ExtendedConfig, bool) { cfg, ok := c.parsedClusterProviderConfigs[strategy] return cfg, ok } func (c *StaticConfig) GetToolsetConfig(name string) (api.ExtendedConfig, bool) { cfg, ok := c.parsedToolsetConfigs[name] return cfg, ok } func (c *StaticConfig) IsRequireOAuth() bool { return c.RequireOAuth } // HasPrompts returns whether prompts were defined in the configuration func (c *StaticConfig) HasPrompts() bool { return c.promptsDefined } // GetPromptsMetadata returns the TOML metadata for prompts func (c *StaticConfig) GetPromptsMetadata() toml.MetaData { return c.promptsMetadata }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/containers/kubernetes-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server