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";import { requireBearerAuth } from "@keycardai/mcp/server/auth/middleware/bearerAuth";
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", requireBearerAuth({ issuers: ZONE_URL }), (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. requireBearerAuth verifies the JWT on every request and pins the verifier to your zone URL, so forged tokens from any other issuer are rejected before any JWKS lookup.
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
Next Steps
Section titled “Next Steps”Your MCP server is now protected — every tool call is tied to a verified user, scoped by policy, and logged for audit.
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