Skip to content

Repository files navigation

Keycard Python SDK

Keycard handles OAuth, identity, and access so your MCP servers don't have to. Add authentication, authorization, and delegated API access to any MCP server with a few lines of Python — no token plumbing, no auth middleware, no security footguns.

  • Drop-in auth for MCP servers (OAuth 2.0, PKCE, token exchange — handled for you)
  • Delegated access — call Google, GitHub, Slack APIs on behalf of your users with automatic token exchange
  • Works with both the MCP Python SDK and the FastMCP framework

Which Package?

You want to...InstallGuide
Add auth to MCP servers (using the mcp SDK)pip install keycardai-mcpQuick Start
Add auth to FastMCP serverspip install keycardai-fastmcpQuick Start
Connect to MCP servers as a clientpip install keycardai-mcpMCP Client docs
Build agent-to-agent (A2A) servicespip install keycardai-a2aA2A docs
Use the OAuth 2.0 client directlypip install keycardai-oauthOAuth docs

Key Concepts

  • Zone — A Keycard environment that groups your identity providers, MCP resources, and access policies. Get your zone ID from console.keycard.ai.
  • Delegated Access — Calling external APIs (Google, GitHub, Slack, etc.) on behalf of your authenticated users via RFC 8693 token exchange.
  • @grant decorator — Declares which external APIs a tool needs. Automatically exchanges the user's token for a scoped token before your function runs.
  • AccessContext — The result of a grant. Contains exchanged tokens or errors. Non-throwing by design — always check .has_errors() before using tokens.
  • Application Credentials — How your server authenticates with Keycard for token exchange. Three types: ClientSecret, WebIdentity, EKSWorkloadIdentity.

Known Limitations & Non-Goals

Current Limitations

  • FastMCP 3.x Required: The keycardai-fastmcp package requires FastMCP 3.0 or later. FastMCP 3.0 made ctx.get_state() and ctx.set_state() async; all tool functions using these calls must be async def.
  • MCP Protocol Version: Tested against MCP protocol version as implemented by mcp>=1.13.1. Newer MCP protocol versions may introduce incompatibilities.

Non-Goals

  • Standalone Identity Provider: Keycard SDKs are designed to integrate with Keycard's hosted identity service, not to provide standalone identity management.
  • Multi-Language Support: This SDK is Python-only. Other language SDKs are separate projects.
  • Offline Operation: All authentication flows require network connectivity to Keycard services.

Compatibility Matrix

Python Version Support

Python VersionStatus
3.9Not Supported
3.10Supported (minimum)
3.11Supported
3.12Supported
3.13Supported

Key Dependency Constraints

PackageDependencyVersion ConstraintRationale
keycardai-fastmcpfastmcp>=3.0.0FastMCP 3.0+ required. ctx.get_state()/ctx.set_state() are now async.
All packagespydantic>=2.11.7No upper bound - Pydantic 2.x maintains backward compatibility.
All packageshttpx>=0.27.2No upper bound - httpx follows semver.
keycardai-mcpmcp>=1.13.1No upper bound - API is protocol-defined.

Why No Upper Bounds on Most Dependencies?

Following Python packaging best practices:

  1. Upper bounds cause resolver conflicts in end-user projects when multiple packages specify conflicting ranges.
  2. Well-maintained libraries (pydantic, httpx) follow semantic versioning and maintain backward compatibility.
  3. Testing against latest via CI catches issues before users encounter them.

Versioning & Breaking Changes

Versioning Strategy

All packages follow Semantic Versioning:

  • MAJOR.MINOR.PATCH (e.g., 0.15.0)
  • During 0.x.y development:
    • MINOR bumps (0.x.0) may contain breaking changes
    • PATCH bumps (0.x.y) are backward-compatible bug fixes

When Will Packages Reach 1.0?

Packages will graduate to 1.0.0 when:

  • Public API is stable and well-documented
  • Comprehensive test coverage exists
  • Production usage validates the design
  • No planned breaking changes for foreseeable future

Breaking Change Policy

  1. Breaking changes are documented in CHANGELOG.md with migration guides
  2. Deprecation warnings will be added before removal where feasible
  3. Commit messages use ! suffix (e.g., feat!:) for breaking changes
  4. Release notes highlight breaking changes prominently

mcp vs fastmcp: The mcp SDK includes a built-in FastMCP class (from mcp.server.fastmcp import FastMCP), while fastmcp is a separate standalone framework (from fastmcp import FastMCP). They're different libraries. keycardai-mcp wraps the former; keycardai-fastmcp wraps the latter.

Prerequisites

  1. Python 3.10+ and a virtual environment
  2. Sign up at keycard.ai and get your zone ID from Zone Settings
  3. Configure an identity provider (Google, Microsoft, etc.) and create an MCP resource in your zone

Quick Start: FastMCP

uv add keycardai-fastmcp fastmcp
fromfastmcpimportFastMCPfromkeycardai.fastmcpimportAuthProvider# Configure Keycard authenticationauth_provider=AuthProvider(
zone_id="your-zone-id", # From console.keycard.aimcp_server_name="My Server",
mcp_base_url="http://localhost:8000/"
)
# Create authenticated MCP serverauth=auth_provider.get_remote_auth_provider()
mcp=FastMCP("My Server", auth=auth)
@mcp.tool()defhello_world(name: str) ->str:
"""Say hello to someone."""returnf"Hello, {name}!"if__name__=="__main__":
mcp.run(transport="streamable-http")

See the FastMCP examples for runnable projects.

Quick Start: Standard MCP

pip install keycardai-mcp uvicorn
frommcp.server.fastmcpimportFastMCPfromkeycardai.mcp.server.authimportAuthProviderimportuvicorn# Your MCP servermcp=FastMCP("My Server")
@mcp.tool()defhello_world(name: str) ->str:
"""Say hello to someone."""returnf"Hello, {name}!"# Add Keycard authenticationauth_provider=AuthProvider(
zone_id="your-zone-id", # From console.keycard.aimcp_server_name="My Server",
mcp_server_url="http://localhost:8000/"
)
# Wrap with auth and runapp=auth_provider.app(mcp)
uvicorn.run(app, host="0.0.0.0", port=8000)

See the MCP examples for runnable projects.

Delegated Access

Delegated access lets your MCP tools call external APIs (Google Calendar, GitHub, Slack, etc.) on behalf of authenticated users via automatic token exchange.

Setup: Get client credentials from console.keycard.ai, then set KEYCARD_CLIENT_ID and KEYCARD_CLIENT_SECRET as environment variables.

FastMCP

importosimporthttpxfromfastmcpimportFastMCP, Contextfromkeycardai.fastmcpimportAuthProvider, AccessContext, ClientSecretauth_provider=AuthProvider(
zone_id="your-zone-id",
mcp_server_name="My Server",
mcp_base_url="http://localhost:8000/",
application_credential=ClientSecret((
os.getenv("KEYCARD_CLIENT_ID"),
os.getenv("KEYCARD_CLIENT_SECRET")
))
)
auth=auth_provider.get_remote_auth_provider()
mcp=FastMCP("My Server", auth=auth)
@mcp.tool()@auth_provider.grant("https://www.googleapis.com/calendar/v3")asyncdefget_calendar_events(ctx: Context) ->dict:
"""Get the user's calendar events with delegated access."""# Retrieve access context from FastMCP contextaccess_context: AccessContext=awaitctx.get_state("keycardai")
ifaccess_context.has_errors():
return {"error": f"Token exchange failed: {access_context.get_errors()}"}
token=access_context.access("https://www.googleapis.com/calendar/v3").access_tokenasyncwithhttpx.AsyncClient() asclient:
response=awaitclient.get(
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
returnresponse.json()

Standard MCP

importosimporthttpxfrommcp.server.fastmcpimportFastMCP, Contextfromkeycardai.mcp.server.authimportAuthProvider, AccessContext, ClientSecretauth_provider=AuthProvider(
zone_id="your-zone-id",
mcp_server_name="My Server",
mcp_server_url="http://localhost:8000/",
application_credential=ClientSecret((
os.getenv("KEYCARD_CLIENT_ID"),
os.getenv("KEYCARD_CLIENT_SECRET")
))
)
mcp=FastMCP("My Server")
@mcp.tool()@auth_provider.grant("https://www.googleapis.com/calendar/v3")asyncdefget_calendar_events(access_ctx: AccessContext, ctx: Context) ->dict:
"""Get the user's calendar events with delegated access."""# @grant requires both AccessContext (for tokens) and Context (for request state)ifaccess_ctx.has_errors():
return {"error": f"Token exchange failed: {access_ctx.get_errors()}"}
token=access_ctx.access("https://www.googleapis.com/calendar/v3").access_tokenasyncwithhttpx.AsyncClient() asclient:
response=awaitclient.get(
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
returnresponse.json()
app=auth_provider.app(mcp)

Key difference: In keycardai-mcp, the @grant decorator requires both access_ctx: AccessContext and ctx: Context as function parameters. In keycardai-fastmcp, AccessContext is retrieved from the FastMCP Context via await ctx.get_state("keycardai").

For complete delegated access examples with error handling patterns, see:

Connecting Your AI Client

Configure the remote MCP in your AI client (e.g., Cursor):

{
"mcpServers": {
"my-server": {
"url": "http://localhost:8000/mcp"
}
}
}

Using FastAPI

Mounting a FastMCP server into a larger FastAPI service introduces a few gotchas, particularly related to the various OAuth metadata endpoints.

Standards Compliant Approach

Note

Most MCP clients expect standards-compliance. Follow this approach if you're using those clients or the official MCP SDKs.

The OAuth spec declares that your metadata must be exposed at the root of your service.

/.well-known/oauth-protected-resource

This causes a problem when you're mounting multiple APIs or MCP servers to a common FastAPI service. Each API or MCP Server will potentially have their own OAuth metadata.

The OAuth spec defines that the metadata for each individual service should be exposed as an extension to the base well-known URI. For example:

/.well-known/oauth-protected-resource/api
/.well-known/oauth-protected-resource/mcp-server/mcp

To ensure FastMCP and FastAPI produce this, you need to ensure your routing is defined in a specific way:

fromfastmcpimportFastMCPfromfastapiimportFastAPImcp=FastMCP("MCP Server")
mcp_app=mcp.http_app() # DO NOT specify a path hereapp=FastAPI(title="API", lifespan=mcp_app.lifespan)
# You MUST mount the MCP's `http_app` to the full path for FastMCP to expose the# OAuth metadata correctly.app.mount("/mcp-server/mcp", mcp_app)

Custom, Non Standards Compliant, Approach

Warning

This is not advised. Only follow this if you know for sure you need flexibility outside of what the spec requires.

If you've built custom clients or need to mount the metadata at a different, non standards compliant, location, you can do that manually.

Mounting at a Custom Root

fromfastmcpimportFastMCPfromfastapiimportFastAPIfromkeycardai.mcp.server.routers.metadataimportwell_known_metadata_mountauth_provider=AuthProvider(
zone_id="your-zone-id",
mcp_server_name="My Server",
mcp_base_url="http://127.0.0.1:8000/"
)
auth=auth_provider.get_remote_auth_provider()
mcp=FastMCP("MCP Server", auth=auth)
mcp_app=mcp.http_app()
app=FastAPI(title="API", lifespan=mcp_app.lifespan)
app.mount(
"/custom-well-known",
well_known_metadata_mount(issuer=auth.zone_url),
)

which will produce the following endpoints

/custom-well-known/oauth-protected-resource
/custom-well-known/oauth-authorization-server

Mounting at a Specific URI

If you need even more control, you can mount the individual routes at a specific URI.

fromfastmcpimportFastMCPfromfastapiimportFastAPIfromkeycardai.mcp.server.routers.metadataimport (
well_known_authorization_server_route,
well_known_protected_resource_route,
)
auth_provider=AuthProvider(
zone_id="your-zone-id",
mcp_server_name="My Server",
mcp_base_url="http://127.0.0.1:8000/"
)
auth=auth_provider.get_remote_auth_provider()
mcp=FastMCP("MCP Server", auth=auth)
mcp_app=mcp.http_app()
app=FastAPI(title="API", lifespan=mcp_app.lifespan)
app.router.routes.append(
well_known_protected_resource_route(
path="/my/custom/path/to/well-known/oauth-protected-resource",
issuer=auth.zone_url,
)
)
app.router.routes.append(
well_known_authorization_server_route(
path="/my/custom/path/to/well-known/oauth-authorization-server",
issuer=auth.zone_url,
)
)

which will produce the following endpoints

/my/custom/path/to/well-known/oauth-protected-resource
/my/custom/path/to/well-known/oauth-authorization-server

FAQ

How to test the MCP server with modelcontextprotocol/inspector?

When testing your MCP server with the modelcontextprotocol/inspector, you may need to configure CORS to allow the inspector's web interface to access your protected endpoints from localhost.

Note: This applies specifically to keycardai-mcp. When using keycardai-fastmcp, no middleware is currently required as FastMCP permits access to metadata endpoints by default.

fromstarlette.middlewareimportMiddlewarefromstarlette.middleware.corsimportCORSMiddlewaremiddleware= [
Middleware(
CORSMiddleware,
allow_origins=["*"], # For local dev onlyallow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
]
app=auth_provider.app(mcp, middleware=middleware)

Important: The allow_origins=["*"] setting is for local development only. In production, restrict to specific domains.

Documentation

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

About

Keycard Python SDK

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages