Skip to main content
MCP SDKs support OAuth 2.1, but implementing it fully involves auth servers, resource servers, authorization codes, and token exchanges. This lesson walks you through basic auth first — a simpler foundation you can harden progressively using the patterns in Module 2: Security and Advanced Topics: OAuth2.

What is auth?

Auth covers two things:
TermDefinitionExample
AuthenticationVerify the caller is who they claim to beValid API key or credentials
AuthorizationVerify the caller can access the requested resourceRead-only vs. admin access (RBAC)

Basic authentication flow

The simplest approach is to require an Authorization header on every request and validate it in server middleware:
User → Client → [Authorization: secret123] → Server middleware → MCP tools

                                        401 Unauthorized (missing header)
                                        403 Forbidden (invalid token)

Implementing auth middleware

Add Starlette middleware that validates the Authorization header before the request reaches MCP handlers:
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

VALID_TOKENS = {"secret123", "another-token"}

def valid_token(auth_header: str) -> bool:
    return auth_header in VALID_TOKENS

class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        has_header = request.headers.get("Authorization")
        if not has_header:
            print("-> Missing Authorization header!")
            return Response(status_code=401, content="Unauthorized")

        if not valid_token(has_header):
            print("-> Invalid token!")
            return Response(status_code=403, content="Forbidden")

        print("Valid token, proceeding...")
        response = await call_next(request)
        return response

# Apply middleware to the Starlette app wrapping your MCP server
starlette_app.add_middleware(AuthMiddleware)

Creating the server and Starlette app

from mcp.server.fastmcp import FastMCP
import uvicorn

app = FastMCP(
    name="MCP Resource Server",
    host="0.0.0.0",
    port=8080,
)

# Wrap in Starlette for middleware support
starlette_app = app.streamable_http_app()
starlette_app.add_middleware(AuthMiddleware)

if __name__ == "__main__":
    uvicorn.run(starlette_app, host="0.0.0.0", port=8080)

Making an authenticated client request

Once your server requires auth, clients must include the Authorization header:
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
    async with streamablehttp_client(
        "http://localhost:8080/mcp",
        headers={"Authorization": "secret123"}
    ) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print(f"Available tools: {[t.name for t in tools.tools]}")

Role-Based Access Control (RBAC)

After verifying identity, you can also check permissions per route or tool:
ROLES = {
    "admin-token": ["read", "write", "delete"],
    "reader-token": ["read"],
}

def has_permission(token: str, action: str) -> bool:
    return action in ROLES.get(token, [])

class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        token = request.headers.get("Authorization", "")
        if not token:
            return Response(status_code=401, content="Unauthorized")

        # For write operations, require write permission
        if request.method == "POST" and not has_permission(token, "write"):
            return Response(status_code=403, content="Insufficient permissions")

        return await call_next(request)
Basic auth with a static secret is suitable for development and internal tools. For production, implement OAuth 2.1 or Microsoft Entra ID authentication as covered in the Advanced Topics section.

Key takeaways

  • Add a middleware layer that validates the Authorization header before requests reach MCP handlers.
  • Return 401 Unauthorized for missing tokens, 403 Forbidden for invalid ones.
  • RBAC maps tokens to permission sets and checks per action.
  • Upgrade to OAuth 2.1 for production — this basic pattern is a stepping stone, not a final destination.

Build docs developers (and LLMs) love