-
Notifications
You must be signed in to change notification settings - Fork 53
Description
Architectural Proposal: Separate Gateway and Registry Containers
Overview
This document provides a detailed plan for separating the NGINX gateway and Registry application into two separate containers. This will enable organizations to use their own API gateways while still leveraging the MCP Gateway Registry infrastructure.
Problem Statement
Currently, NGINX and the Registry application are bundled in the SAME container (docker/Dockerfile.registry). This tight coupling prevents organizations from:
- Using their own existing API gateway (AWS API Gateway, Kong, Apigee, Traefik, etc.)
- Deploying registry-only without unused NGINX overhead
- Scaling gateway and registry independently
- Integrating with existing enterprise infrastructure
Current Architecture
What Exists Today
ONE container contains BOTH NGINX and Registry:
docker/Dockerfile.registry builds:
┌─────────────────────────────────────────┐
│ Registry Container (ONE big container) │
├─────────────────────────────────────────┤
│ │
│ 1. NGINX Server (running on 80/443) │
│ • nginx binary │
│ • nginx.conf file │
│ • Routes traffic │
│ │
│ 2. FastAPI App (running on 7860) │
│ • registry/main.py │
│ • Serves Web UI │
│ • Manages servers │
│ │
│ 3. Python Code that manages NGINX │
│ • registry/core/nginx_service.py │
│ • Generates nginx.conf dynamically │
│ • Reloads NGINX when servers change│
│ │
└─────────────────────────────────────────┘
The Problem: Who Controls NGINX Config?
Currently: The Registry Python code (registry/core/nginx_service.py) generates and controls NGINX configuration:
- When you add/remove/toggle a server in the registry Web UI
- Registry Python code rewrites
nginx.conffile - Registry Python code reloads NGINX
- This violates separation of concerns - the registry shouldn't manage the gateway
Architecture Diagram
CLIENTS
Human Developer (Web Browser) AI Agent/Assistant (MCP Protocol)
│ │
│ HTTPS (Web UI) MCP (Streamable HTTP/SSE)
└──────────────┬───────────────────────┘
│
▼
╔═══════════════════════════════════════════════════╗
║ REGISTRY CONTAINER (ONE CONTAINER - PROBLEM!) ║
╠═══════════════════════════════════════════════════╣
║ ║
║ NGINX (80/443) ║
║ • SSL/TLS termination ║
║ • Reverse proxy ║
║ • Auth validation ║
║ │ ║
║ │ Internal communication ║
║ ▼ ║
║ FastAPI (7860) ║
║ • Web UI ║
║ • Server management ║
║ • Tool discovery ║
║ ║
╚═══════════════════════════════════════════════════╝
│ │ │
│ │ │
▼ ▼ ▼
Auth Server MCP Servers Metrics Service
(8888) (8001, etc.) (8890)
Proposed Architecture
The Solution: Two Separate Containers
Split the single container into TWO containers:
┌──────────────────────────────────┐
│ Gateway Container (NEW) │
├──────────────────────────────────┤
│ │
│ 1. NGINX Server (80/443) │
│ • nginx binary │
│ • nginx.conf file │
│ • Routes traffic │
│ │
│ 2. Python Code │
│ • gateway/config_generator.py│
│ • Watches registry API │
│ • Generates nginx.conf │
│ • Reloads NGINX │
│ │
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Registry Container (CLEANED UP) │
├──────────────────────────────────┤
│ │
│ 1. FastAPI App ONLY (7860) │
│ • registry/main.py │
│ • Serves Web UI │
│ • Manages servers │
│ • NO NGINX code! │
│ │
└──────────────────────────────────┘
Who Controls NGINX Config Now?
After separation:
- Gateway container controls its own NGINX configuration
- Registry container just provides API endpoints - doesn't touch NGINX at all
- Gateway watches registry API and updates its own config when it detects changes
How Does Gateway Know About Server Changes?
Gateway uses polling (checking registry API periodically):
# Inside gateway container, gateway/main.py runs:
while True:
# Poll registry API
servers = requests.get("http://registry:7860/api/servers").json()
# Regenerate NGINX config if servers changed
generate_nginx_config(servers)
# Reload NGINX
reload_nginx()
# Wait 30 seconds before checking again
sleep(30)Why polling? Simple, reliable, and doesn't require registry to know about gateway.
Architecture Diagram
CLIENTS
Human Developer (Web Browser) AI Agent/Assistant (MCP Protocol)
│ │
│ HTTPS (Web UI) MCP (Streamable HTTP/SSE)
└──────────────┬───────────────────────┘
│
▼
┌────────────────────────────────────────────────┐
│ GATEWAY CONTAINER (NEW - Separate) │
├────────────────────────────────────────────────┤
│ │
│ NGINX (80/443) │
│ • SSL/TLS termination │
│ • Reverse proxy │
│ • Auth validation │
│ │
└──────┬────────────────┬───────────────┬────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌─────────────┐
│ Auth Server│ │ Registry │ │ MCP Servers │
│ (8888) │ │ (7860) │ │ (8001...) │
│ │ │ │ └─────────────┘
│ │ │ FastAPI only │
│ │ │ • Web UI │
│ │ │ • REST APIs │
│ │ │ • Discovery │
└────────────┘ └──────────────┘
Benefits
Why Separate?
-
Enable Enterprise Adoption
- Organizations can use their existing AWS API Gateway, Kong, Apigee, etc.
- Deploy only the registry stack (registry + auth + metrics + keycloak)
- Skip our NGINX gateway entirely if they have their own
-
Independent Scaling
- Scale gateway and registry separately based on load
- Deploy multiple gateway containers in different regions → single centralized registry
-
Resource Efficiency
- Don't run unused NGINX if customer has their own gateway
- Smaller container images (each container has only what it needs)
-
Better Architecture
- Single responsibility principle - each container does one thing
- All other services already separated (auth-server, metrics-service, keycloak)
- This completes the microservices architecture
What Stays the Same
- ✅ Default deployment still includes NGINX gateway (no breaking changes)
- ✅ Same reverse proxy pattern (protocol independence preserved)
- ✅ Same auth flow, same routing, same functionality
- ✅ Existing users can continue as before
- ✅ Just split into two containers instead of one
Deployment Modes
Mode 1: Full Mode (Gateway + Registry) - DEFAULT
What starts:
- ✅ Gateway container (NGINX + Python)
- ✅ Registry container (FastAPI)
- ✅ Auth-server
- ✅ Metrics-service
- ✅ Keycloak
- ✅ MCP servers (fininfo, mcpgw, currenttime, etc.)
What the Gateway does:
- Serves as reverse proxy to Registry Web UI (port 7860)
- Serves as reverse proxy to MCP servers (ports 8001, 8003, etc.)
- Serves as reverse proxy to Auth server (port 8888)
- SSL/TLS termination
- Auth validation for all requests
User Access:
- User goes to
https://your-domain.com→ NGINX gateway → proxies to registry:7860 → Web UI - AI agent calls
https://your-domain.com/fininfo/→ NGINX gateway → proxies to fininfo-server:8001
Who uses this: Most users who want everything to work out of the box
Mode 2: Registry-Only Mode (No Gateway)
What starts:
- ❌ NO Gateway container
- ✅ Registry container (FastAPI) - DIRECTLY exposes port 7860
- ✅ Auth-server
- ✅ Metrics-service
- ✅ Keycloak
- ✅ MCP servers (customer's own gateway routes to these)
What the Registry does:
- FastAPI serves Web UI directly on port 7860 (no NGINX in front)
- Provides REST APIs on port 7860
- Doesn't know about gateway at all
User Access:
- User goes to
http://registry-host:7860→ DIRECTLY to FastAPI Web UI (no NGINX) - OR customer sets up their own gateway:
- Customer's gateway (AWS API GW, Kong, etc.) → proxies to registry:7860
- Customer's gateway → proxies to fininfo-server:8001, mcpgw-server:8003, etc.
Who uses this: Enterprises with existing API gateway infrastructure (AWS API Gateway, Kong, Apigee, etc.)
Mode 3: Standalone Registry (No Gateway, No MCP Servers)
What starts:
- ❌ NO Gateway container
- ✅ Registry container (FastAPI) - port 7860
- ✅ Auth-server
- ✅ Metrics-service
- ✅ Keycloak
- ❌ NO MCP servers (or they run somewhere else)
What the Registry does:
- Just provides Web UI for managing server definitions
- Server definitions point to MCP servers running elsewhere
- Pure management/discovery layer
User Access:
- User goes to
http://registry-host:7860directly to manage servers - MCP servers run on other hosts, registry just stores their URLs
Who uses this: Organizations that want centralized registry but run MCP servers on different infrastructure
Key Question: Does Registry Container Include the Web UI Frontend?
YES! The registry container includes the Web UI:
registry/
├── main.py # FastAPI serves the Web UI
└── ...
frontend/build/ # React Web UI (HTML/CSS/JS)
├── index.html
├── static/
└── ...
In registry/main.py:
# Serve React static files
FRONTEND_BUILD_PATH = Path(__file__).parent.parent / "frontend" / "build"
if FRONTEND_BUILD_PATH.exists():
# Serve static assets
app.mount("/static", StaticFiles(directory=FRONTEND_BUILD_PATH / "static"), name="static")
# Serve React app for all other routes (SPA)
@app.get("/{full_path:path}")
async def serve_react_app(full_path: str):
return FileResponse(FRONTEND_BUILD_PATH / "index.html")So the FastAPI app SERVES the Web UI directly - no NGINX needed for the UI!
What Does Gateway NGINX Actually Do?
The gateway NGINX provides:
- SSL/TLS termination - HTTPS instead of HTTP
- Single entry point - One domain for everything
- Auth validation - Validates ALL requests before they reach backends
- Routing - Routes to registry, auth, MCP servers based on path
- Protocol handling - SSE, WebSocket support for MCP protocol
But if you don't need these features:
- You can access registry directly on port 7860 (HTTP, no SSL)
- You can use your own gateway for SSL/auth/routing
Summary
| Mode | Gateway Container | Registry Serves Web UI | MCP Servers | Use Case |
|---|---|---|---|---|
| Full | ✅ Runs (NGINX) | ✅ Via gateway proxy | ✅ Local | Default deployment |
| Registry-Only | ❌ Not started | ✅ Direct (port 7860) | ✅ Customer routes to them | Enterprise with own gateway |
| Standalone Registry | ❌ Not started | ✅ Direct (port 7860) | ❌ Run elsewhere | Centralized management only |
Bottom line: The registry container ALWAYS serves the Web UI (via FastAPI). The gateway is OPTIONAL and just adds SSL/auth/routing on top.
New Project Structure
Directory Layout
mcp-gateway-registry/
├── gateway/ # NEW FOLDER: All gateway code
│ ├── config_generator.py # Generates nginx.conf (moved from registry/)
│ ├── config.py # Gateway-specific settings
│ ├── templates/
│ │ ├── nginx_http_https.conf # NGINX template files
│ │ └── nginx_http_only.conf
│ ├── main.py # Python service that watches registry
│ └── requirements.txt # Python deps (requests, etc.)
│
├── registry/ # CLEANED UP: No gateway code
│ ├── core/
│ │ └── nginx_service.py # DELETE THIS FILE
│ ├── main.py # REMOVE nginx operations (lines 115-120)
│ └── ... # Everything else stays same
│
├── docker/
│ ├── Dockerfile.gateway # NEW: Builds gateway container
│ │ # Contains: NGINX + Python + gateway/ code
│ ├── Dockerfile.registry-only # NEW: Builds registry container
│ │ # Contains: ONLY Python + registry/ code
│ ├── start-gateway.sh # NEW: Starts both NGINX and Python in gateway
│ └── Dockerfile.registry # KEEP for backward compatibility (optional)
│
└── docker-compose.yml # UPDATED: Two separate services
Implementation Plan
Phase 1: Create Gateway Folder and Move Code
Step 1.1: Create gateway folder structure
mkdir -p gateway/templatesStep 1.2: Move NGINX-related code
# Move nginx_service.py to gateway folder (and rename it)
mv registry/core/nginx_service.py gateway/config_generator.py
# Move NGINX templates to gateway folder
mv docker/nginx_rev_proxy_http_and_https.conf gateway/templates/
mv docker/nginx_rev_proxy_http_only.conf gateway/templates/Step 1.3: Create gateway/main.py
This file polls the registry API and updates NGINX config:
#!/usr/bin/env python3
"""
Gateway Configuration Service
Watches registry for server changes and updates NGINX config
"""
import asyncio
import logging
import httpx
from .config_generator import NginxConfigGenerator
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s,p%(process)s,{%(filename)s:%(lineno)d},%(levelname)s,%(message)s",
)
logger = logging.getLogger(__name__)
# Registry API URL
REGISTRY_API_URL = "http://registry:7860/api/servers"
# Poll interval in seconds
POLL_INTERVAL = 30
async def watch_registry():
"""Poll registry API for server changes and regenerate NGINX config"""
nginx_generator = NginxConfigGenerator()
while True:
try:
logger.info(f"Polling registry API: {REGISTRY_API_URL}")
# Get server list from registry
async with httpx.AsyncClient() as client:
response = await client.get(REGISTRY_API_URL)
response.raise_for_status()
servers_data = response.json()
# Extract enabled servers
enabled_servers = {
path: info
for path, info in servers_data.items()
if info.get("enabled", False)
}
logger.info(f"Found {len(enabled_servers)} enabled servers")
# Generate NGINX config
success = await nginx_generator.generate_config_async(enabled_servers)
if success:
logger.info("NGINX configuration updated successfully")
else:
logger.error("Failed to update NGINX configuration")
except Exception as e:
logger.error(f"Error polling registry: {e}", exc_info=True)
# Wait before next poll
await asyncio.sleep(POLL_INTERVAL)
if __name__ == "__main__":
logger.info("Starting Gateway Configuration Service")
logger.info(f"Polling interval: {POLL_INTERVAL} seconds")
asyncio.run(watch_registry())Step 1.4: Create gateway/init.py
Make gateway a proper Python module:
"""Gateway configuration service for MCP Gateway Registry"""Step 1.5: Create gateway/requirements.txt
httpx>=0.24.0
Step 1.6: Create gateway/config.py
This file handles gateway settings and reads from environment variables:
import os
class GatewaySettings:
"""Gateway configuration settings"""
# Registry API endpoint to poll for server list
registry_api_url: str = os.getenv(
"REGISTRY_API_URL", "http://registry:7860/api/servers"
)
# How often to poll registry (seconds)
poll_interval: int = int(os.getenv("POLL_INTERVAL", "30"))
# NGINX config paths
nginx_config_dir: str = "/etc/nginx"
nginx_templates_dir: str = "/etc/nginx/templates"
settings = GatewaySettings()Step 1.7: Update gateway/main.py to use environment variables
Change the hardcoded values to use settings:
import os
from gateway.config import settings
# Use settings instead of hardcoded values
REGISTRY_API_URL = settings.registry_api_url
POLL_INTERVAL = settings.poll_intervalStep 1.8: Update gateway/config_generator.py
After moving from registry/ to gateway/, update the file:
- Update imports: Change any cross-module imports
# OLD (when in registry/core/):
from registry.constants import NGINX_CONFIG_DIR
# NEW (now in gateway/):
from gateway.config import settings- Update config paths: Make sure NGINX config generation points to correct locations
# Use gateway settings for paths
config_path = f"{settings.nginx_config_dir}/nginx.conf"
template_path = f"{settings.nginx_templates_dir}/nginx_http_https.conf"- Handle missing templates gracefully: Add error handling
import os
from pathlib import Path
def _load_template(self, template_name: str) -> str:
"""Load NGINX template file"""
template_path = Path(settings.nginx_templates_dir) / template_name
if not template_path.exists():
logger.error(f"Template not found: {template_path}")
raise FileNotFoundError(f"NGINX template not found: {template_name}")
with open(template_path, 'r') as f:
return f.read()Step 1.9: Add error handling and startup logic
Update gateway/main.py to handle errors gracefully:
async def watch_registry():
"""Poll registry API for server changes and regenerate NGINX config"""
nginx_generator = NginxConfigGenerator()
# Wait for registry to be available before starting
await _wait_for_registry()
while True:
try:
logger.info(f"Polling registry API: {REGISTRY_API_URL}")
# Get server list from registry
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(REGISTRY_API_URL)
response.raise_for_status()
servers_data = response.json()
# Extract enabled servers
enabled_servers = {
path: info
for path, info in servers_data.items()
if info.get("enabled", False)
}
logger.info(f"Found {len(enabled_servers)} enabled servers")
# Generate NGINX config
success = await nginx_generator.generate_config_async(enabled_servers)
if success:
logger.info("NGINX configuration updated successfully")
else:
logger.error("Failed to update NGINX configuration")
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error polling registry: {e.response.status_code}")
except httpx.RequestError as e:
logger.error(f"Network error polling registry: {e}")
except Exception as e:
logger.error(f"Unexpected error: {e}", exc_info=True)
# Wait before next poll
await asyncio.sleep(POLL_INTERVAL)
async def _wait_for_registry():
"""Wait for registry to become available before starting main loop"""
logger.info("Waiting for registry to become available...")
max_retries = 30
retry_delay = 2
for attempt in range(max_retries):
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(REGISTRY_API_URL)
if response.status_code == 200:
logger.info("Registry is available!")
return
except Exception as e:
logger.debug(f"Registry not ready (attempt {attempt + 1}/{max_retries}): {e}")
await asyncio.sleep(retry_delay)
logger.warning("Registry did not become available, continuing anyway...")Step 1.10: Update gateway/config_generator.py to reload NGINX
Add method to reload NGINX after config generation:
import subprocess
import logging
logger = logging.getLogger(__name__)
class NginxConfigGenerator:
# ... existing code ...
def reload_nginx(self) -> bool:
"""Reload NGINX to apply new configuration"""
try:
# Test config first
result = subprocess.run(
["nginx", "-t"],
capture_output=True,
text=True,
check=False
)
if result.returncode != 0:
logger.error(f"NGINX config test failed: {result.stderr}")
return False
# Reload NGINX
result = subprocess.run(
["nginx", "-s", "reload"],
capture_output=True,
text=True,
check=False
)
if result.returncode == 0:
logger.info("NGINX reloaded successfully")
return True
else:
logger.error(f"NGINX reload failed: {result.stderr}")
return False
except Exception as e:
logger.error(f"Error reloading NGINX: {e}", exc_info=True)
return FalsePhase 2: Clean Up Registry Code
Step 2.1: Remove NGINX code from registry/main.py
Find and delete these lines (around line 115-120):
# DELETE THIS BLOCK:
logger.info("🌐 Generating initial Nginx configuration...")
enabled_servers = {
path: server_service.get_server_info(path)
for path in server_service.get_enabled_services()
}
await nginx_service.generate_config_async(enabled_servers)Step 2.2: Remove nginx_service import
Find and delete this import from registry/main.py:
# DELETE THIS LINE:
from registry.core.nginx_service import nginx_serviceStep 2.3: Delete registry/core/nginx_service.py
This file has been moved to gateway/, so delete the old copy:
rm registry/core/nginx_service.pyStep 2.4: Remove NGINX reload calls from server toggle operations
Find where servers are enabled/disabled (likely in registry/api/server_routes.py) and remove NGINX reload calls.
Search for calls like:
await nginx_service.generate_config_async(...)And delete them. The gateway will pick up changes automatically via polling.
Phase 3: Create Dockerfiles
Step 3.1: Create docker/Dockerfile.gateway
This builds the gateway container with NGINX + Python:
FROM nginx:alpine
# Install Python in the nginx container
RUN apk add --no-cache python3 py3-pip
# Copy gateway Python code
COPY gateway/ /app/gateway/
COPY gateway/requirements.txt /app/
RUN pip3 install --no-cache-dir -r /app/requirements.txt
# Copy NGINX config templates
COPY gateway/templates/ /etc/nginx/templates/
# Copy SSL certificates (if they exist)
COPY ssl/ /etc/ssl/ 2>/dev/null || true
EXPOSE 80 443
# Start both NGINX and the Python config generator
COPY docker/start-gateway.sh /start-gateway.sh
RUN chmod +x /start-gateway.sh
CMD ["/start-gateway.sh"]Step 3.2: Create initial NGINX configuration
Create gateway/templates/nginx_initial.conf - used before registry is available:
events {
worker_connections 1024;
}
http {
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Basic server - will be replaced by dynamic config
server {
listen 80 default_server;
server_name _;
location / {
return 503 "Gateway initializing, please wait...";
add_header Content-Type text/plain;
}
location /health {
return 200 "OK";
add_header Content-Type text/plain;
}
}
}Step 3.3: Create docker/start-gateway.sh
This script initializes NGINX config then starts both NGINX and Python watcher:
#!/bin/sh
set -e
echo "Initializing Gateway..."
# Create necessary directories
mkdir -p /etc/nginx/conf.d
mkdir -p /var/log/nginx
# Copy initial NGINX config if main config doesn't exist
if [ ! -f /etc/nginx/nginx.conf ]; then
echo "Creating initial NGINX configuration..."
cp /app/gateway/templates/nginx_initial.conf /etc/nginx/nginx.conf
fi
# Test NGINX configuration
echo "Testing NGINX configuration..."
nginx -t
# Start NGINX in background
echo "Starting NGINX..."
nginx -g 'daemon off;' &
NGINX_PID=$!
# Give NGINX a moment to start
sleep 2
# Start Python config generator (watches registry)
echo "Starting Gateway Configuration Service..."
cd /app
python3 -m gateway.main &
PYTHON_PID=$!
# Function to handle shutdown
shutdown() {
echo "Shutting down..."
kill $NGINX_PID 2>/dev/null || true
kill $PYTHON_PID 2>/dev/null || true
exit 0
}
# Trap SIGTERM and SIGINT
trap shutdown SIGTERM SIGINT
# Wait for both processes
waitStep 3.3: Create docker/Dockerfile.registry-only
This builds the registry container with ONLY FastAPI (no NGINX):
FROM python:3.11-slim
# Copy requirements and install
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy ONLY registry application code (no gateway code)
COPY registry/ /app/registry/
COPY auth_server/ /app/auth_server/
COPY frontend/build/ /app/frontend/build/
WORKDIR /app
# Only FastAPI - NO NGINX!
EXPOSE 7860
CMD ["uvicorn", "registry.main:app", "--host", "0.0.0.0", "--port", "7860"]Key Differences Between Dockerfiles:
| Dockerfile.gateway | Dockerfile.registry-only |
|---|---|
Based on nginx:alpine |
Based on python:3.11-slim |
| Has NGINX server | NO NGINX |
Has Python + gateway/ code |
Has Python + registry/ code |
| Runs 2 processes (NGINX + Python) | Runs 1 process (FastAPI only) |
| Exposes ports 80, 443 | Exposes port 7860 only |
Contains gateway/config_generator.py |
NO gateway code at all |
| Watches registry API | Doesn't watch anything |
Phase 4: Update docker-compose.yml
Step 4.1: Split registry into two services
Update docker-compose.yml to have separate gateway and registry services:
services:
# NEW: Separate gateway service
gateway:
build:
context: .
dockerfile: docker/Dockerfile.gateway
container_name: mcp-gateway
ports:
- "80:80"
- "443:443"
environment:
- REGISTRY_API_URL=http://registry:7860/api/servers
- POLL_INTERVAL=30
volumes:
- ${HOME}/mcp-gateway/ssl:/etc/ssl:ro
- gateway-logs:/var/log/nginx
depends_on:
registry:
condition: service_healthy
auth-server:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
networks:
- mcp-network
# UPDATED: Registry without NGINX
registry:
build:
context: .
dockerfile: docker/Dockerfile.registry-only
container_name: mcp-registry
ports:
- "7860:7860"
environment:
- AUTH_SERVER_URL=http://auth-server:8888
- METRICS_SERVICE_URL=http://metrics-service:8890
volumes:
- ${HOME}/mcp-gateway/servers:/app/registry/servers
- ${HOME}/mcp-gateway/logs/registry:/app/logs
depends_on:
- auth-server
- metrics-service
- keycloak
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
restart: unless-stopped
networks:
- mcp-network
# All other services stay the same (auth-server, metrics-service, keycloak, mcp servers)
auth-server:
# ... (unchanged)
metrics-service:
# ... (unchanged)
keycloak:
# ... (unchanged)
# MCP servers...
fininfo-server:
# ... (unchanged)
networks:
mcp-network:
driver: bridgePhase 5: Create Registry-Only Deployment
Step 5.1: Create docker-compose-registry-only.yml
For customers who want to use their own gateway:
services:
# NO gateway service - customer provides their own
registry:
build:
context: .
dockerfile: docker/Dockerfile.registry-only
ports:
- "7860:7860"
environment:
- AUTH_SERVER_URL=http://auth-server:8888
- METRICS_SERVICE_URL=http://metrics-service:8890
- TRUST_PROXY_HEADERS=true # Trust X-Forwarded-* headers from customer gateway
volumes:
- ${HOME}/mcp-gateway/servers:/app/registry/servers
depends_on:
- auth-server
- metrics-service
- keycloak
restart: unless-stopped
networks:
- mcp-network
auth-server:
# Same as main docker-compose.yml
# ...
metrics-service:
# Same as main docker-compose.yml
# ...
keycloak:
# Same as main docker-compose.yml
# ...
# MCP servers (customer gateway will route to these)
fininfo-server:
# Same as main docker-compose.yml
# ...
networks:
mcp-network:
driver: bridgePhase 6: Testing
Step 6.1: Test Combined Mode (gateway + registry)
# Build and start both containers
docker-compose build
docker-compose up
# Verify:
# 1. Gateway container is running with both NGINX and Python
# 2. Registry container is running with only FastAPI
# 3. Web UI accessible at http://localhost
# 4. Add/remove servers and verify NGINX config updates (check logs)Step 6.2: Test Registry-Only Mode
# Start registry-only stack
docker-compose -f docker-compose-registry-only.yml up
# Set up a custom NGINX on host machine pointing to registry:7860
# Verify registry works without our gatewayStep 6.3: Verify No Breaking Changes
- Existing users should be able to run
docker-compose upwithout changes - All functionality should work exactly as before
- Check logs for any errors
Phase 7: Documentation
Step 7.1: Update README.md
Add sections explaining both deployment modes:
- Combined deployment (default)
- Registry-only deployment
Step 7.2: Create Custom Gateway Integration Guide
Document requirements for organizations using their own gateway.
Step 7.3: Add deployment examples
Include examples for:
- AWS API Gateway integration
- Kong integration
- Custom NGINX configuration
Container Details
Gateway Container - What's Inside
Contains:
- ✅ NGINX server (binary)
- ✅ NGINX config file (nginx.conf)
- ✅ Python 3
- ✅
gateway/folder code - ✅
gateway/main.py- polls registry API every 30 seconds - ✅
gateway/config_generator.py- generates nginx.conf from server list - ✅ SSL certificates (if provided)
Responsibilities:
- NGINX listens on ports 80/443
- Python script polls
http://registry:7860/api/serversevery 30 seconds - When servers change, regenerates nginx.conf
- Reloads NGINX with new config
- Routes traffic to auth-server, registry, MCP servers
Does NOT contain:
- ❌ No registry code
- ❌ No FastAPI
- ❌ No web UI
- ❌ No server management logic
Registry Container - What's Inside
Contains:
- ✅ Python 3
- ✅ FastAPI application
- ✅
registry/folder code - ✅ Web UI (HTML/CSS/JS)
- ✅ Server management logic
- ✅ FAISS search index
- ✅ Health monitoring
Responsibilities:
- Runs FastAPI on port 7860
- Serves web UI
- Provides API endpoints:
GET /api/servers- list all serversPOST /api/servers- add serverPUT /api/servers/{id}- update serverDELETE /api/servers/{id}- remove server
- Manages server state (enabled/disabled)
- Health checks MCP servers
- Tool discovery with FAISS
Does NOT contain:
- ❌ No NGINX
- ❌ No nginx.conf
- ❌ No NGINX management code
- ❌ Doesn't know about gateway at all
Communication Flow
How the system works after separation:
1. User adds server in Web UI
↓
2. POST to registry:7860/api/servers
↓
3. Registry saves server to servers.json
↓
4. Registry returns 200 OK
↓
5. (30 seconds pass)
↓
6. Gateway polls registry API
↓
7. Gateway sees new server in response
↓
8. Gateway regenerates nginx.conf
↓
9. Gateway reloads NGINX
↓
10. New server is now routable through gateway
Key Point: Registry and Gateway don't directly communicate. Gateway just polls registry's public API. This is a clean separation.
Custom Gateway Integration
For organizations that want to use their own gateway instead of ours, document these requirements:
Requirements for Custom Gateway
-
Call auth server for validation
For each incoming request: 1. Call GET http://auth-server:8888/auth/validate 2. Pass Authorization header from original request 3. If 200 OK: forward X-User and X-Scopes headers to backend 4. If 401/403: reject the request -
Route traffic to appropriate backends
/ → http://registry:7860 (Web UI and REST APIs) /fininfo/ → http://fininfo-server:8001 /mcpgw/ → http://mcpgw-server:8003 /currenttime/ → http://currenttime-server:8000 /atlassian/ → http://atlassian-server:8005 -
Support required protocols
- HTTP/HTTPS
- Server-Sent Events (SSE) for MCP protocol
- WebSocket for real-time health updates
- Long-running connections
-
Forward required headers
X-Forwarded-ForX-Forwarded-ProtoX-Real-IPAuthorization(to auth-server)X-UserandX-Scopes(from auth-server to backends)
Implementation Phases
| Phase | Task |
|---|---|
| 1 | Create gateway folder and move code |
| 2 | Clean up registry code |
| 3 | Create Dockerfiles |
| 4 | Update docker-compose.yml |
| 5 | Create registry-only compose file |
| 6 | Testing (combined + registry-only) |
| 7 | Documentation |
Success Criteria
- Gateway container runs independently with NGINX + Python watcher
- Registry container runs independently with only FastAPI
- Gateway polls registry API every 30 seconds
- NGINX config regenerates when servers are added/removed
- Combined deployment works (docker-compose up)
- Registry-only deployment works (docker-compose-registry-only.yml)
- Registry supports HTTPS in standalone mode (with SSL certificates)
- No breaking changes for existing users
- Documentation updated with both deployment modes
- All tests pass
- Security scans pass (bandit)
HTTPS Configuration for Registry-Only Mode
CRITICAL: Registry Must Support HTTPS Without Gateway
When running in registry-only mode (without the gateway), the registry must be able to serve HTTPS directly. This is essential for production deployments.
Implementation: FastAPI with SSL/TLS Support
Step 1: Update registry/main.py to support SSL
Add SSL configuration at the end of registry/main.py:
import ssl
import os
from pathlib import Path
# SSL Configuration
SSL_ENABLED = os.getenv("SSL_ENABLED", "false").lower() == "true"
SSL_CERTFILE = os.getenv("SSL_CERTFILE", "/etc/ssl/cert.pem")
SSL_KEYFILE = os.getenv("SSL_KEYFILE", "/etc/ssl/key.pem")
if __name__ == "__main__":
import uvicorn
if SSL_ENABLED:
# Verify SSL files exist
cert_path = Path(SSL_CERTFILE)
key_path = Path(SSL_KEYFILE)
if not cert_path.exists():
logger.error(f"SSL certificate not found: {SSL_CERTFILE}")
raise FileNotFoundError(f"SSL certificate not found: {SSL_CERTFILE}")
if not key_path.exists():
logger.error(f"SSL key not found: {SSL_KEYFILE}")
raise FileNotFoundError(f"SSL key not found: {SSL_KEYFILE}")
logger.info("Starting registry with HTTPS enabled")
logger.info(f"SSL Certificate: {SSL_CERTFILE}")
uvicorn.run(
"registry.main:app",
host="0.0.0.0",
port=7860,
ssl_certfile=SSL_CERTFILE,
ssl_keyfile=SSL_KEYFILE,
ssl_keyfile_password=os.getenv("SSL_KEYFILE_PASSWORD")
)
else:
logger.info("Starting registry with HTTP only (no SSL)")
uvicorn.run(
"registry.main:app",
host="0.0.0.0",
port=7860
)Step 2: Update Dockerfile.registry-only to support SSL certificates
FROM python:3.11-slim
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY registry/ /app/registry/
COPY auth_server/ /app/auth_server/
COPY frontend/build/ /app/frontend/build/
WORKDIR /app
# Expose HTTP and HTTPS ports
EXPOSE 7860
# Set Python path
ENV PYTHONPATH=/app
# Run registry with SSL support
CMD ["python", "-m", "registry.main"]Step 3: Update docker-compose-registry-only.yml with SSL configuration
services:
registry:
build:
context: .
dockerfile: docker/Dockerfile.registry-only
container_name: mcp-registry
ports:
- "7860:7860" # HTTPS when SSL_ENABLED=true, HTTP otherwise
environment:
- AUTH_SERVER_URL=http://auth-server:8888
- METRICS_SERVICE_URL=http://metrics-service:8890
- TRUST_PROXY_HEADERS=true
# SSL Configuration for standalone HTTPS
- SSL_ENABLED=true
- SSL_CERTFILE=/etc/ssl/cert.pem
- SSL_KEYFILE=/etc/ssl/key.pem
# Optional: SSL key password
# - SSL_KEYFILE_PASSWORD=${SSL_KEYFILE_PASSWORD}
volumes:
- ${HOME}/mcp-gateway/servers:/app/registry/servers
- ${HOME}/mcp-gateway/logs/registry:/app/logs
# Mount SSL certificates for HTTPS support
- ${HOME}/mcp-gateway/ssl/cert.pem:/etc/ssl/cert.pem:ro
- ${HOME}/mcp-gateway/ssl/key.pem:/etc/ssl/key.pem:ro
depends_on:
- auth-server
- metrics-service
- keycloak
healthcheck:
# Use HTTPS for health check when SSL is enabled
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('https://localhost:7860/health' if ${SSL_ENABLED:-false} else 'http://localhost:7860/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
restart: unless-stopped
networks:
- mcp-networkStep 4: Generate SSL certificates for testing
Create a helper script scripts/generate-ssl-certs.sh:
#!/bin/bash
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
SSL_DIR="${HOME}/mcp-gateway/ssl"
echo "Creating SSL directory..."
mkdir -p "$SSL_DIR"
echo "Generating self-signed SSL certificate..."
openssl req -x509 -newkey rsa:4096 -nodes \
-keyout "$SSL_DIR/key.pem" \
-out "$SSL_DIR/cert.pem" \
-days 365 \
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
echo "Setting secure permissions..."
chmod 600 "$SSL_DIR/key.pem"
chmod 644 "$SSL_DIR/cert.pem"
echo "SSL certificates generated successfully:"
echo " Certificate: $SSL_DIR/cert.pem"
echo " Private Key: $SSL_DIR/key.pem"
echo ""
echo "For production, replace these with certificates from a trusted CA."Step 5: Access Registry with HTTPS
With SSL enabled:
# Generate certs (first time only)
bash scripts/generate-ssl-certs.sh
# Start registry-only mode with SSL
docker-compose -f docker-compose-registry-only.yml up -d
# Access via HTTPS
curl -k https://localhost:7860/health
# Or in browser
open https://localhost:7860SSL Configuration Summary
| Mode | SSL/TLS | Port | Access URL | Certificate Location |
|---|---|---|---|---|
| Gateway (default) | ✅ Gateway handles | 443 | https://your-domain.com | Mounted in gateway container |
| Registry-Only (HTTP) | ❌ Disabled | 7860 | http://registry-host:7860 | Not needed |
| Registry-Only (HTTPS) | ✅ Registry handles | 7860 | https://registry-host:7860 | Mounted in registry container |
Build and Deployment Scripts
Create Build Scripts for ECR Deployment
Step 1: Create scripts/build-and-push-gateway.sh
#!/bin/bash
# Build and push gateway container to Amazon ECR
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PARENT_DIR="$(dirname "$SCRIPT_DIR")"
# Configuration
AWS_REGION="${AWS_REGION:-us-east-1}"
ECR_REPO_NAME="mcp-gateway"
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ECR_REPO_URI="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO_NAME"
echo "Building and pushing Gateway container..."
echo "Region: $AWS_REGION"
echo "Repository: $ECR_REPO_NAME"
# Login to Amazon ECR
echo "Logging in to Amazon ECR..."
aws ecr get-login-password --region $AWS_REGION | \
docker login --username AWS --password-stdin \
"$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
# Create repository if it doesn't exist
echo "Creating ECR repository if it doesn't exist..."
aws ecr describe-repositories --repository-names "$ECR_REPO_NAME" --region "$AWS_REGION" 2>/dev/null || \
aws ecr create-repository --repository-name "$ECR_REPO_NAME" --region "$AWS_REGION"
# Build the Docker image
echo "Building Gateway Docker image..."
docker build -f "$PARENT_DIR/docker/Dockerfile.gateway" -t "$ECR_REPO_NAME" "$PARENT_DIR"
# Tag the image
echo "Tagging image..."
docker tag "$ECR_REPO_NAME":latest "$ECR_REPO_URI":latest
# Push the image to ECR
echo "Pushing image to ECR..."
docker push "$ECR_REPO_URI":latest
echo "Successfully built and pushed gateway image:"
echo "$ECR_REPO_URI:latest"
# Save the container URI to a file for reference
echo "$ECR_REPO_URI:latest" > "$SCRIPT_DIR/.gateway_container_uri"
echo "Container URI saved to $SCRIPT_DIR/.gateway_container_uri"Step 2: Create scripts/build-and-push-registry.sh
#!/bin/bash
# Build and push registry container to Amazon ECR
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PARENT_DIR="$(dirname "$SCRIPT_DIR")"
# Configuration
AWS_REGION="${AWS_REGION:-us-east-1}"
ECR_REPO_NAME="mcp-registry"
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ECR_REPO_URI="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO_NAME"
echo "Building and pushing Registry container..."
echo "Region: $AWS_REGION"
echo "Repository: $ECR_REPO_NAME"
# Login to Amazon ECR
echo "Logging in to Amazon ECR..."
aws ecr get-login-password --region $AWS_REGION | \
docker login --username AWS --password-stdin \
"$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
# Create repository if it doesn't exist
echo "Creating ECR repository if it doesn't exist..."
aws ecr describe-repositories --repository-names "$ECR_REPO_NAME" --region "$AWS_REGION" 2>/dev/null || \
aws ecr create-repository --repository-name "$ECR_REPO_NAME" --region "$AWS_REGION"
# Build the Docker image
echo "Building Registry Docker image..."
docker build -f "$PARENT_DIR/docker/Dockerfile.registry-only" -t "$ECR_REPO_NAME" "$PARENT_DIR"
# Tag the image
echo "Tagging image..."
docker tag "$ECR_REPO_NAME":latest "$ECR_REPO_URI":latest
# Push the image to ECR
echo "Pushing image to ECR..."
docker push "$ECR_REPO_URI":latest
echo "Successfully built and pushed registry image:"
echo "$ECR_REPO_URI:latest"
# Save the container URI to a file for reference
echo "$ECR_REPO_URI:latest" > "$SCRIPT_DIR/.registry_container_uri"
echo "Container URI saved to $SCRIPT_DIR/.registry_container_uri"Step 3: Make scripts executable
chmod +x scripts/build-and-push-gateway.sh
chmod +x scripts/build-and-push-registry.sh
chmod +x scripts/generate-ssl-certs.shSecurity and Code Quality
Phase 8: Security Validation
Add security scanning to the implementation plan:
Step 8.1: Run Bandit security scanner on gateway code
# Scan new gateway code for security issues
uv run bandit -r gateway/
# Generate report
uv run bandit -r gateway/ -f json -o reports/bandit-gateway.jsonStep 8.2: Scan registry code
# Scan registry code
uv run bandit -r registry/
# Generate report
uv run bandit -r registry/ -f json -o reports/bandit-registry.jsonStep 8.3: Docker image security scanning
# Scan gateway image
docker scan docker/Dockerfile.gateway
# Scan registry image
docker scan docker/Dockerfile.registry-onlySecrets Management Best Practices
Never commit these files:
- SSL certificates (
*.pem,*.crt,*.key) - Environment files with credentials (
.env) - Database credentials
- API keys
Add to .gitignore:
# SSL Certificates
*.pem
*.crt
*.key
*.p12
# Environment files
.env
.env.local
.env.production
# Container URIs (from build scripts)
scripts/.gateway_container_uri
scripts/.registry_container_uri
# Security reports
reports/Testing Strategy
Unit Tests
Create tests/gateway/test_config_generator.py
import pytest
from unittest.mock import Mock, patch
from gateway.config_generator import NginxConfigGenerator
class TestNginxConfigGenerator:
"""Tests for NGINX config generation"""
def test_generate_config_with_enabled_servers(self):
"""Test config generation with enabled servers"""
generator = NginxConfigGenerator()
servers = {
"fininfo": {
"enabled": True,
"port": 8001,
"protocol": "http"
}
}
config = generator._generate_config(servers)
assert "fininfo" in config
assert "8001" in config
def test_generate_config_empty_servers(self):
"""Test config generation with no servers"""
generator = NginxConfigGenerator()
config = generator._generate_config({})
assert config is not None
assert len(config) > 0
@patch('subprocess.run')
def test_reload_nginx_success(self, mock_run):
"""Test successful NGINX reload"""
mock_run.return_value = Mock(returncode=0, stderr="")
generator = NginxConfigGenerator()
result = generator.reload_nginx()
assert result is True
assert mock_run.call_count == 2 # test + reload
@patch('subprocess.run')
def test_reload_nginx_config_test_fails(self, mock_run):
"""Test NGINX reload when config test fails"""
mock_run.return_value = Mock(returncode=1, stderr="config error")
generator = NginxConfigGenerator()
result = generator.reload_nginx()
assert result is FalseCreate tests/registry/test_ssl_config.py
import pytest
import os
from pathlib import Path
class TestRegistrySSLConfig:
"""Tests for registry SSL configuration"""
def test_ssl_enabled_environment_variable(self):
"""Test SSL enabled via environment variable"""
os.environ["SSL_ENABLED"] = "true"
# Import after setting env var
from registry.main import SSL_ENABLED
assert SSL_ENABLED is True
def test_ssl_disabled_by_default(self):
"""Test SSL disabled by default"""
os.environ.pop("SSL_ENABLED", None)
from registry.main import SSL_ENABLED
assert SSL_ENABLED is False
def test_ssl_cert_paths_configurable(self):
"""Test SSL cert paths are configurable"""
test_cert = "/custom/path/cert.pem"
test_key = "/custom/path/key.pem"
os.environ["SSL_CERTFILE"] = test_cert
os.environ["SSL_KEYFILE"] = test_key
from registry.main import SSL_CERTFILE, SSL_KEYFILE
assert SSL_CERTFILE == test_cert
assert SSL_KEYFILE == test_keyIntegration Tests
Create tests/integration/test_deployment_modes.py
import pytest
import httpx
import asyncio
import docker
@pytest.mark.asyncio
class TestDeploymentModes:
"""Integration tests for different deployment modes"""
async def test_full_mode_gateway_and_registry(self):
"""Test full mode with both gateway and registry"""
async with httpx.AsyncClient(verify=False) as client:
# Gateway should be accessible
gateway_response = await client.get("http://localhost/health")
assert gateway_response.status_code == 200
# Registry should be accessible through gateway
registry_response = await client.get("http://localhost/api/servers")
assert registry_response.status_code == 200
async def test_registry_only_mode_http(self):
"""Test registry-only mode with HTTP"""
async with httpx.AsyncClient() as client:
response = await client.get("http://localhost:7860/health")
assert response.status_code == 200
async def test_registry_only_mode_https(self):
"""Test registry-only mode with HTTPS"""
async with httpx.AsyncClient(verify=False) as client:
response = await client.get("https://localhost:7860/health")
assert response.status_code == 200
async def test_gateway_polls_registry(self):
"""Test that gateway successfully polls registry"""
# Add a server via registry API
async with httpx.AsyncClient() as client:
# Add test server
response = await client.post(
"http://localhost:7860/api/servers",
json={
"name": "test-server",
"port": 9999,
"enabled": True
}
)
assert response.status_code == 200
# Wait for gateway to poll
await asyncio.sleep(35)
# Check gateway config updated
# (Would need to check NGINX config or test routing)Environment Variables Reference
Complete Environment Variable Documentation
| Variable | Container | Default | Description |
|---|---|---|---|
| REGISTRY_API_URL | gateway | http://registry:7860/api/servers | Registry API endpoint to poll |
| POLL_INTERVAL | gateway | 30 | How often gateway polls registry (seconds) |
| SSL_ENABLED | gateway, registry | false | Enable HTTPS/SSL |
| SSL_CERTFILE | gateway, registry | /etc/ssl/cert.pem | Path to SSL certificate |
| SSL_KEYFILE | gateway, registry | /etc/ssl/key.pem | Path to SSL private key |
| SSL_KEYFILE_PASSWORD | gateway, registry | (none) | Password for encrypted SSL key |
| AUTH_SERVER_URL | registry | http://auth-server:8888 | Authentication server URL |
| METRICS_SERVICE_URL | registry | http://metrics-service:8890 | Metrics service URL |
| TRUST_PROXY_HEADERS | registry | false | Trust X-Forwarded-* headers from proxy |
| LOG_LEVEL | gateway, registry | INFO | Logging level (DEBUG, INFO, WARNING, ERROR) |
Operational Details
Logging
Gateway logs:
- NGINX access:
/var/log/nginx/access.log - NGINX error:
/var/log/nginx/error.log - Python service:
docker logs mcp-gateway
Registry logs:
- Application:
docker logs mcp-registry - Mounted volume:
~/mcp-gateway/logs/registry/
View live logs:
# Gateway logs (both NGINX and Python)
docker logs -f mcp-gateway
# Registry logs
docker logs -f mcp-registry
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f gateway
docker-compose logs -f registryMonitoring and Health Checks
Health check endpoints:
# Gateway health
curl http://localhost/health
# Registry health (direct)
curl http://localhost:7860/health
# Registry health (via gateway)
curl http://localhost/api/healthMetrics to monitor:
- Gateway poll frequency (should be ~30s)
- NGINX reload success rate
- Registry API response times
- Container restart count
- SSL certificate expiration dates
Container resource limits (production):
services:
gateway:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
registry:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '1.0'
memory: 1GREADME.md Updates
Add Deployment Modes Section
Add this section to the main README.md:
## Deployment Modes
MCP Gateway Registry supports three deployment modes to fit different infrastructure needs.
### Mode 1: Full Stack with Gateway (Default)
**What you get:**
- NGINX gateway with SSL/TLS termination
- Registry with Web UI
- Authentication server (Keycloak integration)
- Metrics service
- All MCP servers
**Quick start:**
```bash
# Clone repository
git clone https://github.com/your-org/mcp-gateway-registry.git
cd mcp-gateway-registry
# Start all services
docker-compose up -d
# Access Web UI
open https://localhostBest for: Most users who want a complete, ready-to-use solution
Mode 2: Registry-Only (Bring Your Own Gateway)
What you get:
- Registry with Web UI (HTTP or HTTPS)
- Authentication server
- Metrics service
- MCP servers (you route to them via your gateway)
Quick start:
# Generate SSL certificates (for HTTPS)
bash scripts/generate-ssl-certs.sh
# Start registry stack
docker-compose -f docker-compose-registry-only.yml up -d
# Access via HTTPS
curl -k https://localhost:7860/healthConfigure your gateway to route traffic:
/→http://registry:7860(orhttps://registry:7860if SSL enabled)/fininfo/→http://fininfo-server:8001/mcpgw/→http://mcpgw-server:8003- etc.
Best for: Enterprises with existing API gateways (AWS API Gateway, Kong, Traefik, etc.)
Mode 3: Standalone Registry
What you get:
- Registry with Web UI only
- MCP servers run elsewhere (you manage them separately)
Quick start:
# Edit docker-compose-registry-only.yml
# Remove or comment out MCP server services
# Start registry only
docker-compose -f docker-compose-registry-only.yml up registry auth-server keycloakBest for: Organizations that want centralized server management but run MCP servers on different infrastructure
SSL/HTTPS Configuration
Full Mode (with Gateway)
Gateway handles all SSL/TLS:
# Place your SSL certificates
mkdir -p ~/mcp-gateway/ssl
cp your-cert.pem ~/mcp-gateway/ssl/cert.pem
cp your-key.pem ~/mcp-gateway/ssl/key.pem
# Start services
docker-compose up -d
# Access via HTTPS (gateway)
open https://your-domain.comRegistry-Only Mode with HTTPS
Registry serves HTTPS directly:
# Generate self-signed cert (testing only)
bash scripts/generate-ssl-certs.sh
# Or use your own certificates
mkdir -p ~/mcp-gateway/ssl
cp your-cert.pem ~/mcp-gateway/ssl/cert.pem
cp your-key.pem ~/mcp-gateway/ssl/key.pem
# Ensure SSL_ENABLED=true in docker-compose-registry-only.yml
# Start services
docker-compose -f docker-compose-registry-only.yml up -d
# Access via HTTPS (registry direct)
curl -k https://localhost:7860/healthProduction SSL certificates:
For production, use certificates from a trusted CA:
- Let's Encrypt (free)
- AWS Certificate Manager
- Your organization's CA
Custom Gateway Integration Examples
AWS API Gateway
# API Gateway HTTP API configuration
Resources:
MCPRegistryIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref HttpApi
IntegrationType: HTTP_PROXY
IntegrationUri: https://registry-host:7860
IntegrationMethod: ANY
PayloadFormatVersion: '1.0'
TlsConfig:
ServerNameToVerify: registry-hostKong
# kong.yml
services:
- name: mcp-registry
url: https://registry:7860
routes:
- name: registry-route
paths:
- /
plugins:
- name: jwt
- name: rate-limiting
config:
minute: 100Traefik
# docker-compose.yml with Traefik
services:
registry:
labels:
- "traefik.enable=true"
- "traefik.http.routers.registry.rule=Host(`registry.example.com`)"
- "traefik.http.routers.registry.tls=true"
- "traefik.http.services.registry.loadbalancer.server.port=7860"
- "traefik.http.services.registry.loadbalancer.server.scheme=https"
## Migration Guide
### Migrating from Monolithic to Separated Architecture
**For existing deployments:**
#### Step 1: Backup Current State
```bash
# Stop current containers
docker-compose down
# Backup configuration and data
cp -r ~/mcp-gateway ~/mcp-gateway-backup-$(date +%Y%m%d)
# Backup current git state
git branch backup-before-separation
Step 2: Pull Latest Changes
# Update to latest version
git pull origin main
# Review changes
git log --oneline -10Step 3: Rebuild Containers
# Clean rebuild (removes old images)
docker-compose build --no-cache
# Or rebuild specific services
docker-compose build gateway
docker-compose build registryStep 4: Update Configuration
# If using custom SSL certificates, ensure they're in the right location
ls -la ~/mcp-gateway/ssl/
# Verify docker-compose.yml has correct volume mounts
grep -A 5 "volumes:" docker-compose.ymlStep 5: Start New Architecture
# Start all services
docker-compose up -d
# Watch logs for any errors
docker-compose logs -fStep 6: Verify Services
# Check containers are running
docker ps | grep mcp-
# Expected output:
# mcp-gateway (new - separate container)
# mcp-registry (new - separate container)
# mcp-auth-server
# mcp-metrics-service
# mcp-keycloak
# mcp-fininfo-server
# etc.
# Test gateway health
curl http://localhost/health
# Test registry health (direct)
curl http://localhost:7860/health
# Test Web UI
open https://localhostStep 7: Verify Functionality
# Add a test server via Web UI
# Toggle server on/off
# Check gateway logs to confirm config regeneration
docker logs mcp-gateway | grep "NGINX configuration updated"Rollback Plan
If issues occur:
# Stop new architecture
docker-compose down
# Checkout previous version
git checkout backup-before-separation
# Restore backup if needed
rm -rf ~/mcp-gateway
mv ~/mcp-gateway-backup-YYYYMMDD ~/mcp-gateway
# Start old version
docker-compose up -dExpected Changes
What changes:
- Two containers instead of one (gateway + registry)
- Gateway polls registry every 30 seconds (slight delay in config updates)
- Separate health endpoints
What stays the same:
- Web UI functionality
- API endpoints
- MCP server routing
- Authentication flow
- All features
Conclusion
This separation provides:
- Clean architecture - each container has single responsibility
- Enterprise adoption - organizations can use their own gateways
- No breaking changes - existing users continue unchanged
- Better scalability - independent scaling of gateway and registry
The separation completes the microservices architecture (auth, metrics, keycloak already separated) and enables broader enterprise adoption while maintaining all existing benefits.