|
| 1 | +--- |
| 2 | +title: Authorization |
| 3 | +description: |
| 4 | + How to configure role-based access control and claims-based authorization for |
| 5 | + the Registry Server |
| 6 | +--- |
| 7 | + |
| 8 | +The Registry server provides a claims-based authorization model that controls |
| 9 | +who can manage sources, registries, and entries. Authorization builds on top of |
| 10 | +[authentication](./authentication.mdx) — you need OAuth authentication enabled |
| 11 | +before configuring authorization. |
| 12 | + |
| 13 | +## How authorization works |
| 14 | + |
| 15 | +Authorization in the Registry server operates at two levels: |
| 16 | + |
| 17 | +1. **Role-based access control (RBAC)**: Determines which admin operations a |
| 18 | + caller can perform (manage sources, manage registries, publish/delete |
| 19 | + entries). |
| 20 | +2. **Claims-based scoping**: Limits visibility and access to specific sources, |
| 21 | + registries, and entries based on key-value claims attached to both resources |
| 22 | + and callers. |
| 23 | + |
| 24 | +When a caller makes an API request, the server: |
| 25 | + |
| 26 | +1. Extracts the caller's claims from their JWT token |
| 27 | +2. Resolves the caller's roles based on those claims and the `authz` |
| 28 | + configuration |
| 29 | +3. Checks whether the caller's role permits the operation |
| 30 | +4. Checks whether the caller's claims satisfy the resource's claims |
| 31 | + |
| 32 | +```mermaid |
| 33 | +flowchart LR |
| 34 | + JWT["JWT token"] --> Claims["Extract claims"] |
| 35 | + Claims --> Roles["Resolve roles"] |
| 36 | + Roles --> RoleCheck{"Role\npermitted?"} |
| 37 | + RoleCheck -->|Yes| ClaimCheck{"Claims\nsatisfied?"} |
| 38 | + RoleCheck -->|No| Deny403["403 Forbidden"] |
| 39 | + ClaimCheck -->|Yes| Allow["Allow"] |
| 40 | + ClaimCheck -->|No| Deny403 |
| 41 | +``` |
| 42 | + |
| 43 | +## Configure roles |
| 44 | + |
| 45 | +Define roles in the `auth.authz.roles` section of your configuration file. Each |
| 46 | +role maps to a list of claim rules — if a caller's JWT claims match any rule in |
| 47 | +the list, they are granted that role. |
| 48 | + |
| 49 | +```yaml title="config-authz.yaml" |
| 50 | +auth: |
| 51 | + mode: oauth |
| 52 | + oauth: |
| 53 | + resourceUrl: https://registry.example.com |
| 54 | + providers: |
| 55 | + - name: keycloak |
| 56 | + issuerUrl: https://keycloak.example.com/realms/mcp |
| 57 | + audience: registry-api |
| 58 | + # highlight-start |
| 59 | + authz: |
| 60 | + roles: |
| 61 | + superAdmin: |
| 62 | + - role: 'super-admin' |
| 63 | + manageSources: |
| 64 | + - org: 'acme' |
| 65 | + role: 'admin' |
| 66 | + manageRegistries: |
| 67 | + - org: 'acme' |
| 68 | + role: 'admin' |
| 69 | + manageEntries: |
| 70 | + - role: 'writer' |
| 71 | + # highlight-end |
| 72 | +``` |
| 73 | + |
| 74 | +### Available roles |
| 75 | + |
| 76 | +| Role | Grants access to | |
| 77 | +| ------------------ | ------------------------------------------------------------- | |
| 78 | +| `superAdmin` | All operations; bypasses all claim checks | |
| 79 | +| `manageSources` | Create, update, delete, and list sources via the admin API | |
| 80 | +| `manageRegistries` | Create, update, delete, and list registries via the admin API | |
| 81 | +| `manageEntries` | Publish and delete MCP server versions and skills | |
| 82 | + |
| 83 | +### Role rule matching |
| 84 | + |
| 85 | +Each role is defined as a list of claim maps. A caller is granted the role if |
| 86 | +their JWT claims match **any** map in the list (OR logic). Within a single map, |
| 87 | +**all** key-value pairs must match (AND logic). |
| 88 | + |
| 89 | +```yaml title="Example: grant manageSources to org admins OR platform leads" |
| 90 | +authz: |
| 91 | + roles: |
| 92 | + manageSources: |
| 93 | + # Rule 1: any admin in the acme org |
| 94 | + - org: 'acme' |
| 95 | + role: 'admin' |
| 96 | + # Rule 2: anyone with the platform-lead role |
| 97 | + - role: 'platform-lead' |
| 98 | +``` |
| 99 | +
|
| 100 | +Claim values can be strings or arrays. When a JWT claim is an array (for |
| 101 | +example, `roles: ["admin", "writer"]`), the server checks whether any element |
| 102 | +matches the required value. A rule with `role: "admin"` would match this JWT |
| 103 | +because `"admin"` is one of the array elements. |
| 104 | +
|
| 105 | +### Super-admin role |
| 106 | +
|
| 107 | +The `superAdmin` role bypasses **all** claim checks across the entire server. A |
| 108 | +super-admin can: |
| 109 | + |
| 110 | +- Access any registry regardless of its claims |
| 111 | +- See all entries regardless of source or entry claims |
| 112 | +- Manage any source or registry, even those with claims outside their JWT |
| 113 | +- Publish and delete entries without claim validation |
| 114 | + |
| 115 | +Use this role sparingly and only for platform operators who need unrestricted |
| 116 | +access. |
| 117 | + |
| 118 | +## Configure claims on sources and registries |
| 119 | + |
| 120 | +Claims are key-value pairs attached to sources and registries in your |
| 121 | +configuration file. They act as access boundaries — only callers whose JWT |
| 122 | +claims satisfy the resource's claims can access it. |
| 123 | + |
| 124 | +### Source claims |
| 125 | + |
| 126 | +Claims on a source are **inherited by all entries** during sync. This means |
| 127 | +every MCP server or skill ingested from that source carries the source's claims. |
| 128 | + |
| 129 | +```yaml title="config-source-claims.yaml" |
| 130 | +sources: |
| 131 | + - name: platform-tools |
| 132 | + format: toolhive |
| 133 | + git: |
| 134 | + repository: https://github.com/acme/platform-tools.git |
| 135 | + branch: main |
| 136 | + path: registry.json |
| 137 | + syncPolicy: |
| 138 | + interval: '30m' |
| 139 | + # highlight-start |
| 140 | + claims: |
| 141 | + org: 'acme' |
| 142 | + team: 'platform' |
| 143 | + # highlight-end |
| 144 | +
|
| 145 | + - name: data-tools |
| 146 | + format: toolhive |
| 147 | + git: |
| 148 | + repository: https://github.com/acme/data-tools.git |
| 149 | + branch: main |
| 150 | + path: registry.json |
| 151 | + syncPolicy: |
| 152 | + interval: '30m' |
| 153 | + claims: |
| 154 | + org: 'acme' |
| 155 | + team: 'data' |
| 156 | +``` |
| 157 | + |
| 158 | +With this configuration: |
| 159 | + |
| 160 | +- Entries from `platform-tools` are visible only to callers with `org: "acme"` |
| 161 | + **and** `team: "platform"` in their JWT |
| 162 | +- Entries from `data-tools` are visible only to callers with `org: "acme"` |
| 163 | + **and** `team: "data"` in their JWT |
| 164 | +- A super-admin sees all entries regardless of claims |
| 165 | + |
| 166 | +### Registry claims |
| 167 | + |
| 168 | +Claims on a registry act as an **access gate** for the consumer API. Before |
| 169 | +returning any data from a registry's endpoints, the server checks that the |
| 170 | +caller's JWT claims satisfy the registry's claims. |
| 171 | + |
| 172 | +```yaml title="config-registry-claims.yaml" |
| 173 | +registries: |
| 174 | + - name: platform |
| 175 | + sources: ['platform-tools'] |
| 176 | + # highlight-start |
| 177 | + claims: |
| 178 | + org: 'acme' |
| 179 | + team: 'platform' |
| 180 | + # highlight-end |
| 181 | +
|
| 182 | + - name: public |
| 183 | + sources: ['community-tools'] |
| 184 | + # No claims — accessible to all authenticated users |
| 185 | +``` |
| 186 | + |
| 187 | +A caller who requests `GET /platform/v0.1/servers` must have JWT claims that |
| 188 | +include `org: "acme"` and `team: "platform"`. Otherwise, the server returns |
| 189 | +`403 Forbidden`. |
| 190 | + |
| 191 | +### Claim containment |
| 192 | + |
| 193 | +The server uses **containment** (superset check) for claim validation: the |
| 194 | +caller's claims must be a superset of the resource's claims. For example: |
| 195 | + |
| 196 | +| Resource claims | Caller JWT claims | Result | |
| 197 | +| --------------------------------- | --------------------------------- | ------- | |
| 198 | +| `{org: "acme"}` | `{org: "acme", team: "platform"}` | Allowed | |
| 199 | +| `{org: "acme", team: "platform"}` | `{org: "acme"}` | Denied | |
| 200 | +| `{}` (no claims) | `{org: "acme"}` | Allowed | |
| 201 | +| `{org: "acme"}` | `{org: "contoso"}` | Denied | |
| 202 | + |
| 203 | +Resources with no claims are accessible to all authenticated callers. |
| 204 | + |
| 205 | +## Claims on published entries |
| 206 | + |
| 207 | +When you publish an MCP server version or skill to a managed source, you can |
| 208 | +attach claims to the entry. The server enforces two rules: |
| 209 | + |
| 210 | +1. **Publish claims must be a subset of the publisher's JWT claims.** You cannot |
| 211 | + publish entries with broader visibility than your own identity allows. For |
| 212 | + example, if your JWT has `{org: "acme", team: "platform"}`, you can publish |
| 213 | + entries with `{org: "acme", team: "platform"}` but not with `{org: "acme"}` |
| 214 | + alone (which would be visible to all teams). |
| 215 | + |
| 216 | +2. **Subsequent versions must have the same claims as the first.** Once you |
| 217 | + publish the first version of an entry with specific claims, all future |
| 218 | + versions must carry the exact same claims. This prevents accidentally |
| 219 | + narrowing or broadening visibility across versions. |
| 220 | + |
| 221 | +```bash title="Publish a server with claims" |
| 222 | +curl -X POST \ |
| 223 | + https://registry.example.com/default/v0.1/publish \ |
| 224 | + -H "Authorization: Bearer $TOKEN" \ |
| 225 | + -H "Content-Type: application/json" \ |
| 226 | + -d '{ |
| 227 | + "name": "my-server", |
| 228 | + "version": "1.0.0", |
| 229 | + "url": "https://mcp.example.com/my-server", |
| 230 | + "description": "Team-scoped MCP server", |
| 231 | + "claims": { |
| 232 | + "org": "acme", |
| 233 | + "team": "platform" |
| 234 | + } |
| 235 | + }' |
| 236 | +``` |
| 237 | + |
| 238 | +## Admin API claim scoping |
| 239 | + |
| 240 | +When authorization is enabled, the admin API endpoints for managing sources and |
| 241 | +registries are also scoped by claims: |
| 242 | + |
| 243 | +- **List sources/registries**: Only returns resources whose claims the caller's |
| 244 | + JWT satisfies. |
| 245 | +- **Get source/registry by name**: Returns `404 Not Found` (not `403`) when the |
| 246 | + caller's claims don't match — this prevents information disclosure about |
| 247 | + resources the caller cannot access. |
| 248 | +- **Create source/registry**: The request claims must be a subset of the |
| 249 | + caller's JWT claims. |
| 250 | +- **Update/delete source/registry**: The caller's JWT claims must satisfy the |
| 251 | + existing resource's claims. |
| 252 | + |
| 253 | +## Anonymous mode |
| 254 | + |
| 255 | +When authentication is set to `anonymous`, all authorization checks are |
| 256 | +bypassed. There are no JWT claims to validate, so all sources, registries, and |
| 257 | +entries are accessible without restriction. This is suitable for development and |
| 258 | +testing environments only. |
| 259 | + |
| 260 | +## Complete example |
| 261 | + |
| 262 | +This example shows a multi-team setup with full RBAC and claims-based scoping: |
| 263 | + |
| 264 | +```yaml title="config-multi-tenant.yaml" |
| 265 | +sources: |
| 266 | + - name: platform-tools |
| 267 | + format: toolhive |
| 268 | + git: |
| 269 | + repository: https://github.com/acme/platform-tools.git |
| 270 | + branch: main |
| 271 | + path: registry.json |
| 272 | + syncPolicy: |
| 273 | + interval: '30m' |
| 274 | + claims: |
| 275 | + org: 'acme' |
| 276 | + team: 'platform' |
| 277 | +
|
| 278 | + - name: data-tools |
| 279 | + format: toolhive |
| 280 | + git: |
| 281 | + repository: https://github.com/acme/data-tools.git |
| 282 | + branch: main |
| 283 | + path: registry.json |
| 284 | + syncPolicy: |
| 285 | + interval: '30m' |
| 286 | + claims: |
| 287 | + org: 'acme' |
| 288 | + team: 'data' |
| 289 | +
|
| 290 | + - name: shared |
| 291 | + managed: {} |
| 292 | +
|
| 293 | +registries: |
| 294 | + - name: platform |
| 295 | + sources: ['platform-tools', 'shared'] |
| 296 | + claims: |
| 297 | + org: 'acme' |
| 298 | + team: 'platform' |
| 299 | +
|
| 300 | + - name: data |
| 301 | + sources: ['data-tools', 'shared'] |
| 302 | + claims: |
| 303 | + org: 'acme' |
| 304 | + team: 'data' |
| 305 | +
|
| 306 | +auth: |
| 307 | + mode: oauth |
| 308 | + oauth: |
| 309 | + resourceUrl: https://registry.example.com |
| 310 | + providers: |
| 311 | + - name: keycloak |
| 312 | + issuerUrl: https://keycloak.example.com/realms/mcp |
| 313 | + audience: registry-api |
| 314 | + authz: |
| 315 | + roles: |
| 316 | + superAdmin: |
| 317 | + - role: 'super-admin' |
| 318 | + manageSources: |
| 319 | + - org: 'acme' |
| 320 | + role: 'admin' |
| 321 | + manageRegistries: |
| 322 | + - org: 'acme' |
| 323 | + role: 'admin' |
| 324 | + manageEntries: |
| 325 | + - role: 'writer' |
| 326 | +``` |
| 327 | + |
| 328 | +With this configuration: |
| 329 | + |
| 330 | +- **Platform team members** (JWT with `org: "acme"`, `team: "platform"`) can |
| 331 | + access the `platform` registry and see entries from `platform-tools` and |
| 332 | + `shared`. |
| 333 | +- **Data team members** (JWT with `org: "acme"`, `team: "data"`) can access the |
| 334 | + `data` registry and see entries from `data-tools` and `shared`. |
| 335 | +- **Writers** (JWT with `role: "writer"`) can publish to the `shared` managed |
| 336 | + source. |
| 337 | +- **Admins** (JWT with `org: "acme"`, `role: "admin"`) can manage sources and |
| 338 | + registries within the `acme` org. |
| 339 | +- **Super-admins** (JWT with `role: "super-admin"`) can access and manage |
| 340 | + everything. |
| 341 | + |
| 342 | +Entries published to the `shared` source without claims are visible through any |
| 343 | +registry that includes it, subject only to the registry-level claims gate. To |
| 344 | +restrict visibility further, attach claims when |
| 345 | +[publishing entries](#claims-on-published-entries). |
| 346 | + |
| 347 | +## Next steps |
| 348 | + |
| 349 | +- [Configure authentication](./authentication.mdx) to set up OAuth providers |
| 350 | +- [Configure sources and registries](./configuration.mdx) to set up your data |
| 351 | + sources |
| 352 | +- [Manage skills](./skills.mdx) to publish and discover reusable skills |
| 353 | + |
| 354 | +## Related information |
| 355 | + |
| 356 | +- [Registry server introduction](./intro.mdx) - architecture and features |
| 357 | + overview |
0 commit comments