Skip to content

Commit b78d453

Browse files
aittalampeteski22
andauthored
Config system for mcpd including support for CORS (#142)
* added github.com/go-chi/cors * introducing CORS support * Introduce dependencies and options for API Server * Update API Server * Support configuring CORS and API shutdown timeout via options * Introduce dependencies and options for Daemon * Add support for daemon config * Implemented: mcpd config daemon --help * set: sets dot-path keys to values * remove: removes dot-path keys (and their values) * list: shows all config * list --available: show all config keys that can be set * get: gets the current value for a dot-path key * Add validate command (mcpd config daemon validate) * Adjust output for list (mcpd config daemon list) * Update documentation --------- Co-authored-by: Peter Wilson <peter@mozilla.ai>
1 parent af9c706 commit b78d453

34 files changed

+9476
-186
lines changed

cmd/add_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ func (f *fakeConfig) ListServers() []config.ServerEntry {
3737
return []config.ServerEntry{f.entry}
3838
}
3939

40+
func (f *fakeConfig) SaveConfig() error {
41+
return nil
42+
}
43+
4044
type fakeLoader struct {
4145
cfg *fakeConfig
4246
err error

cmd/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/spf13/cobra"
55

66
"github.com/mozilla-ai/mcpd/v2/cmd/config/args"
7+
"github.com/mozilla-ai/mcpd/v2/cmd/config/daemon"
78
"github.com/mozilla-ai/mcpd/v2/cmd/config/env"
89
"github.com/mozilla-ai/mcpd/v2/cmd/config/export"
910
"github.com/mozilla-ai/mcpd/v2/cmd/config/tools"
@@ -21,6 +22,7 @@ func NewConfigCmd(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Comman
2122
// Sub-commands for: mcpd config
2223
fns := []func(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Command, error){
2324
args.NewCmd, // args
25+
daemon.NewCmd, // daemon
2426
env.NewCmd, // env
2527
tools.NewCmd, // tools
2628
export.NewCmd, // export

cmd/config/daemon/cmd.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package daemon
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/mozilla-ai/mcpd/v2/internal/cmd"
7+
"github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
8+
)
9+
10+
func NewCmd(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Command, error) {
11+
cobraCmd := &cobra.Command{
12+
Use: "daemon",
13+
Short: "Manages daemon configuration",
14+
Long: "Manages daemon configuration in .mcpd.toml including API settings, CORS, timeouts and intervals",
15+
}
16+
17+
// Sub-commands for: mcpd config daemon
18+
fns := []func(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Command, error){
19+
NewSetCmd, // set
20+
NewGetCmd, // get
21+
NewListCmd, // list
22+
NewRemoveCmd, // remove
23+
NewValidateCmd, // validate
24+
}
25+
26+
for _, fn := range fns {
27+
tempCmd, err := fn(baseCmd, opt...)
28+
if err != nil {
29+
return nil, err
30+
}
31+
cobraCmd.AddCommand(tempCmd)
32+
}
33+
34+
return cobraCmd, nil
35+
}

cmd/config/daemon/get.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package daemon
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/spf13/cobra"
8+
9+
"github.com/mozilla-ai/mcpd/v2/internal/cmd"
10+
cmdopts "github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
11+
"github.com/mozilla-ai/mcpd/v2/internal/config"
12+
)
13+
14+
type GetCmd struct {
15+
*cmd.BaseCmd
16+
cfgLoader config.Loader
17+
}
18+
19+
func NewGetCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
20+
opts, err := cmdopts.NewOptions(opt...)
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
c := &GetCmd{
26+
BaseCmd: baseCmd,
27+
cfgLoader: opts.ConfigLoader,
28+
}
29+
30+
cobraCmd := &cobra.Command{
31+
Use: "get <key>",
32+
Short: "Get daemon configuration value",
33+
Long: `Get a specific daemon configuration value from .mcpd.toml file using dotted key notation.
34+
35+
Examples:
36+
mcpd config daemon get api.addr
37+
mcpd config daemon get api.cors.enable
38+
mcpd config daemon get mcp.timeout.health`,
39+
RunE: c.run,
40+
Args: cobra.ExactArgs(1),
41+
}
42+
43+
return cobraCmd, nil
44+
}
45+
46+
func (c *GetCmd) run(cmd *cobra.Command, args []string) error {
47+
cfg, err := c.LoadConfig(c.cfgLoader)
48+
if err != nil {
49+
return err
50+
}
51+
52+
if cfg.Daemon == nil {
53+
return fmt.Errorf("no daemon configuration found")
54+
}
55+
56+
// Split dotted notation into keys for variadic Get
57+
key := args[0]
58+
keys := strings.Split(key, ".")
59+
60+
value, err := cfg.Daemon.Get(keys...)
61+
if err != nil {
62+
return err
63+
}
64+
65+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), c.formatValue(value))
66+
return nil
67+
}
68+
69+
func (c *GetCmd) formatValue(value any) string {
70+
switch v := value.(type) {
71+
case *config.Duration:
72+
if v == nil {
73+
return ""
74+
}
75+
return v.String()
76+
case config.Duration:
77+
return v.String()
78+
case []string:
79+
if len(v) == 0 {
80+
return "[]"
81+
}
82+
return strings.Join(v, ",")
83+
case *string:
84+
if v == nil {
85+
return ""
86+
}
87+
return *v
88+
case *bool:
89+
if v == nil {
90+
return ""
91+
}
92+
return fmt.Sprintf("%t", *v)
93+
default:
94+
return fmt.Sprintf("%v", v)
95+
}
96+
}

cmd/config/daemon/list.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package daemon
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/mozilla-ai/mcpd/v2/internal/cmd"
11+
cmdopts "github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
12+
"github.com/mozilla-ai/mcpd/v2/internal/config"
13+
)
14+
15+
type ListCmd struct {
16+
*cmd.BaseCmd
17+
cfgLoader config.Loader
18+
available bool
19+
}
20+
21+
func NewListCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
22+
opts, err := cmdopts.NewOptions(opt...)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
c := &ListCmd{
28+
BaseCmd: baseCmd,
29+
cfgLoader: opts.ConfigLoader,
30+
}
31+
32+
cobraCmd := &cobra.Command{
33+
Use: "list",
34+
Short: "List daemon configuration",
35+
Long: `List daemon configuration from .mcpd.toml file.
36+
37+
Examples:
38+
mcpd config daemon list # Show current configuration
39+
mcpd config daemon list --available # Show all available configuration keys`,
40+
RunE: c.run,
41+
Args: cobra.NoArgs,
42+
}
43+
44+
cobraCmd.Flags().
45+
BoolVar(&c.available, "available", false, "Show all available configuration keys with descriptions")
46+
47+
return cobraCmd, nil
48+
}
49+
50+
func (c *ListCmd) run(cmd *cobra.Command, args []string) error {
51+
if c.available {
52+
return c.showAvailableKeys(cmd)
53+
}
54+
55+
cfg, err := c.LoadConfig(c.cfgLoader)
56+
if err != nil {
57+
return err
58+
}
59+
60+
if cfg.Daemon == nil {
61+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No daemon configuration found")
62+
return nil
63+
}
64+
65+
// Use Getter to get all configuration
66+
allConfig, err := cfg.Daemon.Get()
67+
if err != nil {
68+
return err
69+
}
70+
71+
return c.showConfig(cmd, allConfig, "daemon")
72+
}
73+
74+
func (c *ListCmd) showConfig(cmd *cobra.Command, config any, prefix string) error {
75+
// Flatten the config into dotted key-value pairs
76+
flatConfig := make(map[string]any)
77+
c.flattenConfig(config, "", flatConfig)
78+
79+
// Sort the keys
80+
var keys []string
81+
for key := range flatConfig {
82+
keys = append(keys, key)
83+
}
84+
sort.Strings(keys)
85+
86+
// Print the sorted key-value pairs
87+
for _, key := range keys {
88+
value := flatConfig[key]
89+
c.printKeyValue(cmd, key, value)
90+
}
91+
92+
return nil
93+
}
94+
95+
// flattenConfig recursively flattens a nested configuration map into dotted key-value pairs.
96+
// The prefix parameter is used to build the full dotted path for nested keys.
97+
func (c *ListCmd) flattenConfig(value any, prefix string, result map[string]any) {
98+
prefix = strings.ToLower(strings.TrimSpace(prefix))
99+
switch v := value.(type) {
100+
case map[string]any:
101+
for key, val := range v {
102+
newKey := key
103+
if prefix != "" {
104+
newKey = prefix + "." + key
105+
}
106+
c.flattenConfig(val, newKey, result)
107+
}
108+
default:
109+
if prefix != "" {
110+
result[prefix] = value
111+
}
112+
}
113+
}
114+
115+
// printKeyValue formats and prints a single configuration key-value pair with appropriate type formatting.
116+
func (c *ListCmd) printKeyValue(cmd *cobra.Command, key string, value any) {
117+
switch v := value.(type) {
118+
case []string:
119+
if len(v) > 0 {
120+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s = %q\n", key, v)
121+
}
122+
case bool:
123+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s = %t\n", key, v)
124+
case string:
125+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s = %q\n", key, v)
126+
default:
127+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s = %v\n", key, v)
128+
}
129+
}
130+
131+
// showAvailableKeys displays all available daemon configuration keys with their types and descriptions.
132+
func (c *ListCmd) showAvailableKeys(cmd *cobra.Command) error {
133+
// Create a dummy daemon config to get the available keys
134+
daemonConfig := &config.DaemonConfig{}
135+
keys := daemonConfig.AvailableKeys()
136+
137+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Available daemon configuration keys:")
138+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "")
139+
140+
// Group keys by top-level section
141+
var apiKeys []config.SchemaKey
142+
var mcpKeys []config.SchemaKey
143+
144+
for _, key := range keys {
145+
if strings.HasPrefix(key.Path, "api.") {
146+
apiKeys = append(apiKeys, key)
147+
} else if strings.HasPrefix(key.Path, "mcp.") {
148+
mcpKeys = append(mcpKeys, key)
149+
}
150+
}
151+
152+
// Sort keys within each section
153+
sort.Slice(apiKeys, func(i, j int) bool {
154+
return apiKeys[i].Path < apiKeys[j].Path
155+
})
156+
sort.Slice(mcpKeys, func(i, j int) bool {
157+
return mcpKeys[i].Path < mcpKeys[j].Path
158+
})
159+
160+
// Show API keys
161+
if len(apiKeys) > 0 {
162+
c.showKeySection(cmd, "API Configuration:", apiKeys)
163+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "")
164+
}
165+
166+
// Show MCP keys
167+
if len(mcpKeys) > 0 {
168+
c.showKeySection(cmd, "MCP Configuration:", mcpKeys)
169+
}
170+
171+
return nil
172+
}
173+
174+
// showKeySection displays a section of configuration keys with consistent formatting.
175+
func (c *ListCmd) showKeySection(cmd *cobra.Command, title string, keys []config.SchemaKey) {
176+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), title)
177+
for _, key := range keys {
178+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " %-35s %-12s %s\n", key.Path, "("+key.Type+")", key.Description)
179+
}
180+
}

0 commit comments

Comments
 (0)