OAuth Keycloak Example

March 10, 2026 · View on GitHub

This example demonstrates MCP server authorization using Keycloak as the OAuth 2.0 / OpenID Connect provider.

Features

  • JWT token validation with automatic JWKS discovery
  • Protected Resource Metadata (RFC 9728) at /.well-known/oauth-protected-resource
  • MCP tools protected by OAuth authentication
  • Pre-configured Keycloak realm with test user

Quick Start

  1. Start the services:
docker compose up -d
  1. Wait for Keycloak to be ready (may take 30-60 seconds):
docker compose logs -f keycloak
# Wait until you see "Running the server in development mode"
  1. Get an access token:
# Using Resource Owner Password Credentials (for testing only)
TOKEN=$(curl -s -X POST "http://localhost:8180/realms/mcp/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=mcp-client" \
  -d "username=demo" \
  -d "password=demo123" \
  -d "grant_type=password" \
  -d "scope=openid mcp" | jq -r '.access_token')

echo $TOKEN
  1. Test the MCP server:
# Get Protected Resource Metadata
curl http://localhost:8000/.well-known/oauth-protected-resource

# Call MCP endpoint without token (should get 401)
curl -i http://localhost:8000/mcp

# Call MCP endpoint with token
curl -X POST http://localhost:8000/mcp \
  -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"}}}'
  1. Use with MCP Inspector:

MCP Inspector can call this server if you provide a valid Bearer token manually (Authorization header). It does not run the OAuth login flow automatically.

Keycloak Configuration

The realm is pre-configured with:

ItemValue
Realmmcp
Client (public)mcp-client
Client (resource)mcp-server
Test Userdemo / demo123
Scopesmcp:read, mcp:write

Keycloak Admin Console

Access at http://localhost:8180/admin with:

  • Username: admin
  • Password: admin

Architecture

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   MCP Client    │────▶│     Nginx       │────▶│    PHP-FPM      │
│                 │     │   (port 8000)   │     │   MCP Server    │
└─────────────────┘     └─────────────────┘     └─────────────────┘
        │                                               │
        │ Get Token                                     │ Validate JWT
        ▼                                               ▼
┌─────────────────┐                            ┌─────────────────┐
│    Keycloak     │◀───────────────────────────│   JWKS Fetch    │
│   (port 8180)   │                            │                 │
└─────────────────┘                            └─────────────────┘

Files

  • docker-compose.yml - Docker Compose configuration
  • Dockerfile - PHP-FPM container with dependencies
  • nginx/default.conf - Nginx configuration for MCP endpoint
  • keycloak/mcp-realm.json - Pre-configured Keycloak realm
  • server.php - MCP server with OAuth middleware
  • McpElements.php - MCP tools and resources

Configuration

This example uses hard-coded values in server.php for consistency with other examples:

  • Keycloak external URL: http://localhost:8180
  • Keycloak internal URL: http://keycloak:8180
  • Realm: mcp
  • Audience: mcp-server

Troubleshooting

Token validation fails

  1. Ensure Keycloak is fully started (check health endpoint)
  2. Verify the token hasn't expired (default: 5 minutes)
  3. Check that the audience claim matches mcp-server

Connection refused

  1. Wait for Keycloak health check to pass
  2. Check Docker network connectivity: docker compose logs

JWKS fetch fails

The MCP server needs to reach Keycloak at http://keycloak:8180 (Docker network). For local development outside Docker, use http://localhost:8180.

Cleanup

docker compose down -v