MCP Authorization in Practice: Metadata, Client IDs and Sign-In
How MCP authorization works on the wire: the 401 challenge, protected resource metadata, Client ID Metadata Documents, PKCE, scopes and the client side.
Cet article est en anglais.
TL;DR. MCP authorization is OAuth 2.1 applied to remote MCP servers over HTTP. A protected server answers an unauthenticated request with 401 Unauthorized and points at its protected resource metadata (RFC 9728); the client reads that document to find the authorization server, fetches the authorization server's metadata, obtains a client ID (since the 2026-07-28 revision, preferably by hosting a Client ID Metadata Document), runs the browser flow with PKCE and a resource parameter, and then sends Authorization: Bearer on every request. This article walks through each step with the exact requirements from the specification, shows what the flow looks like from inside a client, and explains how mcp.tc tells you in advance whether a server will ask you to sign in.
What MCP authorization covers
MCP authorization is the part of the specification that lets a client call a protected remote server on behalf of a user, and it applies to HTTP transports only. The Authorization section of the 2026-07-28 specification says authorization is "OPTIONAL for MCP implementations", that implementations using an HTTP-based transport "SHOULD conform to this specification", and that implementations using a STDIO transport "SHOULD NOT follow this specification, and instead retrieve credentials from the environment". A local server launched by your client reads an API key from an environment variable; a remote server runs the flow below. The remote vs local post covers when to choose which.
There are three roles. The MCP server "acts as an OAuth 2.1 resource server": it accepts access tokens and checks them. The MCP client "acts as an OAuth 2.1 client". The authorization server "is responsible for interacting with the user (if necessary) and issuing access tokens", and the spec says it "may be hosted with the resource server or a separate entity". An MCP server never has to run a login page itself. It has to tell clients where the login lives and validate what comes back.
Step 1: the 401 and protected resource metadata
A protected MCP server answers a request without a token with HTTP 401, and the client learns where to authenticate from that response. The spec's example:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"The resource_metadata parameter comes from RFC 9728, OAuth 2.0 Protected Resource Metadata (April 2025). Per the discovery page of the spec, MCP servers "MUST implement one of the following discovery mechanisms": the header, or a document at the well-known URI. The well-known URI is /.well-known/oauth-protected-resource, either with the MCP endpoint's path appended (the spec's example: a server at https://example.com/public/mcp hosts metadata at https://example.com/.well-known/oauth-protected-resource/public/mcp) or at the root. Clients "MUST support both discovery mechanisms", use the header's URL when present, and otherwise probe the path form first and the root second.
The document itself is small. RFC 9728 marks resource as REQUIRED and authorization_servers as OPTIONAL, but MCP tightens the second: the document "MUST include the authorization_servers field containing at least one authorization server". scopes_supported is RECOMMENDED in the RFC and matters in MCP because clients fall back to it when the challenge carries no scope. A minimal document for the server above:
{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["files:read", "files:write"],
"bearer_methods_supported": ["header"]
}If a document lists several authorization servers, choosing one is the client's job, and the client "MUST maintain separate registration state (client credentials, tokens) per authorization server".
Every URL in this step comes from the server you are about to trust. The security best practices tutorial lists the resource_metadata URL, the authorization_servers entries and the endpoints in the authorization server metadata as inputs a hostile server could point at internal addresses, and says clients deployed to a server "MUST consider SSRF risks" and "SHOULD block requests to private and reserved IP address ranges".
Step 2: finding the authorization server's endpoints
With an issuer URL in hand, the client fetches the authorization server's metadata from a well-known path, and the spec fixes the order of paths to try. MCP uses the default suffix from RFC 8414, /.well-known/oauth-authorization-server, and defines no suffix of its own. For an issuer without a path, such as https://auth.example.com, clients "MUST try" /.well-known/oauth-authorization-server first and /.well-known/openid-configuration second. For an issuer with a path, such as https://auth.example.com/tenant1, the order is:
https://auth.example.com/.well-known/oauth-authorization-server/tenant1https://auth.example.com/.well-known/openid-configuration/tenant1https://auth.example.com/tenant1/.well-known/openid-configuration
Authorization servers "MUST provide at least one" of the two discovery formats, and clients must handle both.
Two checks happen on the document before anything else. First, the issuer value "MUST be identical to the issuer identifier used to construct the well-known URL"; the spec's own example is a document fetched from https://attacker.example/.well-known/oauth-authorization-server claiming "issuer": "https://honest.example", which "MUST be rejected". Second, PKCE support. The security considerations page says clients "MUST rely on authorization server metadata to verify this capability": if code_challenge_methods_supported is absent, "MCP clients MUST refuse to proceed". That applies to OpenID Connect discovery documents too, even though the OpenID metadata spec does not define the field, so authorization servers that offer OIDC discovery "MUST include code_challenge_methods_supported" to work with MCP.
The metadata also tells the client which registration options exist: a registration_endpoint for dynamic registration, and client_id_metadata_document_supported: true for the mechanism the next step describes.
Step 3: getting a client ID without a prior relationship
The client needs a client_id before it can start the browser flow, and the client registration page gives it three mechanisms to get one, with a priority order that clients supporting all of them "SHOULD" follow:
- Pre-registered client information for this server, if the client has it.
- A Client ID Metadata Document, if the authorization server advertises
client_id_metadata_document_supported. - Dynamic Client Registration (RFC 7591), if the metadata lists a
registration_endpoint. - Asking the user to type client details, when nothing else is available.
Client ID Metadata Documents (CIMD) entered the specification in the 2025-11-25 revision, and the 2026-07-28 revision makes them the default. The changelog deprecates Dynamic Client Registration "as a client registration mechanism in favor of Client ID Metadata Documents", keeping it "available for backwards compatibility with authorization servers that do not support Client ID Metadata Documents". The spec's overview now reads: authorization servers and clients "SHOULD support OAuth Client ID Metadata Documents" and "MAY support" dynamic registration.
The idea, from the IETF draft by Aaron Parecki and Emelia Smith (October 2025), is that the client's client_id is an HTTPS URL, and that URL serves a JSON document describing the client. MCP's requirements for the client side:
- The URL "MUST use the 'https' scheme and contain a path component", for example
https://example.com/client.json. - The document "MUST include at least the following properties:
client_id,client_name,redirect_uris". - The
client_idinside the document must match the document URL "exactly". - Per the draft,
token_endpoint_auth_method"MUST NOT includeclient_secret_post,client_secret_basic,client_secret_jwt, or any other method based around a shared symmetric secret". A client may useprivate_key_jwtwith a JWKS instead.
The spec's example document:
{
"client_id": "https://app.example.com/oauth/client-metadata.json",
"client_name": "Example MCP Client",
"client_uri": "https://app.example.com",
"logo_uri": "https://app.example.com/logo.png",
"redirect_uris": [
"http://127.0.0.1:3000/callback",
"http://localhost:3000/callback"
],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}On the authorization server side, the server detects a URL-shaped client_id, fetches the document, "MUST validate that the fetched document's client_id matches the URL exactly", "MUST validate redirect URIs presented in an authorization request against those in the metadata document", and "SHOULD cache metadata respecting HTTP cache headers". The draft adds that if the document cannot be retrieved, the server "SHOULD abort the authorization request".
The spec explains the motive in one sentence: CIMD "addresses the common MCP scenario where servers and clients have no pre-existing relationship". A CIMD client ID is also "portable across authorization servers", so "No re-registration is needed when the authorization server changes". Credentials from pre-registration or dynamic registration, by contrast, "MUST" be keyed by the issuer that issued them and "MUST NOT" be reused with a different authorization server.
Dynamic registration is still around. Clients that use it "MUST specify an appropriate application_type": "native" for desktop apps, CLIs and locally hosted web apps on localhost, "web" for remote browser apps. Omitting it "defaults to "web" under OIDC, which can conflict with native-style redirect URIs".
Step 4: the browser round trip
With a client ID, the client opens the authorization URL in a browser, and the spec adds three things to a textbook authorization code flow: PKCE, a resource parameter, and issuer validation on the way back.
PKCE is mandatory. Clients "MUST implement PKCE" and "MUST use the S256 code challenge method when technically capable". Redirect URIs "MUST be either localhost or use HTTPS", and all authorization server endpoints "MUST be served over HTTPS". Clients "SHOULD use and verify state parameters" and discard results whose state does not match.
The resource parameter is RFC 8707, Resource Indicators for OAuth 2.0 (February 2020). MCP clients "MUST" include it "in both authorization requests and token requests", it "MUST identify the MCP server that the client intends to use the token with", and clients "MUST send this parameter regardless of whether authorization servers support it". Its value is the server's canonical URI: an absolute URI with no fragment. The spec lists https://mcp.example.com/mcp, https://mcp.example.com and https://mcp.example.com:8443 as valid, and mcp.example.com (no scheme) and https://mcp.example.com#fragment as invalid. Encoded in the request it looks like this:
&resource=https%3A%2F%2Fmcp.example.comThis is audience binding. A token minted for https://mcp.example.com/mcp carries that audience, and the server on the other end "MUST validate that access tokens were issued specifically for them as the intended audience".
Issuer validation is the newer piece. Before redirecting, the client "MUST record the issuer value" from the validated metadata document alongside its PKCE verifier. Authorization servers "SHOULD include the iss parameter in authorization responses" (RFC 9207) and, when they do, "MUST advertise this" with authorization_response_iss_parameter_supported: true. On the callback, if the server advertised support and iss is missing, the client rejects the response; if iss is present, the client compares it to the recorded issuer by simple string comparison, with no case folding or trailing-slash normalization. This counters mix-up attacks, where an attacker-controlled authorization server tries to obtain a code issued by an honest one. The security tutorial notes that "PKCE alone does not prevent this attack".
Step 5: tokens, scopes and step-up
Once the client has a token, every request to the MCP server carries it in a header, and scope problems come back as HTTP errors the client is expected to act on. Clients "MUST use the Authorization request header field", in the form Authorization: Bearer <access-token>, authorization "MUST be included in every HTTP request from client to server", and tokens "MUST NOT be included in the URI query string".
The status codes are fixed:
| Status | Meaning in the spec |
|---|---|
| 401 | "Authorization required or token invalid" |
| 403 | "Invalid scopes or insufficient permissions" |
| 400 | "Malformed authorization request" |
Scopes are meant to grow. On the first 401, clients "SHOULD" use the scope from the WWW-Authenticate header if present, and otherwise request everything in scopes_supported. When a later call needs more, the server "SHOULD respond with" 403 and a challenge like:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="files:write",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
error_description="File write permission required for this operation"The client then computes "the union of the client's previously requested scope set and the scopes from the current challenge", re-authorizes with that set, and retries "no more than a few times". Servers "SHOULD include all scopes required for the current operation in a single challenge" rather than one per retry, and "MUST account for scope hierarchies".
Two server-side rules close the loop. An MCP server "MUST only accept tokens that are valid for use with their own resources" and "MUST NOT accept or transit any other tokens". If the server calls an upstream API, "the access token used at the upstream API is a separate token", and the server "MUST NOT pass through the token it received from the MCP client". The security tutorial calls this anti-pattern token passthrough and says it "is explicitly forbidden". If you are building a server, the Streamable HTTP server guide covers the bearer check on the transport side.
What the flow looks like from a client
From inside a client, the whole sequence above shrinks to one sign-in prompt, and the client's documentation tells you which knobs it exposes. The steps below are the ones in Claude Code's MCP documentation. You add a remote server with:
claude mcp add --transport http example-server https://mcp.example.com/mcpthen run /mcp inside a session and "follow the steps in your browser to log in", or run the flow from a shell with claude mcp login example-server (add --no-browser to be prompted with the authorization URL even when a local browser is available). The docs state that Claude Code "supports servers that use a Client ID Metadata Document (CIMD) instead of Dynamic Client Registration, and discovers these automatically". When automatic discovery fails, the docs say you may see an error like Incompatible auth server: does not support dynamic client registration, and you pass pre-registered credentials with --client-id and --client-secret (in automation, from MCP_CLIENT_SECRET); the docs say the secret is stored "in your system keychain (macOS) or a credentials file, not in your config". Two JSON options map directly onto steps 2 and 5: oauth.authServerMetadataUrl to "bypass the default discovery chain", and oauth.scopes to "pin the scopes Claude Code requests during the authorization flow".
Each client documents its own options. mcp.tc's connect docs cover setup for Claude Code, claude.ai, Claude Desktop, ChatGPT, Cursor, VS Code, Devin Desktop, Codex and Gemini CLI, including what to check when a sign-in does not complete.
Whatever the client, the browser window belongs to the authorization server, and the token ends up in the client's own storage. If you add a server from an mcp.tc listing or with npx mcp-tc add, the client is pointed at the server's own URL; the sign-in happens between the client and the authorization server, and mcp.tc never sees the token.
Checklist for server authors
If you run a remote MCP server behind OAuth, the specification's requirements for your side fit in a short list:
- Return 401 for a missing or invalid token, with
WWW-Authenticate: Bearer resource_metadata="...", and addscope="..."(a SHOULD). - Serve the protected resource metadata at
/.well-known/oauth-protected-resource(path form, root form, or both) withresourceand at least one entry inauthorization_servers. - Make sure the authorization server publishes RFC 8414 or OpenID Connect metadata with
code_challenge_methods_supported, and preferablyclient_id_metadata_document_supported: trueandauthorization_response_iss_parameter_supported: true. - Validate the audience of every token against your own canonical URI, and never forward a client's token upstream.
- Return 403 with
error="insufficient_scope"and the full set of missing scopes when a token is valid but too narrow.
One consequence for anyone who reads your tool list from outside: the tools page says the set returned by tools/list "MAY vary by the authorization presented on the request", for example returning only the tools the caller's scopes permit. An unauthenticated reader may see fewer tools than a signed-in user, or none.
What to do next on mcp.tc
mcp.tc tells you before you connect whether a server will ask you to sign in, and keeps the sign-in itself between your client and the server. Every listing page states whether the server needs sign-in, and the directory filters by it: no sign-in, sign-in or API key, or runs locally. On 18 October 2026 the directory listed 633 remote servers: 424 sign you in with OAuth, 40 take an API key, 153 need no sign-in, and 16 work without sign-in but accept one.
Each listing has setup steps for Claude Code, claude.ai, Claude Desktop, ChatGPT, Cursor, VS Code, Devin Desktop, Codex and Gemini CLI, plus a generic JSON config, all using the server's own URL. The mcp-tc command-line tool does the same from a terminal: npx mcp-tc add shows the command or config change it is about to make and asks first, and API keys stay on your computer.
The History tab is where authorization has a visible limit. mcp.tc reads tool lists without signing in, every 3 hours, for the 227 listings (on 18 October 2026) whose servers list tools without sign-in or publish an MCP bundle manifest. A server that answers tools/list with 401 until you sign in has no watched tool list; its History tab records the new versions mcp.tc can read and the edits its owner saves. The changes docs spell out what the checks can and cannot see. If a server you rely on is missing, suggest it with its URL; the checker tries the handshake and the listing records whether the server asks for sign-in.
FAQ
Does every MCP server need OAuth?
No. Authorization is optional in the specification. Servers over stdio should read credentials from the environment rather than run OAuth, and a remote server can be open, require an API key in a header, or implement the OAuth flow. On 18 October 2026, 153 of the 633 remote servers listed on mcp.tc needed no sign-in. What a server requires is shown on its mcp.tc listing and can be used as a filter in the directory.
What is protected resource metadata?
It is a JSON document, defined in RFC 9728 (April 2025), that a protected server publishes at /.well-known/oauth-protected-resource or points to from a WWW-Authenticate header. It names the resource and lists the authorization servers that issue tokens for it, optionally with supported scopes. MCP servers must implement it, and MCP clients must use it to find the authorization server before they can start a sign-in.
What is a Client ID Metadata Document?
A way for an OAuth client to identify itself without registering first. The client's client_id is an HTTPS URL, and that URL serves a JSON document with at least client_id, client_name and redirect_uris. The authorization server fetches and validates it on demand. The 2025-11-25 MCP revision added it as a recommended mechanism, and the 2026-07-28 revision deprecated Dynamic Client Registration in its favour, keeping the older mechanism available for authorization servers that do not support the new one.
Why does the client send a resource parameter?
To bind the token to one server. RFC 8707 lets a client name the resource a token is for, and MCP makes it mandatory in both the authorization and token requests, using the server's canonical URI. The server then checks that the token's audience is itself and rejects tokens issued for anything else. That check is what stops a token obtained for one service from being replayed against another.
What happens when a tool needs a scope I did not grant?
The server answers with HTTP 403 and a WWW-Authenticate header containing error="insufficient_scope" and the scopes it needs. A client that follows the spec computes the union of what it already requested and the new scopes, sends you through the authorization server again with that set, and retries the call a limited number of times. Servers are expected to list all missing scopes in one challenge rather than one per retry.
Does mcp.tc take part in the sign-in?
No. Setup steps and the mcp-tc tool configure your client with the server's own URL. When the server returns 401, your client and the server's authorization server handle the rest, and the token is stored by your client. mcp.tc reads public tool lists without signing in, so for servers that require sign-in before listing tools, the History tab shows versions and owner edits rather than tool changes.
Sources
- modelcontextprotocol.io/specification/2026-07-28/basic/authorization
- modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery
- modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration
- modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations
- modelcontextprotocol.io/specification/2026-07-28/changelog
- modelcontextprotocol.io/specification/latest/server/tools
- modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices
- datatracker.ietf.org/doc/html/rfc9728
- datatracker.ietf.org/doc/html/rfc8414
- datatracker.ietf.org/doc/html/rfc8707
- datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00
- code.claude.com/docs/en/mcp