<!-- Provide a brief summary of your changes --> ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> The following PR addresses the feedback from #634 around: * Use a proper library - https://github.com/distribution/reference, instead of the implementing the OCI parsing ourselves (note: left the tests so we can ensure we are not breaking any existing behavior) * Updating one of the error messages to now assume MCPB. ## How Has This Been Tested? <!-- Have you tested this in a real application? Which scenarios were tested? --> ## Breaking Changes <!-- Will users need to update their code or configurations? --> ## 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 --> Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
90 lines
2.8 KiB
Go
90 lines
2.8 KiB
Go
package registries
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/modelcontextprotocol/registry/pkg/model"
|
|
)
|
|
|
|
var (
|
|
ErrMissingIdentifierForNPM = errors.New("package identifier is required for NPM packages")
|
|
ErrMissingVersionForNPM = errors.New("package version is required for NPM packages")
|
|
)
|
|
|
|
// NPMPackageResponse represents the structure returned by the NPM registry API
|
|
type NPMPackageResponse struct {
|
|
MCPName string `json:"mcpName"`
|
|
}
|
|
|
|
// ValidateNPM validates that an NPM package contains the correct MCP server name
|
|
func ValidateNPM(ctx context.Context, pkg model.Package, serverName string) error {
|
|
// Set default registry base URL if empty
|
|
if pkg.RegistryBaseURL == "" {
|
|
pkg.RegistryBaseURL = model.RegistryURLNPM
|
|
}
|
|
|
|
if pkg.Identifier == "" {
|
|
return ErrMissingIdentifierForNPM
|
|
}
|
|
|
|
// we need version to look up the package metadata
|
|
// not providing version will return all the versions
|
|
// and we won't be able to validate the mcpName field
|
|
// against the server name
|
|
if pkg.Version == "" {
|
|
return ErrMissingVersionForNPM
|
|
}
|
|
|
|
// Validate that MCPB-specific fields are not present
|
|
if pkg.FileSHA256 != "" {
|
|
return fmt.Errorf("NPM packages must not have 'fileSha256' field")
|
|
}
|
|
|
|
// Validate that the registry base URL matches NPM exactly
|
|
if pkg.RegistryBaseURL != model.RegistryURLNPM {
|
|
return fmt.Errorf("registry type and base URL do not match: '%s' is not valid for registry type '%s'. Expected: %s",
|
|
pkg.RegistryBaseURL, model.RegistryTypeNPM, model.RegistryURLNPM)
|
|
}
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
requestURL := pkg.RegistryBaseURL + "/" + url.PathEscape(pkg.Identifier) + "/" + url.PathEscape(pkg.Version)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "MCP-Registry-Validator/1.0")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch package metadata from NPM: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("NPM package '%s' not found (status: %d)", pkg.Identifier, resp.StatusCode)
|
|
}
|
|
|
|
var npmResp NPMPackageResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&npmResp); err != nil {
|
|
return fmt.Errorf("failed to parse NPM package metadata: %w", err)
|
|
}
|
|
|
|
if npmResp.MCPName == "" {
|
|
return fmt.Errorf("NPM package '%s' is missing required 'mcpName' field. Add this to your package.json: \"mcpName\": \"%s\"", pkg.Identifier, serverName)
|
|
}
|
|
|
|
if npmResp.MCPName != serverName {
|
|
return fmt.Errorf("NPM package ownership validation failed. Expected mcpName '%s', got '%s'", serverName, npmResp.MCPName)
|
|
}
|
|
|
|
return nil
|
|
}
|