This example demonstrates MCP server authorization using Keycloak as the OAuth 2.0 / OpenID Connect provider.
- 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
- 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:
The MCP Inspector doesn't support OAuth out of the box, but you can test using curl or build a custom client.
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 |
Access at http://localhost:8180/admin with:
- Username:
admin - Password:
admin
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ MCP Client │────▶│ Nginx │────▶│ PHP-FPM │
│ │ │ (port 8000) │ │ MCP Server │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ Get Token │ Validate JWT
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Keycloak │◀───────────────────────────│ JWKS Fetch │
│ (port 8180) │ │ │
└─────────────────┘ └─────────────────┘
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
| Variable | Default | Description |
|---|---|---|
KEYCLOAK_EXTERNAL_URL |
http://localhost:8180 |
Keycloak URL as seen by clients (token issuer) |
KEYCLOAK_INTERNAL_URL |
http://keycloak:8080 |
Keycloak URL from within Docker network (for JWKS) |
KEYCLOAK_REALM |
mcp |
Keycloak realm name |
MCP_AUDIENCE |
mcp-server |
Expected JWT audience |
- 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
- Wait for Keycloak health check to pass
- Check Docker network connectivity:
docker compose logs
The MCP server needs to reach Keycloak at http://keycloak:8080 (Docker network).
For local development outside Docker, use http://localhost:8180.
docker compose down -v