Every AI agent client that ever touched your MCP server left a row in a registration table — a client_id you minted, redirect URIs you stored, lifecycle state you now own forever. On July 28, 2026, the final MCP specification called that model what it always was for infrastructure tooling: the wrong shape. Dynamic Client Registration is formally deprecated, Client ID Metadata Documents take its place, and the deprecation registry names the earliest removal date: the first spec revision released on or after July 28, 2027. You have roughly twelve months.
This post is the migration plan. The checklist lands first; the reasoning follows.
The migration checklist, up front
If you operate a self-hosted MCP server with OAuth, here is the whole job, in order:
- Build an SSRF-safe metadata fetcher — HTTPS-only, no redirects, a ~5-second timeout, a ~10 KB response cap, and hard blocks on private, loopback, and cloud-metadata IP ranges. Your authorization server is about to fetch URLs supplied by strangers.
- Validate exactly — the document's
client_idmust match the fetch URL byte-for-byte, and the authorization request'sredirect_urimust match an entry in the document'sredirect_urislist. Exact string comparison, no URL normalization cleverness. - Cache with bounds you chose — respect HTTP cache headers but cap the TTL and the entry count yourself. An unbounded cache keyed by caller-supplied URLs is a memory-exhaustion bug wearing a performance feature's clothes.
- Advertise support only after 1–3 work — set
client_id_metadata_document_supported: truein your RFC 8414 authorization-server metadata once the fetch path is real. Advertising the flag without implementing the fetch breaks CIMD clients outright. - Keep DCR running as a fallback — with an expiry plan dated no later than July 2027. Old clients still need the
registration_endpointduring the window; new clients will prefer CIMD the moment you advertise it. - Harden the client side in the same window — validate the RFC 9207
issparameter before redeeming any authorization code, and key every stored token by the issuer that minted it. If your MCP client talks to more than one authorization server, this is the item that stops a token minted by server A from being replayed at server B.
That is the complete scope: three spec changes, six work items, one deadline. Now the why.
Why DCR was always the wrong shape for agent infrastructure
OAuth 2.0 was designed for a world where the authorization server already knows its clients: a SaaS app registers once in a developer portal, gets a client_id, and the relationship is stable for years. Dynamic Client Registration (RFC 7591) extended that to clients that show up unannounced — POST your metadata to the registration endpoint, get a client_id back. Reasonable for a bounded set of apps. Catastrophic for agent infrastructure.
An MCP client — an IDE helper, a coding agent, a ChatGPT-style connector — can discover and connect to thousands of servers it has never seen before. Under DCR, every one of those first contacts is a database write on your server: a new row, a minted credential, redirect URIs to store, and lifecycle state to expire, rotate, and audit. You accumulate a permanent registry of clients you may never see again. And the registration endpoint itself is a public, unauthenticated POST that accepts writes from the internet — a purpose-built target for junk-client spam and implementation probing.
CIMD flips the model. The client identifies itself with an HTTPS URL it controls, and your server fetches a JSON metadata document from that URL when — and only when — it needs to. No write endpoint, no permanent row, no lifecycle to manage:
| Dynamic Client Registration | Client ID Metadata Documents | |
|---|---|---|
| Client identity | Server-minted client_id from a POST | HTTPS URL the client controls |
| Server state per client | Permanent registry row | Fetch-and-cache with HTTP semantics |
| Write surface | Public registration_endpoint | None — servers fetch, never accept writes |
| Lifecycle burden | Expire, rotate, audit every row | Cache TTL; nothing to garbage-collect |
| Audit trail | Built in (the registry) | Logs of fetches, if you keep them |
The honest caveat sits in that last row: some regulated environments genuinely like DCR's server-side registry as an audit trail, which is why the spec keeps DCR as a supported fallback rather than deleting it. But for a self-hosted MCP server sitting in front of a deploy pipeline or an agent sandbox fleet, the registry was never an asset — it was a table that only grew.
How CIMD actually works
The mechanism is specified in draft-ietf-oauth-client-id-metadata-document (MCP SEP-991) and the flow has exactly one new moving part: your authorization server becomes an HTTP client.
A CIMD-capable client arrives at your /authorize endpoint with a URL-shaped client_id:
GET /authorize?response_type=code
&client_id=https%3A%2F%2Fagent.example.com%2Foauth-client.json
&redirect_uri=https%3A%2F%2Fagent.example.com%2Fcallback
&code_challenge=abc123...Your server fetches that URL, validates the JSON document it finds there, and proceeds with the normal authorization-code flow. The document looks like this:
{
"client_id": "https://agent.example.com/oauth-client.json",
"client_name": "Example Deploy Agent",
"client_uri": "https://agent.example.com",
"redirect_uris": ["https://agent.example.com/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}Three validation rules are non-negotiable: the client_id in the document must match the URL you fetched byte-for-byte (this is the anti-spoofing anchor — anyone can host JSON, but only the domain owner serves it at that address), the request's redirect_uri must appear in redirect_uris, and the client_id URL must use HTTPS with a path component. Your consent screen renders client_name from the fetched document, so operators finally see "Example Deploy Agent" instead of an opaque minted string.
Clients follow a defined priority order: use pre-registered credentials if they have them, use CIMD if your server advertises client_id_metadata_document_supported, fall back to DCR if you still expose a registration_endpoint, and prompt the user for manual entry only as a last resort. The practical consequence: the day you flip the advertisement flag, every modern client stops calling your registration endpoint. Keep DCR up for the laggards, but watch its traffic decay toward zero — that decay curve is your migration progress metric.
The two companion hardenings you ship in the same window
The July 2026 revision hardened authorization on three fronts at once, and CIMD is only the most visible. The other two close holes that a self-hosted server with multiple upstreams cannot afford to leave open.
RFC 9207 iss validation (SEP-2468): clients MUST check the issuer before redeeming a code. The attack it kills is the authorization-server mix-up: a malicious server tricks your client into starting a flow against the attacker's authorization endpoint, then replays the resulting code against the real server. The defense is a single parameter — the authorization server stamps iss on its authorization response, and the client verifies it matches the issuer from discovery metadata before touching code or state. The 2026-07-28 spec promotes this from SHOULD to MUST, and major clients already enforce it strictly: miss the iss parameter and the flow dies with a generic authorization failure that tells you nothing.
If you run the authorization server, emit iss and advertise authorization_response_iss_parameter_supported. If you run the client, validate with exact string comparison — and treat a missing iss from an AS that advertises support as a hard failure, not a legacy quirk.
Issuer-bound credentials (SEP-2352): key every stored token by who minted it. Under this rule, a client must record the issuer alongside each credential, must never present a token to a different authorization server, and must re-register when the server it talks to changes issuers. The reference implementations made this concrete by re-keying their token stores from (server) to (server, issuer): when discovery returns a different issuer than the stored one, cached credentials are dropped and the user re-authenticates instead of replaying a token across a trust boundary. For a self-hosted platform this has a sharp operational edge — if you ever migrate your MCP server from one identity provider to another, every connected agent client will (correctly) demand a fresh login. Plan that cutover as a user-visible event, not a silent backend swap.
Field notes: how teams are already getting this wrong
The spec has been final for weeks and the failure modes are already catalogued in public issue trackers. Three patterns worth stealing as negative checklists:
Fetching attacker-supplied URLs with a generic HTTP client. The CIMD fetch is a security boundary, not a convenience GET. The spec's security section and every serious implementation agree on the shape: HTTPS only, no redirects, resolve DNS once and pin the dial to the validated IP (TOCTOU-safe), refuse private, loopback, link-local, and cloud instance-metadata ranges, enforce a short timeout and a small body cap, and require a JSON content type. Skip any one of these and your authorization server is an SSRF primitive that happens to also do OAuth. Validate response_type and PKCE parameters before the fetch so unauthenticated callers cannot turn your /authorize endpoint into their personal fetch proxy.
Caching without bounds. One public implementation cached every successfully fetched document keyed by the caller-supplied client_id, with the TTL taken straight from the attacker's Cache-Control header — so anyone with a domain could mint unlimited distinct CIMD URLs and pin an entry per URL for up to 24 hours. Cap the entry count, cap the TTL regardless of what the header claims, add a short negative cache for failed fetches, and put a concurrency limit on outstanding fetches. This is checklist item 3 for a reason.
Advertising the flag before the fetch works. At least one major open-source project shipped client_id_metadata_document_supported: true in its discovery metadata while its authorization handler never fetched the document — so CIMD clients arrived with URL client IDs, failed redirect validation, and died with "Invalid redirect URI." The priority order in the spec means the advertisement is a promise every modern client will take you up on immediately. Implement first, advertise second, and test the full loop with a real CIMD client (the MCP Inspector speaks the new flow) before you flip the bit.
One more server-side rule that surprises people: refuse client_credentials grants for CIMD clients outright. A client that registered itself by hosting a JSON file has no secret, and a grant type with no user and no secret must not be reachable by self-asserted identity. If a client presents a secret alongside a CIMD identity, reject it rather than ignoring it — silent acceptance trains everyone to misunderstand the trust model.
Twelve months is a window, not a grace period
The deprecation registry's language is precise: DCR becomes eligible for removal in the first revision released on or after July 28, 2027. Actual removal is a maintainer decision that could land later — but "stops being guaranteed" is the correct way to read the deadline, because client behavior is already moving. New SDKs default to CIMD, new clients prefer it on sight of the flag, and DCR traffic on a dual-mode server will only shrink.
The teams that treat this as twelve months of runway for a careful, ordered migration — fetcher, validation, bounded cache, advertisement, then DCR sunset — will spend a quiet week on it. The teams that treat it as twelve months of nothing will discover the migration the week a major client drops its DCR fallback.
Start with the fetcher. Everything else is downstream of getting that one boundary right.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. If your agents deploy through a self-hosted MCP server, that server deserves infrastructure you control end to end. Star the repo on GitHub or deploy your first app today.



