Skip to content
API Reference

MCP

OAuth authentication for MCP servers. Bearer middleware, metadata endpoints, and grant decorators.

The MCP package adds OAuth-based authentication to your MCP server. It handles bearer token verification, serves OAuth metadata endpoints, and provides grant decorators for delegated access to external APIs.

  • Building an MCP server that requires user authentication
  • Adding delegated access to external APIs (GitHub, Google, etc.) from MCP tools
  • Serving OAuth .well-known metadata for MCP clients
Terminal window
# Standard MCP SDK
pip install keycardai-mcp
# FastMCP framework (see FastMCP section below)
pip install keycardai-fastmcp

keycardai.mcp.server.auth

Export Description
AuthProvider Core auth provider. Wraps your MCP app with OAuth middleware
AccessContext Token exchange result. Check errors, access tokens per resource
TokenVerifier Verify incoming bearer tokens
ClientSecret Application credential using client ID + secret
WebIdentity Application credential using private key JWT
EKSWorkloadIdentity Application credential for EKS environments
from mcp.server.mcpserver import MCPServer
from keycardai.mcp.server.auth import AuthProvider
mcp = MCPServer("Hello World Server")
auth_provider = AuthProvider(
zone_id="<zone-id>",
mcp_server_name="Hello World Server",
mcp_server_url="http://localhost:8000/",
audience="http://localhost:8000/mcp",
)
@mcp.tool()
async def hello() -> str:
return "Hello from a protected MCP server!"
# Wrap the MCP app with authentication
app = auth_provider.app(mcp)
import httpx
from mcp.server.mcpserver import Context, MCPServer
from keycardai.mcp.server.auth import AccessContext, AuthProvider, ClientSecret
auth_provider = AuthProvider(
zone_id="<zone-id>",
mcp_server_name="GitHub Server",
mcp_server_url="http://localhost:8000/",
audience="http://localhost:8000/mcp",
application_credential=ClientSecret(("client_id", "client_secret")),
)
mcp = MCPServer("GitHub Server")
@mcp.tool()
@auth_provider.grant("https://api.github.com")
async def get_repos(access_ctx: AccessContext, ctx: Context) -> dict:
if access_ctx.has_errors():
return {"error": access_ctx.get_errors()}
token = access_ctx.access("https://api.github.com").access_token
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://api.github.com/user/repos",
headers={"Authorization": f"Bearer {token}"},
)
return resp.json()
app = auth_provider.app(mcp)

@grant injects the AccessContext parameter and strips it from the tool schema, so the model never sees it. The decorated function needs both an AccessContext-annotated and a Context-annotated parameter.

By default, grant() exchanges the caller’s bearer token — the tool acts as whoever is calling. When the server must act as a named user instead — a scheduled job, a queue worker, or a tool that operates on another user’s data — supply a user-identifier resolver and the grant impersonates that user via a substitute-user token exchange. The server’s own application credential authenticates the exchange; no user token is needed. See Act on Behalf of Absent Users for the consent setup and policy requirements.

@mcp.tool()
@auth_provider.grant(
"https://api.github.com",
user_identifier=lambda **kwargs: kwargs["user_email"],
)
async def get_repos(access_ctx: AccessContext, ctx: Context, user_email: str) -> dict:
if access_ctx.has_errors():
return {"error": access_ctx.get_errors()}
token = access_ctx.access("https://api.github.com").access_token
# Call the API as user_email...

The user_identifier callable receives the tool’s keyword arguments and returns the identifier string.

The keycardai-fastmcp package provides a dedicated integration for Python’s FastMCP framework. It wraps the same auth primitives with FastMCP-specific APIs.

Terminal window
pip install keycardai-fastmcp

Upgrading a server from FastMCP 2.x? Migrate to FastMCP 3.0 covers the package rename and the await ctx.get_state() change, tool by tool.

Feature keycardai-mcp keycardai-fastmcp
Framework Standard MCP SDK (mcp 2.x) FastMCP 3.x (mcp 1.x)
Auth setup auth_provider.app(mcp) auth_provider.get_remote_auth_provider()
Grant decorator @auth_provider.grant(resource) Same
Access context Injected AccessContext parameter await ctx.get_state("keycardai")
Testing N/A mock_access_context test utility

The two packages track different upstream majors on purpose: keycardai-mcp follows the standard MCP SDK 2.x line, while keycardai-fastmcp follows FastMCP 3.x, which still builds on mcp 1.x. Pick the one that matches your framework; neither replaces the other.

from fastmcp import Context, FastMCP
from keycardai.fastmcp import AuthProvider, ClientSecret, AccessContext
auth_provider = AuthProvider(
zone_id="<zone-id>",
mcp_server_name="GitHub API Server",
mcp_server_url="http://localhost:8000/",
application_credential=ClientSecret(("client_id", "client_secret")),
)
auth = auth_provider.get_remote_auth_provider()
mcp = FastMCP("GitHub API Server", auth=auth)
@mcp.tool()
@auth_provider.grant("https://api.github.com")
async def get_github_user(ctx: Context) -> dict:
access_context: AccessContext = await ctx.get_state("keycardai")
if access_context.has_errors():
return {"error": access_context.get_errors()}
token = access_context.access("https://api.github.com").access_token
# Use token to call GitHub API...