Skip to content

Commit f592a22

Browse files
Merge pull request #81 from masegraye/task/catalog-management
Enable User-Managed Catalogs for MCP Gateway
2 parents a57c84e + f0c89cd commit f592a22

File tree

190 files changed

+5434
-1491
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

190 files changed

+5434
-1491
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/bootstrap.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package catalog
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"gopkg.in/yaml.v3"
10+
)
11+
12+
// Bootstrap creates a starter catalog file with Docker and Docker Hub server entries as examples
13+
func Bootstrap(ctx context.Context, outputPath string) error {
14+
// Check if output file already exists
15+
if _, err := os.Stat(outputPath); err == nil {
16+
return fmt.Errorf("file %q already exists - will not overwrite", outputPath)
17+
}
18+
19+
// Load Docker catalog configuration
20+
_, err := ReadConfigWithDefaultCatalog(ctx)
21+
if err != nil {
22+
return fmt.Errorf("failed to load Docker catalog config: %w", err)
23+
}
24+
25+
// Read Docker catalog YAML data
26+
dockerCatalogData, err := ReadCatalogFile(DockerCatalogName)
27+
if err != nil {
28+
return fmt.Errorf("failed to read Docker catalog: %w", err)
29+
}
30+
31+
// Parse the Docker catalog to extract server entries
32+
var dockerCatalog map[string]any
33+
if err := yaml.Unmarshal(dockerCatalogData, &dockerCatalog); err != nil {
34+
return fmt.Errorf("failed to parse Docker catalog: %w", err)
35+
}
36+
37+
// Extract registry section
38+
registryInterface, ok := dockerCatalog["registry"]
39+
if !ok {
40+
return fmt.Errorf("docker catalog missing 'registry' section")
41+
}
42+
43+
registry, ok := registryInterface.(map[string]any)
44+
if !ok {
45+
return fmt.Errorf("docker catalog 'registry' section is not a map")
46+
}
47+
48+
// Extract Docker and Docker Hub servers
49+
dockerHubServer, hasDockerHub := registry[DockerHubServerName]
50+
dockerCLIServer, hasDockerCLI := registry[DockerCLIServerName]
51+
52+
if !hasDockerHub {
53+
return fmt.Errorf("docker catalog missing '%s' server", DockerHubServerName)
54+
}
55+
if !hasDockerCLI {
56+
return fmt.Errorf("docker catalog missing '%s' server", DockerCLIServerName)
57+
}
58+
59+
// Create bootstrap catalog with just the Docker servers
60+
bootstrapCatalog := map[string]any{
61+
"registry": map[string]any{
62+
DockerHubServerName: dockerHubServer,
63+
DockerCLIServerName: dockerCLIServer,
64+
},
65+
}
66+
67+
// Marshal to YAML
68+
bootstrapData, err := yaml.Marshal(bootstrapCatalog)
69+
if err != nil {
70+
return fmt.Errorf("failed to marshal bootstrap catalog: %w", err)
71+
}
72+
73+
// Create output directory if it doesn't exist
74+
outputDir := filepath.Dir(outputPath)
75+
if err := os.MkdirAll(outputDir, 0o755); err != nil {
76+
return fmt.Errorf("failed to create output directory: %w", err)
77+
}
78+
79+
// Write the bootstrap catalog file
80+
if err := os.WriteFile(outputPath, bootstrapData, 0o644); err != nil {
81+
return fmt.Errorf("failed to write bootstrap catalog file: %w", err)
82+
}
83+
84+
return nil
85+
}

cmd/docker-mcp/catalog/catalog.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ 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"
15+
16+
// Docker server names for bootstrap command
17+
DockerHubServerName = "dockerhub"
18+
DockerCLIServerName = "docker"
1419
)
1520

1621
var aliasToURL = map[string]string{

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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package catalog
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
10+
"gopkg.in/yaml.v3"
11+
12+
"github.com/docker/mcp-gateway/cmd/docker-mcp/internal/catalog"
13+
"github.com/docker/mcp-gateway/cmd/docker-mcp/internal/user"
14+
)
15+
16+
// Export exports a configured catalog to a file
17+
// This function only allows exporting user-managed catalogs, not the Docker catalog
18+
func Export(_ context.Context, catalogName, outputPath string) error {
19+
// Validate that we're not trying to export the Docker catalog
20+
if catalogName == DockerCatalogName || catalogName == DockerCatalogFilename {
21+
return fmt.Errorf("cannot export the Docker MCP catalog as it is managed by Docker")
22+
}
23+
24+
// Get configured catalogs to verify the catalog exists
25+
configuredCatalogs, err := getConfiguredCatalogs()
26+
if err != nil {
27+
return fmt.Errorf("failed to read configured catalogs: %w", err)
28+
}
29+
30+
// Check if the catalog exists in the configured catalogs
31+
catalogFileName := catalogName + ".yaml"
32+
found := false
33+
for _, configuredCatalog := range configuredCatalogs {
34+
if configuredCatalog == catalogFileName {
35+
found = true
36+
break
37+
}
38+
}
39+
40+
if !found {
41+
return fmt.Errorf("catalog '%s' not found in configured catalogs", catalogName)
42+
}
43+
44+
// Read the catalog file
45+
homeDir, err := user.HomeDir()
46+
if err != nil {
47+
return fmt.Errorf("failed to get home directory: %w", err)
48+
}
49+
50+
catalogPath := filepath.Join(homeDir, ".docker", "mcp", "catalogs", catalogFileName)
51+
catalogData, err := os.ReadFile(catalogPath)
52+
if err != nil {
53+
return fmt.Errorf("failed to read catalog file: %w", err)
54+
}
55+
56+
// Parse the catalog to validate it
57+
var catalogContent catalog.Catalog
58+
if err := yaml.Unmarshal(catalogData, &catalogContent); err != nil {
59+
return fmt.Errorf("failed to parse catalog: %w", err)
60+
}
61+
62+
// Create output directory if it doesn't exist
63+
outputDir := filepath.Dir(outputPath)
64+
if err := os.MkdirAll(outputDir, 0o755); err != nil {
65+
return fmt.Errorf("failed to create output directory: %w", err)
66+
}
67+
68+
// Write the catalog to the output file
69+
if err := os.WriteFile(outputPath, catalogData, 0o644); err != nil {
70+
return fmt.Errorf("failed to write export file: %w", err)
71+
}
72+
73+
fmt.Printf("Catalog '%s' exported to '%s'\n", catalogName, outputPath)
74+
return nil
75+
}
76+
77+
// Helper function to get configured catalogs (same logic as in internal/catalog)
78+
func getConfiguredCatalogs() ([]string, error) {
79+
homeDir, err := user.HomeDir()
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to get home directory: %w", err)
82+
}
83+
84+
catalogRegistryPath := filepath.Join(homeDir, ".docker", "mcp", "catalog.json")
85+
86+
// Read the catalog registry file
87+
data, err := os.ReadFile(catalogRegistryPath)
88+
if err != nil {
89+
if os.IsNotExist(err) {
90+
return []string{}, nil // No configured catalogs, return empty list
91+
}
92+
return nil, fmt.Errorf("failed to read catalog registry: %w", err)
93+
}
94+
95+
// Parse the registry
96+
var registry struct {
97+
Catalogs map[string]struct {
98+
DisplayName string `json:"displayName"`
99+
URL string `json:"url"`
100+
LastUpdate string `json:"lastUpdate"`
101+
} `json:"catalogs"`
102+
}
103+
104+
if err := json.Unmarshal(data, &registry); err != nil {
105+
return nil, fmt.Errorf("failed to parse catalog registry: %w", err)
106+
}
107+
108+
// Convert catalog names to file paths
109+
var catalogFiles []string
110+
for catalogName := range registry.Catalogs {
111+
catalogFiles = append(catalogFiles, catalogName+".yaml")
112+
}
113+
114+
return catalogFiles, nil
115+
}

cmd/docker-mcp/catalog/fork.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,23 @@ 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+
if _, ok := cfg.Catalogs[src]; !ok {
21+
return fmt.Errorf("catalog %q not found", src)
22+
}
1423
}
24+
1525
if _, ok := cfg.Catalogs[dst]; ok {
1626
return fmt.Errorf("catalog %q already exists", dst)
1727
}

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

cmd/docker-mcp/commands/bootstrap.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package commands
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/docker/mcp-gateway/cmd/docker-mcp/catalog"
7+
)
8+
9+
func bootstrapCatalogCommand() *cobra.Command {
10+
return &cobra.Command{
11+
Use: "bootstrap <output-file-path>",
12+
Short: "Create a starter catalog file with Docker and Docker Hub server entries as examples",
13+
Long: `Create a starter catalog file with Docker Hub and Docker CLI server entries as examples.
14+
This command extracts the official Docker server definitions and creates a properly formatted
15+
catalog file that users can modify and use as a foundation for their custom catalogs.
16+
17+
The output file is standalone and not automatically imported - users can modify it and then
18+
import it or use it as a source for the 'catalog add' command.`,
19+
Args: cobra.ExactArgs(1),
20+
RunE: func(cmd *cobra.Command, args []string) error {
21+
return catalog.Bootstrap(cmd.Context(), args[0])
22+
},
23+
}
24+
}

0 commit comments

Comments
 (0)