Skip to content

Authorization

Over Streamable HTTP your MCP server is an ordinary web service, and you protect it the way you protect any web service: with OAuth 2.1 bearer tokens.

In OAuth terms, your server is a resource server. It never signs anyone in and it never issues a token. It does one thing: look at the Authorization header on each request and decide whether the token in it is good.

The three parties

  • The authorization server signs people in and issues access tokens. You don't write this. It's your identity provider (Auth0, Keycloak, Entra, your own).
  • The resource server is your MCP server. It verifies the token on every request.
  • The client discovers which authorization server you trust, gets a token from it, and sends it back to you as Authorization: Bearer <token>.

That's the whole triangle. Everything on this page is the middle bullet.

A token verifier

The SDK has no opinion about what a valid token looks like. You tell it, by implementing TokenVerifier:

server.py
from pydantic import AnyHttpUrl

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

KNOWN_TOKENS = {
    "alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"]),
}


class StaticTokenVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        return KNOWN_TOKENS.get(token)


mcp = MCPServer(
    "Notes",
    token_verifier=StaticTokenVerifier(),
    auth=AuthSettings(
        issuer_url=AnyHttpUrl("https://auth.example.com"),
        resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
        required_scopes=["notes:read"],
    ),
)


@mcp.tool()
def list_notes() -> list[str]:
    """List every note in the notebook."""
    return ["Buy milk", "Ship the release"]
  • TokenVerifier is a protocol with one async method. verify_token gets the raw token from the Authorization header and returns an AccessToken if it's valid, None if it isn't. There is nothing else to implement.
  • This one looks the token up in a table. A real one verifies a JWT signature or calls the authorization server's token-introspection endpoint. That code is yours; the SDK only calls it.
  • token_verifier= and auth= always travel together. Pass one without the other and MCPServer(...) raises a ValueError before it ever serves a request.

AuthSettings is the public face of your resource server:

  • issuer_url: the authorization server that issues your tokens.
  • resource_server_url: the public URL of this MCP endpoint. It names which resource a token is for, and it's where the discovery document lives.
  • required_scopes: every token must carry all of them.

Tip

examples/servers/simple-auth/ in the SDK repository has an IntrospectionTokenVerifier that calls a real authorization server's RFC 7662 endpoint. It's the shape most production verifiers take.

What you get over HTTP

Authorization lives in HTTP headers, so it exists only on the HTTP transports. Run it on the one you deploy: mcp.run(transport="streamable-http") puts it on http://127.0.0.1:8000/mcp, and Running your server has the rest. The app now has two routes:

/mcp
/.well-known/oauth-protected-resource/mcp

You registered one tool. The second route is the SDK's.

Discovery

GET that well-known path and you get RFC 9728 Protected Resource Metadata, built straight from your AuthSettings:

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

This document is how a client that has never heard of your server finds its way in: it reads authorization_servers and goes there for a token. You wrote none of it.

Check

Call /mcp with no token (or with one your verifier returned None for) and the request is stopped at the door:

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

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

Nothing was parsed and no tool ran. And that resource_metadata pointer in WWW-Authenticate is what makes discovery automatic: 401 -> metadata document -> authorization server -> token -> retry.

Warning

None of this protects stdio. A pipe has no Authorization header, so token_verifier is never consulted there. A stdio server's security boundary is the process that launched it. The same goes for the in-memory Client(mcp) you use in tests: it connects straight to the server object and skips the HTTP layer, authorization included.

The caller's identity

Inside any handler, get_access_token() is the AccessToken your verifier returned for the current request:

server.py
from pydantic import AnyHttpUrl

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

KNOWN_TOKENS = {
    "alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"]),
}


class StaticTokenVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        return KNOWN_TOKENS.get(token)


mcp = MCPServer(
    "Notes",
    token_verifier=StaticTokenVerifier(),
    auth=AuthSettings(
        issuer_url=AnyHttpUrl("https://auth.example.com"),
        resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"),
        required_scopes=["notes:read"],
    ),
)


@mcp.tool()
def whoami() -> str:
    """Report which OAuth client is calling."""
    token = get_access_token()
    if token is None:
        return "anonymous"
    return f"{token.client_id} (scopes: {', '.join(token.scopes)})"
  • It works in tools, resources, and prompts, and there is nothing to pass around: the auth middleware stores it in a context variable per request.
  • You get back the same object your verifier built: client_id, scopes, subject, expires_at, and any extra claims you attached. That's the hook for per-tool rules: read the scopes and refuse.
  • Outside an authenticated HTTP request it returns None. In-memory and over stdio it is always None.

Call whoami with Authorization: Bearer alice-token and the model reads:

alice (scopes: notes:read)

The half the SDK doesn't do

The SDK gives you the resource-server half: verify, advertise, refuse. It does not give you a login page, a consent screen, or a token.

To watch all three parties move, run examples/servers/simple-auth/ from the SDK repository (a small authorization server and a resource server set up exactly like this page) and then point examples/clients/simple-auth-client/ at it for the full discovery-and-token dance.

Info

There is a second constructor argument, auth_server_provider=, that embeds a full authorization server inside your MCP server. It predates the AS/RS separation that the MCP authorization spec is built around. New servers should not reach for it.

An authorization server can also accept an enterprise identity provider's signed assertion in place of a user clicking through a consent screen, and the SDK supports both sides of that exchange. The grant, and the client that presents it, is Identity assertion.

Recap

  • Over Streamable HTTP your server is an OAuth 2.1 resource server: it verifies tokens, it never issues them.
  • TokenVerifier is the whole integration surface: one async method, token in, AccessToken | None out.
  • token_verifier= and auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...]) always travel together.
  • The SDK publishes RFC 9728 Protected Resource Metadata at /.well-known/oauth-protected-resource/... and answers unauthenticated requests with a 401 whose WWW-Authenticate header points at it. That is the entire discovery story.
  • get_access_token() in any handler is who's calling.
  • Authorization is an HTTP concern. stdio and the in-memory client never see it.

The other side of the handshake, a client that discovers your authorization server and fetches the token for you, is OAuth clients.