Skip to content

Commit 3c578eb

Browse files
authored
Add --no-tools to catalog show and server ls (#270)
* feat: add --no-tools option and server ls To address issue where too much data was being printed when showing an entire catalog, introduce a option to specify --no-tools to exclude tools from the response. Additionally, add a `docker mcp catalog-next server ls` command so that we can get individual server information from a catalog that does include the tools. * fix: return catalog list data Using --format json or yaml would return the entire contents of all the catalogs. Instead, return the data that is returned for human readable output.
1 parent c4ca6b3 commit 3c578eb

File tree

8 files changed

+1007
-52
lines changed

8 files changed

+1007
-52
lines changed

cmd/docker-mcp/commands/catalog_next.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func catalogNextCommand() *cobra.Command {
2626
cmd.AddCommand(pushCatalogNextCommand())
2727
cmd.AddCommand(pullCatalogNextCommand())
2828
cmd.AddCommand(tagCatalogNextCommand())
29+
cmd.AddCommand(catalogNextServerCommand())
2930

3031
return cmd
3132
}
@@ -83,6 +84,7 @@ func tagCatalogNextCommand() *cobra.Command {
8384
func showCatalogNextCommand() *cobra.Command {
8485
format := string(workingset.OutputFormatHumanReadable)
8586
pullOption := string(catalognext.PullOptionNever)
87+
var noTools bool
8688

8789
cmd := &cobra.Command{
8890
Use: "show <oci-reference> [--pull <pull-option>]",
@@ -98,13 +100,14 @@ func showCatalogNextCommand() *cobra.Command {
98100
return err
99101
}
100102
ociService := oci.NewService()
101-
return catalognext.Show(cmd.Context(), dao, ociService, args[0], workingset.OutputFormat(format), pullOption)
103+
return catalognext.Show(cmd.Context(), dao, ociService, args[0], workingset.OutputFormat(format), pullOption, noTools)
102104
},
103105
}
104106

105107
flags := cmd.Flags()
106108
flags.StringVar(&format, "format", string(workingset.OutputFormatHumanReadable), fmt.Sprintf("Supported: %s.", strings.Join(workingset.SupportedFormats(), ", ")))
107109
flags.StringVar(&pullOption, "pull", string(catalognext.PullOptionNever), fmt.Sprintf("Supported: %s, or duration (e.g. '1h', '1d'). Duration represents time since last update.", strings.Join(catalognext.SupportedPullOptions(), ", ")))
110+
flags.BoolVar(&noTools, "no-tools", false, "Exclude tools from output")
108111
return cmd
109112
}
110113

@@ -181,3 +184,62 @@ func pullCatalogNextCommand() *cobra.Command {
181184
},
182185
}
183186
}
187+
188+
func catalogNextServerCommand() *cobra.Command {
189+
cmd := &cobra.Command{
190+
Use: "server",
191+
Short: "Manage servers in catalogs",
192+
}
193+
194+
cmd.AddCommand(listCatalogNextServersCommand())
195+
196+
return cmd
197+
}
198+
199+
func listCatalogNextServersCommand() *cobra.Command {
200+
var opts struct {
201+
Filters []string
202+
Format string
203+
}
204+
205+
cmd := &cobra.Command{
206+
Use: "ls <oci-reference>",
207+
Aliases: []string{"list"},
208+
Short: "List servers in a catalog",
209+
Long: `List all servers in a catalog.
210+
211+
Use --filter to search for servers matching a query (case-insensitive substring matching on server names).
212+
Filters use key=value format (e.g., name=github).`,
213+
Example: ` # List all servers in a catalog
214+
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest
215+
216+
# Filter servers by name
217+
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest --filter name=github
218+
219+
# Combine multiple filters (using short flag)
220+
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest -f name=slack -f name=github
221+
222+
# Output in JSON format
223+
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest --format json`,
224+
Args: cobra.ExactArgs(1),
225+
RunE: func(cmd *cobra.Command, args []string) error {
226+
supported := slices.Contains(workingset.SupportedFormats(), opts.Format)
227+
if !supported {
228+
return fmt.Errorf("unsupported format: %s", opts.Format)
229+
}
230+
231+
dao, err := db.New()
232+
if err != nil {
233+
return err
234+
}
235+
236+
return catalognext.ListServers(cmd.Context(), dao, args[0], opts.Filters, workingset.OutputFormat(opts.Format))
237+
},
238+
}
239+
240+
flags := cmd.Flags()
241+
flags.StringArrayVarP(&opts.Filters, "filter", "f", []string{}, "Filter output (e.g., name=github)")
242+
flags.StringVar(&opts.Format, "format", string(workingset.OutputFormatHumanReadable), fmt.Sprintf("Supported: %s.", strings.Join(workingset.SupportedFormats(), ", ")))
243+
244+
return cmd
245+
}

pkg/catalog_next/catalog.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ type CatalogWithDigest struct {
2525
Digest string `yaml:"digest" json:"digest"`
2626
}
2727

28+
type CatalogSummary struct {
29+
Ref string `yaml:"ref" json:"ref"`
30+
Digest string `yaml:"digest" json:"digest"`
31+
Title string `yaml:"title" json:"title"`
32+
}
33+
2834
// Source prefixes must be of the form "<prefix>:"
2935
const (
3036
SourcePrefixWorkingSet = "profile:"

pkg/catalog_next/list.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,23 @@ func List(ctx context.Context, dao db.DAO, format workingset.OutputFormat) error
2323
return nil
2424
}
2525

26-
catalogs := make([]CatalogWithDigest, len(dbCatalogs))
26+
summaries := make([]CatalogSummary, len(dbCatalogs))
2727
for i, dbCatalog := range dbCatalogs {
28-
catalogs[i] = NewFromDb(&dbCatalog)
28+
summaries[i] = CatalogSummary{
29+
Ref: dbCatalog.Ref,
30+
Digest: dbCatalog.Digest,
31+
Title: dbCatalog.Title,
32+
}
2933
}
3034

3135
var data []byte
3236
switch format {
3337
case workingset.OutputFormatHumanReadable:
34-
data = []byte(printListHumanReadable(catalogs))
38+
data = []byte(printListHumanReadable(summaries))
3539
case workingset.OutputFormatJSON:
36-
data, err = json.MarshalIndent(catalogs, "", " ")
40+
data, err = json.MarshalIndent(summaries, "", " ")
3741
case workingset.OutputFormatYAML:
38-
data, err = yaml.Marshal(catalogs)
42+
data, err = yaml.Marshal(summaries)
3943
}
4044
if err != nil {
4145
return fmt.Errorf("failed to marshal catalogs: %w", err)
@@ -46,7 +50,7 @@ func List(ctx context.Context, dao db.DAO, format workingset.OutputFormat) error
4650
return nil
4751
}
4852

49-
func printListHumanReadable(catalogs []CatalogWithDigest) string {
53+
func printListHumanReadable(catalogs []CatalogSummary) string {
5054
lines := ""
5155
for _, catalog := range catalogs {
5256
lines += fmt.Sprintf("%s\t| %s\t| %s\n", catalog.Ref, catalog.Digest, catalog.Title)

pkg/catalog_next/list_test.go

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -138,26 +138,20 @@ func TestListJSON(t *testing.T) {
138138
})
139139

140140
// Parse JSON output
141-
var catalogs []CatalogWithDigest
141+
var catalogs []CatalogSummary
142142
err = json.Unmarshal([]byte(output), &catalogs)
143143
require.NoError(t, err)
144144
assert.Len(t, catalogs, 2)
145145

146-
// Verify first catalog
146+
// Verify first catalog (summary only)
147+
assert.Equal(t, "test/catalog4:latest", catalogs[0].Ref)
147148
assert.Equal(t, "catalog-one", catalogs[0].Title)
148-
assert.Equal(t, "source-1", catalogs[0].Source)
149-
assert.Len(t, catalogs[0].Servers, 1)
150-
assert.Equal(t, workingset.ServerTypeImage, catalogs[0].Servers[0].Type)
151-
assert.Equal(t, "test:v1", catalogs[0].Servers[0].Image)
152-
assert.Equal(t, []string{"tool1"}, catalogs[0].Servers[0].Tools)
149+
assert.NotEmpty(t, catalogs[0].Digest)
153150

154-
// Verify second catalog
151+
// Verify second catalog (summary only)
152+
assert.Equal(t, "test/catalog5:latest", catalogs[1].Ref)
155153
assert.Equal(t, "catalog-two", catalogs[1].Title)
156-
assert.Equal(t, "source-2", catalogs[1].Source)
157-
assert.Len(t, catalogs[1].Servers, 1)
158-
assert.Equal(t, workingset.ServerTypeRegistry, catalogs[1].Servers[0].Type)
159-
assert.Equal(t, "https://example.com", catalogs[1].Servers[0].Source)
160-
assert.Equal(t, []string{"tool2", "tool3"}, catalogs[1].Servers[0].Tools)
154+
assert.NotEmpty(t, catalogs[1].Digest)
161155
}
162156

163157
func TestListJSONEmpty(t *testing.T) {
@@ -170,7 +164,7 @@ func TestListJSONEmpty(t *testing.T) {
170164
})
171165

172166
// Parse JSON output
173-
var catalogs []CatalogWithDigest
167+
var catalogs []CatalogSummary
174168
err := json.Unmarshal([]byte(output), &catalogs)
175169
require.NoError(t, err)
176170
assert.Empty(t, catalogs)
@@ -206,18 +200,15 @@ func TestListYAML(t *testing.T) {
206200
})
207201

208202
// Parse YAML output
209-
var catalogs []CatalogWithDigest
203+
var catalogs []CatalogSummary
210204
err = yaml.Unmarshal([]byte(output), &catalogs)
211205
require.NoError(t, err)
212206
assert.Len(t, catalogs, 1)
213207

214-
// Verify catalog
208+
// Verify catalog (summary only)
209+
assert.Equal(t, "test/catalog6:latest", catalogs[0].Ref)
215210
assert.Equal(t, "catalog-yaml", catalogs[0].Title)
216-
assert.Equal(t, "yaml-source", catalogs[0].Source)
217-
assert.Len(t, catalogs[0].Servers, 1)
218-
assert.Equal(t, workingset.ServerTypeImage, catalogs[0].Servers[0].Type)
219-
assert.Equal(t, "test:yaml", catalogs[0].Servers[0].Image)
220-
assert.Equal(t, []string{"tool1", "tool2"}, catalogs[0].Servers[0].Tools)
211+
assert.NotEmpty(t, catalogs[0].Digest)
221212
}
222213

223214
func TestListYAMLEmpty(t *testing.T) {
@@ -230,7 +221,7 @@ func TestListYAMLEmpty(t *testing.T) {
230221
})
231222

232223
// Parse YAML output
233-
var catalogs []CatalogWithDigest
224+
var catalogs []CatalogSummary
234225
err := yaml.Unmarshal([]byte(output), &catalogs)
235226
require.NoError(t, err)
236227
assert.Empty(t, catalogs)
@@ -271,15 +262,15 @@ func TestListWithSnapshot(t *testing.T) {
271262
require.NoError(t, err)
272263
})
273264

274-
var result []CatalogWithDigest
265+
var result []CatalogSummary
275266
err = json.Unmarshal([]byte(output), &result)
276267
require.NoError(t, err)
277268
assert.Len(t, result, 1)
278269

279-
// Verify snapshot is included
280-
require.NotNil(t, result[0].Servers[0].Snapshot)
281-
assert.Equal(t, "test-server", result[0].Servers[0].Snapshot.Server.Name)
282-
assert.Equal(t, "Test description", result[0].Servers[0].Snapshot.Server.Description)
270+
// Verify only summary fields are present
271+
assert.Equal(t, "test/catalog7:latest", result[0].Ref)
272+
assert.Equal(t, "snapshot-catalog", result[0].Title)
273+
assert.NotEmpty(t, result[0].Digest)
283274
}
284275

285276
func TestListWithMultipleServers(t *testing.T) {
@@ -319,19 +310,15 @@ func TestListWithMultipleServers(t *testing.T) {
319310
require.NoError(t, err)
320311
})
321312

322-
var result []CatalogWithDigest
313+
var result []CatalogSummary
323314
err = json.Unmarshal([]byte(output), &result)
324315
require.NoError(t, err)
325316
assert.Len(t, result, 1)
326-
assert.Len(t, result[0].Servers, 3)
327317

328-
// Just verify that all three server types are present, order may vary
329-
types := make(map[workingset.ServerType]int)
330-
for _, s := range result[0].Servers {
331-
types[s.Type]++
332-
}
333-
assert.Equal(t, 2, types[workingset.ServerTypeImage])
334-
assert.Equal(t, 1, types[workingset.ServerTypeRegistry])
318+
// Verify only summary fields are present
319+
assert.Equal(t, "test/catalog8:latest", result[0].Ref)
320+
assert.Equal(t, "multi-server-catalog", result[0].Title)
321+
assert.NotEmpty(t, result[0].Digest)
335322
}
336323

337324
func TestListHumanReadableEmptyDoesNotShowInJSON(t *testing.T) {
@@ -344,7 +331,7 @@ func TestListHumanReadableEmptyDoesNotShowInJSON(t *testing.T) {
344331
require.NoError(t, err)
345332
})
346333

347-
var catalogs []CatalogWithDigest
334+
var catalogs []CatalogSummary
348335
err := json.Unmarshal([]byte(outputJSON), &catalogs)
349336
require.NoError(t, err)
350337
assert.Empty(t, catalogs)

0 commit comments

Comments
 (0)