Skip to main content

Authorization

The Registry server provides a claims-based authorization model that controls who can manage sources, registries, and entries. Authorization builds on top of authentication — you need OAuth authentication enabled before configuring authorization.

How authorization works

Authorization in the Registry server operates at two levels:

  1. Role-based access control (RBAC): Determines which admin operations a caller can perform (manage sources, manage registries, publish/delete entries).
  2. Claims-based scoping: Limits visibility and access to specific sources, registries, and entries based on key-value claims attached to both resources and callers.

When a caller makes an API request, the server:

  1. Extracts the caller's claims from their JWT token
  2. Resolves the caller's roles based on those claims and the authz configuration
  3. Checks whether the caller's role permits the operation
  4. Checks whether the caller's claims satisfy the resource's claims

Configure roles

Define roles in the auth.authz.roles section of your configuration file. Each role maps to a list of claim rules — if a caller's JWT claims match any rule in the list, they are granted that role.

config-authz.yaml
auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com
providers:
- name: keycloak
issuerUrl: https://keycloak.example.com/realms/mcp
audience: registry-api
authz:
roles:
superAdmin:
- role: 'super-admin'
manageSources:
- org: 'acme'
role: 'admin'
manageRegistries:
- org: 'acme'
role: 'admin'
manageEntries:
- role: 'writer'

Available roles

RoleGrants access to
superAdminAll operations; bypasses all claim checks
manageSourcesCreate, update, delete, and list sources via the admin API
manageRegistriesCreate, update, delete, and list registries via the admin API
manageEntriesPublish and delete MCP server versions and skills

Role rule matching

Each role is defined as a list of claim maps. A caller is granted the role if their JWT claims match any map in the list (OR logic). Within a single map, all key-value pairs must match (AND logic).

Example: grant manageSources to org admins OR platform leads
authz:
roles:
manageSources:
# Rule 1: any admin in the acme org
- org: 'acme'
role: 'admin'
# Rule 2: anyone with the platform-lead role
- role: 'platform-lead'

Claim values can be strings or arrays. When a JWT claim is an array (for example, roles: ["admin", "writer"]), the server checks whether any element matches the required value. A rule with role: "admin" would match this JWT because "admin" is one of the array elements.

Super-admin role

The superAdmin role bypasses all claim checks across the entire server. A super-admin can:

  • Access any registry regardless of its claims
  • See all entries regardless of source or entry claims
  • Manage any source or registry, even those with claims outside their JWT
  • Publish and delete entries without claim validation

Use this role sparingly and only for platform operators who need unrestricted access.

Configure claims on sources and registries

Claims are key-value pairs attached to sources and registries in your configuration file. They act as access boundaries — only callers whose JWT claims satisfy the resource's claims can access it.

Source claims

Claims on a source are inherited by all entries during sync. This means every MCP server or skill ingested from that source carries the source's claims.

config-source-claims.yaml
sources:
- name: platform-tools
format: toolhive
git:
repository: https://github.com/acme/platform-tools.git
branch: main
path: registry.json
syncPolicy:
interval: '30m'
claims:
org: 'acme'
team: 'platform'

- name: data-tools
format: toolhive
git:
repository: https://github.com/acme/data-tools.git
branch: main
path: registry.json
syncPolicy:
interval: '30m'
claims:
org: 'acme'
team: 'data'

With this configuration:

  • Entries from platform-tools are visible only to callers with org: "acme" and team: "platform" in their JWT
  • Entries from data-tools are visible only to callers with org: "acme" and team: "data" in their JWT
  • A super-admin sees all entries regardless of claims

Registry claims

Claims on a registry act as an access gate for the consumer API. Before returning any data from a registry's endpoints, the server checks that the caller's JWT claims satisfy the registry's claims.

config-registry-claims.yaml
registries:
- name: platform
sources: ['platform-tools']
claims:
org: 'acme'
team: 'platform'

- name: public
sources: ['community-tools']
# No claims — accessible to all authenticated users

A caller who requests GET /platform/v0.1/servers must have JWT claims that include org: "acme" and team: "platform". Otherwise, the server returns 403 Forbidden.

Claim containment

The server uses containment (superset check) for claim validation: the caller's claims must be a superset of the resource's claims. For example:

Resource claimsCaller JWT claimsResult
{org: "acme"}{org: "acme", team: "platform"}Allowed
{org: "acme", team: "platform"}{org: "acme"}Denied
{} (no claims){org: "acme"}Allowed
{org: "acme"}{org: "contoso"}Denied

Resources with no claims are accessible to all authenticated callers.

Claims on published entries

When you publish an MCP server version or skill to a managed source, you can attach claims to the entry. The server enforces two rules:

  1. Publish claims must be a subset of the publisher's JWT claims. You cannot publish entries with broader visibility than your own identity allows. For example, if your JWT has {org: "acme", team: "platform"}, you can publish entries with {org: "acme", team: "platform"} but not with {org: "acme"} alone (which would be visible to all teams).

  2. Subsequent versions must have the same claims as the first. Once you publish the first version of an entry with specific claims, all future versions must carry the exact same claims. This prevents accidentally narrowing or broadening visibility across versions.

Publish a server with claims
curl -X POST \
https://registry.example.com/default/v0.1/publish \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-server",
"version": "1.0.0",
"url": "https://mcp.example.com/my-server",
"description": "Team-scoped MCP server",
"claims": {
"org": "acme",
"team": "platform"
}
}'

Admin API claim scoping

When authorization is enabled, the admin API endpoints for managing sources and registries are also scoped by claims:

  • List sources/registries: Only returns resources whose claims the caller's JWT satisfies.
  • Get source/registry by name: Returns 404 Not Found (not 403) when the caller's claims don't match — this prevents information disclosure about resources the caller cannot access.
  • Create source/registry: The request claims must be a subset of the caller's JWT claims.
  • Update/delete source/registry: The caller's JWT claims must satisfy the existing resource's claims.

Anonymous mode

When authentication is set to anonymous, all authorization checks are bypassed. There are no JWT claims to validate, so all sources, registries, and entries are accessible without restriction. This is suitable for development and testing environments only.

Complete example

This example shows a multi-team setup with full RBAC and claims-based scoping:

config-multi-tenant.yaml
sources:
- name: platform-tools
format: toolhive
git:
repository: https://github.com/acme/platform-tools.git
branch: main
path: registry.json
syncPolicy:
interval: '30m'
claims:
org: 'acme'
team: 'platform'

- name: data-tools
format: toolhive
git:
repository: https://github.com/acme/data-tools.git
branch: main
path: registry.json
syncPolicy:
interval: '30m'
claims:
org: 'acme'
team: 'data'

- name: shared
managed: {}

registries:
- name: platform
sources: ['platform-tools', 'shared']
claims:
org: 'acme'
team: 'platform'

- name: data
sources: ['data-tools', 'shared']
claims:
org: 'acme'
team: 'data'

auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com
providers:
- name: keycloak
issuerUrl: https://keycloak.example.com/realms/mcp
audience: registry-api
authz:
roles:
superAdmin:
- role: 'super-admin'
manageSources:
- org: 'acme'
role: 'admin'
manageRegistries:
- org: 'acme'
role: 'admin'
manageEntries:
- role: 'writer'

With this configuration:

  • Platform team members (JWT with org: "acme", team: "platform") can access the platform registry and see entries from platform-tools and shared.
  • Data team members (JWT with org: "acme", team: "data") can access the data registry and see entries from data-tools and shared.
  • Writers (JWT with role: "writer") can publish to the shared managed source.
  • Admins (JWT with org: "acme", role: "admin") can manage sources and registries within the acme org.
  • Super-admins (JWT with role: "super-admin") can access and manage everything.

Entries published to the shared source without claims are visible through any registry that includes it, subject only to the registry-level claims gate. To restrict visibility further, attach claims when publishing entries.

Next steps