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
15 changes: 8 additions & 7 deletions cmd/docker-mcp/commands/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ import (

clientcli "github.com/docker/mcp-gateway/cmd/docker-mcp/client"
"github.com/docker/mcp-gateway/pkg/client"
"github.com/docker/mcp-gateway/pkg/features"
)

func clientCommand(dockerCli command.Cli, cwd string) *cobra.Command {
func clientCommand(dockerCli command.Cli, cwd string, features features.Features) *cobra.Command {
cfg := client.ReadConfig()
cmd := &cobra.Command{
Use: fmt.Sprintf("client (Supported: %s)", strings.Join(client.GetSupportedMCPClients(*cfg), ", ")),
Short: "Manage MCP clients",
}
cmd.AddCommand(listClientCommand(cwd, *cfg))
cmd.AddCommand(connectClientCommand(dockerCli, cwd, *cfg))
cmd.AddCommand(connectClientCommand(dockerCli, cwd, *cfg, features))
cmd.AddCommand(disconnectClientCommand(cwd, *cfg))
cmd.AddCommand(manualClientCommand(dockerCli))
cmd.AddCommand(manualClientCommand(features))
return cmd
}

Expand All @@ -46,7 +47,7 @@ func listClientCommand(cwd string, cfg client.Config) *cobra.Command {
return cmd
}

func connectClientCommand(dockerCli command.Cli, cwd string, cfg client.Config) *cobra.Command {
func connectClientCommand(dockerCli command.Cli, cwd string, cfg client.Config, features features.Features) *cobra.Command {
var opts struct {
Global bool
Quiet bool
Expand All @@ -63,7 +64,7 @@ func connectClientCommand(dockerCli command.Cli, cwd string, cfg client.Config)
flags := cmd.Flags()
addGlobalFlag(flags, &opts.Global)
addQuietFlag(flags, &opts.Quiet)
if isWorkingSetsFeatureEnabled(dockerCli) {
if features.IsProfilesFeatureEnabled() {
addWorkingSetFlag(flags, &opts.WorkingSet)
}
return cmd
Expand All @@ -88,7 +89,7 @@ func disconnectClientCommand(cwd string, cfg client.Config) *cobra.Command {
return cmd
}

func manualClientCommand(dockerCli command.Cli) *cobra.Command {
func manualClientCommand(features features.Features) *cobra.Command {
cmd := &cobra.Command{
Use: "manual-instructions",
Short: "Display the manual instructions to connect the MCP client",
Expand All @@ -100,7 +101,7 @@ func manualClientCommand(dockerCli command.Cli) *cobra.Command {
}

command := []string{"docker", "mcp", "gateway", "run"}
if isWorkingSetsFeatureEnabled(dockerCli) {
if features.IsProfilesFeatureEnabled() {
gordonProfile, err := client.ReadGordonProfile()
if err != nil {
return fmt.Errorf("failed to read gordon profile: %w", err)
Expand Down
46 changes: 30 additions & 16 deletions cmd/docker-mcp/commands/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/spf13/cobra"

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

// featureCommand creates the `feature` command and its subcommands
func featureCommand(dockerCli command.Cli) *cobra.Command {
func featureCommand(dockerCli command.Cli, features features.Features) *cobra.Command {
cmd := &cobra.Command{
Use: "feature",
Short: "Manage experimental features",
Expand All @@ -21,16 +23,16 @@ and control optional functionality that may change in future versions.`,
}

cmd.AddCommand(
featureEnableCommand(dockerCli),
featureDisableCommand(dockerCli),
featureListCommand(dockerCli),
featureEnableCommand(dockerCli, features),
featureDisableCommand(dockerCli, features),
featureListCommand(dockerCli, features),
)

return cmd
}

// featureEnableCommand creates the `feature enable` command
func featureEnableCommand(dockerCli command.Cli) *cobra.Command {
func featureEnableCommand(dockerCli command.Cli, features features.Features) *cobra.Command {
return &cobra.Command{
Use: "enable <feature-name>",
Short: "Enable an experimental feature",
Expand All @@ -40,15 +42,15 @@ Available features:
oauth-interceptor Enable GitHub OAuth flow interception for automatic authentication
mcp-oauth-dcr Enable Dynamic Client Registration (DCR) for automatic OAuth client setup
dynamic-tools Enable internal MCP management tools (mcp-find, mcp-add, mcp-remove)
profiles Enable profile management (docker mcp profile <subcommand>)
tool-name-prefix Prefix all tool names with server name to avoid conflicts`,
` + notDockerDesktop(features, `profiles Enable profile management (docker mcp profile <subcommand>)
`) + `tool-name-prefix Prefix all tool names with server name to avoid conflicts`,
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
featureName := args[0]

// Validate feature name
if !isKnownFeature(featureName) {
return fmt.Errorf("unknown feature: %s\n\nAvailable features:\n oauth-interceptor Enable GitHub OAuth flow interception\n mcp-oauth-dcr Enable Dynamic Client Registration for automatic OAuth setup\n dynamic-tools Enable internal MCP management tools\n profiles Enable profile management (docker mcp profile <subcommand>)\n tool-name-prefix Prefix all tool names with server name", featureName)
if !isKnownFeature(featureName, features) {
return fmt.Errorf("unknown feature: %s\n\nAvailable features:\n oauth-interceptor Enable GitHub OAuth flow interception\n mcp-oauth-dcr Enable Dynamic Client Registration for automatic OAuth setup\n dynamic-tools Enable internal MCP management tools\n"+notDockerDesktop(features, " profiles Enable profile management (docker mcp profile <subcommand>)\n")+" tool-name-prefix Prefix all tool names with server name", featureName)
}

// Enable the feature
Expand Down Expand Up @@ -105,7 +107,7 @@ Available features:
}

// featureDisableCommand creates the `feature disable` command
func featureDisableCommand(dockerCli command.Cli) *cobra.Command {
func featureDisableCommand(dockerCli command.Cli, features features.Features) *cobra.Command {
return &cobra.Command{
Use: "disable <feature-name>",
Short: "Disable an experimental feature",
Expand All @@ -115,7 +117,7 @@ func featureDisableCommand(dockerCli command.Cli) *cobra.Command {
featureName := args[0]

// Validate feature name
if !isKnownFeature(featureName) {
if !isKnownFeature(featureName, features) {
return fmt.Errorf("unknown feature: %s", featureName)
}

Expand All @@ -138,7 +140,7 @@ func featureDisableCommand(dockerCli command.Cli) *cobra.Command {
}

// featureListCommand creates the `feature list` command
func featureListCommand(dockerCli command.Cli) *cobra.Command {
func featureListCommand(dockerCli command.Cli, features features.Features) *cobra.Command {
return &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Expand All @@ -151,7 +153,10 @@ func featureListCommand(dockerCli command.Cli) *cobra.Command {
fmt.Println()

// Show all known features
knownFeatures := []string{"oauth-interceptor", "mcp-oauth-dcr", "dynamic-tools", "profiles", "tool-name-prefix"}
knownFeatures := []string{"oauth-interceptor", "mcp-oauth-dcr", "dynamic-tools", "tool-name-prefix"}
if !features.IsRunningInDockerDesktop() {
knownFeatures = append(knownFeatures, "profiles")
}
for _, feature := range knownFeatures {
status := "disabled"
if isFeatureEnabledFromCli(dockerCli, feature) {
Expand Down Expand Up @@ -180,7 +185,7 @@ func featureListCommand(dockerCli command.Cli) *cobra.Command {
if configFile.Features != nil {
unknownFeatures := make([]string, 0)
for feature := range configFile.Features {
if !isKnownFeature(feature) {
if !isKnownFeature(feature, features) {
unknownFeatures = append(unknownFeatures, feature)
}
}
Expand Down Expand Up @@ -235,14 +240,16 @@ func isFeatureEnabledFromConfig(configFile *configfile.ConfigFile, feature strin
}

// isKnownFeature checks if the feature name is valid
func isKnownFeature(feature string) bool {
func isKnownFeature(feature string, features features.Features) bool {
knownFeatures := []string{
"oauth-interceptor",
"mcp-oauth-dcr",
"dynamic-tools",
"profiles",
"tool-name-prefix",
}
if !features.IsRunningInDockerDesktop() {
knownFeatures = append(knownFeatures, "profiles")
}

for _, known := range knownFeatures {
if feature == known {
Expand All @@ -251,3 +258,10 @@ func isKnownFeature(feature string) bool {
}
return false
}

func notDockerDesktop(features features.Features, msg string) string {
if features.IsRunningInDockerDesktop() {
return ""
}
return msg
}
42 changes: 36 additions & 6 deletions cmd/docker-mcp/commands/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (

"github.com/docker/cli/cli/config/configfile"
"github.com/stretchr/testify/assert"

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

func TestIsFeatureEnabledOAuthInterceptor(t *testing.T) {
Expand Down Expand Up @@ -115,12 +117,40 @@ func TestIsFeatureEnabledMcpOAuthDcr(t *testing.T) {

func TestIsKnownFeature(t *testing.T) {
// Test valid features
assert.True(t, isKnownFeature("oauth-interceptor"))
assert.True(t, isKnownFeature("mcp-oauth-dcr"))
assert.True(t, isKnownFeature("dynamic-tools"))
assert.True(t, isKnownFeature("oauth-interceptor", &mockFeatures{}))
assert.True(t, isKnownFeature("mcp-oauth-dcr", &mockFeatures{}))
assert.True(t, isKnownFeature("dynamic-tools", &mockFeatures{}))

// Test invalid features
assert.False(t, isKnownFeature("invalid-feature"))
assert.False(t, isKnownFeature("configured-catalogs")) // No longer supported
assert.False(t, isKnownFeature(""))
assert.False(t, isKnownFeature("invalid-feature", &mockFeatures{}))
assert.False(t, isKnownFeature("configured-catalogs", &mockFeatures{})) // No longer supported
assert.False(t, isKnownFeature("", &mockFeatures{}))

// Test profiles feature - unknown in Docker Desktop, known in CE
assert.True(t, isKnownFeature("profiles", &mockFeatures{
runningDockerDesktop: false,
}))
assert.False(t, isKnownFeature("profiles", &mockFeatures{
runningDockerDesktop: true,
}))
}

type mockFeatures struct {
initErr error
runningDockerDesktop bool
profilesEnabled bool
}

var _ features.Features = &mockFeatures{}

func (m *mockFeatures) InitError() error {
return m.initErr
}

func (m *mockFeatures) IsRunningInDockerDesktop() bool {
return m.runningDockerDesktop
}

func (m *mockFeatures) IsProfilesFeatureEnabled() bool {
return m.profilesEnabled
}
23 changes: 5 additions & 18 deletions cmd/docker-mcp/commands/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import (
"github.com/docker/mcp-gateway/cmd/docker-mcp/catalog"
catalogTypes "github.com/docker/mcp-gateway/pkg/catalog"
"github.com/docker/mcp-gateway/pkg/docker"
"github.com/docker/mcp-gateway/pkg/features"
"github.com/docker/mcp-gateway/pkg/gateway"
)

func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command {
func gatewayCommand(docker docker.Client, dockerCli command.Cli, features features.Features) *cobra.Command {
cmd := &cobra.Command{
Use: "gateway",
Short: "Manage the MCP Server gateway",
Expand Down Expand Up @@ -57,7 +58,7 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
},
}
}
if !isWorkingSetsFeatureEnabled(dockerCli) {
if !features.IsProfilesFeatureEnabled() {
// Default these only if we aren't defaulting to profiles
setLegacyDefaults(&options)
}
Expand All @@ -67,7 +68,7 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
Short: "Run the gateway",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
if isWorkingSetsFeatureEnabled(dockerCli) {
if features.IsProfilesFeatureEnabled() {
if len(options.ServerNames) > 0 || enableAllServers ||
len(options.CatalogPath) > 0 || len(options.RegistryPath) > 0 || len(options.ConfigPath) > 0 || len(options.ToolsPath) > 0 ||
len(additionalCatalogs) > 0 || len(additionalRegistries) > 0 || len(additionalConfigs) > 0 || len(additionalToolsConfig) > 0 ||
Expand Down Expand Up @@ -178,7 +179,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)")
if isWorkingSetsFeatureEnabled(dockerCli) {
if features.IsProfilesFeatureEnabled() {
runCmd.Flags().StringVar(&options.WorkingSet, "profile", "", "Profile ID 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)")
Expand Down Expand Up @@ -355,20 +356,6 @@ func isToolNamePrefixFeatureEnabled(dockerCli command.Cli) bool {
return value == "enabled"
}

// isWorkingSetsFeatureEnabled checks if the profiles feature is enabled
func isWorkingSetsFeatureEnabled(dockerCli command.Cli) bool {
configFile := dockerCli.ConfigFile()
if configFile == nil || configFile.Features == nil {
return false
}

value, exists := configFile.Features["profiles"]
if !exists {
return false
}
return value == "enabled"
}

func setLegacyDefaults(options *gateway.Config) {
if os.Getenv("DOCKER_MCP_IN_CONTAINER") == "1" {
if len(options.CatalogPath) == 0 {
Expand Down
18 changes: 13 additions & 5 deletions cmd/docker-mcp/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/docker/mcp-gateway/pkg/db"
"github.com/docker/mcp-gateway/pkg/desktop"
"github.com/docker/mcp-gateway/pkg/docker"
"github.com/docker/mcp-gateway/pkg/features"
"github.com/docker/mcp-gateway/pkg/migrate"
)

Expand All @@ -36,6 +37,8 @@ Examples:
func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command {
dockerClient := docker.NewClient(dockerCli)

features := features.New(ctx, dockerCli)

cmd := &cobra.Command{
Use: "mcp [OPTIONS]",
Short: "Manage MCP servers and clients",
Expand All @@ -50,8 +53,13 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command
return err
}

// Check the feature initialization error here for clearer error messages for the user
if features.InitError() != nil {
return features.InitError()
}

if os.Getenv("DOCKER_MCP_IN_CONTAINER") != "1" {
if isWorkingSetsFeatureEnabled(dockerCli) {
if features.IsProfilesFeatureEnabled() {
if isSubcommandOf(cmd, []string{"catalog-next", "catalog", "profile"}) {
dao, err := db.New()
if err != nil {
Expand Down Expand Up @@ -84,15 +92,15 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command
return []string{"--help"}, cobra.ShellCompDirectiveNoFileComp
})

if isWorkingSetsFeatureEnabled(dockerCli) {
if features.IsProfilesFeatureEnabled() {
cmd.AddCommand(workingSetCommand())
cmd.AddCommand(catalogNextCommand())
}
cmd.AddCommand(catalogCommand(dockerCli))
cmd.AddCommand(clientCommand(dockerCli, cwd))
cmd.AddCommand(clientCommand(dockerCli, cwd, features))
cmd.AddCommand(configCommand(dockerClient))
cmd.AddCommand(featureCommand(dockerCli))
cmd.AddCommand(gatewayCommand(dockerClient, dockerCli))
cmd.AddCommand(featureCommand(dockerCli, features))
cmd.AddCommand(gatewayCommand(dockerClient, dockerCli, features))
cmd.AddCommand(oauthCommand())
cmd.AddCommand(policyCommand())
cmd.AddCommand(registryCommand())
Expand Down
Loading
Loading