Downloading Artifacts
6 min read
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:
- Client sends a
GETrequest to the artifact URL - Registry verifies authentication and authorization
- 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)
- Client receives the artifact and verifies its digest
Artifact Endpoints
All artifact endpoints follow the same pattern:
GET /v1/org/{org}/artifacts/{digest}/{kind}
| Parameter | Description |
|---|---|
org | Organization slug |
digest | SHA-256 digest of the artifact (e.g., sha256:abc123...) |
kind | Artifact 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
-Lflag 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
Do not send the Authorization header when following the redirect to a presigned S3 URL. The presigned URL carries its own authentication via query parameters. Sending a Bearer token to S3 will cause the request to fail.
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
}
Digest verification is the primary integrity mechanism in the registry. Even though artifacts are stored by digest on the server, network issues, proxy bugs, or storage corruption could cause a mismatch. Always verify after download.
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
| Status | Meaning |
|---|---|
| 200 | Artifact bytes returned directly (proxy mode) |
| 302 | Redirect to presigned URL (S3 mode) |
| 304 | Not Modified (conditional request, artifact unchanged) |
| 401 | Missing or invalid authentication |
| 403 | Token lacks artifact:download or evidence:read scope |
| 404 | Artifact not found (unknown digest or kind) |