Skip to content

Commit c4ca6b3

Browse files
authored
Merge pull request #268 from docker/type-mcp-profile-server-configuration
use appropriate type for config attributes
2 parents 8f13ce3 + f91c71c commit c4ca6b3

File tree

4 files changed

+87
-10
lines changed

4 files changed

+87
-10
lines changed

cmd/docker-mcp/commands/workingset.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func configWorkingSetCommand() *cobra.Command {
6363
}
6464

6565
flags := cmd.Flags()
66-
flags.StringArrayVar(&set, "set", []string{}, "Set configuration values: <key>=<value> (can be specified multiple times)")
66+
flags.StringArrayVar(&set, "set", []string{}, "Set configuration values: <key>=<value> (repeatable). Value may be JSON to set typed values (arrays, numbers, booleans, objects).")
6767
flags.StringArrayVar(&get, "get", []string{}, "Get configuration values: <key> (can be specified multiple times)")
6868
flags.StringArrayVar(&del, "del", []string{}, "Delete configuration values: <key> (can be specified multiple times)")
6969
flags.BoolVar(&getAll, "get-all", false, "Get all configuration values")

docs/profiles.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,27 @@ docker mcp profile config my-profile --get-all --format yaml
270270
- `--get-all`: Retrieves all configuration values from all servers in the profile
271271
- `--format`: Output format - `human` (default), `json`, or `yaml`
272272

273+
**Typed values via JSON:**
274+
275+
- Values passed to `--set` are parsed as JSON when possible. This allows arrays, numbers, booleans and objects.
276+
- If the value is not valid JSON, it is stored as a plain string.
277+
- Examples:
278+
279+
```bash
280+
# Numbers and booleans
281+
docker mcp profile config my-profile --set github.timeout=60 --set github.debug=true
282+
283+
# Strings (either raw or JSON-quoted both work)
284+
docker mcp profile config my-profile --set slack.channel=general
285+
docker mcp profile config my-profile --set slack.channel='"general"'
286+
287+
# Arrays
288+
docker mcp profile config my-profile --set filesystem.paths='["/Users/dk/dev","/Users/dk/projects"]'
289+
290+
# Objects
291+
docker mcp profile config my-profile --set build.options='{"retries":3,"cache":false}'
292+
```
293+
273294
**Important notes:**
274295
- The server name must match the name from the server's snapshot (not the image or source URL)
275296
- Use `docker mcp profile show <profile-id> --format yaml` to see available server names

pkg/workingset/config.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ func UpdateConfig(ctx context.Context, dao db.DAO, ociService oci.Service, id st
3939
return fmt.Errorf("failed to resolve snapshots: %w", err)
4040
}
4141

42-
outputMap := make(map[string]string)
42+
outputMap := make(map[string]any)
4343

4444
if getAll {
4545
for _, server := range workingSet.Servers {
4646
for configName, value := range server.Config {
47-
outputMap[fmt.Sprintf("%s.%s", server.Snapshot.Server.Name, configName)] = fmt.Sprintf("%v", value)
47+
outputMap[fmt.Sprintf("%s.%s", server.Snapshot.Server.Name, configName)] = value
4848
}
4949
}
5050
} else {
@@ -60,7 +60,7 @@ func UpdateConfig(ctx context.Context, dao db.DAO, ociService oci.Service, id st
6060
}
6161

6262
if server.Config != nil && server.Config[configName] != nil {
63-
outputMap[configArg] = fmt.Sprintf("%v", server.Config[configName])
63+
outputMap[configArg] = server.Config[configName]
6464
}
6565
}
6666
}
@@ -84,9 +84,14 @@ func UpdateConfig(ctx context.Context, dao db.DAO, ociService oci.Service, id st
8484
if server.Config == nil {
8585
server.Config = make(map[string]any)
8686
}
87-
// 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)
88-
server.Config[configName] = value
89-
outputMap[key] = value
87+
// TODO(cody): validate that schema supports the config we're adding
88+
finalValue := any(value)
89+
var decoded any
90+
if err := json.Unmarshal([]byte(value), &decoded); err == nil {
91+
finalValue = decoded
92+
}
93+
server.Config[configName] = finalValue
94+
outputMap[key] = finalValue
9095
}
9196

9297
for _, delConfigArg := range delConfigArgs {
@@ -116,7 +121,7 @@ func UpdateConfig(ctx context.Context, dao db.DAO, ociService oci.Service, id st
116121
switch outputFormat {
117122
case OutputFormatHumanReadable:
118123
for configName, value := range outputMap {
119-
fmt.Printf("%s=%s\n", configName, value)
124+
fmt.Printf("%s=%v\n", configName, value)
120125
}
121126
case OutputFormatJSON:
122127
data, err := json.MarshalIndent(outputMap, "", " ")

pkg/workingset/config_test.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ func TestUpdateConfig_SetMultipleValues(t *testing.T) {
9292
dbSet, err := dao.GetWorkingSet(ctx, "test-set")
9393
require.NoError(t, err)
9494
assert.Equal(t, "secret123", dbSet.Servers[0].Config["api_key"])
95-
assert.Equal(t, "30", dbSet.Servers[0].Config["timeout"])
96-
assert.Equal(t, "true", dbSet.Servers[0].Config["enabled"])
95+
assert.Equal(t, 30, int(dbSet.Servers[0].Config["timeout"].(float64)))
96+
assert.Equal(t, true, dbSet.Servers[0].Config["enabled"])
9797
}
9898

9999
func TestUpdateConfig_GetSingleValue(t *testing.T) {
@@ -356,6 +356,57 @@ func TestUpdateConfig_YAMLOutput(t *testing.T) {
356356
assert.Equal(t, "secret123", result["test-server.api_key"])
357357
}
358358

359+
func TestUpdateConfig_JSONOutput_TypedArray(t *testing.T) {
360+
dao := setupTestDB(t)
361+
ctx := t.Context()
362+
363+
err := dao.CreateWorkingSet(ctx, db.WorkingSet{
364+
ID: "test-set",
365+
Name: "Test Working Set",
366+
Servers: db.ServerList{
367+
{
368+
Type: "image",
369+
Image: "myimage:latest",
370+
Snapshot: &db.ServerSnapshot{
371+
Server: catalog.Server{
372+
Name: "filesystem",
373+
},
374+
},
375+
},
376+
},
377+
Secrets: db.SecretMap{},
378+
})
379+
require.NoError(t, err)
380+
381+
ociService := getMockOciService()
382+
383+
// Set typed array value
384+
_ = captureStdout(func() {
385+
err = UpdateConfig(ctx, dao, ociService, "test-set",
386+
[]string{`filesystem.paths=["/Users/dk/dev","/Users/dk/projects"]`},
387+
[]string{}, []string{}, false, OutputFormatHumanReadable)
388+
require.NoError(t, err)
389+
})
390+
391+
// Get JSON, ensure array is preserved as array
392+
output := captureStdout(func() {
393+
err = UpdateConfig(ctx, dao, ociService, "test-set",
394+
[]string{}, []string{"filesystem.paths"}, []string{}, false, OutputFormatJSON)
395+
require.NoError(t, err)
396+
})
397+
398+
var result map[string]any
399+
err = json.Unmarshal([]byte(output), &result)
400+
require.NoError(t, err)
401+
402+
raw := result["filesystem.paths"]
403+
list, ok := raw.([]any)
404+
require.True(t, ok)
405+
require.Len(t, list, 2)
406+
assert.Equal(t, "/Users/dk/dev", list[0].(string))
407+
assert.Equal(t, "/Users/dk/projects", list[1].(string))
408+
}
409+
359410
func TestUpdateConfig_WorkingSetNotFound(t *testing.T) {
360411
dao := setupTestDB(t)
361412
ctx := t.Context()

0 commit comments

Comments
 (0)