Your MCP server is an OAuth resource server: what it must do

Your MCP server is an OAuth resource server: what it must do

When I wrote about the Model Context Protocol (MCP) in MCP is in its Yahoo era, two numbers stayed with me: in the study I cited there, about 40% of live remote servers asked for no authentication at all, and only about 30% used OAuth. What surprised me, reading the MCP authorization rules closely, is how small the server’s share of the work is. As a resource server, it doesn’t issue tokens. It publishes one document, answers with the right challenge, checks every token it receives and keeps those tokens to itself.

This is part 18 of my series on what an MCP server does under the 2026-07-28 specification, after subscriptions/listen in part 17. The facts are as I read them in October 2026.

In brief

  1. Authorization is optional, but an HTTP server that uses it SHOULD follow the MCP authorization specification. A server on standard input and output (stdio) SHOULD NOT: it takes its credentials from the environment.
  2. Your server is the resource server. It accepts and validates access tokens; an authorization server, bundled with it or separate, issues them.
  3. It MUST publish Protected Resource Metadata, and its 401 points clients there. The challenge SHOULD name the scope the request needs.
  4. Every token is validated, its audience included. A token issued for anything other than your server is refused.
  5. Tokens never pass through. If your server calls another application programming interface (API), it gets a separate token for it.

Who does what

The specification’s authorization page makes authorization OPTIONAL. When you implement it, the rule depends on the transport (part 7 covers the two standard ones):

TransportRule
HTTP-based, such as Streamable HTTPSHOULD follow the MCP authorization specification
stdioSHOULD NOT; it retrieves credentials from the environment
Any otherMUST follow established security practice for its protocol

The specification builds on a selected subset of established standards, among them OAuth 2.1, Client ID Metadata Documents, OpenID Connect Discovery and these five Requests for Comments (RFCs):

StandardCovers
RFC 6750Bearer tokens
RFC 8414Authorization server metadata
RFC 9728Protected Resource Metadata
RFC 8707Resource indicators
RFC 9207Issuer identification

A protected MCP server is a resource server: it accepts and validates access tokens but doesn’t issue them. The authorization server can be bundled with your MCP server or run separately (Entra ID, Okta, Auth0, Keycloak and so on), and its internals are outside the specification. The client gets its token there, with Proof Key for Code Exchange (PKCE), and sends it on every request:

User resource owner → Authorization server issues tokens: signs in, consents; MCP client OAuth 2.1 client → Authorization server issues tokens: authorization + token requests PKCE, resource parameter; Authorization server issues tokens → MCP client OAuth 2.1 client: access token audience = MCP server; MCP client OAuth 2.1 client → MCP server OAuth 2.1 resource server: Authorization: Bearer token on every request; MCP server OAuth 2.1 resource server → Authorization server issues tokens: Protected Resource Metadata points toUserresource ownerMCP clientOAuth 2.1 clientAuthorization serverissues tokensMCP serverOAuth 2.1 resource serversigns in, consentsauthorization + token requestsPKCE, resource parameteraccess tokenaudience = MCP serverAuthorization: Bearer tokenon every requestProtected Resource Metadatapoints to

From the first 401 to a token

Most of the flow belongs to the client and the authorization server: discovery, registration, the user’s sign-in and the token request. The client sends the user to sign in with PKCE and a resource parameter naming your server’s canonical Uniform Resource Identifier (URI), then checks the issuer before it asks for the token. Your server’s part is the first two answers and the check at the end:

MCP client → MCP server resource server: MCP request without token; MCP server resource server → MCP client: 401 + WWW-Authenticate resource_metadata URL, scope; MCP client → MCP server resource server: GET Protected Resource Metadata, RFC 9728; MCP server resource server → MCP client: resource, authorization_servers, scopes_supported; MCP client → Authorization server: GET authorization server metadata, RFC 8414 or OpenID Connect discovery; Authorization server → MCP client: endpoints, capabilities, issuer; alt: Client ID Metadata Document, preferred; Note (MCP client, Authorization server): client_id is an HTTPS URL the authorization server fetches and validates; else: Pre-registered client; Note (MCP client): Uses an existing client_id; else: Dynamic Client Registration, deprecated; MCP client → Authorization server: POST /register application_type; Authorization server → MCP client: client credentials; Note (MCP client): Generate PKCE, record expected issuer, apply scope selection, set resource = canonical MCP URI; MCP client → Browser: open authorization URL code_challenge, resource, scope; Browser → Authorization server: authorization request; Note (Authorization server): User signs in and consents; Authorization server → Browser: redirect with code + iss; Browser → MCP client: callback with code + iss; Note (MCP client): Validate iss against recorded issuer, RFC 9207; MCP client → Authorization server: token request code_verifier, resource; Authorization server → MCP client: access token + refresh token; MCP client → MCP server resource server: MCP request + Authorization Bearer token; Note (MCP server resource server): Validate token, audience, scopes; MCP server resource server → MCP client: MCP responseAuthorizationserverMCP serverresource serverMCP clientBrowserclient_id is an HTTPS URLthe authorization server fetches and validatesUses an existingclient_idalt[Client ID MetadataDocument, preferred][Pre-registered client][Dynamic ClientRegistration, deprecated]Generate PKCE,record expected issuer,apply scope selection,set resource =canonical MCP URIUser signs inand consentsValidate iss againstrecorded issuer,RFC 9207Validate token,audience, scopesMCP requestwithout token401 +WWW-Authenticateresource_metadataURL, scopeGET ProtectedResource Metadata,RFC 9728resource,authorization_servers,scopes_supportedGET authorizationserver metadata,RFC 8414 or OpenIDConnect discoveryendpoints,capabilities, issuerPOST /registerapplication_typeclient credentialsopenauthorization URLcode_challenge,resource, scopeauthorization requestredirect with code + isscallbackwith code + isstoken requestcode_verifier,resourceaccess token+ refresh tokenMCP request+ AuthorizationBearer tokenMCP response

What your server must do

Duty 1: publish Protected Resource Metadata

MCP servers MUST implement OAuth 2.0 Protected Resource Metadata (RFC 9728), and clients MUST use it to discover the authorization server. The document names your authorization server or servers and your scopes_supported. Keep that list to the minimal set for basic functionality: more scopes are requested later, through step-up authorization.

Duty 2: challenge correctly with a 401

A request without a valid token gets a 401 whose WWW-Authenticate header points to that document:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"

Servers SHOULD include scope in the challenge. Clients treat the challenged scopes as authoritative for the current operation, and those scopes may differ from scopes_supported in any way.

Duty 3: validate every token, including its audience

Clients MUST send Authorization: Bearer <token> on every HTTP request, and never in the query string. Servers MUST:

  • validate tokens as OAuth 2.1 section 5.2 describes;
  • check that the token was issued specifically for this server as its audience (RFC 8707);
  • return 401 for an invalid or expired token.

Duty 4: never pass tokens through

Servers MUST only accept tokens valid for their own resources, and MUST NOT accept or transit any other tokens. If your server calls downstream APIs, get a separate token for them, for example through an on-behalf-of or token-exchange flow. Never forward the client’s token.

The resource-server half in Python

The official Python software development kit (SDK), mcp 2.3.0, has most of the first three duties built in. You supply a TokenVerifier and AuthSettings; MCPServer adds the 401, the metadata route and the audience check. My verifier is a labelled stand-in:

from mcp.server import MCPServer
from mcp.server.auth.middleware.auth_context import get_access_token
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings

RESOURCE = "https://mcp.example.com/mcp"
# The audience of each demo token. The strings are labels, not credentials.
DEMO_TOKENS = {
    "demo-token-for-this-server": RESOURCE,
    "demo-token-for-another-api": "https://api.example.com",
}


class StandInVerifier(TokenVerifier):
    """Stands in for a real verifier, which checks the signature, issuer and expiry."""

    async def verify_token(self, token: str) -> AccessToken | None:
        if token not in DEMO_TOKENS:
            return None
        return AccessToken(token=token, client_id="demo-client", scopes=["files:read"],
                           resource=DEMO_TOKENS[token])  # the audience it was issued for


mcp = MCPServer(
    "files",
    token_verifier=StandInVerifier(),
    auth=AuthSettings(
        issuer_url="https://auth.example.com",
        resource_server_url=RESOURCE,
        required_scopes=["files:read"],
        validate_token_resource=True,  # refuse tokens issued for another resource
    ),
)


@mcp.tool()
def list_files() -> str:
    """List the files the caller may read."""
    token = get_access_token()  # the token the middleware validated
    return f"{token.client_id} may read: report.pdf, notes.txt"

In the SDK, authorization applies only over HTTP (stdio and the in-memory test client skip it), so I served it over Streamable HTTP inside one Python process, with no network. A request without a token got this:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"

{"error": "invalid_token", "error_description": "Authentication required"}

And the metadata, at the address in the challenge:

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": [
    "https://auth.example.com"
  ],
  "scopes_supported": [
    "files:read"
  ],
  "bearer_methods_supported": [
    "header"
  ]
}

What the runs showed:

  • Metadata. The SDK serves it at the well-known path with the endpoint’s path, /mcp, appended, and builds it from AuthSettings: required_scopes becomes scopes_supported.
  • Challenge. It points to the metadata but carries no scope, so the SDK doesn’t meet the SHOULD of duty 2 on its own.
  • Audience. The token my verifier marked as issued for https://api.example.com got the same 401, and the server logged why. Left unset, validate_token_resource makes 2.3.0 warn and skip the check; the setting’s description says 3.0 makes it the default, and that you set it to False when your verifier checks the token’s audience itself.
  • Header only. A token in the query string got a 401 too, because the SDK reads only the Authorization header.
  • Expiry. The middleware also refuses a token whose expires_at has passed.
  • Success. The SDK client with the right token negotiated 2026-07-28, and the tool answered demo-client may read: report.pdf, notes.txt.

Duty 4 is up to your own code. My advice: get_access_token() hands your tool the client’s token, and that’s where it should stay.

Method and caveats

  • Built from my guide’s chapter on authorization, written against the 2026-07-28 specification.
  • The Python sample ran against mcp 2.3.0 on 9 October 2026, served by the SDK’s own HTTP app inside one process, and the responses shown are what it returned. The notes on expiry, on the unset setting and on the in-memory client come from the package’s source and short probes.
  • This part covers the server’s first four duties. Step-up with 403, the status codes, the changes for clients and authorization servers, and authorization without sessions come next.