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.
When to Use
Section titled “When to Use”- Building an MCP server that requires user authentication
- Adding delegated access to external APIs (GitHub, Google, etc.) from MCP tools
- Serving OAuth
.well-knownmetadata for MCP clients
Installation
Section titled “Installation”# Standard MCP SDKpip install keycardai-mcp
# FastMCP framework (see FastMCP section below)pip install keycardai-mcp-fastmcpnpm install @keycardai/mcpgo get github.com/keycardai/credentials-goKey Exports
Section titled “Key Exports”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 |
@keycardai/mcp
| Export | Description |
|---|---|
AuthProvider | Core auth provider with grant() middleware |
AccessContext | Token exchange result. Check errors, access tokens per resource |
mcpAuthMetadataRouter | Express router for OAuth .well-known endpoints |
requireBearerAuth | Express middleware for bearer token verification |
ClientSecret | Application credential using client ID + secret |
WebIdentity | Application credential using private key JWT |
EKSWorkloadIdentity | Application credential for EKS environments |
credentials-go/mcp. Composable HTTP middleware for MCP server auth
| Export | Description |
|---|---|
AuthProvider | Core auth provider with Grant() middleware |
AccessContext | Token exchange result. Check errors, access tokens per resource |
AuthMetadataHandler | http.Handler for OAuth .well-known endpoints |
RequireBearerAuth | http.Handler middleware for bearer token verification |
AuthInfoFromContext | Retrieve auth info from context.Context (for tool handlers) |
AccessContextFromContext | Retrieve access context from context.Context (for tool handlers) |
ClientSecretCredential | Application credential using client ID + secret (NewClientSecret) |
WebIdentityCredential | Application credential using private key JWT (NewWebIdentity) |
EKSWorkloadIdentity | Application credential for EKS environments |
Quickstart
Section titled “Quickstart”Hello World: Authenticated MCP Server
Section titled “Hello World: Authenticated MCP Server”from mcp.server.fastmcp import FastMCPfrom keycardai.mcp.server.auth import AuthProvider
mcp = FastMCP("Hello World Server")auth_provider = AuthProvider( zone_id="your-zone-id", mcp_server_name="Hello World Server", mcp_server_url="http://localhost:8000/",)
@mcp.tool()async def hello() -> str: return "Hello from a protected MCP server!"
# Wrap the MCP app with authenticationapp = auth_provider.app(mcp)import express from "express";import { mcpAuthMetadataRouter } from "@keycardai/mcp/server/auth/router";import { requireBearerAuth } from "@keycardai/mcp/server/auth/middleware/bearerAuth";
const ZONE_URL = process.env.KEYCARD_ZONE_URL!;const app = express();
// Serve OAuth metadataapp.use( mcpAuthMetadataRouter({ oauthMetadata: { issuer: ZONE_URL }, scopesSupported: ["mcp:tools"], resourceName: "Hello World MCP Server", }),);
// Protect routes with bearer token verification. `issuers` is required.// It pins the verifier to your zone so forged tokens from any other// issuer are rejected before any JWKS lookup.app.use( "/api", requireBearerAuth({ issuers: ZONE_URL, requiredScopes: ["mcp:tools"], }),);
app.get("/api/whoami", (req, res) => { res.json({ message: "Hello from a protected MCP server!" });});
app.listen(8000);package main
import ( "context" "fmt" "log" "os"
"github.com/keycardai/credentials-go/mcp" mcpgo "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server")
func main() { zoneURL := os.Getenv("KEYCARD_ZONE_URL")
// Create MCP server with your preferred library s := server.NewMCPServer("Hello World Server", "1.0.0") s.AddTool( mcpgo.NewTool("hello", mcpgo.WithDescription("Say hello."), mcpgo.WithString("name", mcpgo.Required()), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { name, _ := req.GetArguments()["name"].(string) authInfo := mcp.AuthInfoFromContext(ctx) return mcpgo.NewToolResultText( fmt.Sprintf("Hello, %s! Client: %s", name, authInfo.ClientID), ), nil }, )
// Mount Keycard auth on your own mux httpMux := http.NewServeMux() httpMux.Handle("/.well-known/", mcp.AuthMetadataHandler( mcp.WithIssuer(zoneURL), mcp.WithScopesSupported([]string{"mcp:tools"}), mcp.WithResourceName("Hello World Server"), )) httpMux.Handle("/mcp", mcp.RequireBearerAuth( mcp.WithRequiredScopes("mcp:tools"), )(server.NewStreamableHTTPServer(s)))
log.Fatal(http.ListenAndServe(":8080", httpMux))}Delegated Access: Call External APIs
Section titled “Delegated Access: Call External APIs”import httpxfrom mcp.server.fastmcp import FastMCPfrom keycardai.mcp.server.auth import AuthProvider, AccessContext, ClientSecret
auth_provider = AuthProvider( zone_id="your-zone-id", mcp_server_name="GitHub Server", mcp_server_url="http://localhost:8000/", application_credential=ClientSecret(("client_id", "client_secret")),)
mcp = FastMCP("GitHub Server")
@mcp.tool()@auth_provider.grant("https://api.github.com")async def get_repos(ctx) -> 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 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)import express from "express";import { AuthProvider } from "@keycardai/mcp/server/auth/provider";import { ClientSecret } from "@keycardai/mcp/server/auth/credentials";import type { DelegatedRequest } from "@keycardai/mcp/server/auth/provider";
const authProvider = new AuthProvider({ zoneUrl: process.env.KEYCARD_ZONE_URL!, applicationCredential: new ClientSecret( process.env.KEYCARD_CLIENT_ID!, process.env.KEYCARD_CLIENT_SECRET!, ),});
const app = express();
app.get( "/api/repos", authProvider.grant("https://api.github.com"), async (req, res) => { const { accessContext } = req as DelegatedRequest;
if (accessContext.hasErrors()) { res.status(502).json({ error: accessContext.getErrors() }); return; }
const token = accessContext.access("https://api.github.com").accessToken; const resp = await fetch("https://api.github.com/user/repos", { headers: { Authorization: `Bearer ${token}` }, }); res.json(await resp.json()); },);
app.listen(8000);package main
import ( "context" "io" "log" "net/http" "os"
"github.com/keycardai/credentials-go/mcp" mcpgo "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server")
func main() { s := server.NewMCPServer("GitHub Server", "1.0.0") s.AddTool( mcpgo.NewTool("get_repos", mcpgo.WithDescription("List user repos")), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { ac := mcp.AccessContextFromContext(ctx) if ac.HasErrors() { return mcpgo.NewToolResultError("Token exchange failed"), nil }
token, _ := ac.Access("https://api.github.com") ghReq, _ := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user/repos", nil) ghReq.Header.Set("Authorization", "Bearer "+token.AccessToken)
resp, _ := http.DefaultClient.Do(ghReq) defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return mcpgo.NewToolResultText(string(body)), nil }, )
// Set up auth provider for token exchange authProvider, _ := mcp.NewAuthProvider( mcp.WithZoneURL(os.Getenv("KEYCARD_ZONE_URL")), mcp.WithApplicationCredential(mcp.NewClientSecret( os.Getenv("KEYCARD_CLIENT_ID"), os.Getenv("KEYCARD_CLIENT_SECRET"), )), )
httpMux := http.NewServeMux() httpMux.Handle("/.well-known/", mcp.AuthMetadataHandler( mcp.WithIssuer(os.Getenv("KEYCARD_ZONE_URL")), mcp.WithScopesSupported([]string{"mcp:tools"}), mcp.WithResourceName("GitHub Server"), )) httpMux.Handle("/mcp", mcp.RequireBearerAuth( mcp.WithRequiredScopes("mcp:tools"), )(authProvider.Grant("https://api.github.com")( server.NewStreamableHTTPServer(s), )))
log.Fatal(http.ListenAndServe(":8080", httpMux))}FastMCP Integration
Section titled “FastMCP Integration”The keycardai-mcp-fastmcp package provides a dedicated integration for Python’s FastMCP framework. It wraps the same auth primitives with FastMCP-specific APIs.
pip install keycardai-mcp-fastmcpKey Differences from keycardai-mcp
Section titled “Key Differences from keycardai-mcp”| Feature | keycardai-mcp | keycardai-mcp-fastmcp |
|---|---|---|
| Framework | Standard MCP SDK | FastMCP 3.x |
| Auth setup | auth_provider.app(mcp) | auth_provider.get_remote_auth_provider() |
| Grant decorator | @auth_provider.grant(resource) | Same |
| Access context | await ctx.get_state("keycardai") | Same |
| Testing | N/A | mock_access_context test utility |
FastMCP Example
Section titled “FastMCP Example”from fastmcp import Context, FastMCPfrom keycardai.mcp.integrations.fastmcp import AuthProvider, ClientSecret, AccessContext
auth_provider = AuthProvider( zone_id="your-zone-id", mcp_server_name="GitHub API Server", mcp_base_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...Related
Section titled “Related”SEE ALSO