The registry system is one of ToolHive's key innovations - providing a curated catalog of trusted MCP servers with metadata, configuration, and provenance information. This document explains how registries work, how to use them, and how to host your own.
ToolHive was early to adopt the concept of an MCP server registry. The registry provides:
- Curated catalog of trusted MCP servers
- Metadata including tools, permissions, and configuration
- Provenance information for supply chain security
- Easy deployment - just reference by name
- Custom registries for organizations
graph TB
subgraph "Registry Sources"
Builtin[Built-in Registry<br/>Embedded JSON]
Git[Git Repository]
CM[ConfigMap]
ExtAPI[External Registry API<br/>ToolHive Registry Server<br/>or MCP Registry]
end
subgraph "ToolHive CLI"
CLI[thv CLI]
Provider[Provider Interface<br/>Local/Remote/API]
end
subgraph "Kubernetes"
MCPReg[MCPRegistry CRD]
Operator[thv-operator]
IntAPI[Internal Registry API<br/>Optional per-CRD]
end
Builtin --> Provider
ExtAPI --> Provider
Git --> MCPReg
CM --> MCPReg
Provider --> CLI
MCPReg --> Operator
Operator --> IntAPI
style Builtin fill:#81c784
style Git fill:#90caf9
style CM fill:#90caf9
style ExtAPI fill:#ce93d8
ToolHive ships with a curated registry from toolhive-catalog.
Features:
- Maintained by Stacklok
- Trusted and verified servers
- Provenance information
- Regular updates
Browse registry:
thv registry list
thv search <query>Run from registry:
thv run server-nameImplementation:
- Embedded:
pkg/registry/data/registry.json - Manager:
pkg/registry/provider.go,pkg/registry/provider_local.go,pkg/registry/provider_remote.go
Implementation: pkg/registry/types.go
{
"version": "1.0.0",
"last_updated": "2025-10-13T12:00:00Z",
"servers": {
"server-name": { /* ImageMetadata */ }
},
"remote_servers": {
"remote-name": { /* RemoteServerMetadata */ }
},
"groups": [
{ /* Group */ }
]
}Implementation: pkg/registry/types.go
{
"name": "weather-server",
"description": "Provides weather information for locations",
"tier": "Official",
"status": "active",
"image": "ghcr.io/stacklok/mcp-weather:v1.0.0",
"transport": "sse",
"target_port": 3000,
"tools": ["get-weather", "get-forecast"],
"permissions": {
"network": {
"outbound": {
"allow_host": ["api.weather.gov"],
"allow_port": [443]
}
}
},
"env_vars": [
{
"name": "API_KEY",
"description": "Weather API key",
"required": true,
"secret": true
}
],
"args": ["--port", "3000"],
"docker_tags": ["v1.0.0", "latest"],
"metadata": {
"stars": 150,
"pulls": 5000,
"last_updated": "2025-10-01T10:00:00Z"
},
"repository_url": "https://github.com/example/weather-mcp",
"tags": ["weather", "api", "official"],
"provenance": {
"sigstore_url": "https://rekor.sigstore.dev",
"repository_uri": "https://github.com/example/weather-mcp",
"signer_identity": "build@example.com",
"runner_environment": "github-actions",
"cert_issuer": "https://token.actions.githubusercontent.com"
}
}Implementation: pkg/registry/types.go
{
"name": "cloud-mcp-server",
"description": "Cloud-hosted MCP server",
"tier": "Partner",
"status": "active",
"url": "https://mcp.example.com/sse",
"transport": "sse",
"tools": ["data-analysis", "ml-inference"],
"headers": [
{
"name": "X-API-Key",
"description": "API key for authentication",
"required": true,
"secret": true
}
],
"env_vars": [
{
"name": "REGION",
"description": "Cloud region",
"required": false,
"default": "us-east-1"
}
],
"metadata": {
"stars": 200,
"last_updated": "2025-10-10T15:00:00Z"
},
"repository_url": "https://github.com/example/cloud-mcp",
"tags": ["cloud", "ml", "partner"]
}Implementation: pkg/registry/types.go
{
"name": "data-pipeline",
"description": "Data processing pipeline tools",
"servers": {
"data-ingestion": { /* ImageMetadata */ },
"data-transform": { /* ImageMetadata */ }
},
"remote_servers": {
"data-storage": { /* RemoteServerMetadata */ }
}
}List all servers:
thv registry listSearch by keyword:
thv search weatherShow server details:
thv registry info weather-serverImplementation: cmd/thv/app/registry.go, cmd/thv/app/search.go
Simple run:
thv run weather-serverWhat happens:
- Look up
weather-serverin registry - Get image, transport, permissions from metadata
- Prompt for required env vars
- Create RunConfig with registry defaults
- Deploy workload
With overrides:
thv run weather-server \
--env API_KEY=xyz \
--proxy-port 9000 \
--permission-profile custom.jsonUser overrides take precedence over registry defaults.
Implementation: cmd/thv/app/run.go
Registry defines requirements:
{
"env_vars": [
{
"name": "API_KEY",
"description": "Weather API key from weather.gov",
"required": true,
"secret": true
},
{
"name": "CACHE_TTL",
"description": "Cache TTL in seconds",
"required": false,
"default": "3600"
}
]
}ToolHive handles:
- Prompts for required variables if not provided
- Uses defaults for optional variables
- Stores secrets securely
- Adds to RunConfig
Implementation: pkg/registry/types.go
Organizations can provide their own registries.
Create registry JSON:
{
"version": "1.0.0",
"servers": {
"internal-tool": {
"name": "internal-tool",
"image": "registry.company.com/mcp/internal-tool:latest",
"transport": "stdio",
"permissions": { "network": { "outbound": { "insecure_allow_all": true }}}
}
}
}Add to ToolHive:
Custom registries can be configured in the ToolHive configuration file.
Configuration location:
- Linux:
~/.config/toolhive/config.yaml - macOS:
~/Library/Application Support/toolhive/config.yaml
Implementation: pkg/config/
Remote registries can be configured in the ToolHive configuration file to fetch registry data from external sources.
ToolHive fetches:
- On startup
- Caches locally
Authentication:
- Basic auth:
https://user:pass@registry.company.com/registry.json - Bearer token: via environment variable
Implementation: pkg/registry/provider.go, pkg/registry/provider_local.go, pkg/registry/provider_remote.go, pkg/registry/factory.go
ToolHive supports live MCP Registry API endpoints that implement the official MCP Registry API v0.1 specification. This enables on-demand querying of servers from dynamic registry APIs.
Key differences from Remote Registry:
- On-demand queries: Fetches servers as needed, not bulk download
- Live data: Always queries the latest data from the API
- Standard protocol: Uses official MCP Registry API specification
- Pagination support: Handles large registries via cursor-based pagination
- Search capabilities: Supports server search via API queries
Set API registry:
# URLs without .json extension are probed - if they implement /v0.1/servers, they're treated as API endpoints
thv config set-registry https://registry.example.comWith private IP support:
thv config set-registry https://registry.internal.company.com --allow-private-ipCheck current registry:
thv config get-registry
# Output: Current registry: https://registry.example.com (API endpoint)Unset API registry:
thv config unset-registryAPI Requirements:
The API endpoint must implement:
GET /v0.1/servers- List all servers with paginationGET /v0.1/servers/:name- Get specific server by reverse-DNS nameGET /v0.1/servers?search=<query>- Search serversGET /openapi.yaml- OpenAPI specification (version 1.0.0)
Response format:
Servers are returned in the upstream MCP Registry format:
{
"server": {
"name": "io.github.example/weather",
"description": "Weather information MCP server",
"packages": [
{
"registry_type": "oci",
"identifier": "ghcr.io/example/weather-mcp:v1.0.0",
"version": "v1.0.0"
}
],
"remotes": [],
"repository": {
"type": "git",
"url": "https://github.com/example/weather-mcp"
}
}
}Type conversion:
ToolHive automatically converts upstream MCP Registry types to internal format:
- Container servers:
packageswithregistry_type: "oci"→ImageMetadata - Remote servers:
remoteswith SSE/HTTP transport →RemoteServerMetadata - Package formats:
oci/docker→ Docker image referencenpm→npx://<package>@<version>pypi→uvx://<package>@<version>
Implementation:
pkg/registry/api/client.go- MCP Registry API clientpkg/registry/provider_api.go- API provider implementation with type conversionpkg/config/registry.go- Configuration methods (setRegistryAPI)pkg/registry/factory.go- Provider factory with API supportcmd/thv/app/config.go- CLI commands
Use cases:
- Connect to official MCP Registry at https://registry.modelcontextprotocol.io
- Point to organization's private MCP Registry API
- Use third-party registry services
- Dynamic server catalogs that update frequently
Stacklok's Registry Server Implementation:
For organizations needing a full-featured registry server, ToolHive Registry Server provides enterprise features:
- Multiple data sources (Git, API, File, Managed, Kubernetes)
- PostgreSQL backend for scalable storage
- Enterprise OAuth 2.0/OIDC authentication (Okta, Auth0, Azure AD)
- Background synchronization with automatic updates
- Docker Compose and Kubernetes/Helm deployment options
For detailed setup and configuration, see the Registry Server documentation.
When multiple registries configured, ToolHive uses this priority order:
- API Registry (if configured) - Highest priority for live data
- Remote Registry (if configured) - Static remote registry URL
- Local Registry (if configured) - Custom local file
- Built-in Registry - Default embedded registry
The factory selects the first configured registry type in this order. The thv config set-registry command auto-detects the registry type:
# API registry - URLs without .json are probed for /v0.1/servers endpoint
thv config set-registry https://registry.modelcontextprotocol.io
# Remote static registry - URLs ending in .json are treated as static files
thv config set-registry https://example.com/registry.json
# Local file registry
thv config set-registry /path/to/registry.json
# Check current registry configuration
thv config get-registry
# Remove custom registry (fall back to built-in)
thv config unset-registryImplementation: pkg/registry/factory.go, pkg/registry/provider.go, pkg/registry/provider_local.go, pkg/registry/provider_remote.go, pkg/registry/provider_api.go
For organizations requiring a centralized, scalable registry server, ToolHive Registry Server provides enterprise-grade capabilities.
| Scenario | Recommended Solution |
|---|---|
| Single user, local development | Built-in embedded registry (default) |
| Team sharing curated servers | Static JSON file via thv config set-registry https://example.com/registry.json |
| Dynamic organization-wide registry | Standalone ToolHive Registry Server with thv config set-registry https://registry.company.com |
| Kubernetes cluster with shared registry | MCPRegistry CRD (deploys ToolHive Registry Server in-cluster) |
| Multi-cluster enterprise | Standalone ToolHive Registry Server as central API, connect via thv config set-registry |
ToolHive Registry Server implements a 4-layer architecture:
- API Layer: Chi router with OAuth/OIDC middleware
- Service Layer: PostgreSQL or in-memory backends
- Registry Layer: Git, API, File, Managed, Kubernetes registry handlers
- Sync Layer: Background coordinator for automatic updates
| Type | Sync Mode | Description |
|---|---|---|
| API | Automatic | Upstream MCP Registry API endpoints |
| Git | Automatic | Git repositories containing registry JSON |
| File | Automatic | Local filesystem (ToolHive or upstream format) |
| Managed | On-demand | API-managed registries with publish/delete |
| Kubernetes | On-demand | K8s deployment discovery |
CLI configuration:
# Point CLI to your registry server
thv config set-registry https://registry.company.com
# For internal deployments
thv config set-registry https://registry.internal.company.com --allow-private-ipFor complete registry server documentation, see:
- Registry Server Guides - Configuration, authentication, deployment
- Registry API Reference - API endpoint documentation
- Upstream Registry Schema - Registry format reference
For Kubernetes deployments, registries managed via MCPRegistry CRD.
Implementation: cmd/thv-operator/api/v1beta1/mcpregistry_types.go
The MCPRegistry CRD uses a configYAML field that contains the complete
ToolHive Registry Server
config.yaml verbatim. The operator passes this content through to the
registry server without parsing or transforming it -- configuration
validation is the registry server's responsibility.
Any files referenced in configYAML (registry data, Git credentials, TLS
certs) must be mounted into the registry-api container via explicit
volumes and volumeMounts fields on the CRD.
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPRegistry
metadata:
name: company-registry
namespace: toolhive-system
spec:
configYAML: |
sources:
- name: company-repo
git:
repository: https://github.com/company/mcp-registry
branch: main
path: registry.json
syncPolicy:
interval: 1h
registries:
- name: default
sources: ["company-repo"]
database:
host: registry-db-rw
port: 5432
user: db_app
database: registry
auth:
mode: anonymousSources are defined inside configYAML. The registry server supports
several source types; the most common are Git, file (ConfigMap-backed),
and Kubernetes.
configYAML: |
sources:
- name: my-source
git:
repository: https://github.com/example/registry
branch: main
path: registry.json
syncPolicy:
interval: 1h
registries:
- name: default
sources: ["my-source"]
database:
host: postgres
port: 5432
user: db_app
database: registry
auth:
mode: anonymousFeatures:
- Automatic sync from Git repository
- Branch or tag tracking
- Shallow clones for efficiency
- Private repository authentication via HTTP Basic Auth
Private Repository Authentication:
Git credentials are mounted as files using volumes/volumeMounts and
referenced via passwordFile in the source configuration.
spec:
configYAML: |
sources:
- name: private-repo
git:
repository: https://github.com/org/private-registry
branch: main
path: registry.json
auth:
username: "git" # Use "git" for GitHub PATs
passwordFile: /secrets/git-credentials/token
syncPolicy:
interval: 1h
registries:
- name: default
sources: ["private-repo"]
database:
host: postgres
port: 5432
user: db_app
database: registry
auth:
mode: anonymous
volumes:
- name: git-auth-credentials
secret:
secretName: git-credentials
items:
- key: token
path: token
volumeMounts:
- name: git-auth-credentials
mountPath: /secrets/git-credentials
readOnly: trueThe password Secret is mounted explicitly into the registry-api pod via
the volumes and volumeMounts fields. The passwordFile path in
configYAML must match the mountPath.
Implementation: cmd/thv-operator/pkg/registryapi/
Registry data from a ConfigMap is served by using a file: source in
configYAML and mounting the ConfigMap with volumes/volumeMounts.
spec:
configYAML: |
sources:
- name: production
file:
path: /config/registry/production/registry.json
syncPolicy:
interval: 1h
registries:
- name: default
sources: ["production"]
database:
host: postgres
port: 5432
user: db_app
database: registry
auth:
mode: anonymous
volumes:
- name: registry-data-production
configMap:
name: mcp-registry-data
items:
- key: registry.json
path: registry.json
volumeMounts:
- name: registry-data-production
mountPath: /config/registry/production
readOnly: trueFeatures:
- Native Kubernetes resource
- Direct updates via kubectl
- No external dependencies
- File path in
configYAMLmust match themountPath
Implementation: cmd/thv-operator/pkg/registryapi/
Sync intervals are configured per-source inside configYAML:
configYAML: |
sources:
- name: my-source
git:
repository: https://github.com/example/registry
branch: main
path: registry.json
syncPolicy:
interval: 1hOmit the syncPolicy block on a source for manual-only sync.
Implementation: cmd/thv-operator/controllers/mcpregistry_controller.go
The operator always creates a registry API deployment for each MCPRegistry:
- Deployment: Running ToolHive Registry Server (image:
ghcr.io/stacklok/thv-registry-api) - Service: Exposing API endpoints
- ConfigMap: Containing the
configYAMLcontent mounted at/config/config.yaml
Access:
# Within cluster
curl http://company-registry-api.default.svc.cluster.local:8080/api/v1/registry
# Via port-forward
kubectl port-forward svc/company-registry-api 8080:8080
curl http://localhost:8080/api/v1/registryImplementation: cmd/thv-operator/pkg/registryapi/
Status fields:
status:
phase: Ready
message: "Registry API is ready and serving requests"
url: "http://company-registry-api.default.svc.cluster.local:8080"
readyReplicas: 1
observedGeneration: 1
conditions:
- type: Ready
status: "True"
reason: Ready
message: "Registry API is ready and serving requests"Phases:
Pending- Initial state, deployment not ready yetReady- Registry API is ready and serving requestsFailed- Deployment or reconciliation failedTerminating- Registry being deleted
Implementation: cmd/thv-operator/controllers/mcpregistry_controller.go
Registry data is managed by the registry server itself. The operator creates a
{name}-registry-server-config ConfigMap containing the registry server's
configuration (from configYAML), and the registry server fetches and stores
data from its configured sources (Git, API, Kubernetes, etc.) at runtime.
Required fields:
image- Container image referencedescription- What the server doestransport- Communication protocoltier- Classification (Official, Partner, Community)
Optional fields:
target_port- Port for SSE/Streamable HTTPpermissions- Permission profileenv_vars- Environment variable definitionsargs- Default command argumentsdocker_tags- Available tagsprovenance- Supply chain metadatatools- List of tool namesmetadata- Stars, pulls, last updatedrepository_url- Source code URLtags- Categorization labels
Implementation: pkg/registry/types.go
Required fields:
url- Remote server endpointdescription- What the server doestransport- Must besseorstreamable-httptier- Classification
Optional fields:
headers- HTTP headers for authenticationoauth_config- OAuth/OIDC configurationenv_vars- Client environment variablestools- List of tool namesmetadata- Popularity metricsrepository_url- Documentation URLtags- Categorization labels
Implementation: pkg/registry/types.go
Structure:
{
"name": "data-pipeline",
"description": "Complete data processing pipeline",
"servers": {
"data-reader": { /* ImageMetadata */ },
"data-processor": { /* ImageMetadata */ }
},
"remote_servers": {
"data-warehouse": { /* RemoteServerMetadata */ }
}
}Use cases:
- Deploy related servers together
- Virtual MCP aggregation
- Organizational structure
Run all servers in group:
thv group run data-pipeline # assuming 'data-pipeline' is defined in your registryImplementation: pkg/registry/types.go
ToolHive supports Sigstore verification:
Provenance fields:
sigstore_url- Sigstore/Rekor instancerepository_uri- Source repositoryrepository_ref- Git ref (tag, commit)signer_identity- Who built the imagerunner_environment- Build environmentcert_issuer- Certificate authorityattestation- SLSA attestation data
Verification:
thv run weather-server --image-verification enabledImplementation:
pkg/registry/types.go- Provenance type definitionspkg/container/verifier/- Sigstore/cosign verification using sigstore-go librarypkg/runner/retriever/retriever.go- Image verification orchestration
Best practices:
- Pin image tags: Use specific versions, not
latest - Verify provenance: Check signer identity
- Review permissions: Audit network/file access
- Check repository: Review source code
- Monitor updates: Track registry updates
ToolHive consumes registries in the upstream MCP registry format. The legacy ToolHive-native format is no longer accepted; existing files can be migrated with thv registry convert --in <file> --in-place.
Key features:
- Standardized schema: Upstream MCP server format from the modelcontextprotocol/registry project
- Publisher-provided extensions: ToolHive-specific metadata via
_meta["io.modelcontextprotocol.registry/publisher-provided"] - Lossless migration: Every legacy ToolHive field maps to a publisher-provided extension on the corresponding upstream server entry
ToolHive uses the io.modelcontextprotocol.registry/publisher-provided extension mechanism to add custom metadata to MCP server definitions in the upstream format. This allows ToolHive to provide:
- Security permissions for container-based servers
- OAuth/OIDC configuration for remote servers
- Categorization metadata (tags, tier, tools)
- Supply chain provenance information
- Popularity metrics (stars, pulls, last_updated)
Extension structure:
{
"_meta": {
"io.modelcontextprotocol.registry/publisher-provided": {
"io.github.stacklok": {
"ghcr.io/stacklok/mcp-server-example:latest": {
"status": "active",
"tier": "Official",
"tools": ["example-tool"],
"permissions": {
"network": {
"outbound": {
"allow_host": ["api.example.com"]
}
}
}
}
}
}
}
}For the complete schema definition, see:
- Schemas: published in
stacklok/toolhive-coreunderregistry/types/data/ - Documentation:
docs/registry/schema.md - Validation:
pkg/registry/schema_validation.go
Implementation: pkg/registry/
List servers:
thv registry listShow server info:
thv registry info <server-name>Implementation: cmd/thv/app/registry.go
Create registry:
kubectl apply -f mcpregistry.yamlCheck status:
kubectl get mcpregistry company-registry -o yamlTrigger manual sync:
kubectl annotate mcpregistry company-registry toolhive.stacklok.dev/sync-trigger=trueImplementation: cmd/thv-operator/controllers/mcpregistry_controller.go
- Core Concepts - Registry concept
- Architecture Overview - Registry in platform
- Deployment Modes - Registry usage per mode
- Groups - Groups in registry
- Operator Architecture - MCPRegistry CRD
- Skills System - Skills discovery and distribution via registry
- ToolHive User Documentation - User-facing guides
- Registry Server Documentation - Enterprise registry server
- Upstream Registry Schema - MCP standard format used by ToolHive
- Registry API Reference - API specification
- ToolHive Registry Server - Registry server component
- toolhive-catalog - Curated server catalog
- MCP Registry - Upstream MCP registry specification