
Services
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.

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.
WWW-Authenticate header that points at the RFC 9728 metadata document. A token that lacks a required scope is 403.resource parameter when they ask for the token.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.
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.
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.
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.
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.
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.
On HTTP, yes. Revision 2026-07-28 uses OAuth 2.1 bearer tokens. The MCP server verifies them. A separate authorization server issues them.
No. The spec keeps stdio off this HTTP flow. The process that launched the server is the boundary, and credentials come from the environment.
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.
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.
Free 30-minute scoping call. We will name the issuer, the audience URL, and the scopes each tool requires.
Book a Scoping CallTayyab 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.