Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,97 @@ uvx kubernetes-mcp-server@latest --help
|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--port` | Starts the MCP server in Streamable HTTP mode (path /mcp) and Server-Sent Event (SSE) (path /sse) mode and listens on the specified port . |
| `--log-level` | Sets the logging level (values [from 0-9](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md)). Similar to [kubectl logging levels](https://kubernetes.io/docs/reference/kubectl/quick-reference/#kubectl-output-verbosity-and-debugging). |
| `--config` | (Optional) Path to the main TOML configuration file. See [Configuration Files](#configuration-files) section below for details. |
| `--config-dir` | (Optional) Path to drop-in configuration directory. Files are loaded in lexical (alphabetical) order. See [Drop-in Configuration](#drop-in-configuration) section below for details. |
| `--kubeconfig` | Path to the Kubernetes configuration file. If not provided, it will try to resolve the configuration (in-cluster, default location, etc.). |
| `--list-output` | Output format for resource list operations (one of: yaml, table) (default "table") |
| `--read-only` | If set, the MCP server will run in read-only mode, meaning it will not allow any write operations (create, update, delete) on the Kubernetes cluster. This is useful for debugging or inspecting the cluster without making changes. |
| `--disable-destructive` | If set, the MCP server will disable all destructive operations (delete, update, etc.) on the Kubernetes cluster. This is useful for debugging or inspecting the cluster without accidentally making changes. This option has no effect when `--read-only` is used. |
| `--toolsets` | Comma-separated list of toolsets to enable. Check the [🛠️ Tools and Functionalities](#tools-and-functionalities) section for more information. |
| `--disable-multi-cluster` | If set, the MCP server will disable multi-cluster support and will only use the current context from the kubeconfig file. This is useful if you want to restrict the MCP server to a single cluster. |

### Drop-in Configuration <a id="drop-in-configuration"></a>

The Kubernetes MCP server supports flexible configuration through both a main config file and drop-in files. **Both are optional** - you can use either, both, or neither (server will use built-in defaults).

#### Configuration Loading Order

Configuration values are loaded and merged in the following order (later sources override earlier ones):

1. **Internal Defaults** - Always loaded (hardcoded default values)
2. **Main Configuration File** - Optional, loaded via `--config` flag
3. **Drop-in Files** - Optional, loaded from `--config-dir` in **lexical (alphabetical) order**

#### How Drop-in Files Work

- **File Naming**: Use numeric prefixes to control loading order (e.g., `00-base.toml`, `10-cluster.toml`, `99-override.toml`)
- **File Extension**: Only `.toml` files are processed; dotfiles (starting with `.`) are ignored
- **Partial Configuration**: Drop-in files can contain only a subset of configuration options
- **Merge Behavior**: Values present in a drop-in file override previous values; missing values are preserved

#### Dynamic Configuration Reload

To reload configuration after modifying config files, send a `SIGHUP` signal to the running server process:

```shell
# Find the process ID
ps aux | grep kubernetes-mcp-server

# Send SIGHUP to reload configuration
kill -HUP <pid>

# Or use pkill
pkill -HUP kubernetes-mcp-server
```

The server will:
- Reload the main config file and all drop-in files
- Update toolsets and enabled tools
- Reconnect to clusters if needed
- Log the reload status

**Note**: SIGHUP reload is not available on Windows. On Windows, restart the server to reload configuration.

#### Example: Using Both Config Methods

**Command:**
```shell
kubernetes-mcp-server --config /etc/kubernetes-mcp-server/config.toml \
--config-dir /etc/kubernetes-mcp-server/config.d/
```

**Directory structure:**
```
/etc/kubernetes-mcp-server/
├── config.toml # Main configuration
└── config.d/
├── 00-base.toml # Base overrides
├── 10-toolsets.toml # Toolset-specific config
└── 99-local.toml # Local overrides
```

**Example drop-in file** (`10-toolsets.toml`):
```toml
# Override only the toolsets - all other config preserved
toolsets = ["core", "config", "helm", "logs"]
```

**Example drop-in file** (`99-local.toml`):
```toml
# Local development overrides
log_level = 9
read_only = true
```

**To apply changes:**
```shell
# Edit config files
vim /etc/kubernetes-mcp-server/config.d/99-local.toml

# Reload without restarting
pkill -HUP kubernetes-mcp-server
```

## 🛠️ Tools and Functionalities <a id="tools-and-functionalities"></a>

The Kubernetes MCP server supports enabling or disabling specific groups of tools and functionalities (tools, resources, prompts, and so on) via the `--toolsets` command-line flag or `toolsets` configuration option.
Expand Down
146 changes: 135 additions & 11 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/BurntSushi/toml"
"k8s.io/klog/v2"
)

const (
Expand Down Expand Up @@ -95,26 +98,147 @@
}
}

// Read reads the toml file and returns the StaticConfig, with any opts applied.
func Read(configPath string, opts ...ReadConfigOpt) (*StaticConfig, error) {
configData, err := os.ReadFile(configPath)
// 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 string, configDir string, opts ...ReadConfigOpt) (*StaticConfig, error) {
// Start with defaults
cfg := Default()

// Get the absolute dir path for the main config file
var dirPath string
if configPath != "" {
absPath, err := filepath.Abs(configPath)
if err != nil {
return nil, fmt.Errorf("failed to resolve absolute path to config file: %w", err)
}
dirPath = filepath.Dir(absPath)

// Load main config file
klog.V(2).Infof("Loading main config from: %s", configPath)
if err := mergeConfigFile(cfg, configPath, append(opts, withDirPath(dirPath))...); err != nil {
return nil, fmt.Errorf("failed to load main config file %s: %w", configPath, err)
}
}

// Load drop-in config files if directory is specified
if configDir != "" {
if err := loadDropInConfigs(cfg, configDir, append(opts, withDirPath(dirPath))...); err != nil {
return nil, fmt.Errorf("failed to load drop-in configs from %s: %w", configDir, err)
}
}

return cfg, nil
}

// mergeConfigFile reads a config file and merges its values into the target config.
// Values present in the file will overwrite existing values in cfg.
// Values not present in the file will remain unchanged in cfg.
func mergeConfigFile(cfg *StaticConfig, filePath string, opts ...ReadConfigOpt) error {
configData, err := os.ReadFile(filePath)
if err != nil {
return nil, err
return err
}

// get and save the absolute dir path to the config file, so that other config parsers can use it
absPath, err := filepath.Abs(configPath)
md, err := toml.NewDecoder(bytes.NewReader(configData)).Decode(cfg)
if err != nil {
return nil, fmt.Errorf("failed to resolve absolute path to config file: %w", err)
return fmt.Errorf("failed to decode TOML: %w", err)
}

for _, opt := range opts {
opt(cfg)
}

if err := cfg.parseClusterProviderConfigs(md); err != nil {

Check failure on line 152 in pkg/config/config.go

View workflow job for this annotation

GitHub Actions / Build on macos-latest

cfg.parseClusterProviderConfigs undefined (type *StaticConfig has no field or method parseClusterProviderConfigs)

Check failure on line 152 in pkg/config/config.go

View workflow job for this annotation

GitHub Actions / Build on macos-15-intel

cfg.parseClusterProviderConfigs undefined (type *StaticConfig has no field or method parseClusterProviderConfigs)

Check failure on line 152 in pkg/config/config.go

View workflow job for this annotation

GitHub Actions / Build on ubuntu-latest

cfg.parseClusterProviderConfigs undefined (type *StaticConfig has no field or method parseClusterProviderConfigs)

Check failure on line 152 in pkg/config/config.go

View workflow job for this annotation

GitHub Actions / Build on ubuntu-24.04-arm

cfg.parseClusterProviderConfigs undefined (type *StaticConfig has no field or method parseClusterProviderConfigs)
return err
}

if err := cfg.parseToolsetConfigs(md); err != nil {

Check failure on line 156 in pkg/config/config.go

View workflow job for this annotation

GitHub Actions / Build on macos-latest

cfg.parseToolsetConfigs undefined (type *StaticConfig has no field or method parseToolsetConfigs)

Check failure on line 156 in pkg/config/config.go

View workflow job for this annotation

GitHub Actions / Build on macos-15-intel

cfg.parseToolsetConfigs undefined (type *StaticConfig has no field or method parseToolsetConfigs)

Check failure on line 156 in pkg/config/config.go

View workflow job for this annotation

GitHub Actions / Build on ubuntu-latest

cfg.parseToolsetConfigs undefined (type *StaticConfig has no field or method parseToolsetConfigs)

Check failure on line 156 in pkg/config/config.go

View workflow job for this annotation

GitHub Actions / Build on ubuntu-24.04-arm

cfg.parseToolsetConfigs undefined (type *StaticConfig has no field or method parseToolsetConfigs)
return err
}
dirPath := filepath.Dir(absPath)

cfg, err := ReadToml(configData, append(opts, withDirPath(dirPath))...)
return nil
}

// 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(cfg *StaticConfig, dropInDir string, opts ...ReadConfigOpt) error {
// Check if directory exists
info, err := os.Stat(dropInDir)
if err != nil {
return nil, err
if os.IsNotExist(err) {
klog.V(2).Infof("Drop-in config directory does not exist, skipping: %s", dropInDir)
return nil
}
return fmt.Errorf("failed to stat drop-in directory: %w", err)
}

return cfg, nil
if !info.IsDir() {
return fmt.Errorf("drop-in config path is not a directory: %s", dropInDir)
}

// Get all .toml files in the directory
files, err := getSortedConfigFiles(dropInDir)
if err != nil {
return err
}

if len(files) == 0 {
klog.V(2).Infof("No drop-in config files found in: %s", dropInDir)
return nil
}

klog.V(2).Infof("Loading %d drop-in config file(s) from: %s", len(files), dropInDir)

// Merge each file in order
for _, file := range files {
klog.V(3).Infof(" - Merging drop-in config: %s", filepath.Base(file))
if err := mergeConfigFile(cfg, file, opts...); err != nil {
return fmt.Errorf("failed to merge drop-in config %s: %w", file, err)
}
}

return nil
}

// 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
}

// ReadToml reads the toml data and returns the StaticConfig, with any opts applied
Expand Down
Loading
Loading