Skip to content

Commit 31e16a1

Browse files
Avish34Avish Porwal
andauthored
Extra validation for registries (#484)
<!-- Provide a brief summary of your changes --> Package identifier and version are required for some of the registries else we won't be able to pull the meta data to validate the packages. ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> This is in continuation to #424 ## How Has This Been Tested? <!-- Have you tested this in a real application? Which scenarios were tested? --> Unit test. ## 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) - [ ] 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. --> - [ ] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [ ] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions --> Co-authored-by: Avish Porwal <avishporwal@microsoft.com>
1 parent ddcc586 commit 31e16a1

File tree

10 files changed

+150
-10
lines changed

10 files changed

+150
-10
lines changed

internal/validators/registries/mcpb.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,19 @@ import (
1212
"github.com/modelcontextprotocol/registry/pkg/model"
1313
)
1414

15+
var (
16+
ErrMissingIdentifierForMCPB = fmt.Errorf("package identifier is required for MCPB packages")
17+
ErrMissingFileSHA256ForMCPB = fmt.Errorf("must include a fileSha256 hash for integrity verification")
18+
)
19+
1520
func ValidateMCPB(ctx context.Context, pkg model.Package, _ string) error {
1621
// MCPB packages must include a file hash for integrity verification
1722
if pkg.FileSHA256 == "" {
18-
return fmt.Errorf("MCPB package must include a fileSha256 hash for integrity verification")
23+
return ErrMissingFileSHA256ForMCPB
24+
}
25+
26+
if pkg.Identifier == "" {
27+
return ErrMissingIdentifierForMCPB
1928
}
2029

2130
err := validateMCPBUrl(pkg.Identifier)

internal/validators/registries/mcpb_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ func TestValidateMCPB(t *testing.T) {
2020
expectError bool
2121
errorMessage string
2222
}{
23+
{
24+
name: "empty package identifier should fail",
25+
packageName: "",
26+
serverName: "com.example/test",
27+
fileSHA256: "abc123ef4567890abcdef1234567890abcdef1234567890abcdef1234567890",
28+
expectError: true,
29+
errorMessage: "package identifier is required for MCPB packages",
30+
},
31+
{
32+
name: "empty file SHA256 should fail",
33+
packageName: "https://github.com/example/server/releases/download/v1.0.0/server.mcpb",
34+
serverName: "com.example/test",
35+
fileSHA256: "",
36+
expectError: true,
37+
errorMessage: "must include a fileSha256 hash for integrity verification",
38+
},
39+
{
40+
name: "both empty identifier and file SHA256 should fail with file SHA256 error first",
41+
packageName: "",
42+
serverName: "com.example/test",
43+
fileSHA256: "",
44+
expectError: true,
45+
errorMessage: "must include a fileSha256 hash for integrity verification",
46+
},
2347
{
2448
name: "valid MCPB package should pass",
2549
packageName: "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb",

internal/validators/registries/npm.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package registries
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/http"
89
"net/url"
@@ -11,6 +12,11 @@ import (
1112
"github.com/modelcontextprotocol/registry/pkg/model"
1213
)
1314

15+
var (
16+
ErrMissingIdentifierForNPM = errors.New("package identifier is required for NPM packages")
17+
ErrMissingVersionForNPM = errors.New("package version is required for NPM packages")
18+
)
19+
1420
// NPMPackageResponse represents the structure returned by the NPM registry API
1521
type NPMPackageResponse struct {
1622
MCPName string `json:"mcpName"`
@@ -24,15 +30,15 @@ func ValidateNPM(ctx context.Context, pkg model.Package, serverName string) erro
2430
}
2531

2632
if pkg.Identifier == "" {
27-
return fmt.Errorf("package identifier is required for NPM packages")
33+
return ErrMissingIdentifierForNPM
2834
}
2935

3036
// we need version to look up the package metadata
3137
// not providing version will return all the versions
3238
// and we won't be able to validate the mcpName field
3339
// against the server name
3440
if pkg.Version == "" {
35-
return fmt.Errorf("package version is required for NPM packages")
41+
return ErrMissingVersionForNPM
3642
}
3743

3844
// Validate that the registry base URL matches NPM exactly

internal/validators/registries/nuget.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package registries
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"io"
78
"net/http"
@@ -11,13 +12,22 @@ import (
1112
"github.com/modelcontextprotocol/registry/pkg/model"
1213
)
1314

15+
var (
16+
ErrMissingIdentifierForNuget = errors.New("package identifier is required for NuGet packages")
17+
ErrMissingVersionForNuget = errors.New("package version is required for NuGet packages")
18+
)
19+
1420
// ValidateNuGet validates that a NuGet package contains the correct MCP server name
1521
func ValidateNuGet(ctx context.Context, pkg model.Package, serverName string) error {
1622
// Set default registry base URL if empty
1723
if pkg.RegistryBaseURL == "" {
1824
pkg.RegistryBaseURL = model.RegistryURLNuGet
1925
}
2026

27+
if pkg.Identifier == "" {
28+
return ErrMissingIdentifierForNuget
29+
}
30+
2131
// Validate that the registry base URL matches NuGet exactly
2232
if pkg.RegistryBaseURL != model.RegistryURLNuGet {
2333
return fmt.Errorf("registry type and base URL do not match: '%s' is not valid for registry type '%s'. Expected: %s",
@@ -29,7 +39,7 @@ func ValidateNuGet(ctx context.Context, pkg model.Package, serverName string) er
2939
lowerID := strings.ToLower(pkg.Identifier)
3040
lowerVersion := strings.ToLower(pkg.Version)
3141
if lowerVersion == "" {
32-
return fmt.Errorf("NuGet package validation requires a specific version, but none was provided")
42+
return ErrMissingVersionForNuget
3343
}
3444

3545
// Try to get README from the package

internal/validators/registries/nuget_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ func TestValidateNuGet_RealPackages(t *testing.T) {
2020
expectError bool
2121
errorMessage string
2222
}{
23+
{
24+
name: "empty package identifier should fail",
25+
packageName: "",
26+
version: "1.0.0",
27+
serverName: "com.example/test",
28+
expectError: true,
29+
errorMessage: "package identifier is required for NuGet packages",
30+
},
31+
{
32+
name: "empty package version should fail",
33+
packageName: "test-package",
34+
version: "",
35+
serverName: "com.example/test",
36+
expectError: true,
37+
errorMessage: "package version is required for NuGet packages",
38+
},
39+
{
40+
name: "both empty identifier and version should fail with identifier error first",
41+
packageName: "",
42+
version: "",
43+
serverName: "com.example/test",
44+
expectError: true,
45+
errorMessage: "package identifier is required for NuGet packages",
46+
},
2347
{
2448
name: "non-existent package should fail",
2549
packageName: generateRandomNuGetPackageName(),
@@ -34,7 +58,7 @@ func TestValidateNuGet_RealPackages(t *testing.T) {
3458
version: "", // No version provided
3559
serverName: "com.example/test",
3660
expectError: true,
37-
errorMessage: "requires a specific version",
61+
errorMessage: "package version is required for NuGet packages",
3862
},
3963
{
4064
name: "real package with non-existent version should fail",

internal/validators/registries/oci.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import (
1313
"github.com/modelcontextprotocol/registry/pkg/model"
1414
)
1515

16+
var (
17+
ErrMissingIdentifierForOCI = errors.New("package identifier is required for OCI packages")
18+
ErrMissingVersionForOCI = errors.New("package version is required for OCI packages")
19+
)
20+
1621
const (
1722
dockerIoAPIBaseURL = "https://registry-1.docker.io"
1823
ghcrAPIBaseURL = "https://ghcr.io"
@@ -80,6 +85,15 @@ func ValidateOCI(ctx context.Context, pkg model.Package, serverName string) erro
8085
pkg.RegistryBaseURL = model.RegistryURLDocker
8186
}
8287

88+
if pkg.Identifier == "" {
89+
return ErrMissingIdentifierForOCI
90+
}
91+
92+
// we need version (tag) to look up the image manifest
93+
if pkg.Version == "" {
94+
return ErrMissingVersionForOCI
95+
}
96+
8397
// Validate that the registry base URL is supported
8498
if err := validateRegistryURL(pkg.RegistryBaseURL); err != nil {
8599
return err
@@ -258,7 +272,6 @@ func getRegistryAuthToken(ctx context.Context, client *http.Client, config *Regi
258272
return authResp.Token, nil
259273
}
260274

261-
262275
// getSpecificManifest retrieves a specific manifest for multi-arch images
263276
func getSpecificManifest(ctx context.Context, client *http.Client, registryConfig *RegistryConfig, namespace, repo, digest string) (*OCIManifest, error) {
264277
manifestURL := fmt.Sprintf("%s/v2/%s/%s/manifests/%s", registryConfig.APIBaseURL, namespace, repo, digest)

internal/validators/registries/oci_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,30 @@ func TestValidateOCI_RealPackages(t *testing.T) {
2121
errorMessage string
2222
registryURL string
2323
}{
24+
{
25+
name: "empty package identifier should fail",
26+
packageName: "",
27+
version: "latest",
28+
serverName: "com.example/test",
29+
expectError: true,
30+
errorMessage: "package identifier is required for OCI packages",
31+
},
32+
{
33+
name: "empty package version should fail",
34+
packageName: "test-image",
35+
version: "",
36+
serverName: "com.example/test",
37+
expectError: true,
38+
errorMessage: "package version is required for OCI packages",
39+
},
40+
{
41+
name: "both empty identifier and version should fail with identifier error first",
42+
packageName: "",
43+
version: "",
44+
serverName: "com.example/test",
45+
expectError: true,
46+
errorMessage: "package identifier is required for OCI packages",
47+
},
2448
{
2549
name: "non-existent image should fail",
2650
packageName: generateRandomImageName(),

internal/validators/registries/pypi.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package registries
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/http"
89
"strings"
@@ -11,6 +12,11 @@ import (
1112
"github.com/modelcontextprotocol/registry/pkg/model"
1213
)
1314

15+
var (
16+
ErrMissingIdentifierForPyPI = errors.New("package identifier is required for PyPI packages")
17+
ErrMissingVersionForPyPi = errors.New("package version is required for PyPI packages")
18+
)
19+
1420
// PyPIPackageResponse represents the structure returned by the PyPI JSON API
1521
type PyPIPackageResponse struct {
1622
Info struct {
@@ -25,6 +31,14 @@ func ValidatePyPI(ctx context.Context, pkg model.Package, serverName string) err
2531
pkg.RegistryBaseURL = model.RegistryURLPyPI
2632
}
2733

34+
if pkg.Identifier == "" {
35+
return ErrMissingIdentifierForPyPI
36+
}
37+
38+
if pkg.Version == "" {
39+
return ErrMissingVersionForPyPi
40+
}
41+
2842
// Validate that the registry base URL matches PyPI exactly
2943
if pkg.RegistryBaseURL != model.RegistryURLPyPI {
3044
return fmt.Errorf("registry type and base URL do not match: '%s' is not valid for registry type '%s'. Expected: %s",
@@ -33,7 +47,7 @@ func ValidatePyPI(ctx context.Context, pkg model.Package, serverName string) err
3347

3448
client := &http.Client{Timeout: 10 * time.Second}
3549

36-
url := fmt.Sprintf("%s/pypi/%s/json", pkg.RegistryBaseURL, pkg.Identifier)
50+
url := fmt.Sprintf("%s/pypi/%s/%s/json", pkg.RegistryBaseURL, pkg.Identifier, pkg.Version)
3751
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
3852
if err != nil {
3953
return fmt.Errorf("failed to create request: %w", err)

internal/validators/registries/pypi_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ func TestValidatePyPI_RealPackages(t *testing.T) {
2020
expectError bool
2121
errorMessage string
2222
}{
23+
{
24+
name: "empty package identifier should fail",
25+
packageName: "",
26+
version: "1.0.0",
27+
serverName: "com.example/test",
28+
expectError: true,
29+
errorMessage: "package identifier is required for PyPI packages",
30+
},
31+
{
32+
name: "empty package version should fail",
33+
packageName: "mcp-server-example",
34+
version: "",
35+
serverName: "com.example/test",
36+
expectError: true,
37+
errorMessage: "package version is required for PyPI packages",
38+
},
2339
{
2440
name: "non-existent package should fail",
2541
packageName: generateRandomPackageName(),
@@ -47,7 +63,7 @@ func TestValidatePyPI_RealPackages(t *testing.T) {
4763
{
4864
name: "real package with server name in README should pass",
4965
packageName: "time-mcp-pypi",
50-
version: "1.0.0",
66+
version: "1.0.6",
5167
serverName: "io.github.domdomegg/time-mcp-pypi",
5268
expectError: false,
5369
},
@@ -71,4 +87,4 @@ func TestValidatePyPI_RealPackages(t *testing.T) {
7187
}
7288
})
7389
}
74-
}
90+
}

internal/validators/registries/testutils_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ func generateRandomImageName() string {
2929
return "nonexistent-image-fallback"
3030
}
3131
return fmt.Sprintf("nonexistent-image-%s", hex.EncodeToString(bytes)[:16])
32-
}
32+
}

0 commit comments

Comments
 (0)