A Python SDK for Model Context Protocol (MCP) functionality with simplified authentication and authorization
Project description
Keycard MCP SDK
A comprehensive Python SDK for Model Context Protocol (MCP) functionality that simplifies authentication and authorization concerns for developers working with AI/LLM integrations.
Requirements
- Python 3.9 or greater
- Virtual environment (recommended)
Setup Guide
Option 1: Using uv (Recommended)
If you have uv installed:
# Create a new project with uv
uv init my-mcp-project
cd my-mcp-project
# Create and activate virtual environment
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
Option 2: Using Standard Python
# Create project directory
mkdir my-mcp-project
cd my-mcp-project
# Create and activate virtual environment
python3 -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Upgrade pip (recommended)
pip install --upgrade pip
Installation
pip install keycardai-mcp
Quick Start
Add Keycard authentication to your existing MCP server:
Install the Package
pip install keycardai-mcp
Get Your Keycard Zone ID
- Sign up at keycard.ai
- Navigate to Zone Settings to get your zone ID
- Configure your preferred identity provider (Google, Microsoft, etc.)
- Create an MCP resource in your zone
Add Authentication to Your MCP Server
from mcp.server.fastmcp import FastMCP
from keycardai.mcp.server.auth import AuthProvider
# Your existing MCP server
mcp = FastMCP("My Secure MCP Server")
@mcp.tool()
def my_protected_tool(data: str) -> str:
return f"Processed: {data}"
# Add Keycard authentication
access = AuthProvider(
zone_id="your_zone_id_here",
mcp_server_name="My Secure MCP Server",
)
# Create authenticated app
app = access.app(mcp)
Run with Authentication
pip install uvicorn
uvicorn server:app
🎉 Your MCP server is now protected with Keycard authentication! 🎉
Features
- ✅ OAuth 2.0 Authentication: Secure your MCP server with industry-standard OAuth flows
- ✅ Easy Integration: Add authentication with just a few lines of code
- ✅ Multi-Zone Support: Support multiple Keycard zones in one application
- ✅ Token Exchange: Automatic delegated token exchange for accessing external APIs
- ✅ Production Ready: Battle-tested security patterns and error handling
Delegated Access
Keycard allows MCP servers to access other resources on behalf of users with automatic consent and secure token exchange.
Setup Protected Resources
- Configure credential provider (e.g., Google Workspace)
- Configure protected resource (e.g., Google Drive API)
- Set MCP server dependencies to allow delegated access
- Create client secret identity to provide authentication method
Zone Configuration
Keycard zones are isolated environments for authentication and authorization. You can configure zone settings explicitly in code or automatically discover them from environment variables.
Configuration Methods
1. Explicit Configuration (Recommended for Production)
from keycardai.mcp.server.auth import AuthProvider
# Using zone_id (constructs zone URL automatically)
auth_provider = AuthProvider(
zone_id="your-zone-id",
mcp_server_name="My MCP Server"
)
# Using explicit zone_url
auth_provider = AuthProvider(
zone_url="https://your-zone-id.keycard.cloud",
mcp_server_name="My MCP Server"
)
# Using custom base_url with zone_id
auth_provider = AuthProvider(
zone_id="your-zone-id",
base_url="https://custom.keycard.example.com",
mcp_server_name="My MCP Server"
)
2. Environment Variable Discovery
The SDK automatically discovers zone configuration from environment variables:
# Option 1: Set zone_id (URL will be constructed)
export KEYCARD_ZONE_ID="your-zone-id"
# Option 2: Set explicit zone URL
export KEYCARD_ZONE_URL="https://your-zone-id.keycard.cloud"
# Option 3: Customize base URL for zone construction
export KEYCARD_ZONE_ID="your-zone-id"
export KEYCARD_BASE_URL="https://custom.keycard.example.com"
from keycardai.mcp.server.auth import AuthProvider
# Automatically discovers zone configuration from environment
auth_provider = AuthProvider(
mcp_server_name="My MCP Server"
)
Configuration Precedence
When multiple zone configuration methods are present, the SDK follows this precedence order (highest to lowest):
- Explicit
zone_urlparameter - Always takes priority KEYCARD_ZONE_URLenvironment variable - Direct zone URL- Explicit
zone_idparameter - Combined with base_url to construct zone URL KEYCARD_ZONE_IDenvironment variable - Combined with base_url to construct zone URL- Error - At least one zone configuration method is required
For base_url, the precedence is:
- Explicit
base_urlparameter - Custom base URL KEYCARD_BASE_URLenvironment variable - Custom base URL from environment- Default:
https://keycard.cloud- Standard Keycard cloud URL
Environment Variables Reference
| Environment Variable | Purpose | Default Value |
|---|---|---|
KEYCARD_ZONE_ID |
Zone identifier for constructing zone URL | None (required if zone_url not set) |
KEYCARD_ZONE_URL |
Complete zone URL (overrides zone_id) | None |
KEYCARD_BASE_URL |
Base URL for zone construction | https://keycard.cloud |
Application Credentials for Token Exchange
To enable token exchange (required for the @grant decorator), you need to configure application credentials. The SDK supports multiple credential types and provides automatic discovery via environment variables.
Credential Types
The SDK supports three types of application credentials:
- ClientSecret - OAuth client credentials (client_id/client_secret) issued by Keycard
- WebIdentity - Private key JWT authentication for MCP servers
- EKSWorkloadIdentity - AWS EKS Pod Identity for Kubernetes deployments
Configuration Methods
1. Explicit Configuration (Recommended for Production)
Explicitly provide credentials when creating the AuthProvider:
from keycardai.mcp.server.auth import AuthProvider, ClientSecret
# Client Secret credentials
auth_provider = AuthProvider(
zone_id="your-zone-id",
mcp_server_name="My MCP Server",
application_credential=ClientSecret(("your_client_id", "your_client_secret"))
)
from keycardai.mcp.server.auth import AuthProvider, WebIdentity
# Web Identity (Private Key JWT)
auth_provider = AuthProvider(
zone_id="your-zone-id",
mcp_server_name="My MCP Server",
application_credential=WebIdentity(
mcp_server_name="My MCP Server",
storage_dir="./mcp_keys" # Directory for key storage
)
)
from keycardai.mcp.server.auth import AuthProvider, EKSWorkloadIdentity
# EKS Workload Identity
auth_provider = AuthProvider(
zone_id="your-zone-id",
mcp_server_name="My MCP Server",
application_credential=EKSWorkloadIdentity()
)
2. Environment Variable Discovery (Convenient for Development)
The SDK automatically discovers credentials from environment variables:
# Option A: Client Credentials
export KEYCARD_CLIENT_ID="your_client_id"
export KEYCARD_CLIENT_SECRET="your_client_secret"
# Option B: Explicit Credential Type
export KEYCARD_APPLICATION_CREDENTIAL_TYPE="web_identity"
export KEYCARD_WEB_IDENTITY_KEY_STORAGE_DIR="./mcp_keys" # Optional
# Option C: EKS Workload Identity
export KEYCARD_APPLICATION_CREDENTIAL_TYPE="eks_workload_identity"
# Optional: Custom token file path (defaults to AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE or AWS_WEB_IDENTITY_TOKEN_FILE)
export KEYCARD_EKS_WORKLOAD_IDENTITY_TOKEN_FILE="/var/run/secrets/token"
With environment variables configured, create the AuthProvider without explicit credentials:
from keycardai.mcp.server.auth import AuthProvider
# Credentials automatically discovered from environment variables
auth_provider = AuthProvider(
zone_id="your-zone-id",
mcp_server_name="My MCP Server"
)
Configuration Precedence
When multiple configuration methods are present, the SDK follows this precedence order (highest to lowest):
- Explicit
application_credentialparameter - Always takes priority KEYCARD_CLIENT_ID+KEYCARD_CLIENT_SECRET- Client credentials via environmentKEYCARD_APPLICATION_CREDENTIAL_TYPE- Explicit credential type selectionAWS_CONTAINER_AUTHORIZATION_TOKEN_FILE- Automatic EKS detection- None - No credentials configured (token exchange disabled)
Environment Variables Reference
| Environment Variable | Purpose | Used By | Default Value |
|---|---|---|---|
KEYCARD_CLIENT_ID |
OAuth client identifier | ClientSecret |
None |
KEYCARD_CLIENT_SECRET |
OAuth client secret | ClientSecret |
None |
KEYCARD_APPLICATION_CREDENTIAL_TYPE |
Explicit credential type selection | All | None |
KEYCARD_WEB_IDENTITY_KEY_STORAGE_DIR |
Directory for private key storage | WebIdentity |
"./mcp_keys" |
KEYCARD_EKS_WORKLOAD_IDENTITY_TOKEN_FILE |
Custom path to EKS token file | EKSWorkloadIdentity |
None |
AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE |
Path to EKS token file (AWS default) | EKSWorkloadIdentity |
None |
AWS_WEB_IDENTITY_TOKEN_FILE |
Path to EKS token file (AWS fallback) | EKSWorkloadIdentity |
None |
Running Without Application Credentials
If no application credentials are configured, the AuthProvider will work for basic authentication but the @grant decorator will be unable to perform token exchange. This is useful for MCP servers that only need user authentication without delegated access to external resources.
Add Delegation to Your Tools
from mcp.server.fastmcp import FastMCP, Context
from keycardai.mcp.server.auth import AuthProvider, AccessContext, ClientSecret
import os
# Configure your provider with client credentials
access = AuthProvider(
zone_id="your_zone_id",
mcp_server_name="My MCP Server",
application_credential=ClientSecret((
os.getenv("KEYCARD_CLIENT_ID"),
os.getenv("KEYCARD_CLIENT_SECRET")
))
)
mcp = FastMCP("My MCP Server")
@mcp.tool()
@access.grant("https://protected-api")
def protected_tool(ctx: Context, access_context: AccessContext, name: str) -> str:
# Use the access_context to call external APIs on behalf of the user
token = access_context.access("https://protected-api").access_token
# Make authenticated API calls...
return f"Protected data for {name}"
app = access.app(mcp)
Lowlevel Integration
For advanced use cases requiring direct control over the MCP server lifecycle, you can integrate Keycard with the lowlevel MCP server API.
Requirements
When using lowlevel integration with Keycard:
-
Function Parameters: Functions decorated with
@auth.grant()must acceptRequestContextandAccessContextparameters:@auth.grant("https://protected-api") def echo_handler(arguments: dict[str, Any], ctx: RequestContext, access_context: AccessContext) -> list[TextContent]: # Your implementation
-
RequestContext Responsibility: Unlike FastMCP which automatically injects the context, lowlevel servers require you to manually pass the
RequestContextfromserver.request_contextto your handler functions.@server.call_tool() async def handle_call_tool(name: str, arguments: dict[str, Any] | None = None) -> list[TextContent]: # Pass server.request_context to the tool call. return await echo_handler(arguments, server.request_context)
-
ASGI Integration: Use
auth.get_mcp_router()to create authenticated routes that wrap your MCP transport or session manager.
Option 1: Using StreamableHTTPServerTransport
import uvicorn
import asyncio
from contextlib import asynccontextmanager
from starlette.applications import Starlette
from starlette.types import Scope, Receive, Send
from mcp.server.lowlevel import Server
from mcp.shared.context import RequestContext
from mcp.types import Tool, TextContent
from mcp.server.streamable_http import StreamableHTTPServerTransport
from typing import Any
from keycardai.mcp.server.auth import AuthProvider, AccessContext
# Configure Keycard authentication
auth = AuthProvider(
zone_id="your_zone_id",
mcp_server_name="lowlevel-mcp",
enable_multi_zone=True,
)
class StreamableHTTPASGIApp:
def __init__(self, transport: StreamableHTTPServerTransport):
self.transport = transport
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await self.transport.handle_request(scope, receive, send)
# Create MCP server and transport
server = Server("lowlevel-mcp")
transport = StreamableHTTPServerTransport()
# Define protected tool with delegated access
@auth.grant("https://protected-api")
def echo_handler(arguments: dict[str, Any], ctx: RequestContext, access_context: AccessContext) -> list[TextContent]:
if access_context.has_errors():
return [TextContent(type="text", text=f"Error: {access_context.get_errors()}")]
# Access external API with delegated token
token = access_context.access("https://protected-api").access_token
return [TextContent(type="text", text=f"Echo: {arguments['message']}")]
# Register tools
@server.list_tools()
async def handle_list_tools() -> list[Tool]:
return [Tool(
name="echo",
description="Echo a message with protected access",
inputSchema={
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"]
}
)]
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any] | None = None) -> list[TextContent]:
if name == "echo":
# Pass RequestContext from server to decorated handler
return await echo_handler(arguments, server.request_context)
raise Exception(f"Unknown tool: {name}")
@asynccontextmanager
async def lifespan(app):
async with transport.connect() as (read_stream, write_stream):
server_task = asyncio.create_task(server.run(
read_stream, write_stream, server.create_initialization_options()
))
try:
yield
finally:
server_task.cancel()
# Create authenticated ASGI app
app = Starlette(
routes=auth.get_mcp_router(StreamableHTTPASGIApp(transport)),
lifespan=lifespan,
)
Option 2: Using StreamableHTTPSessionManager
import uvicorn
import asyncio
from starlette.applications import Starlette
from starlette.types import Scope, Receive, Send
from mcp.server.lowlevel import Server
from mcp.shared.context import RequestContext
from mcp.types import Tool, TextContent
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from typing import Any
from keycardai.mcp.server.auth import AuthProvider, AccessContext
# Configure Keycard authentication
auth = AuthProvider(
zone_id="your_zone_id",
mcp_server_name="lowlevel-mcp",
enable_multi_zone=True,
)
class StreamableHTTPASGIApp:
def __init__(self, session_manager: StreamableHTTPSessionManager):
self.session_manager = session_manager
async def __call__(self, scope: Scope, receive: Send, send: Send) -> None:
await self.session_manager.handle_request(scope, receive, send)
# Create MCP server and session manager
server = Server("lowlevel-mcp")
session_manager = StreamableHTTPSessionManager(
app=server,
stateless=True,
)
# Define protected tool with delegated access
@auth.grant("https://protected-api")
def echo_handler(arguments: dict[str, Any], ctx: RequestContext, access_context: AccessContext) -> list[TextContent]:
if access_context.has_errors():
return [TextContent(type="text", text=f"Error: {access_context.get_errors()}")]
# Access external API with delegated token
token = access_context.access("https://protected-api").access_token
return [TextContent(type="text", text=f"Echo: {arguments['message']}")]
# Register tools
@server.list_tools()
async def handle_list_tools() -> list[Tool]:
return [Tool(
name="echo",
description="Echo a message with protected access",
inputSchema={
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"]
}
)]
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any] | None = None) -> list[TextContent]:
if name == "echo":
# Pass RequestContext from server to decorated handler
return await echo_handler(arguments, server.request_context)
raise Exception(f"Unknown tool: {name}")
# Create authenticated ASGI app
app = Starlette(
routes=auth.get_mcp_router(StreamableHTTPASGIApp(session_manager)),
lifespan=lambda app: session_manager.run(),
)
# Run the server
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Both approaches provide full control over the MCP server lifecycle while maintaining Keycard's authentication and delegated access capabilities.
Error Handling
The Keycard MCP package implements a robust error handling system that allows functions to continue execution even when delegation processes fail. This is achieved through the AccessContext object, which manages both successful tokens and error states without raising exceptions.
How AccessContext Manages Errors
The AccessContext serves as a centralized error management system during the OAuth token delegation process:
Error Types:
- Global Errors: Affect all resources (e.g., missing authentication, configuration issues)
- Resource-Specific Errors: Affect individual resources during token exchange
The @grant decorator automatically handles all error scenarios and populates the AccessContext with appropriate error information, ensuring your functions can always execute and handle errors gracefully.
Error Scenarios
The @grant decorator handles multiple error scenarios automatically:
- Authentication Errors: Missing or invalid authentication tokens
- Configuration Errors: Server misconfiguration or missing zone information
- Token Exchange Errors: Failures when exchanging tokens for specific resources
All errors include descriptive messages to help with debugging and user-friendly error handling.
Usage Patterns
Basic Error Checking:
@provider.grant("https://api.example.com")
def my_tool(access_ctx: AccessContext, ctx: Context, user_id: str):
# Check for any errors first
if access_ctx.has_errors():
error_info = access_ctx.get_errors()
return {"error": "Token delegation failed", "details": error_info}
# Proceed with successful token
token = access_ctx.access("https://api.example.com").access_token
return call_external_api(token, user_id)
Partial Success Handling:
@provider.grant(["https://api1.com", "https://api2.com"])
def multi_resource_tool(access_ctx: AccessContext, ctx: Context):
results = {}
# Handle successful resources
for resource in access_ctx.get_successful_resources():
token = access_ctx.access(resource).access_token
results[resource] = call_api(resource, token)
# Handle failed resources
for resource in access_ctx.get_failed_resources():
error = access_ctx.get_resource_errors(resource)
results[resource] = {"error": error["error"]}
return results
Status-Based Handling:
@provider.grant("https://api.example.com")
def status_aware_tool(access_ctx: AccessContext, ctx: Context):
status = access_ctx.get_status() # "success", "partial_error", or "error"
if status == "error":
return {"status": "failed", "reason": access_ctx.get_error()}
elif status == "partial_error":
return {"status": "partial", "details": access_ctx.get_errors()}
else:
token = access_ctx.access("https://api.example.com").access_token
return {"status": "success", "data": call_api(token)}
FAQ
How to test the MCP server with modelcontexprotocol/inspector?
When testing your MCP server with the modelcontexprotocol/inspector, you may need to configure CORS (Cross-Origin Resource Sharing) to allow the inspector's web interface to access your protected endpoints from localhost.
You can use Starlette's built-in CORSMiddleware to configure CORS settings:
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
middleware = [
Middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins for testing
allow_credentials=True,
allow_methods=["*"], # Allow all HTTP methods
allow_headers=["*"], # Allow all headers
)
]
app = access.app(mcp, middleware=middleware)
Important Security Note: The configuration above uses permissive CORS settings (allow_origins=["*"]) which should only be used for local development and testing. In production environments, you should restrict allow_origins to specific domains that need access to your MCP server.
For production use, consider more restrictive settings:
middleware = [
Middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"], # Specific allowed origins
allow_credentials=True,
allow_methods=["GET", "POST"], # Only required methods
allow_headers=["Authorization", "Content-Type"], # Only required headers
)
]
Examples
For complete examples and advanced usage patterns, see our documentation.
License
MIT License - see LICENSE file for details.
Support
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keycardai_mcp-0.15.0.tar.gz.
File metadata
- Download URL: keycardai_mcp-0.15.0.tar.gz
- Upload date:
- Size: 64.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
78dc8b6165298826aee3bf603412617d712bbfae88d16a18c6bc880990a9c13e
|
|
| MD5 |
0d402ef069cc624c6fc76f8d15616480
|
|
| BLAKE2b-256 |
fb2731fb748c863718c3a302da7e185861a3e9cc85dabd40cc88a0a607af226d
|
File details
Details for the file keycardai_mcp-0.15.0-py3-none-any.whl.
File metadata
- Download URL: keycardai_mcp-0.15.0-py3-none-any.whl
- Upload date:
- Size: 42.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b652f6bdef1e7fd36114ea388630fe84f713f306940bd7be07dd4f1d0fa0167
|
|
| MD5 |
9ecf7540f20bc1fb33e98f7526576f0f
|
|
| BLAKE2b-256 |
2633fe91e447d259162484904532d11dcc9e2ff05308ee394e2d876578a4c9ca
|