diff --git a/cmd/thv/app/config.go b/cmd/thv/app/config.go index 62ee55f5..ea13070a 100644 --- a/cmd/thv/app/config.go +++ b/cmd/thv/app/config.go @@ -17,6 +17,7 @@ import ( "github.com/stacklok/toolhive/pkg/labels" "github.com/stacklok/toolhive/pkg/logger" "github.com/stacklok/toolhive/pkg/networking" + "github.com/stacklok/toolhive/pkg/registry" "github.com/stacklok/toolhive/pkg/transport" ) @@ -115,10 +116,86 @@ var unsetRegistryURLCmd = &cobra.Command{ RunE: unsetRegistryURLCmdFunc, } +var addDefaultServerCmd = &cobra.Command{ + Use: "add-default-server [server]", + Short: "Add a server to the list of default servers", + Long: `Add a server to the list of default servers that will be started when running 'thv run' without arguments. +The server must exist in the registry. + +Example: + thv config add-default-server my-server`, + Args: cobra.ExactArgs(1), + RunE: addDefaultServerCmdFunc, +} + +var removeDefaultServerCmd = &cobra.Command{ + Use: "remove-default-server [server]", + Short: "Remove a server from the list of default servers", + Long: `Remove a server from the list of default servers. +This only removes the server from the default list, it does not delete or stop the server. + +Example: + thv config remove-default-server my-server`, + Args: cobra.ExactArgs(1), + RunE: removeDefaultServerCmdFunc, +} + var ( allowPrivateRegistryIp bool ) +var ( + resetServerArgsAll bool +) + +var resetServerArgsCmd = &cobra.Command{ + Use: "reset-server-args [SERVER_NAME]", + Short: "Reset/delete arguments for MCP servers from the config", + Long: `Reset/delete configured arguments for MCP servers from the config. +This command removes saved arguments from the config file. +Future runs of the affected servers will not use any pre-configured arguments unless explicitly provided. + +Examples: + # Reset arguments for a specific server + thv config reset-server-args my-server + + # Reset arguments for all servers + thv config reset-server-args --all`, + Args: cobra.MaximumNArgs(1), + RunE: resetServerArgsCmdFunc, + PreRunE: func(_ *cobra.Command, args []string) error { + if !resetServerArgsAll && len(args) == 0 { + return fmt.Errorf("server name is required when --all flag is not set") + } + if resetServerArgsAll && len(args) > 0 { + return fmt.Errorf("server name cannot be specified when using --all flag") + } + return nil + }, +} + +var setGlobalServerArgsCmd = &cobra.Command{ + Use: "set-global-server-args KEY=VALUE [KEY=VALUE...]", + Short: "Set global arguments for all MCP servers", + Long: `Set global arguments that will be applied to all MCP servers. +These arguments will be used as defaults for all servers unless overridden by server-specific arguments. + +Example: + thv config set-global-server-args debug=true log-level=info`, + Args: cobra.MinimumNArgs(1), + RunE: setGlobalServerArgsCmdFunc, +} + +var resetGlobalServerArgsCmd = &cobra.Command{ + Use: "reset-global-server-args", + Short: "Reset/delete global arguments for all MCP servers", + Long: `Reset/delete global arguments from the config. +This command removes all saved global arguments from the config file. +Future runs of servers will not use any pre-configured global arguments.`, + Args: cobra.NoArgs, + RunE: resetGlobalServerArgsCmdFunc, +} + func init() { // Add config command to root command rootCmd.AddCommand(configCmd) @@ -131,15 +208,16 @@ func init() { configCmd.AddCommand(getCACertCmd) configCmd.AddCommand(unsetCACertCmd) configCmd.AddCommand(setRegistryURLCmd) - setRegistryURLCmd.Flags().BoolVarP( - &allowPrivateRegistryIp, - "allow-private-ip", - "p", - false, - "Allow setting the registry URL, even if it references a private IP address", - ) configCmd.AddCommand(getRegistryURLCmd) configCmd.AddCommand(unsetRegistryURLCmd) + configCmd.AddCommand(resetServerArgsCmd) + configCmd.AddCommand(setGlobalServerArgsCmd) + configCmd.AddCommand(resetGlobalServerArgsCmd) + configCmd.AddCommand(addDefaultServerCmd) + configCmd.AddCommand(removeDefaultServerCmd) + + resetServerArgsCmd.Flags().BoolVarP(&resetServerArgsAll, "all", "a", false, + "Reset arguments for all MCP servers") // Add OTEL parent command to config configCmd.AddCommand(OtelCmd) @@ -480,3 +558,133 @@ func listRegisteredClientsCmdFunc(_ *cobra.Command, _ []string) error { return nil } + +func resetServerArgsCmdFunc(_ *cobra.Command, args []string) error { + // Load the config + cfg, err := config.LoadOrCreateConfig() + if err != nil { + return fmt.Errorf("failed to load config: %v", err) + } + + if resetServerArgsAll { + // Delete all server arguments + if err := cfg.DeleteAllServerArgs(); err != nil { + return fmt.Errorf("failed to reset all server arguments: %v", err) + } + logger.Info("Successfully reset arguments for all servers") + } else { + // Delete arguments for specific server + serverName := args[0] + if err := cfg.DeleteServerArgs(serverName); err != nil { + return fmt.Errorf("failed to reset server arguments: %v", err) + } + logger.Infof("Successfully reset arguments for server %s", serverName) + } + + return nil +} + +func setGlobalServerArgsCmdFunc(_ *cobra.Command, args []string) error { + // Parse the arguments into a map + argsMap := make(map[string]string) + for _, arg := range args { + parts := strings.SplitN(arg, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid argument format: %s (expected KEY=VALUE)", arg) + } + argsMap[parts[0]] = parts[1] + } + + // Load the config + cfg, err := config.LoadOrCreateConfig() + if err != nil { + return fmt.Errorf("failed to load config: %v", err) + } + + // Set the global arguments + if err := cfg.SetGlobalServerArgs(argsMap); err != nil { + return fmt.Errorf("failed to set global server arguments: %v", err) + } + + logger.Info("Successfully set global server arguments") + return nil +} + +func resetGlobalServerArgsCmdFunc(_ *cobra.Command, _ []string) error { + // Load the config + cfg, err := config.LoadOrCreateConfig() + if err != nil { + return fmt.Errorf("failed to load config: %v", err) + } + + // Reset the global arguments + if err := cfg.DeleteGlobalServerArgs(); err != nil { + return fmt.Errorf("failed to reset global server arguments: %v", err) + } + + logger.Info("Successfully reset global server arguments") + return nil +} + +func addDefaultServerCmdFunc(_ *cobra.Command, args []string) error { + serverName := args[0] + + // Load the registry to validate the server exists + provider, err := registry.GetDefaultProvider() + if err != nil { + return fmt.Errorf("failed to get registry provider: %v", err) + } + + // Check if the server exists in the registry + if _, err := provider.GetServer(serverName); err != nil { + return fmt.Errorf("server %q not found in registry: %w", serverName, err) + } + + // Update the configuration + err = config.UpdateConfig(func(c *config.Config) { + // Check if server is already in default servers + for _, s := range c.DefaultServers { + if s == serverName { + fmt.Printf("Server %q is already in default servers list\n", serverName) + return + } + } + + // Add the server to default servers + c.DefaultServers = append(c.DefaultServers, serverName) + }) + if err != nil { + return fmt.Errorf("failed to update configuration: %w", err) + } + + fmt.Printf("Successfully added %q to default servers\n", serverName) + return nil +} + +func removeDefaultServerCmdFunc(_ *cobra.Command, args []string) error { + serverName := args[0] + + // Update the configuration + err := config.UpdateConfig(func(c *config.Config) { + // Find and remove the server from the default servers list + found := false + for i, s := range c.DefaultServers { + if s == serverName { + // Remove the server by appending the slice before and after the index + c.DefaultServers = append(c.DefaultServers[:i], c.DefaultServers[i+1:]...) + found = true + break + } + } + if !found { + fmt.Printf("Server %q not found in default servers list\n", serverName) + return + } + }) + if err != nil { + return fmt.Errorf("failed to update configuration: %w", err) + } + + fmt.Printf("Successfully removed %q from default servers\n", serverName) + return nil +} diff --git a/cmd/thv/app/run.go b/cmd/thv/app/run.go index 470b9f5a..0f7294df 100644 --- a/cmd/thv/app/run.go +++ b/cmd/thv/app/run.go @@ -1,9 +1,11 @@ package app import ( + "context" "fmt" "net" "os" + "strings" "github.com/spf13/cobra" @@ -20,7 +22,7 @@ import ( ) var runCmd = &cobra.Command{ - Use: "run [flags] SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...]", + Use: "run [flags] [SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...]]", Short: "Run an MCP server", Long: `Run an MCP server with the specified name, image, or protocol scheme. @@ -45,9 +47,12 @@ ToolHive supports three ways to run an MCP server: or go (Golang). For Go, you can also specify local paths starting with './' or '../' to build and run local Go projects. +If no server is specified, ToolHive will run all servers configured in the +default_servers list in the config file. + The container will be started with the specified transport mode and permission profile. Additional configuration can be provided via flags.`, - Args: cobra.MinimumNArgs(1), + Args: cobra.ArbitraryArgs, RunE: runCmdFunc, // Ignore unknown flags to allow passing flags to the MCP server FParseErrWhitelist: cobra.FParseErrWhitelist{ @@ -73,6 +78,7 @@ var ( runK8sPodPatch string runCACertPath string runVerifyImage string + runSaveConfig bool // OpenTelemetry flags runOtelEndpoint string @@ -167,6 +173,8 @@ func init() { retriever.VerifyImageDisabled, ), ) + runCmd.Flags().BoolVarP(&runSaveConfig, "save-config", "s", false, + "Save the provided command line arguments to the config file for this server") // This is used for the K8s operator which wraps the run command, but shouldn't be visible to users. if err := runCmd.Flags().MarkHidden("k8s-pod-patch"); err != nil { @@ -207,12 +215,39 @@ func runCmdFunc(cmd *cobra.Command, args []string) error { } runHost = validatedHost + // If no server is specified, run all default servers + if len(args) == 0 { + return runDefaultServers(ctx, cmd) + } + + return runServer(ctx, cmd, args) +} + +// runDefaultServers runs all default servers +func runDefaultServers(ctx context.Context, cmd *cobra.Command) error { + cfg := config.GetConfig() + if len(cfg.DefaultServers) == 0 { + return fmt.Errorf("no default servers configured") + } + + // Run each default server + for _, server := range cfg.DefaultServers { + if err := runServer(ctx, cmd, []string{server}); err != nil { + logger.Warnf("Failed to run default server %s: %v", server, err) + } + } + return nil +} + +// runServer runs a single MCP server with the given arguments +func runServer(ctx context.Context, cmd *cobra.Command, args []string) error { // Get the name of the MCP server to run. // This may be a server name from the registry, a container image, or a protocol scheme. serverOrImage := args[0] // Process command arguments using os.Args to find everything after -- cmdArgs := parseCommandArguments(os.Args) + mergedArgs := resolveCommandAndConfigArgs(cmdArgs, serverOrImage) // Print the processed command arguments for debugging logger.Debugf("Processed cmdArgs: %v", cmdArgs) @@ -284,7 +319,7 @@ func runCmdFunc(cmd *cobra.Command, args []string) error { runConfig, err := runner.NewRunConfigFromFlags( ctx, rt, - cmdArgs, + argsMapToSlice(mergedArgs), runName, imageURL, imageMetadata, @@ -342,6 +377,154 @@ func parseCommandArguments(args []string) []string { return cmdArgs } +// parseArgsToMap converts a slice of command line arguments to a map[string]string +// It handles both short flags (-f) and long flags (--flag) with their values +func parseArgsToMap(args []string) map[string]string { + result := make(map[string]string) + + for i := 0; i < len(args); i++ { + arg := args[i] + + // Skip empty arguments + if arg == "" { + continue + } + + // Handle long flags (--flag=value or --flag value) + if strings.HasPrefix(arg, "--") { + // Check if it's --flag=value format + if strings.Contains(arg, "=") { + parts := strings.SplitN(arg, "=", 2) + flag := strings.TrimPrefix(parts[0], "--") + result[flag] = parts[1] + } else { + // Check if next argument is a value (not a flag) + flag := strings.TrimPrefix(arg, "--") + if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { + result[flag] = args[i+1] + i++ // Skip the value in next iteration + } else { + // Flag without value, set to empty string + result[flag] = "" + } + } + continue + } + + // Handle short flags (-f=value, -f value, or -f) + if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") { + // Check if it's -f=value format + if strings.Contains(arg, "=") { + parts := strings.SplitN(arg, "=", 2) + flag := strings.TrimPrefix(parts[0], "-") + result[flag] = parts[1] + } else { + // Check if next argument is a value (not a flag) + flag := strings.TrimPrefix(arg, "-") + if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { + result[flag] = args[i+1] + i++ // Skip the value in next iteration + } else { + // Flag without value, set to empty string + result[flag] = "" + } + } + continue + } + + // If it's not a flag, treat as positional argument + // You might want to handle this differently based on your needs + result[fmt.Sprintf("arg_%d", len(result))] = arg + } + + return result +} + +func resolveCommandAndConfigArgs(cmdArgs []string, serverOrImage string) map[string]string { + cmdArgsMap := parseArgsToMap(cmdArgs) + + var mergedArgs map[string]string + + // Get any configured arguments for this server + cfg, err := config.LoadOrCreateConfig() + if err != nil { + logger.Warnf("Failed to load config: %v", err) + mergedArgs = cmdArgsMap + } else if cfg != nil { + // Get global arguments first + globalArgs := cfg.GetGlobalServerArgs() + logger.Debugf("Found global arguments: %v", globalArgs) + + // Get server-specific arguments + serverArgs := cfg.GetServerArgs(serverOrImage) + logger.Debugf("Found server-specific arguments: %v", serverArgs) + + // If save-config flag is set, save the command line arguments to config + // Do this before merging to only save explicitly provided arguments + if runSaveConfig && len(cmdArgsMap) > 0 { + if err := cfg.SetServerArgs(serverOrImage, cmdArgsMap); err != nil { + logger.Warnf("Failed to save arguments to config: %v", err) + } else { + logger.Infof("Saved arguments to config for server %s: %v", serverOrImage, cmdArgsMap) + } + } + + // Merge config args with command line args for this run + if len(serverArgs) > 0 || len(globalArgs) > 0 { + mergedArgs = mergeArguments(globalArgs, serverArgs, cmdArgsMap) + logger.Debugf("Merged arguments: %v", mergedArgs) + } else { + logger.Warnf("No configured arguments found for server %s", serverOrImage) + mergedArgs = cmdArgsMap + } + } else { + logger.Debugf("No config found for server %s", serverOrImage) + mergedArgs = cmdArgsMap + } + + return mergedArgs +} + +// mergeArguments merges arguments from config and command line +// In increasing order of precedence: +// 1. Global arguments +// 2. Server-specific arguments +// 3. Command line arguments +func mergeArguments(globalArgs map[string]string, serverArgs map[string]string, cmdArgs map[string]string) map[string]string { + // Start with global args + merged := make(map[string]string) + for k, v := range globalArgs { + merged[k] = v + } + + // Override with server-specific arguments + for k, v := range serverArgs { + merged[k] = v + } + + // Override with command line args + for k, v := range cmdArgs { + merged[k] = v + } + + return merged +} + +// argsMapToSlice converts a map of arguments to a slice of strings +func argsMapToSlice(args map[string]string) []string { + // Convert back to slice of strings + var result []string + for k, v := range args { + if v == "" { + result = append(result, fmt.Sprintf("--%s", k)) + } else { + result = append(result, fmt.Sprintf("--%s=%s", k, v)) + } + } + + return result +} + // ValidateAndNormaliseHostFlag validates and normalizes the host flag resolving it to an IP address if hostname is provided func ValidateAndNormaliseHostFlag(host string) (string, error) { // Check if the host is a valid IP address diff --git a/docs/cli/thv_config.md b/docs/cli/thv_config.md index 7aac0c2b..0e2995af 100644 --- a/docs/cli/thv_config.md +++ b/docs/cli/thv_config.md @@ -21,13 +21,18 @@ The config command provides subcommands to manage application configuration sett ### SEE ALSO * [thv](thv.md) - ToolHive (thv) is a lightweight, secure, and fast manager for MCP servers +* [thv config add-default-server](thv_config_add-default-server.md) - Add a server to the list of default servers * [thv config get-ca-cert](thv_config_get-ca-cert.md) - Get the currently configured CA certificate path * [thv config get-registry-url](thv_config_get-registry-url.md) - Get the currently configured registry URL * [thv config list-registered-clients](thv_config_list-registered-clients.md) - List all registered MCP clients * [thv config otel](thv_config_otel.md) - Manage OpenTelemetry configuration * [thv config register-client](thv_config_register-client.md) - Register a client for MCP server configuration * [thv config remove-client](thv_config_remove-client.md) - Remove a client from MCP server configuration +* [thv config remove-default-server](thv_config_remove-default-server.md) - Remove a server from the list of default servers +* [thv config reset-global-server-args](thv_config_reset-global-server-args.md) - Reset/delete global arguments for all MCP servers +* [thv config reset-server-args](thv_config_reset-server-args.md) - Reset/delete arguments for MCP servers from the config * [thv config set-ca-cert](thv_config_set-ca-cert.md) - Set the default CA certificate for container builds +* [thv config set-global-server-args](thv_config_set-global-server-args.md) - Set global arguments for all MCP servers * [thv config set-registry-url](thv_config_set-registry-url.md) - Set the MCP server registry URL * [thv config unset-ca-cert](thv_config_unset-ca-cert.md) - Remove the configured CA certificate * [thv config unset-registry-url](thv_config_unset-registry-url.md) - Remove the configured registry URL diff --git a/docs/cli/thv_config_add-default-server.md b/docs/cli/thv_config_add-default-server.md new file mode 100644 index 00000000..48150fd4 --- /dev/null +++ b/docs/cli/thv_config_add-default-server.md @@ -0,0 +1,32 @@ +## thv config add-default-server + +Add a server to the list of default servers + +### Synopsis + +Add a server to the list of default servers that will be started when running 'thv run' without arguments. +The server must exist in the registry. + +Example: + thv config add-default-server my-server + +``` +thv config add-default-server [server] [flags] +``` + +### Options + +``` + -h, --help help for add-default-server +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv config](thv_config.md) - Manage application configuration + diff --git a/docs/cli/thv_config_remove-default-server.md b/docs/cli/thv_config_remove-default-server.md new file mode 100644 index 00000000..3134fbff --- /dev/null +++ b/docs/cli/thv_config_remove-default-server.md @@ -0,0 +1,32 @@ +## thv config remove-default-server + +Remove a server from the list of default servers + +### Synopsis + +Remove a server from the list of default servers. +This only removes the server from the default list, it does not delete or stop the server. + +Example: + thv config remove-default-server my-server + +``` +thv config remove-default-server [server] [flags] +``` + +### Options + +``` + -h, --help help for remove-default-server +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv config](thv_config.md) - Manage application configuration + diff --git a/docs/cli/thv_config_reset-global-server-args.md b/docs/cli/thv_config_reset-global-server-args.md new file mode 100644 index 00000000..2821e23e --- /dev/null +++ b/docs/cli/thv_config_reset-global-server-args.md @@ -0,0 +1,30 @@ +## thv config reset-global-server-args + +Reset/delete global arguments for all MCP servers + +### Synopsis + +Reset/delete global arguments from the config. +This command removes all saved global arguments from the config file. +Future runs of servers will not use any pre-configured global arguments. + +``` +thv config reset-global-server-args [flags] +``` + +### Options + +``` + -h, --help help for reset-global-server-args +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv config](thv_config.md) - Manage application configuration + diff --git a/docs/cli/thv_config_reset-server-args.md b/docs/cli/thv_config_reset-server-args.md new file mode 100644 index 00000000..a8ac8095 --- /dev/null +++ b/docs/cli/thv_config_reset-server-args.md @@ -0,0 +1,38 @@ +## thv config reset-server-args + +Reset/delete arguments for MCP servers from the config + +### Synopsis + +Reset/delete configured arguments for MCP servers from the config. +This command removes saved arguments from the config file. +Future runs of the affected servers will not use any pre-configured arguments unless explicitly provided. + +Examples: + # Reset arguments for a specific server + thv config reset-server-args my-server + + # Reset arguments for all servers + thv config reset-server-args --all + +``` +thv config reset-server-args [SERVER_NAME] [flags] +``` + +### Options + +``` + -a, --all Reset arguments for all MCP servers + -h, --help help for reset-server-args +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv config](thv_config.md) - Manage application configuration + diff --git a/docs/cli/thv_config_set-global-server-args.md b/docs/cli/thv_config_set-global-server-args.md new file mode 100644 index 00000000..932f204c --- /dev/null +++ b/docs/cli/thv_config_set-global-server-args.md @@ -0,0 +1,32 @@ +## thv config set-global-server-args + +Set global arguments for all MCP servers + +### Synopsis + +Set global arguments that will be applied to all MCP servers. +These arguments will be used as defaults for all servers unless overridden by server-specific arguments. + +Example: + thv config set-global-server-args debug=true log-level=info + +``` +thv config set-global-server-args KEY=VALUE [KEY=VALUE...] [flags] +``` + +### Options + +``` + -h, --help help for set-global-server-args +``` + +### Options inherited from parent commands + +``` + --debug Enable debug mode +``` + +### SEE ALSO + +* [thv config](thv_config.md) - Manage application configuration + diff --git a/docs/cli/thv_config_set-registry-url.md b/docs/cli/thv_config_set-registry-url.md index 920be62a..60431e71 100644 --- a/docs/cli/thv_config_set-registry-url.md +++ b/docs/cli/thv_config_set-registry-url.md @@ -17,8 +17,7 @@ thv config set-registry-url [flags] ### Options ``` - -p, --allow-private-ip Allow setting the registry URL, even if it references a private IP address - -h, --help help for set-registry-url + -h, --help help for set-registry-url ``` ### Options inherited from parent commands diff --git a/docs/cli/thv_run.md b/docs/cli/thv_run.md index d69e80f6..0d8d4dd9 100644 --- a/docs/cli/thv_run.md +++ b/docs/cli/thv_run.md @@ -27,11 +27,14 @@ ToolHive supports three ways to run an MCP server: or go (Golang). For Go, you can also specify local paths starting with './' or '../' to build and run local Go projects. +If no server is specified, ToolHive will run all servers configured in the +default_servers list in the config file. + The container will be started with the specified transport mode and permission profile. Additional configuration can be provided via flags. ``` -thv run [flags] SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...] +thv run [flags] [SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...]] ``` ### Options @@ -61,6 +64,7 @@ thv run [flags] SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...] --otel-service-name string OpenTelemetry service name (defaults to toolhive-mcp-proxy) --permission-profile string Permission profile to use (none, network, or path to JSON file) (default "network") --port int Port for the HTTP proxy to listen on (host port) + -s, --save-config Save the provided command line arguments to the config file for this server --secret stringArray Specify a secret to be fetched from the secrets manager and set as an environment variable (format: NAME,target=TARGET) --target-host string Host to forward traffic to (only applicable to SSE or Streamable HTTP transport) (default "127.0.0.1") --target-port int Port for the container to expose (only applicable to SSE or Streamable HTTP transport) diff --git a/pkg/config/config.go b/pkg/config/config.go index e81c1783..164a8f39 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,6 +29,102 @@ type Config struct { AllowPrivateRegistryIp bool `yaml:"allow_private_registry_ip"` CACertificatePath string `yaml:"ca_certificate_path,omitempty"` OTEL OpenTelemetryConfig `yaml:"otel,omitempty"` + ServerArgs ServerArgs `yaml:"server_args,omitempty"` + DefaultServers []string `yaml:"default_servers,omitempty"` +} + +// ServerArgs stores arguments for specific MCP servers +type ServerArgs struct { + Servers map[string]map[string]string `yaml:"servers,omitempty"` + Global map[string]string `yaml:"global,omitempty"` +} + +// GetServerArgs returns the configured arguments for a specific server as a map +func (c *Config) GetServerArgs(serverName string) map[string]string { + if c.ServerArgs.Servers == nil { + return nil + } + return c.ServerArgs.Servers[serverName] +} + +// SetServerArgs updates the arguments for a specific server +// Only arguments provided in the args map will be updated +// Existing arguments not present in args will be preserved +func (c *Config) SetServerArgs(serverName string, args map[string]string) error { + // Get existing arguments or create new map if none exist + existing := c.ServerArgs.Servers[serverName] + if existing == nil { + existing = make(map[string]string) + } + + // Only update arguments that are provided in args + for k, v := range args { + existing[k] = v + } + + c.ServerArgs.Servers[serverName] = existing + return c.save() +} + +// DeleteServerArgs removes all arguments for a specific server from the config +// Returns an error if the server doesn't exist in the config +func (c *Config) DeleteServerArgs(serverName string) error { + if c.ServerArgs.Servers == nil { + return fmt.Errorf("no server arguments found in config") + } + + if _, exists := c.ServerArgs.Servers[serverName]; !exists { + return fmt.Errorf("no arguments found for server %s", serverName) + } + + delete(c.ServerArgs.Servers, serverName) + return c.save() +} + +// DeleteAllServerArgs removes all server arguments from the config +// Returns an error if there are no server arguments in the config +func (c *Config) DeleteAllServerArgs() error { + if len(c.ServerArgs.Servers) == 0 { + return fmt.Errorf("no server arguments found in config") + } + + c.ServerArgs.Servers = make(map[string]map[string]string) + return c.save() +} + +// GetGlobalServerArgs returns the configured arguments for a specific server as a map +func (c *Config) GetGlobalServerArgs() map[string]string { + return c.ServerArgs.Global +} + +// SetGlobalServerArgs updates the arguments for a specific server +// Only arguments provided in the args map will be updated +// Existing arguments not present in args will be preserved +func (c *Config) SetGlobalServerArgs(args map[string]string) error { + // Get existing arguments or create new map if none exist + existing := c.ServerArgs.Global + if existing == nil { + existing = make(map[string]string) + } + + // Only update arguments that are provided in args + for k, v := range args { + existing[k] = v + } + + c.ServerArgs.Global = existing + return c.save() +} + +// DeleteGlobalServerArgs removes all arguments for a specific server from the config +// Returns an error if the server doesn't exist in the config +func (c *Config) DeleteGlobalServerArgs() error { + if c.ServerArgs.Global == nil { + return fmt.Errorf("no server arguments found in config") + } + + c.ServerArgs.Global = make(map[string]string) + return c.save() } // Secrets contains the settings for secrets management.