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 and TypeScript.
Prerequisites
Section titled “Prerequisites”- Python 3.10+ or Node.js 18+
- 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-fastmcp fastmcpnpm install @keycardai/mcp @modelcontextprotocol/sdk express zodnpm install -D typescript @types/express tsxRegister 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 Provider -
Click Create
-
In Zone Settings, turn off “Require invitation to sign in to this zone” so users can self-register when they authenticate for the first time.
Configure Environment
Section titled “Configure Environment”Create a .env file in your project root:
KEYCARD_ZONE_ID=<your-zone-id>MCP_SERVER_URL=http://localhost:8000KEYCARD_ZONE_URL=https://<your-zone-id>.keycard.cloudPORT=8080Write the Server
Section titled “Write the Server”import os
from fastmcp import FastMCPfrom keycardai.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 { z } from "zod";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";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-id>.keycard.cloud";const PORT = Number(process.env.PORT ?? 8080);
const app = express();app.use(express.json());
// OAuth metadata endpoints (unauthenticated)app.use( mcpAuthMetadataRouter({ oauthMetadata: { issuer: ZONE_URL }, resourceName: "Hello World MCP Server", }),);
// MCP transport (protected by bearer auth)app.post( "/mcp", requireBearerAuth({ issuers: ZONE_URL }), async (req, res) => { const server = new McpServer({ name: "Hello World Server", version: "1.0.0" });
server.tool( "hello_world", "Say hello to an authenticated user.", { name: z.string().describe("Name to greet") }, async ({ name }) => ({ content: [{ type: "text", text: `Hello, ${name}! You are authenticated.` }], }), );
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); await server.connect(transport); await transport.handleRequest(req, res, req.body); },);
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 and rejects tokens from any issuer other than your zone. The MCP transport is mounted at /mcp in stateless mode. Each request is handled independently with no session state.
Run and Test
Section titled “Run and Test”-
Start the server
Terminal window python server.pyTerminal window npx tsx server.ts -
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. Keycard will prompt you to sign in to your zone. This is a separate account from your Keycard Console login. If it’s your first time, click Sign up to create one.
- 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.