Skip to main content

Getting Started with MCP Gateway

This guide walks you through setting up Traefik Hub MCP Gateway to proxy and govern a remote MCP server. You'll configure authentication, implement tool filtering policies, and verify the setup with an MCP client.

What You'll Build

By the end of this guide, you will have:

  • MCP Gateway proxying DeepWiki's public MCP server
  • JWT-based authentication protecting access
  • Policy-based tool filtering (controlling access to tools and resources)
  • OAuth 2.1/2.0 resource discovery for MCP clients
  • Verified setup using MCP Inspector

Why use MCP Gateway?

  • Centralized governance: Control which tools and resources users can access across all MCP servers
  • Security: Add authentication and authorization to MCP servers that lack it
  • Observability: Monitor all MCP operations with OpenTelemetry metrics and traces
  • Scalability: Proxy multiple remote MCP servers through a single gateway

About DeepWiki MCP

This guide uses DeepWiki, a public MCP server that provides AI agents with access to GitHub repository documentation. DeepWiki is ideal for this tutorial because:

  • Publicly accessible: No authentication required to access the upstream server, letting us focus on adding gateway-level security
  • Real-world use case: Demonstrates how to govern AI agent access to external knowledge sources
  • Multiple tools: Provides several tools (read_wiki_structure, read_wiki_contents, ask_question) that we can selectively allow or deny through policies

DeepWiki connects to GitHub repositories and makes their documentation queryable by AI agents. We'll use the MCP Gateway to add authentication and implement tool-level access control—allowing some users to read documentation structure while restricting others from querying content.

Prerequisites

Before you begin, ensure you have:

  • Kubernetes cluster: k3s, k3d, kind, or any Kubernetes cluster
  • Traefik Hub license: Log in to the Traefik Hub Online Dashboard and create a new Hub Gateway. If you don't yet have a Traefik Hub account, please contact our sales team.
  • MCP Inspector: For testing the gateway. We'll set this up later with npx or Docker (no installation needed)
Quick Cluster Setup

If you don't have a cluster, create one with k3d:

k3d cluster create traefik-hub --port 80:30000@loadbalancer --port 443:30001@loadbalancer --k3s-arg "--disable=traefik@server:0"

Step 1: Enable MCP Gateway

First, install Traefik Hub with the MCP Gateway feature enabled.

helm repo add traefik https://traefik.github.io/charts
helm repo update

helm install traefik traefik/traefik \
--namespace traefik \
--create-namespace \
--set hub.token=<YOUR_HUB_TOKEN> \
--set hub.mcpgateway.enabled=true \
--set hub.mcpgateway.maxRequestBodySize=2097152

What this does:

  • hub.mcpgateway.enabled=true: Enables the MCP Gateway feature
  • hub.mcpgateway.maxRequestBodySize=2097152: Sets max request body to 2MB (default is 1MB)

Verify installation:

kubectl -n traefik wait --for=condition=ready pod -l app.kubernetes.io/name=traefik --timeout=90s

Step 2: Create External Service for DeepWiki MCP Server

Create a Kubernetes ExternalName service that points to DeepWiki's public MCP server. This allows the gateway to route traffic to the remote server.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: deepwiki-mcp-server
namespace: apps
spec:
type: ExternalName
externalName: mcp.deepwiki.com
ports:
- name: https
port: 443
protocol: TCP
EOF

Verify the service:

kubectl get svc deepwiki-mcp-server -n apps

Expected output:

NAME                  TYPE           CLUSTER-IP   EXTERNAL-IP         PORT(S)   AGE
deepwiki-mcp-server ExternalName <none> mcp.deepwiki.com 443/TCP 5s

Step 3: Configure JWT Authentication

Create a JWT middleware to authenticate users accessing the MCP server.

Create JWT Secret

For this guide, we'll use a basic signing secret. In production, use JWKS URLs or public/private key pairs.

kubectl create secret generic mcp-jwt-secret -n apps \
--from-literal=signingSecret='my-super-secret-key-change-in-production'

Create JWT Middleware

cat <<EOF | kubectl apply -f -
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: deepwiki-mcp-jwt
namespace: apps
spec:
plugin:
jwt:
signingSecret: urn:k8s:secret:mcp-jwt-secret:signingSecret
forwardAuthorization: false
wwwAuthenticate: https://mcp.localhost/.well-known/oauth-protected-resource/deepwiki-mcp
forwardHeaders:
X-User-ID: sub
X-User-Groups: groups
EOF
Why forwardAuthorization is false

DeepWiki's MCP server doesn't require authentication, so we set forwardAuthorization: false to prevent the JWT token from being forwarded to the upstream server. The JWT is only used by the gateway for authentication and policy enforcement. The user identity is extracted to X-User-ID and X-User-Groups headers if needed by the upstream.

Understanding the wwwAuthenticate Option

The wwwAuthenticate option needs to point to the auto-generated well-known endpoint created by the MCP Gateway.

How it works:

  1. You configure resourceMetadata.resource in the MCP middleware (we'll do this in Step 4)
  2. The MCP Gateway automatically creates a well-known endpoint following this pattern:
    • Pattern: https://<host>/.well-known/oauth-protected-resource/<path>
    • Example: https://mcp.localhost/.well-known/oauth-protected-resource/github-mcp
  3. The JWT middleware's wwwAuthenticate should point to this exact URL

In our configuration:

  • MCP middleware will have: resource: https://mcp.localhost/deepwiki-mcp
  • Auto-generated endpoint: https://mcp.localhost/.well-known/oauth-protected-resource/deepwiki-mcp
  • JWT wwwAuthenticate: Points to the auto-generated endpoint above

Why this matters: When the JWT middleware returns a 401 Unauthorized, it includes a WWW-Authenticate header pointing to this URL. OAuth-compliant MCP clients use this to discover authorization requirements automatically (per RFC 6750).

We'll verify this endpoint works correctly in Step 7.

Step 4: Configure MCP Middleware

Create the MCP middleware that will proxy DeepWiki's MCP server. We'll start with an allow-all configuration for testing, then add restrictive policies in the smoke tests.

cat <<EOF | kubectl apply -f -
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: deepwiki-mcp-gateway
namespace: apps
spec:
plugin:
mcp:
resourceMetadata:
resource: https://mcp.localhost/deepwiki-mcp
authorizationServers:
- https://mcp.localhost/oauth/authorize
resourceDocumentation: https://docs.devin.ai/work-with-devin/deepwiki-mcp
defaultAction: allow
EOF
Authorization Server

The authorizationServers field is required by the MCP Gateway but we're using a placeholder URL (https://mcp.localhost/oauth/authorize) since DeepWiki doesn't require OAuth authentication. In production, replace this with your actual OAuth provider (for example, https://your-tenant.auth0.com/authorize, https://accounts.google.com/o/oauth2/v2/auth, etc.).

Configuration explanation:

  • defaultAction: allow: Allows all MCP operations for initial testing. We'll add restrictive policies later in the smoke tests to demonstrate Task-Based Access Control (TBAC)—the authorization model that enables fine-grained control over what tasks agents can perform, which tools they can access, and what transaction limits apply.

Verify middleware:

kubectl get middleware deepwiki-mcp-gateway -n apps

Step 5: Create StripPrefix Middleware

Create a StripPrefix middleware to remove the /deepwiki-mcp prefix before forwarding requests to the upstream server.

cat <<EOF | kubectl apply -f -
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: deepwiki-strip-prefix
namespace: apps
spec:
stripPrefix:
prefixes:
- /deepwiki-mcp
EOF

What this does:

The StripPrefix middleware removes the /deepwiki-mcp prefix from the request path before forwarding to the upstream MCP server. For example:

  • Request: http://mcp.localhost/deepwiki-mcp/mcp
  • Forwarded to upstream: https://mcp.deepwiki.com/mcp

This middleware will be used in the IngressRoute we create in the next step.

Verify the middleware:

kubectl get middleware deepwiki-strip-prefix -n apps

Step 6: Create IngressRoute

Create an IngressRoute that chains the JWT, MCP, and StripPrefix middlewares together and routes traffic to the DeepWiki MCP server.

cat <<EOF | kubectl apply -f -
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: deepwiki-mcp-route
namespace: apps
spec:
entryPoints:
- web
routes:
- kind: Rule
match: Host(\`mcp.localhost\`) && PathPrefix(\`/deepwiki-mcp\`)
middlewares:
- name: deepwiki-mcp-jwt
- name: deepwiki-mcp-gateway
- name: deepwiki-strip-prefix
services:
- name: deepwiki-mcp-server
port: 443
scheme: https
passHostHeader: false
EOF

Middleware chain order matters:

  1. JWT middleware first: Validates the token and extracts claims
  2. MCP middleware second: Uses JWT claims to enforce policies
  3. StripPrefix middleware third: Removes the /deepwiki-mcp prefix before forwarding to the upstream server

Verify the route:

kubectl get ingressroute deepwiki-mcp-route -n apps

Step 7: Test OAuth Discovery Endpoint

The MCP Gateway automatically creates an OAuth 2.1/2.0 resource metadata endpoint. Let's verify it works.

curl http://mcp.localhost/.well-known/oauth-protected-resource/deepwiki-mcp | jq

Expected output:

{
"resource": "https://mcp.localhost/deepwiki-mcp",
"authorization_servers": [
"https://mcp.localhost/oauth/authorize"
],
"bearer_methods_supported": [
"header"
],
"resource_documentation": "https://docs.devin.ai/work-with-devin/deepwiki-mcp"
}

Success! The MCP Gateway has automatically generated the OAuth discovery endpoint based on your resourceMetadata configuration.

This endpoint allows MCP clients to discover:

  • What resource this endpoint protects (resource)
  • Where to get authorization (authorization_servers)
  • How to send authentication tokens (bearer_methods_supported)
  • Where to find documentation (resource_documentation)
DeepWiki Authentication

While DeepWiki's MCP server doesn't require authentication from DeepWiki itself, our MCP Gateway adds JWT authentication as a governance layer. The authorization_servers field points to a placeholder OAuth endpoint - in production, you'd replace this with your actual OAuth provider.

Step 8: Generate a Test JWT

Create a JWT token for testing. This token will include a groups claim to test our policies.

First, create a small script to generate the JWT:

cat > generate-jwt.sh <<'SCRIPT'
#!/bin/bash
# Simple JWT generator for testing
# Requires: jwt-cli (install with: cargo install jwt-cli)

SECRET="my-super-secret-key-change-in-production"

jwt encode \
--secret "$SECRET" \
--exp='+1 hour' \
'{"sub":"test-user","groups":["developer"],"email":"[email protected]"}'
SCRIPT

chmod +x generate-jwt.sh
Install jwt-cli

If you don't have jwt-cli, install it:

# Using Homebrew (macOS/Linux)
brew install mike-engel/jwt-cli/jwt-cli

# Using cargo
cargo install jwt-cli

# Using Manjaro/Arch:
sudo pacman -S jwt-cli

Generate the token:

export TEST_JWT=$(./generate-jwt.sh)
echo $TEST_JWT

Verify the token (optional):

jwt decode $TEST_JWT

You should see the payload with sub, groups, and email claims.

Step 9: Verify the Setup with curl

Let's verify that the MCP Gateway is working correctly before connecting with MCP Inspector.

Test OAuth Discovery

Verify that the auto-generated OAuth discovery endpoint is accessible:

curl http://mcp.localhost/.well-known/oauth-protected-resource/deepwiki-mcp | jq

Expected output:

{
"resource": "https://mcp.localhost/deepwiki-mcp",
"authorization_servers": [
"https://mcp.localhost/oauth/authorize"
],
"bearer_methods_supported": [
"header"
],
"scopes_supported": null,
"resource_documentation": "https://docs.devin.ai/work-with-devin/deepwiki-mcp"
}

Test JWT Authentication

Verify that JWT authentication is working by calling the MCP endpoint without a token:

curl -v http://mcp.localhost/deepwiki-mcp/mcp

Expected output:

curl -v http://mcp.localhost/deepwiki-mcp/mcp
* Host mcp.localhost:80 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:80...
* Connected to mcp.localhost (::1) port 80
> GET /deepwiki-mcp/mcp HTTP/1.1
> Host: mcp.localhost
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 401 Unauthorized
< Www-Authenticate: https://mcp.localhost/.well-known/oauth-protected-resource/deepwiki-mcp
< Date: Fri, 03 Oct 2025 20:36:14 GMT
< Content-Length: 0
<
* Connection #0 to host mcp.localhost left intact

This confirms JWT authentication is required. Now test with your JWT:

curl -v \
-H "Authorization: Bearer $TEST_JWT" \
http://mcp.localhost/deepwiki-mcp/mcp

Expected behavior:

The connection should return an event-stream response. This is correct! It means:

  • JWT authentication passed
  • The request reached DeepWiki's MCP server
  • The MCP Gateway is proxying correctly

Enter Ctrl+C to stop the curl command.

If you get 404 Not Found, verify the IngressRoute is correctly configured.

Step 10: Connect with MCP Inspector

Now that the gateway is verified, let's use MCP Inspector to test the DeepWiki MCP server through the gateway.

Start MCP Inspector

MCP Inspector is a developer tool for testing and debugging MCP servers. It supports Streamable HTTP, SSE, and stdio transports.

Start the MCP Inspector UI:

npx @modelcontextprotocol/inspector

The Inspector will launch at http://localhost:6274?MCP_PROXY_AUTH_TOKEN=YOUR_AUTOGENERATED_TOKEN

MCP Inspector Version

This guide uses MCP Inspector v0.17.0 or later. Earlier versions (like v0.16.6) have UI differences, including the absence of the "Via Proxy" connection option. If you encounter UI differences, update to the latest version using npx @modelcontextprotocol/inspector@latest or pull the latest Docker image.

Configure Connection

  1. Open http://localhost:6274 in your browser

  2. In the connection form, select Streamable HTTP transport

  3. Configure the connection:

    • Server URL: http://mcp.localhost/deepwiki-mcp/mcp

    • Connection Type: Via Proxy

    • Select Add Header and add:

      • Header name: Authorization
      • Header value: Bearer YOUR_JWT_TOKEN_HERE
  4. Replace YOUR_JWT_TOKEN_HERE with your test JWT:

echo $TEST_JWT

Copy the output and paste it into the Authorization header value.

  1. Select Connect

Expected result:

  • Connection status should show Connected
  • You should see available tools in the sidebar
DNS Resolution for Docker Users

If you're using the Docker version of MCP Inspector, the container cannot resolve mcp.localhost to your host's localhost. You'll see an error like:

Error: getaddrinfo ENOTFOUND mcp.docker.localhost

To fix this, get your Traefik LoadBalancer IP and add it to your host machine's /etc/hosts:

# Get Traefik LoadBalancer IP
kubectl get svc -A | grep LoadBalancer

Then add to /etc/hosts:

<TRAEFIK_LB_IP> mcp.localhost

For npx users, mcp.localhost should resolve automatically. If it doesn't, add to /etc/hosts:

127.0.0.1 mcp.localhost

Step 11: Smoke Test: Read Kubernetes Wiki Structure

Now let's test the DeepWiki MCP server by reading the Kubernetes repository wiki structure.

  1. In the MCP Inspector sidebar, select Tools

  2. Select List Tools to fetch available tools from the server

  3. Find and select read_wiki_structure

  4. Fill in the arguments with the repository in owner/repo format:

kubernetes/kubernetes
Argument Format

Enter the repository as a single string in owner/repo format. Do not add line breaks or extra formatting—only the bare string.

  1. Select Run Tool

You should see a list of documentation topics from the Kubernetes wiki! This confirms:

  • JWT authentication passed
  • MCP Gateway proxied the request
  • Policy allowed read_wiki_structure tool
  • DeepWiki returned the wiki structure

Wiki Structure

Smoke Test: Read Wiki Contents

  1. Select List Tools to fetch available tools from the server if you haven't already

  2. Select read_wiki_contents in the Tools sidebar

  3. Fill in the arguments with the repository in owner/repo format:

kubernetes/kubernetes
  1. Select Run Tool

You should see the generated content explaining the Kubernetes repo and it's content

Wiki Content

Step 12: Smoke Test: Policy-Based Access Control (TBAC)

Now let's test the MCP Gateway's policy enforcement by blocking the read_wiki_contents tool for regular users.

Update MCP Middleware with Restrictive Policy

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: deepwiki-mcp-gateway
namespace: apps
spec:
plugin:
mcp:
resourceMetadata:
resource: https://mcp.localhost/deepwiki-mcp
authorizationServers:
- https://mcp.localhost/oauth/authorize
resourceDocumentation: https://docs.devin.ai/work-with-devin/deepwiki-mcp
policies:
# Allow listing tools
- match: Equals(`mcp.method`, `tools/list`)
action: allow

# Allow read_wiki_structure for everyone
- match: Equals(`mcp.method`, `tools/call`) && Equals(`mcp.params.name`, `read_wiki_structure`)
action: allow

# Allow read_wiki_contents only for admin group
- match: Equals(`mcp.method`, `tools/call`) && Equals(`mcp.params.name`, `read_wiki_contents`) && Contains(`jwt.groups`, `admin`)
action: allow

defaultAction: deny

Test with Developer JWT (Should Fail)

Your current JWT has groups: ["developer"], so trying to call read_wiki_contents should now fail:

  1. Try calling read_wiki_contents again
  2. Expected result: 403 Forbidden

Read Wiki Content Endpoint Returning 403

Test with Admin JWT (Should Succeed)

Generate an admin JWT and test:

# Generate admin JWT
SECRET="my-super-secret-key-change-in-production"
ADMIN_JWT=$(jwt encode --secret "$SECRET" --exp='+1 hour' '{"sub":"admin-user","groups":["admin"],"email":"[email protected]"}')
echo $ADMIN_JWT

Update the Authorization header in MCP Inspector with the admin JWT and reconnect.

Now try read_wiki_contents again:

Expected result: Success! The tool returns wiki content because your JWT has the admin group.

Read Wiki Content Endpoint Returning 200 With Admnin JWT

What happens when a tool is blocked:

  • The MCP middleware returns HTTP 403 Forbidden
  • MCP Inspector displays the error in the response panel
  • You can see the exact policy that blocked the request in the Traefik DEBUG logs
2025-10-07T13:39:51Z DBG github.com/traefik/traefik-hub/v3/hub/pkg/middleware/mcp/middleware.go:188
> The request has been denied since no policy matched middlewareName=apps-deepwiki-mcp-
gateway@kubernetescrd middlewareType=mcp
Production Authentication

In production, replace the test JWT with a proper authentication flow:

  • Use the OIDC middleware for OAuth/OIDC authentication
  • Integrate with your identity provider (Okta, Auth0, Azure AD, etc.)
  • Extract user groups from OIDC claims
  • MCP Gateway policies will automatically use the real user's groups

Step 13: Monitor MCP Traffic

The MCP Gateway automatically emits OpenTelemetry metrics and traces for all MCP operations.

What gets collected:

  • For example,

  • Metrics: mcp.client.operation.duration histogram with attributes:

    • mcp.method.name:for example, tools/call, resources/read
    • mcp.tool.name: for example, read_wiki_content
    • mcp.session.id: Session identifier
    • mcp.request.id: Request identifier
    • mcp.request.argument.*: Tool arguments
  • Traces: Full request context including MCP method, tool name, and arguments

Example observability backends:

For detailed observability configuration, see the Observability section in the MCP middleware reference documentation .

Next Steps

Now that you have a working MCP Gateway setup, you can:

Connect Different Types of MCP Servers

The guide above showed how to protect an MCP server without built-in authentication (DeepWiki). Here are examples for other common scenarios:

API Key-Protected MCP Servers (For Example, GitHub MCP)

For MCP servers that require API keys or Personal Access Tokens (PAT), add a Headers middleware to inject the upstream authentication:

Headers middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: github-upstream-auth
namespace: apps
spec:
headers:
customRequestHeaders:
Authorization: "Bearer ghp_YOUR_GITHUB_PAT"

The Headers middleware injects the API key that the upstream MCP server requires, while your gateway still enforces JWT authentication and policies.

OAuth-Protected MCP Servers (Passthrough OAuth)

For MCP servers with existing OAuth protection (like Notion or Number Guessing Game), you need to:

  1. Copy the upstream's OAuth configuration to your gateway middleware
  2. Add HTTPS support (required for OAuth discovery)
  3. Configure CORS for browser-based clients
Example: Notion MCP with OAuth Passthrough
---
apiVersion: v1
kind: Service
metadata:
name: notion-mcp-server
namespace: apps
spec:
type: ExternalName
externalName: mcp.notion.com
ports:
- name: https
port: 443
protocol: TCP
Important notes for OAuth passthrough:
  • HTTPS entrypoint required: The .well-known/oauth-protected-resource endpoint must be accessible via HTTPS. Add websecure to entryPoints.
  • Accept self-signed cert: For local testing with MCP Inspector, visit https://notion-mcp.localhost/ in your browser and accept the self-signed certificate before connecting.
  • CORS headers: Set accessControlExposeHeaders: ["*"] to ensure MCP Inspector can read OAuth headers.
  • Resource URL override: Set resourceMetadata.resource to your gateway's URL (for example, https://notion-mcp.localhost/mcp).
  • Copy authorization servers: Set authorizationServers to match the upstream MCP server's values (found in their .well-known/oauth-protected-resource endpoint).

Replace Test JWTs with Production OAuth

The guide above used static test JWTs for simplicity. For production, replace them with real OAuth/OIDC authentication at the gateway level:

Gateway Authentication vs Upstream OAuth

This section is about authenticating users to your gateway (replacing the jwt-cli test tokens). This is different from the OAuth passthrough section above, which is about proxying MCP servers that have their own OAuth protection.

Production OAuth setup:

  • Configure JWT middleware to validate tokens from your OAuth provider (Auth0, Okta, Azure AD, etc.)
  • Point jwksUrl to your provider's JWKS endpoint (for example, https://your-tenant.auth0.com/.well-known/jwks.json)
  • Users authenticate with your OAuth provider and receive JWT access tokens
  • MCP clients send the JWT in the Authorization: Bearer header
  • JWT middleware validates the token signature and extracts claims for policy evaluation

See the JWT Middleware documentation for complete OAuth/OIDC configuration options.

Add Multi-Tenancy

Isolate users by tenant using variable substitution:

policies:
# Users can only access their tenant's resources
- match: Contains(`mcp.params.uri`, `tenant-${jwt.tenant_id}`)
action: allow

Explore Advanced Policies

Use the full power of the expression language:

String matching functions:

  • SplitContains for OAuth scopes: SplitContains('jwt.scope', ' ', 'mcp:admin')
  • OneOf for multiple allowed values: OneOf('mcp.params.name', 'tool1', 'tool2')
  • Prefix for path matching: Prefix('mcp.params.uri', 'file:///safe/')

Numeric comparison functions:

  • Lte for amount limits: Lte('mcp.params.arguments.amount', '${jwt.approval_limit}')
  • Gte for minimum values: Gte('jwt.clearance_level', '5')
  • Lt for strict less than: Lt('mcp.params.arguments.count', '100')
  • Gt for strict greater than: Gt('jwt.rate_limit', '${mcp.params.arguments.requests}')

Complex boolean logic:

  • Combine conditions: (A && B) || (C && !D)
  • Nested expressions: Equals('mcp.method', 'tools/call') && (Contains('jwt.groups', 'admin') || Lte('mcp.params.arguments.amount', '1000'))

See Policy Expression Language for the full reference.