A standards-compliant MCP Registry API server for ToolHive
The ToolHive Registry API (thv-registry-api) implements the official Model Context Protocol (MCP) Registry API specification. It provides a standardized REST API for discovering and accessing MCP servers from multiple backend sources.
- Standards-compliant: Implements the official MCP Registry API specification
- Multiple data sources: Git repositories, API endpoints, and local files
- Automatic synchronization: Background sync with configurable intervals and retry logic
- Container-ready: Designed for deployment in Kubernetes clusters
- Flexible deployment: Works standalone or as part of ToolHive infrastructure
- Production-ready: Built-in health checks, graceful shutdown, and sync status persistence
- Go 1.23 or later
- Task for build automation
# Build the binary
task buildAll configuration is done via YAML configuration files. See the examples/ directory for sample configurations.
Quick start with Git source:
thv-registry-api serve --config examples/config-git.yamlWith local file:
thv-registry-api serve --config examples/config-file.yamlWith API endpoint:
thv-registry-api serve --config examples/config-api.yamlThe server starts on port 8080 by default. Use --address :PORT to customize.
What happens when the server starts:
- Loads configuration from the specified YAML file
- Immediately fetches registry data from the configured source
- Starts background sync coordinator for automatic updates
- Serves MCP Registry API endpoints on the configured address
For detailed configuration options and examples, see the examples/README.md.
The thv-registry-api CLI provides the following commands:
# Start the API server
thv-registry-api serve --config config.yaml [--address :8080]
# Run database migrations
thv-registry-api migrate up --config config.yaml [--yes]
thv-registry-api migrate down --config config.yaml --num-steps N [--yes]
# Display version information
thv-registry-api version [--format json]
# Show help
thv-registry-api --help
thv-registry-api <command> --helpSee the Database Migrations section for more details on using migration commands.
The server implements the standard MCP Registry API:
GET /api/v0/servers- List all available MCP serversGET /api/v0/servers/{name}- Get details for a specific serverGET /api/v0/deployed- List deployed server instances (Kubernetes only)GET /api/v0/deployed/{name}- Get deployed instances of a specific server
See the MCP Registry API specification for full API details. Note: The current implementation is not strictly compliant with the standard. The deviations will be fixed in the next iterations.
All configuration is done via YAML files. The server requires a --config flag pointing to a YAML configuration file.
# Registry name/identifier (optional, defaults to "default")
registryName: my-registry
# Data source configuration (required)
source:
# Source type: git, api, or file
type: git
# Data format: toolhive (native) or upstream (MCP registry format)
format: toolhive
# Source-specific configuration
git:
repository: https://github.com/stacklok/toolhive.git
branch: main
path: pkg/registry/data/registry.json
# Automatic sync policy (required)
syncPolicy:
# Sync interval (e.g., "30m", "1h", "24h")
interval: "30m"
# Optional: Server filtering
filter:
names:
include: ["official/*"]
exclude: ["*/deprecated"]
tags:
include: ["production"]
exclude: ["experimental"]
# Optional: Database configuration
database:
host: localhost
port: 5432
user: registry
passwordFile: /secrets/db-password # Recommended for production
database: registry
sslMode: require
maxOpenConns: 25
maxIdleConns: 5
connMaxLifetime: "5m"| Flag | Description | Required | Default |
|---|---|---|---|
--config |
Path to YAML configuration file | Yes | - |
--address |
Server listen address | No | :8080 |
The server supports three data source types:
-
Git Repository - Clone and sync from Git repositories
- Supports branch, tag, or commit pinning
- Ideal for version-controlled registries
- Example: config-git.yaml
-
API Endpoint - Sync from upstream MCP Registry APIs
- Supports federation and aggregation scenarios
- Format conversion from upstream to ToolHive format
- Example: config-api.yaml
-
Local File - Read from filesystem
- Ideal for local development and testing
- Supports mounted volumes in containers
- Example: config-file.yaml
For complete configuration examples and advanced options, see examples/README.md.
The server optionally supports PostgreSQL database connectivity for storing registry state and metadata.
| 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 |
passwordFile |
string | No* | - | Path to file containing the database password |
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") |
* Password configuration is required but has multiple sources (see Password Security below)
The server supports secure password management with the following priority order:
-
Password File (Recommended for production):
- Set
passwordFileto the path of a file containing only the password - The file content will have leading/trailing whitespace trimmed
- Ideal for Kubernetes secrets mounted as files
- Example:
database: passwordFile: /secrets/db-password
- Set
-
Environment Variable:
- Set
THV_DATABASE_PASSWORDenvironment variable - Used if
passwordFileis not specified - Example:
export THV_DATABASE_PASSWORD="your-secure-password" thv-registry-api serve --config config.yaml
- Set
Security Best Practices:
- Never commit passwords directly in configuration files
- Use password files with restricted permissions (e.g.,
chmod 400) - In Kubernetes, mount passwords from Secrets
- Rotate passwords regularly
The server uses connection pooling for efficient database resource management:
- MaxOpenConns: Limits concurrent database connections to prevent overwhelming the database
- MaxIdleConns: Maintains idle connections for faster query execution
- ConnMaxLifetime: Automatically closes and recreates connections to prevent connection leaks
Tune these values based on your workload:
- High-traffic scenarios: Increase
maxOpenConnsandmaxIdleConns - Resource-constrained environments: Decrease pool sizes
- Long-running services: Set shorter
connMaxLifetime(e.g., "1h")
The server includes built-in database migration commands to manage the database schema.
Running migrations with CLI:
# Apply all pending migrations
thv-registry-api migrate up --config examples/config-database-dev.yaml
# Apply migrations non-interactively (useful for CI/CD)
thv-registry-api migrate up --config config.yaml --yes
# Revert last migration (requires --num-steps for safety)
thv-registry-api migrate down --config config.yaml --num-steps 1
# View migration help
thv-registry-api migrate --helpRunning migrations with Task:
# Apply migrations (development)
export THV_DATABASE_PASSWORD="devpassword"
task migrate-up CONFIG=examples/config-database-dev.yaml
# Revert migrations (specify number of steps for safety)
task migrate-down CONFIG=examples/config-database-dev.yaml NUM_STEPS=1Migration workflow:
- Configure database: Create a config file with database settings (see examples/config-database-dev.yaml)
- Set password: Either set
THV_DATABASE_PASSWORDenv var or usepasswordFilein config - Run migrations: Use
migrate upto apply schema changes - Start server: Run
servecommand with the same config file
Example: Local development setup
# 1. Start PostgreSQL (example with Docker)
docker run -d --name postgres \
-e POSTGRES_USER=thv_user \
-e POSTGRES_PASSWORD=devpassword \
-e POSTGRES_DB=toolhive_registry \
-p 5432:5432 \
postgres:16
# 2. Set password environment variable
export THV_DATABASE_PASSWORD="devpassword"
# 3. Run migrations
task migrate-up CONFIG=examples/config-database-dev.yaml
# 4. Start the server
thv-registry-api serve --config examples/config-database-dev.yamlExample: Production deployment
# 1. Create password file
echo "your-secure-password" > /run/secrets/db_password
chmod 400 /run/secrets/db_password
# 2. Run migrations (using passwordFile from config)
thv-registry-api migrate up \
--config examples/config-database-prod.yaml \
--yes
# 3. Start the server
thv-registry-api serve --config examples/config-database-prod.yamlSafety features:
migrate downrequires--num-stepsflag to prevent accidental full rollback- Interactive confirmation prompts (bypass with
--yesflag) - Strong warnings displayed for destructive operations
- Configuration validation before connecting to database
For complete examples, see:
- examples/config-database-dev.yaml - Development configuration
- examples/config-database-prod.yaml - Production configuration
apiVersion: v1
kind: Secret
metadata:
name: registry-db-password
type: Opaque
stringData:
password: your-secure-password
---
apiVersion: v1
kind: ConfigMap
metadata:
name: registry-api-config
data:
config.yaml: |
registryName: my-registry
source:
type: git
format: toolhive
git:
repository: https://github.com/stacklok/toolhive.git
branch: main
path: pkg/registry/data/registry.json
syncPolicy:
interval: "15m"
database:
host: postgres.default.svc.cluster.local
port: 5432
user: registry
passwordFile: /secrets/db-password
database: registry
sslMode: require
maxOpenConns: 25
maxIdleConns: 5
connMaxLifetime: "5m"
---
# Run migrations as a Kubernetes Job before deploying the server
apiVersion: batch/v1
kind: Job
metadata:
name: registry-migrate
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: migrate
image: ghcr.io/stacklok/toolhive/thv-registry-api:latest
args:
- migrate
- up
- --config=/etc/registry/config.yaml
- --yes
volumeMounts:
- name: config
mountPath: /etc/registry
- name: db-password
mountPath: /secrets
readOnly: true
volumes:
- name: config
configMap:
name: registry-api-config
- name: db-password
secret:
secretName: registry-db-password
items:
- key: password
path: db-password
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry-api
spec:
template:
spec:
containers:
- name: registry-api
image: ghcr.io/stacklok/toolhive/thv-registry-api:latest
args:
- serve
- --config=/etc/registry/config.yaml
volumeMounts:
- name: config
mountPath: /etc/registry
- name: db-password
mountPath: /secrets
readOnly: true
volumes:
- name: config
configMap:
name: registry-api-config
- name: db-password
secret:
secretName: registry-db-password
items:
- key: password
path: db-password# Build the binary
task build
# Run linting
task lint
# Fix linting issues automatically
task lint-fix
# Run tests
task test
# Generate mocks
task gen
# Build container image
task build-image
# Database migrations
task migrate-up CONFIG=examples/config-database-dev.yaml
task migrate-down CONFIG=examples/config-database-dev.yaml NUM_STEPS=1cmd/thv-registry-api/
├── api/ # REST API implementation
│ └── v1/ # API v1 handlers and routes
├── app/ # CLI commands and application setup
├── internal/service/ # Legacy service layer (being refactored)
│ ├── file_provider.go # File-based registry provider
│ ├── k8s_provider.go # Kubernetes provider
│ └── service.go # Core service implementation
└── main.go # Application entry point
pkg/
├── config/ # Configuration loading and validation
├── sources/ # Data source handlers
│ ├── git.go # Git repository source
│ ├── api.go # API endpoint source
│ ├── file.go # File system source
│ ├── factory.go # Source handler factory
│ └── storage_manager.go # Storage abstraction
├── sync/ # Sync manager and coordination
│ └── manager.go # Background sync logic
└── status/ # Sync status tracking
└── persistence.go # Status file persistence
examples/ # Example configurations
The server follows a clean architecture pattern with the following layers:
- API Layer (
cmd/thv-registry-api/api): HTTP handlers implementing the MCP Registry API - Service Layer (
cmd/thv-registry-api/internal/service): Legacy business logic (being refactored) - Configuration Layer (
pkg/config): YAML configuration loading and validation - Source Handler Layer (
pkg/sources): Pluggable data source implementationsGitSourceHandler: Clones Git repositories and extracts registry filesAPISourceHandler: Fetches from upstream MCP Registry APIsFileSourceHandler: Reads from local filesystem
- Sync Manager (
pkg/sync): Coordinates automatic registry synchronization - Storage Layer (
pkg/sources): Persists registry data to local storage - Status Tracking (
pkg/status): Tracks and persists sync status
The project uses table-driven tests with mocks generated via go.uber.org/mock:
# Generate mocks before testing
task gen
# Run all tests
task testThe Registry API is designed to run as a sidecar container alongside the ToolHive Operator's MCPRegistry controller. Example deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry-api
spec:
template:
spec:
containers:
- name: registry-api
image: ghcr.io/stacklok/toolhive/thv-registry-api:latest
args:
- serve
- --config=/etc/registry/config.yaml
ports:
- containerPort: 8080
volumeMounts:
- name: config
mountPath: /etc/registry
volumes:
- name: config
configMap:
name: registry-api-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: registry-api-config
data:
config.yaml: |
registryName: my-registry
source:
type: git
format: toolhive
git:
repository: https://github.com/stacklok/toolhive.git
branch: main
path: pkg/registry/data/registry.json
syncPolicy:
interval: "15m"# Build the image
task build-image
# Run with Git source
docker run -v $(pwd)/examples:/config \
ghcr.io/stacklok/toolhive/thv-registry-api:latest \
serve --config /config/config-git.yaml
# Run with file source (mount local registry file)
docker run -v $(pwd)/examples:/config \
-v /path/to/registry.json:/data/registry.json \
ghcr.io/stacklok/toolhive/thv-registry-api:latest \
serve --config /config/config-file.yaml
# Run with database password from environment variable
docker run -v $(pwd)/examples:/config \
-e THV_DATABASE_PASSWORD=your-password \
ghcr.io/stacklok/toolhive/thv-registry-api:latest \
serve --config /config/config-database-dev.yamlA complete Docker Compose setup is provided in the repository root that includes PostgreSQL, automatic migrations, and the API server.
Quick start:
# Start all services (PostgreSQL + migrations + API)
docker-compose up
# Run in detached mode
docker-compose up -d
# View logs
docker-compose logs -f registry-api
# Stop all services
docker-compose down
# Stop and remove volumes (WARNING: deletes database data)
docker-compose down -vArchitecture:
The docker-compose.yaml includes three services:
- postgres - PostgreSQL 18 database server
- migrate - One-time migration service (runs schema migrations)
- registry-api - Main API server
Service startup flow:
postgres (healthy) → migrate (completes) → registry-api (starts)
Configuration:
- Config file:
examples/config-docker.yaml - Sample data:
examples/registry-sample.json - Database password: Set via
THV_DATABASE_PASSWORDenvironment variable in docker-compose.yaml
The setup demonstrates:
- Database-backed registry storage
- Automatic schema migrations on startup
- File-based data source (for demo purposes)
- Proper service dependencies and health checks
Accessing the API:
Once running, the API is available at http://localhost:8080
# List all servers
curl http://localhost:8080/api/v0/servers
# Get specific server
curl http://localhost:8080/api/v0/servers/example%2FfilesystemCustomization:
To use your own registry data:
- Edit
examples/registry-sample.jsonwith your MCP servers - Or change the source configuration in
examples/config-docker.yaml - Restart:
docker-compose restart registry-api
Database access:
To connect to the PostgreSQL database directly:
# Using psql
docker exec -it toolhive-registry-postgres psql -U registry -d registry
# Using environment variables from compose
PGPASSWORD=registry_password psql -h localhost -U registry -d registryThe Registry API server works seamlessly with the ToolHive ecosystem:
- ToolHive Operator: Automatically deployed as part of MCPRegistry resources
See the ToolHive documentation for more details.
We welcome contributions! Please see:
- Run
task lint-fixbefore committing - Ensure tests pass with
task test - Follow Go standard project layout
- Use mockgen for test mocks, not hand-written mocks
- See CLAUDE.md for AI assistant guidance
This project is licensed under the Apache 2.0 License.
Part of the ToolHive project - Simplify and secure MCP servers