Skip to content

Commit ec5ad7d

Browse files
committed
Support hidden --include-pypi flag for catalog created from community registry.
1 parent e5f028f commit ec5ad7d

File tree

7 files changed

+104
-67
lines changed

7 files changed

+104
-67
lines changed

cmd/docker-mcp/commands/catalog_next.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func createCatalogNextCommand() *cobra.Command {
4040
FromLegacyCatalog string
4141
FromCommunityRegistry string
4242
Servers []string
43+
IncludePyPI bool
4344
}
4445

4546
cmd := &cobra.Command{
@@ -61,13 +62,17 @@ func createCatalogNextCommand() *cobra.Command {
6162
return fmt.Errorf("only one of --from-profile, --from-legacy-catalog, or --from-community-registry can be specified")
6263
}
6364

65+
if opts.IncludePyPI && opts.FromCommunityRegistry == "" {
66+
return fmt.Errorf("--include-pypi can only be used when creating a catalog from a community registry")
67+
}
68+
6469
dao, err := db.New()
6570
if err != nil {
6671
return err
6772
}
6873
registryClient := registryapi.NewClient()
6974
ociService := oci.NewService()
70-
return catalognext.Create(cmd.Context(), dao, registryClient, ociService, args[0], opts.Servers, opts.FromWorkingSet, opts.FromLegacyCatalog, opts.FromCommunityRegistry, opts.Title)
75+
return catalognext.Create(cmd.Context(), dao, registryClient, ociService, args[0], opts.Servers, opts.FromWorkingSet, opts.FromLegacyCatalog, opts.FromCommunityRegistry, opts.Title, opts.IncludePyPI)
7176
},
7277
}
7378

@@ -78,6 +83,9 @@ func createCatalogNextCommand() *cobra.Command {
7883
flags.StringVar(&opts.FromCommunityRegistry, "from-community-registry", "", "Community registry hostname to fetch servers from (e.g. registry.modelcontextprotocol.io)")
7984
flags.StringVar(&opts.Title, "title", "", "Title of the catalog")
8085

86+
flags.BoolVar(&opts.IncludePyPI, "include-pypi", false, "Include PyPI servers when creating a catalog from a community registry")
87+
cmd.Flags().MarkHidden("include-pypi") //nolint:errcheck
88+
8189
return cmd
8290
}
8391

pkg/catalog/registry_to_catalog.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ import (
1717
// no compatible package type (e.g. no OCI+stdio package and no remote).
1818
var ErrIncompatibleServer = errors.New("incompatible server")
1919

20+
// TransformSource describes the package type that TransformToDocker resolved.
21+
type TransformSource string
22+
23+
const (
24+
TransformSourceOCI TransformSource = "oci"
25+
TransformSourcePyPI TransformSource = "pypi"
26+
TransformSourceRemote TransformSource = "remote"
27+
)
28+
2029
// TransformOption configures the behavior of TransformToDocker.
2130
type TransformOption func(*transformOptions)
2231

@@ -397,8 +406,9 @@ func getPublisherProvidedMeta(meta *v0.ServerMeta) map[string]any {
397406
return meta.PublisherProvided
398407
}
399408

400-
// TransformToDocker transforms a ServerDetail (community format) to Server (catalog format)
401-
func TransformToDocker(ctx context.Context, serverDetail ServerDetail, opts ...TransformOption) (*Server, error) {
409+
// TransformToDocker transforms a ServerDetail (community format) to Server (catalog format).
410+
// The returned TransformSource indicates which package type was used (oci, pypi, or remote).
411+
func TransformToDocker(ctx context.Context, serverDetail ServerDetail, opts ...TransformOption) (*Server, TransformSource, error) {
402412
options := transformOptions{
403413
allowPyPI: true,
404414
}
@@ -441,20 +451,23 @@ func TransformToDocker(ctx context.Context, serverDetail ServerDetail, opts ...T
441451
Description: serverDetail.Description,
442452
}
443453

454+
var source TransformSource
455+
444456
// Add image and command for OCI or PyPI package
445457
if pkg != nil {
446458
switch pkg.RegistryType {
447459
case "oci":
448460
if image := extractImageInfo(*pkg); image != "" {
449461
server.Image = image
450462
server.Type = "server"
463+
source = TransformSourceOCI
451464
}
452465
case "pypi":
453466
var pythonVersion string
454467
if options.pypiResolver != nil {
455468
pv, found := options.pypiResolver(ctx, pkg.Identifier, pkg.Version, pkg.RegistryBaseURL)
456469
if !found {
457-
return nil, fmt.Errorf("pypi package %s@%s was not found", pkg.Identifier, pkg.Version)
470+
return nil, "", fmt.Errorf("pypi package %s@%s was not found", pkg.Identifier, pkg.Version)
458471
}
459472
pythonVersion = pv
460473
}
@@ -465,9 +478,10 @@ func TransformToDocker(ctx context.Context, serverDetail ServerDetail, opts ...T
465478
server.Volumes = volumes
466479
server.Type = "server"
467480
server.LongLived = true
481+
source = TransformSourcePyPI
468482
}
469483
default:
470-
return nil, fmt.Errorf("unsupported registry type: %s", pkg.RegistryType)
484+
return nil, "", fmt.Errorf("unsupported registry type: %s", pkg.RegistryType)
471485
}
472486
}
473487

@@ -476,11 +490,12 @@ func TransformToDocker(ctx context.Context, serverDetail ServerDetail, opts ...T
476490
remoteVal := convertRemote(*remote, serverName)
477491
server.Remote = remoteVal
478492
server.Type = "remote"
493+
source = TransformSourceRemote
479494
}
480495

481496
// Validate that we have at least one way to run the server
482497
if server.Image == "" && server.Remote.URL == "" {
483-
return nil, fmt.Errorf("%w: no compatible packages for %s", ErrIncompatibleServer, serverDetail.Name)
498+
return nil, "", fmt.Errorf("%w: no compatible packages for %s", ErrIncompatibleServer, serverDetail.Name)
484499
}
485500

486501
// Add config schema if we have config variables
@@ -548,5 +563,5 @@ func TransformToDocker(ctx context.Context, serverDetail ServerDetail, opts ...T
548563
}
549564
}
550565

551-
return server, nil
566+
return server, source, nil
552567
}

pkg/catalog/registry_to_catalog_test.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
)
1212

1313
// transformTestJSON is a test helper that unmarshals registry JSON, calls TransformToDocker,
14-
// and returns both the Server and a pretty-printed JSON string of the result.
14+
// and returns the Server, the TransformSource, and a pretty-printed JSON string of the result.
1515
func transformTestJSON(t *testing.T, registryJSON string, resolver PyPIVersionResolver) (Server, string) {
1616
t.Helper()
1717
var serverResponse v0.ServerResponse
@@ -22,7 +22,7 @@ func transformTestJSON(t *testing.T, registryJSON string, resolver PyPIVersionRe
2222
if resolver != nil {
2323
opts = append(opts, WithPyPIResolver(resolver))
2424
}
25-
result, err := TransformToDocker(t.Context(), serverResponse.Server, opts...)
25+
result, _, err := TransformToDocker(t.Context(), serverResponse.Server, opts...)
2626
if err != nil {
2727
t.Fatalf("TransformToDocker failed: %v", err)
2828
}
@@ -1137,7 +1137,7 @@ func TestTransformPyPIDisallowed(t *testing.T) {
11371137
t.Fatalf("Failed to parse registry JSON: %v", err)
11381138
}
11391139

1140-
_, err := TransformToDocker(t.Context(), serverResponse.Server, WithAllowPyPI(false))
1140+
_, _, err := TransformToDocker(t.Context(), serverResponse.Server, WithAllowPyPI(false))
11411141
if err == nil {
11421142
t.Fatal("Expected error when PyPI is disallowed, got nil")
11431143
}
@@ -1169,10 +1169,13 @@ func TestTransformPyPIAllowedByDefault(t *testing.T) {
11691169
t.Fatalf("Failed to parse registry JSON: %v", err)
11701170
}
11711171

1172-
result, err := TransformToDocker(t.Context(), serverResponse.Server)
1172+
result, source, err := TransformToDocker(t.Context(), serverResponse.Server)
11731173
if err != nil {
11741174
t.Fatalf("Expected success for PyPI with default options, got: %v", err)
11751175
}
1176+
if source != TransformSourcePyPI {
1177+
t.Errorf("Expected source %q, got %q", TransformSourcePyPI, source)
1178+
}
11761179
if result.Type != "server" {
11771180
t.Errorf("Expected type 'server', got '%s'", result.Type)
11781181
}
@@ -1207,7 +1210,7 @@ func TestTransformPyPIPackageNotFound(t *testing.T) {
12071210
t.Fatalf("Failed to parse registry JSON: %v", err)
12081211
}
12091212

1210-
_, err := TransformToDocker(t.Context(), serverResponse.Server, WithPyPIResolver(notFoundResolver))
1213+
_, _, err := TransformToDocker(t.Context(), serverResponse.Server, WithPyPIResolver(notFoundResolver))
12111214
if err == nil {
12121215
t.Fatal("Expected error when PyPI package is not found, got nil")
12131216
}

pkg/catalog_next/create.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
"github.com/docker/mcp-gateway/pkg/workingset"
2222
)
2323

24-
func Create(ctx context.Context, dao db.DAO, registryClient registryapi.Client, ociService oci.Service, refStr string, servers []string, workingSetID string, legacyCatalogURL string, communityRegistryRef string, title string) error {
24+
func Create(ctx context.Context, dao db.DAO, registryClient registryapi.Client, ociService oci.Service, refStr string, servers []string, workingSetID string, legacyCatalogURL string, communityRegistryRef string, title string, includePyPI bool) error {
2525
telemetry.Init()
2626
start := time.Now()
2727
var success bool
@@ -49,7 +49,7 @@ func Create(ctx context.Context, dao db.DAO, registryClient registryapi.Client,
4949
return fmt.Errorf("failed to create catalog from legacy catalog: %w", err)
5050
}
5151
} else if communityRegistryRef != "" {
52-
catalog, err = createCatalogFromCommunityRegistry(ctx, registryClient, communityRegistryRef)
52+
catalog, err = createCatalogFromCommunityRegistry(ctx, registryClient, communityRegistryRef, includePyPI)
5353
if err != nil {
5454
return fmt.Errorf("failed to create catalog from community registry: %w", err)
5555
}
@@ -202,13 +202,14 @@ func workingSetServerToCatalogServer(server workingset.Server) Server {
202202
type communityRegistryResult struct {
203203
serversAdded int
204204
serversOCI int
205+
serversPyPI int
205206
serversRemote int
206207
serversSkipped int
207208
totalServers int
208209
skippedByType map[string]int
209210
}
210211

211-
func createCatalogFromCommunityRegistry(ctx context.Context, registryClient registryapi.Client, registryRef string) (Catalog, error) {
212+
func createCatalogFromCommunityRegistry(ctx context.Context, registryClient registryapi.Client, registryRef string, includePyPI bool) (Catalog, error) {
212213
baseURL := "https://" + registryRef
213214
servers, err := registryClient.ListServers(ctx, baseURL, "")
214215
if err != nil {
@@ -217,10 +218,10 @@ func createCatalogFromCommunityRegistry(ctx context.Context, registryClient regi
217218

218219
catalogServers := make([]Server, 0)
219220
skippedByType := make(map[string]int)
220-
var ociCount, remoteCount int
221+
var ociCount, remoteCount, pypiCount int
221222

222223
for _, serverResp := range servers {
223-
catalogServer, err := legacycatalog.TransformToDocker(ctx, serverResp.Server, legacycatalog.WithAllowPyPI(false))
224+
catalogServer, transformSource, err := legacycatalog.TransformToDocker(ctx, serverResp.Server, legacycatalog.WithAllowPyPI(includePyPI), legacycatalog.WithPyPIResolver(legacycatalog.DefaultPyPIVersionResolver()))
224225
if err != nil {
225226
if !errors.Is(err, legacycatalog.ErrIncompatibleServer) {
226227
fmt.Fprintf(os.Stderr, "Warning: failed to transform server %q: %v\n", serverResp.Server.Name, err)
@@ -242,7 +243,12 @@ func createCatalogFromCommunityRegistry(ctx context.Context, registryClient regi
242243
var s Server
243244
switch catalogServer.Type {
244245
case "server":
245-
ociCount++
246+
switch transformSource {
247+
case legacycatalog.TransformSourcePyPI:
248+
pypiCount++
249+
default:
250+
ociCount++
251+
}
246252
s = Server{
247253
Type: workingset.ServerTypeImage,
248254
Image: catalogServer.Image,
@@ -272,12 +278,13 @@ func createCatalogFromCommunityRegistry(ctx context.Context, registryClient regi
272278
result := communityRegistryResult{
273279
serversAdded: len(catalogServers),
274280
serversOCI: ociCount,
281+
serversPyPI: pypiCount,
275282
serversRemote: remoteCount,
276283
serversSkipped: totalSkipped(skippedByType),
277284
totalServers: len(servers),
278285
skippedByType: skippedByType,
279286
}
280-
printCommunityRegistryResult(registryRef, result)
287+
printCommunityRegistryResult(registryRef, result, includePyPI)
281288

282289
return Catalog{
283290
CatalogArtifact: CatalogArtifact{
@@ -296,11 +303,14 @@ func totalSkipped(skippedByType map[string]int) int {
296303
return total
297304
}
298305

299-
func printCommunityRegistryResult(refStr string, result communityRegistryResult) {
306+
func printCommunityRegistryResult(refStr string, result communityRegistryResult, includePyPI bool) {
300307
fmt.Fprintf(os.Stderr, "Fetched %d servers from %s\n", result.serversAdded, refStr)
301308
fmt.Fprintf(os.Stderr, " Total in registry: %d\n", result.totalServers)
302309
fmt.Fprintf(os.Stderr, " Imported: %d\n", result.serversAdded)
303310
fmt.Fprintf(os.Stderr, " OCI (stdio): %d\n", result.serversOCI)
311+
if includePyPI {
312+
fmt.Fprintf(os.Stderr, " PyPI (stdio): %d\n", result.serversPyPI)
313+
}
304314
fmt.Fprintf(os.Stderr, " Remote: %d\n", result.serversRemote)
305315
fmt.Fprintf(os.Stderr, " Skipped: %d\n", result.serversSkipped)
306316

0 commit comments

Comments
 (0)