Skip to main content

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 MCPRegistry CRD
  • 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 MCPRegistry resource 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
  • jq for 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
What's happening?

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:

postgres.yaml
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'
What's happening?

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:

registry-data.yaml
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:

my-registry.yaml
apiVersion: toolhive.stacklok.dev/v1alpha1
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
What's happening?

The operator reconciles the MCPRegistry resource and creates a Deployment that runs the Registry Server container. On startup, the server:

  1. Runs database migrations against the registry database
  2. Reads the registry JSON from the mounted ConfigMap
  3. 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
Stacklok Enterprise

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.

Learn more about Stacklok Enterprise.

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:

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.