Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 55 additions & 18 deletions docs/toolhive/guides-registry/authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ on each CRD. Managed source entries get claims from the publish request payload.
```yaml title="config-source-claims.yaml"
sources:
- name: platform-tools
format: upstream
git:
repository: https://github.com/acme/platform-tools.git
branch: main
Expand All @@ -154,7 +153,6 @@ sources:
# highlight-end

- name: data-tools
format: upstream
git:
repository: https://github.com/acme/data-tools.git
branch: main
Expand Down Expand Up @@ -230,37 +228,75 @@ for Kubernetes sources).
## 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:
attach claims to the entry. The server enforces three rules:

1. **Publish claims must be a subset of the publisher's JWT claims.** Every
1. **Claims are required when authentication is enabled.** Publishing without
claims, or with an empty `claims` object, against an authenticated endpoint
returns `400 Bad Request`. Without claims, the entry would be invisible to
every non-super-admin caller, so the server rejects the request up front.
2. **Publish claims must be a subset of the publisher's JWT claims.** Every
claim key in the publish request must exist in the publisher's JWT with a
matching value. For example, if your JWT has
`{org: "acme", team: "platform"}`, you can publish entries with
`{org: "acme"}`, `{org: "acme", team: "platform"}`, or no claims at all, but
not with `{org: "contoso"}` (a value your JWT doesn't have).

2. **Subsequent versions must have the same claims as the first.** Once you
`{org: "acme"}` or `{org: "acme", team: "platform"}`, but not
`{org: "contoso"}` (a value your JWT doesn't have).
3. **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.
versions must carry the exact same claims. If the first version had none,
subsequent versions must also have none. This prevents accidentally narrowing
or broadening visibility across versions.

```bash title="Publish a server with claims"
curl -X POST \
https://registry.example.com/default/v0.1/publish \
https://registry.example.com/v1/entries \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"server": {
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.acme/my-server",
"description": "Team-scoped MCP server",
"version": "1.0.0"
},
"claims": {
"org": "acme",
"team": "platform"
}
}'
```

See the [Registry API reference](../reference/registry-api.mdx) for the full
server payload schema, including `packages`, `remotes`, and `_meta` fields.

### Update claims on an existing entry

Use `PUT /v1/entries/{type}/{name}/claims` to change the claims on every version
of a previously published entry. The `type` path parameter is either `server` or
`skill`.

```bash title="Update claims on a published server"
curl -X PUT \
"https://registry.example.com/v1/entries/server/io.github.acme%2Fmy-server/claims" \
-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"
}
}'
```

Because entry names contain a slash separating namespace from server name,
URL-encode the slash as `%2F` so the path is treated as a single `{name}` path
parameter.

Pass an empty `claims` object (`{"claims": {}}`) to clear claims entirely. The
same authorization rules apply: your JWT claims must satisfy both the existing
claims on the entry (so you can modify it) and the new claims you're setting (so
you can't escalate visibility beyond what you're entitled to). Successful
updates return `204 No Content`.

## Admin API claim scoping

When authorization is enabled, the admin API endpoints for managing sources and
Expand Down Expand Up @@ -289,7 +325,10 @@ your configuration, the server runs in **auth-only mode**. In this mode:
- Callers must still authenticate with a valid JWT token
- All claims-based filtering is disabled. Every authenticated caller sees all
entries regardless of source or registry claims
- Role checks are not enforced (no roles are resolved)
- Role checks are pass-throughs, so every authenticated caller can reach every
endpoint
- `GET /v1/me` reports every defined role for authenticated callers, reflecting
that authorization isn't being enforced
- The server logs a warning at startup:
`Authorization not configured, per-entry claims filtering disabled (auth-only mode)`

Expand Down Expand Up @@ -336,7 +375,6 @@ This example shows a multi-team setup with full RBAC and claims-based scoping:
```yaml title="config-multi-tenant.yaml"
sources:
- name: platform-tools
format: upstream
git:
repository: https://github.com/acme/platform-tools.git
branch: main
Expand All @@ -348,7 +386,6 @@ sources:
team: 'platform'

- name: data-tools
format: upstream
git:
repository: https://github.com/acme/data-tools.git
branch: main
Expand Down
69 changes: 42 additions & 27 deletions docs/toolhive/guides-registry/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ sources).
# Sources define where registry data comes from
sources:
- name: toolhive
format: upstream
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
Expand Down Expand Up @@ -96,17 +95,11 @@ Kubernetes lease name suffixes for leader election.
| `--internal-address` | Listen address for internal endpoints (health, readiness, version) | No | `:8081` |
| `--auth-mode` | Override auth mode (`anonymous` or `oauth`) | No | - |

## Registry data formats
## Registry data format

The `format` field on a source specifies the JSON schema format for the registry
data:

- **`upstream`**: The official
[upstream MCP registry format](../reference/registry-schema-upstream.mdx),
used by the MCP Registry API and recommended for new registries.
- **`toolhive`**: The
[ToolHive-native format](../reference/registry-schema-toolhive.mdx), used by
the built-in ToolHive registry.
The Registry Server reads registry data in the official
[upstream MCP registry format](../reference/registry-schema-upstream.mdx). All
synced sources (Git, API, and File) must provide data in this format.

## Registry sources

Expand All @@ -129,7 +122,6 @@ on every container restart, adding startup latency and increasing network usage.
```yaml title="config-git.yaml"
sources:
- name: toolhive
format: upstream
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
Expand Down Expand Up @@ -169,7 +161,6 @@ credentials:
```yaml title="config-git-private.yaml"
sources:
- name: private-registry
format: upstream
git:
repository: https://github.com/my-org/private-registry.git
branch: main
Expand Down Expand Up @@ -253,7 +244,6 @@ scenarios.
```yaml title="config-api.yaml"
sources:
- name: mcp-upstream
format: upstream
api:
endpoint: https://registry.modelcontextprotocol.io
syncPolicy:
Expand All @@ -268,8 +258,6 @@ registries:

- `endpoint` (required): Base URL of the upstream MCP Registry API (without
path)
- `format` (optional): Must be `upstream` if specified (API sources only support
the upstream format)

:::note

Expand All @@ -285,7 +273,6 @@ Read from filesystem. Ideal for local development and testing.
```yaml title="config-file.yaml"
sources:
- name: local
format: upstream
file:
path: /data/registry.json
syncPolicy:
Expand All @@ -301,7 +288,6 @@ Alternatively, the source can be a custom URL.
```yaml title="config-file-url.yaml"
sources:
- name: remote
format: upstream
file:
url: https://www.example.com/registry.json
timeout: 5s
Expand Down Expand Up @@ -344,14 +330,31 @@ registries:
- `managed` (required): Empty object to indicate managed source type
- No sync policy required (managed sources are updated via API, not synced)

:::warning[Single managed source limit]

A server deployment can have at most one managed source. Attempts to create a
second managed source through the admin API return `409 Conflict`. Startup also
fails validation if the configuration file declares multiple managed sources, or
if it declares a managed source while an API-created managed source already
exists in the database.

:::

**Supported operations:**

- `POST /{registryName}/v0.1/publish` - Publish new server versions
- `DELETE /{registryName}/v0.1/servers/{name}/versions/{version}` - Delete
versions
- `GET` operations for listing and retrieving servers
- `POST /v1/entries` - Publish new server or skill versions
- `DELETE /v1/entries/{type}/{name}/versions/{version}` - Delete a specific
version
- `PUT /v1/entries/{type}/{name}/claims` - Update authorization claims on a
published entry
- `GET /registry/{registryName}/v0.1/...` - List and retrieve servers and skills
Comment thread
rdimitrov marked this conversation as resolved.
- [Skills management](./skills.mdx) via the extensions API

When authentication is enabled, publish requests must include a `claims` object.
See
[Claims on published entries](./authorization.mdx#claims-on-published-entries)
for the full rules.

See the [Registry API reference](../reference/registry-api.mdx) for complete
endpoint documentation and request/response examples.

Expand All @@ -378,7 +381,6 @@ RBAC requirements.
```yaml title="config-kubernetes.yaml"
sources:
- name: default
format: upstream
kubernetes: {}

registries:
Expand Down Expand Up @@ -523,7 +525,6 @@ Kubernetes sources.
```yaml
sources:
- name: toolhive
format: upstream
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
Expand Down Expand Up @@ -557,7 +558,6 @@ name patterns.
```yaml
sources:
- name: toolhive
format: upstream
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
Expand Down Expand Up @@ -596,7 +596,6 @@ controls.
```yaml title="config-multi-source.yaml"
sources:
- name: toolhive-catalog
format: upstream
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
Expand All @@ -605,7 +604,6 @@ sources:
interval: '30m'

- name: upstream-mcp
format: upstream
api:
endpoint: https://registry.modelcontextprotocol.io
syncPolicy:
Expand Down Expand Up @@ -666,6 +664,23 @@ The server supports OpenTelemetry for comprehensive observability, including
distributed tracing and metrics collection. See the
[Telemetry and metrics](./telemetry-metrics.mdx) guide for detailed information.

## Environment variables

Most configuration values can be overridden with environment variables prefixed
with `THV_REGISTRY_`. Nested keys use underscores; for example `database.host`
maps to `THV_REGISTRY_DATABASE_HOST`. A few settings are environment-only:

| Variable | Description |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `THV_REGISTRY_COMPRESS_RESPONSE` | Set to `true` to enable gzip compression on HTTP responses |
| `THV_REGISTRY_WATCH_NAMESPACE` | Comma-separated list of namespaces to watch for Kubernetes sources (see [Kubernetes source](#kubernetes-source)) |
| `THV_REGISTRY_LEADER_ELECTION_ID` | Unique identifier for the leader election lease when running multiple instances |
| `THV_REGISTRY_INSECURE_URL` | Set to `true` to allow HTTP OAuth issuer URLs (development only) |

Database passwords can also be provided via environment variables. See
[Database configuration](./database.mdx#password-configuration) for the full
precedence order.

## Next steps

- [Configure authentication](./authentication.mdx) to secure access to your
Expand Down
Loading