API Endpoints
9 min read
All API endpoints are prefixed with /v1/ (except the health check). Request and response bodies use JSON. Authentication is provided via the Authorization header.
Base URL
http://localhost:8080
In production, use HTTPS with your actual hostname.
Authentication
The registry supports three authentication methods:
| Method | Header Format | Notes |
|---|---|---|
| Bearer JWT | Authorization: Bearer <jwt> | Primary method for both OSS and Enterprise modes |
| API Token | Authorization: Token <id>:<secret> | Long-lived tokens for CI/CD |
| Basic Auth | Authorization: Basic <base64> | OSS mode only, when enable_basic is true |
Error Format
All errors return a JSON object:
{
"error": {
"code": "not_found",
"message": "Package acme/unknown not found",
"details": {}
}
}
Common Error Codes
| HTTP Status | Code | Description |
|---|---|---|
| 400 | bad_request | Invalid request format or missing fields |
| 401 | unauthorized | Missing or invalid authentication |
| 403 | forbidden | Authenticated but lacking required scope or resource |
| 404 | not_found | Resource does not exist |
| 409 | conflict | Resource already exists (e.g., duplicate version) |
| 501 | not_implemented | Feature not available (e.g., login in enterprise mode) |
System
Health Check
Check server health status. No authentication required.
GET /healthz
Response 200 OK:
{
"status": "ok"
}
Authentication
Login (OSS Mode Only)
Obtain a JWT using username and password. Only available when auth.mode is oss.
POST /v1/auth/login
Request:
{
"username": "admin",
"password": "secret"
}
Response 200 OK:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900
}
Example:
curl -X POST http://localhost:8080/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret"}'
Errors:
| Status | When |
|---|---|
| 401 | Invalid credentials |
| 501 | Enterprise mode (login not available) |
Catalog
List Packages
List all packages visible to the caller.
GET /v1/catalog
GET /v1/catalog?org={org}
Required scope: mcp:catalog:read
Parameters:
| Name | Location | Required | Description |
|---|---|---|---|
org | Query | No | Filter by organization |
Response 200 OK:
{
"packages": [
{
"id": "acme/weather-service",
"org_id": "acme",
"name": "weather-service",
"visibility": "public",
"description": "Weather data service",
"tags": ["weather", "api"]
}
]
}
Example:
# List all packages
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/v1/catalog
# Filter by organization
curl -H "Authorization: Bearer $TOKEN" "http://localhost:8080/v1/catalog?org=acme"
Notes:
- If
public.read_catalogistrue, unauthenticated requests return only public packages - Authenticated requests return all packages the token has access to
Packages
Get Package Metadata
Get metadata for a specific package.
GET /v1/org/{org}/mcps/{name}
Required scope: mcp:catalog:read
Path parameters:
| Name | Description |
|---|---|
org | Organization slug |
name | Package name |
Response 200 OK:
{
"id": "acme/weather-service",
"org_id": "acme",
"name": "weather-service",
"visibility": "public",
"description": "Weather data service",
"tags": ["weather", "api"],
"default_policy_ref": null
}
Example:
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/v1/org/acme/mcps/weather-service
Errors:
| Status | When |
|---|---|
| 404 | Package does not exist |
Versions
List Versions
List all versions of a package.
GET /v1/org/{org}/mcps/{name}/versions
Required scope: mcp:catalog:read
Response 200 OK:
{
"versions": [
{
"version": "1.0.0",
"status": "published",
"created_at": "2024-01-15T10:30:00Z",
"git_sha": "abc123def456"
},
{
"version": "1.1.0",
"status": "published",
"created_at": "2024-02-01T14:00:00Z",
"git_sha": "789xyz012abc"
}
]
}
Example:
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/v1/org/acme/mcps/weather-service/versions
Resolve Version
Map a reference to a specific version with artifact URLs.
GET /v1/org/{org}/mcps/{name}/resolve?ref={ref}
Required scope: mcp:resolve for published versions; mcp:resolve:prepublish for draft/ingested.
Parameters:
| Name | Location | Required | Description |
|---|---|---|---|
ref | Query | Yes | Version reference: semver (1.0.0), git SHA, manifest digest, or bundle digest |
Response 200 OK:
{
"package": "acme/weather-service",
"ref": "1.0.0",
"resolved": {
"version": "1.0.0",
"status": "published",
"git_sha": "abc123def456",
"repo_url": "https://github.com/acme/weather-service",
"manifest": {
"digest": "sha256:abc123...",
"url": "/v1/org/acme/artifacts/sha256:abc123.../manifest"
},
"bundle": {
"digest": "sha256:def456...",
"url": "/v1/org/acme/artifacts/sha256:def456.../bundle",
"size_bytes": 1048576
},
"evidence": [
{
"kind": "sbom",
"digest": "sha256:789xyz...",
"url": "/v1/org/acme/artifacts/sha256:789xyz.../evidence/sbom"
}
]
}
}
Example:
# Resolve by version
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/v1/org/acme/mcps/weather-service/resolve?ref=1.0.0"
# Resolve by git SHA
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/v1/org/acme/mcps/weather-service/resolve?ref=abc123def456"
Errors:
| Status | When |
|---|---|
| 404 | No version matches the ref |
Publish Version
Publish a new version of a package.
POST /v1/org/{org}/mcps/{name}/publish
Required scope: mcp:publish
Request:
{
"version": "1.0.0",
"bundle_digest": "sha256:def456...",
"bundle_size_bytes": 1048576,
"manifest_json": {
"schema_version": 1,
"package": {
"id": "acme/weather-service",
"version": "1.0.0"
},
"runtime": {
"type": "node",
"version": ">=18.0.0"
},
"entrypoint": {
"command": ["node", "dist/index.js"]
}
},
"git_sha": "abc123def456",
"repo_url": "https://github.com/acme/weather-service",
"repo_visibility": "public",
"repo_provider": "github",
"repo_ref": "v1.0.0",
"repo_commit": "abc123def456",
"certification_level": 0,
"evidence_digests": []
}
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
version | string | Yes | Semantic version string |
bundle_digest | string | Yes | SHA-256 digest of the bundle |
bundle_size_bytes | integer | Yes | Size of the bundle in bytes |
manifest_json | object | Yes | Inline manifest JSON |
git_sha | string | Yes | Source commit hash |
repo_url | string | Yes | Source repository URL |
repo_visibility | string | Yes | public or private |
repo_provider | string | Yes | github, gitlab, bitbucket |
repo_ref | string | Yes | Git ref (tag or branch) |
repo_commit | string | Yes | Full commit hash |
certification_level | integer | No | Certification level (0-3) |
evidence_digests | []string | No | Evidence artifact digests |
Response 200 OK:
{
"version": "1.0.0",
"status": "ingested",
"bundle_upload": {
"url": "https://s3.amazonaws.com/bucket/key?X-Amz-Signature=...",
"method": "PUT",
"headers": {
"Content-Type": "application/octet-stream"
},
"expires_in": 300
}
}
The bundle_upload field contains a presigned URL for uploading the bundle. If presigned URLs are disabled, this field is null and you should upload via the proxy endpoint.
Example:
curl -X POST http://localhost:8080/v1/org/acme/mcps/weather-service/publish \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"version": "1.0.0",
"bundle_digest": "sha256:def456...",
"bundle_size_bytes": 1048576,
"manifest_json": {"schema_version":1, "package":{"id":"acme/weather-service","version":"1.0.0"}, "runtime":{"type":"node","version":">=18.0.0"}, "entrypoint":{"command":["node","dist/index.js"]}},
"git_sha": "abc123",
"repo_url": "https://github.com/acme/weather-service",
"repo_visibility": "public",
"repo_provider": "github",
"repo_ref": "v1.0.0",
"repo_commit": "abc123"
}'
Errors:
| Status | When |
|---|---|
| 400 | Manifest validation failed or package ID mismatch |
| 409 | Version already exists |
Update Version Status
Change the lifecycle status of a version.
POST /v1/org/{org}/mcps/{name}/versions/{version}/status
Required scope: mcp:publish
Path parameters:
| Name | Description |
|---|---|
org | Organization slug |
name | Package name |
version | Semantic version string |
Request:
{
"status": "published"
}
Valid status values: published, revoked
Response 200 OK:
{
"version": "1.0.0",
"status": "published"
}
Example:
curl -X POST \
http://localhost:8080/v1/org/acme/mcps/weather-service/versions/1.0.0/status \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "published"}'
Artifacts
Download Artifact
Download a manifest, bundle, or evidence artifact.
GET /v1/org/{org}/artifacts/{digest}/manifest
GET /v1/org/{org}/artifacts/{digest}/bundle
GET /v1/org/{org}/artifacts/{digest}/evidence/{kind}
Required scope: artifact:download for manifest and bundle; evidence:read for evidence.
Path parameters:
| Name | Description |
|---|---|
org | Organization slug |
digest | SHA-256 digest (e.g., sha256:abc123...) |
kind | Evidence type for evidence endpoints (e.g., sbom, attestation) |
Response:
302 FoundwithLocationheader pointing to presigned URL (S3 storage)200 OKwith artifact bytes in body (filesystem storage)
Examples:
# Download manifest (follow redirects with -L)
curl -H "Authorization: Bearer $TOKEN" -L \
http://localhost:8080/v1/org/acme/artifacts/sha256:abc123.../manifest \
-o manifest.json
# Download bundle
curl -H "Authorization: Bearer $TOKEN" -L \
http://localhost:8080/v1/org/acme/artifacts/sha256:def456.../bundle \
-o bundle.tar.gz
# Download evidence
curl -H "Authorization: Bearer $TOKEN" -L \
http://localhost:8080/v1/org/acme/artifacts/sha256:789xyz.../evidence/sbom \
-o sbom.json
Upload Artifact
Upload a manifest, bundle, or evidence artifact through the registry proxy.
PUT /v1/org/{org}/artifacts/{digest}/manifest
PUT /v1/org/{org}/artifacts/{digest}/bundle
PUT /v1/org/{org}/artifacts/{digest}/evidence/{kind}
Required scope: mcp:publish
Request: Raw artifact bytes in the body.
Headers:
| Header | Value |
|---|---|
Content-Type | application/json for manifests and evidence; application/octet-stream for bundles |
Response 200 OK on success.
Examples:
# Upload manifest
curl -X PUT http://localhost:8080/v1/org/acme/artifacts/sha256:abc123.../manifest \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @manifest.json
# Upload bundle
curl -X PUT http://localhost:8080/v1/org/acme/artifacts/sha256:def456.../bundle \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @bundle.tar.gz
# Upload evidence
curl -X PUT http://localhost:8080/v1/org/acme/artifacts/sha256:789xyz.../evidence/sbom \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @sbom.json
Request Presigned Upload URL
Request a time-limited presigned URL for direct upload to S3. Only available when storage.type is s3 and presign.enabled is true.
POST /v1/org/{org}/artifacts/{digest}/presign?kind={kind}
Required scope: mcp:publish
Parameters:
| Name | Location | Required | Description |
|---|---|---|---|
kind | Query | Yes | Artifact kind: manifest, bundle, or evidence |
Response 200 OK:
{
"url": "https://s3.amazonaws.com/bucket/key?X-Amz-Signature=...",
"method": "PUT",
"headers": {
"Content-Type": "application/octet-stream"
},
"expires_in": 300
}
Example:
curl -X POST \
"http://localhost:8080/v1/org/acme/artifacts/sha256:def456.../presign?kind=bundle" \
-H "Authorization: Bearer $TOKEN"
API Tokens
Create Token
Create a new API token for CI/CD or service account use.
POST /v1/tokens
Required scope: token:create
Request:
{
"description": "GitHub Actions - weather-service",
"scopes": ["mcp:publish", "mcp:resolve"],
"resources": ["org/acme/mcp/weather-service"],
"expires_in": 2592000
}
| Field | Type | Required | Description |
|---|---|---|---|
description | string | Yes | Human-readable description |
scopes | []string | Yes | Permission scopes to grant |
resources | []string | Yes | Resource patterns accessible |
expires_in | integer | No | Expiration in seconds (default: 30 days / 2592000) |
Response 201 Created:
{
"token_id": "mcp_abc123def456",
"secret": "sk_fedcba654321...",
"expires_at": "2024-03-15T10:30:00Z"
}
The secret is only returned once at creation time. Store it securely.
Example:
curl -X POST http://localhost:8080/v1/tokens \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"description": "GitHub Actions",
"scopes": ["mcp:publish"],
"resources": ["org/acme/mcp/weather-service"]
}'
List Tokens
List all API tokens for the current user.
GET /v1/tokens
Required scope: token:list
Response 200 OK:
{
"tokens": [
{
"token_id": "mcp_abc123def456",
"description": "GitHub Actions",
"scopes": ["mcp:publish"],
"resources": ["org/acme/mcp/weather-service"],
"expires_at": "2024-03-15T10:30:00Z"
}
]
}
Example:
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/v1/tokens
Delete Token
Revoke and delete an API token.
DELETE /v1/tokens/{token_id}
Required scope: token:delete
Response 204 No Content on success.
Example:
curl -X DELETE http://localhost:8080/v1/tokens/mcp_abc123def456 \
-H "Authorization: Bearer $TOKEN"
Organization Members
List Members
List all members of an organization.
GET /v1/org/{org}/members
Authentication: Required
Response 200 OK:
{
"members": [
{
"username": "alice",
"org_id": "acme",
"role": "admin"
},
{
"username": "bob",
"org_id": "acme",
"role": "member"
}
]
}
Example:
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/v1/org/acme/members
Add Member
Add a user to an organization.
POST /v1/org/{org}/members
Authentication: Required (admin role)
Request:
{
"username": "bob",
"role": "member"
}
| Field | Type | Required | Description |
|---|---|---|---|
username | string | Yes | Username to add |
role | string | Yes | Role: admin or member |
Response 201 Created on success.
Example:
curl -X POST http://localhost:8080/v1/org/acme/members \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username":"bob","role":"member"}'
Remove Member
Remove a user from an organization.
DELETE /v1/org/{org}/members/{username}
Authentication: Required (admin role)
Response 204 No Content on success.
Example:
curl -X DELETE http://localhost:8080/v1/org/acme/members/bob \
-H "Authorization: Bearer $TOKEN"
Complete Workflow Examples
Resolve and Download
# 1. Login (OSS mode)
TOKEN=$(curl -s -X POST http://localhost:8080/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret"}' | jq -r .access_token)
# 2. Resolve package
RESOLVED=$(curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/v1/org/acme/mcps/weather-service/resolve?ref=1.0.0")
# 3. Extract URLs
MANIFEST_URL=$(echo "$RESOLVED" | jq -r '.resolved.manifest.url')
BUNDLE_URL=$(echo "$RESOLVED" | jq -r '.resolved.bundle.url')
# 4. Download artifacts
curl -H "Authorization: Bearer $TOKEN" -L \
"http://localhost:8080$MANIFEST_URL" -o manifest.json
curl -H "Authorization: Bearer $TOKEN" -L \
"http://localhost:8080$BUNDLE_URL" -o bundle.tar.gz
Publish from CI/CD Pipeline
# 1. Compute digests
BUNDLE_DIGEST="sha256:$(sha256sum bundle.tar.gz | awk '{print $1}')"
BUNDLE_SIZE=$(stat -c%s bundle.tar.gz)
# 2. Initiate publish
RESPONSE=$(curl -s -X POST \
http://localhost:8080/v1/org/acme/mcps/weather-service/publish \
-H "Authorization: Token $TOKEN_ID:$TOKEN_SECRET" \
-H "Content-Type: application/json" \
-d "{
\"version\": \"1.0.0\",
\"bundle_digest\": \"$BUNDLE_DIGEST\",
\"bundle_size_bytes\": $BUNDLE_SIZE,
\"manifest_json\": $(cat manifest.json),
\"git_sha\": \"$GITHUB_SHA\",
\"repo_url\": \"https://github.com/$GITHUB_REPOSITORY\",
\"repo_visibility\": \"public\",
\"repo_provider\": \"github\",
\"repo_ref\": \"$GITHUB_REF\",
\"repo_commit\": \"$GITHUB_SHA\"
}")
# 3. Upload bundle to presigned URL
UPLOAD_URL=$(echo "$RESPONSE" | jq -r '.bundle_upload.url')
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/octet-stream" \
--data-binary @bundle.tar.gz
# 4. Mark as published
curl -X POST \
http://localhost:8080/v1/org/acme/mcps/weather-service/versions/1.0.0/status \
-H "Authorization: Token $TOKEN_ID:$TOKEN_SECRET" \
-H "Content-Type: application/json" \
-d '{"status": "published"}'
Rate Limits and Timeouts
- The registry does not implement rate limiting. Use a reverse proxy (nginx, Caddy, cloud load balancer) for rate limiting in production.
- All requests have a 30-second timeout. For large artifact uploads, use presigned URLs for direct S3 access to avoid this limit.