Skip to content
Merged
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
4 changes: 4 additions & 0 deletions cmd/add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func (f *fakeConfig) ListServers() []config.ServerEntry {
return []config.ServerEntry{f.entry}
}

func (f *fakeConfig) SaveConfig() error {
return nil
}

type fakeLoader struct {
cfg *fakeConfig
err error
Expand Down
2 changes: 2 additions & 0 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"

"github.com/mozilla-ai/mcpd/v2/cmd/config/args"
"github.com/mozilla-ai/mcpd/v2/cmd/config/daemon"
"github.com/mozilla-ai/mcpd/v2/cmd/config/env"
"github.com/mozilla-ai/mcpd/v2/cmd/config/export"
"github.com/mozilla-ai/mcpd/v2/cmd/config/tools"
Expand All @@ -21,6 +22,7 @@ func NewConfigCmd(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Comman
// Sub-commands for: mcpd config
fns := []func(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Command, error){
args.NewCmd, // args
daemon.NewCmd, // daemon
env.NewCmd, // env
tools.NewCmd, // tools
export.NewCmd, // export
Expand Down
35 changes: 35 additions & 0 deletions cmd/config/daemon/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package daemon

import (
"github.com/spf13/cobra"

"github.com/mozilla-ai/mcpd/v2/internal/cmd"
"github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
)

func NewCmd(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Command, error) {
cobraCmd := &cobra.Command{
Use: "daemon",
Short: "Manages daemon configuration",
Long: "Manages daemon configuration in .mcpd.toml including API settings, CORS, timeouts and intervals",
}

// Sub-commands for: mcpd config daemon
fns := []func(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Command, error){
NewSetCmd, // set
NewGetCmd, // get
NewListCmd, // list
NewRemoveCmd, // remove
NewValidateCmd, // validate
}

for _, fn := range fns {
tempCmd, err := fn(baseCmd, opt...)
if err != nil {
return nil, err
}
cobraCmd.AddCommand(tempCmd)
}

return cobraCmd, nil
}
96 changes: 96 additions & 0 deletions cmd/config/daemon/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package daemon

import (
"fmt"
"strings"

"github.com/spf13/cobra"

"github.com/mozilla-ai/mcpd/v2/internal/cmd"
cmdopts "github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
"github.com/mozilla-ai/mcpd/v2/internal/config"
)

type GetCmd struct {
*cmd.BaseCmd
cfgLoader config.Loader
}

func NewGetCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
opts, err := cmdopts.NewOptions(opt...)
if err != nil {
return nil, err
}

c := &GetCmd{
BaseCmd: baseCmd,
cfgLoader: opts.ConfigLoader,
}

cobraCmd := &cobra.Command{
Use: "get <key>",
Short: "Get daemon configuration value",
Long: `Get a specific daemon configuration value from .mcpd.toml file using dotted key notation.

Examples:
mcpd config daemon get api.addr
mcpd config daemon get api.cors.enable
mcpd config daemon get mcp.timeout.health`,
RunE: c.run,
Args: cobra.ExactArgs(1),
}

return cobraCmd, nil
}

func (c *GetCmd) run(cmd *cobra.Command, args []string) error {
cfg, err := c.LoadConfig(c.cfgLoader)
if err != nil {
return err
}

if cfg.Daemon == nil {
return fmt.Errorf("no daemon configuration found")
}

// Split dotted notation into keys for variadic Get
key := args[0]
keys := strings.Split(key, ".")

value, err := cfg.Daemon.Get(keys...)
if err != nil {
return err
}

_, _ = fmt.Fprintln(cmd.OutOrStdout(), c.formatValue(value))
return nil
}

func (c *GetCmd) formatValue(value any) string {
switch v := value.(type) {
case *config.Duration:
if v == nil {
return ""
}
return v.String()
case config.Duration:
return v.String()
case []string:
if len(v) == 0 {
return "[]"
}
return strings.Join(v, ",")
case *string:
if v == nil {
return ""
}
return *v
case *bool:
if v == nil {
return ""
}
return fmt.Sprintf("%t", *v)
default:
return fmt.Sprintf("%v", v)
}
}
180 changes: 180 additions & 0 deletions cmd/config/daemon/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package daemon

import (
"fmt"
"sort"
"strings"

"github.com/spf13/cobra"

"github.com/mozilla-ai/mcpd/v2/internal/cmd"
cmdopts "github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
"github.com/mozilla-ai/mcpd/v2/internal/config"
)

type ListCmd struct {
*cmd.BaseCmd
cfgLoader config.Loader
available bool
}

func NewListCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
opts, err := cmdopts.NewOptions(opt...)
if err != nil {
return nil, err
}

c := &ListCmd{
BaseCmd: baseCmd,
cfgLoader: opts.ConfigLoader,
}

cobraCmd := &cobra.Command{
Use: "list",
Short: "List daemon configuration",
Long: `List daemon configuration from .mcpd.toml file.

Examples:
mcpd config daemon list # Show current configuration
mcpd config daemon list --available # Show all available configuration keys`,
RunE: c.run,
Args: cobra.NoArgs,
}

cobraCmd.Flags().
BoolVar(&c.available, "available", false, "Show all available configuration keys with descriptions")

return cobraCmd, nil
}

func (c *ListCmd) run(cmd *cobra.Command, args []string) error {
if c.available {
return c.showAvailableKeys(cmd)
}

cfg, err := c.LoadConfig(c.cfgLoader)
if err != nil {
return err
}

if cfg.Daemon == nil {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No daemon configuration found")
return nil
}

// Use Getter to get all configuration
allConfig, err := cfg.Daemon.Get()
if err != nil {
return err
}

return c.showConfig(cmd, allConfig, "daemon")
}

func (c *ListCmd) showConfig(cmd *cobra.Command, config any, prefix string) error {
// Flatten the config into dotted key-value pairs
flatConfig := make(map[string]any)
c.flattenConfig(config, "", flatConfig)

// Sort the keys
var keys []string
for key := range flatConfig {
keys = append(keys, key)
}
sort.Strings(keys)

// Print the sorted key-value pairs
for _, key := range keys {
value := flatConfig[key]
c.printKeyValue(cmd, key, value)
}

return nil
}

// flattenConfig recursively flattens a nested configuration map into dotted key-value pairs.
// The prefix parameter is used to build the full dotted path for nested keys.
func (c *ListCmd) flattenConfig(value any, prefix string, result map[string]any) {
prefix = strings.ToLower(strings.TrimSpace(prefix))
switch v := value.(type) {
case map[string]any:
for key, val := range v {
newKey := key
if prefix != "" {
newKey = prefix + "." + key
}
c.flattenConfig(val, newKey, result)
}
default:
if prefix != "" {
result[prefix] = value
}
}
}

// printKeyValue formats and prints a single configuration key-value pair with appropriate type formatting.
func (c *ListCmd) printKeyValue(cmd *cobra.Command, key string, value any) {
switch v := value.(type) {
case []string:
if len(v) > 0 {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s = %q\n", key, v)
}
case bool:
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s = %t\n", key, v)
case string:
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s = %q\n", key, v)
default:
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s = %v\n", key, v)
}
}

// showAvailableKeys displays all available daemon configuration keys with their types and descriptions.
func (c *ListCmd) showAvailableKeys(cmd *cobra.Command) error {
// Create a dummy daemon config to get the available keys
daemonConfig := &config.DaemonConfig{}
keys := daemonConfig.AvailableKeys()

_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Available daemon configuration keys:")
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "")

// Group keys by top-level section
var apiKeys []config.SchemaKey
var mcpKeys []config.SchemaKey

for _, key := range keys {
if strings.HasPrefix(key.Path, "api.") {
apiKeys = append(apiKeys, key)
} else if strings.HasPrefix(key.Path, "mcp.") {
mcpKeys = append(mcpKeys, key)
}
}

// Sort keys within each section
sort.Slice(apiKeys, func(i, j int) bool {
return apiKeys[i].Path < apiKeys[j].Path
})
sort.Slice(mcpKeys, func(i, j int) bool {
return mcpKeys[i].Path < mcpKeys[j].Path
})

// Show API keys
if len(apiKeys) > 0 {
c.showKeySection(cmd, "API Configuration:", apiKeys)
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "")
}

// Show MCP keys
if len(mcpKeys) > 0 {
c.showKeySection(cmd, "MCP Configuration:", mcpKeys)
}

return nil
}

// showKeySection displays a section of configuration keys with consistent formatting.
func (c *ListCmd) showKeySection(cmd *cobra.Command, title string, keys []config.SchemaKey) {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), title)
for _, key := range keys {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " %-35s %-12s %s\n", key.Path, "("+key.Type+")", key.Description)
}
}
Loading