Skip to content
API Reference

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.

  • 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
Terminal window
pip install keycardai-mcp-fastmcp fastmcp

Your MCP server needs to be registered as a protected Resource in Keycard so that AI agents can authenticate against it.

  1. In Keycard Console, navigate to ResourcesCreate Resource

  2. Configure the resource:

    Resource NameMy MCP Server (Local Dev)
    Resource Identifierhttp://localhost:8000/mcp
    Credential ProviderZone Provider
  3. Click Create

Create a .env file in your project root:

Terminal window
KEYCARD_ZONE_ID=your-zone-id
MCP_SERVER_URL=http://localhost:8000
import os
from fastmcp import FastMCP
from 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.

  1. Start the server

    Terminal window
    python -m my_server
  2. 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 to 8080).

    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
  3. Authenticate and test

    1. Restart your coding agent to detect the new MCP server
    2. Connect to the MCP server when prompted
    3. Complete the OAuth flow with Keycard
    4. Test the tool — ask your agent: "Run the hello_world tool with my name"
  4. Verify in Keycard Console

    Check Audit Logs for:

    users:authenticateYou logged in successfully
    users:authorizeYour access was authorized
    credentials:issueAccess token was issued

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.

The Resource Catalog provides pre-configured integrations that auto-create the resource and provider for you.

  1. In Keycard Console, navigate to ResourcesAdd ResourceExplore Resources
  2. Select GitHub from the catalog
  3. Enter your GitHub OAuth App credentials (Client ID and Client Secret)
  4. The catalog will create the GitHub provider and resource automatically

Your server needs application credentials to perform token exchange.

  1. In Keycard Console, navigate to ApplicationsCreate Application
  2. Set the Provided Resource to your MCP server resource (e.g., My MCP Server (Local Dev))
  3. Add the GitHub API resource as a Dependency
  4. Generate Client Credentials (Client ID + Client Secret)
  5. Save these credentials — you will need them in the next step

Add the application credentials to your .env:

Terminal window
KEYCARD_ZONE_ID=your-zone-id
KEYCARD_CLIENT_ID=your-client-id
KEYCARD_CLIENT_SECRET=your-client-secret
MCP_SERVER_URL=http://localhost:8000

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 API

The @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").

  1. Start the server

    Terminal window
    python -m my_server
  2. Test in Cursor

    Ask Cursor: "Get my GitHub profile" — you should see your GitHub user data returned.

  3. Verify in Keycard Console

    Check Audit Logs for token exchange events alongside the authentication events.

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_ID or KEYCARD_ZONE_URL, KEYCARD_CLIENT_ID, KEYCARD_CLIENT_SECRET, PORT) — never commit secrets to source control