Skip to content
Closed
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
}
77 changes: 72 additions & 5 deletions cmd/docker-mcp/commands/workingset.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ func workingsetServerCommand() *cobra.Command {
cmd.AddCommand(listServersCommand())
cmd.AddCommand(addServerCommand())
cmd.AddCommand(removeServerCommand())
cmd.AddCommand(updateServerCommand())

return cmd
}
Expand Down Expand Up @@ -410,29 +411,95 @@ func addServerCommand() *cobra.Command {

func removeServerCommand() *cobra.Command {
var names []string
var servers []string

cmd := &cobra.Command{
Use: "remove <profile-id> --name <name1> --name <name2> ...",
Use: "remove <profile-id> [--name <name1> ...] [--server <uri1> ...]",
Aliases: []string{"rm"},
Short: "Remove MCP servers from a profile",
Long: "Remove MCP servers from a profile by server name.",
Example: ` # Remove servers by name
Long: "Remove MCP servers from a profile by server name or server URI.",
Example: ` # Remove by name
docker mcp profile server remove dev-tools --name github --name slack

# Remove a single server
docker mcp profile server remove dev-tools --name github`,
# Remove by URI (same as used for add)
docker mcp profile server remove dev-tools --server catalog://mcp/docker-mcp-catalog/github+slack

# Remove by direct image reference
docker mcp profile server remove dev-tools --server docker://mcp/github:latest

# Mix multiple URIs
docker mcp profile server remove dev-tools --server catalog://mcp/docker-mcp-catalog/github --server docker://custom-server:latest`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// Validation: can't specify both
if len(names) > 0 && len(servers) > 0 {
return fmt.Errorf("cannot specify both --name and --server flags")
}
if len(names) == 0 && len(servers) == 0 {
return fmt.Errorf("must specify either --name or --server flag")
}

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

// If servers provided, resolve to names first
if len(servers) > 0 {
registryClient := registryapi.NewClient()
ociService := oci.NewService()
names, err = workingset.ResolveServerURIsToNames(cmd.Context(), dao, registryClient, ociService, servers)
if err != nil {
return fmt.Errorf("failed to resolve server URIs: %w\nHint: Use --name flag with server names from 'docker mcp profile show %s'", err, args[0])
}
}

return workingset.RemoveServers(cmd.Context(), dao, args[0], names)
},
}

flags := cmd.Flags()
flags.StringArrayVar(&names, "name", []string{}, "Server name to remove (can be specified multiple times)")
flags.StringArrayVar(&servers, "server", []string{}, "Server URI to remove - same format as add command (can be specified multiple times)")

return cmd
}

func updateServerCommand() *cobra.Command {
var addServers []string
var removeServers []string

cmd := &cobra.Command{
Use: "update <profile-id> [--add <uri1> --add <uri2> ...] [--remove <uri1> --remove <uri2> ...]",
Short: "Update servers in a profile (add and remove atomically)",
Long: "Atomically add and remove MCP servers in a single operation. Both operations are applied together or fail together.",
Example: ` # Add and remove servers in one atomic operation
docker mcp profile server update dev-tools --add catalog://mcp/docker-mcp-catalog/github --remove catalog://mcp/docker-mcp-catalog/slack

# Add multiple servers while removing others
docker mcp profile server update my-profile --add docker://server1:latest --add docker://server2:latest --remove docker://old-server:latest

# Mix different URI types
docker mcp profile server update dev-tools --add catalog://mcp/docker-mcp-catalog/github --add http://registry.modelcontextprotocol.io/v0/servers/71de5a2a-6cfb-4250-a196-f93080ecc860 --remove docker://old:latest`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(addServers) == 0 && len(removeServers) == 0 {
return fmt.Errorf("must specify at least one --add or --remove flag")
}

dao, err := db.New()
if err != nil {
return err
}
registryClient := registryapi.NewClient()
ociService := oci.NewService()
return workingset.UpdateServers(cmd.Context(), dao, registryClient, ociService, args[0], addServers, removeServers)
},
}

flags := cmd.Flags()
flags.StringArrayVar(&addServers, "add", []string{}, "Server URI to add: https:// (MCP Registry) or docker:// (Docker Image) or catalog:// (Catalog). Can be specified multiple times.")
flags.StringArrayVar(&removeServers, "remove", []string{}, "Server URI to remove - same format as add. Can be specified multiple times.")

return cmd
}
Expand Down
32 changes: 27 additions & 5 deletions docs/profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,26 +110,44 @@ docker mcp profile server add dev-tools \

### Removing Servers from a Profile

Remove servers from a profile by their server name:
Remove servers from a profile by their server name or by the same URI used to add them:

```bash
# Remove servers by name
# Remove by name
docker mcp profile server remove dev-tools \
--name github \
--name slack

# Remove a single server
docker mcp profile server remove dev-tools --name github
# Remove by URI (same format as used for add)
docker mcp profile server remove dev-tools \
--server catalog://mcp/docker-mcp-catalog/github+slack

# Remove by direct image reference
docker mcp profile server remove dev-tools \
--server docker://mcp/github:latest

# Mix multiple URIs
docker mcp profile server remove dev-tools \
--server catalog://mcp/docker-mcp-catalog/github \
--server docker://custom-server:latest

# Using alias
docker mcp profile server rm dev-tools --name github
```

**Server Names:**
**Removal Options:**
- Use `--name` flag to specify server names to remove (can be specified multiple times)
- Use `--server` flag to specify server URIs to remove - same format as the add command (can be specified multiple times)
- You must specify either `--name` or `--server`, but not both
- When using `--server`, the CLI will resolve the URI to find the matching server names and remove them
- Server names are determined by the server's snapshot (not the image name or source URL)
- Use `docker mcp profile show <profile-id>` to see available server names in a profile

**When to use each option:**
- Use `--name` when you know the server's name (faster, no resolution needed)
- Use `--server` for consistency with how you added the server
- Use `--server` with catalog URIs to remove multiple servers at once (e.g., `catalog://.../github+slack`)

### Listing Servers Across Profiles

View all servers grouped by profile, with filtering capabilities:
Expand Down Expand Up @@ -814,6 +832,10 @@ Error: server 'github' not found in profile
- Use `docker mcp profile show <profile-id> --format yaml` to see current servers in the profile
- Ensure you're using the correct server name in the snapshot (not the image name or source URL)
- Server names are case-sensitive
- Alternatively, use `--server` flag with the same URI you used to add the server:
```bash
docker mcp profile server remove <profile-id> --server docker://mcp/github:latest
```

### Invalid Tool Name Format

Expand Down
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
Loading
Loading