Announcing CIMD support for MCP Client registration
Learn more

DCR to CIMD: The Migration Checklist for MCP Servers and Clients

An internal MCP platform at a 3,000-person company had, by its ninth month, 11,400 registered OAuth clients. It had about 400 real ones. The rest were reinstalls, CI runners, laptops that got reimaged, and a coding agent that registered itself fresh on every cold start. Nobody could say which of the 11,400 belonged to a current employee. When a security review asked "which clients can obtain tokens for this server today?", the honest answer was "all of them."

Nothing was misconfigured. Dynamic Client Registration did exactly what RFC 7591 says it does: accept metadata, mint an ID, store a record. The problem is what it cannot do. It cannot tell you who the client is.

Client ID Metadata Documents fix that one property, and the fix is why the 2026-07-28 revision of the Model Context Protocol (MCP) specification deprecated DCR. This post is the migration itself: what changes on each side, what to do in what order, and what still breaks after you flip the switch. For the conceptual comparison and the decision table, read the CIMD vs DCR guide first; this post assumes that choice is made.

TL;DR

  • DCR gives an authorization server a client record it cannot verify. CIMD gives it a URL it can fetch, so identity is tied to a domain the client controls. That is the entire security upgrade; everything else is plumbing.
  • The spec's client precedence order is fixed: pre-registered credentials, then CIMD when the server advertises client_id_metadata_document_supported, then DCR when it advertises a registration_endpoint, then ask the user. Implement it once, in that order, and never reverse it.
  • Server migration is five steps: advertise support, validate the document (self-reference, redirect_uris, HTTPS, path present), guard the fetch against SSRF, cache within the 24-hour ceiling, and keep DCR behind a flag with rate limiting and expiry until the removal date.
  • Client migration is four steps: host one document at a stable URL, send it as client_id, detect server support from metadata rather than guessing, and fall back to DCR with application_type set.
  • The earliest DCR can leave the spec is the first revision on or after 2027-07-28. Plan the fallback's removal now; do not wait to be surprised by it.
  • An agent that calls tools through a Virtual MCP server skips this problem for that hop: the server URL is static and the per-user session token minted before the run carries identity, so there is no registration step at runtime. The checklist still applies to every other server the agent talks to.

What DCR Was Solving, and Where It Stops

MCP clients and servers usually have no prior relationship. A user pastes a server URL into Claude, Cursor or an internal agent runtime, and the client needs a client_id at an authorization server it has never met. DCR was the only standard mechanism that let that happen without a human in the loop, and MCP adopted it for exactly that reason.

The ceiling has three edges, and all three are structural rather than implementation bugs.

Metadata is self-asserted. RFC 7591 requires the authorization server to treat everything in a registration request as unverified unless it arrives in a signed software statement. A client can register as "Visual Studio Code" with VS Code's logo. The consent screen shows the user that name. There is no step at which anyone proves the name is true.

Every registration is a record. Clients with no durable storage register again on each start. Anthropic's connector documentation states that Claude registers a new client on every fresh DCR connection. Across an organization, that produces the 11,400-client table above. Expiring unused records is allowed by RFC 7591, but it is a cleanup policy for a problem the mechanism creates.

The endpoint is open. To serve clients it has never seen, the registration endpoint must accept requests without an initial access token. That is an unauthenticated write endpoint on your authorization server, and the only defense the RFC offers is rate limiting.

DCR is not broken. It is a registration mechanism being used as an identity mechanism, and it was never that.

What CIMD Changes on the Wire

With CIMD, the client publishes one JSON document at an HTTPS URL it controls, and that URL is the client_id. There is no registration call. When an authorization request arrives with a URL-shaped client_id, the authorization server fetches the document and validates it.

{ "client_id": "https://agents.example.com/oauth/client-metadata.json", "client_name": "Example Agent Runtime", "client_uri": "https://agents.example.com", "logo_uri": "https://agents.example.com/static/logo.png", "application_type": "native", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none", "redirect_uris": [ "http://127.0.0.1:33418/callback", "https://agents.example.com/oauth/callback" ] }

Three validations carry the security property:

  1. The document's client_id must equal the URL it was fetched from, exactly. A document that names a different client_id is rejected.
  2. The redirect_uri in the authorization request must appear in the document's redirect_uris.
  3. The URL must be HTTPS and must contain a path; a bare origin is not a valid client_id.

Why that stops impersonation: a phishing client can send client_id=https://vscode.dev/oauth/client-metadata.json in its request, but the authorization server fetches the real document from vscode.dev and only redirects the authorization code to a URI that document lists. The phisher never controls a listed redirect_uri, so the code never reaches it. Identity is bound to control of the domain, and the consent screen can show the hostname rather than a self-chosen name.

A thousand installs of the same client are one client. The registration table goes away.

Where the Spec Stands

The sequence across revisions:

Revision
DCR
CIMD
2025-06-18
SHOULD support
Not in spec
2025-11-25
MAY support
SHOULD support (SEP-991), recommended default
2026-07-28
Deprecated; MAY retain for compatibility
SHOULD support; the standard registration path

The 2026-07-28 revision also brought in a feature lifecycle policy (SEP-2596): a deprecated feature stays in the spec for at least twelve months, and the deprecated-features registry lists DCR's earliest removal as the first revision released on or after 2027-07-28.

Two smaller 2026-07-28 changes matter for migration. Clients still using DCR must send application_type (native for desktop, CLI and localhost apps; web for remote browser apps), because OpenID Connect servers treat a missing value as web and reject loopback redirect URIs. And pre-registered or DCR credentials must be keyed to the issuer that created them, with re-registration when the issuer changes. CIMD identifiers are exempt from that rule because one URL works against every authorization server.

The client precedence order the spec defines, which every client checklist below follows:

  1. Pre-registered client information for this server, if the client has it.
  2. CIMD, if the authorization server's metadata contains client_id_metadata_document_supported: true.
  3. DCR, if the metadata contains a registration_endpoint.
  4. Prompt the user for client details.

Server Checklist

This is the authorization server side. If a vendor runs the authorization server for you, each item is a question to ask them rather than code to write.

1. Advertise support

Add client_id_metadata_document_supported: true to the authorization server metadata at /.well-known/oauth-authorization-server. Keep none in token_endpoint_auth_methods_supported; CIMD clients are public clients and some, Claude among them, use CIMD only when both signals are present and otherwise fall back to DCR.

curl -s https://auth.example.com/.well-known/oauth-authorization-server \ | jq '{client_id_metadata_document_supported, registration_endpoint, token_endpoint_auth_methods_supported}'

If the first field is missing or false, no compliant client will attempt CIMD against you, no matter what you implemented.

2. Detect and fetch

Treat any client_id that parses as an HTTPS URL with a non-empty path as a CIMD identifier. Fetch it with GET, expect 200 and application/json, do not follow redirects, and abort the authorization request on any failure. The IETF draft recommends reading no more than 5 kilobytes.

3. Validate

Reject the document unless all of the following hold:

  • client_id in the body equals the fetched URL byte-for-byte.
  • client_name and redirect_uris are present; the MCP spec requires at least these three fields.
  • The request's redirect_uri is listed in redirect_uris.
  • token_endpoint_auth_method is none, or private_key_jwt with keys supplied via jwks or jwks_uri. Shared-secret methods are forbidden by the draft; a document that lists client_secret_basic is invalid.

4. Guard the fetch

The authorization server is now making outbound HTTP requests to URLs chosen by untrusted clients. That is a server-side request forgery surface:

  • Refuse special-use and private address ranges (loopback, link-local, RFC 1918, metadata endpoints) at DNS resolution time, not just at URL parse time, so a hostname that resolves to 169.254.169.254 is caught.
  • Enforce a short timeout and a response size cap.
  • Apply a domain allowlist or denylist policy where the deployment calls for one; the draft expects regulated environments to pre-register approved CIMD URLs rather than fetch arbitrary ones.

Steps 2 through 4 together, as the function the authorization endpoint calls before it renders consent:

import ipaddress import json import socket from urllib.parse import urlparse import httpx MAX_BYTES = 5 * 1024 TIMEOUT_S = 3.0 ALLOWED_AUTH_METHODS = {"none", "private_key_jwt"} class InvalidClientError(Exception): """Maps to OAuth error=invalid_client on the authorization response.""" def _assert_public_host(hostname: str) -> None: for family, _, _, _, sockaddr in socket.getaddrinfo(hostname, 443): ip = ipaddress.ip_address(sockaddr[0]) if (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved or ip.is_unspecified): raise InvalidClientError(f"client_id host resolves to a non-public address: {ip}") def fetch_and_validate_cimd(client_id: str, redirect_uri: str) -> dict: parsed = urlparse(client_id) if parsed.scheme != "https" or not parsed.hostname or parsed.path in ("", "/"): raise InvalidClientError("client_id must be an https URL with a path") _assert_public_host(parsed.hostname) with httpx.Client(timeout=TIMEOUT_S, follow_redirects=False) as http: resp = http.get(client_id, headers={"Accept": "application/json"}) if resp.status_code != 200: raise InvalidClientError(f"metadata fetch returned {resp.status_code}") if "application/json" not in resp.headers.get("content-type", ""): raise InvalidClientError("metadata is not application/json") if len(resp.content) > MAX_BYTES: raise InvalidClientError("metadata exceeds size limit") doc = json.loads(resp.content) if doc.get("client_id") != client_id: raise InvalidClientError("document client_id does not match its URL") if not doc.get("client_name") or not isinstance(doc.get("redirect_uris"), list): raise InvalidClientError("client_name and redirect_uris are required") if redirect_uri not in doc["redirect_uris"]: raise InvalidClientError("redirect_uri is not registered in the document") if doc.get("token_endpoint_auth_method", "none") not in ALLOWED_AUTH_METHODS: raise InvalidClientError("shared-secret client authentication is not permitted") return doc

An InvalidClientError surfaces to the client as error=invalid_client. Do not include the fetched body in the error description; it is attacker-controlled text.

5. Cache correctly

Respect HTTP cache headers on the document, with a hard ceiling of 24 hours regardless of what the headers say. Never cache a failure or an invalid document; the next request should re-fetch. Cache keys are the full URL.

6. Keep DCR behind a flag, then schedule its removal

Leave DCR on while clients you do not control still need it. Rate-limit the registration endpoint, expire unused registrations, and log every registration with the application_type the client sent. Put a date on the calendar before the first spec revision on or after 2027-07-28 to review whether any client in your logs still depends on it.

7. Show the hostname on consent

Render the client_id's hostname on the consent screen, next to or instead of client_name. Warn when a document lists only loopback redirect_uris, since CIMD cannot prevent another process on the user's machine from listening on that port.

Client Checklist

This is the side you own when you build an agent runtime, an IDE integration, a CLI, or a platform where teams' agents connect to many MCP servers.

1. Host one document

Publish the document at a stable HTTPS URL under a domain you control. Use a path, not a bare origin. Do not use a URL shortener, a CDN path you might rotate, or a preview deployment. Changing the URL creates a new client identity at every authorization server you have ever connected to, and existing consents will not carry over.

Serve it with the headers an authorization server's validator expects. Authorization servers cap caching at 24 hours regardless of what you send, so a shorter max-age is the only value that matters when you need to add a redirect_uri quickly.

HTTP/1.1 200 OK Content-Type: application/json Cache-Control: public, max-age=3600

No redirects, no Vary on cookies, no authentication on the path. The document is public by design; it contains nothing secret.

2. Send the URL as client_id

In the authorization request, client_id is the document URL. Nothing else changes in the Authorization Code flow with PKCE: code_challenge, code_verifier, resource, and redirect_uri are unchanged. The redirect_uri you send must match an entry in the document.

3. Detect, don't assume

Before choosing a mechanism, read the server's metadata. Implement the precedence order exactly:

def choose_registration(as_metadata: dict, preregistered: dict | None) -> str: if preregistered: return "preregistered" if as_metadata.get("client_id_metadata_document_supported") is True \ and "none" in as_metadata.get("token_endpoint_auth_methods_supported", []): return "cimd" if as_metadata.get("registration_endpoint"): return "dcr" return "prompt_user"

Cache the decision per issuer. The same client can be CIMD at one server and DCR at another, and that is correct behavior during the deprecation window.

4. Fall back properly

When falling back to DCR, send application_type and key the resulting credentials to the issuer. Store them so a restart does not re-register. A client that registers on every start is the client that creates the 11,400-row table.

5. Handle the upstream gap

Many first-party authorization servers for the services your agent ultimately needs do not accept CIMD. As of this writing GitHub, Google, Slack and Atlassian still expect pre-registered apps. If your agent runtime is a client to those servers directly, CIMD does nothing for that hop; you are back to pre-registration per provider.

6. Prove it works before you ship it

Three checks, in order, against the authorization server you are migrating:

# 1. The server advertises CIMD and accepts public clients curl -s https://auth.example.com/.well-known/oauth-authorization-server \ | jq '.client_id_metadata_document_supported, .token_endpoint_auth_methods_supported' # expect: true and a list containing "none" # 2. Your document is reachable the way a validator will fetch it curl -sI https://agents.example.com/oauth/client-metadata.json \ | grep -iE '^(HTTP|content-type|cache-control)' # expect: 200, application/json, and a cache-control line # 3. An authorization request with the URL as client_id reaches consent open "https://auth.example.com/authorize?response_type=code\ &client_id=https%3A%2F%2Fagents.example.com%2Foauth%2Fclient-metadata.json\ &redirect_uri=http%3A%2F%2F127.0.0.1%3A33418%2Fcallback\ &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\ &code_challenge_method=S256\ &resource=https%3A%2F%2Fmcp.example.com%2Fmcp\ &state=test"

The third step should land on a consent screen that shows agents.example.com as the requesting host. Then run the negative case: change redirect_uri to one not in the document and confirm the server returns error=invalid_client rather than rendering consent. A server that renders consent for an unlisted redirect_uri has implemented the fetch but not the validation, and is no safer than it was under DCR.

That last item is where the client checklist turns into an architecture question.

The Hop Where Registration Disappears

An agent platform is a client many times over. It connects to internal MCP servers, to third-party hosted MCP servers, and, through those or directly, to the SaaS APIs its users actually work in. The precedence ladder above has to run for each distinct authorization server, and for the first-party SaaS providers it bottoms out at pre-registration no matter what.

There is one hop where none of this applies, and it is worth being precise about why.

When an agent reaches connector tools through a Virtual MCP server, the server has a static mcp_server_url created once per agent role, and the agent authenticates to it with a session token minted on the server side before each run. Identity rides in the token, not in a client registration. The pre-registration with GitHub, Google or Slack that the previous section requires is done once at the connection level, in the dashboard, and the per-user OAuth grant lives on the connected account. No client_id is negotiated at runtime for that hop.

from datetime import timedelta # Once, per agent role server = actions.mcp.create_config( name="repo-assistant", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="github-connect", # must match the connection name in the dashboard tools=["github_user_repos_list", "github_issues_list"], ), ], ) # Before every run, per user token = actions.mcp.create_session_token( mcp_config_id=server.config.id, identifier="user_123", expiry=timedelta(hours=1), ).token mcp_server = { "url": server.config.mcp_server_url, "headers": {"Authorization": f"Bearer {token}"}, }

This is not a replacement for CIMD. It is a different trust boundary. CIMD answers "which client is asking my authorization server for a token?" The session token answers "which user is this run acting as, against which connections?" The first question is about clients you have never met. The second is about users your platform already knows. When the client and the server are both yours, the registration problem was always artificial, and the token model removes it.

Every other hop still needs the checklist. The internal MCP server your platform team wrote for the data warehouse needs the server checklist. The agent runtime connecting to a partner's hosted MCP server needs the client checklist. The Virtual MCP overview covers the session-token hop; this post covers the rest.

Recommended Reading: How MCP clients register using CIMD: a step-by-step example

What Still Hurts After Migration

The draft is still a draft. CIMD is an IETF Internet-Draft, currently at draft-02 of the working-group version, and it has already changed name once since SEP-991 cited it. Field semantics can still move. Pin your implementation to a specific draft revision in code comments and review it when the draft advances.

Confidential clients are the minority path. CIMD's private_key_jwt option makes a confidential client possible without a shared secret, but most MCP clients run on user machines and will remain public clients. If you need a confidential client for a server-to-server agent, pre-register it with the client credentials grant; CIMD is the wrong tool for a client with no user.

Dual-era support is mandatory for a while. Cursor's documentation describes DCR or a static ID and secret, not CIMD. Clients that connect to your server will span both mechanisms until at least mid-2027. A server that turns DCR off early will reject real users; a server that never turns it off will keep the open registration endpoint forever.

Document availability is now part of sign-in. If the metadata URL returns a 500 during a deploy, every authorization request from that client fails until it recovers. Host the document on infrastructure with the same uptime expectations as your login page, because it is part of your login page.

The Decision Behind the Checklist

Registration is the first identity decision an MCP server makes about a client. Under DCR, that decision is made on an unverifiable claim and then stored forever. Under CIMD, it is made on a fetch the server can repeat and verify, bound to a domain someone had to control.

The migration is not large. The server side is a metadata flag, a validating fetch, and an SSRF guard. The client side is one hosted file and a four-step ladder. What the migration buys is the ability to answer the question the security review actually asked: which clients can obtain tokens for this server today, and who are they.

For the hop between your own agents and your own connector tools, that question was never worth asking a registration endpoint. Mint a token per run, scope it to the user and the role, and let the registration ladder handle the servers you do not control.

Start with the server checklist on the authorization server you run, then audit which of your agent runtimes are still registering on every start. Browse the connector catalog to see which third-party connections a Virtual MCP server can carry so the per-provider pre-registration happens once rather than per agent.

FAQs

Why did the MCP specification deprecate Dynamic Client Registration?

DCR accepts self-asserted metadata, creates a new record on every registration, and requires an unauthenticated write endpoint. It registers clients but cannot identify them, which is the property CIMD adds.

What makes a Client ID Metadata Document trustworthy?

The document's URL is the client id, the authorization server fetches it over HTTPS, the body must name the same id, and the redirect URI in the request must be listed in it. Identity is bound to control of the domain.

In what order should a client choose a registration mechanism?

Pre-registered credentials first, then CIMD when the server advertises support, then DCR when it exposes a registration endpoint, then prompting the user. The order is fixed by the spec and should never be reversed.

What is the biggest server-side risk when adding CIMD?

The authorization server now fetches URLs chosen by untrusted clients. Refuse private and special-use addresses at DNS resolution time, enforce a timeout and size cap, and never echo the fetched body in error messages.

When can a server turn DCR off?

The earliest the spec can remove DCR is the first revision on or after 28 July 2027. Keep it behind a flag with rate limiting and expiry until the clients in your logs stop depending on it.

Does CIMD help an agent reach GitHub, Google or Slack?

No. Those providers still expect pre-registered apps. For that hop a Virtual MCP server does the registration once at the connection level and gives each run a short-lived session token instead of negotiating a client id.

No items found.
On this page
Share this article
Scalekit
Find us on ProductHunt

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.