Skip to content

Commit ee7fccd

Browse files
committed
feat: Add id to profile create
Add output options to the profile create command so that we can return an id for the newly create profile. Supported formats are consistent with other commands (i.e. yaml and json)
1 parent 6401238 commit ee7fccd

File tree

4 files changed

+229
-33
lines changed

4 files changed

+229
-33
lines changed

cmd/docker-mcp/commands/workingset.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,11 @@ func createWorkingSetCommand(cfg *client.Config) *cobra.Command {
132132
Name string
133133
Servers []string
134134
Connect []string
135+
Format string
135136
}
136137

137138
cmd := &cobra.Command{
138-
Use: "create --name <name> [--id <id>] --server <ref1> --server <ref2> ... [--connect <client1> --connect <client2> ...]",
139+
Use: "create --name <name> [--id <id>] --server <ref1> --server <ref2> ... [--connect <client1> --connect <client2> ...] [--format <format>]",
139140
Short: "Create a new profile of MCP servers",
140141
Long: `Create a new profile that groups multiple MCP servers together.
141142
A profile allows you to organize and manage related servers as a single unit.
@@ -154,16 +155,23 @@ Profiles are decoupled from catalogs. Servers can be:
154155
docker mcp profile create --name my-profile --server http://registry.modelcontextprotocol.io/v0/servers/71de5a2a-6cfb-4250-a196-f93080ecc860
155156
156157
# Connect to clients upon creation
157-
docker mcp profile create --name dev-tools --connect cursor`,
158+
docker mcp profile create --name dev-tools --connect cursor
159+
160+
# Create a profile with JSON output
161+
docker mcp profile create --name dev-tools --format json`,
158162
Args: cobra.NoArgs,
159163
RunE: func(cmd *cobra.Command, _ []string) error {
164+
supported := slices.Contains(workingset.SupportedFormats(), opts.Format)
165+
if !supported {
166+
return fmt.Errorf("unsupported format: %s", opts.Format)
167+
}
160168
dao, err := db.New()
161169
if err != nil {
162170
return err
163171
}
164172
registryClient := registryapi.NewClient()
165173
ociService := oci.NewService()
166-
return workingset.Create(cmd.Context(), dao, registryClient, ociService, opts.ID, opts.Name, opts.Servers, opts.Connect)
174+
return workingset.Create(cmd.Context(), dao, registryClient, ociService, opts.ID, opts.Name, opts.Servers, opts.Connect, workingset.OutputFormat(opts.Format))
167175
},
168176
}
169177

@@ -172,6 +180,7 @@ Profiles are decoupled from catalogs. Servers can be:
172180
flags.StringVar(&opts.ID, "id", "", "ID of the profile (defaults to a slugified version of the name)")
173181
flags.StringArrayVar(&opts.Servers, "server", []string{}, "Server to include specified with a URI: https:// (MCP Registry reference) or docker:// (Docker Image reference) or catalog:// (Catalog reference) or file:// (Local file path). Can be specified multiple times.")
174182
flags.StringArrayVar(&opts.Connect, "connect", []string{}, fmt.Sprintf("Clients to connect to: mcp-client (can be specified multiple times). Supported clients: %s", client.GetSupportedMCPClients(*cfg)))
183+
flags.StringVar(&opts.Format, "format", string(workingset.OutputFormatHumanReadable), fmt.Sprintf("Supported: %s.", strings.Join(workingset.SupportedFormats(), ", ")))
175184
_ = cmd.MarkFlagRequired("name")
176185

177186
return cmd

pkg/workingset/create.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/docker/mcp-gateway/pkg/telemetry"
1616
)
1717

18-
func Create(ctx context.Context, dao db.DAO, registryClient registryapi.Client, ociService oci.Service, id string, name string, servers []string, connectClients []string) error {
18+
func Create(ctx context.Context, dao db.DAO, registryClient registryapi.Client, ociService oci.Service, id string, name string, servers []string, connectClients []string, format OutputFormat) error {
1919
telemetry.Init()
2020
start := time.Now()
2121
var success bool
@@ -88,9 +88,21 @@ func Create(ctx context.Context, dao db.DAO, registryClient registryapi.Client,
8888
}
8989
}
9090

91-
fmt.Printf("Created profile %s with %d servers\n", id, len(workingSet.Servers))
92-
if len(connectClients) > 0 {
93-
fmt.Printf("Connected to clients: %s\n", strings.Join(connectClients, ", "))
91+
switch format {
92+
case OutputFormatJSON:
93+
// Output JSON with just the profile ID
94+
fmt.Printf("{\"id\":\"%s\"}\n", id)
95+
case OutputFormatYAML:
96+
// Output YAML with just the profile ID
97+
fmt.Printf("id: %s\n", id)
98+
case OutputFormatHumanReadable:
99+
// Output human-readable format (default)
100+
fmt.Printf("Created profile %s with %d servers\n", id, len(workingSet.Servers))
101+
if len(connectClients) > 0 {
102+
fmt.Printf("Connected to clients: %s\n", strings.Join(connectClients, ", "))
103+
}
104+
default:
105+
return fmt.Errorf("unsupported output format: %s", format)
94106
}
95107

96108
success = true

pkg/workingset/create_test.go

Lines changed: 189 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package workingset
22

33
import (
4+
"encoding/json"
45
"testing"
56

67
v0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
@@ -97,7 +98,7 @@ func TestCreateWithDockerImages(t *testing.T) {
9798
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "My Test Set", []string{
9899
"docker://myimage:latest",
99100
"docker://anotherimage:v1.0",
100-
}, []string{})
101+
}, []string{}, OutputFormatHumanReadable)
101102
require.NoError(t, err)
102103

103104
// Verify the working set was created
@@ -123,7 +124,7 @@ func TestCreateWithRegistryServers(t *testing.T) {
123124
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Registry Set", []string{
124125
"https://example.com/v0/servers/server1",
125126
"https://example.com/v0/servers/server2",
126-
}, []string{})
127+
}, []string{}, OutputFormatHumanReadable)
127128
require.NoError(t, err)
128129

129130
// Verify the working set was created
@@ -147,7 +148,7 @@ func TestCreateWithMixedServers(t *testing.T) {
147148
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Mixed Set", []string{
148149
"docker://myimage:latest",
149150
"https://example.com/v0/servers/server1",
150-
}, []string{})
151+
}, []string{}, OutputFormatHumanReadable)
151152
require.NoError(t, err)
152153

153154
// Verify the working set was created
@@ -166,7 +167,7 @@ func TestCreateWithCustomId(t *testing.T) {
166167

167168
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "custom-id", "Test Set", []string{
168169
"docker://myimage:latest",
169-
}, []string{})
170+
}, []string{}, OutputFormatHumanReadable)
170171
require.NoError(t, err)
171172

172173
// Verify the working set was created with custom ID
@@ -185,13 +186,13 @@ func TestCreateWithExistingId(t *testing.T) {
185186
// Create first working set
186187
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "test-id", "Test Set 1", []string{
187188
"docker://myimage:latest",
188-
}, []string{})
189+
}, []string{}, OutputFormatHumanReadable)
189190
require.NoError(t, err)
190191

191192
// Try to create another with the same ID
192193
err = Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "test-id", "Test Set 2", []string{
193194
"docker://anotherimage:latest",
194-
}, []string{})
195+
}, []string{}, OutputFormatHumanReadable)
195196
require.Error(t, err)
196197
assert.Contains(t, err.Error(), "already exists")
197198
}
@@ -203,19 +204,19 @@ func TestCreateGeneratesUniqueIds(t *testing.T) {
203204
// Create first working set
204205
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test Set", []string{
205206
"docker://myimage:latest",
206-
}, []string{})
207+
}, []string{}, OutputFormatHumanReadable)
207208
require.NoError(t, err)
208209

209210
// Create second with same name
210211
err = Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test Set", []string{
211212
"docker://anotherimage:v1.0",
212-
}, []string{})
213+
}, []string{}, OutputFormatHumanReadable)
213214
require.NoError(t, err)
214215

215216
// Create third with same name
216217
err = Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test Set", []string{
217218
"docker://anotherimage:v1.0",
218-
}, []string{})
219+
}, []string{}, OutputFormatHumanReadable)
219220
require.NoError(t, err)
220221

221222
// List all working sets
@@ -242,7 +243,7 @@ func TestCreateWithInvalidServerFormat(t *testing.T) {
242243

243244
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test Set", []string{
244245
"invalid-format",
245-
}, []string{})
246+
}, []string{}, OutputFormatHumanReadable)
246247
require.Error(t, err)
247248
assert.Contains(t, err.Error(), "invalid server value")
248249
}
@@ -253,7 +254,7 @@ func TestCreateWithEmptyName(t *testing.T) {
253254

254255
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "test-id", "", []string{
255256
"docker://myimage:latest",
256-
}, []string{})
257+
}, []string{}, OutputFormatHumanReadable)
257258
require.Error(t, err)
258259
assert.Contains(t, err.Error(), "invalid profile")
259260
}
@@ -262,7 +263,7 @@ func TestCreateWithEmptyServers(t *testing.T) {
262263
dao := setupTestDB(t)
263264
ctx := t.Context()
264265

265-
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Empty Set", []string{}, []string{})
266+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Empty Set", []string{}, []string{}, OutputFormatHumanReadable)
266267
require.NoError(t, err)
267268

268269
// Verify the working set was created with no servers
@@ -279,7 +280,7 @@ func TestCreateAddsDefaultSecrets(t *testing.T) {
279280

280281
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test Set", []string{
281282
"docker://myimage:latest",
282-
}, []string{})
283+
}, []string{}, OutputFormatHumanReadable)
283284
require.NoError(t, err)
284285

285286
// Verify default secrets were added
@@ -328,7 +329,7 @@ func TestCreateNameWithSpecialCharacters(t *testing.T) {
328329

329330
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", tt.inputName, []string{
330331
"docker://myimage:latest",
331-
}, []string{})
332+
}, []string{}, OutputFormatHumanReadable)
332333
require.NoError(t, err)
333334

334335
// Verify the ID was generated correctly
@@ -339,3 +340,177 @@ func TestCreateNameWithSpecialCharacters(t *testing.T) {
339340
})
340341
}
341342
}
343+
344+
// TestCreateOutputFormatJSON tests that JSON output format returns valid JSON with the profile ID
345+
func TestCreateOutputFormatJSON(t *testing.T) {
346+
dao := setupTestDB(t)
347+
ctx := t.Context()
348+
349+
// Capture stdout
350+
output := captureStdout(func() {
351+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test Set", []string{
352+
"docker://myimage:latest",
353+
}, []string{}, OutputFormatJSON)
354+
require.NoError(t, err)
355+
})
356+
357+
// Verify JSON structure
358+
var result map[string]string
359+
err := json.Unmarshal([]byte(output), &result)
360+
require.NoError(t, err, "Output should be valid JSON")
361+
362+
// Verify the ID field exists and has the expected value
363+
assert.Equal(t, "test-set", result["id"])
364+
assert.Len(t, result, 1, "JSON output should only contain the id field")
365+
366+
// Verify the profile was created in the database
367+
dbSet, err := dao.GetWorkingSet(ctx, "test-set")
368+
require.NoError(t, err)
369+
assert.Equal(t, "test-set", dbSet.ID)
370+
}
371+
372+
// TestCreateOutputFormatHuman tests that human-readable format outputs the expected message
373+
func TestCreateOutputFormatHuman(t *testing.T) {
374+
dao := setupTestDB(t)
375+
ctx := t.Context()
376+
377+
// Capture stdout
378+
output := captureStdout(func() {
379+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test Set", []string{
380+
"docker://myimage:latest",
381+
"docker://anotherimage:v1.0",
382+
}, []string{}, OutputFormatHumanReadable)
383+
require.NoError(t, err)
384+
})
385+
386+
// Verify human-readable output
387+
assert.Contains(t, output, "Created profile test-set with 2 servers")
388+
389+
// Verify the profile was created in the database
390+
dbSet, err := dao.GetWorkingSet(ctx, "test-set")
391+
require.NoError(t, err)
392+
assert.Equal(t, "test-set", dbSet.ID)
393+
assert.Len(t, dbSet.Servers, 2)
394+
}
395+
396+
// TestCreateJSONWithDuplicateIDPrevention tests that JSON output reflects the actual ID with duplicate suffix
397+
func TestCreateJSONWithDuplicateIDPrevention(t *testing.T) {
398+
dao := setupTestDB(t)
399+
ctx := t.Context()
400+
401+
// Create first profile
402+
output1 := captureStdout(func() {
403+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test", []string{
404+
"docker://myimage:latest",
405+
}, []string{}, OutputFormatJSON)
406+
require.NoError(t, err)
407+
})
408+
409+
var result1 map[string]string
410+
err := json.Unmarshal([]byte(output1), &result1)
411+
require.NoError(t, err)
412+
assert.Equal(t, "test", result1["id"])
413+
414+
// Create second profile with the same name
415+
output2 := captureStdout(func() {
416+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test", []string{
417+
"docker://myimage:latest",
418+
}, []string{}, OutputFormatJSON)
419+
require.NoError(t, err)
420+
})
421+
422+
var result2 map[string]string
423+
err = json.Unmarshal([]byte(output2), &result2)
424+
require.NoError(t, err)
425+
assert.Equal(t, "test-2", result2["id"], "Second profile should have -2 suffix")
426+
427+
// Create third profile with the same name
428+
output3 := captureStdout(func() {
429+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test", []string{
430+
"docker://myimage:latest",
431+
}, []string{}, OutputFormatJSON)
432+
require.NoError(t, err)
433+
})
434+
435+
var result3 map[string]string
436+
err = json.Unmarshal([]byte(output3), &result3)
437+
require.NoError(t, err)
438+
assert.Equal(t, "test-3", result3["id"], "Third profile should have -3 suffix")
439+
}
440+
441+
// TestCreateDefaultOutputFormat tests that the default format is human-readable
442+
func TestCreateDefaultOutputFormat(t *testing.T) {
443+
dao := setupTestDB(t)
444+
ctx := t.Context()
445+
446+
// Capture stdout with default format (OutputFormatHumanReadable)
447+
output := captureStdout(func() {
448+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test Set", []string{
449+
"docker://myimage:latest",
450+
}, []string{}, OutputFormatHumanReadable)
451+
require.NoError(t, err)
452+
})
453+
454+
// Verify it's human-readable, not JSON
455+
assert.Contains(t, output, "Created profile")
456+
assert.NotContains(t, output, "{\"id\":")
457+
}
458+
459+
// TestCreateJSONWithNoServers tests that JSON output works correctly with empty server list
460+
func TestCreateJSONWithNoServers(t *testing.T) {
461+
dao := setupTestDB(t)
462+
ctx := t.Context()
463+
464+
// Capture stdout
465+
output := captureStdout(func() {
466+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Empty Set", []string{}, []string{}, OutputFormatJSON)
467+
require.NoError(t, err)
468+
})
469+
470+
// Verify JSON structure
471+
var result map[string]string
472+
err := json.Unmarshal([]byte(output), &result)
473+
require.NoError(t, err)
474+
assert.Equal(t, "empty-set", result["id"])
475+
476+
// Verify the profile was created in the database with no servers
477+
dbSet, err := dao.GetWorkingSet(ctx, "empty-set")
478+
require.NoError(t, err)
479+
assert.Equal(t, "empty-set", dbSet.ID)
480+
assert.Empty(t, dbSet.Servers)
481+
}
482+
483+
// TestCreateOutputFormatYAML tests that YAML output format returns valid YAML with the profile ID
484+
func TestCreateOutputFormatYAML(t *testing.T) {
485+
dao := setupTestDB(t)
486+
ctx := t.Context()
487+
488+
// Capture stdout
489+
output := captureStdout(func() {
490+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test Set", []string{
491+
"docker://myimage:latest",
492+
}, []string{}, OutputFormatYAML)
493+
require.NoError(t, err)
494+
})
495+
496+
// Verify YAML format
497+
assert.Equal(t, "id: test-set\n", output)
498+
499+
// Verify the profile was created in the database
500+
dbSet, err := dao.GetWorkingSet(ctx, "test-set")
501+
require.NoError(t, err)
502+
assert.Equal(t, "test-set", dbSet.ID)
503+
}
504+
505+
// TestCreateUnsupportedFormat tests that unsupported formats are rejected
506+
func TestCreateUnsupportedFormat(t *testing.T) {
507+
dao := setupTestDB(t)
508+
ctx := t.Context()
509+
510+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "", "Test Set", []string{
511+
"docker://myimage:latest",
512+
}, []string{}, OutputFormat("unsupported"))
513+
514+
require.Error(t, err)
515+
assert.Contains(t, err.Error(), "unsupported output format")
516+
}

0 commit comments

Comments
 (0)