A complete example demonstrating how to use the @grant decorator for token exchange using the low-level keycardai-mcp package. This enables your MCP server to access external APIs (GitHub) on behalf of authenticated users.
Note: For most use cases, we recommend using the FastMCP integration. This low-level approach is for advanced scenarios requiring more control over the Starlette application.
Keycard lets you securely connect your AI IDE or agent to external resources. With delegated access, your MCP server can:
- Exchange user tokens for API-specific access tokens via OAuth 2.0 Token Exchange
- Access external APIs on behalf of authenticated users with proper scopes
- Maintain audit trails of all delegated operations
| Feature | FastMCP Integration | Low-Level MCP (this example) |
|---|---|---|
| Import | keycardai.mcp.integrations.fastmcp |
keycardai.mcp.server.auth |
| AccessContext | await ctx.get_state("keycardai") |
Function parameter |
| Server startup | mcp.run() |
uvicorn.run(auth_provider.app(mcp)) |
Before running this example, set up Keycard for delegated access:
1. Sign up at keycard.ai
A zone is your authentication boundary. Create one in the Keycard console.
Set up an identity provider (Google, Microsoft, etc.) for user authentication.
Create a GitHub App in your organization (or personal account) to enable delegated access:
- Go to your GitHub organization → Settings → Developer settings → GitHub Apps
- Click New GitHub App
- Configure the app with the permissions your MCP server needs (e.g., Repository access, User profile read)
- Generate a Client Secret
- Note down the Client ID and Client Secret — you'll use these to configure the credential provider in Keycard
Set up the GitHub credential provider so Keycard can obtain tokens on behalf of users:
- In your Keycard zone, go to Providers
- Add a new GitHub credential provider using the Client ID and Client Secret from the GitHub App you created in step 4
- This credential provider is what Keycard uses to issue GitHub API tokens on behalf of authenticated users
Add GitHub as an API resource in your zone:
- In your Keycard zone, go to Resources
- Add a new API resource for GitHub:
- Resource URL:
https://api.github.com - Credential Provider: Select the GitHub credential provider you created in step 5
- Scopes:
read:user,repo(adjust based on your needs)
- Resource URL:
Create an application that will represent your MCP server:
- Go to Applications in your zone
- Create a new application
- Identifier: Set this to match your
MCP_SERVER_URL(e.g.,http://localhost:8000/)
- Identifier: Set this to match your
- Add the GitHub API resource as a dependency of this application
- Generate Application Credentials (Client ID and Client Secret)
- These are what you'll use for
KEYCARD_CLIENT_IDandKEYCARD_CLIENT_SECRET
- These are what you'll use for
Register your MCP server with Keycard:
- Go to Resources and add a new MCP Server resource
- Set the URL to your server's MCP endpoint:
http://localhost:8000/mcp - Configure the resource:
- Provided by: Select the application you created in step 7
- Credential Provider: Keycard STS Zone Provider
Note: Delegated token exchange requires Keycard to reach your MCP server. For local development, use a tunneling service (e.g., ngrok, Cloudflare Tunnel) or host the server on a publicly accessible URL.
See Delegated Access Setup for detailed instructions.
Delegated access requires Keycard to reach your server. For local development, set up a tunnel:
# Using ngrok
ngrok http 8000
# Or using Cloudflare Tunnel
cloudflared tunnel --url http://localhost:8000Use the public URL from your tunnel as MCP_SERVER_URL.
export KEYCARD_ZONE_ID="your-zone-id"
export KEYCARD_CLIENT_ID="your-client-id"
export KEYCARD_CLIENT_SECRET="your-client-secret"
export MCP_SERVER_URL="https://your-tunnel-url.ngrok.io/" # Must be publicly reachablecd packages/mcp/examples/delegated_access
uv syncuv run python main.pyThe server will start on http://localhost:8000.
Check that OAuth metadata is being served:
curl http://localhost:8000/.well-known/oauth-authorization-serverYou should see JSON with issuer, authorization_endpoint, and other OAuth metadata.
- Connect to your server using an MCP-compatible client (e.g., Cursor, Claude Desktop)
- Authenticate through your configured identity provider
- When prompted by Keycard, authorize GitHub access
- Call the
get_github_userorlist_github_repostools - Verify GitHub user data is returned
User MCP Server Keycard GitHub
| | | |
|---- Authenticate ------>| | |
| |<-- User Token -------| |
| | | |
|---- Call Tool --------->| | |
| |-- Exchange Token --->| |
| |<- GitHub Token ------| |
| | | |
| |----------------------|-- API Request ------->|
| |<---------------------|-- API Response -------|
|<--- Tool Result --------| | |
- User authenticates to your MCP server via Keycard
- When a tool with
@grantis called, Keycard exchanges the user's token - The exchanged token has the scopes configured for the external resource
- Your server uses this token to call GitHub API on behalf of the user
In the low-level MCP integration, AccessContext is passed directly as a function parameter:
@mcp.tool()
@auth_provider.grant("https://api.github.com")
async def get_github_user(access_ctx: AccessContext) -> dict:
# access_ctx is injected by the @grant decorator
token = access_ctx.access("https://api.github.com").access_token
...This differs from the FastMCP integration where you retrieve it via await ctx.get_state("keycardai").
The example demonstrates comprehensive error handling patterns:
| Method | Description |
|---|---|
has_errors() |
Check for any errors (global or resource-specific) |
get_errors() |
Get all error details as a dictionary |
has_resource_error(url) |
Check for errors on a specific resource |
get_resource_errors(url) |
Get errors for a specific resource |
has_error() |
Check for global errors only |
get_error() |
Get global error details |
| Variable | Required | Description |
|---|---|---|
KEYCARD_ZONE_ID |
Yes | Your Keycard zone ID |
KEYCARD_CLIENT_ID |
Yes | Client ID from application credentials |
KEYCARD_CLIENT_SECRET |
Yes | Client secret from application credentials |
MCP_SERVER_URL |
Yes | Server URL (must be publicly reachable for delegated access) |
- FastMCP Integration Example (recommended for most use cases)
- Keycard Documentation
- Delegated Access Guide
- GitHub API Documentation