Downloading Artifacts

How to download artifacts from the registry, including presigned URLs, proxy mode, and integrity validation.

After resolving a version, the next step is downloading the actual artifact bytes. The registry supports two delivery modes depending on the storage backend: presigned URL redirects (S3) and direct proxy (filesystem).

Download Protocol

The download flow starts from the artifact URLs returned by the resolve endpoint:

  1. Client sends a GET request to the artifact URL
  2. Registry verifies authentication and authorization
  3. Registry responds with either:
    • A 302 redirect to a presigned S3 URL (S3 storage with presigning enabled)
    • The artifact bytes directly (filesystem storage or presigning disabled)
  4. Client receives the artifact and verifies its digest

Artifact Endpoints

All artifact endpoints follow the same pattern:

GET /v1/org/{org}/artifacts/{digest}/{kind}
ParameterDescription
orgOrganization slug
digestSHA-256 digest of the artifact (e.g., sha256:abc123...)
kindArtifact type: manifest, bundle, or evidence/{evidence_kind}

Downloading a Manifest

curl -H "Authorization: Bearer $TOKEN" -L \
  "https://registry.example.com/v1/org/acme/artifacts/sha256:e3b0c4429.../manifest" \
  -o manifest.json

Required scope: artifact:download

Downloading a Bundle

curl -H "Authorization: Bearer $TOKEN" -L \
  "https://registry.example.com/v1/org/acme/artifacts/sha256:a1b2c3d4.../bundle" \
  -o bundle.tar.gz

Required scope: artifact:download

Downloading Evidence

curl -H "Authorization: Bearer $TOKEN" -L \
  "https://registry.example.com/v1/org/acme/artifacts/sha256:789abc.../evidence/sbom" \
  -o sbom.json

Required scope: evidence:read

Presigned URL Redirects (S3 Storage)

When the registry uses S3 storage with presigned URLs enabled, download requests return a 302 Found redirect to a time-limited presigned URL.

How It Works

Client                          Registry                    S3 Storage
  |                                |                           |
  | GET /v1/org/.../manifest       |                           |
  | Authorization: Bearer <jwt>    |                           |
  |------------------------------->|                           |
  |                                | Verify auth + scope       |
  |                                | Generate presigned URL    |
  |                                |                           |
  | 302 Found                      |                           |
  | Location: https://s3.../...    |                           |
  |<-------------------------------|                           |
  |                                                            |
  | GET https://s3.../...?X-Amz-Signature=...                 |
  | (no Authorization header)                                  |
  |----------------------------------------------------------->|
  |                                                            |
  | 200 OK                                                     |
  | <artifact bytes>                                           |
  |<-----------------------------------------------------------|

Key points:

  • The presigned URL is time-limited (configured by presign.ttl_seconds, default: 300 seconds)
  • No authorization header is needed when fetching from the presigned URL – the signature is embedded in the URL
  • Use the -L flag with curl to follow redirects automatically

Redirect Handling in Code

If your HTTP client does not follow redirects automatically (or you need to strip the Authorization header on redirect), handle the redirect manually:

// Create a client that does not follow redirects automatically
client := &http.Client{
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse
    },
}

req, _ := http.NewRequest("GET", registryURL+artifactURL, nil)
req.Header.Set("Authorization", "Bearer "+token)

resp, err := client.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()

// Handle redirect to presigned URL
if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusTemporaryRedirect {
    redirectURL := resp.Header.Get("Location")
    // Fetch from presigned URL WITHOUT the Authorization header
    req, _ = http.NewRequest("GET", redirectURL, nil)
    resp, err = http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
}

// Read artifact bytes from resp.Body

Direct Proxy (Filesystem Storage)

When the registry uses filesystem storage (or presigned URLs are disabled), artifact bytes are streamed directly through the registry server.

Client                          Registry                    Filesystem
  |                                |                           |
  | GET /v1/org/.../bundle         |                           |
  | Authorization: Bearer <jwt>    |                           |
  |------------------------------->|                           |
  |                                | Verify auth + scope       |
  |                                | Read file from disk       |
  |                                |<------------------------->|
  |                                |                           |
  | 200 OK                         |                           |
  | Content-Type: application/...  |                           |
  | <artifact bytes>               |                           |
  |<-------------------------------|                           |

In proxy mode, the response is a standard 200 OK with the artifact bytes in the body. No redirect handling is needed.

Integrity Validation

Always verify downloaded artifacts against the expected digest from the resolution response.

Command Line

# Compute the digest of the downloaded file
COMPUTED="sha256:$(sha256sum bundle.tar.gz | awk '{print $1}')"

# Compare with the expected digest from resolution
EXPECTED="sha256:a1b2c3d4e5f6..."

if [ "$COMPUTED" != "$EXPECTED" ]; then
  echo "INTEGRITY CHECK FAILED"
  echo "Expected: $EXPECTED"
  echo "Got:      $COMPUTED"
  rm bundle.tar.gz
  exit 1
fi

echo "Integrity verified"

Go

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"
    "os"
)

func verifyDigest(filePath, expectedDigest string) error {
    f, err := os.Open(filePath)
    if err != nil {
        return fmt.Errorf("opening file: %w", err)
    }
    defer f.Close()

    h := sha256.New()
    if _, err := io.Copy(h, f); err != nil {
        return fmt.Errorf("computing digest: %w", err)
    }

    computed := "sha256:" + hex.EncodeToString(h.Sum(nil))
    if computed != expectedDigest {
        return fmt.Errorf("digest mismatch: expected %s, got %s",
            expectedDigest, computed)
    }

    return nil
}

Cache Headers

The registry sets cache-related headers on artifact responses to help clients and proxies make caching decisions.

ETag

The ETag header is set to the artifact digest:

ETag: "sha256:a1b2c3d4e5f6..."

Clients can use this for conditional requests:

curl -H "Authorization: Bearer $TOKEN" \
  -H "If-None-Match: \"sha256:a1b2c3d4e5f6...\"" \
  "https://registry.example.com/v1/org/acme/artifacts/sha256:a1b2c3d4.../bundle"

If the artifact has not changed, the response is 304 Not Modified with no body.

Cache-Control

For published artifacts (immutable content), the registry sets long cache TTLs:

Cache-Control: public, max-age=31536000, immutable

Since artifacts are content-addressed and immutable, they can be cached indefinitely – the same digest always returns the same bytes.

Public Downloads

If the registry is configured with public.download_artifacts: true, artifacts from public packages can be downloaded without authentication:

# No Authorization header needed for public packages
curl -L \
  "https://registry.example.com/v1/org/acme/artifacts/sha256:a1b2c3.../bundle" \
  -o bundle.tar.gz

Private package artifacts always require authentication, regardless of this setting.

Complete Download Example

This example shows the full flow from resolution to verified download:

#!/bin/bash
set -euo pipefail

REGISTRY_URL="https://registry.example.com"
TOKEN="your-token"
ORG="acme"
NAME="weather-service"
REF="1.0.0"
OUTPUT_DIR="./downloaded"

mkdir -p "$OUTPUT_DIR"

# 1. Resolve
echo "Resolving $ORG/$NAME@$REF..."
RESOLVED=$(curl -sf -H "Authorization: Bearer $TOKEN" \
  "$REGISTRY_URL/v1/org/$ORG/mcps/$NAME/resolve?ref=$REF")

VERSION=$(echo "$RESOLVED" | jq -r '.resolved.version')
MANIFEST_URL=$(echo "$RESOLVED" | jq -r '.resolved.manifest.url')
MANIFEST_DIGEST=$(echo "$RESOLVED" | jq -r '.resolved.manifest.digest')
BUNDLE_URL=$(echo "$RESOLVED" | jq -r '.resolved.bundle.url')
BUNDLE_DIGEST=$(echo "$RESOLVED" | jq -r '.resolved.bundle.digest')
BUNDLE_SIZE=$(echo "$RESOLVED" | jq -r '.resolved.bundle.size_bytes')

echo "Resolved to version $VERSION (bundle: $BUNDLE_SIZE bytes)"

# 2. Download manifest
echo "Downloading manifest..."
curl -sfL -H "Authorization: Bearer $TOKEN" \
  "$REGISTRY_URL$MANIFEST_URL" -o "$OUTPUT_DIR/manifest.json"

# 3. Verify manifest digest
COMPUTED="sha256:$(sha256sum "$OUTPUT_DIR/manifest.json" | awk '{print $1}')"
if [ "$COMPUTED" != "$MANIFEST_DIGEST" ]; then
  echo "Manifest digest mismatch!"
  exit 1
fi
echo "Manifest integrity verified"

# 4. Download bundle
echo "Downloading bundle..."
curl -sfL -H "Authorization: Bearer $TOKEN" \
  "$REGISTRY_URL$BUNDLE_URL" -o "$OUTPUT_DIR/bundle.tar.gz"

# 5. Verify bundle digest
COMPUTED="sha256:$(sha256sum "$OUTPUT_DIR/bundle.tar.gz" | awk '{print $1}')"
if [ "$COMPUTED" != "$BUNDLE_DIGEST" ]; then
  echo "Bundle digest mismatch!"
  exit 1
fi
echo "Bundle integrity verified"

# 6. Download evidence (if any)
EVIDENCE_COUNT=$(echo "$RESOLVED" | jq '.resolved.evidence | length')
for i in $(seq 0 $((EVIDENCE_COUNT - 1))); do
  KIND=$(echo "$RESOLVED" | jq -r ".resolved.evidence[$i].kind")
  EV_URL=$(echo "$RESOLVED" | jq -r ".resolved.evidence[$i].url")
  EV_DIGEST=$(echo "$RESOLVED" | jq -r ".resolved.evidence[$i].digest")

  echo "Downloading evidence: $KIND..."
  curl -sfL -H "Authorization: Bearer $TOKEN" \
    "$REGISTRY_URL$EV_URL" -o "$OUTPUT_DIR/$KIND.json"

  COMPUTED="sha256:$(sha256sum "$OUTPUT_DIR/$KIND.json" | awk '{print $1}')"
  if [ "$COMPUTED" != "$EV_DIGEST" ]; then
    echo "Evidence ($KIND) digest mismatch!"
    exit 1
  fi
  echo "Evidence ($KIND) integrity verified"
done

echo "All artifacts downloaded and verified successfully"

Error Responses

StatusMeaning
200Artifact bytes returned directly (proxy mode)
302Redirect to presigned URL (S3 mode)
304Not Modified (conditional request, artifact unchanged)
401Missing or invalid authentication
403Token lacks artifact:download or evidence:read scope
404Artifact not found (unknown digest or kind)