Skip to content

Commit e2bb24c

Browse files
committed
Use constants, protect docker-mcpEnsures no one can modify the docker-mcp resource
1 parent 6cfe6ac commit e2bb24c

File tree

9 files changed

+208
-8
lines changed

9 files changed

+208
-8
lines changed

cmd/docker-mcp/catalog/add.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ func ParseAddArgs(dst, src, catalogFile string) *ParsedAddArgs {
2323
}
2424

2525
func ValidateArgs(args ParsedAddArgs) error {
26+
// Prevent users from modifying the Docker catalog
27+
if args.Dst == DockerCatalogName {
28+
return fmt.Errorf("cannot add servers to catalog '%s' as it is managed by Docker", args.Dst)
29+
}
30+
2631
cfg, err := ReadConfig()
2732
if err != nil {
2833
return err

cmd/docker-mcp/catalog/catalog.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import (
99
)
1010

1111
const (
12-
DockerCatalogName = "docker-mcp"
13-
DockerCatalogURL = "https://desktop.docker.com/mcp/catalog/v2/catalog.yaml"
12+
DockerCatalogName = "docker-mcp"
13+
DockerCatalogURL = "https://desktop.docker.com/mcp/catalog/v2/catalog.yaml"
14+
DockerCatalogFilename = "docker-mcp.yaml"
1415

1516
// Docker server names for bootstrap command
1617
DockerHubServerName = "dockerhub"

cmd/docker-mcp/catalog/create.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import (
55
)
66

77
func Create(name string) error {
8+
// Prevent users from creating the Docker catalog
9+
if name == DockerCatalogName {
10+
return fmt.Errorf("cannot create catalog '%s' as it is reserved for Docker's official catalog", name)
11+
}
12+
813
cfg, err := ReadConfig()
914
if err != nil {
1015
return err

cmd/docker-mcp/catalog/export.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
// This function only allows exporting user-managed catalogs, not the Docker catalog
1818
func Export(ctx context.Context, catalogName, outputPath string) error {
1919
// Validate that we're not trying to export the Docker catalog
20-
if catalogName == "docker-mcp" || catalogName == "docker-mcp.yaml" {
20+
if catalogName == DockerCatalogName || catalogName == DockerCatalogFilename {
2121
return fmt.Errorf("cannot export the Docker MCP catalog as it is managed by Docker")
2222
}
2323

cmd/docker-mcp/catalog/fork.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,26 @@ import (
55
)
66

77
func Fork(src, dst string) error {
8+
// Prevent users from creating a destination catalog with the Docker name
9+
if dst == DockerCatalogName {
10+
return fmt.Errorf("cannot create catalog '%s' as it is reserved for Docker's official catalog", dst)
11+
}
12+
813
cfg, err := ReadConfig()
914
if err != nil {
1015
return err
1116
}
12-
if _, ok := cfg.Catalogs[src]; !ok {
13-
return fmt.Errorf("catalog %q not found", src)
17+
18+
// Special handling for Docker catalog - it exists but not in cfg.Catalogs
19+
if src == DockerCatalogName {
20+
// Allow forking from Docker catalog, but it won't be in cfg.Catalogs
21+
// We'll read it directly from the DockerCatalogName
22+
} else {
23+
if _, ok := cfg.Catalogs[src]; !ok {
24+
return fmt.Errorf("catalog %q not found", src)
25+
}
1426
}
27+
1528
if _, ok := cfg.Catalogs[dst]; ok {
1629
return fmt.Errorf("catalog %q already exists", dst)
1730
}

cmd/docker-mcp/catalog/rm.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import (
77
)
88

99
func Rm(name string) error {
10+
// Prevent users from removing the Docker catalog
11+
if name == DockerCatalogName {
12+
return fmt.Errorf("cannot remove catalog '%s' as it is managed by Docker", name)
13+
}
14+
1015
cfg, err := ReadConfig()
1116
if err != nil {
1217
return err
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/docker/mcp-gateway/cmd/docker-mcp/catalog"
13+
)
14+
15+
func TestDockerCatalogProtection(t *testing.T) {
16+
// Create temporary home directory
17+
tempHome := t.TempDir()
18+
originalHome := os.Getenv("HOME")
19+
defer func() {
20+
if originalHome != "" {
21+
os.Setenv("HOME", originalHome)
22+
}
23+
}()
24+
25+
if err := os.Setenv("HOME", tempHome); err != nil {
26+
t.Fatal(err)
27+
}
28+
29+
// Create the MCP directory structure
30+
mcpDir := filepath.Join(tempHome, ".docker", "mcp")
31+
catalogsDir := filepath.Join(mcpDir, "catalogs")
32+
require.NoError(t, os.MkdirAll(catalogsDir, 0755))
33+
34+
// Initialize the catalog system
35+
ctx := context.Background()
36+
require.NoError(t, catalog.Init(ctx))
37+
38+
t.Run("TestCreateDockerCatalogPrevented", func(t *testing.T) {
39+
err := catalog.Create(catalog.DockerCatalogName)
40+
assert.Error(t, err)
41+
assert.Contains(t, err.Error(), "cannot create catalog 'docker-mcp' as it is reserved for Docker's official catalog")
42+
})
43+
44+
t.Run("TestRemoveDockerCatalogPrevented", func(t *testing.T) {
45+
err := catalog.Rm(catalog.DockerCatalogName)
46+
assert.Error(t, err)
47+
assert.Contains(t, err.Error(), "cannot remove catalog 'docker-mcp' as it is managed by Docker")
48+
})
49+
50+
t.Run("TestForkToDockerCatalogPrevented", func(t *testing.T) {
51+
// First create a source catalog to fork from
52+
require.NoError(t, catalog.Create("source-catalog"))
53+
54+
err := catalog.Fork("source-catalog", catalog.DockerCatalogName)
55+
assert.Error(t, err)
56+
assert.Contains(t, err.Error(), "cannot create catalog 'docker-mcp' as it is reserved for Docker's official catalog")
57+
})
58+
59+
t.Run("TestForkFromDockerCatalogAllowed", func(t *testing.T) {
60+
// Create a minimal docker-mcp.yaml file to fork from
61+
dockerCatalog := `
62+
registry:
63+
test-server:
64+
title: "Test Server"
65+
description: "A test server"
66+
image: "test/server:latest"
67+
`
68+
dockerCatalogPath := filepath.Join(catalogsDir, "docker-mcp.yaml")
69+
require.NoError(t, os.WriteFile(dockerCatalogPath, []byte(dockerCatalog), 0644))
70+
71+
// Forking FROM Docker catalog should work
72+
err := catalog.Fork(catalog.DockerCatalogName, "my-docker-fork")
73+
assert.NoError(t, err)
74+
75+
// Verify the fork was created
76+
cfg, err := catalog.ReadConfig()
77+
require.NoError(t, err)
78+
_, exists := cfg.Catalogs["my-docker-fork"]
79+
assert.True(t, exists)
80+
})
81+
82+
t.Run("TestAddToDockerCatalogPrevented", func(t *testing.T) {
83+
// Create a source catalog file
84+
sourceCatalog := `
85+
registry:
86+
source-server:
87+
title: "Source Server"
88+
description: "A source server"
89+
image: "source/server:latest"
90+
`
91+
sourcePath := filepath.Join(catalogsDir, "source.yaml")
92+
require.NoError(t, os.WriteFile(sourcePath, []byte(sourceCatalog), 0644))
93+
94+
// Try to add to Docker catalog
95+
args := catalog.ParseAddArgs(catalog.DockerCatalogName, "test-server", sourcePath)
96+
err := catalog.ValidateArgs(*args)
97+
assert.Error(t, err)
98+
assert.Contains(t, err.Error(), "cannot add servers to catalog 'docker-mcp' as it is managed by Docker")
99+
})
100+
101+
t.Run("TestExportDockerCatalogPrevented", func(t *testing.T) {
102+
outputPath := filepath.Join(tempHome, "exported-docker.yaml")
103+
err := catalog.Export(ctx, catalog.DockerCatalogName, outputPath)
104+
assert.Error(t, err)
105+
assert.Contains(t, err.Error(), "cannot export the Docker MCP catalog as it is managed by Docker")
106+
})
107+
108+
t.Run("TestExportDockerCatalogFilenamePrevented", func(t *testing.T) {
109+
outputPath := filepath.Join(tempHome, "exported-docker.yaml")
110+
err := catalog.Export(ctx, catalog.DockerCatalogFilename, outputPath)
111+
assert.Error(t, err)
112+
assert.Contains(t, err.Error(), "cannot export the Docker MCP catalog as it is managed by Docker")
113+
})
114+
115+
t.Run("TestNormalCatalogOperationsStillWork", func(t *testing.T) {
116+
// Create a normal catalog
117+
err := catalog.Create("normal-catalog")
118+
assert.NoError(t, err)
119+
120+
// Verify it exists
121+
cfg, err := catalog.ReadConfig()
122+
require.NoError(t, err)
123+
_, exists := cfg.Catalogs["normal-catalog"]
124+
assert.True(t, exists)
125+
126+
// Fork it
127+
err = catalog.Fork("normal-catalog", "forked-catalog")
128+
assert.NoError(t, err)
129+
130+
// Add to it (create source file first)
131+
sourceCatalog := `
132+
registry:
133+
normal-server:
134+
title: "Normal Server"
135+
description: "A normal server"
136+
image: "normal/server:latest"
137+
`
138+
sourcePath := filepath.Join(catalogsDir, "normal-source.yaml")
139+
require.NoError(t, os.WriteFile(sourcePath, []byte(sourceCatalog), 0644))
140+
141+
args := catalog.ParseAddArgs("normal-catalog", "normal-server", sourcePath)
142+
err = catalog.ValidateArgs(*args)
143+
assert.NoError(t, err)
144+
145+
err = catalog.Add(*args, false)
146+
assert.NoError(t, err)
147+
148+
// Export it
149+
outputPath := filepath.Join(tempHome, "exported-normal.yaml")
150+
err = catalog.Export(ctx, "normal-catalog", outputPath)
151+
assert.NoError(t, err)
152+
153+
// Verify export file exists
154+
_, err = os.Stat(outputPath)
155+
assert.NoError(t, err)
156+
157+
// Remove it
158+
err = catalog.Rm("normal-catalog")
159+
assert.NoError(t, err)
160+
161+
// Verify it's gone
162+
cfg, err = catalog.ReadConfig()
163+
require.NoError(t, err)
164+
_, exists = cfg.Catalogs["normal-catalog"]
165+
assert.False(t, exists)
166+
})
167+
}

cmd/docker-mcp/commands/gateway.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
4444
} else {
4545
// On-host.
4646
options = gateway.Config{
47-
CatalogPath: []string{"docker-mcp.yaml"},
47+
CatalogPath: []string{catalog.DockerCatalogFilename},
4848
RegistryPath: []string{"registry.yaml"},
4949
ConfigPath: []string{"config.yaml"},
5050
ToolsPath: []string{"tools.yaml"},

cmd/docker-mcp/internal/catalog/catalog.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ import (
1616
"github.com/docker/mcp-gateway/cmd/docker-mcp/internal/user"
1717
)
1818

19+
const (
20+
DockerCatalogFilename = "docker-mcp.yaml"
21+
)
22+
1923
func Get(ctx context.Context) (Catalog, error) {
20-
return ReadFrom(ctx, []string{"docker-mcp.yaml"})
24+
return ReadFrom(ctx, []string{DockerCatalogFilename})
2125
}
2226

2327
func ReadFrom(ctx context.Context, fileOrURLs []string) (Catalog, error) {
@@ -115,7 +119,7 @@ func isURL(fileOrURL string) bool {
115119

116120
// GetWithOptions loads catalogs with enhanced options for configured catalogs and additional catalogs
117121
func GetWithOptions(ctx context.Context, useConfigured bool, additionalCatalogs []string) (Catalog, error) {
118-
catalogPaths := []string{"docker-mcp.yaml"}
122+
catalogPaths := []string{DockerCatalogFilename}
119123

120124
// Add configured catalogs if enabled
121125
if useConfigured {

0 commit comments

Comments
 (0)