Skip to content
Merged
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
!/.git
!/server.go
!/test/servers
!/test/testdata
95 changes: 92 additions & 3 deletions cmd/docker-mcp/commands/catalog.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package commands

import (
"context"
"encoding/json"
"fmt"

"github.com/spf13/cobra"

"github.com/docker/mcp-gateway/cmd/docker-mcp/catalog"
catalogTypes "github.com/docker/mcp-gateway/cmd/docker-mcp/internal/catalog"
"github.com/docker/mcp-gateway/cmd/docker-mcp/internal/yq"
)

func catalogCommand() *cobra.Command {
Expand All @@ -31,21 +35,40 @@ func catalogCommand() *cobra.Command {
}

func importCatalogCommand() *cobra.Command {
return &cobra.Command{
var mcpRegistry string
var dryRun bool
cmd := &cobra.Command{
Use: "import <alias|url|file>",
Short: "Import a catalog from URL or file",
Long: `Import an MCP server catalog from a URL or local file. The catalog will be downloaded
and stored locally for use with the MCP gateway.`,
and stored locally for use with the MCP gateway.

When --mcp-registry flag is used, the argument must be an existing catalog name, and the
command will import servers from the MCP registry URL into that catalog.`,
Args: cobra.ExactArgs(1),
Example: ` # Import from URL
docker mcp catalog import https://example.com/my-catalog.yaml

# Import from local file
docker mcp catalog import ./shared-catalog.yaml`,
docker mcp catalog import ./shared-catalog.yaml

# Import from MCP registry URL into existing catalog
docker mcp catalog import my-catalog --mcp-registry https://registry.example.com/server`,
RunE: func(cmd *cobra.Command, args []string) error {
// If mcp-registry flag is provided, import to existing catalog
if mcpRegistry != "" {
if dryRun {
return runOfficialregistryImport(cmd.Context(), mcpRegistry, nil)
}
return importMCPRegistryToCatalog(cmd.Context(), args[0], mcpRegistry)
}
// Default behavior: import entire catalog
return catalog.Import(cmd.Context(), args[0])
},
}
cmd.Flags().StringVar(&mcpRegistry, "mcp-registry", "", "Import server from MCP registry URL into existing catalog")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show Imported Data but do not update the Catalog")
return cmd
}

func exportCatalogCommand() *cobra.Command {
Expand Down Expand Up @@ -237,3 +260,69 @@ func resetCatalogCommand() *cobra.Command {
},
}
}

// importMCPRegistryToCatalog imports a server from an MCP registry URL into an existing catalog
func importMCPRegistryToCatalog(ctx context.Context, catalogName, mcpRegistryURL string) error {
// Check if the catalog exists
cfg, err := catalog.ReadConfig()
if err != nil {
return fmt.Errorf("failed to read catalog config: %w", err)
}

_, exists := cfg.Catalogs[catalogName]
if !exists {
return fmt.Errorf("catalog '%s' does not exist", catalogName)
}

// Prevent users from modifying the Docker catalog
if catalogName == catalog.DockerCatalogName {
return fmt.Errorf("cannot import servers into catalog '%s' as it is managed by Docker", catalogName)
}

// Fetch server from MCP registry
var servers []catalogTypes.Server
if err := runOfficialregistryImport(ctx, mcpRegistryURL, &servers); err != nil {
return fmt.Errorf("failed to fetch server from MCP registry: %w", err)
}

if len(servers) == 0 {
return fmt.Errorf("no servers found at MCP registry URL")
}

// For now, we'll import the first server (MCP registry URLs typically contain one server)
server := servers[0]

serverName := server.Name

// Convert the server to JSON for injection into the catalog
serverJSON, err := json.Marshal(server)
if err != nil {
return fmt.Errorf("failed to marshal server: %w", err)
}

// Read the current catalog content
catalogContent, err := catalog.ReadCatalogFile(catalogName)
if err != nil {
return fmt.Errorf("failed to read catalog file: %w", err)
}

// Inject the server into the catalog using the same pattern as the add function
updatedContent, err := injectServerIntoCatalog(catalogContent, serverName, serverJSON)
if err != nil {
return fmt.Errorf("failed to inject server into catalog: %w", err)
}

// Write the updated catalog back
if err := catalog.WriteCatalogFile(catalogName, updatedContent); err != nil {
return fmt.Errorf("failed to write updated catalog: %w", err)
}

fmt.Printf("Successfully imported server '%s' from MCP registry into catalog '%s'\n", serverName, catalogName)
return nil
}

// injectServerIntoCatalog injects a server JSON into a catalog YAML using yq
func injectServerIntoCatalog(yamlData []byte, serverName string, serverJSON []byte) ([]byte, error) {
query := fmt.Sprintf(`.registry."%s" = %s`, serverName, string(serverJSON))
return yq.Evaluate(query, yamlData, yq.NewYamlDecoder(), yq.NewYamlEncoder())
}
14 changes: 2 additions & 12 deletions cmd/docker-mcp/commands/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ func featureEnableCommand(dockerCli command.Cli) *cobra.Command {
Long: `Enable an experimental feature.

Available features:
configured-catalogs Allow gateway to use user-managed catalogs alongside Docker catalog
oauth-interceptor Enable GitHub OAuth flow interception for automatic authentication
dynamic-tools Enable internal MCP management tools (mcp-find, mcp-add, mcp-remove)`,
Args: cobra.ExactArgs(1),
Expand All @@ -46,7 +45,7 @@ Available features:

// Validate feature name
if !isKnownFeature(featureName) {
return fmt.Errorf("unknown feature: %s\n\nAvailable features:\n configured-catalogs Allow gateway to use user-managed catalogs\n oauth-interceptor Enable GitHub OAuth flow interception\n dynamic-tools Enable internal MCP management tools", featureName)
return fmt.Errorf("unknown feature: %s\n\nAvailable features:\n oauth-interceptor Enable GitHub OAuth flow interception\n dynamic-tools Enable internal MCP management tools", featureName)
}

// Enable the feature
Expand All @@ -65,12 +64,6 @@ Available features:

// Provide usage hints for features
switch featureName {
case "configured-catalogs":
fmt.Println("\nTo use configured catalogs with the gateway, run:")
fmt.Println(" docker mcp gateway run --use-configured-catalogs")
fmt.Println("\nTo create and manage catalogs, use:")
fmt.Println(" docker mcp catalog create <name>")
fmt.Println(" docker mcp catalog add <catalog> <server-name> <server-file>")
case "oauth-interceptor":
fmt.Println("\nThis feature enables automatic GitHub OAuth interception when 401 errors occur.")
fmt.Println("When enabled, the gateway will automatically provide OAuth URLs for authentication.")
Expand Down Expand Up @@ -135,7 +128,7 @@ func featureListCommand(dockerCli command.Cli) *cobra.Command {
fmt.Println()

// Show all known features
knownFeatures := []string{"configured-catalogs", "oauth-interceptor", "dynamic-tools"}
knownFeatures := []string{"oauth-interceptor", "dynamic-tools"}
for _, feature := range knownFeatures {
status := "disabled"
if isFeatureEnabledFromCli(dockerCli, feature) {
Expand All @@ -146,8 +139,6 @@ func featureListCommand(dockerCli command.Cli) *cobra.Command {

// Add description for each feature
switch feature {
case "configured-catalogs":
fmt.Printf(" %-20s %s\n", "", "Allow gateway to use user-managed catalogs alongside Docker catalog")
case "oauth-interceptor":
fmt.Printf(" %-20s %s\n", "", "Enable GitHub OAuth flow interception for automatic authentication")
case "dynamic-tools":
Expand Down Expand Up @@ -212,7 +203,6 @@ func isFeatureEnabledFromConfig(configFile *configfile.ConfigFile, feature strin
// isKnownFeature checks if the feature name is valid
func isKnownFeature(feature string) bool {
knownFeatures := []string{
"configured-catalogs",
"oauth-interceptor",
"dynamic-tools",
}
Expand Down
Loading
Loading