Skip to content

Commit 5e766a1

Browse files
committed
Catalog bootstrap command to make it easy to get started
1 parent fd5dcbb commit 5e766a1

File tree

10 files changed

+995
-163
lines changed

10 files changed

+995
-163
lines changed

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]interface{}
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]interface{})
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]interface{}{
61+
"registry": map[string]interface{}{
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, 0755); 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, 0644); 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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import (
1111
const (
1212
DockerCatalogName = "docker-mcp"
1313
DockerCatalogURL = "https://desktop.docker.com/mcp/catalog/v2/catalog.yaml"
14+
15+
// Docker server names for bootstrap command
16+
DockerHubServerName = "dockerhub"
17+
DockerCLIServerName = "docker"
1418
)
1519

1620
var aliasToURL = map[string]string{

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(ctx context.Context, catalogName, outputPath string) error {
19+
// Validate that we're not trying to export the Docker catalog
20+
if catalogName == "docker-mcp" || catalogName == "docker-mcp.yaml" {
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, 0755); 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, 0644); 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/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)