diff --git a/cmd/docker-mcp/commands/workingset.go b/cmd/docker-mcp/commands/workingset.go index f30576cf..c1381a93 100644 --- a/cmd/docker-mcp/commands/workingset.go +++ b/cmd/docker-mcp/commands/workingset.go @@ -63,7 +63,7 @@ func configWorkingSetCommand() *cobra.Command { } flags := cmd.Flags() - flags.StringArrayVar(&set, "set", []string{}, "Set configuration values: = (can be specified multiple times)") + flags.StringArrayVar(&set, "set", []string{}, "Set configuration values: = (repeatable). Value may be JSON to set typed values (arrays, numbers, booleans, objects).") flags.StringArrayVar(&get, "get", []string{}, "Get configuration values: (can be specified multiple times)") flags.StringArrayVar(&del, "del", []string{}, "Delete configuration values: (can be specified multiple times)") flags.BoolVar(&getAll, "get-all", false, "Get all configuration values") diff --git a/docs/profiles.md b/docs/profiles.md index 8314c1c7..decc22ae 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -270,6 +270,27 @@ docker mcp profile config my-profile --get-all --format yaml - `--get-all`: Retrieves all configuration values from all servers in the profile - `--format`: Output format - `human` (default), `json`, or `yaml` +**Typed values via JSON:** + +- Values passed to `--set` are parsed as JSON when possible. This allows arrays, numbers, booleans and objects. +- If the value is not valid JSON, it is stored as a plain string. +- Examples: + +```bash +# Numbers and booleans +docker mcp profile config my-profile --set github.timeout=60 --set github.debug=true + +# Strings (either raw or JSON-quoted both work) +docker mcp profile config my-profile --set slack.channel=general +docker mcp profile config my-profile --set slack.channel='"general"' + +# Arrays +docker mcp profile config my-profile --set filesystem.paths='["/Users/dk/dev","/Users/dk/projects"]' + +# Objects +docker mcp profile config my-profile --set build.options='{"retries":3,"cache":false}' +``` + **Important notes:** - The server name must match the name from the server's snapshot (not the image or source URL) - Use `docker mcp profile show --format yaml` to see available server names diff --git a/pkg/workingset/config.go b/pkg/workingset/config.go index 0dd2d07b..96867231 100644 --- a/pkg/workingset/config.go +++ b/pkg/workingset/config.go @@ -39,12 +39,12 @@ func UpdateConfig(ctx context.Context, dao db.DAO, ociService oci.Service, id st return fmt.Errorf("failed to resolve snapshots: %w", err) } - outputMap := make(map[string]string) + outputMap := make(map[string]any) if getAll { for _, server := range workingSet.Servers { for configName, value := range server.Config { - outputMap[fmt.Sprintf("%s.%s", server.Snapshot.Server.Name, configName)] = fmt.Sprintf("%v", value) + outputMap[fmt.Sprintf("%s.%s", server.Snapshot.Server.Name, configName)] = value } } } else { @@ -60,7 +60,7 @@ func UpdateConfig(ctx context.Context, dao db.DAO, ociService oci.Service, id st } if server.Config != nil && server.Config[configName] != nil { - outputMap[configArg] = fmt.Sprintf("%v", server.Config[configName]) + outputMap[configArg] = server.Config[configName] } } } @@ -84,9 +84,14 @@ func UpdateConfig(ctx context.Context, dao db.DAO, ociService oci.Service, id st if server.Config == nil { server.Config = make(map[string]any) } - // TODO(cody): validate that schema supports the config we're adding and map it to the right type (right now we're forcing a string) - server.Config[configName] = value - outputMap[key] = value + // TODO(cody): validate that schema supports the config we're adding + finalValue := any(value) + var decoded any + if err := json.Unmarshal([]byte(value), &decoded); err == nil { + finalValue = decoded + } + server.Config[configName] = finalValue + outputMap[key] = finalValue } for _, delConfigArg := range delConfigArgs { @@ -116,7 +121,7 @@ func UpdateConfig(ctx context.Context, dao db.DAO, ociService oci.Service, id st switch outputFormat { case OutputFormatHumanReadable: for configName, value := range outputMap { - fmt.Printf("%s=%s\n", configName, value) + fmt.Printf("%s=%v\n", configName, value) } case OutputFormatJSON: data, err := json.MarshalIndent(outputMap, "", " ") diff --git a/pkg/workingset/config_test.go b/pkg/workingset/config_test.go index 79377fcc..f6427358 100644 --- a/pkg/workingset/config_test.go +++ b/pkg/workingset/config_test.go @@ -92,8 +92,8 @@ func TestUpdateConfig_SetMultipleValues(t *testing.T) { dbSet, err := dao.GetWorkingSet(ctx, "test-set") require.NoError(t, err) assert.Equal(t, "secret123", dbSet.Servers[0].Config["api_key"]) - assert.Equal(t, "30", dbSet.Servers[0].Config["timeout"]) - assert.Equal(t, "true", dbSet.Servers[0].Config["enabled"]) + assert.Equal(t, 30, int(dbSet.Servers[0].Config["timeout"].(float64))) + assert.Equal(t, true, dbSet.Servers[0].Config["enabled"]) } func TestUpdateConfig_GetSingleValue(t *testing.T) { @@ -356,6 +356,57 @@ func TestUpdateConfig_YAMLOutput(t *testing.T) { assert.Equal(t, "secret123", result["test-server.api_key"]) } +func TestUpdateConfig_JSONOutput_TypedArray(t *testing.T) { + dao := setupTestDB(t) + ctx := t.Context() + + err := dao.CreateWorkingSet(ctx, db.WorkingSet{ + ID: "test-set", + Name: "Test Working Set", + Servers: db.ServerList{ + { + Type: "image", + Image: "myimage:latest", + Snapshot: &db.ServerSnapshot{ + Server: catalog.Server{ + Name: "filesystem", + }, + }, + }, + }, + Secrets: db.SecretMap{}, + }) + require.NoError(t, err) + + ociService := getMockOciService() + + // Set typed array value + _ = captureStdout(func() { + err = UpdateConfig(ctx, dao, ociService, "test-set", + []string{`filesystem.paths=["/Users/dk/dev","/Users/dk/projects"]`}, + []string{}, []string{}, false, OutputFormatHumanReadable) + require.NoError(t, err) + }) + + // Get JSON, ensure array is preserved as array + output := captureStdout(func() { + err = UpdateConfig(ctx, dao, ociService, "test-set", + []string{}, []string{"filesystem.paths"}, []string{}, false, OutputFormatJSON) + require.NoError(t, err) + }) + + var result map[string]any + err = json.Unmarshal([]byte(output), &result) + require.NoError(t, err) + + raw := result["filesystem.paths"] + list, ok := raw.([]any) + require.True(t, ok) + require.Len(t, list, 2) + assert.Equal(t, "/Users/dk/dev", list[0].(string)) + assert.Equal(t, "/Users/dk/projects", list[1].(string)) +} + func TestUpdateConfig_WorkingSetNotFound(t *testing.T) { dao := setupTestDB(t) ctx := t.Context()