Complete guide for setting up and using a self-hosted Model Context Protocol (MCP) Registry on Kubernetes for local development and testing.
- Overview
- Quick Start
- Architecture
- Configuration
- Authentication
- Server Publishing
- Server Management
- Troubleshooting
- Production Migration
- Resources
This implementation provides:
- Self-hosted MCP Registry (v1.3.10) on Kubernetes with PostgreSQL 16 backend
- Anonymous Authentication for local development (no GitHub/DNS auth required)
- Local MCP Server execution via Docker packages
- Remote MCP Server deployment with HTTP/SSE transport protocols
- Server Publishing with PowerShell automation scripts
- Multi-transport Support for stdio, Docker, streamable-http, and SSE
Local MCP Registry
Self-hosted registry running on Kubernetes with published servers
MCP AI Control Panel
Managing MCP servers in VS Code Copilot settings
Allowed MCP Servers
Configured and enabled MCP servers ready for use
Disabled MCP Servers
MCP servers that can be enabled when needed
✅ Local Development Focus
- Anonymous authentication enabled (no production auth complexity)
- Registry validation disabled (allows non-existent packages)
- Persistent PostgreSQL storage
- Ingress routing with friendly hostname
✅ Publishing Workflow
- Automated PowerShell scripts for publishing
- Version management and history
- Support for both package-based and remote servers
- Namespace-based access control
✅ Kubernetes Native
- Containerized deployment
- Service discovery
- ConfigMap and Secret management
- Health checks and readiness probes
- Kubernetes cluster (Docker Desktop, Minikube, or kind)
- kubectl CLI configured
- PowerShell 7+ or PowerShell Core
- Administrator access (for hosts file modification)
# Deploy everything (registry + PKI + optional example servers)
.\scripts\deploy-all.ps1 -WaitWindows (requires Administrator):
- Open Notepad as Administrator
- Open file:
C:\Windows\System32\drivers\etc\hosts - Add these lines at the end:
127.0.0.1 mcp-registry.local
127.0.0.1 mcp-devtools.local
4. Save and close
**macOS/Linux:**
```bash
sudo sh -c 'echo "127.0.0.1 mcp-registry.local" >> /etc/hosts'
sudo sh -c 'echo "127.0.0.1 mcp-devtools.local" >> /etc/hosts'
Verify:
ping mcp-registry.local
# Should resolve to 127.0.0.1# Check pods
kubectl get pods -n mcp-registry
# Expected output:
# NAME READY STATUS RESTARTS AGE
# mcp-registry-xxxxx-xxxxx 1/1 Running 0 1m
# postgres-xxxxx-xxxxx 1/1 Running 0 1m
# Test health endpoint (HTTPS)
curl https://mcp-registry.local/v0.1/health
# List servers
curl https://mcp-registry.local/v0.1/serversThis repo is set up for HTTPS everywhere (registry + remote MCP servers). For local development, the key is making your client trust the local CA.
# Create a local Root CA inside the cluster, create the CA issuer, and export the CA to .\.certs\...
.\scripts\setup-local-ssl.ps1
# Recommended on Windows: trust the CA and enable Node to use the Windows trust store
.\scripts\trust-local-ca-windows.ps1
# Quick validation (Node fetch against devtools + registry)
node -e "fetch('https://mcp-registry.local/v0.1/health').then(r=>console.log('registry',r.status)).catch(e=>console.error(e.cause||e))"
node -e "fetch('https://mcp-devtools.local/').then(r=>console.log('devtools',r.status)).catch(e=>console.error(e.cause||e))"This repo includes both:
- Local MCP server (runs on your machine via Docker + stdio):
time-local - Remote MCP server (runs in Kubernetes via HTTPS):
devtools-remote
- Ensure your workspace MCP config includes both servers:
- Local: Docker command (stdio)
- Remote:
https://mcp-devtools.local
See .vscode/mcp.json.
-
In VS Code, restart MCP servers (or reload the window).
-
Open Copilot Chat and copy/paste these test prompts:
Test local server (time-local):
Call time-local.get_current_time with timezone = UTC
Convert 13:37 from UTC to America/Los_Angeles using time-local.convert_time
Test remote server (devtools-remote):
Call devtools-remote.get_timestamp with format = iso
Call devtools-remote.generate_uuid with version = v4
Call devtools-remote.hash_text with text = hello and algorithm = sha256
Call devtools-remote.encode_decode with text = hello and operation = encode
If the remote calls fail with fetch failed, it usually means TLS trust is not set up. Run:
.\scripts\setup-local-ssl.ps1
.\scripts\trust-local-ca-windows.ps1This is not a full MCP protocol test, but it confirms HTTPS + DNS + certificate trust are working:
node -e "fetch('https://mcp-devtools.local/').then(r=>console.log('devtools',r.status)).catch(e=>console.error(e.cause||e))"Tip: a plain GET may return 406 (expected) because MCP endpoints expect specific headers; the important part is that TLS completes without certificate errors.
This repository includes two pre-configured example servers:
- mcp-time-local - Package-based server using Docker stdio transport
- mcp-devtools-remote - Remote server with streamable-http transport
# Publish all example servers
.\scripts\publish-servers.ps1
# Verify publication
curl https://mcp-registry.local/v0.1/servers | ConvertFrom-Json | Select-Object -ExpandProperty serversThe MCP DevTools server provides developer utility tools accessible via remote HTTP transport.
Features:
get_timestamp- Current timestamps in multiple formatsencode_decode- Base64 encoding/decodinggenerate_uuid- UUID v4/v5 generationhash_text- MD5, SHA1, SHA256, SHA512 hashing
Deploy to Kubernetes:
# Apply deployment
kubectl apply -f .\servers\mcp-devtools\k8s\deployment.yaml
# Add to hosts file (Windows - requires admin):
# C:\Windows\System32\drivers\etc\hosts
127.0.0.1 mcp-devtools.local
# Verify deployment
kubectl wait --for=condition=ready pod -l app=mcp-devtools -n mcp-devtools --timeout=60s
# TLS sanity check (a plain GET typically returns 406 because MCP expects specific Accept headers)
curl -i https://mcp-devtools.local/
# View logs
kubectl logs -n mcp-devtools -l app=mcp-devtoolsThe server will be accessible at https://mcp-devtools.local and can be used directly with VS Code MCP client:
// .vscode/mcp.json
{
"servers": {
"devtools-remote": {
"url": "https://mcp-devtools.local"
}
}
}Important: HTTPS + Trust
This repo assumes HTTPS everywhere. If your MCP client shows fetch failed, it’s usually not reachability — it’s that Node/VS Code does not trust the local CA.
Run the scripts in “Enable Local HTTPS (Recommended)” above.
See servers/mcp-devtools/README.md for full documentation.
Manual Publishing (Alternative):
# Get authentication token and publish
$authResp = curl -s https://mcp-registry.local/v0/auth/none -X POST | ConvertFrom-Json
$token = $authResp.registry_token
$headers = @{"Authorization" = "Bearer $token"}
Invoke-RestMethod -Uri "https://mcp-registry.local/v0.1/publish" `
-Method POST `
-ContentType "application/json" `
-Headers $headers `
-Body (Get-Content .\servers\your-server.json -Raw)PowerShell scripts are in scripts/.
- scripts/setup-local-ssl.ps1 - Creates a cluster-local Root CA (cert-manager), exports it to
.certs/. - scripts/trust-local-ca-windows.ps1 - Imports the CA into the Windows CurrentUser root store and enables Node system CA mode.
- scripts/deploy-all.ps1 - Applies PKI + registry + optional example servers with one command.
- scripts/publish-servers.ps1 - Publishes
*.server.jsondefinitions in servers/ to the local registry. - scripts/unpublish-servers.ps1 - Removes published servers by deleting rows directly from Postgres (dangerous; read help text first).
flowchart TD
A[Deploy cluster resources] --> B[cert-manager issues local Root CA]
B --> C[Ingress certs signed by local CA]
C --> D[Export Root CA PEM to .certs/]
D --> E[Trust Root CA on developer machine]
E --> F[Node/VS Code accepts TLS chain]
F --> G[VS Code MCP connects to https://mcp-devtools.local]
F --> H[Publisher uses https://mcp-registry.local]
classDef deploy fill:#e1f5ff,stroke:#0078d4,stroke-width:2px,color:#000
classDef cert fill:#fff4ce,stroke:#ca5010,stroke-width:2px,color:#000
classDef trust fill:#dff6dd,stroke:#107c10,stroke-width:2px,color:#000
classDef client fill:#f4ecff,stroke:#8764b8,stroke-width:2px,color:#000
class A deploy
class B,C,D cert
class E,F trust
class G,H client
flowchart LR
U[Developer] --> HOSTS[Add hosts entries]
HOSTS --> DEP["Run scripts/deploy-all.ps1 -Wait"]
DEP --> SSL["Run scripts/setup-local-ssl.ps1"]
SSL --> TRUST["Run scripts/trust-local-ca-windows.ps1"]
TRUST --> VS["Open VS Code workspace"]
VS --> MCP["Use .vscode/mcp.json servers"]
MCP --> LOCAL["Test time-local<br/>Docker/stdio"]
MCP --> REMOTE["Test devtools-remote<br/>HTTPS"]
VS --> PUB["Run scripts/publish-servers.ps1"]
classDef user fill:#f3f2f1,stroke:#323130,stroke-width:2px,color:#000
classDef setup fill:#fff4ce,stroke:#ca5010,stroke-width:2px,color:#000
classDef ssl fill:#dff6dd,stroke:#107c10,stroke-width:2px,color:#000
classDef vscode fill:#e1f5ff,stroke:#0078d4,stroke-width:2px,color:#000
classDef test fill:#f4ecff,stroke:#8764b8,stroke-width:2px,color:#000
class U user
class HOSTS,DEP setup
class SSL,TRUST ssl
class VS,MCP vscode
class LOCAL,REMOTE,PUB test
flowchart TB
subgraph DevMachine["Developer Machine"]
VSCode["VS Code MCP Client<br/>(Node extension host)"]
Docker["Docker<br/>(time-local package)"]
VSCode -->|stdio| Docker
end
subgraph Cluster["Kubernetes Cluster"]
CM["cert-manager<br/>local Root CA + ClusterIssuer"]
NGINX["Ingress Controller<br/>(mcp-*.local)"]
REG["MCP Registry<br/>Service/Pod"]
PG["PostgreSQL<br/>Service/Pod"]
DEV["MCP DevTools<br/>Service/Pod"]
REG --> PG
NGINX --> REG
NGINX --> DEV
CM -->|"signs TLS certs"| NGINX
end
VSCode -->|HTTPS| NGINX
classDef client fill:#e1f5ff,stroke:#0078d4,stroke-width:2px,color:#000
classDef security fill:#dff6dd,stroke:#107c10,stroke-width:2px,color:#000
classDef network fill:#fff4ce,stroke:#ca5010,stroke-width:2px,color:#000
classDef service fill:#f4ecff,stroke:#8764b8,stroke-width:2px,color:#000
classDef data fill:#fef0f1,stroke:#d13438,stroke-width:2px,color:#000
class VSCode,Docker client
class CM security
class NGINX network
class REG,DEV service
class PG data
┌─────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ Namespace: mcp-registry │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Nginx Ingress Controller │ │
│ │ mcp-registry.local (127.0.0.1) │ │
│ └──────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌──────────────────▼─────────────────────────────┐ │
│ │ MCP Registry Service │ │
│ │ Port 8080 │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ MCP Registry Pod │ │ │
│ │ │ ghcr.io/modelcontextprotocol/registry │ │ │
│ │ │ v1.3.10 │ │ │
│ │ │ │ │ │
│ │ │ Environment: │ │ │
│ │ │ - ENABLE_ANONYMOUS_AUTH=true │ │ │
│ │ │ - ENABLE_REGISTRY_VALIDATION=false │ │ │
│ │ │ - PUBLIC_BASE_URL=https://... │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └──────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌──────────────────▼─────────────────────────────┐ │
│ │ PostgreSQL Service (Port 5432) │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ PostgreSQL 16 Pod │ │ │
│ │ │ Database: mcp_registry │ │ │
│ │ │ │ │ │
│ │ │ Storage: PersistentVolumeClaim (2Gi) │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Publishing a Server:
Client → POST /v0/auth/none → JWT Token
↓
Client → POST /v0.1/publish (with token) → Registry Pod
↓
Registry Pod → Validate Schema & Permissions
↓
Registry Pod → Store in PostgreSQL
↓
Return Success Response
Listing Servers:
Client → GET /v0.1/servers → Registry Pod
↓
Registry Pod → Query PostgreSQL
↓
Return Server List (JSON)
env:
# Database connection
- name: MCP_REGISTRY_DATABASE_URL
value: "postgresql://postgres:postgres@postgres.mcp-registry.svc.cluster.local:5432/mcp_registry?sslmode=disable"
# JWT signing key (32 bytes hex)
- name: MCP_REGISTRY_JWT_PRIVATE_KEY
value: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"
# Public URL for API
- name: PUBLIC_BASE_URL
value: "https://mcp-registry.local"
# Enable anonymous auth (LOCAL DEV ONLY!)
- name: MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH
value: "true"
# Disable package validation (allows fake packages)
- name: MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION
value: "false"| Variable | Value | Purpose | Production? |
|---|---|---|---|
MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH |
"true" |
Enables /v0/auth/none endpoint |
❌ Must be false |
MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION |
"false" |
Skip npm/PyPI/Docker validation | ❌ Should be true |
PUBLIC_BASE_URL |
"https://mcp-registry.local" |
Base URL for API | ✅ Update for prod |
MCP_REGISTRY_JWT_PRIVATE_KEY |
64-char hex | Ed25519 seed for JWT signing | ✅ Use secrets |
# Check anonymous auth is enabled
kubectl -n mcp-registry get deploy mcp-registry -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH")].value}'
# Expected: true
# Check validation is disabled
kubectl -n mcp-registry get deploy mcp-registry -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION")].value}'
# Expected: false
# Check pods are running
kubectl -n mcp-registry get pods
# Expected: Both pods Running 1/1
# Test API
curl https://mcp-registry.local/v0.1/health
# Expected: {"status":"ok"}The registry supports anonymous authentication for local development:
Endpoint: POST /v0/auth/none
Response:
{
"registry_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
"expires_at": 1765643590
}Token Claims:
{
"auth_method": "none",
"auth_method_sub": "anonymous",
"permissions": [
{
"action": "publish",
"resource": "io.modelcontextprotocol.anonymous/*"
},
{
"action": "edit",
"resource": "io.modelcontextprotocol.anonymous/*"
}
]
}Important: Tokens are scoped to the io.modelcontextprotocol.anonymous/* namespace only.
$authResp = curl -s https://mcp-registry.local/v0/auth/none -X POST | ConvertFrom-Json
$token = $authResp.registry_token
Write-Host "Token expires at: $(Get-Date -UnixTimeSeconds $authResp.expires_at)"# Bash/curl
curl -X POST https://mcp-registry.local/v0/auth/none | jq -r '.registry_token'$headers = @{"Authorization" = "Bearer $token"}
Invoke-RestMethod -Uri "https://mcp-registry.local/v0.1/publish" `
-Method POST `
-Headers $headers `
-Body $jsonBodyNote: on Windows, PowerShell HTTPS requests use the Windows certificate trust store. If you see certificate errors, run .\scripts\trust-local-ca-windows.ps1 first.
# ❌ WRONG - Wrong field name
$token = $authResp.registryToken # Should be registry_token
# ❌ WRONG - Missing Bearer prefix
-Headers @{"Authorization" = "$token"}
# ❌ WRONG - Extra quotes
-Headers @{"Authorization" = "'Bearer $token'"}
# ✅ CORRECT
$token = $authResp.registry_token
-Headers @{"Authorization" = "Bearer $token"}{
"$schema": "2025-10-17",
"name": "io.modelcontextprotocol.anonymous/my-server",
"title": "My MCP Server",
"description": "Server description here",
"version": "1.0.0",
"packages": [
{
"registryType": "docker",
"identifier": "myorg/my-mcp-server",
"version": "1.0.0",
"transport": {
"type": "stdio"
}
}
]
}Remote servers MUST:
- Be publicly accessible via HTTPS (for production)
- Implement the MCP protocol over HTTP or SSE
- NOT use placeholder URLs like
httpbin.orgin production
For Local Testing Only:
When MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=false, you can use placeholder URLs for testing:
{
"$schema": "2025-10-17",
"name": "io.modelcontextprotocol.anonymous/test-remote-server",
"title": "Test Remote Server (Local Dev Only)",
"description": "⚠️ Uses placeholder URL - NOT for production use",
"version": "0.0.1",
"remotes": [
{
"type": "streamable-http",
"url": "https://httpbin.org/status/200"
}
]
}For Production/Real Deployments:
{
"$schema": "2025-10-17",
"name": "io.github.username/my-remote-server",
"title": "My Production Remote MCP Server",
"description": "Cloud-hosted MCP server",
"version": "1.0.0",
"remotes": [
{
"type": "streamable-http",
"url": "https://mcp.myservice.com/api/v1"
}
]
}With SSE Transport:
{
"remotes": [
{
"type": "sse",
"url": "https://mcp.myservice.com/events"
}
]
}Multi-transport Support:
{
"remotes": [
{
"type": "streamable-http",
"url": "https://mcp.myservice.com/http"
},
{
"type": "sse",
"url": "https://mcp.myservice.com/sse"
}
]
}With URL Variables (Multi-tenant):
{
"remotes": [
{
"type": "streamable-http",
"url": "https://{tenant_id}.mcp.myservice.com/api",
"variables": {
"tenant_id": {
"description": "Your tenant identifier",
"isRequired": true
}
}
}
]
}With Authentication Headers:
{
"remotes": [
{
"type": "streamable-http",
"url": "https://mcp.myservice.com/api",
"headers": [
{
"name": "X-API-Key",
"description": "API key for authentication",
"isRequired": true,
"isSecret": true
}
]
}
]
}- Schema Version: Use
"2025-10-17"(string, not URL) - Namespace: Must be
io.modelcontextprotocol.anonymous/*for local dev - Version: Use semantic versioning (e.g., "1.0.0"), not "latest"
- Remote URLs:
- Production: HTTPS, publicly accessible, actual MCP endpoints
- Local testing: Can use placeholders when validation is disabled
- Transport Types:
- Packages:
stdio - Remotes:
streamable-http(recommended) orsse
- Packages:
Use the provided script:
.\scripts\publish-servers.ps1
# With custom registry URL
.\scripts\publish-servers.ps1 -RegistryUrl "https://my-registry.local"
# Skip verification
.\scripts\publish-servers.ps1 -SkipVerification# 1. Get token
$authResp = curl -s https://mcp-registry.local/v0/auth/none -X POST | ConvertFrom-Json
$token = $authResp.registry_token
# 2. Publish server
$headers = @{"Authorization" = "Bearer $token"}
$body = Get-Content .\servers\my-server.json -Raw
Invoke-RestMethod -Uri "https://mcp-registry.local/v0.1/publish" `
-Method POST `
-ContentType "application/json" `
-Headers $headers `
-Body $body# List all servers
curl https://mcp-registry.local/v0.1/servers | ConvertFrom-Json |
Select-Object -ExpandProperty servers |
Select-Object @{N='name';E={$_.server.name}},
@{N='version';E={$_.server.version}},
@{N='title';E={$_.server.title}} |
Format-Table -AutoSize
# Get specific server
curl "https://mcp-registry.local/v0.1/servers?name=io.modelcontextprotocol.anonymous/my-server"
# Get specific version
curl "https://mcp-registry.local/v0.1/servers?name=io.modelcontextprotocol.anonymous/my-server&version=1.0.0"Use the unpublish script:
# Unpublish specific server version
.\scripts\unpublish-servers.ps1 -ServerName "io.modelcontextprotocol.anonymous/my-server" -Version "1.0.0"
# Unpublish all versions of a server
.\scripts\unpublish-servers.ps1 -ServerName "io.modelcontextprotocol.anonymous/my-server" -AllVersions
# Unpublish ALL servers in namespace (requires 'yes' confirmation)
.\scripts\unpublish-servers.ps1 -UnpublishAll
# Unpublish ALL servers without confirmation (DANGEROUS - use with caution!)
.\scripts\unpublish-servers.ps1 -UnpublishAll -SkipConfirmation-UnpublishAll option directly deletes ALL servers from the PostgreSQL database. Always verify what will be deleted before confirming!
Best Practices:
- Use semantic versioning:
MAJOR.MINOR.PATCH - Increment version for each publish
- Cannot publish duplicate versions (by design)
- All versions are preserved in history
Example progression:
0.0.1 → Initial development
0.0.2 → Bug fix
0.1.0 → New features
1.0.0 → Stable release
Symptoms:
{"title":"Unauthorized","status":401,"detail":"Invalid Authorization header format"}
Cause: Token not properly extracted from auth response
Solution: Use correct field name registry_token (snake_case):
$authResp = curl -s https://mcp-registry.local/v0/auth/none -X POST | ConvertFrom-Json
$token = $authResp.registry_token # NOT $authResp.registryTokenSymptoms:
{"title":"Bad Request","detail":"invalid version: cannot publish duplicate version"}
Cause: Server with that name and version already exists
Solution: Increment the version number in your server.json
Symptoms:
{"title":"Forbidden","detail":"Permission denied for namespace"}
Cause: Server name doesn't start with io.modelcontextprotocol.anonymous/
Solution: Fix the namespace:
{
"name": "io.modelcontextprotocol.anonymous/my-server" // ✅ Correct
// NOT: "com.example/my-server" // ❌ Wrong for anonymous auth
}Symptoms:
404 Not Found when calling /v0/auth/none
Cause: MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH not set to "true"
Solution:
# Verify setting
kubectl -n mcp-registry get deploy mcp-registry -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH")].value}'
# If not "true", edit k8s/mcp-registry.yaml and reapply
kubectl apply -f .\k8s\mcp-registry.yaml
kubectl -n mcp-registry rollout restart deployment/mcp-registrySymptoms:
{"title":"Bad Request","detail":"schema version https://... is not supported"}
Cause: Using URL instead of version string
Solution: Use string version:
{
"$schema": "2025-10-17" // ✅ Correct
// NOT: "$schema": "https://example.com/schema.json" // ❌ Wrong
}Symptoms:
{"title":"Bad Request","detail":"Remote URL not accessible or invalid"}
Causes:
- Using httpbin.org or other non-MCP endpoints with validation enabled
- URL not publicly accessible
- URL doesn't implement MCP protocol
Solutions:
-
For local testing: Ensure validation is disabled
kubectl -n mcp-registry get deploy mcp-registry -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION")].value}' # Should return: false
-
For production: Use actual MCP server endpoint
{ "remotes": [{ "type": "streamable-http", "url": "https://your-actual-mcp-server.com/mcp" }] }
View Registry Logs:
$podName = kubectl -n mcp-registry get pods -l app=mcp-registry -o jsonpath='{.items[0].metadata.name}'
kubectl -n mcp-registry logs $podName --tail=100 --followCheck Database:
$pgPod = kubectl -n mcp-registry get pods -l app=postgres -o jsonpath='{.items[0].metadata.name}'
kubectl -n mcp-registry exec -it $pgPod -- psql -U postgres -d mcp_registry
# Inside psql:
\dt # List tables
SELECT * FROM servers; # View servers
SELECT * FROM versions; # View versions
\q # ExitPort Forward (Bypass Ingress):
$podName = kubectl -n mcp-registry get pods -l app=mcp-registry -o jsonpath='{.items[0].metadata.name}'
kubectl -n mcp-registry port-forward $podName 8080:8080
# Test: curl http://localhost:8080/v0.1/serversCheck Ingress:
kubectl get ingress -n mcp-registry
kubectl describe ingress mcp-registry -n mcp-registry-
Install mcp-publisher CLI
# Download from: https://github.com/modelcontextprotocol/registry/releases -
Publish Actual Package
# npm npm publish # PyPI poetry publish # Docker docker push myorg/my-server:1.0.0
-
Add Validation Metadata
For npm (
package.json):{ "name": "my-mcp-server", "mcpName": "io.github.username/my-server" }For PyPI (
README.md):mcp-name: io.github.username/my-server
For Docker (
Dockerfile):LABEL io.modelcontextprotocol.server.name="io.github.username/my-server"
-
Authenticate
# GitHub OAuth (interactive) mcp-publisher login github # GitHub OIDC (CI/CD) mcp-publisher login github-oidc # DNS verification (custom domain) mcp-publisher login dns --domain=example.com
-
Update Namespace
{ "name": "io.github.username/my-server" // For GitHub auth // OR "name": "com.example/my-server" // For DNS auth } -
Publish to Official Registry
mcp-publisher publish
| Aspect | Local Development | Production |
|---|---|---|
| Authentication | Anonymous (/v0/auth/none) |
GitHub/DNS/HTTP |
| Namespace | io.modelcontextprotocol.anonymous/* |
io.github.username/* or com.example/* |
| Validation | Disabled (fake packages OK) | Enabled (real packages required) |
| Registry URL | https://mcp-registry.local |
https://registry.modelcontextprotocol.io |
| Publishing Tool | PowerShell + curl | mcp-publisher CLI |
| Package Metadata | Not required | mcpName field required |
| Remote URLs | Placeholders OK | Must be real MCP endpoints |
- MCP Registry: https://github.com/modelcontextprotocol/registry
- API Documentation: https://registry.modelcontextprotocol.io/docs
- MCP Protocol: https://modelcontextprotocol.io
- Remote Servers Guide: https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/remote-servers.mdx
mcp-registry/
├── k8s/ # Kubernetes manifests
│ ├── mcp-registry.yaml # Main registry deployment
│ └── mcp-remote.yaml # Remote configuration
├── scripts/ # Automation scripts
│ ├── setup-local-ssl.ps1 # Create/export local Root CA
│ ├── trust-local-ca-windows.ps1 # Trust CA on Windows + Node system CA
│ ├── deploy-all.ps1 # One-command deploy
│ ├── publish-servers.ps1 # Publish server definitions
│ └── unpublish-servers.ps1 # Unpublish via direct DB delete (dangerous)
├── servers/ # Server definitions
│ ├── time-local.server.json # Example package-based server
│ ├── mcp-devtools-remote.server.json # Remote DevTools server
│ └── mcp-devtools/ # DevTools server source
│ ├── src/server.py # Python MCP server
│ ├── Dockerfile # Container build
│ └── k8s/deployment.yaml # Kubernetes deployment
└── README.md # This file
- Current:
2025-10-17(working in this setup) - Latest Official:
2025-12-11(check GitHub for updates)
# Deploy
kubectl apply -f .\k8s\mcp-registry.yaml
# Check Status
kubectl get pods -n mcp-registry
curl https://mcp-registry.local/v0.1/health
# Get Token
$token = (curl -s https://mcp-registry.local/v0/auth/none -X POST | ConvertFrom-Json).registry_token
# List Servers
curl https://mcp-registry.local/v0.1/servers | ConvertFrom-Json | Select-Object -ExpandProperty servers
# Publish Server
.\scripts\publish-servers.ps1
# Unpublish Server
.\scripts\unpublish-servers.ps1 -ServerName "io.modelcontextprotocol.anonymous/my-server" -Version "1.0.0"
# View Logs
kubectl -n mcp-registry logs -l app=mcp-registry --follow
# Restart Registry
kubectl -n mcp-registry rollout restart deployment/mcp-registry
# Clean Up
kubectl delete namespace mcp-registryNote: This setup is for local development and testing only. For production deployment to the official MCP Registry, follow the production migration guide above.