This guide shows how to build a production-grade, federated MCP gateway that exposes both REST and GraphQL APIs as unified MCP tools for AI agents.
Architecture:
- Tier 1: Protocol-specific converters (apollo-mcp-server for GraphQL)
- Tier 2: Central gateway (mcp-context-forge for REST + MCP federation)
- Result: Single public endpoint for all your APIs with enterprise security
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TIER 1: Protocol-Specific Converters β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β apollo-mcp-server (port 4000) β β
β β Rust-based GraphQL β MCP converter β β
β β β β
β β Exposes: β β
β β ββ github_list_repos() β β
β β ββ github_create_issue() β β
β β ββ shopify_get_products() β β
β β ββ hasura_query_database() β β
β β ββ Any GraphQL API as MCP tools β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β (Future: Add more specialized converters) β
β ββ gRPC β MCP converter β
β ββ SOAP β MCP converter β
β ββ Custom protocol converters β
βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β (MCP protocol)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TIER 2: Central Gateway & Aggregator β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β IBM mcp-context-forge (port 3000) β β
β β Python-based MCP Gateway & Registry β β
β β β β
β β Functions: β β
β β ββ Direct REST β MCP conversion: β β
β β β ββ Radarr (localhost:7878) β β
β β β ββ Sonarr (localhost:8989) β β
β β β ββ Plex (192.168.1.219:32400) β β
β β β ββ Prowlarr (localhost:9696) β β
β β β β β
β β ββ MCP Server Federation: β β
β β β ββ apollo-mcp-server (localhost:4000) β β
β β β β β
β β ββ Unified Security Layer: β β
β β β ββ JWT authentication β β
β β β ββ OAuth support β β
β β β ββ Rate limiting β β
β β β ββ API key management β β
β β β β β
β β ββ Admin Web UI: β β
β β β ββ Real-time monitoring β β
β β β ββ Tool discovery β β
β β β ββ Configuration management β β
β β β β β
β β ββ Observability: β β
β β ββ OpenTelemetry metrics β β
β β ββ Request logging β β
β β ββ Performance analytics β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Unified Endpoint: http://localhost:3000 β
βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββ
β ngrok HTTPS Tunnel β
β https://abc123.ngrok.io β
βββββββββββββ¬ββββββββββββββββ
β
βββββββββββββββββββββββββββββ
β Public Internet β
βββββββββββββ¬ββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββ
β AI Clients β
β ββ OpenAI Agent Builder β
β ββ Claude Desktop β
β ββ Custom AI applications β
β ββ Any MCP-compatible client β
βββββββββββββββββββββββββββββββββββββ
Option A: Only mcp-context-forge
- β Limited to REST APIs only
- β No GraphQL support
- β Would need custom code for each GraphQL API
Option B: Only apollo-mcp-server
- β No REST API conversion
- β No unified security layer
- β No admin UI
- β No federation of multiple servers
Tier 1 (apollo-mcp-server):
- β Specialized GraphQL β MCP conversion
- β Handles complex GraphQL schemas
- β Built by Apollo (GraphQL experts)
- β High-performance Rust implementation
Tier 2 (mcp-context-forge):
- β Direct REST β MCP conversion
- β Federates multiple MCP servers (including apollo-mcp-server)
- β Unified authentication across all tools
- β Single admin UI for all services
- β Single public endpoint (one ngrok tunnel)
- β Enterprise security & observability
AI Agent: "Add The Matrix to Radarr and create a GitHub issue to track it"
Flow:
1. radarr_search_movie("The Matrix") β mcp-context-forge β Radarr REST API
2. radarr_add_movie(...) β mcp-context-forge β Radarr REST API
3. github_create_issue(...) β mcp-context-forge β apollo-mcp-server β GitHub GraphQL
AI Agent: "Check Shopify inventory and download missing product data"
Flow:
1. shopify_get_products() β mcp-context-forge β apollo-mcp-server β Shopify GraphQL
2. sabnzbd_add_nzb(...) β mcp-context-forge β SABnzbd REST API
AI Agent: "Search Plex for sci-fi movies and check GitHub for related projects"
Flow:
1. plex_search("sci-fi") β mcp-context-forge β Plex REST API
2. github_search_repos("sci-fi movies") β mcp-context-forge β apollo-mcp-server β GitHub GraphQL
Hardware: ZimaBoard with Proxmox (or any Docker host)
- LXC 103 (192.168.1.175) with Docker installed
- 2GB+ RAM available
- 10GB+ storage
Software:
- Docker & Docker Compose
- ngrok account (free tier works)
- API keys for services you want to expose
Network:
- LAN access to services (Radarr, Sonarr, Plex, etc.)
- Internet access for ngrok
# SSH into Proxmox host
ssh [email protected]
# Enter LXC 103 container
pct enter 103
# Create project directory
mkdir -p /opt/mcp-gateway
cd /opt/mcp-gateway
# Verify Docker is running
docker ps
cd /opt/mcp-gateway
mkdir -p apollo-mcp-server/{config,operations}
cd apollo-mcp-server
cat > Dockerfile <<'EOF'
FROM rust:1.75-slim as builder
WORKDIR /app
# Install dependencies
RUN apt-get update && apt-get install -y \
git \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Clone and build apollo-mcp-server
RUN git clone https://github.com/apollographql/apollo-mcp-server.git .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/apollo-mcp-server /usr/local/bin/
WORKDIR /app
COPY config /app/config
COPY operations /app/operations
EXPOSE 4000
CMD ["apollo-mcp-server", "--config", "/app/config/server.toml"]
EOF
cat > config/server.toml <<'EOF'
[server]
host = "0.0.0.0"
port = 4000
transport = "http"
[logging]
level = "info"
format = "json"
# Add your GraphQL endpoints here
[[graphs]]
name = "github"
url = "https://api.github.com/graphql"
operations_dir = "/app/operations/github"
[graphs.headers]
Authorization = "Bearer ${GITHUB_TOKEN}"
[[graphs]]
name = "shopify"
url = "https://${SHOPIFY_STORE}.myshopify.com/admin/api/2024-01/graphql.json"
operations_dir = "/app/operations/shopify"
[graphs.headers]
X-Shopify-Access-Token = "${SHOPIFY_ACCESS_TOKEN}"
EOF
mkdir -p operations/github
cat > operations/github/repos.graphql <<'EOF'
# List user repositories
query ListRepos($owner: String!, $first: Int = 100) {
user(login: $owner) {
repositories(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
name
description
stargazerCount
forkCount
url
primaryLanguage {
name
}
updatedAt
}
}
}
}
# Search repositories
query SearchRepos($query: String!, $first: Int = 50) {
search(query: $query, type: REPOSITORY, first: $first) {
nodes {
... on Repository {
name
description
stargazerCount
url
owner {
login
}
}
}
}
}
# Create issue
mutation CreateIssue($repositoryId: ID!, $title: String!, $body: String) {
createIssue(input: {repositoryId: $repositoryId, title: $title, body: $body}) {
issue {
number
url
title
}
}
}
# List issues
query ListIssues($owner: String!, $repo: String!, $first: Int = 50) {
repository(owner: $owner, name: $repo) {
issues(first: $first, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes {
number
title
state
url
createdAt
author {
login
}
}
}
}
}
EOF
mkdir -p operations/shopify
cat > operations/shopify/products.graphql <<'EOF'
# List products
query GetProducts($first: Int = 50, $query: String) {
products(first: $first, query: $query) {
edges {
node {
id
title
description
handle
productType
vendor
tags
status
createdAt
updatedAt
variants(first: 10) {
edges {
node {
id
title
price
inventoryQuantity
sku
}
}
}
images(first: 5) {
edges {
node {
url
altText
}
}
}
}
}
}
}
# Create product
mutation CreateProduct($title: String!, $descriptionHtml: String, $productType: String) {
productCreate(input: {
title: $title
descriptionHtml: $descriptionHtml
productType: $productType
}) {
product {
id
title
}
userErrors {
field
message
}
}
}
EOF
cat > docker-compose.yml <<'EOF'
version: '3.8'
services:
apollo-mcp:
build: .
container_name: apollo-mcp-server
ports:
- "4000:4000"
environment:
- GITHUB_TOKEN=${GITHUB_TOKEN}
- SHOPIFY_STORE=${SHOPIFY_STORE}
- SHOPIFY_ACCESS_TOKEN=${SHOPIFY_ACCESS_TOKEN}
volumes:
- ./config:/app/config:ro
- ./operations:/app/operations:ro
restart: unless-stopped
networks:
- mcp-network
networks:
mcp-network:
driver: bridge
EOF
cat > .env <<'EOF'
# GitHub Personal Access Token
GITHUB_TOKEN=ghp_your_token_here
# Shopify (optional)
SHOPIFY_STORE=your-store-name
SHOPIFY_ACCESS_TOKEN=shpat_your_token_here
EOF
# Build the Docker image
docker-compose build
# Start the service
docker-compose up -d
# Check logs
docker-compose logs -f
# Test the endpoint
curl http://localhost:4000/health
cd /opt/mcp-gateway
mkdir -p mcp-context-forge
cd mcp-context-forge
cat > config.yml <<'EOF'
# mcp-context-forge central gateway configuration
server:
host: "0.0.0.0"
port: 3000
cors:
enabled: true
origins: ["*"]
security:
jwt:
enabled: true
secret: "${JWT_SECRET}"
rate_limiting:
enabled: true
requests_per_minute: 100
admin:
enabled: true
path: "/admin"
username: "admin"
password: "${ADMIN_PASSWORD}"
observability:
opentelemetry:
enabled: true
endpoint: "http://localhost:4318"
logging:
level: "info"
format: "json"
# Service Configurations
services:
# ============================================
# TIER 1: Direct REST API Conversions
# ============================================
- name: radarr
type: rest
enabled: true
baseUrl: "http://192.168.1.175:7878/api/v3"
description: "Movie collection management and automation"
authentication:
type: apikey
header: "X-Api-Key"
value: "${RADARR_API_KEY}"
timeout: 30s
retry:
max_attempts: 3
backoff: exponential
- name: sonarr
type: rest
enabled: true
baseUrl: "http://192.168.1.175:8989/api/v3"
description: "TV series collection management and automation"
authentication:
type: apikey
header: "X-Api-Key"
value: "${SONARR_API_KEY}"
timeout: 30s
retry:
max_attempts: 3
backoff: exponential
- name: plex
type: rest
enabled: true
baseUrl: "http://192.168.1.219:32400"
description: "Plex Media Server API"
authentication:
type: header
header: "X-Plex-Token"
value: "NNHuTaV8e1wy78cdWYVX"
timeout: 30s
- name: prowlarr
type: rest
enabled: true
baseUrl: "http://192.168.1.175:9696/api/v1"
description: "Indexer manager and proxy"
authentication:
type: apikey
header: "X-Api-Key"
value: "${PROWLARR_API_KEY}"
timeout: 30s
- name: qbittorrent
type: rest
enabled: true
baseUrl: "http://192.168.1.175:8080/api/v2"
description: "Torrent client management"
authentication:
type: cookie
login_endpoint: "/auth/login"
credentials:
username: "${QBITTORRENT_USER}"
password: "${QBITTORRENT_PASS}"
timeout: 30s
- name: sabnzbd
type: rest
enabled: true
baseUrl: "http://192.168.1.175:8085/api"
description: "Usenet downloader"
authentication:
type: query_param
param: "apikey"
value: "${SABNZBD_API_KEY}"
timeout: 30s
# ============================================
# TIER 2: MCP Server Federation (Proxies)
# ============================================
- name: graphql-tools
type: mcp-proxy
enabled: true
description: "GraphQL APIs exposed via apollo-mcp-server"
endpoint: "http://localhost:4000"
transport: http
prefix: "graphql"
health_check:
enabled: true
endpoint: "/health"
interval: 60s
retry:
max_attempts: 3
backoff: exponential
# Tool Discovery and Registration
discovery:
enabled: true
auto_register: true
scan_interval: 300s
# Virtual MCP Server Composition
composition:
enabled: true
namespaces:
- name: "media"
services: ["radarr", "sonarr", "plex"]
description: "Media management tools"
- name: "downloads"
services: ["prowlarr", "qbittorrent", "sabnzbd"]
description: "Download management tools"
- name: "dev"
services: ["graphql-tools"]
description: "Development and API tools"
EOF
cat > .env <<'EOF'
# Security
JWT_SECRET=your-super-secret-jwt-key-change-this
ADMIN_PASSWORD=your-admin-password-here
# Radarr
RADARR_API_KEY=your_radarr_api_key_here
# Sonarr
SONARR_API_KEY=your_sonarr_api_key_here
# Prowlarr
PROWLARR_API_KEY=your_prowlarr_api_key_here
# qBittorrent
QBITTORRENT_USER=admin
QBITTORRENT_PASS=your_qbittorrent_password
# SABnzbd
SABNZBD_API_KEY=your_sabnzbd_api_key_here
EOF
cat > docker-compose.yml <<'EOF'
version: '3.8'
services:
mcp-context-forge:
image: ghcr.io/ibm/mcp-context-forge:latest
container_name: mcp-context-forge
ports:
- "3000:3000"
environment:
- JWT_SECRET=${JWT_SECRET}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
- RADARR_API_KEY=${RADARR_API_KEY}
- SONARR_API_KEY=${SONARR_API_KEY}
- PROWLARR_API_KEY=${PROWLARR_API_KEY}
- QBITTORRENT_USER=${QBITTORRENT_USER}
- QBITTORRENT_PASS=${QBITTORRENT_PASS}
- SABNZBD_API_KEY=${SABNZBD_API_KEY}
volumes:
- ./config.yml:/app/config/config.yml:ro
- ./data:/app/data
- ./logs:/app/logs
depends_on:
- apollo-mcp
restart: unless-stopped
networks:
- mcp-network
networks:
mcp-network:
external: true
name: apollo-mcp-server_mcp-network
EOF
# Start the service
docker-compose up -d
# Check logs
docker-compose logs -f
# Test the endpoints
curl http://localhost:3000/health
curl http://localhost:3000/admin -u admin:your-admin-password
# Test tool discovery
curl http://localhost:3000/tools
# Install ngrok (if not already installed)
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | \
sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null && \
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | \
sudo tee /etc/apt/sources.list.d/ngrok.list && \
sudo apt update && sudo apt install ngrok
# Configure ngrok with your auth token
ngrok config add-authtoken YOUR_NGROK_AUTH_TOKEN
# Create ngrok configuration
mkdir -p ~/.ngrok2
cat > ~/.ngrok2/ngrok.yml <<'EOF'
version: "2"
authtoken: YOUR_NGROK_AUTH_TOKEN
tunnels:
mcp-gateway:
proto: http
addr: 3000
bind_tls: true
inspect: true
hostname: your-custom-domain.ngrok.io # Optional: requires paid plan
EOF
# Start ngrok tunnel
ngrok start mcp-gateway
# Or simple command
ngrok http 3000 --log=stdout
Save your ngrok URL: https://abc123.ngrok.io
To ensure everything starts on boot:
# Create systemd service for ngrok
cat > /etc/systemd/system/ngrok-mcp.service <<'EOF'
[Unit]
Description=ngrok tunnel for MCP Gateway
After=network.target docker.service
[Service]
Type=simple
User=root
WorkingDirectory=/root
ExecStart=/usr/local/bin/ngrok http 3000 --log=stdout
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# Enable and start the service
systemctl daemon-reload
systemctl enable ngrok-mcp.service
systemctl start ngrok-mcp.service
# Check status
systemctl status ngrok-mcp.service
- Go to https://platform.openai.com/playground/assistants
- Create new Agent
- Click "Add Tools" β "MCP Server"
- Enter your ngrok URL:
https://abc123.ngrok.io
- Add authentication header:
Authorization: Bearer YOUR_JWT_TOKEN
Add to ~/Library/Application Support/Claude/claude_desktop_config.json
:
{
"mcpServers": {
"media-gateway": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-client"],
"env": {
"MCP_SERVER_URL": "https://abc123.ngrok.io",
"MCP_AUTH_TOKEN": "Bearer YOUR_JWT_TOKEN"
}
}
}
}
import requests
MCP_GATEWAY_URL = "https://abc123.ngrok.io"
AUTH_TOKEN = "Bearer YOUR_JWT_TOKEN"
headers = {
"Authorization": AUTH_TOKEN,
"Content-Type": "application/json"
}
# Discover available tools
response = requests.get(f"{MCP_GATEWAY_URL}/tools", headers=headers)
tools = response.json()
print(f"Available tools: {len(tools)}")
for tool in tools:
print(f" - {tool['name']}: {tool['description']}")
# Call a tool
response = requests.post(
f"{MCP_GATEWAY_URL}/tools/radarr_search_movie",
headers=headers,
json={"query": "The Matrix"}
)
result = response.json()
print(f"Search result: {result}")
Radarr:
radarr_list_movies()
- List all moviesradarr_search_movie(title)
- Search for movieradarr_add_movie(title, quality_profile)
- Add movie to libraryradarr_get_queue()
- Check download queueradarr_delete_movie(id)
- Remove movie
Sonarr:
sonarr_list_series()
- List all TV seriessonarr_search_series(title)
- Search for seriessonarr_add_series(title, quality_profile)
- Add series to librarysonarr_get_episodes(series_id)
- List episodessonarr_get_queue()
- Check download queue
Plex:
plex_list_libraries()
- List all media librariesplex_search(query)
- Search all contentplex_get_recently_added(count)
- Recently added itemsplex_get_on_deck()
- Continue watchingplex_play_status()
- Current playback status
Prowlarr:
prowlarr_list_indexers()
- List configured indexersprowlarr_search(query)
- Search across all indexersprowlarr_test_indexer(id)
- Test indexer connection
qBittorrent:
qbittorrent_list_torrents()
- List all torrentsqbittorrent_add_torrent(url)
- Add torrentqbittorrent_pause_torrent(hash)
- Pause torrentqbittorrent_resume_torrent(hash)
- Resume torrentqbittorrent_delete_torrent(hash)
- Delete torrent
SABnzbd:
sabnzbd_get_queue()
- Get download queuesabnzbd_add_nzb(url)
- Add NZB downloadsabnzbd_pause_queue()
- Pause all downloadssabnzbd_resume_queue()
- Resume downloadssabnzbd_get_history()
- Download history
GitHub:
github_list_repos(owner, first)
- List repositoriesgithub_search_repos(query)
- Search repositoriesgithub_create_issue(repository_id, title, body)
- Create issuegithub_list_issues(owner, repo)
- List issuesgithub_get_repo(owner, name)
- Get repository details
Shopify (if configured):
shopify_get_products(first, query)
- List productsshopify_create_product(title, description)
- Create productshopify_update_inventory(variant_id, quantity)
- Update inventory
# Test apollo-mcp-server
curl http://192.168.1.175:4000/health
# Test mcp-context-forge
curl http://192.168.1.175:3000/health
# Test through ngrok
curl https://abc123.ngrok.io/health
# List all available tools
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
https://abc123.ngrok.io/tools | jq .
# Should return tools from both REST and GraphQL tiers
curl -X POST \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "The Matrix"}' \
https://abc123.ngrok.io/tools/radarr_search_movie
curl -X POST \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"owner": "jmanhype", "first": 10}' \
https://abc123.ngrok.io/tools/github_list_repos
Open browser: http://192.168.1.175:3000/admin
- Username:
admin
- Password: (from your .env file)
You should see:
- Active tools count
- Request metrics
- Real-time traffic
- Configuration management
# In mcp-context-forge config.yml
security:
jwt:
enabled: true
secret: "USE-A-LONG-RANDOM-STRING-HERE" # 32+ chars
expiry: "24h"
api_keys:
enabled: true
keys:
- name: "openai-agent"
key: "sk-mcp-your-random-key-here"
permissions: ["read", "write"]
- name: "claude-desktop"
key: "sk-mcp-another-random-key"
permissions: ["read"]
security:
rate_limiting:
enabled: true
rules:
- name: "default"
requests_per_minute: 100
burst: 20
- name: "expensive-tools"
pattern: "^(radarr|sonarr)_add_.*"
requests_per_minute: 10
burst: 2
# In ngrok config, force HTTPS
ngrok http 3000 --bind-tls=true
# docker-compose.yml
networks:
mcp-network:
driver: bridge
internal: false # Set to true for no external access
ipam:
config:
- subnet: 172.20.0.0/16
Never commit secrets! Use environment variables or secret management:
# Option 1: .env file (gitignored)
cat > .env <<EOF
JWT_SECRET=$(openssl rand -base64 32)
ADMIN_PASSWORD=$(openssl rand -base64 16)
EOF
# Option 2: Docker secrets
echo "your-jwt-secret" | docker secret create jwt_secret -
# Option 3: HashiCorp Vault (production)
vault kv put secret/mcp-gateway \
jwt_secret="..." \
admin_password="..."
# Add OpenTelemetry collector to docker-compose.yml
cat >> docker-compose.yml <<'EOF'
otel-collector:
image: otel/opentelemetry-collector:latest
container_name: otel-collector
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4318:4318" # OTLP HTTP
- "4317:4317" # OTLP gRPC
networks:
- mcp-network
EOF
# Create OpenTelemetry config
cat > otel-config.yaml <<'EOF'
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 10s
exporters:
logging:
loglevel: debug
prometheus:
endpoint: "0.0.0.0:8889"
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [logging]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [logging, prometheus]
EOF
# Add to docker-compose.yml
cat >> docker-compose.yml <<'EOF'
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
ports:
- "9090:9090"
networks:
- mcp-network
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-data:/var/lib/grafana
networks:
- mcp-network
volumes:
prometheus-data:
grafana-data:
EOF
Solution: Use pre-built image or increase build timeout
# Option 1: Increase Docker timeout
DOCKER_BUILDKIT=1 docker-compose build --no-cache --build-arg BUILDKIT_INLINE_CACHE=1
# Option 2: Build on host (faster)
cargo build --release
cp target/release/apollo-mcp-server ./apollo-mcp-server/
# Then use COPY in Dockerfile instead of building
Solution: Check network connectivity
# From mcp-context-forge container
docker exec -it mcp-context-forge curl http://apollo-mcp-server:4000/health
# Check if containers are on same network
docker network inspect apollo-mcp-server_mcp-network
Solution: Use systemd service or paid ngrok plan
# Check ngrok logs
journalctl -u ngrok-mcp.service -f
# Restart service
systemctl restart ngrok-mcp.service
Solution: Verify environment variables are loaded
# Check container environment
docker exec mcp-context-forge env | grep API_KEY
# Re-create container with new env
docker-compose down
docker-compose up -d
- Create GraphQL operations file
- Add to apollo-mcp-server config
- Restart apollo-mcp-server
- Tools automatically appear in mcp-context-forge
# In mcp-context-forge config.yml
services:
- name: custom-python-server
type: mcp-proxy
endpoint: "http://my-custom-server:5000"
transport: stdio
# Run multiple mcp-context-forge instances
services:
mcp-forge-1:
image: ghcr.io/ibm/mcp-context-forge:latest
# ... config
mcp-forge-2:
image: ghcr.io/ibm/mcp-context-forge:latest
# ... config
nginx:
image: nginx:latest
# Load balance across instances
security:
oauth:
enabled: true
providers:
- name: google
client_id: "${GOOGLE_CLIENT_ID}"
client_secret: "${GOOGLE_CLIENT_SECRET}"
redirect_uri: "https://abc123.ngrok.io/oauth/callback"
- IBM mcp-context-forge: https://github.com/IBM/mcp-context-forge
- Apollo MCP Server: https://github.com/apollographql/apollo-mcp-server
- Model Context Protocol: https://modelcontextprotocol.io
- ngrok Documentation: https://ngrok.com/docs
- Radarr API: https://radarr.video/docs/api/
- Sonarr API: https://sonarr.tv/docs/api/
- Plex API: https://www.plexopedia.com/plex-media-server/api/
- GitHub GraphQL API: https://docs.github.com/en/graphql
- Proxmox Host: Already owned (ZimaBoard)
- LXC Container: Free (part of Proxmox)
- Docker: Free and open source
- mcp-context-forge: Free (Apache 2.0)
- apollo-mcp-server: Free (MIT)
- ngrok Free Tier:
- 1 concurrent tunnel
- Random URL (e.g., abc123.ngrok.io)
- HTTP/HTTPS traffic
- Limitation: URL changes on restart
Total: $0/month
-
ngrok Personal Plan: $8/month
- 3 concurrent tunnels
- Custom domain (your-gateway.ngrok.io)
- Static URL (doesn't change)
- Priority support
-
ngrok Pro Plan: $20/month
- Everything in Personal
- IP whitelisting
- OAuth integration
- Advanced features
Total: $8-20/month (only ngrok)
- ngrok Enterprise: Custom pricing
- Dedicated Proxmox Hardware: One-time cost
- Additional API quota costs: Varies by service
You now have a production-grade, federated MCP gateway that:
β Exposes REST APIs (Radarr, Sonarr, Plex, etc.) as MCP tools β Exposes GraphQL APIs (GitHub, Shopify, etc.) as MCP tools β Provides unified authentication and security β Offers admin UI for management β Scales horizontally β Costs $0-20/month β Runs on your ZimaBoard Proxmox infrastructure
Architecture Benefits:
- Tier 1 (apollo-mcp-server): Specialized GraphQL conversion
- Tier 2 (mcp-context-forge): Central gateway, REST conversion, and federation
- Result: Single public endpoint with all your APIs as AI-accessible tools
Generated: October 12, 2025 Author: AI Infrastructure Assistant Platform: ZimaBoard + Proxmox VE 8.4.1 LXC: 103 (192.168.1.175) Architecture: Multi-Tier MCP Federation (apollo-mcp-server + mcp-context-forge) Public Access: ngrok HTTPS tunnel