Tutorial 6 min read

How to Add Authentication to an MCP Server

How to add authentication to an MCP server: your HTTP server is an OAuth 2.1 resource server. It verifies a bearer token, checks the audience, and answers 401. Stdio does not use this flow.

Tayyab JavedFreelance AI agent developer
How to Add Authentication to an MCP Server
On this page 6 sections
  1. What the server has to verify
  2. Python bearer gate
  3. TypeScript bearer gate
  4. What to leave out of the tool
  5. Frequently Asked Questions
  6. Conclusion

Add authentication to an MCP server by treating the HTTP endpoint as an OAuth 2.1 resource server. On every request, read Authorization: Bearer, check that the token was issued for this server, and answer 401 when it was not. The MCP server does not sign the user in and does not mint the token. A stdio server skips this flow and reads secrets from the environment.

By Tayyab Javed, AI agent developer.

Quick answer

  • Spec revision 2026-07-28. HTTP authorization is optional. When you turn it on, follow this spec. Stdio should not.
  • Your process is the resource server. Auth0, Keycloak, Entra, or your own issuer is the authorization server.
  • A missing or bad token is 401 with a WWW-Authenticate header that points at the RFC 9728 metadata document. A token that lacks a required scope is 403.
  • Refuse a token whose audience is a different URL. That check is RFC 8707. Clients send the resource parameter when they ask for the token.

What the server has to verify

The client sends the access token in the Authorization header on every HTTP request, never in the query string. Your verifier reports client id, scopes, expiry, and resource (the aud claim). The SDK rejects a missing expiry, an expired token, a foreign resource, and a missing scope. I ran the samples on October 12, 2026 against Python mcp 2.3.0 and @modelcontextprotocol/server 2.3.1. Spec: authorization.

Python bearer gate

TokenVerifier is one async method. validate_token_resource=True turns on the audience check. The token below is a stand-in for a JWT or an RFC 7662 introspection call. Both still return an AccessToken.

Python
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

RESOURCE = "http://127.0.0.1:8000/mcp"

class StaticTokenVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        if token != "alice-token":
            return None
        return AccessToken(
            token=token,
            client_id="alice",
            scopes=["notes:read"],
            resource=RESOURCE,
            expires_at=2_000_000_000,
        )

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

@mcp.tool()
def whoami() -> str:
    """Report which OAuth client is calling."""
    token = get_access_token()
    if token is None:
        return "anonymous"
    return token.client_id + " [" + ", ".join(token.scopes) + "]"

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

I served this app and called whoami. No header: 401, with WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", resource_metadata="http://127.0.0.1:8765/.well-known/oauth-protected-resource/mcp". Bearer alice-token returned alice [notes:read]. A token whose resource was https://other.example/mcp was 401, and the tool did not run. GET /.well-known/oauth-protected-resource/mcp returned the resource, authorization_servers, and scopes_supported. Pass token_verifier and auth together or the constructor raises. get_access_token() is None on stdio.

TypeScript bearer gate

createMcpHandler does not read the Authorization header. authInfo is pass-through. requireBearerAuth is the gate. If it returns a Response, send that. Otherwise pass authInfo into handler.fetch.

TypeScript
import {
    createMcpHandler,
    getOAuthProtectedResourceMetadataUrl,
    McpServer,
    OAuthError,
    OAuthErrorCode,
    requireBearerAuth
} from '@modelcontextprotocol/server';

const mcpServerUrl = new URL('https://mcp.example.com/mcp');

const verifier = {
    async verifyAccessToken(token) {
        if (token !== 'alice-token') {
            throw new OAuthError(OAuthErrorCode.InvalidToken, 'Unknown token');
        }
        return {
            token,
            clientId: 'alice',
            scopes: ['notes:read'],
            expiresAt: Math.floor(Date.now() / 1000) + 3600,
            resource: new URL(mcpServerUrl.href)
        };
    }
};

const gate = requireBearerAuth({
    verifier,
    requiredScopes: ['notes:read'],
    resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
    expectedResource: mcpServerUrl
});

const handler = createMcpHandler(({ authInfo }) => {
    const server = new McpServer({ name: 'notes', version: '1.0.0' });
    server.registerTool(
        'whoami',
        { description: 'Name the authenticated caller' },
        async () => ({
            content: [{ type: 'text', text: (authInfo && authInfo.clientId) || 'anonymous' }]
        })
    );
    return server;
});

export default {
    async fetch(request) {
        const auth = await gate(request);
        if (auth instanceof Response) return auth;
        return handler.fetch(request, { authInfo: auth });
    }
};

On 2.3.1 the same gate answered 401 Missing Authorization header, 401 Unknown token, 401 Token was not issued for this resource for a different audience, and 403 insufficient_scope when notes:read was missing. Bearer alice-token reached the tool, which returned the text alice. Throw OAuthError for a bad token. Any other exception becomes HTTP 500. Set expiresAt, or the gate answers 401. Express users: @modelcontextprotocol/express was 2.0.2 that day, not 2.3.1, and its middleware does forward expectedResource. Mount it on /mcp. Handlers then read ctx.http.authInfo.

What to leave out of the tool

Keep secrets out of the tool description. The model reads that text. Leave login and token minting on the authorization server. The Python SDK still accepts auth_server_provider, and the current docs tell new servers to skip it. Old TypeScript helpers that issue tokens live in @modelcontextprotocol/server-legacy/auth. Rate limits are still your code. The spec does not add them.

The server under this gate is how to build an MCP server. The public URL is how to deploy an MCP server. The rest of the failure list is the MCP production guide.

Frequently Asked Questions

Does MCP authentication use OAuth?

On HTTP, yes. Revision 2026-07-28 uses OAuth 2.1 bearer tokens. The MCP server verifies them. A separate authorization server issues them.

Does a stdio MCP server need OAuth?

No. The spec keeps stdio off this HTTP flow. The process that launched the server is the boundary, and credentials come from the environment.

Where does the MCP access token go?

In the Authorization header on every HTTP request. A missing, expired, or wrong-audience token gets 401. A token missing a required scope gets 403.

Conclusion

Publish the metadata document, verify the bearer token, and refuse every token that names a different resource. Then rate-limit the same route. If you want that gate in front of tools that write to a real system, hire me and we will sketch the issuer, the scopes, and which calls need a person.

Hire Tayyab to put auth on the server

Free 30-minute scoping call. We will name the issuer, the audience URL, and the scopes each tool requires.

Book a Scoping Call
Share
Tayyab Javed
About the author

Tayyab Javed

Tayyab is a freelance AI agent developer and founder of Workly. He does research, spec, architecture, UX, and the build — solo, no handoff failures. Ex-Principal PM behind a Fortune 500 AI contact center (40% CSAT lift). He helps founders and SMBs ship production-grade agentic systems end to end.