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
2 changes: 1 addition & 1 deletion cmd/docker-mcp/commands/workingset.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func configWorkingSetCommand() *cobra.Command {
}

flags := cmd.Flags()
flags.StringArrayVar(&set, "set", []string{}, "Set configuration values: <key>=<value> (can be specified multiple times)")
flags.StringArrayVar(&set, "set", []string{}, "Set configuration values: <key>=<value> (repeatable). Value may be JSON to set typed values (arrays, numbers, booleans, objects).")
flags.StringArrayVar(&get, "get", []string{}, "Get configuration values: <key> (can be specified multiple times)")
flags.StringArrayVar(&del, "del", []string{}, "Delete configuration values: <key> (can be specified multiple times)")
flags.BoolVar(&getAll, "get-all", false, "Get all configuration values")
Expand Down
21 changes: 21 additions & 0 deletions docs/profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <profile-id> --format yaml` to see available server names
Expand Down
19 changes: 12 additions & 7 deletions pkg/workingset/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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]
}
}
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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, "", " ")
Expand Down
55 changes: 53 additions & 2 deletions pkg/workingset/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down
Loading