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
- Start the services:
docker compose up -d
- 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"
- 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
- 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"}}}'
- 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:
| Item | Value |
|---|---|
| Realm | mcp |
| Client (public) | mcp-client |
| Client (resource) | mcp-server |
| Test User | demo / demo123 |
| Scopes | mcp: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 configurationDockerfile- PHP-FPM container with dependenciesnginx/default.conf- Nginx configuration for MCP endpointkeycloak/mcp-realm.json- Pre-configured Keycloak realmserver.php- MCP server with OAuth middlewareMcpElements.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
- Ensure Keycloak is fully started (check health endpoint)
- Verify the token hasn't expired (default: 5 minutes)
- Check that the audience claim matches
mcp-server
Connection refused
- Wait for Keycloak health check to pass
- 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