Build an MCP Server
Create an OAuth-protected MCP server with Keycard
MCP servers expose tools to AI agents — but without authentication, any agent can call any tool with no accountability. Keycard adds OAuth-based authentication to your MCP server so that every tool call is tied to a verified user, scoped to explicit permissions, and logged for audit.
This guide walks through building a protected MCP server from scratch — first a hello world, then adding delegated access to external APIs — with examples in Python, TypeScript, and Go.
Prerequisites
Section titled “Prerequisites”- Python 3.10+, Node.js 18+, or Go 1.21+
- Keycard account with access to Console
- Cursor IDE or another MCP-compatible client for testing
Hello World MCP Server
Section titled “Hello World MCP Server”Install Dependencies
Section titled “Install Dependencies”pip install keycardai-mcp-fastmcp fastmcpnpm install @keycardai/mcp @modelcontextprotocol/sdk expressnpm install -D typescript @types/expressgo mod init my-mcp-servergo get github.com/keycardai/credentials-go/mcpRegister Your MCP Server in Keycard
Section titled “Register Your MCP Server in Keycard”Your MCP server needs to be registered as a protected Resource in Keycard so that AI agents can authenticate against it.
-
In Keycard Console, navigate to Resources → Create Resource
-
Configure the resource:
Resource Name My MCP Server (Local Dev)Resource Identifier http://localhost:8000/mcpCredential Provider Zone ProviderResource Name My MCP Server (Local Dev)Resource Identifier http://localhost:8080/mcpCredential Provider Zone ProviderResource Name My MCP Server (Local Dev)Resource Identifier http://localhost:8080/mcpCredential Provider Zone Provider -
Click Create
Configure Environment
Section titled “Configure Environment”Create a .env file in your project root:
KEYCARD_ZONE_ID=your-zone-idMCP_SERVER_URL=http://localhost:8000KEYCARD_ZONE_URL=https://your-zone-id.keycard.cloudPORT=8080KEYCARD_ZONE_URL=https://your-zone-id.keycard.cloudPORT=8080Write the Server
Section titled “Write the Server”import os
from fastmcp import FastMCPfrom keycardai.mcp.integrations.fastmcp import AuthProvider
auth_provider = AuthProvider( zone_id=os.getenv("KEYCARD_ZONE_ID", "your-zone-id"), mcp_server_name="Hello World Server", mcp_base_url=os.getenv("MCP_SERVER_URL", "http://localhost:8000"),)
auth = auth_provider.get_remote_auth_provider()mcp = FastMCP("Hello World Server", auth=auth)
@mcp.tool()def hello_world(name: str) -> str: """Say hello to an authenticated user.""" return f"Hello, {name}! You are authenticated."
def main(): mcp.run(transport="streamable-http")
if __name__ == "__main__": main()AuthProvider handles all OAuth metadata and token verification. It connects your server to your Keycard zone so that MCP clients can discover how to authenticate. get_remote_auth_provider() returns the auth configuration that FastMCP needs, and mcp.run() starts the server.
import express from "express";import { mcpAuthMetadataRouter } from "@keycardai/mcp/server/auth/router";
const ZONE_URL = process.env.KEYCARD_ZONE_URL ?? "https://your-zone.keycard.cloud";const PORT = Number(process.env.PORT ?? 8080);
const app = express();
app.use( mcpAuthMetadataRouter({ oauthMetadata: { issuer: ZONE_URL }, resourceName: "Hello World MCP Server", }),);
app.get("/api/whoami", (req, res) => { res.json({ message: "Hello from Keycard!", authenticated: true, });});
app.listen(PORT, () => { console.log(`Hello World MCP Server running on http://localhost:${PORT}`);});mcpAuthMetadataRouter serves the .well-known OAuth endpoints that MCP clients use to discover how to authenticate. Keycard handles token verification automatically — your server just needs to expose the metadata.
package main
import ( "encoding/json" "log" "net/http" "os"
"github.com/keycardai/credentials-go/mcp")
func main() { zoneURL := os.Getenv("KEYCARD_ZONE_URL") port := os.Getenv("PORT") if port == "" { port = "8080" }
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"), ))
mux.HandleFunc("GET /api/hello", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "message": "Hello from Keycard!", "authenticated": true, }) })
log.Printf("Hello World MCP Server running on http://localhost:%s", port) log.Fatal(http.ListenAndServe(":"+port, mux))}mcp.AuthMetadataHandler serves the .well-known OAuth endpoints that MCP clients use to discover how to authenticate. The Go SDK uses the standard net/http library — no frameworks required.
Run and Test
Section titled “Run and Test”-
Start the server
Terminal window python -m my_serverTerminal window npx tsx src/server.tsTerminal window go run . -
Configure your coding agent
Add the MCP server to your agent’s configuration. Use the URL your server is running on (Python defaults to port
8000, TypeScript to8080).Cursor — add to
.cursor/mcp.json:{"mcpServers": {"my-mcp-server": {"url": "http://localhost:8000/mcp"}}}Claude Code — run in your terminal:
Terminal window claude mcp add --transport http my-mcp-server http://localhost:8000/mcp -
Authenticate and test
- Restart your coding agent to detect the new MCP server
- Connect to the MCP server when prompted
- Complete the OAuth flow with Keycard
- Test the tool — ask your agent:
"Run the hello_world tool with my name"
-
Verify in Keycard Console
Check Audit Logs for:
users:authenticateYou logged in successfully users:authorizeYour access was authorized credentials:issueAccess token was issued
Add Delegated Access
Section titled “Add Delegated Access”Delegated access lets your MCP server call external APIs (like GitHub) on behalf of authenticated users using token exchange (RFC 8693). Keycard exchanges the user’s token for an API-specific token automatically.
Add GitHub from Resource Catalog
Section titled “Add GitHub from Resource Catalog”The Resource Catalog provides pre-configured integrations that auto-create the resource and provider for you.
- In Keycard Console, navigate to Resources → Add Resource → Explore Resources
- Select GitHub from the catalog
- Enter your GitHub OAuth App credentials (Client ID and Client Secret)
- The catalog will create the GitHub provider and resource automatically
Create Application Credentials
Section titled “Create Application Credentials”Your server needs application credentials to perform token exchange.
- In Keycard Console, navigate to Applications → Create Application
- Set the Provided Resource to your MCP server resource (e.g.,
My MCP Server (Local Dev)) - Add the GitHub API resource as a Dependency
- Generate Client Credentials (Client ID + Client Secret)
- Save these credentials — you will need them in the next step
Update Environment
Section titled “Update Environment”Add the application credentials to your .env:
KEYCARD_ZONE_ID=your-zone-idKEYCARD_CLIENT_ID=your-client-idKEYCARD_CLIENT_SECRET=your-client-secretMCP_SERVER_URL=http://localhost:8000KEYCARD_ZONE_URL=https://your-zone-id.keycard.cloudKEYCARD_CLIENT_ID=your-client-idKEYCARD_CLIENT_SECRET=your-client-secretPORT=8080KEYCARD_ZONE_URL=https://your-zone-id.keycard.cloudKEYCARD_CLIENT_ID=your-client-idKEYCARD_CLIENT_SECRET=your-client-secretPORT=8080Use the Grant Pattern
Section titled “Use the Grant Pattern”The key Keycard pattern for delegated access is the grant decorator/middleware. It tells Keycard to exchange the user’s token for an API-specific token before your tool runs:
from keycardai.mcp.integrations.fastmcp import AccessContext, AuthProvider, ClientSecret
auth_provider = AuthProvider( zone_id=os.getenv("KEYCARD_ZONE_ID"), mcp_server_name="GitHub API Server", mcp_base_url=os.getenv("MCP_SERVER_URL", "http://localhost:8000"), application_credential=ClientSecret(( os.getenv("KEYCARD_CLIENT_ID"), os.getenv("KEYCARD_CLIENT_SECRET"), )),)
@mcp.tool()@auth_provider.grant("https://api.github.com")async def get_github_user(ctx: Context) -> dict: """Get the authenticated user's GitHub profile.""" access_context: AccessContext = ctx.get_state("keycardai")
if access_context.has_errors(): return {"error": "Token exchange failed", "details": access_context.get_errors()}
token = access_context.access("https://api.github.com").access_token # Use token to call GitHub APIThe @auth_provider.grant("https://api.github.com") decorator tells Keycard to exchange the user’s token for a GitHub API token before the tool runs. The exchanged token is available via ctx.get_state("keycardai").
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: ZONE_URL, applicationCredential: new ClientSecret(CLIENT_ID, CLIENT_SECRET),});
app.get( "/api/github-user", authProvider.grant("https://api.github.com"), async (req, res) => { const { accessContext } = req as DelegatedRequest;
if (accessContext.hasErrors()) { res.status(502).json({ error: "Token exchange failed" }); return; }
const token = accessContext.access("https://api.github.com").accessToken; // Use token to call GitHub API },);The authProvider.grant("https://api.github.com") middleware performs token exchange before your route handler runs. The exchanged token is available on req.accessContext.
import ( "fmt" "io" "net/http" "os"
"github.com/keycardai/credentials-go/mcp")
authProvider, err := mcp.NewAuthProvider( mcp.WithZoneURL(os.Getenv("KEYCARD_ZONE_URL")), mcp.WithApplicationCredential(mcp.NewClientSecret( os.Getenv("KEYCARD_CLIENT_ID"), os.Getenv("KEYCARD_CLIENT_SECRET"), )),)
// Chain: bearer auth → grant → handlerhandler := 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") // Use token.AccessToken to call GitHub API }),))The authProvider.Grant("https://api.github.com") middleware performs token exchange before your handler runs. The exchanged token is available via mcp.AccessContextFromRequest(r).
Run and Test
Section titled “Run and Test”-
Start the server
Terminal window python -m my_serverTerminal window npx tsx src/server.tsTerminal window go run . -
Test in Cursor
Ask Cursor:
"Get my GitHub profile"— you should see your GitHub user data returned. -
Verify in Keycard Console
Check Audit Logs for token exchange events alongside the authentication events.
Deploy to Production
Section titled “Deploy to Production”When deploying your MCP server to production:
- Update the resource identifier in Keycard Console to your production URL (e.g.,
https://my-mcp-server.example.com/mcp) — it must use HTTPS - Set environment variables securely in your hosting platform (
KEYCARD_ZONE_IDorKEYCARD_ZONE_URL,KEYCARD_CLIENT_ID,KEYCARD_CLIENT_SECRET,PORT) — never commit secrets to source control