Back to blog
Product

Verified MCP clients: URL-based identity and admission control for MCP OAuth

Nolan Sullivan

Nolan Sullivan

September 18, 2026 · 9 min read

Verified MCP clients: URL-based identity and admission control for MCP OAuth

Today MCP clients can identify themselves to Speakeasy-hosted MCP servers with a Client ID Metadata Document (CIMD). The AI Control Plane fetches the CIMD document, confirms it names itself, and validates the redirect URI. Access is then decided based on the policies set up for the org. The control plane runs an OAuth authorization server for each MCP server, and each issuer carries a policy for which client URLs it is allowed to fetch. That provides you an additional lever of control. Each MCP server can be permissioned for its own set of clients.

The diagram below shows where that sits. Clients arrive at the Control Plane, which is the OAuth authorization server and MCP gateway for the server they want. The Control Plane admits or rejects the client URL, fetches and verifies the document, signs the user in through your identity provider, and only then lets tool calls through to the MCP server.

Diagram of the MCP OAuth flow through the Speakeasy AI Control Plane. On the left, Claude Code and Codex each present a client_id that is a URL to their client.json document and send the user to /authorize. A third, dashed tile labeled "any other client" with a URL not on the issuer's list is stopped with an invalid_client error in presets mode. In the center, the Control Plane acts as the OAuth 2.1 authorization server and MCP gateway for this server and runs four steps: admit, checking the client URL against the issuer's policy in open or presets mode; verify, fetching the document from the vendor's domain, confirming it names itself, and checking the redirect URI; authorize, signing the user in through the identity provider, naming the client on the consent screen from the document, and issuing a token; and gateway, where every tool call carries the user and client, is checked against policy, and is written to the audit log. Dashed lines show the document fetch going out to the vendor domain and the sign-in going to an identity provider such as Okta, Entra, or Google. A solid line carries tool calls from the gateway to the MCP server, which fronts SaaS APIs and accepts only tokens issued in the authorize step. A footnote reads that DCR clients still register at /register and join the same path at step 3, while CIMD clients skip registration entirely.

What are the Limitations of DCR-only auth?

MCP adopted OAuth as its authentication protocol because OAuth had support for DCR. DCR provided the mechanism that MCP needed: a way for a client to obtain a client_id without a human going into a developer portal and registering an app by hand. With DCR, the client posts a short JSON body with a display name and its redirect URIs to a registration endpoint, and the server mints a client_id in response.

DCR has three limitations that follow from that design.

  1. Everything in the registration is self-asserted. The server receives a POST with no signature and no proof that a real vendor sent it, so it records whatever the body says. A malicious app can register with client_name: "Claude", and when a user later reaches the consent screen it reads "Claude wants to access your account". The user sees a trusted name and approves. Manual registration avoided this because a human reviewed the app before it existed.

  2. Every install registers separately. Registration happens per instance, so each copy of Claude Code on each laptop does its own POST and receives its own client_id. A company with 500 engineers on three tools does not end up with three client records. It ends up with more than 1,500, and the count grows every time someone reinstalls or clears local state. Because none of those rows carry verifiable information, an administrator looking at the client table cannot tell legitimate installs from stale ones or from something suspicious.

  3. There is no whitelist. You can't limit which clients may appear at /authorize at all. That endpoint is where an OAuth flow begins, and in a traditional setup the set of clients allowed to start one is the set an administrator registered. With open DCR, anyone can register and any registered client can start a flow. There is no policy layer where an administrator can say that only Claude Code and VS Code may request access to this server. Even if you built one, you would have nothing reliable to write the rule against, since names can be typed and client IDs are random per-install values. This is the first question a security review asks, and DCR has no answer for it.

DCR was the right call when MCP adopted OAuth. It works with any client and asks nothing of the client vendor beyond an HTTP POST. Its two weaknesses, self-asserted metadata and one registration per install, follow directly from that design, and no amount of tuning on the authorization server side removes them.

How CIMD patches the holes

A Client ID Metadata Document (CIMD) replaces the registration call with a URL. The client vendor publishes a JSON document describing the client at an HTTPS address on a domain it controls, and that URL is the client_id everywhere the client goes. When the client reaches your authorization server, the server fetches the document, confirms the document names itself, and checks the redirect URI the client presented against the ones the document lists. No registration happens, no rows accumulate, and the name on the consent screen comes from the vendor's published document. The identity is a domain you can verify and write into a policy. The MCP specification adopted CIMD in November 2025, and its latest release deprecates DCR.

A CIMD client publishes its metadata once and carries the same URL as its identity to every server. When Claude Code reaches your authorization server, the client_id is a document on claude.ai, fetched over HTTPS and checked to confirm it names itself. It does not prove that the binary on the laptop is genuine, and user authentication still happens through your identity provider. What changes is that "who is this client" stops being a free-text field and becomes a domain.

The MCP specification moved the same way. SEP-991 was merged in November 2025, so MCP clients and servers should support CIMD and may support DCR. The 2026-07-28 specification release deprecates DCR and keeps it for backward compatibility. CIMD itself is still an IETF Internet-Draft, draft-ietf-oauth-client-id-metadata-document-02, so details can still shift. Auth0 and WorkOS have both written up the same trade-off from the authorization server side.

Create a client whitelist

Open CIMD admits any URL that resolves to a valid document. For many teams that is the right posture, and it is the default policy in the control plane. Some security teams need more control; that is what presets address.

In presets mode, a client URL has to match one of two lists before Speakeasy will fetch anything. The first is a catalog Speakeasy maintains of client documents published by the vendors we see most in production. Matching is by exact URL and never by origin, because claude.ai as an origin is far broader than the specific documents worth trusting. The one relaxation is a single-path-segment wildcard for vendors that mint a separate document per connector, so their clients keep working without every install being enumerated. When Speakeasy adds a vendor to the catalog, every issuer in presets mode accepts it on the next deploy with no action on your side.

The second list is custom URLs on the issuer itself, for internal clients or tools whose document URL is set per install. Before saving one, you can verify it, and the check reports why a URL fails rather than only that it failed: unreachable host, document that does not parse, or a document whose client_id does not match. Custom URLs match exactly, so a wildcard in one is an inert character.

In presets mode, admission gates the start of a flow and not the continuation of one, so a catalog change does not terminate sessions that are already authorized. Mode changes and custom URL additions are written to the audit log, so the policy has a history.

The policy is configured per issuer from the dashboard or the management API, and the Platform MCP exposes it as get_mcp_client_admission and set_mcp_client_admission, so an administrator working from an agent can read and set it too. MoonPay runs this way, with approved clients allowlisted and unsanctioned servers blocked by default.

What this release does not change

DCR remains supported on the inbound side. Clients that have not shipped CIMD register the way they always have, and several widely used clients and bridges have not shipped it yet. We have no deprecation date for DCR.

What's next

The catalog will keep growing from production signal, confidential client support is separate work, and outbound CIMD becomes useful as upstream providers ship it. CIMD is on for every organization's session authorization server today. Admission policy is per issuer, so you can tighten one server at a time. The OAuth documentation covers the mechanics, and if you are working through which MCP clients your security review will admit, we'd like to talk.

Frequently asked questions
Does Dynamic Client Registration still work?

Yes. DCR stays supported on the inbound side alongside CIMD, and pre-registered clients are unchanged. A client that has not shipped CIMD support registers the way it always has. Setting an issuer to disabled stops advertising CIMD, and clients then use DCR.

Which admission mode should we use?

An issuer that has never chosen a mode rests at open, which admits any URL that resolves to a valid client metadata document. Move to presets when your security review requires naming the clients that may appear at authorize. Under presets, a client URL must match the Speakeasy catalog or a custom URL on that issuer, and a miss is a hard denial with no fallback to DCR.

What does a user see when their client is denied?

An OAuth error from their client. Speakeasy returns invalid_client with a description saying the client URL is not admitted for this server. MCP clients do not retry through DCR after a rejected URL client ID, so an operator has to add the URL as a custom client or the catalog has to cover it.

Does this cover confidential clients using private_key_jwt?

This release covers public clients, which use token_endpoint_auth_method none. That is what shipping MCP clients use today. Confidential client authentication is separate work and is not part of this release.

Does Speakeasy present a CIMD client ID to upstream providers?

It can, for upstream authorization servers that advertise CIMD support. Speakeasy hosts the metadata document and sends its URL as the client ID. GitHub, Slack, Google, and Atlassian do not accept CIMD today, so most upstream connections continue to use DCR or a pre-registered client.

Last updated on

AI everywhere.

Control here.