diff --git a/docs/toolhive/guides-registry/authorization.mdx b/docs/toolhive/guides-registry/authorization.mdx index e55f6dd9..e56f7bb3 100644 --- a/docs/toolhive/guides-registry/authorization.mdx +++ b/docs/toolhive/guides-registry/authorization.mdx @@ -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 @@ -154,7 +153,6 @@ sources: # highlight-end - name: data-tools - format: upstream git: repository: https://github.com/acme/data-tools.git branch: main @@ -230,30 +228,58 @@ 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" @@ -261,6 +287,16 @@ curl -X POST \ }' ``` +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 @@ -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)` @@ -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 @@ -348,7 +386,6 @@ sources: team: 'platform' - name: data-tools - format: upstream git: repository: https://github.com/acme/data-tools.git branch: main diff --git a/docs/toolhive/guides-registry/configuration.mdx b/docs/toolhive/guides-registry/configuration.mdx index 001ecf7b..03906fd8 100644 --- a/docs/toolhive/guides-registry/configuration.mdx +++ b/docs/toolhive/guides-registry/configuration.mdx @@ -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 @@ -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 @@ -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 @@ -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 @@ -253,7 +244,6 @@ scenarios. ```yaml title="config-api.yaml" sources: - name: mcp-upstream - format: upstream api: endpoint: https://registry.modelcontextprotocol.io syncPolicy: @@ -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 @@ -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: @@ -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 @@ -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 - [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. @@ -378,7 +381,6 @@ RBAC requirements. ```yaml title="config-kubernetes.yaml" sources: - name: default - format: upstream kubernetes: {} registries: @@ -523,7 +525,6 @@ Kubernetes sources. ```yaml sources: - name: toolhive - format: upstream git: repository: https://github.com/stacklok/toolhive-catalog.git branch: main @@ -557,7 +558,6 @@ name patterns. ```yaml sources: - name: toolhive - format: upstream git: repository: https://github.com/stacklok/toolhive-catalog.git branch: main @@ -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 @@ -605,7 +604,6 @@ sources: interval: '30m' - name: upstream-mcp - format: upstream api: endpoint: https://registry.modelcontextprotocol.io syncPolicy: @@ -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 diff --git a/docs/toolhive/guides-registry/database.mdx b/docs/toolhive/guides-registry/database.mdx index a8cfa5ab..3c4c8dad 100644 --- a/docs/toolhive/guides-registry/database.mdx +++ b/docs/toolhive/guides-registry/database.mdx @@ -28,36 +28,75 @@ database: ### Configuration fields -| Field | Type | Required | Default | Description | -| ----------------- | ------ | -------- | --------- | -------------------------------------------------------------------------------------------------- | -| `host` | string | Yes | - | Database server hostname or IP address | -| `port` | int | Yes | - | Database server port | -| `user` | string | Yes | - | Database username for normal operations | -| `migrationUser` | string | No | `user` | Database username for running migrations (should have elevated privileges) | -| `database` | string | Yes | - | Database name | -| `sslMode` | string | No | `require` | SSL mode (`disable`, `require`, `verify-ca`, `verify-full`) | -| `maxOpenConns` | int | No | `25` | Maximum number of open connections to the database | -| `maxIdleConns` | int | No | `5` | Maximum number of idle connections in the pool | -| `connMaxLifetime` | string | No | `5m` | Maximum lifetime of a connection (e.g., "1h", "30m") | -| `maxMetaSize` | int | No | `262144` | Maximum allowed size in bytes for publisher-provided metadata extensions (256 KB) | -| `dynamicAuth` | object | No | - | Dynamic authentication configuration (see [Dynamic authentication](#dynamic-authentication) below) | +| Field | Type | Required | Default | Description | +| ------------------- | ------ | -------- | --------- | -------------------------------------------------------------------------------------------------- | +| `host` | string | Yes | - | Database server hostname or IP address | +| `port` | int | Yes | - | Database server port | +| `user` | string | Yes | - | Database username for normal operations | +| `migrationUser` | string | No | `user` | Database username for running migrations (should have elevated privileges) | +| `password` | string | No | - | Password for the application user. Mutually exclusive with `dynamicAuth` | +| `migrationPassword` | string | No | - | Password for the migration user. Defaults to `password` when `migrationUser` equals `user` | +| `database` | string | Yes | - | Database name | +| `sslMode` | string | No | `require` | SSL mode (`disable`, `require`, `verify-ca`, `verify-full`) | +| `maxOpenConns` | int | No | `25` | Maximum number of open connections to the database | +| `maxIdleConns` | int | No | `5` | Maximum number of idle connections in the pool | +| `connMaxLifetime` | string | No | `5m` | Maximum lifetime of a connection (e.g., "1h", "30m") | +| `maxMetaSize` | int | No | `262144` | Maximum allowed size in bytes for publisher-provided metadata extensions (256 KB) | +| `dynamicAuth` | object | No | - | Dynamic authentication configuration (see [Dynamic authentication](#dynamic-authentication) below) | + +Passwords are optional in the config file; see +[Password configuration](#password-configuration) for the full precedence order. + +## Password configuration + +The server supports several mechanisms for providing database credentials. +`dynamicAuth` is mutually exclusive with the static password options: setting +both a `password` (or `migrationPassword`) field and a `dynamicAuth` block fails +startup validation. + +When more than one source is configured, the server resolves them in this order +of precedence: + +1. **Dynamic authentication** (e.g., AWS RDS IAM) - short-lived tokens generated + automatically via the `dynamicAuth` config block +2. **Config field or environment variable** - the `password` and + `migrationPassword` fields, or the `THV_REGISTRY_DATABASE_PASSWORD` and + `THV_REGISTRY_DATABASE_MIGRATIONPASSWORD` environment variables +3. **pgpass file** - PostgreSQL's standard `~/.pgpass` or `$PGPASSFILE` + mechanism (used when no password is configured via the methods above) + +### Configure passwords via environment variables + +Environment variables are convenient for containerized deployments because they +let you inject secrets from Kubernetes Secrets, Docker secrets, or your CI/CD +system without baking passwords into config files: -\* Password configuration is required but has multiple sources (see Password -Security and Dynamic authentication below) +```bash +export THV_REGISTRY_DATABASE_PASSWORD=app_password +export THV_REGISTRY_DATABASE_MIGRATIONPASSWORD=migrator_password +thv-registry-api serve --config config.yaml +``` -## Password security +The environment variable takes precedence over a value set in the YAML config +file. When `migrationUser` equals `user` and `migrationPassword` is not set, the +server reuses `password` for migrations. -The server supports secure password management with separate credentials for -migrations and normal operations. This follows the principle of least privilege -by using elevated privileges only when necessary. +:::tip[Kubernetes Secrets] -Password configuration is done using a -[Postgres Password File](https://www.postgresql.org/docs/current/libpq-pgpass.html) -and exporting the `PGPASSFILE` environment variable. +In Kubernetes, reference a Secret via `envFrom` or `secretKeyRef` rather than +storing the password in a ConfigMap. See the +[operator deploy guide](./deploy-operator.mdx) for complete examples. -### Recommended setup +::: -For production deployments, use separate database users: +### Configure passwords via a pgpass file + +As an alternative, the server supports PostgreSQL's standard pgpass file. This +approach works well for traditional VM deployments or when you want the server +to pick up credentials without any environment configuration. + +For production, use separate database users regardless of how you provide +passwords: 1. **Application user** (`user`): Limited privileges for normal operations - SELECT, INSERT, UPDATE, DELETE on application tables @@ -67,7 +106,7 @@ For production deployments, use separate database users: - CREATE, ALTER, DROP on schemas and tables - Used only during migration operations -### Example configuration with separate users +Example configuration: ```yaml title="config-production.yaml" database: @@ -90,8 +129,6 @@ echo "db.example.com:5432:registry:db_migrator:migrator_password" >> /etc/secret chmod 600 /etc/secrets/pgpassfile ``` -**Using the pgpass file:** - Set the `PGPASSFILE` environment variable when running the server: ```bash diff --git a/docs/toolhive/guides-registry/deploy-manual.mdx b/docs/toolhive/guides-registry/deploy-manual.mdx index a07f9093..4a21a74f 100644 --- a/docs/toolhive/guides-registry/deploy-manual.mdx +++ b/docs/toolhive/guides-registry/deploy-manual.mdx @@ -115,17 +115,16 @@ metadata: data: config.yaml: | sources: - - name: toolhive - format: upstream - git: - repository: https://github.com/stacklok/toolhive-catalog.git - branch: main - path: pkg/catalog/toolhive/data/registry-upstream.json - syncPolicy: - interval: "15m" + - name: toolhive + git: + repository: https://github.com/stacklok/toolhive-catalog.git + branch: main + path: pkg/catalog/toolhive/data/registry-upstream.json + syncPolicy: + interval: "15m" registries: - - name: default - sources: ["toolhive"] + - name: default + sources: ["toolhive"] auth: mode: oauth oauth: diff --git a/docs/toolhive/guides-registry/deploy-operator.mdx b/docs/toolhive/guides-registry/deploy-operator.mdx index 67791098..356a9d1f 100644 --- a/docs/toolhive/guides-registry/deploy-operator.mdx +++ b/docs/toolhive/guides-registry/deploy-operator.mdx @@ -80,7 +80,6 @@ spec: configYAML: | sources: - name: toolhive - format: upstream git: repository: https://github.com/stacklok/toolhive-catalog.git branch: main @@ -125,7 +124,6 @@ and each registry references sources by name. configYAML: | sources: - name: my-source - format: upstream git: { ... } registries: - name: default @@ -147,7 +145,6 @@ spec: configYAML: | sources: - name: toolhive - format: upstream git: repository: https://github.com/stacklok/toolhive-catalog.git branch: main @@ -201,7 +198,6 @@ spec: configYAML: | sources: - name: local - format: upstream file: path: /data/registry/registry.json syncPolicy: @@ -229,7 +225,6 @@ spec: configYAML: | sources: - name: upstream - format: upstream api: endpoint: https://registry.example.com syncPolicy: @@ -257,7 +252,6 @@ spec: configYAML: | sources: - name: k8s - format: upstream kubernetes: {} registries: - name: default @@ -279,7 +273,6 @@ spec: configYAML: | sources: - name: remote - format: upstream file: url: https://example.com/registry.json syncPolicy: @@ -315,7 +308,6 @@ spec: configYAML: | sources: - name: toolhive - format: upstream git: repository: https://github.com/stacklok/toolhive-catalog.git branch: main @@ -368,7 +360,6 @@ spec: configYAML: | sources: - name: toolhive - format: upstream git: repository: https://github.com/stacklok/toolhive-catalog.git branch: main @@ -450,7 +441,6 @@ spec: configYAML: | sources: - name: toolhive - format: upstream git: repository: https://github.com/stacklok/toolhive-catalog.git branch: main @@ -511,7 +501,6 @@ spec: configYAML: | sources: - name: k8s - format: upstream kubernetes: {} registries: - name: default @@ -554,7 +543,6 @@ spec: configYAML: | sources: - name: k8s - format: upstream kubernetes: {} registries: - name: default @@ -592,7 +580,6 @@ spec: configYAML: | sources: - name: toolhive - format: upstream git: repository: https://github.com/stacklok/toolhive-catalog.git branch: main @@ -643,7 +630,6 @@ spec: configYAML: | sources: - name: toolhive - format: upstream git: repository: https://github.com/stacklok/toolhive-catalog.git branch: main @@ -819,7 +805,8 @@ Common causes include: - **Source unavailable**: Git repository, API endpoint, or file is inaccessible - **Invalid JSON format**: Registry file contains invalid JSON -- **Format mismatch**: The `format` field doesn't match the actual data format +- **Schema mismatch**: The data does not conform to the + [upstream MCP registry schema](../reference/registry-schema-upstream.mdx) - **Filter too restrictive**: Filters may be excluding all servers diff --git a/docs/toolhive/guides-registry/intro.mdx b/docs/toolhive/guides-registry/intro.mdx index 0c9d8fd4..1d85489d 100644 --- a/docs/toolhive/guides-registry/intro.mdx +++ b/docs/toolhive/guides-registry/intro.mdx @@ -103,7 +103,7 @@ The server supports five source types: 1. **Managed** - A fully-managed MCP source - Ideal for hosting private MCP server catalogs - Automatically exposes entries following upstream MCP Registry format - - Supports adding new MCP servers via `/publish` endpoint + - Supports publishing MCP servers and skills via the `/v1/entries` admin API 2. **Upstream API** - Sync from upstream MCP Registry APIs - Supports federation and aggregation scenarios diff --git a/docs/toolhive/guides-registry/skills.mdx b/docs/toolhive/guides-registry/skills.mdx index 2770ca79..05a3bd51 100644 --- a/docs/toolhive/guides-registry/skills.mdx +++ b/docs/toolhive/guides-registry/skills.mdx @@ -38,6 +38,13 @@ config has `registries: [{name: my-registry, ...}]`). To publish a new skill version, send a `POST` request to the `/v1/entries` endpoint with the skill metadata wrapped in a `skill` object: +:::note + +The example below omits claims. When authentication is enabled, requests must +also include a top-level `claims` object - see [Claims](#claims) below. + +::: + ```bash title="Publish a skill" curl -X POST \ https://registry.example.com/v1/entries \ @@ -68,7 +75,7 @@ curl -X POST \ }' ``` -**Required fields:** `namespace`, `name`, `version` +**Required fields in the `skill` object:** `namespace`, `name`, `version` A successful response returns `201 Created` with the published skill. If the version already exists, the server returns `409 Conflict`. @@ -78,15 +85,17 @@ insensitive). The server normalizes status values to uppercase internally. ### Claims -When [authorization](./authorization.mdx) is enabled, you can attach claims to -published skills to control visibility. Include a `claims` object in the request -body: +When [authentication](./authentication.mdx) is enabled, publish requests must +include a `claims` object. Skipping it returns `400 Bad Request`. Attach claims +at the top level of the request body, alongside the `skill` object: ```json { - "namespace": "io.github.acme", - "name": "code-review", - "version": "1.0.0", + "skill": { + "namespace": "io.github.acme", + "name": "code-review", + "version": "1.0.0" + }, "claims": { "org": "acme", "team": "platform" } } ``` @@ -189,10 +198,13 @@ To delete a specific version, use the `/v1/entries` endpoint: ```bash title="Delete a skill version" curl -X DELETE \ - https://registry.example.com/v1/entries/skill/code-review/versions/1.0.0 \ + "https://registry.example.com/v1/entries/skill/io.github.acme%2Fcode-review/versions/1.0.0" \ -H "Authorization: Bearer " ``` +The skill name includes the namespace prefix. URL-encode the slash as `%2F` so +the full `namespace/name` is captured as a single `{name}` path parameter. + A successful delete returns `204 No Content`. Deleting a non-existent version returns `404 Not Found`. diff --git a/docs/toolhive/reference/registry-schema-toolhive.mdx b/docs/toolhive/reference/registry-schema-toolhive.mdx index e8630c5b..8270c899 100644 --- a/docs/toolhive/reference/registry-schema-toolhive.mdx +++ b/docs/toolhive/reference/registry-schema-toolhive.mdx @@ -16,10 +16,11 @@ conform to a consistent format. :::info This format is considered legacy now that an -[official upstream registry format](registry-schema-upstream.mdx) exists. -ToolHive supports both formats, and the ToolHive-native format continues to work -for the built-in registry, custom file-based registries, and the ToolHive -Registry Server. +[official upstream registry format](registry-schema-upstream.mdx) exists. The +ToolHive-native format continues to work for the built-in registry and for +custom file-based registries used by the CLI and UI. The +[Registry Server](/toolhive/guides-registry/intro) only accepts the upstream +format. ::: diff --git a/docs/toolhive/reference/registry-schema-upstream.mdx b/docs/toolhive/reference/registry-schema-upstream.mdx index c8fa7cef..8526b11c 100644 --- a/docs/toolhive/reference/registry-schema-upstream.mdx +++ b/docs/toolhive/reference/registry-schema-upstream.mdx @@ -18,10 +18,10 @@ server schema, which is documented separately below. :::info -ToolHive also supports the -[ToolHive-native registry format](./registry-schema-toolhive.mdx), which is used -by the built-in registry. Both formats work with custom file-based registries -and the ToolHive Registry Server. +The [Registry Server](/toolhive/guides-registry/intro) only accepts this +upstream format. The CLI and UI additionally support the legacy +[ToolHive-native registry format](./registry-schema-toolhive.mdx) for their +built-in and custom file-based registries. :::