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-go/mcpKey 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
| 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 |
ClientSecretCredential | Application credential using client ID + secret (NewClientSecret) |
WebIdentityCredential | Application credential using private key JWT (NewWebIdentity) |
EKSWorkloadIdentity | Application credential for EKS environments |
Quick Start
Section titled “Quick Start”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 verificationapp.use("/api", requireBearerAuth({ requiredScopes: ["mcp:tools"] }));
app.get("/api/whoami", (req, res) => { res.json({ message: "Hello from a protected MCP server!" });});
app.listen(8000);package main
import ( "encoding/json" "log" "net/http" "os"
"github.com/keycardai/credentials-go/mcp")
func main() { zoneURL := os.Getenv("KEYCARD_ZONE_URL")
mux := http.NewServeMux()
// Serve OAuth metadata endpoints mux.Handle("/.well-known/", mcp.AuthMetadataHandler( mcp.WithIssuer(zoneURL), mcp.WithScopesSupported([]string{"mcp:tools"}), mcp.WithResourceName("Hello World MCP Server"), ))
// Protected endpoint with bearer auth protected := mcp.RequireBearerAuth( mcp.WithRequiredScopes("mcp:tools"), )(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authInfo := mcp.AuthInfoFromRequest(r) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "message": "Hello from a protected MCP server!", "client_id": authInfo.ClientID, }) }))
mux.Handle("GET /api/hello", protected) log.Fatal(http.ListenAndServe(":8080", mux))}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 = 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 ( "fmt" "io" "log" "net/http" "os"
"github.com/keycardai/credentials-go/mcp")
func main() { zoneURL := os.Getenv("KEYCARD_ZONE_URL")
authProvider, err := mcp.NewAuthProvider( mcp.WithZoneURL(zoneURL), mcp.WithApplicationCredential(mcp.NewClientSecret( os.Getenv("KEYCARD_CLIENT_ID"), os.Getenv("KEYCARD_CLIENT_SECRET"), )), ) if err != nil { log.Fatal(err) }
mux := http.NewServeMux()
// Chain: bearer auth -> grant -> handler apiHandler := mcp.RequireBearerAuth( mcp.WithRequiredScopes("mcp:tools"), )(authProvider.Grant("https://api.github.com")( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ac := mcp.AccessContextFromRequest(r)
if ac.HasErrors() { http.Error(w, "token exchange failed", http.StatusBadGateway) return }
token, _ := ac.Access("https://api.github.com") req, _ := http.NewRequestWithContext( r.Context(), "GET", "https://api.github.com/user/repos", nil, ) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
resp, err := http.DefaultClient.Do(req) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } defer resp.Body.Close()
w.Header().Set("Content-Type", "application/json") io.Copy(w, resp.Body) }), ))
mux.Handle("GET /api/repos", apiHandler) log.Fatal(http.ListenAndServe(":8080", mux))}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 2.x |
| Auth setup | auth_provider.app(mcp) | auth_provider.get_remote_auth_provider() |
| Grant decorator | @auth_provider.grant(resource) | Same |
| Access context | ctx.get_state("keycardai") | Same |
| Testing | — | 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 = 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