When MCP grows up: giving it authentication with Keycloak

Contents

When a child is small, it is enough to make sure they do not hurt themselves. You put a gate on the stairs, you cover the sockets and little else. But the child grows. They start going out alone, handling money, making decisions that have consequences. And then raising them stops being “don’t let them fall” and turns into something far more demanding: teaching them to identify themselves, to ask permission, to know how far they can go and to account for what they do.

That is exactly what has happened with our MCP servers.

MCP is no longer a toy

Not long ago, a Model Context Protocol server was an experiment running on someone’s laptop so a model could read a couple of files or call an internal API. It did not matter who used it: it was us, locally, with nothing at stake.

That is over. MCP servers have become infrastructure pieces that AI uses to touch real systems. And, like any child who grows up, they have developed new needs they did not have before:

  • Multitenancy: there is no longer a single user but many, and one user’s data cannot be visible to another.
  • Cost control: every model call consumes tokens, compute and, sometimes, paid APIs. You have to measure, attribute and set limits.
  • Authentication and authorisation: knowing who is on the other side, what permissions they hold and which specific server their credential is good for.

These are three separate fronts. Today we focus on the third, which is the one that opens the door to the other two: authentication. Without knowing who is calling, there is no tenant to separate and no cost to attribute.

Why MCP authentication is a problem with a name of its own

The good news is that nothing has to be invented here. The MCP specification did not pull a security mechanism of its own out of a hat: it builds on OAuth, the same standard that has been behind half the internet’s “Sign in with…” for years. MCP defines how an MCP server acts as a protected resource and delegates credential issuance to an authorisation server.

And that authorisation server can be Keycloak, the open source identity manager from the Cloud Native Computing Foundation that you probably already run for the rest of your applications. The idea is not to add one more identity silo just because we are now talking to models: reuse the one that already governs your users.

The important nuance is that MCP is not a single frozen standard but a living specification with several versions, and each one asks for different things. As of today four coexist:

  • 2025-11-25 (the latest)
  • 2025-06-18
  • 2025-03-26
  • 2024-11-05 (the initial one, which did not even contemplate authorisation)

Just as with raising a child what they need at 6 is not what they need at 16, what Keycloak has to satisfy depends on the MCP version you work with. Let us look at it.

What MCP demands from an authorisation server (and what Keycloak provides)

The specification lists a series of OAuth standards the authorisation server must support, with different levels of requirement (MUST, SHOULD, MAY). This is the picture, cross-referenced with what Keycloak supports:

Standard2025-11-252025-06-182025-03-26Keycloak
OAuth 2.1 Authorization Framework (draft)MUSTMUSTMUSTSupported
OAuth 2.0 Authorization Server Metadata (RFC 8414)MUSTMUSTMUSTSupported
Resource Indicators for OAuth 2.0 (RFC 8707)MUSTMUSTNot supported
Dynamic Client Registration (RFC 7591)MAYSHOULDSHOULDSupported
OAuth Client ID Metadata Document (draft)SHOULDSupported (experimental)

One clarification: MCP also adopts OAuth 2.0 Protected Resource Metadata (RFC 9728), but that standard is the MCP server’s business, not the authorisation server’s, so it does not appear in the Keycloak table.

If we take as our conformance criterion that Keycloak “supports” a version when it meets all its MUST and SHOULD requirements, the result is this:

MCP versionConformance
2025-03-26Supported
2025-06-18Partial, without Resource Indicators (RFC 8707)
2025-11-25Partial, without Resource Indicators (RFC 8707)

The only piece Keycloak lacks today is Resource Indicators for OAuth 2.0 (RFC 8707), the resource parameter that ties a token to a specific server. The Keycloak community already has support for it on the roadmap; in the meantime there is a perfectly valid workaround we look at below. Following the analogy, it is that subject the child still struggles with but can pass with a study trick until they fully mature.

Down to work: setting up Keycloak for your MCP version

For MCP 2025-03-26

Nothing special has to be configured. This version does not require the resource parameter, so Keycloak complies out of the box. The child is still small and the gate on the stairs is enough.

For MCP 2025-06-18 and 2025-11-25: tying the token to its audience

Here is where it gets interesting. For security, these versions require an access token to be bound to the audience it was issued for. In plain terms:

  • The MCP client must include the resource parameter (from RFC 8707) in the authorisation and token requests, with the value that identifies the MCP server it is going to use.
  • The MCP server must validate that the tokens it receives were issued specifically for it.

This prevents a stolen or reused token from getting into a server other than the intended one. The problem, as we said, is that Keycloak does not yet understand the resource parameter.

The solution until native RFC 8707 support arrives is to use OAuth’s scope parameter to achieve the same effect. Picture this situation:

  • The MCP server is at https://example.com/mcp.
  • It supports three scopes: mcp:tools, mcp:prompts and mcp:resources.
  • The client asks for a token with resource = https://example.com/mcp and a combination of those scopes.
  • We want Keycloak to issue a token whose aud (audience) is precisely https://example.com/mcp.

To achieve it, we configure Keycloak like this, repeating the pattern for each scope:

  • Create a client scope mcp:tools of type Optional and add an Audience mapper whose Included Custom Audience is https://example.com/mcp.
  • Create a client scope mcp:prompts of type Optional with its Audience mapper pointing at https://example.com/mcp.
  • Create a client scope mcp:resources of type Optional with its Audience mapper pointing at https://example.com/mcp.

The key is that the Included Custom Audience of each client scope is identical to the value of the request’s resource parameter and to the MCP server URL. With this, if the client asks for a token with those three scopes, Keycloak issues something like:

{
  "aud": "https://example.com/mcp",
  "scope": "mcp:resources mcp:tools mcp:prompts"
}

The MCP server can now check the aud and reject any token that was not meant for it. We have tied the credential to its recipient without waiting for RFC 8707.

If you use MCP Inspector

MCP Inspector is the official tool for debugging MCP servers, and it registers clients dynamically against Keycloak by running JavaScript from its backend. For it to work you have to adjust CORS in the client registration anonymous access policies:

  • Allowed Client Scopes: include the scopes your MCP server supports.
  • Allowed Registration Web Origins: include the web origin of the MCP Inspector backend.
  • Trusted Hosts: include the host or IP of the machine sending the dynamic registration request, that is, where your browser runs.

For MCP 2025-11-25: client registration with Client ID Metadata Document

The latest version adds a new chapter: how clients register. It contemplates three approaches, and you choose according to your scenario:

  • Client ID Metadata Documents: when client and server do not know each other beforehand (the most common case).
  • Pre-registration: when a prior relationship already exists.
  • Dynamic Client Registration: for backward compatibility or specific requirements.

Keycloak supports the first, OAuth Client ID Metadata Document (CIMD), although it is worth knowing that it is an experimental feature: it may bring breaking changes in future versions. To enable it, start Keycloak with the flag:

bin/kc.sh start --features=cimd

The idea behind CIMD is that the client_id stops being an opaque identifier and becomes a URL pointing at a document with the client’s metadata. Keycloak downloads that document and processes the request with what it finds there. For it to do so, you have to create a client policy profile and a policy that trigger it.

Profile. In Realm Settings → Client Policies → Profiles, create a profile (e.g. cimd-profile), add the client-id-metadata-document executor and configure it:

  • Allow http scheme: allows http for the URLs (client_id, client_uri, logo_uri, jwks_uri, etc.). Development only; in production OFF.
  • Trusted domains: wildcard patterns of accepted domains (e.g. *.example.org). If empty, all are denied.
  • Restrict same domain: if ON, it requires the client_id URL, the redirect_uri and the metadata URLs to all be under the same trusted domain.
  • Required properties: properties the metadata document must include.
  • Only Allow Confidential Client: if ON, only confidential clients are accepted (with jwks/jwks_uri and private_key_jwt or tls_client_auth authentication).

Policy. In Realm Settings → Client Policies → Policies, create a policy (e.g. cimd-policy), add the client-id-uri condition and configure it:

  • URI scheme: schemes to recognise in the client_id (in production, https only).
  • Trusted domains: domains accepted in the client_id host. If filled in, the condition is only true when the host matches; if left empty, it is always false.

Then associate the cimd-profile with this policy. From then on, when a request arrives with a client_id that is an https URL from a trusted domain, Keycloak downloads the metadata document and uses it to process the request.

Global executor settings. Some parameters are not in the console and are passed as SPI options at startup:

  • min-cache-time: minimum document cache time (300 s by default).
  • max-cache-time: maximum cache time (259200 s by default, 3 days).
  • upper-limit-metadata-bytes: maximum document size (5000 bytes by default).
bin/kc.sh start \
  --spi-client-policy-executor--client-id-metadata-document--min-cache-time=600 \
  --spi-client-policy-executor--client-id-metadata-document--max-cache-time=86400 \
  --spi-client-policy-executor--client-id-metadata-document--upper-limit-metadata-bytes=10000

A real case: VS Code desktop as an MCP client

Visual Studio Code is an MCP client that uses CIMD. When it connects to an MCP server that requires authorisation, it sends a client_id that is an https URL hosted on vscode.dev (e.g. https://vscode.dev/mcp-client), and Keycloak downloads its metadata from there. The detail to bear in mind is that VS Code is a public client using PKCE (no client secret) and, for the OAuth callback, it starts a local server with a redirect_uri of the form http://127.0.0.1:<port>/callback. Since that redirect is not in the vscode.dev domain, you have to leave Restrict same domain OFF.

Start with --features=cimd and configure:

Profile vscode-cimd-profile (client-id-metadata-document executor):

  • Allow http scheme: OFF
  • Trusted domains: vscode.dev, 127.0.0.1
  • Restrict same domain: OFF (the redirect is a localhost, not vscode.dev)
  • Only Allow Confidential Client: OFF (VS Code is public)

Policy vscode-cimd-policy (client-id-uri condition):

  • URI scheme: https
  • Trusted domains: vscode.dev
  • Associate the vscode-cimd-profile profile.

With this, when VS Code launches the request, Keycloak recognises the client_id as a vscode.dev URL, downloads the Client ID Metadata Document and completes the OAuth flow with the callback on localhost.

The education continues

We have taught our MCP to identify itself and to ask permission in a standard way, reusing the identity Keycloak already governs in the rest of the house. That is no small thing: with a token properly tied to its audience, everything else starts to become possible.

But, as with any child who grows up, this does not end here. The other two subjects we mentioned at the start are still pending, separating the tenants (multitenancy) and controlling what it spends (costs), and Keycloak still has some maturing to do in places, such as native Resource Indicators support. We will cover them in future posts. For now, our MCP already knows how to say who it is. And in security, that is the first day of school.

See also

  • LiteLLM’s MCP gateway — the layer that sits in front of these servers when there are several: permissions, cost, audit and the state of the specification in September 2026.
  • Keycloak in an AI platform — the piece from the inside and its position in the architecture, with the conformance status against MCP revision 2026-07-28.
  • Completing Keycloak for MCP — what this post’s setup is missing under revision 2026-07-28: protected resource metadata, audience validation and the end of dynamic registration.

Sources