Skip to main content
Authorization in the Model Context Protocol (MCP) secures access to sensitive resources and operations exposed by MCP servers. If your MCP server handles user data or administrative actions, authorization lets only permitted users reach its endpoints. MCP authorization follows OAuth 2.1 and works with any authorization or identity system that meets its requirements. This guide explains the flow, then shows how to protect a small server. The full requirements are in the Authorization specification.

When Should You Use Authorization?

Authorization is optional for MCP servers. It is strongly recommended when:
  • Your server works with user-specific data such as emails, documents, or databases
  • You need to audit who performed which actions
  • Your server grants access to its APIs that require user consent
  • You’re building for enterprise environments with strict access controls
  • You want to rate-limit or track usage per user
Authorization for Local MCP ServersThe OAuth flow in this guide is for HTTP-based transports. Servers that use the stdio transport should retrieve credentials from the environment instead. They run locally, so they can also use credentials from libraries embedded in the server.

The Authorization Flow: Step by Step

Three parties take part. Your MCP server is an OAuth resource server. In this role it validates access tokens and doesn’t issue them. The MCP client is an OAuth client acting on behalf of a user. The authorization server authenticates the user and issues tokens for your MCP server. It can be a separate service or be hosted together with the MCP server.
1

Initial Request

When a client sends a request without a valid token, your server responds with 401 Unauthorized. The WWW-Authenticate header can point to the Protected Resource Metadata (PRM) document. It should also name the scopes needed to access the resource.
The client should request the scopes named in the challenge. If the challenge names none, it should request everything in the PRM document’s scopes_supported. If that is missing too, it leaves out the scope parameter. See Scope Selection Strategy.A server must include the resource_metadata pointer or serve the PRM document at a well-known URI. It can do both. When the pointer is missing, the client first tries /.well-known/oauth-protected-resource followed by the path of the MCP endpoint. It then tries the same URL without the path. See Protected Resource Metadata Discovery Requirements.
2

Protected Resource Metadata Discovery

The client fetches the PRM document to learn which authorization servers the MCP server trusts. authorization_servers must contain at least one entry.
resource identifies your MCP server. Use the exact URL that clients connect to. RFC 9728 Section 3.3 says clients must not use a document whose resource doesn’t match.
3

Authorization Server Discovery

The client picks an authorization server and fetches its metadata. The metadata can be at an OAuth 2.0 Authorization Server Metadata or an OpenID Connect Discovery URL. Clients must support both and try the URLs in the order the specification defines.
The client must check two of these values:
  • issuer must be identical to the issuer identifier used to build the metadata URL. If it isn’t, the client must not use the document.
  • code_challenge_methods_supported must be present. If it’s absent, the client must refuse to proceed, because the authorization server doesn’t support PKCE.
4

Client Registration

The client needs a client ID that the authorization server accepts. The options are summarized in Client Registration Approaches.
5

User Authorization

The client opens a browser at the authorization_endpoint. The request includes a PKCE code_challenge and the resource parameter (RFC 8707), set to the MCP server’s URL:
The line breaks in this example are only there to make it easier to read.After the user logs in and consents, the authorization server redirects back to the client with a code and the state. If it supports RFC 9207, it also returns iss.Before it opens the browser, the client must record the issuer from the validated metadata. Before it exchanges the code, it must validate iss against that issuer, without normalizing either value. If the authorization server advertises support for iss, the client must reject a response that leaves it out. The same check applies to error responses. If it fails, the client must not act on the error or display it.The client should also discard a response whose state is missing or different from the value it sent.The client then sends the code, the PKCE verifier, and the same resource value to the token_endpoint. It receives an access token, and the authorization server can choose to return a refresh token too:
6

Making Authenticated Requests

The client sends the access token in the Authorization header of every HTTP request, and never in the URL:
Before it processes the request, the MCP server validates the token and checks that the token was issued for this server. An invalid or expired token gets a 401.If the token is valid but lacks a scope, the server should respond with 403 Forbidden and an insufficient_scope challenge:
The client should then start a step-up authorization flow. It requests the scopes in the challenge together with the scopes it requested before, and it should limit how often it retries.

Client Registration Approaches

A client that supports all three should try them in that order. If none is available, it should ask the user to enter the client information. See Client Registration.

Client ID Metadata Documents

With a Client ID Metadata Document (CIMD), the client uses an HTTPS URL as its client_id and hosts a JSON document there. The authorization server fetches the document and validates it when it receives an authorization request.
The URL must use https and contain a path, such as /oauth/client-metadata.json. The document must contain at least client_id, client_name, and redirect_uris, and its client_id must match the URL exactly. Authorization servers advertise support with "client_id_metadata_document_supported": true. A client can use the same client ID with every authorization server that supports CIMD. It doesn’t need to register again if your MCP server changes authorization servers.

Pre-registration

The client ID is set up in advance, along with credentials if they’re needed. They can be built into the client or entered by the user.

Dynamic Client Registration

Dynamic Client Registration (DCR) is deprecated. New implementations should use Client ID Metadata Documents instead. DCR remains available for backwards compatibility with authorization servers that don’t support them.
With DCR, the client posts its metadata to the authorization server’s registration_endpoint and receives a client ID. Clients must include an appropriate application_type:
Pre-registered and DCR credentials belong to the authorization server that issued them. Clients must store them by issuer and never use them with another one. See Authorization Server Binding.

Choosing an Authorization Server

Use an existing identity provider or authorization server product. Client registration happens between the client and the authorization server, so your MCP server needs no CIMD or DCR code. Check these points before you choose one:
  • Audience. Your MCP server must reject tokens that weren’t issued for it. The resource value from the client should become the token’s audience, which is the aud claim in a JWT. Some products need a setting for this, or let you configure the audience on a scope instead. Others always use their own identifier. In that case, compare the audience with that identifier, and keep your server’s URL as the PRM resource. The identifier must belong to this MCP server only, so don’t share one application registration between servers.
  • Metadata. It must serve metadata through RFC 8414 or OpenID Connect Discovery. The metadata must contain code_challenge_methods_supported. Look at the real document, because some products support PKCE but leave this value out, and clients that follow the specification then refuse to connect.
  • CIMD. It should support CIMD, which it advertises with client_id_metadata_document_supported. In some products you have to enable this, or list the client domains you accept.
  • Token format. The examples below need signed JWT access tokens. Otherwise, you need an introspection endpoint.
  • Public clients. Many MCP clients have no secret. The authorization server must accept them, and its token_endpoint_auth_methods_supported should contain none.
  • Scopes. Define a scope such as mcp:tools and make it available to the clients that will connect. Some products add a prefix to scope names. If yours does, use the full name everywhere the examples use mcp:tools. If you define more than one scope, read Scope Minimization, and list only the scopes needed for basic use in scopes_supported.
  • Token lifetime. Access tokens should be short-lived, because a stolen token works until it expires. The authorization server must rotate refresh tokens for public clients.
  • Issuer in responses. It should return iss in authorization responses. If it does, it must advertise authorization_response_iss_parameter_supported.
Testing LocallyYou can run an authorization server on your own machine. For example, Keycloak has experimental support for CIMD and resource indicators. To enable both, start it with --features=cimd,resource-indicators. You then have to create a client policy for CIMD and add your server’s URL to its resource indicator allow list.

Implementation Example

The examples below protect an MCP server that has one tool, add. Each server runs at http://localhost:3000/mcp, requires the mcp:tools scope, and accepts JWT access tokens signed with RS256. Before you run one, change these values:
  • ISSUER is the exact issuer value from your authorization server’s metadata.
  • METADATA_URL is the URL of that metadata. If the issuer has a path, as in https://auth.example.com/tenant1, the RFC 8414 URL is https://auth.example.com/.well-known/oauth-authorization-server/tenant1, and the OpenID Connect URL is usually https://auth.example.com/tenant1/.well-known/openid-configuration.
  • RS256 (RsaSha256 in C#) is the signing algorithm. Change it if your authorization server uses a different one. In Python, the published keys must then have an alg value.
  • mcp:tools is the required scope.
  • The expected audience is the server URL. Change it if your authorization server puts its own identifier in aud. It is the audience option in TypeScript and Python, and ValidAudience in C#.
In the C# example, the first two are called issuer and metadataUrl. The TypeScript and Python examples fetch the metadata at startup, so they exit if the authorization server can’t be reached.
Install the TypeScript SDK packages, a JWT library, and Zod 4:
Set "type": "module" in package.json, and create server.ts:
  • mcpAuthMetadataRouter serves the PRM document at /.well-known/oauth-protected-resource/mcp. It also mirrors the authorization server metadata for older clients that look for it on the MCP server.
  • requireBearerAuth runs your verifier on every request. It answers a missing or invalid token with a 401 challenge, and a missing scope with a 403 challenge.
  • jwtVerify checks the signature, issuer, audience, and lifetime. Keep its audience option, because expectedResource doesn’t look at the token. expectedResource refuses a token if the verifier reports a different resource or none. It requires @modelcontextprotocol/express 2.0.2, and earlier versions ignore it.
  • createMcpExpressApp validates the Host and Origin headers of a server bound to localhost.
Run the server:
To require a scope for a single tool, see Require authorization in the TypeScript SDK documentation.

Opaque Tokens

If your authorization server issues opaque tokens, use its token introspection endpoint instead of validating a JWT. Its URL is the introspection_endpoint value in the metadata. The endpoint requires authorization, so your MCP server needs its own credentials at the authorization server. If you use HTTP Basic authentication, form-encode the client ID and secret first. The credentials must be allowed to introspect tokens for this server. If they aren’t, the response for a valid token has active set to false. If active is false, reject the token. If it is true, check that aud contains the expected audience before you report the resource to the SDK. Check scope and exp too. These values are optional in the RFC, so make sure your authorization server returns them, and reject a token whose aud is missing. If the endpoint can’t be reached or returns an error status, even a 401, treat that as an outage. If you cache tokens or results, encrypt the tokens and delete each entry no later than the token expires. Keep your server’s credentials in a secret manager. In TypeScript and Python, make the call in the verifier. In C#, replace AddJwtBearer with an authentication handler that supports introspection.

Deploying Beyond Localhost

Change the server URL to the exact https URL that clients connect to. It is used as the PRM resource and as the expected audience. If its path isn’t /mcp, change the route too. It is the app.all path in TypeScript and the streamable_http_path argument of mcp.run in Python. The C# example takes it from the URL. In TypeScript, new URL() adds a trailing slash to a URL that has no path, and the token’s aud then has to include it. Clients must reach your server over HTTPS. The examples serve plain HTTP, so put them behind a reverse proxy or load balancer that terminates TLS and keeps the original Host header. If it runs on the same machine, keep listening on localhost and change only the accepted hosts and origins. The examples listen only on localhost and reject requests with any other Host header. Configure the hosts and browser origins you accept:
  • TypeScript: Pass host, allowedHosts, and allowedOrigins to createMcpExpressApp, and change the address given to app.listen. Both lists take host names without a scheme or port. If no browser client connects, pass an empty allowedOrigins list, because the SDK doesn’t check Origin without it. Set NODE_ENV=production so that Express doesn’t send stack traces to clients.
  • Python: Pass host and a TransportSecuritySettings object as transport_security to mcp.run. Import the class from mcp.server.transport_security, and set its allowed_hosts and allowed_origins. The SDK checks neither header if you change host and leave this out. A host must match the Host header exactly, including the port if it has one, and an origin must include its scheme. End an entry with :* to accept any port.
  • C#: Change the URL given to app.Run to the address to listen on, such as http://0.0.0.0:3000. Change allowedOrigins, whose entries include the scheme, and allowedHosts, whose entries have no scheme or port.
Accepting an origin doesn’t enable CORS. Clients that run in a browser also need CORS middleware that answers preflight requests before authentication and exposes the WWW-Authenticate header. This protocol revision has no sessions, so ignore the Mcp-Session-Id header if your server supports only this revision. If it also accepts older clients, never use a session ID as proof of who the caller is. See Session Hijacking in the 2025-11-25 documentation.

Testing the Server

First check discovery. These requests don’t need a token:
The first command should print a 401 with a WWW-Authenticate header. The second should print the PRM document, which contains your server’s URL and your issuer. Also open your METADATA_URL and check that it contains code_challenge_methods_supported. Some clients work without it and others refuse to connect. Then run the full flow with the MCP Inspector, which needs Node.js 22.19 or later. The command below opens a browser for login and calls the tool:
--protocol-era modern makes the Inspector use the 2026-07-28 protocol. The Inspector’s redirect URI is http://127.0.0.1:6276/oauth/callback. --client-metadata-url is the URL of a metadata document that you host, with this redirect URI in its redirect_uris. If your authorization server supports DCR and accepts registration from your machine, you can leave this option out. For a pre-registered client, register the redirect URI and use --client-id instead, with --client-secret if the client has a secret. You can also connect from a client such as Visual Studio Code. Run MCP: Add Server from the Command Palette, choose HTTP, and enter http://localhost:3000/mcp.
Common Issues
  • The authorization server rejects the client ID. It must be able to fetch the client_id URL from its own network. If it has a list of trusted client domains, the list must include the URL’s host.
  • The metadata document is invalid. Its client_id must be identical to its URL, including any trailing slash. The redirect URI in the request must be in redirect_uris. Authorization servers should accept any port for a loopback IP address, but some compare the port too.
  • The authorization server rejects the resource value. Register your server’s URL exactly as clients send it, including the path and any trailing slash. The error might be invalid_target.
  • The Inspector prints auth_required after a successful login. Your server refused the new token. The command prints the same message for a 401 and a 403. To see which one it is, send the token with curl -i -X POST http://localhost:3000/mcp -H "Content-Type: application/json" -H "Authorization: Bearer <token>".
  • The Inspector still fails after you fix the configuration. It keeps tokens and registered clients between runs. Add --relogin to discard them.
  • Every token gets a 401. Decode a test token. iss must be exactly the same as ISSUER, and aud must contain exactly the expected audience, http://localhost:3000/mcp in the examples. If the token isn’t a JWT, see Opaque Tokens. Check too that the clocks of the two servers agree.
  • Every token gets a 403. If the response has an insufficient_scope error, the token’s scope claim doesn’t contain mcp:tools. Some authorization servers use a claim such as scp, or an array instead of a string. Change the code that reads the claim. If the token uses a shorter name than clients request, convert it to the full name there. If there is no such error, the Host or Origin header might have been refused. For a refused Host, the Python SDK answers 421 and ASP.NET Core answers 400.
  • The client refuses to start the flow. Check that the authorization server metadata has code_challenge_methods_supported and that its issuer matches the PRM document.

Common Pitfalls and How to Avoid Them

Read Security Best Practices for attacks and how to prevent them, and Security Considerations for the requirements in the specification. These topics matter most in the following cases: The list below covers other common mistakes in a protected server and its authorization server:
  • Don’t write your own token validation. Use well-tested libraries for signature checks, claims validation, and authorization decisions.
  • Always validate the audience. A correctly signed token from your authorization server might be meant for another service. Compare the audience exactly with your server’s URL, or with the identifier your authorization server uses for this server only. Don’t accept a value that several servers share, such as api.
  • Accept only access tokens. Never accept an ID token. If your authorization server follows RFC 9068, check that the token’s typ header is at+jwt or application/at+jwt. The examples don’t check it, because many authorization servers use another value.
  • Accept only one issuer. Reject tokens from other issuers or tenants, unless your server is meant to be multi-tenant.
  • Return complete challenges. Where your SDK allows it, include resource_metadata and scope in 401 and 403 challenges. List every scope the operation needs at once. Leave offline_access out of challenges and scopes_supported, because your server doesn’t need refresh tokens.
  • Return 5xx during an outage. If your server can’t reach the JWKS or introspection endpoint, return a 5xx status, as the TypeScript and Python examples do. The C# example does this only until it has downloaded the keys once. A 401 makes users log in again, and that doesn’t fix the problem.
  • Separate client and server credentials. Don’t use your MCP server’s introspection credentials in end-user flows.
  • Don’t log credentials. Remove Authorization headers, tokens, authorization codes, and secrets from your logs. Return generic errors to clients and keep the details in your own logs.
  • Restrict Dynamic Client Registration. Anyone can register with an open registration_endpoint, so add controls such as trusted hosts and rate limits.
MCP authorization builds on these specifications: For more details, see: