registry/internal/validators/registries/oci_test.go
Joel Verhagen 2ca4ae9977
Add support for ACR (Azure Container Registry) and MCR (Microsoft Container Registry) (#754)
<!-- Provide a brief summary of your changes -->

## Motivation and Context

Microsoft uses `mcr.microsoft.com` to host our public containers. These
are syndicated to Docker Hub but image references use
`mcr.microsoft.com` which is not currently supported by the Official MCP
Registry.

See https://github.com/modelcontextprotocol/registry/issues/753.

Microsoft teams want to use MCR-based images for Microsoft MCP servers.
I believe we should be adding ACR support as well, so that our customers
using Azure Container Registry can use the MCP Registry as well.

In short, MCR is used for Microsoft managed containers. ACR is a
solution for Azure customers to host their own containers.

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

Unit tests.

Manual test: local push of a server.json with
`mcr.microsoft.com/azure-sdk/azure-mcp:2.0.0-beta.1` and
`azurearcjumpstart.azurecr.io/hello-arc:latest` in it. The error is now
about missing annotation not unsupported OCI registry.

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

No.

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->
2025-11-18 09:58:10 +02:00

244 lines
6.6 KiB
Go

package registries_test
import (
"context"
"testing"
"github.com/modelcontextprotocol/registry/internal/validators/registries"
"github.com/modelcontextprotocol/registry/pkg/model"
"github.com/stretchr/testify/assert"
)
func TestValidateOCI_RegistryAllowlist(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
identifier string
expectError bool
errorMsg string
}{
// Allowed registries - use real public images that exist
// These should fail with "missing required annotation" (no MCP label)
// NOT with "unsupported registry", "does not exist", or "is private" errors
{
name: "Docker Hub should be allowed",
identifier: "docker.io/library/alpine:latest",
expectError: true,
errorMsg: "missing required annotation",
},
{
name: "Docker Hub without explicit registry should default and be allowed",
identifier: "library/hello-world:latest",
expectError: true,
errorMsg: "missing required annotation",
},
{
name: "GHCR should be allowed",
identifier: "ghcr.io/containerbase/base:latest",
expectError: true,
errorMsg: "missing required annotation",
},
{
name: "Artifact Registry regional should be allowed",
identifier: "us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:latest",
expectError: true,
errorMsg: "missing required annotation",
},
{
name: "Artifact Registry multi-region should be allowed",
identifier: "us-docker.pkg.dev/berglas/berglas/berglas:latest",
expectError: true,
errorMsg: "missing required annotation",
},
{
name: "MCR should be allowed",
identifier: "mcr.microsoft.com/dotnet/aspire-dashboard:9.5.0",
expectError: true,
errorMsg: "missing required annotation",
},
{
name: "ACR should be allowed",
identifier: "azurearcjumpstart.azurecr.io/hello-arc:latest",
expectError: true,
errorMsg: "missing required annotation",
},
// Disallowed registries
{
name: "GCR should be rejected",
identifier: "gcr.io/test/image:latest",
expectError: true,
errorMsg: "unsupported OCI registry",
},
{
name: "Quay.io should be rejected",
identifier: "quay.io/test/image:latest",
expectError: true,
errorMsg: "unsupported OCI registry",
},
{
name: "ECR Public should be rejected",
identifier: "public.ecr.aws/test/image:latest",
expectError: true,
errorMsg: "unsupported OCI registry",
},
{
name: "GitLab registry should be rejected",
identifier: "registry.gitlab.com/test/image:latest",
expectError: true,
errorMsg: "unsupported OCI registry",
},
{
name: "Custom registry should be rejected",
identifier: "custom-registry.com/test/image:latest",
expectError: true,
errorMsg: "unsupported OCI registry",
},
{
name: "Harbor registry should be rejected",
identifier: "harbor.example.com/test/image:latest",
expectError: true,
errorMsg: "unsupported OCI registry",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pkg := model.Package{
RegistryType: model.RegistryTypeOCI,
Identifier: tt.identifier,
}
err := registries.ValidateOCI(ctx, pkg, "com.example/test")
if tt.expectError {
assert.Error(t, err)
// Should contain the specific error message
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateOCI_RejectsOldFormat(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
pkg model.Package
errorMessage string
}{
{
name: "OCI package with registryBaseUrl should be rejected",
pkg: model.Package{
RegistryType: model.RegistryTypeOCI,
RegistryBaseURL: "https://docker.io",
Identifier: "docker.io/test/image:latest",
},
errorMessage: "OCI packages must not have 'registryBaseUrl' field",
},
{
name: "OCI package with version field should be rejected",
pkg: model.Package{
RegistryType: model.RegistryTypeOCI,
Identifier: "docker.io/test/image:latest",
Version: "1.0.0",
},
errorMessage: "OCI packages must not have 'version' field",
},
{
name: "OCI package with fileSha256 field should be rejected",
pkg: model.Package{
RegistryType: model.RegistryTypeOCI,
Identifier: "docker.io/test/image:latest",
FileSHA256: "abcd1234",
},
errorMessage: "OCI packages must not have 'fileSha256' field",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := registries.ValidateOCI(ctx, tt.pkg, "com.example/test")
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMessage)
})
}
}
func TestValidateOCI_InvalidReferences(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
identifier string
}{
{
name: "invalid characters in reference",
identifier: "docker.io/test/image:INVALID SPACE",
},
{
name: "malformed reference",
identifier: "not-a-valid-reference::::",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pkg := model.Package{
RegistryType: model.RegistryTypeOCI,
Identifier: tt.identifier,
}
err := registries.ValidateOCI(ctx, pkg, "com.example/test")
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid OCI reference")
})
}
}
func TestValidateOCI_EmptyIdentifier(t *testing.T) {
ctx := context.Background()
pkg := model.Package{
RegistryType: model.RegistryTypeOCI,
Identifier: "",
}
err := registries.ValidateOCI(ctx, pkg, "com.example/test")
assert.Error(t, err)
assert.Contains(t, err.Error(), "package identifier is required")
}
func TestValidateOCI_SuccessfulValidation(t *testing.T) {
ctx := context.Background()
// Test with a real MCP server image that has the correct label
pkg := model.Package{
RegistryType: model.RegistryTypeOCI,
Identifier: "ghcr.io/github/github-mcp-server:latest",
}
err := registries.ValidateOCI(ctx, pkg, "io.github.github/github-mcp-server")
assert.NoError(t, err)
}
func TestValidateOCI_LabelMismatch(t *testing.T) {
ctx := context.Background()
// Test with a real MCP server image but wrong expected server name
// This should fail because the label doesn't match
pkg := model.Package{
RegistryType: model.RegistryTypeOCI,
Identifier: "ghcr.io/github/github-mcp-server:latest",
}
err := registries.ValidateOCI(ctx, pkg, "io.github.github/github-mcp-server-mismatch")
assert.Error(t, err)
assert.Contains(t, err.Error(), "ownership validation failed")
assert.Contains(t, err.Error(), "Expected annotation")
}