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
24 changes: 24 additions & 0 deletions cmd/docker-mcp/commands/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/spf13/cobra"

"github.com/docker/mcp-gateway/cmd/docker-mcp/catalog"
workingset "github.com/docker/mcp-gateway/cmd/docker-mcp/working-set"
catalogTypes "github.com/docker/mcp-gateway/pkg/catalog"
"github.com/docker/mcp-gateway/pkg/docker"
"github.com/docker/mcp-gateway/pkg/gateway"
Expand All @@ -29,6 +30,7 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
var additionalToolsConfig []string
var mcpRegistryUrls []string
var enableAllServers bool
var workingSetName string
if os.Getenv("DOCKER_MCP_IN_CONTAINER") == "1" {
// In-container.
// Note: The catalog URL will be updated after checking the feature flag in RunE
Expand Down Expand Up @@ -125,6 +127,27 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
options.MCPRegistryServers = mcpServers
}

// Handle --working-set flag
if workingSetName != "" {
if len(options.ServerNames) > 0 {
return fmt.Errorf("cannot use --working-set with --servers flag")
}
if enableAllServers {
return fmt.Errorf("cannot use --working-set with --enable-all-servers flag")
}

// Read working-set file
ws, err := workingset.ReadWorkingSetFile(workingSetName)
if err != nil {
return fmt.Errorf("failed to read working-set %q: %w", workingSetName, err)
}

// Set server names from the working-set
// These are treated as server references (can be OCI image refs or catalog names)
// and will go through the self-contained catalog resolution
options.ServerNames = ws.Servers
}

// Handle --enable-all-servers flag
if enableAllServers {
if len(options.ServerNames) > 0 {
Expand All @@ -150,6 +173,7 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
}

runCmd.Flags().StringSliceVar(&options.ServerNames, "servers", nil, "Names of the servers to enable (if non empty, ignore --registry flag)")
runCmd.Flags().StringVar(&workingSetName, "working-set", "", "Name of the working-set to use (mutually exclusive with --servers and --enable-all-servers)")
runCmd.Flags().BoolVar(&enableAllServers, "enable-all-servers", false, "Enable all servers in the catalog (instead of using individual --servers options)")
runCmd.Flags().StringSliceVar(&options.CatalogPath, "catalog", options.CatalogPath, "Paths to docker catalogs (absolute or relative to ~/.docker/mcp/catalogs/)")
runCmd.Flags().StringSliceVar(&additionalCatalogs, "additional-catalog", nil, "Additional catalog paths to append to the default catalogs")
Expand Down
1 change: 1 addition & 0 deletions cmd/docker-mcp/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command
cmd.AddCommand(serverCommand(dockerClient, dockerCli))
cmd.AddCommand(toolsCommand(dockerClient))
cmd.AddCommand(versionCommand())
cmd.AddCommand(workingSetCommand())

if os.Getenv("DOCKER_MCP_SHOW_HIDDEN") == "1" {
unhideHiddenCommands(cmd)
Expand Down
112 changes: 112 additions & 0 deletions cmd/docker-mcp/commands/working-set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package commands

import (
"fmt"

"github.com/spf13/cobra"

workingset "github.com/docker/mcp-gateway/cmd/docker-mcp/working-set"
)

func workingSetCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "working-set",
Aliases: []string{"ws"},
Short: "Manage MCP server working-sets",
Long: `Manage working-sets of MCP servers for organizing and grouping servers together.`,
}
cmd.AddCommand(createWorkingSetCommand())
cmd.AddCommand(listWorkingSetCommand())
cmd.AddCommand(showWorkingSetCommand())
return cmd
}

func createWorkingSetCommand() *cobra.Command {
var opts struct {
Name string
Description string
Servers []string
}
cmd := &cobra.Command{
Use: "create --name <name> [--description <description>] --server <ref1> --server <ref2> ...",
Short: "Create a new working-set of MCP servers",
Long: `Create a new working-set that groups multiple MCP servers together.
A working-set allows you to organize and manage related servers as a single unit.
Working-sets are decoupled from catalogs. Servers can be:
- Catalog server names (e.g., "github", "docker")
- OCI image references with docker:// prefix (e.g., "docker://mcp/github:latest")`,
Example: ` # Create a working-set with multiple servers (OCI references)
docker mcp working-set create --name dev-tools --description "Development tools" --server docker://mcp/github:latest --server docker://mcp/slack:latest

# Create a working-set with catalog server names
docker mcp working-set create --name catalog-servers --server github --server docker

# Mix catalog names and OCI references
docker mcp working-set create --name mixed --server github --server docker://custom/server:v1`,
RunE: func(cmd *cobra.Command, args []string) error {
return workingset.Create(opts.Name, opts.Description, opts.Servers)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.Name, "name", "", "Name of the working-set (required)")
flags.StringVar(&opts.Description, "description", "", "Description of the working-set")
flags.StringArrayVar(&opts.Servers, "server", []string{}, "Server to include: catalog name or OCI reference with docker:// prefix (can be specified multiple times)")

_ = cmd.MarkFlagRequired("name")
_ = cmd.MarkFlagRequired("server")

return cmd
}

func listWorkingSetCommand() *cobra.Command {
var opts struct {
Format workingset.Format
}
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List all working-sets",
Long: `List all configured working-sets and their associated servers.`,
Example: ` # List all working-sets in human-readable format
docker mcp working-set ls

# List working-sets in JSON format
docker mcp working-set ls --format json

# List working-sets in YAML format
docker mcp working-set ls --format yaml`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return workingset.List(opts.Format)
},
}
flags := cmd.Flags()
flags.Var(&opts.Format, "format", fmt.Sprintf("Output format. Supported: %s.", workingset.SupportedFormats()))
return cmd
}

func showWorkingSetCommand() *cobra.Command {
var opts struct {
Format workingset.Format
}
cmd := &cobra.Command{
Use: "show <name>",
Short: "Display working-set details",
Long: `Display the details of a specific working-set including all its servers.`,
Example: ` # Show a working-set in human-readable format
docker mcp working-set show my-dev-tools

# Show a working-set in JSON format
docker mcp working-set show my-dev-tools --format json

# Show a working-set in YAML format
docker mcp working-set show my-dev-tools --format yaml`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return workingset.Show(args[0], opts.Format)
},
}
flags := cmd.Flags()
flags.Var(&opts.Format, "format", fmt.Sprintf("Output format. Supported: %s.", workingset.SupportedFormats()))
return cmd
}
108 changes: 108 additions & 0 deletions cmd/docker-mcp/working-set/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package workingset

import (
"encoding/json"
"os"
"path/filepath"

"github.com/docker/mcp-gateway/pkg/config"
)

type Config struct {
WorkingSets map[string]WorkingSetMetadata `json:"workingSets"`
}

type WorkingSetMetadata struct {
DisplayName string `json:"displayName"`
}

type WorkingSet struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Servers []string `json:"servers"`
}

func ReadConfig() (*Config, error) {
buf, err := config.ReadWorkingSetConfig()
if err != nil {
return nil, err
}

var result Config
if len(buf) > 0 {
if err := json.Unmarshal(buf, &result); err != nil {
return nil, err
}
}

if result.WorkingSets == nil {
result.WorkingSets = map[string]WorkingSetMetadata{}
}

return &result, nil
}

func WriteConfig(cfg *Config) error {
if cfg.WorkingSets == nil {
cfg.WorkingSets = map[string]WorkingSetMetadata{}
}

data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}

return config.WriteWorkingSetConfig(data)
}

func ReadWorkingSetFile(name string) (*WorkingSet, error) {
buf, err := config.ReadWorkingSetFile(name)
if err != nil {
return nil, err
}

if len(buf) == 0 {
return nil, os.ErrNotExist
}

var ws WorkingSet
if err := json.Unmarshal(buf, &ws); err != nil {
return nil, err
}

return &ws, nil
}

func WriteWorkingSetFile(name string, ws *WorkingSet) error {
data, err := json.MarshalIndent(ws, "", " ")
if err != nil {
return err
}

return config.WriteWorkingSetFile(name, data)
}

func ListWorkingSets() (map[string]WorkingSet, error) {
cfg, err := ReadConfig()
if err != nil {
return nil, err
}

workingSets := make(map[string]WorkingSet)
for name := range cfg.WorkingSets {
ws, err := ReadWorkingSetFile(name)
if err != nil {
if os.IsNotExist(err) {
continue // Skip missing files
}
return nil, err
}
workingSets[name] = *ws
}

return workingSets, nil
}

func WorkingSetFilePath(name string) (string, error) {
return config.FilePath(filepath.Join("working-sets", name+".json"))
}
56 changes: 56 additions & 0 deletions cmd/docker-mcp/working-set/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package workingset

import (
"fmt"
)

func Create(name, description string, servers []string) error {
// Validate input
if name == "" {
return fmt.Errorf("working-set name cannot be empty")
}

if len(servers) == 0 {
return fmt.Errorf("at least one server must be specified")
}

// Read existing config
cfg, err := ReadConfig()
if err != nil {
return fmt.Errorf("failed to read working-sets config: %w", err)
}

// Check if working-set already exists
if _, exists := cfg.WorkingSets[name]; exists {
return fmt.Errorf("working-set %q already exists", name)
}

// Create working-set object
ws := &WorkingSet{
Name: name,
Description: description,
Servers: servers,
}

// Write working-set file
if err := WriteWorkingSetFile(name, ws); err != nil {
return fmt.Errorf("failed to write working-set file: %w", err)
}

// Update config with metadata
displayName := name
if description != "" {
displayName = description
}
cfg.WorkingSets[name] = WorkingSetMetadata{
DisplayName: displayName,
}

// Write config
if err := WriteConfig(cfg); err != nil {
return fmt.Errorf("failed to write working-sets config: %w", err)
}

fmt.Printf("Created working-set %q with %d server(s)\n", name, len(servers))
return nil
}
Loading
Loading