Deploy with the ToolHive Operator
Prerequisites
- A Kubernetes cluster (current and two previous minor versions are supported)
- Permissions to create resources in the cluster
kubectlconfigured to communicate with your cluster- The ToolHive operator installed in your cluster (see Deploy the operator)
- A PostgreSQL database
Overview
The ToolHive operator deploys the Registry server in Kubernetes by creating
MCPRegistry resources. Alternatively, you can deploy the Registry Server
manually by following the manual deployment instructions.
High-level architecture
This diagram shows the basic relationship between components. The ToolHive
operator watches for MCPRegistry resources and automatically creates the
necessary infrastructure to run the Registry server.
Create an MCPRegistry resource
You can create MCPRegistry resources in the namespaces where the ToolHive
Operator is deployed.
See Deploy the operator to learn about the different deployment modes.
The MCPRegistry CRD uses the same
sources and registries model as the
standalone configuration: sources define where data comes from, and
registries aggregate sources into named API endpoints.
To deploy a Registry Server instance, define an MCPRegistry resource and apply
it to your cluster. This minimal example configures a source that syncs from the
ToolHive Git repository.
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: my-registry
namespace: my-namespace # Update with your namespace
spec:
displayName: My MCP Registry
authConfig:
mode: anonymous
sources:
- name: toolhive
format: toolhive
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
path: pkg/catalog/toolhive/data/registry-legacy.json
syncPolicy:
interval: '30m'
registries:
- name: default
sources: ['toolhive']
Apply the resource:
kubectl apply -f my-registry.yaml
When you apply an MCPRegistry resource, here's what happens:
- The ToolHive operator detects the new resource (if it's in an allowed namespace)
- The operator creates the necessary RBAC resources in the target namespace
- The operator creates a Deployment containing the Registry server pod and service
- The Registry server syncs data from the configured sources
- The Registry API becomes available at the service endpoint
Configure sources
The MCPRegistry resource supports multiple source types. You can configure one
or more sources, but each source can use only one type (for example, git or
api, not both).
Git repository source
Clone and sync from Git repositories. Ideal for version-controlled registries.
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: git-registry
spec:
authConfig:
mode: anonymous
sources:
- name: toolhive
format: toolhive
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
path: pkg/catalog/toolhive/data/registry-legacy.json
syncPolicy:
interval: '30m'
registries:
- name: default
sources: ['toolhive']
Git source fields:
| Field | Required | Description |
|---|---|---|
repository | Yes | Git repository URL (HTTP/HTTPS/SSH) |
branch | No | Branch name (mutually exclusive with tag, commit) |
tag | No | Tag name (mutually exclusive with branch, commit) |
commit | No | Commit SHA (mutually exclusive with branch, tag) |
path | No | Path to registry file (default: registry.json) |
You can use branch, tag, or commit to pin to a specific version. If
multiple are specified, commit takes precedence over tag, which takes
precedence over branch.
Private repository authentication
To access private Git repositories, configure the auth section with a
Kubernetes Secret:
apiVersion: v1
kind: Secret
metadata:
name: git-credentials
type: Opaque
stringData:
token: <YOUR_GITHUB_TOKEN>
---
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: private-registry
spec:
authConfig:
mode: anonymous
sources:
- name: private
format: toolhive
git:
repository: https://github.com/my-org/private-registry.git
branch: main
path: registry.json
auth:
username: oauth2
passwordSecretRef:
name: git-credentials
key: token
syncPolicy:
interval: '30m'
registries:
- name: default
sources: ['private']
ConfigMap source
Read from a Kubernetes ConfigMap. Ideal for registry data managed within the cluster.
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: configmap-registry
spec:
authConfig:
mode: anonymous
sources:
- name: local
format: upstream
configMapRef:
name: registry-data
key: registry.json
syncPolicy:
interval: '15m'
registries:
- name: default
sources: ['local']
The ConfigMap must exist in the same namespace as the MCPRegistry resource.
URL source
Fetch registry data from a remote URL. Useful for hosted registry files.
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: url-registry
spec:
authConfig:
mode: anonymous
sources:
- name: remote
format: upstream
url:
endpoint: https://example.com/registry.json
timeout: '30s'
syncPolicy:
interval: '1h'
registries:
- name: default
sources: ['remote']
API source
Sync from an upstream MCP Registry API. Supports federation and aggregation scenarios.
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: api-registry
spec:
authConfig:
mode: anonymous
sources:
- name: upstream
format: upstream
api:
endpoint: https://registry.example.com
syncPolicy:
interval: '1h'
registries:
- name: default
sources: ['upstream']
The controller automatically appends the appropriate API paths to the endpoint URL.
Configure synchronization
Each source can have its own sync policy that controls automatic synchronization.
syncPolicy:
interval: '30m' # Go duration format: "1h", "30m", "24h"
Filter source content
You can filter which servers are exposed through the API using name and tag patterns.
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: filtered-registry
spec:
authConfig:
mode: anonymous
sources:
- name: toolhive
format: toolhive
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
path: pkg/catalog/toolhive/data/registry-legacy.json
filter:
names:
include:
- 'official/*'
exclude:
- '*/deprecated'
tags:
include:
- production
exclude:
- experimental
syncPolicy:
interval: '30m'
registries:
- name: default
sources: ['toolhive']
Configure database storage
Configure PostgreSQL database storage for the Registry server.
apiVersion: v1
kind: Secret
metadata:
name: registry-api-db-passwords
type: Opaque
stringData:
db-password: app_password
migration-password: migrator_password
---
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: production-registry
spec:
authConfig:
mode: anonymous
databaseConfig:
host: postgres.database.svc.cluster.local
port: 5432
user: db_app
migrationUser: db_migrator
dbAppUserPasswordSecretRef:
name: registry-api-db-passwords
key: db-password
dbMigrationUserPasswordSecretRef:
name: registry-api-db-passwords
key: migration-password
database: registry
sslMode: verify-full
maxOpenConns: 25
maxIdleConns: 5
connMaxLifetime: '30m'
sources:
- name: toolhive
format: toolhive
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
path: pkg/catalog/toolhive/data/registry-legacy.json
syncPolicy:
interval: '30m'
registries:
- name: default
sources: ['toolhive']
Database configuration fields:
| Field | Default | Description |
|---|---|---|
host | postgres | Database server hostname |
port | 5432 | Database server port |
user | db_app | Application user (SELECT, INSERT, UPDATE, DELETE) |
migrationUser | db_migrator | Migration user (CREATE, ALTER, DROP) |
dbAppUserPasswordSecretRef | - | Password of application user |
dbMigrationUserPasswordSecretRef | - | Password of migration user |
database | registry | Database name |
sslMode | prefer | SSL mode (disable, prefer, require, verify-full) |
maxOpenConns | 10 | Maximum open connections |
maxIdleConns | 2 | Maximum idle connections |
connMaxLifetime | 30m | Maximum connection lifetime |
Credentials are internally configured using a pgpass file mounted as a secret.
Configure authentication
You can configure authentication using the authConfig field in your
MCPRegistry resource.
Authentication modes
| Mode | Description | Use case |
|---|---|---|
oauth | Validates access tokens from identity providers | Production deployments |
anonymous | No authentication required | Development and testing only |
Configuring an authentication mode is mandatory. If you don't need
authentication, set it to anonymous.
OAuth authentication
OAuth mode validates JWT tokens from one or more identity providers. Configure
providers in the authConfig.oauth.providers array.
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: registry
namespace: toolhive-system
spec:
displayName: 'Authenticated MCP Server Registry'
authConfig:
mode: oauth
oauth:
providers:
- name: kubernetes
issuerUrl: https://kubernetes.default.svc.cluster.local
jwksUrl: https://kubernetes.default.svc/openid/v1/jwks
audience: registry-server
caCertPath: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
authTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
allowPrivateIP: true
sources:
- name: toolhive
format: toolhive
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
path: pkg/catalog/toolhive/data/registry-legacy.json
registries:
- name: default
sources: ['toolhive']
OAuth configuration fields
| Field | Required | Default | Description |
|---|---|---|---|
mode | No | anonymous | Authentication mode (oauth or anonymous) |
resourceUrl | No | - | URL identifying this protected resource (RFC 9728) |
realm | No | mcp-registry | Protection space identifier for WWW-Authenticate |
scopesSupported | No | [mcp-registry:read, mcp-registry:write] | OAuth scopes supported by this resource |
Provider configuration fields
| Field | Required | Description |
|---|---|---|
name | Yes | Unique identifier for this provider (for logging and monitoring) |
issuerUrl | Yes | OIDC issuer URL (e.g., https://accounts.google.com) |
audience | Yes | Expected audience claim in the access token |
jwksUrl | No | JWKS endpoint URL (skips OIDC discovery if specified) |
clientId | No | OAuth client ID for token introspection |
clientSecretFile | No | Path to file containing the client secret |
caCertPath | No | Path to CA certificate for TLS verification |
authTokenFile | No | Path to token file for authenticating to OIDC/JWKS endpoints |
introspectionUrl | No | Token introspection endpoint URL for opaque token validation (RFC 7662) |
allowPrivateIP | No | Allow connections to private IP addresses (required for in-cluster) |
Kubernetes service account authentication
For in-cluster deployments, you can configure OAuth to validate Kubernetes service account tokens:
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: registry
spec:
authConfig:
mode: oauth
oauth:
providers:
- name: kubernetes
issuerUrl: https://kubernetes.default.svc.cluster.local
jwksUrl: https://kubernetes.default.svc/openid/v1/jwks
audience: registry-server
caCertPath: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
authTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
allowPrivateIP: true
- issuerUrl: for most Kubernetes distributions,
https://kubernetes.default.svc.cluster.localis the correct value to match theissclaim in Kubernetes service account tokens. - jwksUrl: Specify directly to skip OIDC discovery (the Kubernetes API server doesn't support standard discovery).
- allowPrivateIP: Required for in-cluster communication with the API server.
Multiple providers
You can configure multiple OAuth providers to accept tokens from different identity sources:
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: registry
spec:
authConfig:
mode: oauth
oauth:
providers:
# Kubernetes service accounts (in-cluster workloads)
- name: kubernetes
issuerUrl: https://kubernetes.default.svc.cluster.local
jwksUrl: https://kubernetes.default.svc/openid/v1/jwks
audience: registry-server
caCertPath: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
authTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
allowPrivateIP: true
# External identity provider
- name: okta
issuerUrl: https://YOUR_DOMAIN.okta.com/oauth2/default
audience: registry
The server validates tokens against each provider in order until one succeeds.
Authorization
When OAuth authentication is enabled, you can configure role-based access
control via the authConfig.authz field:
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: registry
spec:
authConfig:
mode: oauth
oauth:
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'
sources:
- name: platform-tools
format: toolhive
git:
repository: https://github.com/acme/platform-tools.git
branch: main
path: registry.json
claims:
org: 'acme'
team: 'platform'
syncPolicy:
interval: '30m'
registries:
- name: platform
sources: ['platform-tools']
claims:
org: 'acme'
team: 'platform'
See the Authorization guide for details on roles, claims, and how they control access.
Anonymous authentication
For development and testing, you can disable authentication entirely:
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: registry
spec:
authConfig:
mode: anonymous
sources:
- name: toolhive
format: toolhive
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
path: pkg/catalog/toolhive/data/registry-legacy.json
registries:
- name: default
sources: ['toolhive']
Anonymous mode provides no access control. Only use it in trusted environments or when other security measures are in place. Do not use anonymous mode in production.
For detailed information about authentication configuration, including provider-specific examples for Keycloak, Auth0, Azure AD, and Okta, see the Authentication configuration guide.
Customize the Registry server pod
You can customize the Registry server pod using the podTemplateSpec field.
This gives you full control over the pod specification.
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPRegistry
metadata:
name: custom-registry
spec:
podTemplateSpec:
spec:
containers:
- name: registry-api # This name must be "registry-api"
resources:
limits:
cpu: '500m'
memory: '512Mi'
requests:
cpu: '100m'
memory: '128Mi'
authConfig:
mode: anonymous
sources:
- name: toolhive
format: toolhive
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
path: pkg/catalog/toolhive/data/registry-legacy.json
syncPolicy:
interval: '30m'
registries:
- name: default
sources: ['toolhive']
When customizing containers in podTemplateSpec, you must use
name: registry-api for the main container to ensure the operator can properly
manage the Registry server. The container name is hardcoded to avoid conflict
issues with user provided containers. Mandating a container name on the Operator
side explicitly tells the Operator it is the main registry server container and
any other containers provided by the user are sidecars/init containers.
Check registry status
To check the status of your MCPRegistry resources in a specific namespace:
kubectl -n <NAMESPACE> get mcpregistries
To check MCPRegistry resources across all namespaces:
kubectl get mcpregistries --all-namespaces
The status displays the phase, message, and age of each resource.
For more details about a specific resource:
kubectl -n <NAMESPACE> describe mcpregistry <NAME>
Registry phases
| Phase | Description |
|---|---|
Pending | The registry is being initialized |
Ready | The registry is ready and operational |
Syncing | The registry is currently syncing data |
Failed | The registry has encountered an error |
Terminating | The registry is being deleted |
Next steps
- Configure authentication to secure access to your registry
- Set up authorization to control access with roles and claims
- Configure sources and registries for standalone configuration reference
- Kubernetes source - the operator
automatically creates a Kubernetes source named
default
Related information
- Kubernetes CRD reference -
Reference for the
MCPRegistryCustom Resource Definition (CRD) - Deploy the operator - Install the ToolHive operator
- Database configuration - Configure PostgreSQL storage
Troubleshooting
MCPRegistry resource not creating pods
If your MCPRegistry resource is created but no pods appear:
- Ensure you created the
MCPRegistryresource in an allowed namespace - Check the operator's configuration:
helm get values toolhive-operator -n toolhive-system
- Check the MCPRegistry status and operator logs:
# Check MCPRegistry status
kubectl -n <NAMESPACE> describe mcpregistry <NAME>
# Check operator logs
kubectl -n toolhive-system logs -l app.kubernetes.io/name=toolhive-operator
# Verify the operator is running
kubectl -n toolhive-system get pods -l app.kubernetes.io/name=toolhive-operator
Common causes include:
- Operator not running: Ensure the ToolHive operator is deployed and running
- RBAC issues: Check for cluster-level permission issues
- Resource quotas: Check if namespace resource quotas prevent pod creation
Registry stuck in Pending or Syncing phase
If the registry is stuck in Pending or Syncing phase:
# Check registry status
kubectl -n <NAMESPACE> describe mcpregistry <NAME>
# Check registry pod logs
kubectl -n <NAMESPACE> logs -l app.kubernetes.io/instance=<NAME>
Common causes include:
- Git repository inaccessible: Verify the repository URL is correct and accessible
- ConfigMap doesn't exist: Ensure referenced resources exist in the same namespace
- Network policies: Check if network policies are blocking external access
- Invalid registry file format: Verify the registry JSON file is valid
Database connection errors
If you see database connection errors:
# Check registry pod logs
kubectl -n <NAMESPACE> logs -l app.kubernetes.io/instance=<NAME>
Common causes include:
- Database not reachable: Verify database host and port are correct
- Invalid credentials: Check that pgpass file is properly mounted
- SSL configuration mismatch: Verify
sslModematches your database configuration - Permission issues: Ensure database users have required privileges
Sync failures
If synchronization is failing:
# Check sync status
kubectl -n <NAMESPACE> get mcpregistry <NAME> -o jsonpath='{.status.syncStatus}'
# Trigger manual sync to see immediate errors
kubectl annotate mcpregistry <NAME> \
toolhive.stacklok.dev/sync-trigger="$(date +%s)" \
--overwrite
# Check logs
kubectl -n <NAMESPACE> logs -l app.kubernetes.io/instance=<NAME>
Common causes include:
- Source unavailable: Git repository, API endpoint, or URL is inaccessible
- Invalid JSON format: Registry file contains invalid JSON
- Format mismatch: The
formatfield doesn't match the actual data format - Filter too restrictive: Filters may be excluding all servers