Blog
Stop proxying MCP auth through Hydra/Keycloak — here’s why it breaks
The MCP authorization spec requires Dynamic Client Registration (DCR, RFC 7591) — a client should be able to show up at an authorization server it has never seen before, call one endpoint, and get back working OAuth credentials, with no human filling out a form first. Almost no general-purpose OAuth provider supports that. Faced with the gap, a lot of teams reach for infrastructure they already trust — Ory Hydra, Keycloak, PingFederate — and build a translation layer in front of it to fake the part that’s missing. It works, in the demo. It’s also a pattern worth being honest about before it ends up load-bearing in production.
The pattern: a shim in front of a provider that doesn’t speak DCR
The setup shows up in roughly the same shape everywhere. A team already has (or stands up) Ory Hydra, Keycloak, or PingOne as their OAuth provider. None of them expose a spec-compliant POST /register endpoint that a first-time MCP client can call on its own. So the team writes a small service — sometimes a single route, sometimes a whole extra deployment — that sits in front of the real authorization server and:
- Accepts a client’s RFC 7591 registration request (redirect URIs, client name, token endpoint auth method) at a URL the MCP client is told to call.
- Translates that into whatever admin API call actually creates a client in Hydra or Keycloak — usually a different shape, often requiring credentials the MCP client was never meant to see.
- Hand-builds an RFC 7591-shaped JSON response (
client_id, optionallyclient_secret) to send back, since the underlying provider isn’t producing that shape itself. - Sometimes also patches the RFC 8414 discovery document, because the provider’s real
/.well-known/oauth-authorization-serverdoesn’t advertise a registration endpoint at all, and one has to be added by hand for MCP clients to find it.
None of this is unreasonable as a first move — Hydra and Keycloak are mature, capable OAuth servers, and reusing one is a rational instinct when a spec suddenly requires a feature yours doesn’t have. The issue isn’t that the idea is bad. It’s that a hand-written translation shim is now a permanent part of the auth path for every MCP client that connects, and it has to stay correct forever, not just on the day it was written.
Where it actually breaks
A shim that passes a quick test against one MCP client can still fail in ways that only show up later, or only for some clients:
- It drifts from the spec over time. RFC 7591 and the MCP authorization spec it’s wired into aren’t frozen. A shim is a snapshot of one team’s reading of the spec on the day it was written; nobody is responsible for updating it when a client library starts sending a field the shim doesn’t expect, or when the MCP spec clarifies a requirement the shim quietly got wrong.
- It’s another service to run and patch. The shim is real infrastructure — it needs deploys, monitoring, security patches, and on-call attention like anything else in the path between a client and your MCP server, on top of whatever operational burden Hydra or Keycloak already carry.
- Partial compliance fails silently for some clients, not others. This is the sharpest edge. A shim that handles the one MCP client you tested against — say, one that always sends
token_endpoint_auth_method: "none"— can look completely done. Then a different client sends a confidential-client registration, or relies on a field the shim never accounted for, and registration fails for that client only. Nothing about the first client’s success tells you the shim is spec-complete; it only tells you it handles the requests you’ve seen so far.
What native DCR support removes
The alternative isn’t a better shim — it’s not needing one. An authorization server that implements RFC 7591 and RFC 8414 natively means POST /api/oauth/register is a real endpoint the provider maintains, not a translation you wrote and now own. There’s no separate service in the request path whose only job is reshaping requests and responses, and no second copy of “what does the spec actually say” to keep in sync with the first. Spec compliance is the OAuth provider’s job, maintained by whoever ships the provider — which is exactly what mcpauth does: DCR and discovery are built in, not bolted on. The full endpoint contract — request/response shapes for registration, authorization, token issuance, revocation, and introspection — is documented at /docs/dynamic-client-registration.
In practice, replacing a shim with native support looks like pointing your MCP server’s auth middleware at an issuer that already speaks RFC 7591, instead of at your own translation layer:
import { mcpAuth } from "getmcpauth";
app.use(
"/mcp",
mcpAuth({ registrationSecret: process.env.MCPAUTH_SECRET })
);Unauthenticated or invalid requests get a spec-correct 401 before they reach your MCP server’s handlers, and the DCR endpoint your clients call is the same one the provider tests and maintains — not a shim you have to keep matching against a moving spec.
When this isn’t worth fixing yet
None of this means every Hydra or Keycloak deployment in front of an MCP server is a problem. If your organization already runs Hydra or Keycloak for reasons that have nothing to do with MCP — it’s your standard OAuth provider for other apps, it’s operated by a team that knows it well, and the DCR shim in front of it has been stable and hasn’t caused an incident — there’s no urgency to rip it out. The failure modes above are about long-term drift and edge-case client compatibility, not guaranteed, immediate breakage. It’s worth revisiting once the shim starts needing real maintenance, once you add MCP clients you can’t test against ahead of time, or once the person who wrote it leaves and nobody is left who understands its edge cases — not necessarily today.