Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mcp-use.com/llms.txt

Use this file to discover all available pages before exploring further.

Keycloak exposes full OAuth 2.1 + OIDC endpoints on every realm, including native Dynamic Client Registration (RFC 7591). MCP clients discover Keycloak via .well-known metadata, register themselves, complete a PKCE authorization flow, and send the resulting access token as a bearer token on MCP requests — the MCP server only verifies the JWT against Keycloak’s JWKS. No OAuth traffic is proxied through your server.
You need a reachable Keycloak realm with DCR enabled. The provider handles everything on the MCP-server side, but Keycloak itself must permit anonymous DCR from the hosts your MCP clients will register from.

Setup

This guide assumes you already have a Keycloak instance running and admin access to it. If you don’t, see Keycloak’s getting started guide.

1. Enable Dynamic Client Registration

Keycloak exposes /{realm}/clients-registrations/openid-connect on every realm. Anonymous (no-token) registration is gated by Client Registration Policies:
  1. Realm settings → Client registration → Anonymous Access Policies
  2. Open the Trusted Hosts policy
  3. Add the hostnames that MCP clients will register from (localhost, 127.0.0.1) to Trusted Hosts
  4. Make sure Client URIs Must Match is enabled so redirect_uris in the registration request are validated
Browser-based MCP clients (like the inspector) also need the Allowed Registration Web Origins policy (Keycloak 26.6+) listing every origin the client runs from — without it, DCR requests are blocked by CORS with 403 Invalid origin.
For non-localhost redirect URIs, mint an Initial Access Token (Realm settings → Client registration → Initial access token) and have clients pass it on the DCR POST.

2. Environment variables

# Base URL of your Keycloak server — no trailing slash, no /realms path
MCP_USE_OAUTH_KEYCLOAK_SERVER_URL=http://localhost:8080

# Realm name
MCP_USE_OAUTH_KEYCLOAK_REALM=demo

# Optional. If set, the provider enforces that the access token's `aud` claim
# equals this value. Requires an Audience protocol mapper on the client scope.
# MCP_USE_OAUTH_KEYCLOAK_AUDIENCE=http://localhost:3000

Configure the MCP server

// server.ts
import { MCPServer, oauthKeycloakProvider, object } from "mcp-use/server";

const server = new MCPServer({
  name: "my-server",
  version: "1.0.0",
  // Zero-config: reads MCP_USE_OAUTH_KEYCLOAK_* env vars
  oauth: oauthKeycloakProvider(),
});

server.tool(
  {
    name: "get-user-info",
    description: "Return identity info extracted from the Keycloak access token",
  },
  async (_args, ctx) =>
    object({
      userId: ctx.auth.user.userId,
      username: ctx.auth.user.username,
      email: ctx.auth.user.email,
      roles: ctx.auth.user.roles,
      permissions: ctx.auth.permissions,
      scopes: ctx.auth.scopes,
    }),
);

await server.listen(3000);
Or pass config explicitly:
oauth: oauthKeycloakProvider({
  serverUrl: "https://keycloak.example.com",
  realm: "demo",

  // Optional: required `aud` claim — needs an Audience mapper in Keycloak
  audience: "https://my-mcp-server.example.com/mcp",

  // Disable JWT verification during development only
  verifyJwt: process.env.NODE_ENV === "production",

  // Override advertised scopes
  // Default: ["openid", "profile", "email", "offline_access", "roles"]
  scopesSupported: ["openid", "profile", "email"],
});

The flow

MCP Client ──(1) GET /.well-known/oauth-protected-resource   ─▶ MCP Server
MCP Client ──(2) GET /.well-known/oauth-authorization-server ─▶ MCP Server ─▶ Keycloak
MCP Client ──(3) POST /clients-registrations/openid-connect  ─▶ Keycloak      (DCR)
MCP Client ──(4) GET  /protocol/openid-connect/auth          ─▶ Keycloak      (PKCE)
MCP Client ──(5) POST /protocol/openid-connect/token         ─▶ Keycloak
MCP Client ──(6) MCP request + Bearer <token>                ─▶ MCP Server    (verifies JWT via JWKS)
Step 2 is a passthrough from the MCP server back to Keycloak’s metadata — it’s what tells the client where to register and where to send the user for login. Everything else goes directly to Keycloak.

Accessing user info in tools

Keycloak puts realm roles in realm_access.roles and resource roles in resource_access.{client}.roles. The provider normalizes them onto ctx.auth:
server.tool(
  {
    name: "get-user-info",
    description: "Get authenticated user info",
  },
  async (_args, ctx) =>
    object({
      userId: ctx.auth.user.userId,
      email: ctx.auth.user.email,
      username: ctx.auth.user.username,

      // Realm roles
      roles: ctx.auth.user.roles,

      // Resource roles as "client:role" strings
      permissions: ctx.auth.permissions,

      scopes: ctx.auth.scopes,
    }),
);
You can also call Keycloak’s userinfo endpoint with the raw access token:
server.tool(
  {
    name: "get-keycloak-userinfo",
    description: "Fetch the full userinfo document from Keycloak",
  },
  async (_args, ctx) => {
    const serverUrl = process.env.MCP_USE_OAUTH_KEYCLOAK_SERVER_URL!;
    const realm = process.env.MCP_USE_OAUTH_KEYCLOAK_REALM!;
    const res = await fetch(
      `${serverUrl}/realms/${realm}/protocol/openid-connect/userinfo`,
      { headers: { Authorization: `Bearer ${ctx.auth.accessToken}` } },
    );
    return object(await res.json());
  },
);

Role-based access control

server.tool(
  {
    name: "admin-action",
    description: "Admin-only action",
  },
  async (_args, ctx) => {
    if (!ctx.auth.user.roles?.includes("admin")) {
      return {
        content: [{ type: "text", text: "Forbidden: admin role required" }],
        isError: true,
      };
    }

    // ... admin logic
    return { content: [{ type: "text", text: "Done" }] };
  },
);

Production notes

  • Audience enforcement. Keycloak doesn’t set aud to the resource server by default. To require it, add an Audience protocol mapper to the client scope in Keycloak and set MCP_USE_OAUTH_KEYCLOAK_AUDIENCE to the matching value. Without the mapper, tokens will be rejected.
  • Anonymous DCR. Fine for local development, risky in production. Disable anonymous access and issue Initial Access Tokens (Realm settings → Client registration → Initial access token) that clients pass on the DCR request.
  • Transport. Always serve Keycloak and the MCP server over HTTPS outside of local dev.
  • Scope of realm roles. ctx.auth.user.roles only contains realm roles. Use ctx.auth.permissions (formatted as client:role) for per-client roles.

Resources

Next Steps