Configuring Keycloak
The MCP Gateway can use Keycloak as the identity provider behind its downstream
OAuth flow. The mcp-keycloak-oauth-inbound policy is a Keycloak-friendly
wrapper around the generic mcp-oauth-inbound policy: provide your Keycloak
base URL, a realm name, a client ID, and a client secret, and the policy derives
the realm-issuer URL, JWKS URL, and authorize and token URLs from Keycloak's
standard OpenID Connect endpoint layout.
This guide walks through the Keycloak admin console setup, then wires the policy into a gateway project. Read the authentication overview first for the two-layer OAuth model.
Set up Keycloak
The MCP Gateway acts as an OAuth 2.1 authorization server in front of Keycloak. Keycloak handles browser login; the gateway issues its own access tokens that bind to MCP routes.
Create a client in the realm
- In the Keycloak admin console, switch to the realm you want the gateway to use.
- Open Clients and click Create client.
- Give the client a Client ID (for example,
zuplo-mcp-gateway) and click Next. - Enable Client authentication (so the client requires a secret) and leave Standard flow (authorization code) enabled. Disable Service accounts roles and Direct access grants — the gateway only needs the browser code flow.
- Click Next.
- Set Valid redirect URIs to
https://<gateway-host>/oauth/callback. Addhttp://localhost:9000/oauth/callbackfor local development. - Set Web origins to
https://<gateway-host>(andhttp://localhost:9000for local dev). - Click Save.
Note the client credentials
Open the client's Credentials tab. Copy the Client secret. The Client ID is the value you set above.
Wire the policy into the gateway
Add the policy to config/policies.json:
Code
keycloakBaseUrl is the Keycloak server root, without /realms/{realm} — set
the realm separately on the realm option. If your deployment uses a path
prefix (legacy /auth), include that in keycloakBaseUrl
(https://sso.example.com/auth).
Attach the policy to each MCP route in config/routes.oas.json and register the
gateway plugin in modules/zuplo.runtime.ts (see
Configuring Auth0
for the route and plugin patterns — they're identical across all wrappers).
What the wrapper derives
Given keycloakBaseUrl: "https://sso.example.com" and
realm: "customer-portal":
| Generic field | Derived value |
|---|---|
oidc.issuer | https://sso.example.com/realms/customer-portal |
oidc.jwksUrl | https://sso.example.com/realms/customer-portal/protocol/openid-connect/certs |
browserLogin.url | https://sso.example.com/realms/customer-portal/protocol/openid-connect/auth |
browserLogin.tokenUrl | https://sso.example.com/realms/customer-portal/protocol/openid-connect/token |
Test the configuration
The fastest sanity check is to connect an MCP client:
- Open Claude Desktop, Cursor, Claude Code, or another OAuth-aware MCP client.
- Add a remote MCP server pointing at one of your
/mcp/{slug}routes. - The client should redirect you to the Keycloak sign-in page. After login, the gateway's consent screen renders. Approve it.
- The client receives an access token and can call
tools/list.
If something fails partway through, walk the flow manually using the
manual OAuth testing guide — it exercises every
endpoint with curl so you can see the raw responses.
Common issues
keycloakBaseUrlrejected at boot. The value includes/realms/.... Strip the realm path; pass the realm name on therealmoption instead.Invalid redirect_urifrom Keycloak. The callback URL on the client doesn't matchhttps://<gateway-host>/oauth/callback.Invalid client credentials. The client isn't a confidential client (Client authentication off), or the secret value doesn't match. Re-copy the secret from the Credentials tab.