Quickstart: Registry Server
In this tutorial, you'll deploy the ToolHive Registry Server on a local Kubernetes cluster and query its API. By the end, you'll have a working Registry Server serving MCP server entries from a local file, with anonymous authentication for fast experimentation.
This quickstart covers the standalone Registry Server that you host and curate yourself. If you're looking for the built-in catalog that ships with the ToolHive CLI and UI, see the Introduction for a comparison.
What you'll learn
- How to install the ToolHive Operator, which bundles the
MCPRegistryCRD - How to deploy a minimal PostgreSQL database for the Registry Server
- How to define registry data in a ConfigMap using the upstream MCP registry format
- How to create an
MCPRegistryresource that reads from that ConfigMap - How to query the Registry API
Prerequisites
Before starting, make sure you have:
- Helm (v3.10 minimum, v3.14+ recommended)
kubectl- Docker or Podman running
- kind
jqfor formatting the API responses in Step 6- Basic familiarity with Kubernetes concepts
Step 1: Create a kind cluster
Create a local Kubernetes cluster named registry-quickstart:
kind create cluster --name registry-quickstart
Verify the cluster is running:
kubectl cluster-info
Step 2: Install the ToolHive Operator
The Registry Server is managed through the ToolHive Operator using MCPRegistry
custom resources. Install the operator and its CRDs (which include
MCPRegistry):
helm upgrade --install toolhive-operator-crds \
oci://ghcr.io/stacklok/toolhive/toolhive-operator-crds \
-n toolhive-system --create-namespace
helm upgrade --install toolhive-operator \
oci://ghcr.io/stacklok/toolhive/toolhive-operator \
-n toolhive-system --create-namespace
Wait for the operator pod to become ready:
kubectl wait --for=condition=Ready pod \
-l app.kubernetes.io/name=toolhive-operator \
-n toolhive-system --timeout=120s
The operator CRDs chart bundles MCPRegistry alongside the other ToolHive CRDs,
so installing it makes the Registry Server resource type available in your
cluster. The operator itself watches for MCPRegistry resources and provisions
the Registry Server pod, service, and RBAC for each one.
Step 3: Deploy a PostgreSQL database
The Registry Server requires a PostgreSQL database. For this quickstart, you
deploy a minimal single-pod PostgreSQL instance in the toolhive-system
namespace. This setup has no persistent storage or TLS, so don't use it for
production workloads. Production deployments should use separate application and
migration users with distinct privileges. See
Database configuration for the production pattern.
Create a manifest that defines the credentials Secret, Deployment, and Service:
apiVersion: v1
kind: Secret
metadata:
name: postgres-credentials
namespace: toolhive-system
type: Opaque
stringData:
POSTGRES_USER: registry
POSTGRES_PASSWORD: quickstart
POSTGRES_DB: registry
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: toolhive-system
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
envFrom:
- secretRef:
name: postgres-credentials
ports:
- containerPort: 5432
readinessProbe:
exec:
command: ['pg_isready', '-U', 'registry', '-d', 'registry']
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: toolhive-system
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
Apply the manifest and wait for the database to be ready:
kubectl apply -f postgres.yaml
kubectl wait --for=condition=Ready pod -l app=postgres \
-n toolhive-system --timeout=120s
Create a pgpass Secret for the Registry Server to use when connecting:
kubectl create secret generic registry-pgpass \
-n toolhive-system \
--from-literal=.pgpass='postgres:5432:registry:registry:quickstart'
The Registry Server uses PostgreSQL's standard
pgpass file format
for credentials. The line above maps to
hostname:port:database:username:password. The operator mounts this Secret into
the Registry Server pod and points the server at it automatically when you set
pgpassSecretRef on the MCPRegistry resource.
Step 4: Define your registry data
Define registry data in a ConfigMap. The Registry Server reads it through a file-based source. The ConfigMap uses the upstream MCP registry format with a single example server entry:
apiVersion: v1
kind: ConfigMap
metadata:
name: registry-data
namespace: toolhive-system
data:
registry.json: |
{
"version": "1.0.0",
"meta": {
"last_updated": "2026-04-21T00:00:00Z"
},
"data": {
"servers": [
{
"name": "io.github.stacklok/fetch",
"description": "Fetch web content for AI agents",
"title": "fetch",
"repository": {
"url": "https://github.com/StacklokLabs/gofetch",
"source": "github"
},
"version": "1.0.0",
"packages": [
{
"registryType": "oci",
"identifier": "ghcr.io/stackloklabs/gofetch/server:latest",
"transport": {
"type": "stdio"
}
}
],
"_meta": {
"io.modelcontextprotocol.registry/publisher-provided": {
"io.github.stacklok": {
"ghcr.io/stackloklabs/gofetch/server:latest": {
"status": "Active",
"tier": "Community",
"tags": ["web", "fetch"],
"tools": ["fetch"]
}
}
}
}
}
]
}
}
Apply the ConfigMap:
kubectl apply -f registry-data.yaml
Step 5: Create an MCPRegistry resource
Create an MCPRegistry that mounts the ConfigMap, reads from the file, and
exposes the data through a registry named default:
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPRegistry
metadata:
name: my-registry
namespace: toolhive-system
spec:
displayName: My Registry
pgpassSecretRef:
name: registry-pgpass
key: .pgpass
volumes:
- name: registry-data
configMap:
name: registry-data
volumeMounts:
- name: registry-data
mountPath: /data/registry
readOnly: true
configYAML: |
sources:
- name: local
file:
path: /data/registry/registry.json
syncPolicy:
interval: '15m'
registries:
- name: default
sources: ["local"]
auth:
mode: anonymous
database:
host: postgres
port: 5432
user: registry
database: registry
sslMode: disable
Apply the resource and wait for the Registry Server pod to become ready:
kubectl apply -f my-registry.yaml
kubectl wait --for=condition=Ready pod \
-l app.kubernetes.io/instance=my-registry \
-n toolhive-system --timeout=180s
The operator reconciles the MCPRegistry resource and creates a Deployment that
runs the Registry Server container. On startup, the server:
- Runs database migrations against the
registrydatabase - Reads the registry JSON from the mounted ConfigMap
- Starts serving the MCP Registry API on port 8080
The auth.mode: anonymous setting disables authentication entirely, which is
appropriate for local experimentation. Never use anonymous mode in production.
Step 6: Query the Registry API
The operator exposes each MCPRegistry through a Service named
<registry-name>-api. For this quickstart, that's my-registry-api. Confirm it
exists:
kubectl get svc my-registry-api -n toolhive-system
Port-forward the Service to your local machine. This command blocks, so leave it running and open a separate terminal for the curl commands below:
kubectl port-forward svc/my-registry-api 8080:8080 -n toolhive-system
In the separate terminal, list servers in the default registry:
curl -s http://localhost:8080/registry/default/v0.1/servers | jq .
You should see a response containing the io.github.stacklok/fetch entry you
defined in the ConfigMap.
Fetch a specific entry by name. The / inside server names is part of the
identifier (reverse-DNS convention), so URL-encode it as %2F:
curl -s http://localhost:8080/registry/default/v0.1/servers/io.github.stacklok%2Ffetch \
| jq .
Step 7: Clean up
When you're done experimenting, delete the kind cluster to remove every resource you created:
kind delete cluster --name registry-quickstart
The Registry Server is available in both ToolHive Community and Stacklok Enterprise. Enterprise adds turnkey IdP integration (Okta, Entra ID), supply-chain-attested images, and SLA-backed support for teams running an internal MCP catalog at scale.
Next steps
You've deployed the Registry Server, defined a registry in a file, and queried the API. From here, explore the pieces you're most likely to need for a real deployment:
- Configure sources and registries to sync from Git repositories, upstream APIs, or Kubernetes discovery instead of a single file
- Set up authentication with OAuth/OIDC to replace the anonymous mode used here
- Deploy the Registry Server in production using the Helm chart or raw manifests
- Configure the database for production, including separate application and migration users, TLS, and persistent storage
Related information
- Deploy the ToolHive Operator covers production installation, upgrades, and namespace-consistency considerations for the operator used in this quickstart
- Deploy the Registry Server compares the operator, Helm, and manual deployment methods
Troubleshooting
MCPRegistry pod stuck in pending or crash-looping
Check the Registry Server pod status and logs:
kubectl get pods -n toolhive-system -l app.kubernetes.io/instance=my-registry
kubectl logs -n toolhive-system -l app.kubernetes.io/instance=my-registry
The most common causes are database connectivity problems and malformed registry
JSON. Verify that the postgres pod is ready and that the ConfigMap contains
valid JSON.
Empty response from the /servers endpoint
Confirm the source synced successfully by describing the MCPRegistry:
kubectl describe mcpregistry my-registry -n toolhive-system
The status conditions report sync state. If the source reports a parse error, re-apply the ConfigMap with corrected JSON and wait for the next sync interval, or restart the pod to force an immediate sync.
Operator pod not running
If the operator pod never becomes ready, check its logs:
kubectl logs -n toolhive-system deployment/toolhive-operator
Make sure the operator CRDs chart was installed before the operator chart. The operator needs the CRDs to exist before it starts.