Skip to content

schwarztim/enterprise-mcp-gateway

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Enterprise MCP Gateway

A production-ready gateway for exposing Model Context Protocol (MCP) servers to AI coding assistants (Claude Code, OpenCode, etc.) with Azure Entra ID authentication.

Architecture

┌──────────────┐     ┌─────────────────────────────────┐     ┌──────────────────┐
│  AI Client   │────▶│  Caddy (TLS + Routing)  :443    │     │  MCP Containers  │
│  (Claude,    │     │                                 │     │  (via ToolHive)  │
│   OpenCode)  │     │  /.well-known/*  → OAuth meta   │     │                  │
│              │     │  /oauth2/authorize → auth-proxy  │────▶│  github    :9100 │
│              │     │  /oauth2/token  → token-proxy   │     │  atlassian :9101 │
│              │     │  /register      → static JSON   │     │  custom    :9102 │
│              │     │  /mcp           → workload proxy│     │  ...             │
│              │     │  /*             → ToolHive API   │     └──────────────────┘
└──────────────┘     └─────────────────────────────────┘
                              │           │
                     ┌────────┘           └────────┐
                     ▼                             ▼
              ┌─────────────┐              ┌─────────────┐
              │ auth-proxy  │              │ token-proxy  │
              │ :9090       │              │ :9091        │
              │             │              │             │
              │ Strips:     │              │ Strips:     │
              │ - resource  │              │ - resource  │
              │ - prompt    │              │             │
              │             │              │ Swaps:      │
              └──────┬──────┘              │ access_token│
                     │                     │ ← id_token  │
                     ▼                     └──────┬──────┘
              ┌─────────────┐                     │
              │ Azure Entra │◀────────────────────┘
              │ ID (v2.0)   │
              └─────��───────┘

Why This Exists

MCP clients authenticate to remote servers using RFC 9728 OAuth discovery. When the OAuth provider is Azure Entra ID, several incompatibilities arise:

Problem Root Cause Solution
resource parameter rejected Entra v2.0 doesn't support the resource parameter that some MCP SDKs send auth-proxy strips it from authorize requests; token-proxy strips it from token exchange
Forced admin consent screen MCP SDKs send prompt=consent auth-proxy strips the prompt parameter
Token signature verification fails MS Graph access tokens use a non-standard JWT format (nonce in header) that standard OIDC libraries can't verify token-proxy swaps the access_token with the id_token in responses
Redirect URI mismatch MCP clients use dynamic localhost ports for callbacks Register redirect URIs as "Mobile and desktop applications" platform in Entra (ignores port)

Components

Component Port Purpose
Caddy 443 TLS termination, routing, RFC 9728 OAuth metadata
auth-proxy 9090 Strips resource and prompt from authorize requests
token-proxy 9091 Strips resource from token exchange, swaps access_token with id_token
ToolHive 8080 MCP server lifecycle management (Podman/Docker containers)
Workload proxies 9100+ Per-server MCP endpoints with OIDC auth + Cedar authorization

Prerequisites

  • Linux server (RHEL 8+/9+, Ubuntu 20.04+)
  • Caddy v2.7+
  • ToolHive (thv) binary
  • Podman (rootless) or Docker
  • Python 3.6+
  • Azure Entra ID app registration
  • TLS certificate for your domain

Quick Start

1. Azure Entra ID Setup

Create an app registration:

# Note these values — you'll need them throughout
TENANT_ID="your-tenant-id"
CLIENT_ID="your-client-id"
APP_NAME="Enterprise MCP Gateway"

Configure the app:

  • Authentication → Add platform → Mobile and desktop applications
    • Redirect URI: http://127.0.0.1/mcp/oauth/callback
    • Also add: http://localhost/mcp/oauth/callback
  • API Permissions → Add:
    • email, offline_access, openid, profile, User.Read
    • Grant admin consent for your tenant
  • Token configuration (optional) → Add groups claim for Cedar authorization

2. Server Setup

# Clone this repo
git clone https://github.com/YOUR_ORG/enterprise-mcp-gateway.git
cd enterprise-mcp-gateway

# Copy and configure
cp .env.example .env
# Edit .env with your Entra tenant ID, client ID, domain, etc.
source .env

# Generate configs from templates
./scripts/setup.sh

# Install systemd services
./scripts/install-services.sh

3. TLS Certificates

Place your TLS certificate and key:

mkdir -p ~/tls
cp /path/to/cert.pem ~/tls/cert.pem
cp /path/to/key.pem ~/tls/key.pem
chmod 600 ~/tls/key.pem

4. Start Services

# Enable and start all services
systemctl --user enable --now oauth2-proxy token-proxy toolhive

# Start Caddy (system service with access to port 443)
sudo systemctl enable --now caddy-mcp

5. Add MCP Servers

# Run an MCP server with a fixed proxy port
thv run \
  --proxy-port 9100 \
  --transport stdio \
  -e GITHUB_PERSONAL_ACCESS_TOKEN="ghp_..." \
  --permission network \
  ghcr.io/github/github-mcp-server:latest \
  github

# Verify it's running
thv list

6. Client Configuration

OpenCode

{
  "mcp": {
    "my-gateway": {
      "type": "remote",
      "url": "https://your-domain.example.com/mcp"
    }
  }
}

Then authenticate:

opencode mcp auth my-gateway

Claude Code

{
  "mcpServers": {
    "my-gateway": {
      "type": "url",
      "url": "https://your-domain.example.com/mcp"
    }
  }
}

Configuration

Environment Variables

Copy .env.example to .env and configure:

# Azure Entra ID
ENTRA_TENANT_ID="your-tenant-id"
ENTRA_CLIENT_ID="your-client-id"

# Server
GATEWAY_DOMAIN="mcp.example.com"
TLS_CERT_PATH="$HOME/tls/cert.pem"
TLS_KEY_PATH="$HOME/tls/key.pem"

# Proxy ports (for corporate environments)
HTTPS_PROXY=""
HTTP_PROXY=""
NO_PROXY="localhost,127.0.0.1"

# MCP workload ports (fixed)
MCP_PORT_GITHUB=9100
MCP_PORT_ATLASSIAN=9101

Cedar Authorization Policies

ToolHive supports Cedar policies for fine-grained access control. See authz/ for examples:

// Allow users in a specific Entra group to call MCP tools
permit(
  principal,
  action == Action::"call_tool",
  resource
) when {
  principal.claim_groups.contains("your-entra-group-id")
};

Note: To use group-based policies, configure your Entra app to include the groups claim in the id_token via Token configurationAdd groups claim.

Troubleshooting

Common Issues

"SSE error: Non-200 status code (404)"

  • The Caddy catch-all is routing to ToolHive's management API instead of a workload proxy
  • Ensure /mcp route points to the workload proxy port (e.g., 9100), not ToolHive serve (8080)

"Invalid or expired state parameter - potential CSRF attack"

  • Multiple auth processes are running simultaneously
  • Kill all auth processes and retry with a single instance

"AADSTS9010010: resource parameter doesn't match scopes"

  • The resource parameter is leaking through to Entra
  • Verify both auth-proxy and token-proxy are running and Caddy routes to them

Token signature verification fails

  • MS Graph access tokens have a non-standard JWT format
  • Verify token-proxy is swapping access_token with id_token

Need admin approval / consent screen

  • The prompt=consent parameter is not being stripped
  • Verify auth-proxy strips the prompt parameter
  • Ensure admin consent is granted for all API permissions in Entra

Debugging

# Check service status
systemctl --user status oauth2-proxy token-proxy toolhive

# View proxy logs
journalctl --user -u oauth2-proxy -f
journalctl --user -u token-proxy -f

# Test endpoints directly
curl -sk https://your-domain/health
curl -sk https://your-domain/.well-known/oauth-authorization-server | jq
curl -sk https://your-domain/.well-known/oauth-protected-resource | jq

# Test MCP with a token
TOKEN="your-jwt-token"
curl -sk -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' \
  https://your-domain/mcp

Project Structure

├── proxy/
│   ├── auth-proxy.py          # OAuth authorize proxy
│   └── token-proxy.py         # OAuth token exchange proxy
├── caddy/
│   └── Caddyfile.template     # Caddy config template
├── systemd/
│   ├── oauth2-proxy.service   # auth-proxy systemd unit
│   ├── token-proxy.service    # token-proxy systemd unit
│   └── toolhive.service.template  # ToolHive systemd unit template
├── authz/
│   ├── example-policy.cedar   # Example Cedar policy (role-based)
│   └── example-authz.json     # Example ToolHive authz config (group-based)
├── scripts/
│   ├── setup.sh               # Generate configs from templates
│   └── install-services.sh    # Install systemd services
├── .env.example               # Configuration template
└── README.md

How It Works

  1. Client discovers OAuth metadata via RFC 9728 (/.well-known/oauth-protected-resource/.well-known/oauth-authorization-server)
  2. Client registers via /register (returns pre-configured Entra client ID)
  3. Authorization code flow through auth-proxy (strips incompatible params) → Entra → callback
  4. Token exchange through token-proxy (strips resource, swaps access_token with id_token) → Entra
  5. MCP requests with Bearer token → Caddy → workload proxy (OIDC validation) → MCP container

Security Considerations

  • TLS is required — Caddy handles termination
  • All proxy components bind to 127.0.0.1 (localhost only)
  • No secrets in config files — use environment variables or secret managers
  • Cedar policies provide fine-grained authorization per MCP server
  • The id_token swap is safe because the id_token audience matches the client_id and is verified by standard OIDC

License

MIT

About

Production-ready gateway for exposing MCP servers to AI coding assistants with Azure Entra ID OAuth2 authentication

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors