Skip to content
Open
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
64 changes: 63 additions & 1 deletion cmd/docker-mcp/commands/catalog_next.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func catalogNextCommand() *cobra.Command {
cmd.AddCommand(pushCatalogNextCommand())
cmd.AddCommand(pullCatalogNextCommand())
cmd.AddCommand(tagCatalogNextCommand())
cmd.AddCommand(catalogNextServerCommand())

return cmd
}
Expand Down Expand Up @@ -83,6 +84,7 @@ func tagCatalogNextCommand() *cobra.Command {
func showCatalogNextCommand() *cobra.Command {
format := string(workingset.OutputFormatHumanReadable)
pullOption := string(catalognext.PullOptionNever)
var noTools bool

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

flags := cmd.Flags()
flags.StringVar(&format, "format", string(workingset.OutputFormatHumanReadable), fmt.Sprintf("Supported: %s.", strings.Join(workingset.SupportedFormats(), ", ")))
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(), ", ")))
flags.BoolVar(&noTools, "no-tools", false, "Exclude tools from output")
return cmd
}

Expand Down Expand Up @@ -181,3 +184,62 @@ func pullCatalogNextCommand() *cobra.Command {
},
}
}

func catalogNextServerCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "server",
Short: "Manage servers in catalogs",
}

cmd.AddCommand(listCatalogNextServersCommand())

return cmd
}

func listCatalogNextServersCommand() *cobra.Command {
var opts struct {
Filters []string
Format string
}

cmd := &cobra.Command{
Use: "ls <oci-reference>",
Aliases: []string{"list"},
Short: "List servers in a catalog",
Long: `List all servers in a catalog.

Use --filter to search for servers matching a query (case-insensitive substring matching on server names).
Filters use key=value format (e.g., name=github).`,
Example: ` # List all servers in a catalog
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest

# Filter servers by name
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest --filter name=github

# Combine multiple filters (using short flag)
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest -f name=slack -f name=github

# Output in JSON format
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest --format json`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
supported := slices.Contains(workingset.SupportedFormats(), opts.Format)
if !supported {
return fmt.Errorf("unsupported format: %s", opts.Format)
}

dao, err := db.New()
if err != nil {
return err
}

return catalognext.ListServers(cmd.Context(), dao, args[0], opts.Filters, workingset.OutputFormat(opts.Format))
},
}

flags := cmd.Flags()
flags.StringArrayVarP(&opts.Filters, "filter", "f", []string{}, "Filter output (e.g., name=github)")
flags.StringVar(&opts.Format, "format", string(workingset.OutputFormatHumanReadable), fmt.Sprintf("Supported: %s.", strings.Join(workingset.SupportedFormats(), ", ")))

return cmd
}
6 changes: 6 additions & 0 deletions pkg/catalog_next/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ type CatalogWithDigest struct {
Digest string `yaml:"digest" json:"digest"`
}

type CatalogSummary struct {
Ref string `yaml:"ref" json:"ref"`
Digest string `yaml:"digest" json:"digest"`
Title string `yaml:"title" json:"title"`
}

// Source prefixes must be of the form "<prefix>:"
const (
SourcePrefixWorkingSet = "profile:"
Expand Down
16 changes: 10 additions & 6 deletions pkg/catalog_next/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,23 @@ func List(ctx context.Context, dao db.DAO, format workingset.OutputFormat) error
return nil
}

catalogs := make([]CatalogWithDigest, len(dbCatalogs))
summaries := make([]CatalogSummary, len(dbCatalogs))
for i, dbCatalog := range dbCatalogs {
catalogs[i] = NewFromDb(&dbCatalog)
summaries[i] = CatalogSummary{
Ref: dbCatalog.Ref,
Digest: dbCatalog.Digest,
Title: dbCatalog.Title,
}
}

var data []byte
switch format {
case workingset.OutputFormatHumanReadable:
data = []byte(printListHumanReadable(catalogs))
data = []byte(printListHumanReadable(summaries))
case workingset.OutputFormatJSON:
data, err = json.MarshalIndent(catalogs, "", " ")
data, err = json.MarshalIndent(summaries, "", " ")
case workingset.OutputFormatYAML:
data, err = yaml.Marshal(catalogs)
data, err = yaml.Marshal(summaries)
}
if err != nil {
return fmt.Errorf("failed to marshal catalogs: %w", err)
Expand All @@ -46,7 +50,7 @@ func List(ctx context.Context, dao db.DAO, format workingset.OutputFormat) error
return nil
}

func printListHumanReadable(catalogs []CatalogWithDigest) string {
func printListHumanReadable(catalogs []CatalogSummary) string {
lines := ""
for _, catalog := range catalogs {
lines += fmt.Sprintf("%s\t| %s\t| %s\n", catalog.Ref, catalog.Digest, catalog.Title)
Expand Down
61 changes: 24 additions & 37 deletions pkg/catalog_next/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,26 +138,20 @@ func TestListJSON(t *testing.T) {
})

// Parse JSON output
var catalogs []CatalogWithDigest
var catalogs []CatalogSummary
err = json.Unmarshal([]byte(output), &catalogs)
require.NoError(t, err)
assert.Len(t, catalogs, 2)

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

// Verify second catalog
// Verify second catalog (summary only)
assert.Equal(t, "test/catalog5:latest", catalogs[1].Ref)
assert.Equal(t, "catalog-two", catalogs[1].Title)
assert.Equal(t, "source-2", catalogs[1].Source)
assert.Len(t, catalogs[1].Servers, 1)
assert.Equal(t, workingset.ServerTypeRegistry, catalogs[1].Servers[0].Type)
assert.Equal(t, "https://example.com", catalogs[1].Servers[0].Source)
assert.Equal(t, []string{"tool2", "tool3"}, catalogs[1].Servers[0].Tools)
assert.NotEmpty(t, catalogs[1].Digest)
}

func TestListJSONEmpty(t *testing.T) {
Expand All @@ -170,7 +164,7 @@ func TestListJSONEmpty(t *testing.T) {
})

// Parse JSON output
var catalogs []CatalogWithDigest
var catalogs []CatalogSummary
err := json.Unmarshal([]byte(output), &catalogs)
require.NoError(t, err)
assert.Empty(t, catalogs)
Expand Down Expand Up @@ -206,18 +200,15 @@ func TestListYAML(t *testing.T) {
})

// Parse YAML output
var catalogs []CatalogWithDigest
var catalogs []CatalogSummary
err = yaml.Unmarshal([]byte(output), &catalogs)
require.NoError(t, err)
assert.Len(t, catalogs, 1)

// Verify catalog
// Verify catalog (summary only)
assert.Equal(t, "test/catalog6:latest", catalogs[0].Ref)
assert.Equal(t, "catalog-yaml", catalogs[0].Title)
assert.Equal(t, "yaml-source", catalogs[0].Source)
assert.Len(t, catalogs[0].Servers, 1)
assert.Equal(t, workingset.ServerTypeImage, catalogs[0].Servers[0].Type)
assert.Equal(t, "test:yaml", catalogs[0].Servers[0].Image)
assert.Equal(t, []string{"tool1", "tool2"}, catalogs[0].Servers[0].Tools)
assert.NotEmpty(t, catalogs[0].Digest)
}

func TestListYAMLEmpty(t *testing.T) {
Expand All @@ -230,7 +221,7 @@ func TestListYAMLEmpty(t *testing.T) {
})

// Parse YAML output
var catalogs []CatalogWithDigest
var catalogs []CatalogSummary
err := yaml.Unmarshal([]byte(output), &catalogs)
require.NoError(t, err)
assert.Empty(t, catalogs)
Expand Down Expand Up @@ -271,15 +262,15 @@ func TestListWithSnapshot(t *testing.T) {
require.NoError(t, err)
})

var result []CatalogWithDigest
var result []CatalogSummary
err = json.Unmarshal([]byte(output), &result)
require.NoError(t, err)
assert.Len(t, result, 1)

// Verify snapshot is included
require.NotNil(t, result[0].Servers[0].Snapshot)
assert.Equal(t, "test-server", result[0].Servers[0].Snapshot.Server.Name)
assert.Equal(t, "Test description", result[0].Servers[0].Snapshot.Server.Description)
// Verify only summary fields are present
assert.Equal(t, "test/catalog7:latest", result[0].Ref)
assert.Equal(t, "snapshot-catalog", result[0].Title)
assert.NotEmpty(t, result[0].Digest)
}

func TestListWithMultipleServers(t *testing.T) {
Expand Down Expand Up @@ -319,19 +310,15 @@ func TestListWithMultipleServers(t *testing.T) {
require.NoError(t, err)
})

var result []CatalogWithDigest
var result []CatalogSummary
err = json.Unmarshal([]byte(output), &result)
require.NoError(t, err)
assert.Len(t, result, 1)
assert.Len(t, result[0].Servers, 3)

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

func TestListHumanReadableEmptyDoesNotShowInJSON(t *testing.T) {
Expand All @@ -344,7 +331,7 @@ func TestListHumanReadableEmptyDoesNotShowInJSON(t *testing.T) {
require.NoError(t, err)
})

var catalogs []CatalogWithDigest
var catalogs []CatalogSummary
err := json.Unmarshal([]byte(outputJSON), &catalogs)
require.NoError(t, err)
assert.Empty(t, catalogs)
Expand Down
Loading
Loading