(ctx context.Context, pkg model.Package, _ string)
| 18 | ) |
| 19 | |
| 20 | func ValidateMCPB(ctx context.Context, pkg model.Package, _ string) error { |
| 21 | // MCPB packages must include a file hash for integrity verification |
| 22 | if pkg.FileSHA256 == "" { |
| 23 | return ErrMissingFileSHA256ForMCPB |
| 24 | } |
| 25 | |
| 26 | if pkg.Identifier == "" { |
| 27 | return ErrMissingIdentifierForMCPB |
| 28 | } |
| 29 | |
| 30 | // Validate that registryBaseUrl is not present |
| 31 | // MCPB packages use full download URLs in identifier |
| 32 | if pkg.RegistryBaseURL != "" { |
| 33 | return fmt.Errorf("MCPB packages must not have 'registryBaseUrl' field - use the full download URL in 'identifier' instead") |
| 34 | } |
| 35 | // Note: version field is optional for MCPB packages |
| 36 | // It can be included for clarity or omitted if the version is embedded in the download URL |
| 37 | |
| 38 | err := validateMCPBUrl(pkg.Identifier) |
| 39 | if err != nil { |
| 40 | return err |
| 41 | } |
| 42 | |
| 43 | // Parse the URL to validate format |
| 44 | url, err := url.Parse(pkg.Identifier) |
| 45 | if err != nil { |
| 46 | return fmt.Errorf("invalid MCPB package URL: %w", err) |
| 47 | } |
| 48 | if url.Scheme != "https" { |
| 49 | return fmt.Errorf("invalid MCPB package URL, must use HTTPS: %s", pkg.Identifier) |
| 50 | } |
| 51 | |
| 52 | // Check that the URL contains 'mcp' somewhere (case-insensitive) |
| 53 | if !strings.Contains(strings.ToLower(pkg.Identifier), "mcp") { |
| 54 | return fmt.Errorf("MCPB package URL must contain 'mcp': %s", pkg.Identifier) |
| 55 | } |
| 56 | |
| 57 | // Verify the file exists and is publicly accessible. Refuse to follow |
| 58 | // redirects: the URL allowlist (github.com / gitlab.com) only constrains |
| 59 | // the FIRST hop, and a 30x bouncing through CDN/release-asset hosts |
| 60 | // could otherwise be steered toward attacker infrastructure or |
| 61 | // internal-only endpoints reached via DNS quirks. |
| 62 | client := &http.Client{ |
| 63 | Timeout: 10 * time.Second, |
| 64 | CheckRedirect: func(_ *http.Request, _ []*http.Request) error { |
| 65 | return http.ErrUseLastResponse |
| 66 | }, |
| 67 | } |
| 68 | req, err := http.NewRequestWithContext(ctx, http.MethodHead, pkg.Identifier, nil) |
| 69 | if err != nil { |
| 70 | return fmt.Errorf("failed to create request: %w", err) |
| 71 | } |
| 72 | |
| 73 | req.Header.Set("User-Agent", "MCP-Registry-Validator/1.0") |
| 74 | |
| 75 | resp, err := client.Do(req) |
| 76 | if err != nil { |
| 77 | return fmt.Errorf("failed to verify MCPB package accessibility: %w", err) |
searching dependent graphs…