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
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 The client should request the scopes named in the challenge. If the challenge names none, it should request everything in the PRM document’s
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.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:
issuermust 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_supportedmust 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 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
authorization_endpoint. The request includes a PKCE code_challenge and the resource parameter (RFC 8707), set to the MCP server’s URL: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 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 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.
Authorization header of every HTTP request, and never in the URL:401.If the token is valid but lacks a scope, the server should respond with 403 Forbidden and an insufficient_scope challenge: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 itsclient_id and hosts a JSON document there. The authorization server fetches the document and validates it when it receives an authorization request.
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
With DCR, the client posts its metadata to the authorization server’sregistration_endpoint and receives a client ID. Clients must include an appropriate application_type:
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
resourcevalue from the client should become the token’s audience, which is theaudclaim 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 PRMresource. 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_supportedshould containnone. - Scopes. Define a scope such as
mcp:toolsand 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 usemcp:tools. If you define more than one scope, read Scope Minimization, and list only the scopes needed for basic use inscopes_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
issin authorization responses. If it does, it must advertiseauthorization_response_iss_parameter_supported.
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:
ISSUERis the exactissuervalue from your authorization server’s metadata.METADATA_URLis the URL of that metadata. If the issuer has a path, as inhttps://auth.example.com/tenant1, the RFC 8414 URL ishttps://auth.example.com/.well-known/oauth-authorization-server/tenant1, and the OpenID Connect URL is usuallyhttps://auth.example.com/tenant1/.well-known/openid-configuration.RS256(RsaSha256in C#) is the signing algorithm. Change it if your authorization server uses a different one. In Python, the published keys must then have analgvalue.mcp:toolsis the required scope.- The expected audience is the server URL. Change it if your authorization server puts its own identifier in
aud. It is theaudienceoption in TypeScript and Python, andValidAudiencein C#.
issuer and metadataUrl. The TypeScript and Python examples fetch the metadata at startup, so they exit if the authorization server can’t be reached.
- TypeScript
- Python
- C#
Install the TypeScript SDK packages, a JWT library, and Zod 4:Set To require a scope for a single tool, see Require authorization in the TypeScript SDK documentation.
"type": "module" in package.json, and create server.ts:mcpAuthMetadataRouterserves 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.requireBearerAuthruns your verifier on every request. It answers a missing or invalid token with a401challenge, and a missing scope with a403challenge.jwtVerifychecks the signature, issuer, audience, and lifetime. Keep itsaudienceoption, becauseexpectedResourcedoesn’t look at the token.expectedResourcerefuses a token if the verifier reports a different resource or none. It requires@modelcontextprotocol/express2.0.2, and earlier versions ignore it.createMcpExpressAppvalidates theHostandOriginheaders of a server bound to localhost.
Opaque Tokens
If your authorization server issues opaque tokens, use its token introspection endpoint instead of validating a JWT. Its URL is theintrospection_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 exacthttps 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, andallowedOriginstocreateMcpExpressApp, and change the address given toapp.listen. Both lists take host names without a scheme or port. If no browser client connects, pass an emptyallowedOriginslist, because the SDK doesn’t checkOriginwithout it. SetNODE_ENV=productionso that Express doesn’t send stack traces to clients. - Python: Pass
hostand aTransportSecuritySettingsobject astransport_securitytomcp.run. Import the class frommcp.server.transport_security, and set itsallowed_hostsandallowed_origins. The SDK checks neither header if you changehostand leave this out. A host must match theHostheader 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.Runto the address to listen on, such ashttp://0.0.0.0:3000. ChangeallowedOrigins, whose entries include the scheme, andallowedHosts, whose entries have no scheme or port.
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: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_idURL 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_idmust be identical to its URL, including any trailing slash. The redirect URI in the request must be inredirect_uris. Authorization servers should accept any port for a loopback IP address, but some compare the port too. - The authorization server rejects the
resourcevalue. Register your server’s URL exactly as clients send it, including the path and any trailing slash. The error might beinvalid_target. - The Inspector prints
auth_requiredafter a successful login. Your server refused the new token. The command prints the same message for a401and a403. To see which one it is, send the token withcurl -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
--reloginto discard them. - Every token gets a
401. Decode a test token.issmust be exactly the same asISSUER, andaudmust contain exactly the expected audience,http://localhost:3000/mcpin 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 aninsufficient_scopeerror, the token’sscopeclaim doesn’t containmcp:tools. Some authorization servers use a claim such asscp, 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, theHostorOriginheader might have been refused. For a refusedHost, the Python SDK answers421and ASP.NET Core answers400. - The client refuses to start the flow. Check that the authorization server metadata has
code_challenge_methods_supportedand that itsissuermatches 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:- Your server calls other APIs: It must get a separate token for each of them. See Token Passthrough.
- Your server proxies OAuth with one static client ID: It must get the user’s consent for each MCP client. See Confused Deputy Problem.
- Your tools return state handles: See State Handle Hijacking.
- You build a client: See Mix-Up Attacks, OAuth Authorization URL Validation, and Server-Side Request Forgery.
- 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
typheader isat+jwtorapplication/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_metadataandscopein401and403challenges. List every scope the operation needs at once. Leaveoffline_accessout of challenges andscopes_supported, because your server doesn’t need refresh tokens. - Return
5xxduring an outage. If your server can’t reach the JWKS or introspection endpoint, return a5xxstatus, as the TypeScript and Python examples do. The C# example does this only until it has downloaded the keys once. A401makes 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
Authorizationheaders, 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.
Related Standards and Documentation
MCP authorization builds on these specifications:- OAuth 2.1: The core authorization framework
- RFC 9728: Protected Resource Metadata
- RFC 8414: Authorization Server Metadata
- OpenID Connect Discovery 1.0: The alternative to RFC 8414
- RFC 8707: Resource Indicators
- RFC 9207: Authorization Server Issuer Identification
- OAuth Client ID Metadata Document: Client IDs that are URLs
- RFC 7591: Dynamic Client Registration
- RFC 6750: Bearer Token Usage
- Authorization Specification
- Security Best Practices
- Authorization Extensions, for machine-to-machine and enterprise-managed authorization
- Available MCP SDKs