How MCP Clients Sign In Without Registering First

·5 min read

mcpoauth2client-id-metadata-documentsai-agents

Classic OAuth has a setup step everyone forgets about: before a client can ask for a token, someone registers it with the authorization server. They pick a name, list the redirect URIs, and get a client_id back. For a web app you build, that's a one-time chore.

MCP breaks that model. Your MCP server will be reached by Claude, Cursor, VS Code, and clients that don't exist yet. You can't register every one of them ahead of time, and their users shouldn't have to.

The MCP authorization spec's answer is Client ID Metadata Documents: the client's client_id is an HTTPS URL, and the JSON document at that URL is its registration.

The client_id is a URL

A client publishes a small document on a domain it controls:

{
  "client_id": "https://client.example.com/oauth/metadata.json",
  "client_name": "Example MCP Client",
  "client_uri": "https://client.example.com",
  "redirect_uris": ["http://localhost/callback", "http://127.0.0.1/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

Then it uses that URL wherever OAuth expects a client_id. There's no registration request and no stored credential. When an authorization server sees a client_id that looks like an HTTPS URL, it fetches the document and treats what it finds as the client's registration.

That gives you something registration never did: the client's identity is tied to a domain. A document claiming to be Claude Code but served from someone else's domain is just a different client with a misleading name.

The whole flow, end to end

Here's what happens the first time a user adds an MCP server to their client.

1. The client hits the server and gets a challenge. The first request has no token, so the MCP server answers 401 and points at its metadata:

WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

2. The client discovers the authorization server. The protected resource metadata (RFC 9728) names the resource and who issues tokens for it:

{
  "resource": "https://mcp.example.com",
  "authorization_servers": ["https://acme.eu.authaction.com/"]
}

The authorization server's own metadata advertises "client_id_metadata_document_supported": true, which tells the client it can use its URL as the client_id.

3. The client starts an ordinary authorization code flow with PKCE, with two details that matter:

GET /oauth2/authorize?
  response_type=code&
  client_id=https://client.example.com/oauth/metadata.json&
  redirect_uri=http://localhost:53682/callback&
  resource=https://mcp.example.com&
  code_challenge=...&code_challenge_method=S256&state=...

The client_id is the document URL, and resource (RFC 8707) names the MCP server the token is for.

4. The authorization server fetches and checks the document, then signs the user in as usual.

5. The client exchanges the code with its code_verifier. It has no secret to send. The access token's audience is the MCP server, so it can't be replayed against some other API.

What the authorization server has to check

Fetching a URL because someone put it in a query string is a decision with consequences. These are the checks that make it safe.

Decide whether to fetch at all. Check the resource first: is it a server that accepts MCP clients, and does it accept this one? A request for any other API should be refused before the authorization server makes an outbound request on the caller's behalf.

Keep the URL boring. Require https, a domain name rather than an IP address, a path, and no fragment or embedded credentials. Fetch with a short timeout and a size limit. This keeps the fetch from becoming a way to probe your internal network.

The document must name itself. The client_id inside the document has to equal the URL it was fetched from, exactly. Otherwise one domain could publish a document impersonating another client.

Public clients only. The document is public, so it can't carry a secret. Require token_endpoint_auth_method to be none (or absent), reject documents containing a client_secret, and rely on PKCE.

Match redirect URIs exactly. No wildcards, ever, on a document someone else controls. The one sanctioned exception is the port on loopback addresses: native clients listen on a random local port, and RFC 8252 says to ignore it. Allow plain http only for localhost, 127.0.0.1 and [::1].

Show the domain, not just the name. client_name is self-asserted. The part a user can actually trust is the host of the client_id URL, so put it on the consent screen.

Cache, but not forever. Respect the document's Cache-Control within sane bounds and fetch it again once the cached copy expires. That way a client that adds a redirect URI is picked up without anyone doing anything, and one that removes a redirect URI loses it.

Use resources to separate powers

Because the token's audience is the resource the client asked for, a resource is a clean way to draw a line. Give one MCP server two URLs, say https://mcp.example.com for full access and https://mcp.example.com/read for read-only, and register each as its own resource. A token for one won't work at the other.

The important part is where you enforce it. If the MCP server forwards the user's token to your API, the API has to know that a read-only audience means read-only. Otherwise anyone holding that token can skip the MCP server and call the API directly. Hiding write tools in the MCP server is a convenience. The audience check in the API is the security.

Things that still belong to the server owner

Not registering clients doesn't mean you lose control over them. As the owner of an MCP server you should still be able to:

  • turn MCP client access on or off for each API,
  • allow any client, or only the ones you choose (by their client_id URL),
  • pick which login methods users see when they connect through an MCP client.

You're approving clients by URL after the fact, instead of creating them in advance.

Try it

AuthAction supports Client ID Metadata Documents for any API you protect. Turn on MCP Client Access for your MCP server's API, and Claude, Cursor and VS Code can sign users in straight away. The Client ID Metadata Documents guide covers the checks in detail, and MCP Server Authorization walks through the setup. If your MCP server also calls other APIs as the user, read Is your MCP server an agent? next.


Authorization for MCP servers

Turn on MCP Client Access for your MCP server's API and Claude, Cursor and VS Code can sign users in, with no client to create first.

Free, unlimited users. No credit card required.