Skip to content

Commit 8e3f3b9

Browse files
authored
Use Docker Desktop FF for the profiles feature flag (#280)
* Use Docker Desktop FF for profiles. * Fix docs.
1 parent cd5ab1c commit 8e3f3b9

File tree

10 files changed

+234
-57
lines changed

10 files changed

+234
-57
lines changed

cmd/docker-mcp/commands/client.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@ import (
1212

1313
clientcli "github.com/docker/mcp-gateway/cmd/docker-mcp/client"
1414
"github.com/docker/mcp-gateway/pkg/client"
15+
"github.com/docker/mcp-gateway/pkg/features"
1516
)
1617

17-
func clientCommand(dockerCli command.Cli, cwd string) *cobra.Command {
18+
func clientCommand(dockerCli command.Cli, cwd string, features features.Features) *cobra.Command {
1819
cfg := client.ReadConfig()
1920
cmd := &cobra.Command{
2021
Use: fmt.Sprintf("client (Supported: %s)", strings.Join(client.GetSupportedMCPClients(*cfg), ", ")),
2122
Short: "Manage MCP clients",
2223
}
2324
cmd.AddCommand(listClientCommand(cwd, *cfg))
24-
cmd.AddCommand(connectClientCommand(dockerCli, cwd, *cfg))
25+
cmd.AddCommand(connectClientCommand(dockerCli, cwd, *cfg, features))
2526
cmd.AddCommand(disconnectClientCommand(cwd, *cfg))
26-
cmd.AddCommand(manualClientCommand(dockerCli))
27+
cmd.AddCommand(manualClientCommand(features))
2728
return cmd
2829
}
2930

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

49-
func connectClientCommand(dockerCli command.Cli, cwd string, cfg client.Config) *cobra.Command {
50+
func connectClientCommand(dockerCli command.Cli, cwd string, cfg client.Config, features features.Features) *cobra.Command {
5051
var opts struct {
5152
Global bool
5253
Quiet bool
@@ -63,7 +64,7 @@ func connectClientCommand(dockerCli command.Cli, cwd string, cfg client.Config)
6364
flags := cmd.Flags()
6465
addGlobalFlag(flags, &opts.Global)
6566
addQuietFlag(flags, &opts.Quiet)
66-
if isWorkingSetsFeatureEnabled(dockerCli) {
67+
if features.IsProfilesFeatureEnabled() {
6768
addWorkingSetFlag(flags, &opts.WorkingSet)
6869
}
6970
return cmd
@@ -88,7 +89,7 @@ func disconnectClientCommand(cwd string, cfg client.Config) *cobra.Command {
8889
return cmd
8990
}
9091

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

102103
command := []string{"docker", "mcp", "gateway", "run"}
103-
if isWorkingSetsFeatureEnabled(dockerCli) {
104+
if features.IsProfilesFeatureEnabled() {
104105
gordonProfile, err := client.ReadGordonProfile()
105106
if err != nil {
106107
return fmt.Errorf("failed to read gordon profile: %w", err)

cmd/docker-mcp/commands/feature.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import (
77
"github.com/docker/cli/cli/command"
88
"github.com/docker/cli/cli/config/configfile"
99
"github.com/spf13/cobra"
10+
11+
"github.com/docker/mcp-gateway/pkg/features"
1012
)
1113

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

2325
cmd.AddCommand(
24-
featureEnableCommand(dockerCli),
25-
featureDisableCommand(dockerCli),
26-
featureListCommand(dockerCli),
26+
featureEnableCommand(dockerCli, features),
27+
featureDisableCommand(dockerCli, features),
28+
featureListCommand(dockerCli, features),
2729
)
2830

2931
return cmd
3032
}
3133

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

4951
// Validate feature name
50-
if !isKnownFeature(featureName) {
51-
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)
52+
if !isKnownFeature(featureName, features) {
53+
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)
5254
}
5355

5456
// Enable the feature
@@ -105,7 +107,7 @@ Available features:
105107
}
106108

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

117119
// Validate feature name
118-
if !isKnownFeature(featureName) {
120+
if !isKnownFeature(featureName, features) {
119121
return fmt.Errorf("unknown feature: %s", featureName)
120122
}
121123

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

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

153155
// Show all known features
154-
knownFeatures := []string{"oauth-interceptor", "mcp-oauth-dcr", "dynamic-tools", "profiles", "tool-name-prefix"}
156+
knownFeatures := []string{"oauth-interceptor", "mcp-oauth-dcr", "dynamic-tools", "tool-name-prefix"}
157+
if !features.IsRunningInDockerDesktop() {
158+
knownFeatures = append(knownFeatures, "profiles")
159+
}
155160
for _, feature := range knownFeatures {
156161
status := "disabled"
157162
if isFeatureEnabledFromCli(dockerCli, feature) {
@@ -180,7 +185,7 @@ func featureListCommand(dockerCli command.Cli) *cobra.Command {
180185
if configFile.Features != nil {
181186
unknownFeatures := make([]string, 0)
182187
for feature := range configFile.Features {
183-
if !isKnownFeature(feature) {
188+
if !isKnownFeature(feature, features) {
184189
unknownFeatures = append(unknownFeatures, feature)
185190
}
186191
}
@@ -235,14 +240,16 @@ func isFeatureEnabledFromConfig(configFile *configfile.ConfigFile, feature strin
235240
}
236241

237242
// isKnownFeature checks if the feature name is valid
238-
func isKnownFeature(feature string) bool {
243+
func isKnownFeature(feature string, features features.Features) bool {
239244
knownFeatures := []string{
240245
"oauth-interceptor",
241246
"mcp-oauth-dcr",
242247
"dynamic-tools",
243-
"profiles",
244248
"tool-name-prefix",
245249
}
250+
if !features.IsRunningInDockerDesktop() {
251+
knownFeatures = append(knownFeatures, "profiles")
252+
}
246253

247254
for _, known := range knownFeatures {
248255
if feature == known {
@@ -251,3 +258,10 @@ func isKnownFeature(feature string) bool {
251258
}
252259
return false
253260
}
261+
262+
func notDockerDesktop(features features.Features, msg string) string {
263+
if features.IsRunningInDockerDesktop() {
264+
return ""
265+
}
266+
return msg
267+
}

cmd/docker-mcp/commands/feature_test.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55

66
"github.com/docker/cli/cli/config/configfile"
77
"github.com/stretchr/testify/assert"
8+
9+
"github.com/docker/mcp-gateway/pkg/features"
810
)
911

1012
func TestIsFeatureEnabledOAuthInterceptor(t *testing.T) {
@@ -115,12 +117,40 @@ func TestIsFeatureEnabledMcpOAuthDcr(t *testing.T) {
115117

116118
func TestIsKnownFeature(t *testing.T) {
117119
// Test valid features
118-
assert.True(t, isKnownFeature("oauth-interceptor"))
119-
assert.True(t, isKnownFeature("mcp-oauth-dcr"))
120-
assert.True(t, isKnownFeature("dynamic-tools"))
120+
assert.True(t, isKnownFeature("oauth-interceptor", &mockFeatures{}))
121+
assert.True(t, isKnownFeature("mcp-oauth-dcr", &mockFeatures{}))
122+
assert.True(t, isKnownFeature("dynamic-tools", &mockFeatures{}))
121123

122124
// Test invalid features
123-
assert.False(t, isKnownFeature("invalid-feature"))
124-
assert.False(t, isKnownFeature("configured-catalogs")) // No longer supported
125-
assert.False(t, isKnownFeature(""))
125+
assert.False(t, isKnownFeature("invalid-feature", &mockFeatures{}))
126+
assert.False(t, isKnownFeature("configured-catalogs", &mockFeatures{})) // No longer supported
127+
assert.False(t, isKnownFeature("", &mockFeatures{}))
128+
129+
// Test profiles feature - unknown in Docker Desktop, known in CE
130+
assert.True(t, isKnownFeature("profiles", &mockFeatures{
131+
runningDockerDesktop: false,
132+
}))
133+
assert.False(t, isKnownFeature("profiles", &mockFeatures{
134+
runningDockerDesktop: true,
135+
}))
136+
}
137+
138+
type mockFeatures struct {
139+
initErr error
140+
runningDockerDesktop bool
141+
profilesEnabled bool
142+
}
143+
144+
var _ features.Features = &mockFeatures{}
145+
146+
func (m *mockFeatures) InitError() error {
147+
return m.initErr
148+
}
149+
150+
func (m *mockFeatures) IsRunningInDockerDesktop() bool {
151+
return m.runningDockerDesktop
152+
}
153+
154+
func (m *mockFeatures) IsProfilesFeatureEnabled() bool {
155+
return m.profilesEnabled
126156
}

cmd/docker-mcp/commands/gateway.go

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import (
1212
"github.com/docker/mcp-gateway/cmd/docker-mcp/catalog"
1313
catalogTypes "github.com/docker/mcp-gateway/pkg/catalog"
1414
"github.com/docker/mcp-gateway/pkg/docker"
15+
"github.com/docker/mcp-gateway/pkg/features"
1516
"github.com/docker/mcp-gateway/pkg/gateway"
1617
)
1718

18-
func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command {
19+
func gatewayCommand(docker docker.Client, dockerCli command.Cli, features features.Features) *cobra.Command {
1920
cmd := &cobra.Command{
2021
Use: "gateway",
2122
Short: "Manage the MCP Server gateway",
@@ -57,7 +58,7 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
5758
},
5859
}
5960
}
60-
if !isWorkingSetsFeatureEnabled(dockerCli) {
61+
if !features.IsProfilesFeatureEnabled() {
6162
// Default these only if we aren't defaulting to profiles
6263
setLegacyDefaults(&options)
6364
}
@@ -67,7 +68,7 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
6768
Short: "Run the gateway",
6869
Args: cobra.NoArgs,
6970
RunE: func(cmd *cobra.Command, _ []string) error {
70-
if isWorkingSetsFeatureEnabled(dockerCli) {
71+
if features.IsProfilesFeatureEnabled() {
7172
if len(options.ServerNames) > 0 || enableAllServers ||
7273
len(options.CatalogPath) > 0 || len(options.RegistryPath) > 0 || len(options.ConfigPath) > 0 || len(options.ToolsPath) > 0 ||
7374
len(additionalCatalogs) > 0 || len(additionalRegistries) > 0 || len(additionalConfigs) > 0 || len(additionalToolsConfig) > 0 ||
@@ -178,7 +179,7 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
178179
}
179180

180181
runCmd.Flags().StringSliceVar(&options.ServerNames, "servers", nil, "Names of the servers to enable (if non empty, ignore --registry flag)")
181-
if isWorkingSetsFeatureEnabled(dockerCli) {
182+
if features.IsProfilesFeatureEnabled() {
182183
runCmd.Flags().StringVar(&options.WorkingSet, "profile", "", "Profile ID to use (mutually exclusive with --servers and --enable-all-servers)")
183184
}
184185
runCmd.Flags().BoolVar(&enableAllServers, "enable-all-servers", false, "Enable all servers in the catalog (instead of using individual --servers options)")
@@ -354,20 +355,6 @@ func isToolNamePrefixFeatureEnabled(dockerCli command.Cli) bool {
354355
return value == "enabled"
355356
}
356357

357-
// isWorkingSetsFeatureEnabled checks if the profiles feature is enabled
358-
func isWorkingSetsFeatureEnabled(dockerCli command.Cli) bool {
359-
configFile := dockerCli.ConfigFile()
360-
if configFile == nil || configFile.Features == nil {
361-
return false
362-
}
363-
364-
value, exists := configFile.Features["profiles"]
365-
if !exists {
366-
return false
367-
}
368-
return value == "enabled"
369-
}
370-
371358
func setLegacyDefaults(options *gateway.Config) {
372359
if os.Getenv("DOCKER_MCP_IN_CONTAINER") == "1" {
373360
if len(options.CatalogPath) == 0 {

cmd/docker-mcp/commands/root.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/docker/mcp-gateway/pkg/db"
1414
"github.com/docker/mcp-gateway/pkg/desktop"
1515
"github.com/docker/mcp-gateway/pkg/docker"
16+
"github.com/docker/mcp-gateway/pkg/features"
1617
"github.com/docker/mcp-gateway/pkg/migrate"
1718
)
1819

@@ -33,7 +34,7 @@ Examples:
3334
`
3435

3536
// Root returns the root command for the init plugin
36-
func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command {
37+
func Root(ctx context.Context, cwd string, dockerCli command.Cli, features features.Features) *cobra.Command {
3738
dockerClient := docker.NewClient(dockerCli)
3839

3940
cmd := &cobra.Command{
@@ -50,8 +51,13 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command
5051
return err
5152
}
5253

54+
// Check the feature initialization error here for clearer error messages for the user
55+
if features.InitError() != nil {
56+
return features.InitError()
57+
}
58+
5359
if os.Getenv("DOCKER_MCP_IN_CONTAINER") != "1" {
54-
if isWorkingSetsFeatureEnabled(dockerCli) {
60+
if features.IsProfilesFeatureEnabled() {
5561
if isSubcommandOf(cmd, []string{"catalog-next", "catalog", "profile"}) {
5662
dao, err := db.New()
5763
if err != nil {
@@ -84,15 +90,15 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command
8490
return []string{"--help"}, cobra.ShellCompDirectiveNoFileComp
8591
})
8692

87-
if isWorkingSetsFeatureEnabled(dockerCli) {
93+
if features.IsProfilesFeatureEnabled() {
8894
cmd.AddCommand(workingSetCommand())
8995
cmd.AddCommand(catalogNextCommand())
9096
}
9197
cmd.AddCommand(catalogCommand(dockerCli))
92-
cmd.AddCommand(clientCommand(dockerCli, cwd))
98+
cmd.AddCommand(clientCommand(dockerCli, cwd, features))
9399
cmd.AddCommand(configCommand(dockerClient))
94-
cmd.AddCommand(featureCommand(dockerCli))
95-
cmd.AddCommand(gatewayCommand(dockerClient, dockerCli))
100+
cmd.AddCommand(featureCommand(dockerCli, features))
101+
cmd.AddCommand(gatewayCommand(dockerClient, dockerCli, features))
96102
cmd.AddCommand(oauthCommand())
97103
cmd.AddCommand(policyCommand())
98104
cmd.AddCommand(registryCommand())

cmd/docker-mcp/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/docker/mcp-gateway/cmd/docker-mcp/commands"
1616
"github.com/docker/mcp-gateway/cmd/docker-mcp/version"
17+
"github.com/docker/mcp-gateway/pkg/features"
1718
)
1819

1920
func main() {
@@ -31,7 +32,7 @@ func main() {
3132
}
3233

3334
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
34-
return commands.Root(ctx, cwd, dockerCli)
35+
return commands.Root(ctx, cwd, dockerCli, features.New(ctx, dockerCli))
3536
},
3637
manager.Metadata{
3738
SchemaVersion: "0.1.0",

docs/generator/generate.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/spf13/pflag"
1414

1515
"github.com/docker/mcp-gateway/cmd/docker-mcp/commands"
16+
"github.com/docker/mcp-gateway/pkg/features"
1617
)
1718

1819
const defaultSourcePath = "/reference/"
@@ -35,7 +36,7 @@ func gen(opts *options) error {
3536
DisableAutoGenTag: true,
3637
}
3738

38-
cmd.AddCommand(commands.Root(context.TODO(), "", dockerCLI))
39+
cmd.AddCommand(commands.Root(context.TODO(), "", dockerCLI, features.AllDisabled()))
3940

4041
c, err := clidocstool.New(clidocstool.Options{
4142
Root: cmd,

0 commit comments

Comments
 (0)