Show HN: OAuth 2.0 framework for MCP servers A new open-source Python framework, mcp-authflow, provides an OAuth 2.0 authorization server for Model Context Protocol (MCP) servers, enabling token issuance and management to protect MCP tool access. The framework supports RFC 6749, RFC 7523, RFC 7636 PKCE, and RFC 8628 Device Authorization Grant, with PostgreSQL and in-memory token storage, sliding-window rate limiting, and CORS helpers. Developers can install the package via pip and build an authorization server that issues tokens for MCP clients, with endpoints for token issuance and introspection. OAuth 2.0 Authorization Server framework for MCP https://modelcontextprotocol.io/ servers. Issue and manage tokens that protect MCP tool access. Pair with mcp-authflow-resource https://github.com/brooksmcmillin/mcp-authflow-resource on the resource server side. Token storage with PostgreSQL and in-memory backends RFC 6749 standardized OAuth error responses RFC 7523 client authentication with algorithm allowlist and JTI replay protection Redis or in-memory private key jwt RFC 7636 PKCE verification S256 + plain and input validation for the token endpoint RFC 8628 Device Authorization Grant — sans-IO polling state machine and code generators Sliding-window rate limiting for token endpoints Input validation for client IDs and scopes CORS helpers with origin allowlisting Async-first design, built on Starlette pip install mcp-authflow With PostgreSQL token storage production pip install mcp-authflow postgres Build an OAuth authorization server that issues tokens for MCP clients: python import secrets import time from contextlib import asynccontextmanager from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import JSONResponse from starlette.routing import Route from mcp authflow.rate limiting import SlidingWindowRateLimiter from mcp authflow.responses import invalid request, rate limit exceeded from mcp authflow.storage import MemoryTokenStorage from mcp authflow.validation import parse scope field, validate client id --- Setup --- storage = MemoryTokenStorage Use PostgresTokenStorage for production limiter = SlidingWindowRateLimiter requests per window=60, window seconds=3600 --- Token endpoint --- async def token endpoint request: Request - JSONResponse: form = await request.form client id = str form.get "client id", "" Rate limit per client if not limiter.is allowed client id : return rate limit exceeded "Too many requests", retry after=limiter.get retry after client id , Validate client if not validate client id client id : return invalid request "Invalid client id format" Issue token token = secrets.token urlsafe 32 scopes = parse scope field form.get "scope" expires at = int time.time + 3600 await storage.store token token=token, client id=client id, scopes=scopes.split , expires at=expires at, resource=str form.get "resource", "" , return JSONResponse { "access token": token, "token type": "bearer", "expires in": 3600, "scope": scopes, } --- Introspection endpoint called by resource servers --- async def introspect endpoint request: Request - JSONResponse: form = await request.form token = str form.get "token", "" token data = await storage.load token token if not token data or token data "expires at" < time.time : return JSONResponse {"active": False} return JSONResponse { "active": True, "client id": token data "client id" , "scope": " ".join token data "scopes" , "exp": token data "expires at" , "aud": token data.get "resource", "" , } @asynccontextmanager async def lifespan app : await storage.initialize yield await storage.close app = Starlette routes= Route "/token", token endpoint, methods= "POST" , Route "/introspect", introspect endpoint, methods= "POST" , , lifespan=lifespan, Run with: uvicorn myapp:app --port 8000 MCP Client Claude, etc. | 1. Authorization request | v +---------------------+ | Auth Server | <-- this package | mcp-authflow | | | | /token | 2. Issues access token | /introspect | 4. Validates token +---------------------+ ^ | 4. Token introspection RFC 7662 | +---------------------+ | Resource Server | <-- mcp-authflow-resource | MCP tools | | | | 3. Client calls | | MCP tools with | | Bearer token | +---------------------+ - MCP client authenticates with the auth server - Auth server issues an access token stored in PostgreSQL or memory - Client calls MCP tools on the resource server with the Bearer token - Resource server validates the token by calling the auth server's /introspect endpoint Abstract base class with two implementations: python from mcp authflow.storage import MemoryTokenStorage, PostgresTokenStorage In-memory development/testing storage = MemoryTokenStorage PostgreSQL production -- requires postgres extra storage = PostgresTokenStorage database url="postgresql://user:pass@host/db" Or reads DATABASE URL env var if no argument provided storage = PostgresTokenStorage await storage.initialize Open the connection pool in-memory needs no setup PostgresTokenStorage does not create or migrate its schema — it expects the tables to already exist, so you stay in control of migrations. Apply this DDL e.g. via your migration tool before first use: CREATE TABLE IF NOT EXISTS mcp access tokens token TEXT PRIMARY KEY, client id TEXT NOT NULL, scopes TEXT NOT NULL DEFAULT '', resource TEXT, expires at TIMESTAMPTZ NOT NULL, created at TIMESTAMPTZ NOT NULL DEFAULT now , user id INTEGER ; -- Only needed if you use the refresh-token methods. CREATE TABLE IF NOT EXISTS mcp refresh tokens token TEXT PRIMARY KEY, client id TEXT NOT NULL, scopes TEXT NOT NULL DEFAULT '', resource TEXT, expires at TIMESTAMPTZ NOT NULL, created at TIMESTAMPTZ NOT NULL DEFAULT now , user id INTEGER ; Storage interface: | Method | Description | |---|---| store token token, client id, scopes, expires at, resource?, user id? | Store an access token | load token token - dict | None | Look up a token | delete token token | Revoke a token | cleanup expired tokens - int | Purge expired tokens, returns count | get token count - int | Count active tokens | store refresh token ... | Store a refresh token same interface | load refresh token token - dict | None | Look up a refresh token | delete refresh token token | Revoke a refresh token | cleanup expired refresh tokens - int | Purge expired refresh tokens, returns count | Token data returned by load token : { "token": str, "client id": str, "scopes": list str , "resource": str | None, RFC 8707 resource binding "expires at": int, Unix timestamp "created at": int, Unix timestamp "user id": int | None, } Standardized error helpers following RFC 6749: from mcp authflow.responses import invalid request, 400 - Missing/invalid parameters invalid client, 401 - Authentication failure invalid grant, 400 - Expired/invalid code or token invalid scope, 400 - Scope violation slow down, 400 - Device flow rate limiting rate limit exceeded, 429 - Too many requests server error, 500 - Internal error backend timeout, 504 - Upstream timeout Each returns a Starlette JSONResponse with the appropriate status code and Cache-Control: no-store header. python from mcp authflow.rate limiting import SlidingWindowRateLimiter limiter = SlidingWindowRateLimiter requests per window=60, Max requests per window window seconds=3600, Window duration 1 hour if not limiter.is allowed client id : retry after = limiter.get retry after client id Seconds until next allowed request python from mcp authflow.validation import validate client id, parse scope field validate client id "my-client-123" True alphanumeric + hyphens/underscores validate client id "" False parse scope field "read write" "read write" parse scope field "read", "write" "read write" parse scope field None "read" default python from mcp authflow.cors import parse allowed origins, build cors headers Reads ALLOWED MCP ORIGINS env var comma-separated origins = parse allowed origins Returns CORS headers if request origin is in allowlist headers = build cors headers request, origins | Env Variable | Description | Default | |---|---|---| DATABASE URL | PostgreSQL connection string for PostgresTokenStorage | Required for postgres | ALLOWED MCP ORIGINS | Comma-separated allowed CORS origins | Empty no CORS | MIT