diff --git a/docs/docs.json b/docs/docs.json
index dd5c4bb..034686e 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -35,8 +35,8 @@
"group": "Examples",
"pages": [
"examples/fastmcp-integration",
- "examples/dynamic-client-registration",
- "examples/token-exchange"
+ "examples/mcp-client-usage",
+ "examples/mcp-server-auth"
]
}
],
@@ -44,10 +44,31 @@
},
{
"groups": [
+ {
+ "group": "MCP Server",
+ "pages": [
+ "sdk/keycardai-mcp-server-auth-provider",
+ "sdk/keycardai-mcp-server-auth-application_credentials",
+ "sdk/keycardai-mcp-server-exceptions"
+ ]
+ },
+ {
+ "group": "FastMCP Integration",
+ "pages": [
+ "sdk/keycardai-mcp-integrations-fastmcp-provider"
+ ]
+ },
+ {
+ "group": "MCP Client",
+ "pages": [
+ "sdk/keycardai-mcp-client-client",
+ "sdk/keycardai-mcp-client-session",
+ "sdk/keycardai-mcp-client-manager"
+ ]
+ },
{
"group": "OAuth Package",
"pages": [
- "sdk/keycardai-oauth-__init__",
{
"group": "Client",
"pages": [
@@ -57,7 +78,6 @@
{
"group": "HTTP Transport",
"pages": [
- "sdk/keycardai-oauth-http-__init__",
"sdk/keycardai-oauth-http-auth",
"sdk/keycardai-oauth-http-transport"
]
@@ -65,7 +85,6 @@
{
"group": "Types & Models",
"pages": [
- "sdk/keycardai-oauth-types-__init__",
"sdk/keycardai-oauth-types-models",
"sdk/keycardai-oauth-types-oauth"
]
@@ -73,7 +92,6 @@
{
"group": "Utilities",
"pages": [
- "sdk/keycardai-oauth-utils-__init__",
"sdk/keycardai-oauth-utils-bearer",
"sdk/keycardai-oauth-utils-crypto",
"sdk/keycardai-oauth-utils-jwt",
@@ -82,20 +100,6 @@
},
"sdk/keycardai-oauth-exceptions"
]
- },
- {
- "group": "MCP Package",
- "pages": [
- "sdk/keycardai-mcp"
- ]
- },
- {
- "group": "FastMCP Integration",
- "pages": [
- "sdk/keycardai-mcp-integrations-fastmcp-__init__",
- "sdk/keycardai-mcp-integrations-fastmcp-middleware",
- "sdk/keycardai-mcp-integrations-fastmcp-provider"
- ]
}
],
"tab": "API Reference"
diff --git a/docs/examples/dynamic-client-registration.mdx b/docs/examples/dynamic-client-registration.mdx
deleted file mode 100644
index 66bdfb6..0000000
--- a/docs/examples/dynamic-client-registration.mdx
+++ /dev/null
@@ -1,116 +0,0 @@
----
-title: "Dynamic Client Registration"
-description: "How to register OAuth clients dynamically using the Keycard OAuth SDK"
----
-
-# Dynamic Client Registration
-
-This example demonstrates how to use OAuth 2.0 Dynamic Client Registration (RFC 7591) to register a new client with the authorization server.
-
-## Basic Registration
-
-```python
-from keycardai.oauth import Client
-
-with Client("https://your-keycard-zone.com") as client:
- response = client.register_client(client_name="Test Client")
-
- print(f"Client registered: {response.client_id}")
- return response
-```
-
-```python
-from keycardai.oauth import AsyncClient, ClientRegistrationRequest
-
-async def register_client():
- async with AsyncClient("https://your-keycard-zone.com") as client:
- # Register the client
- response = await client.register_client(client_name="My Application",
- redirect_uris=["https://myapp.com/callback"],
- grant_types=["authorization_code", "refresh_token"],
- response_types=["code"],
- scope="openid profile email")
-
- print(f"Client ID: {response.client_id}")
- print(f"Client Secret: {response.client_secret}")
- print(f"Registration Access Token: {response.registration_access_token}")
-
- return response
-
-# Run the registration
-import asyncio
-client_info = asyncio.run(register_client())
-```
-
-## Advanced Registration with Custom Metadata
-
-```python
-from keycardai.oauth import AsyncClient, ClientRegistrationRequest, TokenEndpointAuthMethod
-
-async def register_advanced_client():
- async with AsyncClient("https://your-keycard-zone.com") as client:
- registration_request = ClientRegistrationRequest(
- client_name="Advanced OAuth Client",
- redirect_uris=[
- "https://myapp.com/callback",
- "https://myapp.com/silent-callback"
- ],
- grant_types=["authorization_code", "refresh_token", "client_credentials"],
- response_types=["code"],
- scope="openid profile email api:read api:write",
- token_endpoint_auth_method=TokenEndpointAuthMethod.CLIENT_SECRET_POST,
- application_type="web",
- contacts=["admin@myapp.com"],
- logo_uri="https://myapp.com/logo.png",
- policy_uri="https://myapp.com/privacy",
- tos_uri="https://myapp.com/terms"
- )
-
- response = await client.register_client(registration_request)
-
- # Store these securely!
- print("=== Client Registration Successful ===")
- print(f"Client ID: {response.client_id}")
- print(f"Client Secret: {response.client_secret}")
- print(f"Client ID Issued At: {response.client_id_issued_at}")
- print(f"Client Secret Expires At: {response.client_secret_expires_at}")
-
- return response
-
-# Run the advanced registration
-import asyncio
-client_info = asyncio.run(register_advanced_client())
-```
-
-## Error Handling
-
-```python
-from keycardai.oauth import AsyncClient, ClientRegistrationRequest, OAuthError
-
-async def register_with_error_handling():
- async with AsyncClient("https://your-keycard-zone.com") as client:
- try:
- registration_request = ClientRegistrationRequest(
- client_name="Test Client",
- redirect_uris=["https://myapp.com/callback"],
- grant_types=["authorization_code"],
- response_types=["code"],
- scope="openid profile"
- )
-
- response = await client.register_client(registration_request)
- print("Registration successful!")
- return response
-
- except OAuthError as e:
- print(f"OAuth error: {e}")
- # Handle specific OAuth errors
- except Exception as e:
- print(f"Unexpected error: {e}")
- # Handle other errors
-
-# Run with error handling
-import asyncio
-asyncio.run(register_with_error_handling())
-```
-For token exchange examples, see the [Token Exchange guide](/examples/token-exchange).
diff --git a/docs/examples/fastmcp-integration.mdx b/docs/examples/fastmcp-integration.mdx
index d153fe8..bad9f21 100644
--- a/docs/examples/fastmcp-integration.mdx
+++ b/docs/examples/fastmcp-integration.mdx
@@ -5,94 +5,86 @@ description: "How to use Keycard OAuth with FastMCP servers for automated token
# FastMCP Integration
-Simple examples showing how to use Keycard OAuth components with FastMCP for automated token exchange.
+A step-by-step guide to create an authenticated MCP server with FastMCP and Keycard.
-## Installation
+## Quick Start
+
+### Step 1: Create Project
```bash
-uv add keycardai-mcp-fastmcp keycardai-oauth fastmcp
+uv init --package my-mcp-server
+cd my-mcp-server
```
-## Core Components
+### Step 2: Install Dependencies
-### 1. Auth Provider
+```bash
+uv add keycardai-mcp-fastmcp fastmcp
+```
-Handles JWT authentication, user verification, and provides the grant decorator for token exchange:
+### Step 3: Create Your Server
+
+Create `src/my_mcp_server/__init__.py`:
```python
from keycardai.mcp.integrations.fastmcp import AuthProvider
+from fastmcp import FastMCP
-# Basic setup
+# Configure Keycard authentication
auth_provider = AuthProvider(
- zone_id="your-zone-id",
+ zone_id="your-zone-id", # Get this from console.keycard.ai
mcp_server_name="My Server",
mcp_base_url="http://localhost:8000/"
)
-# Get RemoteAuthProvider for FastMCP
+# Get auth provider for FastMCP
auth = auth_provider.get_remote_auth_provider()
-```
-### 2. Grant Decorator
+# Create authenticated MCP server
+mcp = FastMCP("My Server", auth=auth)
-Automatically exchanges user tokens for resource-specific tokens:
+@mcp.tool()
+def hello_world(name: str) -> str:
+ """Say hello to someone."""
+ return f"Hello, {name}!"
-```python
-from fastmcp import Context
+def main():
+ """Entry point for the MCP server."""
+ mcp.run(transport="streamable-http")
+```
-@auth_provider.grant("https://api.example.com")
-async def my_tool(ctx: Context):
- # Access tokens through context namespace
- access_context = ctx.get_state("keycardai")
-
- # Check for errors
- if access_context.has_errors():
- return {"error": "Failed to obtain access token"}
-
- # Get the token
- token = access_context.access("https://api.example.com").access_token
- return {"token": token}
+### Step 4: Run Your Server
+
+```bash
+uv run my-mcp-server
```
-### 3. Delegated Access with Client Credentials
+Your authenticated MCP server is now running on `http://localhost:8000`!
-For production use with delegated access to external APIs:
+## Delegated Access to External APIs
-```python
-from keycardai.mcp.integrations.fastmcp import AuthProvider, ClientSecret, BasicAuth
-import os
+To access external APIs on behalf of authenticated users, use the `@grant` decorator.
-auth_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")
- ))
-)
-```
+### Step 1: Configure Client Credentials
-**Why this approach is best:**
+Get your client credentials from [console.keycard.ai](https://console.keycard.ai) and set them:
+
+```bash
+export KEYCARD_CLIENT_ID="your-client-id"
+export KEYCARD_CLIENT_SECRET="your-client-secret"
+```
-- ✅ **Unified interface**: Single AuthProvider for both authentication and authorization
-- ✅ **Protocol-based**: Supports multiple credential types (ClientSecret, WebIdentity, EKSWorkloadIdentity)
-- ✅ **Type-safe**: Better autocomplete and type hints
-- ✅ **Error handling**: Built-in error tracking per resource
+### Step 2: Add Delegated Access
-## Basic Server Setup
+Update `src/my_mcp_server/__init__.py`:
```python
import os
from fastmcp import FastMCP, Context
-from keycardai.mcp.integrations.fastmcp import (
- AuthProvider,
- AccessContext,
- ClientSecret,
-)
+from keycardai.mcp.integrations.fastmcp import AuthProvider, AccessContext, ClientSecret
import httpx
-# Configure auth provider with client credentials for delegated access
+# Configure with client credentials for delegated access
auth_provider = AuthProvider(
zone_id="your-zone-id",
mcp_server_name="My Server",
@@ -103,28 +95,28 @@ auth_provider = AuthProvider(
))
)
-# Get RemoteAuthProvider for FastMCP
auth = auth_provider.get_remote_auth_provider()
-
-# Create authenticated FastMCP server
mcp = FastMCP("My Server", auth=auth)
-# Define tool with automatic token exchange
+@mcp.tool()
+def hello_world(name: str) -> str:
+ """Say hello to someone."""
+ return f"Hello, {name}!"
+
+# Tool with delegated access to external API
@mcp.tool()
@auth_provider.grant("https://www.googleapis.com/calendar/v3")
async def get_calendar_events(ctx: Context, maxResults: int = 10) -> dict:
"""Get user's calendar events."""
- # Get access context
access_context: AccessContext = ctx.get_state("keycardai")
- # Check for errors
if access_context.has_errors():
return {"error": f"Token exchange failed: {access_context.get_errors()}"}
- # Get the token
+ # Get delegated token for Google Calendar API
token = access_context.access("https://www.googleapis.com/calendar/v3").access_token
- # Make API call
+ # Call Google Calendar API on behalf of user
async with httpx.AsyncClient() as client:
response = await client.get(
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
@@ -134,93 +126,16 @@ async def get_calendar_events(ctx: Context, maxResults: int = 10) -> dict:
response.raise_for_status()
return response.json()
-# Run server
-if __name__ == "__main__":
+def main():
+ """Entry point for the MCP server."""
mcp.run(transport="streamable-http")
```
-## Configuration
-
-Set environment variables:
+### Step 3: Run with Credentials
```bash
-export ZONE_URL="https://your-zone.keycard.cloud"
-export MCP_SERVER_URL="https://your-server.com/mcp"
-export MCP_SERVER_NAME="My Server"
-```
-
-## Multiple Resource Access
-
-Access multiple APIs with a single decorator:
-
-```python
-@mcp.tool()
-@auth_provider.grant(["https://www.googleapis.com/calendar/v3", "https://www.googleapis.com/drive/v3"])
-async def get_calendar_and_drive(ctx: Context):
- access_context: AccessContext = ctx.get_state("keycardai")
-
- # Check for errors
- if access_context.has_errors():
- return {"error": f"Token exchange failed: {access_context.get_errors()}"}
-
- # Get tokens for both resources
- calendar_token = access_context.access("https://www.googleapis.com/calendar/v3").access_token
- drive_token = access_context.access("https://www.googleapis.com/drive/v3").access_token
-
- return {
- "calendar_headers": {"Authorization": f"Bearer {calendar_token}"},
- "drive_headers": {"Authorization": f"Bearer {drive_token}"}
- }
-
-# Or access them separately with single resource grants
-@mcp.tool()
-@auth_provider.grant("https://www.googleapis.com/calendar/v3")
-async def get_calendar(ctx: Context):
- access_context: AccessContext = ctx.get_state("keycardai")
-
- if access_context.has_errors():
- return {"error": "Token exchange failed"}
-
- token = access_context.access("https://www.googleapis.com/calendar/v3").access_token
- return {"calendar_headers": {"Authorization": f"Bearer {token}"}}
-```
-
-## Error Handling
-
-```python
-import httpx
-from keycardai.mcp.server.exceptions import ResourceAccessError
-
-@mcp.tool()
-@auth_provider.grant("https://api.example.com")
-async def api_call(ctx: Context):
- # Get access context
- access_context: AccessContext = ctx.get_state("keycardai")
-
- # Check for token exchange errors
- if access_context.has_errors():
- errors = access_context.get_errors()
- return {
- "error": "Failed to obtain access token",
- "details": errors
- }
-
- try:
- # Get token and make API call
- token = access_context.access("https://api.example.com").access_token
-
- async with httpx.AsyncClient() as client:
- response = await client.get(
- "https://api.example.com/data",
- headers={"Authorization": f"Bearer {token}"}
- )
- response.raise_for_status()
- return response.json()
- except ResourceAccessError as e:
- return {"error": f"Resource access error: {e}"}
- except httpx.HTTPError as e:
- return {"error": f"API error: {e}"}
- except Exception as e:
- return {"error": f"Unexpected error: {e}"}
+export KEYCARD_CLIENT_ID="your-client-id"
+export KEYCARD_CLIENT_SECRET="your-client-secret"
+uv run my-mcp-server
```
diff --git a/docs/examples/mcp-client-usage.mdx b/docs/examples/mcp-client-usage.mdx
new file mode 100644
index 0000000..fb76937
--- /dev/null
+++ b/docs/examples/mcp-client-usage.mdx
@@ -0,0 +1,70 @@
+---
+title: "MCP Client Usage"
+description: "How to use the Keycard MCP client to connect to authenticated MCP servers"
+---
+
+# MCP Client Usage
+
+A step-by-step guide to connect to authenticated MCP servers using the Keycard MCP client.
+
+## Quick Start
+
+### Step 1: Create Project
+
+```bash
+uv init --package my-mcp-client
+cd my-mcp-client
+```
+
+### Step 2: Install Dependencies
+
+```bash
+uv add keycardai-mcp
+```
+
+### Step 3: Create Your Client
+
+Create `src/my_mcp_client/__init__.py`:
+
+```python
+import asyncio
+from keycardai.mcp.client import Client
+
+# Configure your MCP servers
+servers = {
+ "my-server": {
+ "url": "http://localhost:8000/mcp",
+ "transport": "http",
+ "auth": {"type": "oauth"}
+ }
+}
+
+async def run():
+ async with Client(servers) as client:
+ # List available tools
+ tools = await client.list_tools()
+ print(f"Available tools: {len(tools)}")
+
+ for tool_info in tools:
+ print(f" - {tool_info.tool.name} (from {tool_info.server})")
+
+ # Call a tool by name
+ if tools:
+ result = await client.call_tool(tools[0].tool.name, {})
+ print(f"Result: {result}")
+
+def main():
+ """Entry point for the MCP client."""
+ asyncio.run(run())
+```
+
+### Step 4: Run Your Client
+
+```bash
+uv run my-mcp-client
+```
+
+The client will automatically open your browser for OAuth authentication, then connect and list available tools!
+
+For more advanced usage including web applications, event-driven architectures, and AI agent integrations, see the [client README](https://github.com/keycardai/python-sdk/blob/main/packages/mcp/src/keycardai/mcp/client/README.md).
+
diff --git a/docs/examples/mcp-server-auth.mdx b/docs/examples/mcp-server-auth.mdx
new file mode 100644
index 0000000..be8998c
--- /dev/null
+++ b/docs/examples/mcp-server-auth.mdx
@@ -0,0 +1,142 @@
+---
+title: "MCP Server Authentication"
+description: "How to add Keycard authentication to your MCP server using the server AuthProvider"
+---
+
+# MCP Server Authentication
+
+A step-by-step guide to add Keycard authentication to any MCP server.
+
+## Quick Start
+
+### Step 1: Create Project
+
+```bash
+uv init --package my-secure-mcp
+cd my-secure-mcp
+```
+
+### Step 2: Install Dependencies
+
+```bash
+uv add keycardai-mcp fastmcp uvicorn
+```
+
+### Step 3: Create Your Server
+
+Create `src/my_secure_mcp/__init__.py`:
+
+```python
+from mcp.server.fastmcp import FastMCP
+from keycardai.mcp.server.auth import AuthProvider
+import uvicorn
+
+# Create your MCP server
+mcp = FastMCP("My Secure MCP Server")
+
+@mcp.tool()
+def my_protected_tool(data: str) -> str:
+ """A tool that requires authentication."""
+ return f"Processed: {data}"
+
+# Add Keycard authentication
+auth_provider = AuthProvider(
+ zone_id="your-zone-id", # Get this from console.keycard.ai
+ mcp_server_name="My Secure MCP Server"
+)
+
+# Create authenticated app
+app = auth_provider.app(mcp)
+
+def main():
+ """Entry point for the MCP server."""
+ uvicorn.run(app, host="0.0.0.0", port=8000)
+```
+
+### Step 4: Run Your Server
+
+```bash
+uv run my-secure-mcp
+```
+
+Your authenticated MCP server is now running on `http://localhost:8000`!
+
+## Delegated Access to External APIs
+
+To access external APIs on behalf of authenticated users, use the `@grant` decorator.
+
+### Step 1: Configure Client Credentials
+
+Get your client credentials from [console.keycard.ai](https://console.keycard.ai) and set them:
+
+```bash
+export KEYCARD_CLIENT_ID="your-client-id"
+export KEYCARD_CLIENT_SECRET="your-client-secret"
+```
+
+### Step 2: Add Delegated Access
+
+Update `src/my_secure_mcp/__init__.py`:
+
+```python
+import os
+from mcp.server.fastmcp import FastMCP
+from keycardai.mcp.server.auth import AuthProvider, ClientSecret, AccessContext
+import httpx
+import uvicorn
+
+# Configure with client credentials for delegated access
+auth_provider = 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 Server")
+
+@mcp.tool()
+def my_protected_tool(data: str) -> str:
+ """A tool that requires authentication."""
+ return f"Processed: {data}"
+
+# Tool with delegated access to external API
+@mcp.tool()
+@auth_provider.grant("https://www.googleapis.com/calendar/v3")
+async def get_calendar(access_ctx: AccessContext = None):
+ """Get user's calendar events with delegated access."""
+ if access_ctx.has_errors():
+ return {"error": "Failed to obtain access token"}
+
+ # Get delegated token for Google Calendar API
+ token = access_ctx.access("https://www.googleapis.com/calendar/v3").access_token
+
+ # Call Google Calendar API on behalf of user
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events",
+ headers={"Authorization": f"Bearer {token}"}
+ )
+ response.raise_for_status()
+ return response.json()
+
+# Create authenticated app
+app = auth_provider.app(mcp)
+
+def main():
+ """Entry point for the MCP server."""
+ uvicorn.run(app, host="0.0.0.0", port=8000)
+```
+
+### Step 3: Run with Credentials
+
+```bash
+export KEYCARD_CLIENT_ID="your-client-id"
+export KEYCARD_CLIENT_SECRET="your-client-secret"
+uv run my-secure-mcp
+```
+
+For more details on configuration options and credential types, see the [MCP package documentation](https://github.com/keycardai/python-sdk/tree/main/packages/mcp).
+
diff --git a/docs/examples/token-exchange.mdx b/docs/examples/token-exchange.mdx
deleted file mode 100644
index 95dd43b..0000000
--- a/docs/examples/token-exchange.mdx
+++ /dev/null
@@ -1,175 +0,0 @@
----
-title: "Token Exchange"
-description: "How to exchange tokens using OAuth 2.0 Token Exchange (RFC 8693)"
----
-
-# Token Exchange
-
-This example demonstrates how to use OAuth 2.0 Token Exchange (RFC 8693) for token delegation scenarios, from simple resource access to advanced service impersonation.
-
-## Simple Token Delegation
-
-The easiest way to get started with token exchange is using the `get_access_token_for_resource` helper method:
-
-```python
-from keycardai.oauth import Client
-
-def simple_token_delegation():
- """
- Simple token delegation example - exchange a user token for resource-specific access.
- """
- with Client("https://zoneid.keycard.cloud") as client:
- # Request access to Google Calendar API
- calendar_token = client.get_access_token_for_resource(
- "https://www.googleapis.com/auth/calendar.readonly",
- "user_access_token"
- )
- print(f"Calendar Access Token: {calendar_token}")
-
- # Request access to GitHub API
- github_token = client.get_access_token_for_resource(
- "https://api.github.com",
- "user_access_token"
- )
- print(f"GitHub Access Token: {github_token}")
-
- return calendar_token, github_token
-
-# Run simple delegation
-calendar_token, github_token = simple_token_delegation()
-```
-
-## Async Simple Token Delegation
-
-```python
-from keycardai.oauth import AsyncClient
-
-async def async_simple_token_delegation():
- """
- Async version of simple token delegation.
- """
- async with AsyncClient("https://zoneid.keycard.cloud") as client:
- # Request access to Google Calendar API
- calendar_token = await client.get_access_token_for_resource(
- "https://www.googleapis.com/auth/calendar.readonly",
- "user_access_token"
- )
- print(f"Calendar Access Token: {calendar_token}")
-
- # Request access to GitHub API
- github_token = await client.get_access_token_for_resource(
- "https://api.github.com",
- "user_access_token"
- )
- print(f"GitHub Access Token: {github_token}")
-
- return calendar_token, github_token
-
-# Run async simple delegation
-import asyncio
-calendar_token, github_token = asyncio.run(async_simple_token_delegation())
-```
-
-## Advanced Token Exchange with Service Impersonation
-
-This example shows a real-world scenario where a service needs to impersonate a user to access multiple APIs on their behalf:
-
-```python
-from keycardai.oauth import Client, AsyncClient
-from keycardai.oauth.types.models import TokenExchangeRequest
-
-def service_impersonation_example():
- """
- Advanced token exchange example demonstrating service impersonation
- where a service exchanges tokens to access multiple resources on behalf of a user.
- """
- with Client("https://zoneid.keycard.cloud") as client:
-
- # Method 1: Using TokenExchangeRequest object for full control
- request = TokenExchangeRequest(
- subject_token="user_original_token_xyz123",
- subject_token_type="urn:ietf:params:oauth:token-type:access_token",
- audience="api.microservice.company.com",
- actor_token="service_identity_token_abc456",
- actor_token_type="urn:ietf:params:oauth:token-type:access_token",
- requested_token_type="urn:ietf:params:oauth:token-type:access_token",
- scope="calendar:read drive:write slack:message",
- resource="https://apis.company.com/user-data"
- )
-
- response = client.exchange_token(request)
- print(f"Impersonated Token: {response.access_token}")
- print(f"Token Type: {response.token_type}")
- print(f"Expires In: {response.expires_in} seconds")
- print(f"Issued Token Type: {response.issued_token_type}")
-
- # Method 2: Using keyword arguments for simpler cases
- delegated_response = client.exchange_token(
- subject_token="user_original_token_xyz123",
- subject_token_type="urn:ietf:params:oauth:token-type:access_token",
- audience="api.notifications.company.com",
- scope="notifications:send",
- timeout=30.0
- )
-
- print(f"Delegated Token: {delegated_response.access_token}")
-
- return response, delegated_response
-
-# Run the service impersonation
-impersonated_token, delegated_token = service_impersonation_example()
-```
-
-## Async Advanced Token Exchange
-
-```python
-async def async_service_impersonation_example():
- """
- Async version of advanced token exchange with service impersonation.
- """
- async with AsyncClient("https://zoneid.keycard.cloud") as client:
-
- # Method 1: Using TokenExchangeRequest object for full control
- request = TokenExchangeRequest(
- subject_token="user_original_token_xyz123",
- subject_token_type="urn:ietf:params:oauth:token-type:access_token",
- audience="api.microservice.company.com",
- actor_token="service_identity_token_abc456",
- actor_token_type="urn:ietf:params:oauth:token-type:access_token",
- requested_token_type="urn:ietf:params:oauth:token-type:access_token",
- scope="calendar:read drive:write slack:message",
- resource="https://apis.company.com/user-data"
- )
-
- response = await client.exchange_token(request)
- print(f"Impersonated Token: {response.access_token}")
- print(f"Token Type: {response.token_type}")
- print(f"Expires In: {response.expires_in} seconds")
- print(f"Issued Token Type: {response.issued_token_type}")
-
- # Method 2: Using keyword arguments for simpler cases
- delegated_response = await client.exchange_token(
- subject_token="user_original_token_xyz123",
- subject_token_type="urn:ietf:params:oauth:token-type:access_token",
- audience="api.notifications.company.com",
- scope="notifications:send",
- timeout=30.0
- )
-
- print(f"Delegated Token: {delegated_response.access_token}")
-
- return response, delegated_response
-
-# Run the async service impersonation
-import asyncio
-impersonated_token, delegated_token = asyncio.run(async_service_impersonation_example())
-```
-
-## Key Features Demonstrated
-
-- **Service Impersonation**: Using `actor_token` to impersonate users while maintaining audit trails
-- **Resource-Specific Tokens**: Requesting tokens for specific audiences and resources
-- **Flexible API**: Both `TokenExchangeRequest` objects and keyword arguments supported
-- **Scope Management**: Fine-grained permission control with custom scopes
-- **Token Metadata**: Access to token expiration, type, and issuer information
-- **Async Support**: Full async/await support for high-performance applications
\ No newline at end of file
diff --git a/docs/sdk/keycardai-mcp-client-__init__.mdx b/docs/sdk/keycardai-mcp-client-__init__.mdx
new file mode 100644
index 0000000..954eaf5
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-__init__.mdx
@@ -0,0 +1,8 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `keycardai.mcp.client`
+
+*This module is empty or contains only private/internal implementations.*
diff --git a/docs/sdk/keycardai-mcp-client-auth-__init__.mdx b/docs/sdk/keycardai-mcp-client-auth-__init__.mdx
new file mode 100644
index 0000000..cd5b6f4
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-__init__.mdx
@@ -0,0 +1,9 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `keycardai.mcp.client.auth`
+
+
+Authentication module for MCP client.
diff --git a/docs/sdk/keycardai-mcp-client-auth-coordinators-__init__.mdx b/docs/sdk/keycardai-mcp-client-auth-coordinators-__init__.mdx
new file mode 100644
index 0000000..125c169
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-coordinators-__init__.mdx
@@ -0,0 +1,9 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `keycardai.mcp.client.auth.coordinators`
+
+
+Authentication coordinators.
diff --git a/docs/sdk/keycardai-mcp-client-auth-coordinators-base.mdx b/docs/sdk/keycardai-mcp-client-auth-coordinators-base.mdx
new file mode 100644
index 0000000..114b263
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-coordinators-base.mdx
@@ -0,0 +1,222 @@
+---
+title: base
+sidebarTitle: base
+---
+
+# `keycardai.mcp.client.auth.coordinators.base`
+
+
+Authentication coordination for MCP clients.
+
+## Classes
+
+### `AuthCoordinator`
+
+
+Abstract base for authentication coordination.
+
+Facade coordinating authentication components:
+- EndpointManager: HTTP redirect endpoints (local server or remote)
+- AuthStateStorage: Pending auth state management
+- CompletionRouter: Completion routing and handler invocation
+- Subscribers: Observer notifications for completion events
+
+
+**Methods:**
+
+#### `storage`
+
+```python
+storage(self) -> NamespacedStorage
+```
+
+Get the coordinator's storage (for backward compatibility).
+
+Returns the underlying storage used by state_store.
+
+
+#### `endpoint_type`
+
+```python
+endpoint_type(self) -> str
+```
+
+Type of endpoint this coordinator uses.
+
+**Returns:**
+- "local" for LocalAuthCoordinator, "remote" for StarletteAuthCoordinator
+
+
+#### `requires_synchronous_cleanup`
+
+```python
+requires_synchronous_cleanup(self) -> bool
+```
+
+Whether this coordinator requires synchronous callback cleanup.
+
+Override in subclasses if they need cleanup to complete before
+callback response (e.g., LocalAuthCoordinator needs this to avoid
+race conditions with blocking wait patterns).
+
+**Returns:**
+- False by default (asynchronous cleanup is fine)
+
+
+#### `create_context`
+
+```python
+create_context(self, context_id: str, metadata: dict[str, Any] | None = None) -> Context
+```
+
+Create or retrieve a context with properly scoped storage.
+
+Idempotent - returns same Context instance for same context_id.
+
+**Args:**
+- `context_id`: Context identifier (e.g., "user\:alice" or just "alice")
+- `metadata`: Optional metadata dict to attach to context (e.g., user info, session data)
+
+**Returns:**
+- Context instance with namespaced storage
+
+
+#### `subscribe`
+
+```python
+subscribe(self, subscriber: CompletionSubscriber) -> None
+```
+
+Subscribe to completion notifications.
+
+
+#### `unsubscribe`
+
+```python
+unsubscribe(self, subscriber: CompletionSubscriber) -> None
+```
+
+Unsubscribe from completion notifications.
+
+
+#### `handle_completion`
+
+```python
+handle_completion(self, params: dict[str, str]) -> dict[str, Any]
+```
+
+Handle authentication completion using completion router.
+
+Delegates to CompletionRouter which retrieves completion metadata from
+storage and invokes the registered handler. Notifies subscribers of
+the result.
+
+**Args:**
+- `params`: Completion parameters (e.g., {"code"\: "...", "state"\: "..."})
+
+**Returns:**
+- Dict with completion result including metadata
+
+**Raises:**
+- `ValueError`: If state is invalid or no handler registered
+
+
+#### `set_auth_pending`
+
+```python
+set_auth_pending(self, context_id: str, server_name: str, auth_metadata: dict[str, Any], ttl: timedelta | None = None) -> None
+```
+
+Signal that authentication is pending for a server.
+
+Delegates to AuthStateStorage for type-safe state management.
+
+**Args:**
+- `context_id`: Context identifier
+- `server_name`: Server name requiring auth
+- `auth_metadata`: Strategy-specific metadata (e.g., authorization_url, state)
+- `ttl`: Optional expiration time
+
+
+#### `get_auth_pending`
+
+```python
+get_auth_pending(self, context_id: str, server_name: str) -> dict[str, Any] | None
+```
+
+Get pending authentication metadata for a server.
+
+Delegates to AuthStateStorage for type-safe state retrieval.
+
+**Args:**
+- `context_id`: Context identifier
+- `server_name`: Server name to check
+
+**Returns:**
+- Auth metadata if pending, None otherwise
+
+
+#### `clear_auth_pending`
+
+```python
+clear_auth_pending(self, context_id: str, server_name: str) -> None
+```
+
+Clear pending authentication state.
+
+Delegates to AuthStateStorage for cleanup.
+
+**Args:**
+- `context_id`: Context identifier
+- `server_name`: Server name
+
+
+#### `register_completion_route`
+
+```python
+register_completion_route(self, routing_key: str, handler_name: str, storage_namespace: str, context_id: str, server_name: str, routing_param: str = 'state', handler_kwargs: dict[str, Any] | None = None, metadata: dict[str, Any] | None = None, ttl: timedelta | None = None) -> None
+```
+
+Register completion routing info for stateless execution.
+
+Delegates to AuthStateStorage for type-safe route registration.
+
+**Args:**
+- `routing_key`: Key to route completion (e.g., OAuth state parameter value)
+- `handler_name`: Name of registered completion handler (e.g., "oauth_completion")
+- `storage_namespace`: Storage namespace path for strategy data
+- `context_id`: Context identifier
+- `server_name`: Server name for auth pending cleanup
+- `routing_param`: Query parameter name for routing (default\: "state")
+- `handler_kwargs`: Optional kwargs to pass to completion handler (e.g., client_factory)
+- `metadata`: Optional metadata
+- `ttl`: Expiration time
+
+
+#### `get_redirect_uris`
+
+```python
+get_redirect_uris(self) -> list[str] | None
+```
+
+Get OAuth redirect URIs for this coordinator.
+
+Delegates to EndpointManager. For coordinators with infrastructure
+(e.g., local server), this may start it if not already running.
+
+**Returns:**
+- List of redirect URIs (e.g., ["http://localhost:8080/callback"])
+
+
+#### `handle_redirect`
+
+```python
+handle_redirect(self, authorization_url: str, metadata: dict[str, Any])
+```
+
+Handle user redirect to authorization URL.
+
+**Args:**
+- `authorization_url`: URL to redirect user to
+- `metadata`: Flow metadata (e.g., server_name, state)
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-coordinators-endpoint_managers.mdx b/docs/sdk/keycardai-mcp-client-auth-coordinators-endpoint_managers.mdx
new file mode 100644
index 0000000..2005481
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-coordinators-endpoint_managers.mdx
@@ -0,0 +1,175 @@
+---
+title: endpoint_managers
+sidebarTitle: endpoint_managers
+---
+
+# `keycardai.mcp.client.auth.coordinators.endpoint_managers`
+
+
+Redirect endpoint management for OAuth flows.
+
+## Classes
+
+### `EndpointManager`
+
+
+Abstract base for managing OAuth redirect endpoints.
+
+
+**Methods:**
+
+#### `get_redirect_uris`
+
+```python
+get_redirect_uris(self) -> list[str]
+```
+
+Get available redirect URIs for OAuth registration.
+
+May start infrastructure (e.g., local server) if needed.
+
+**Returns:**
+- List of redirect URIs
+
+
+#### `initiate_redirect`
+
+```python
+initiate_redirect(self, url: str, metadata: dict[str, Any]) -> None
+```
+
+Initiate user redirect to authorization URL.
+
+**Args:**
+- `url`: Authorization URL to redirect user to
+- `metadata`: Flow metadata (e.g., state, server_name)
+
+
+### `LocalEndpointManager`
+
+
+Local HTTP server endpoint manager for CLI/desktop applications.
+
+Runs a local HTTP server to receive OAuth callbacks. The server is
+shared across all auth flows and routes callbacks to a handler.
+
+Key behaviors:
+- Starts local HTTP server on demand (lazy initialization)
+- Opens user's browser to authorization URL (configurable)
+- BLOCKS in initiate_redirect() until callback is received (configurable)
+- Tracks pending flows by OAuth state parameter
+
+Use for: CLI apps, desktop apps, local development.
+
+
+**Methods:**
+
+#### `set_callback_handler`
+
+```python
+set_callback_handler(self, handler: Any) -> None
+```
+
+Set the completion handler to invoke when OAuth callbacks are received.
+
+The handler should be an async function that takes params dict
+and returns a result dict.
+
+**Args:**
+- `handler`: Async completion handler function (e.g., coordinator.handle_completion)
+
+
+#### `get_redirect_uris`
+
+```python
+get_redirect_uris(self) -> list[str]
+```
+
+Get redirect URI for local server.
+
+Starts the local server if not already running (lazy initialization).
+
+**Returns:**
+- List with single redirect URI (e.g., ["http://localhost:8080/callback"])
+
+
+#### `initiate_redirect`
+
+```python
+initiate_redirect(self, url: str, metadata: dict[str, Any]) -> None
+```
+
+Initiate OAuth redirect (configurable browser opening and blocking).
+
+Behavior depends on configuration:
+- auto_open_browser=True: Opens browser automatically
+- auto_open_browser=False: Logs URL for manual opening
+- block_until_callback=True: Blocks until callback received
+- block_until_callback=False: Returns immediately
+
+**Args:**
+- `url`: Authorization URL to open in browser
+- `metadata`: Flow metadata including server_name
+
+**Raises:**
+- `TimeoutError`: If authorization not completed within 300 seconds (when blocking)
+
+
+#### `shutdown`
+
+```python
+shutdown(self) -> None
+```
+
+Stop local callback server.
+
+Gracefully shuts down the HTTP server and cleans up resources.
+
+
+### `RemoteEndpointManager`
+
+
+Remote endpoint manager for web applications.
+
+Uses an external redirect URI (provided by the web application).
+Does not run its own server or open browsers - the web app handles
+all HTTP interactions.
+
+Key behaviors:
+- Returns configured redirect URI
+- initiate_redirect() is a no-op (web app handles redirect via HTTP)
+- No blocking behavior (suitable for async web frameworks)
+
+Use for: Web apps, APIs, microservices (FastAPI, Flask, Starlette, etc.)
+
+
+**Methods:**
+
+#### `get_redirect_uris`
+
+```python
+get_redirect_uris(self) -> list[str]
+```
+
+Get configured redirect URI.
+
+**Returns:**
+- List with single redirect URI
+
+
+#### `initiate_redirect`
+
+```python
+initiate_redirect(self, url: str, metadata: dict[str, Any]) -> None
+```
+
+No-op for remote redirects.
+
+Web applications handle redirects via HTTP responses, not by
+opening browsers programmatically. The authorization URL is
+stored in pending auth metadata and returned to the client.
+
+**Args:**
+- `url`: Authorization URL (not used)
+- `metadata`: Flow metadata for logging
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-coordinators-local.mdx b/docs/sdk/keycardai-mcp-client-auth-coordinators-local.mdx
new file mode 100644
index 0000000..8726fff
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-coordinators-local.mdx
@@ -0,0 +1,61 @@
+---
+title: local
+sidebarTitle: local
+---
+
+# `keycardai.mcp.client.auth.coordinators.local`
+
+
+Local completion server coordinator for authentication flows.
+
+## Classes
+
+### `LocalAuthCoordinator`
+
+
+Local completion server coordinator for authentication flows.
+
+Runs a local HTTP server to receive auth completions via LocalEndpointManager.
+Server is SHARED across all contexts using this coordinator.
+
+Use for: CLI apps, desktop apps, local development.
+
+Key behaviors:
+- Opens browser to authorization URL (configurable)
+- Blocks in handle_redirect() until completion arrives (configurable)
+- Requires synchronous cleanup to avoid race conditions (when blocking)
+
+
+**Methods:**
+
+#### `endpoint_type`
+
+```python
+endpoint_type(self) -> str
+```
+
+Type of endpoint: local HTTP server.
+
+
+#### `requires_synchronous_cleanup`
+
+```python
+requires_synchronous_cleanup(self) -> bool
+```
+
+LocalAuthCoordinator requires synchronous cleanup.
+
+This prevents race conditions with the blocking wait pattern
+in handle_redirect() which waits for completion to arrive.
+
+
+#### `shutdown`
+
+```python
+shutdown(self) -> None
+```
+
+Stop local completion server.
+
+Delegates to LocalEndpointManager for graceful shutdown.
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-coordinators-remote.mdx b/docs/sdk/keycardai-mcp-client-auth-coordinators-remote.mdx
new file mode 100644
index 0000000..15673c7
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-coordinators-remote.mdx
@@ -0,0 +1,53 @@
+---
+title: remote
+sidebarTitle: remote
+---
+
+# `keycardai.mcp.client.auth.coordinators.remote`
+
+
+Remote completion coordinator for web applications.
+
+## Classes
+
+### `StarletteAuthCoordinator`
+
+
+Remote completion coordinator for web applications.
+
+Uses RemoteEndpointManager which provides a redirect URI but doesn't
+run its own server. You provide the HTTP endpoint in your web framework.
+
+Use for: Web apps, APIs, microservices (Starlette, FastAPI, Flask, etc.)
+
+Key behaviors:
+- Returns configured redirect URI
+- handle_redirect() is a no-op (web app handles via HTTP)
+- Asynchronous cleanup (suitable for web frameworks)
+
+
+**Methods:**
+
+#### `endpoint_type`
+
+```python
+endpoint_type(self) -> str
+```
+
+Type of endpoint: remote web application.
+
+
+#### `get_completion_endpoint`
+
+```python
+get_completion_endpoint(self)
+```
+
+Get HTTP endpoint handler for OAuth completions.
+
+This handler is protocol-agnostic and works with any auth strategy
+that uses OAuth redirect flows.
+
+**Returns:**
+- Async function that handles HTTP OAuth completion requests
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-events.mdx b/docs/sdk/keycardai-mcp-client-auth-events.mdx
new file mode 100644
index 0000000..47ca968
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-events.mdx
@@ -0,0 +1,54 @@
+---
+title: events
+sidebarTitle: events
+---
+
+# `keycardai.mcp.client.auth.events`
+
+
+
+Subscriber protocol for authentication completion notifications.
+
+Defines the interface and data structures for receiving notifications
+when authentication completions are handled.
+
+
+## Classes
+
+### `CompletionEvent`
+
+
+Event data for authentication completion notifications.
+
+This dataclass encapsulates all relevant information about a
+completion event, allowing subscribers to react appropriately.
+
+
+### `CompletionSubscriber`
+
+
+Protocol for receiving authentication completion notifications.
+
+Classes implementing this protocol can subscribe to an AuthCoordinator
+to receive notifications when completions are handled. This allows for
+decoupled event handling, logging, analytics, UI updates, etc.
+
+
+**Methods:**
+
+#### `on_completion_handled`
+
+```python
+on_completion_handled(self, event: CompletionEvent) -> None
+```
+
+Called when an authentication completion has been handled.
+
+This method is invoked by the AuthCoordinator after a completion
+has been processed, whether successful or not. Implementations
+should not raise exceptions, as they could interfere with the
+completion flow.
+
+**Args:**
+- `event`: Completion event containing all relevant information
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-handlers.mdx b/docs/sdk/keycardai-mcp-client-auth-handlers.mdx
new file mode 100644
index 0000000..16b9f62
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-handlers.mdx
@@ -0,0 +1,250 @@
+---
+title: handlers
+sidebarTitle: handlers
+---
+
+# `keycardai.mcp.client.auth.handlers`
+
+
+Auth completion routing and handlers.
+
+## Functions
+
+### `get_default_handler_registry`
+
+```python
+get_default_handler_registry() -> CompletionHandlerRegistry
+```
+
+
+Get the default global handler registry.
+
+**Returns:**
+- The global CompletionHandlerRegistry instance
+
+
+### `register_completion_handler`
+
+```python
+register_completion_handler(name: str) -> Callable[[CompletionHandlerFunc], CompletionHandlerFunc]
+```
+
+
+Decorator to register a completion handler in the default registry.
+
+This is the main decorator used throughout the SDK to mark functions
+as available for auth completion.
+
+**Args:**
+- `name`: Unique name for the completion handler
+
+**Returns:**
+- Decorator function
+
+
+### `oauth_completion_handler`
+
+```python
+oauth_completion_handler(coordinator: 'AuthCoordinator', storage: 'NamespacedStorage', params: dict[str, str], client_factory: Callable[[], 'AsyncClient'] | None = None, run_cleanup_in_background: bool = True) -> dict[str, Any]
+```
+
+
+Handle OAuth authorization code completion.
+
+This unified completion handler works with any coordinator by retrieving all necessary
+state from storage.
+
+Steps:
+1. Extract code and state from params
+2. Load PKCE state from storage
+3. Exchange authorization code for tokens
+4. Store tokens in strategy storage
+5. Clear pending auth state
+6. Return success result
+
+**Args:**
+- `coordinator`: Auth coordinator (for cleanup)
+- `storage`: Strategy's namespaced storage
+- `params`: Completion parameters (e.g., {"code"\: "...", "state"\: "..."})
+- `client_factory`: Optional factory function that returns an AsyncClient instance.
+ If None, uses default factory that creates AsyncClient()
+- `run_cleanup_in_background`: If True (default), cleanup tasks run asynchronously
+ after response. If False, cleanup runs synchronously.
+ Use False in unit tests to ensure cleanup completes.
+
+**Returns:**
+- Result dict with success status and metadata
+
+**Raises:**
+- `ValueError`: If required parameters are missing or state is invalid
+
+
+## Classes
+
+### `CompletionRouter`
+
+
+Routes auth completions to registered handlers.
+
+Handles both stateful (in-memory) and stateless (storage-based) routing.
+Retrieves completion metadata from storage, invokes the appropriate handler,
+and manages cleanup.
+
+Responsibilities:
+- Retrieve completion routing metadata from storage
+- Look up and invoke registered completion handlers
+- Navigate to correct storage namespace for handler
+- Schedule cleanup of routing metadata
+
+
+**Methods:**
+
+#### `route_completion`
+
+```python
+route_completion(self, coordinator: 'AuthCoordinator', params: dict[str, str]) -> dict[str, Any]
+```
+
+Route completion to appropriate handler.
+
+Retrieves routing metadata from storage, invokes the registered handler,
+and schedules cleanup. The coordinator is passed to handlers for context
+creation and other coordinator operations.
+
+**Args:**
+- `coordinator`: AuthCoordinator instance
+- `params`: Completion parameters (e.g., {"code"\: "...", "state"\: "..."})
+
+**Returns:**
+- Handler result dict merged with completion metadata
+
+**Raises:**
+- `ValueError`: If state is missing, invalid, or handler not found
+
+
+### `CompletionHandlerRegistry`
+
+
+Registry of auth completion handlers.
+
+Manages registration and discovery of completion handler functions that
+can be used to complete authentication flows.
+
+
+**Methods:**
+
+#### `register`
+
+```python
+register(self, name: str, handler: CompletionHandlerFunc | None = None) -> CompletionHandlerFunc | Callable[[CompletionHandlerFunc], CompletionHandlerFunc]
+```
+
+Register a completion handler.
+
+Can be used as a decorator or called directly.
+
+**Args:**
+- `name`: Unique name for the completion handler
+- `handler`: Optional handler function (if not using as decorator)
+
+**Returns:**
+- The handler function (for decorator chaining) or decorator function
+
+
+#### `get`
+
+```python
+get(self, name: str) -> CompletionHandlerFunc
+```
+
+Get a registered completion handler by name.
+
+**Args:**
+- `name`: Name of the completion handler
+
+**Returns:**
+- The completion handler function
+
+**Raises:**
+- `ValueError`: If handler is not registered
+
+
+#### `has`
+
+```python
+has(self, name: str) -> bool
+```
+
+Check if a completion handler is registered.
+
+**Args:**
+- `name`: Name of the completion handler
+
+**Returns:**
+- True if handler is registered, False otherwise
+
+
+#### `list_handlers`
+
+```python
+list_handlers(self) -> list[str]
+```
+
+List all registered completion handler names.
+
+**Returns:**
+- List of handler names
+
+
+#### `unregister`
+
+```python
+unregister(self, name: str) -> None
+```
+
+Unregister a completion handler.
+
+**Args:**
+- `name`: Name of the handler to unregister
+
+
+#### `set_client_factory`
+
+```python
+set_client_factory(self, factory: ClientFactory, handler_name: str | None = None) -> None
+```
+
+Set a client factory for completion handlers.
+
+**Args:**
+- `factory`: Factory function that returns an HTTP client (e.g., AsyncClient)
+- `handler_name`: Optional handler name to set factory for specific handler.
+ If None, sets as default factory for all handlers.
+
+
+#### `get_client_factory`
+
+```python
+get_client_factory(self, handler_name: str) -> ClientFactory | None
+```
+
+Get the client factory for a specific completion handler.
+
+**Args:**
+- `handler_name`: Name of the completion handler
+
+**Returns:**
+- Client factory function or None if not set.
+- Checks handler-specific factory first, then falls back to default.
+
+
+#### `clear_client_factories`
+
+```python
+clear_client_factories(self) -> None
+```
+
+Clear all client factories (both default and handler-specific).
+
+Useful for testing or reconfiguration.
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-oauth-__init__.mdx b/docs/sdk/keycardai-mcp-client-auth-oauth-__init__.mdx
new file mode 100644
index 0000000..0be7872
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-oauth-__init__.mdx
@@ -0,0 +1,9 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `keycardai.mcp.client.auth.oauth`
+
+
+OAuth service modules for MCP client authentication.
diff --git a/docs/sdk/keycardai-mcp-client-auth-oauth-discovery.mdx b/docs/sdk/keycardai-mcp-client-auth-oauth-discovery.mdx
new file mode 100644
index 0000000..e0109c6
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-oauth-discovery.mdx
@@ -0,0 +1,72 @@
+---
+title: discovery
+sidebarTitle: discovery
+---
+
+# `keycardai.mcp.client.auth.oauth.discovery`
+
+
+OAuth discovery service for resource and authorization server metadata.
+
+## Classes
+
+### `OAuthDiscoveryService`
+
+
+Handles OAuth 2.0 discovery protocol.
+
+Responsibilities:
+- Discover protected resource metadata
+- Discover authorization server metadata
+- Cache authorization server metadata
+
+
+**Methods:**
+
+#### `discover_resource`
+
+```python
+discover_resource(self, challenge_response: Response) -> dict[str, Any]
+```
+
+Discover protected resource metadata from 401 challenge.
+
+Fetches the OAuth protected resource metadata from the well-known endpoint
+based on the challenge response URL.
+
+**Args:**
+- `challenge_response`: HTTP Response with 401 status code
+
+**Returns:**
+- Resource metadata including authorization_servers list
+
+**Raises:**
+- `ValueError`: If no authorization servers found in discovery response
+- `httpx.HTTPStatusError`: If discovery request fails
+
+
+#### `discover_auth_server`
+
+```python
+discover_auth_server(self, resource_metadata: dict[str, Any]) -> dict[str, Any]
+```
+
+Discover authorization server metadata.
+
+Uses cached metadata if available, otherwise fetches from the
+authorization server's well-known endpoint and caches it.
+
+**Args:**
+- `resource_metadata`: Resource metadata with authorization_servers list
+
+**Returns:**
+- Authorization server metadata including:
+- - authorization_endpoint
+- - token_endpoint
+- - registration_endpoint
+- - etc.
+
+**Raises:**
+- `ValueError`: If no authorization servers found, required fields missing,
+ or all discovery attempts fail
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-oauth-exchange.mdx b/docs/sdk/keycardai-mcp-client-auth-oauth-exchange.mdx
new file mode 100644
index 0000000..132274b
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-oauth-exchange.mdx
@@ -0,0 +1,53 @@
+---
+title: exchange
+sidebarTitle: exchange
+---
+
+# `keycardai.mcp.client.auth.oauth.exchange`
+
+
+OAuth token exchange service.
+
+## Classes
+
+### `OAuthTokenExchangeService`
+
+
+Handles OAuth 2.0 token exchange.
+
+Responsibilities:
+- Exchange authorization code for access/refresh tokens
+- Store tokens securely
+- Clean up PKCE state after successful exchange
+
+
+**Methods:**
+
+#### `exchange_code_for_tokens`
+
+```python
+exchange_code_for_tokens(self, code: str, state: str, auth_server_metadata: dict[str, Any], client_info: dict[str, Any]) -> dict[str, Any]
+```
+
+Exchange authorization code for access/refresh tokens.
+
+Retrieves PKCE state, exchanges the code for tokens, stores the tokens,
+and cleans up the PKCE state.
+
+**Args:**
+- `code`: Authorization code from OAuth callback
+- `state`: OAuth state parameter (used to retrieve PKCE state)
+- `auth_server_metadata`: Authorization server metadata with token_endpoint
+- `client_info`: Client registration info with client_id
+
+**Returns:**
+- OAuth tokens dict containing:
+- - access_token: Access token for API requests
+- - refresh_token: Optional refresh token
+- - expires_in: Token expiration time
+- - token_type: Token type (usually "Bearer")
+
+**Raises:**
+- `ValueError`: If PKCE state not found, token endpoint missing,
+ or token exchange fails
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-oauth-flow.mdx b/docs/sdk/keycardai-mcp-client-auth-oauth-flow.mdx
new file mode 100644
index 0000000..63b5285
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-oauth-flow.mdx
@@ -0,0 +1,59 @@
+---
+title: flow
+sidebarTitle: flow
+---
+
+# `keycardai.mcp.client.auth.oauth.flow`
+
+
+OAuth flow initiation service.
+
+## Classes
+
+### `FlowMetadata`
+
+
+Metadata about an initiated OAuth flow.
+
+Contains the information needed to redirect the user and track the flow.
+
+
+### `OAuthFlowInitiatorService`
+
+
+Initiates OAuth 2.0 PKCE authorization flows.
+
+Responsibilities:
+- Generate PKCE parameters (code verifier and challenge)
+- Generate secure state parameter
+- Build authorization URL with all parameters
+- Store PKCE state for callback validation
+
+
+**Methods:**
+
+#### `initiate_flow`
+
+```python
+initiate_flow(self, auth_server_metadata: dict[str, Any], client_info: dict[str, Any], resource_url: str, server_name: str, scopes: list[str] | None = None, pkce_ttl: timedelta | None = None) -> FlowMetadata
+```
+
+Initiate an OAuth 2.0 PKCE authorization flow.
+
+Generates PKCE parameters, builds the authorization URL, and stores
+the necessary state for callback validation.
+
+**Args:**
+- `auth_server_metadata`: Authorization server metadata with authorization_endpoint
+- `client_info`: Client registration info with client_id and redirect_uris
+- `resource_url`: Resource URL being accessed (included in token request)
+- `server_name`: Server name for this OAuth flow (needed for callback cleanup)
+- `scopes`: Optional list of scopes to request
+- `pkce_ttl`: Time-to-live for PKCE state (default\: 10 minutes)
+
+**Returns:**
+- FlowMetadata with authorization URL, state, and resource URL
+
+**Raises:**
+- `ValueError`: If required fields are missing
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-oauth-registration.mdx b/docs/sdk/keycardai-mcp-client-auth-oauth-registration.mdx
new file mode 100644
index 0000000..3267675
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-oauth-registration.mdx
@@ -0,0 +1,51 @@
+---
+title: registration
+sidebarTitle: registration
+---
+
+# `keycardai.mcp.client.auth.oauth.registration`
+
+
+OAuth dynamic client registration service.
+
+## Classes
+
+### `OAuthClientRegistrationService`
+
+
+Handles OAuth 2.0 dynamic client registration.
+
+Responsibilities:
+- Register new OAuth clients with authorization servers
+- Cache client registration information
+- Reuse existing client registrations when possible
+
+
+**Methods:**
+
+#### `get_or_register_client`
+
+```python
+get_or_register_client(self, auth_server_metadata: dict[str, Any], redirect_uris: list[str]) -> dict[str, Any]
+```
+
+Get existing client registration or register a new one.
+
+Checks storage for an existing registration first. If found and redirect URIs
+match, returns the cached registration. Otherwise, registers a new client.
+
+**Args:**
+- `auth_server_metadata`: Authorization server metadata with registration_endpoint
+- `redirect_uris`: Redirect URIs to register for the client
+
+**Returns:**
+- Client registration information including:
+- - client_id: The OAuth client identifier
+- - redirect_uris: Registered redirect URIs
+- - grant_types: Supported grant types
+- - etc.
+
+**Raises:**
+- `ValueError`: If registration endpoint not found
+- `httpx.HTTPStatusError`: If registration request fails
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-protocols.mdx b/docs/sdk/keycardai-mcp-client-auth-protocols.mdx
new file mode 100644
index 0000000..36c7c83
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-protocols.mdx
@@ -0,0 +1,145 @@
+---
+title: protocols
+sidebarTitle: protocols
+---
+
+# `keycardai.mcp.client.auth.protocols`
+
+
+Protocol definitions for auth subsystem components.
+
+## Classes
+
+### `RedirectHandler`
+
+
+Protocol for handling OAuth redirects during auth flows.
+
+
+**Methods:**
+
+#### `get_redirect_uris`
+
+```python
+get_redirect_uris(self) -> list[str]
+```
+
+Get available redirect URIs for OAuth registration.
+
+**Returns:**
+- List of redirect URIs to register with OAuth server
+
+
+#### `initiate_redirect`
+
+```python
+initiate_redirect(self, url: str, metadata: dict[str, Any]) -> None
+```
+
+Initiate user redirect to authorization URL.
+
+**Args:**
+- `url`: Authorization URL to redirect user to
+- `metadata`: Additional metadata (e.g., server_name, state)
+
+
+### `AuthStateManager`
+
+
+Protocol for managing pending auth state.
+
+Tracks authentication flows that are pending user action
+(e.g., OAuth flows waiting for user to authorize in browser).
+
+
+**Methods:**
+
+#### `mark_auth_pending`
+
+```python
+mark_auth_pending(self, context_id: str, server_name: str, metadata: dict[str, Any], ttl: timedelta | None = None) -> None
+```
+
+Mark authentication as pending for a context/server.
+
+Called by strategies when they initiate an auth flow that requires
+user interaction (e.g., OAuth redirect).
+
+**Args:**
+- `context_id`: Context identifier (e.g., "user\:alice")
+- `server_name`: Server name requiring auth (e.g., "slack")
+- `metadata`: Strategy-specific metadata (e.g., authorization_url, state)
+- `ttl`: Optional expiration time for pending state
+
+
+#### `get_auth_status`
+
+```python
+get_auth_status(self, context_id: str, server_name: str) -> dict[str, Any] | None
+```
+
+Get pending auth metadata, if any.
+
+**Args:**
+- `context_id`: Context identifier
+- `server_name`: Server name to check
+
+**Returns:**
+- Auth metadata if pending, None otherwise
+
+
+#### `clear_auth_status`
+
+```python
+clear_auth_status(self, context_id: str, server_name: str) -> None
+```
+
+Clear pending auth state.
+
+Called when auth flow completes (successfully or not).
+
+**Args:**
+- `context_id`: Context identifier
+- `server_name`: Server name
+
+
+### `CompletionRegistrar`
+
+
+Protocol for registering auth completion handlers.
+
+Used in stateless environments (e.g., serverless) where callback
+handling happens in a different process/invocation than the one
+that initiated the auth flow.
+
+
+**Methods:**
+
+#### `register_completion_handler`
+
+```python
+register_completion_handler(self, routing_key: str, handler_name: str, storage_path: str, context_id: str, server_name: str, ttl: timedelta | None = None) -> None
+```
+
+Register a completion handler for stateless routing.
+
+Stores metadata needed to route incoming callbacks to the correct
+handler with the correct storage context.
+
+**Args:**
+- `routing_key`: Key to route callback (e.g., OAuth state parameter)
+- `handler_name`: Name of registered handler (e.g., "oauth_callback")
+- `storage_path`: Full storage namespace path for handler's data
+- `context_id`: Context identifier
+- `server_name`: Server name (for cleanup)
+- `ttl`: Optional expiration time for routing metadata
+
+
+### `CompletionHandler`
+
+
+Protocol for auth completion handler functions.
+
+Completion handlers process callbacks from auth providers
+(e.g., OAuth authorization code callbacks).
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-storage_facades.mdx b/docs/sdk/keycardai-mcp-client-auth-storage_facades.mdx
new file mode 100644
index 0000000..b89ddcc
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-storage_facades.mdx
@@ -0,0 +1,319 @@
+---
+title: storage_facades
+sidebarTitle: storage_facades
+---
+
+# `keycardai.mcp.client.auth.storage_facades`
+
+
+Type-safe storage facades for auth strategies and coordinator state management.
+
+## Classes
+
+### `OAuthStorage`
+
+
+Type-safe facade for OAuth strategy storage.
+
+Encapsulates the storage keys and provides a clean API
+for storing/retrieving OAuth-related data.
+
+Storage hierarchy:
+```
+oauth/
+ ├─ tokens # OAuth access/refresh tokens
+ ├─ client_info # Dynamic client registration info
+ ├─ _pkce_state:{state} # PKCE verifiers (TTL)
+ └─ _auth_server_metadata # Cached AS metadata
+```
+
+
+**Methods:**
+
+#### `save_tokens`
+
+```python
+save_tokens(self, tokens: dict[str, Any]) -> None
+```
+
+Save OAuth tokens.
+
+**Args:**
+- `tokens`: Token dict with access_token, refresh_token, etc.
+
+
+#### `get_tokens`
+
+```python
+get_tokens(self) -> dict[str, Any] | None
+```
+
+Retrieve OAuth tokens.
+
+**Returns:**
+- Token dict if present, None otherwise
+
+
+#### `delete_tokens`
+
+```python
+delete_tokens(self) -> None
+```
+
+Delete stored tokens.
+
+
+#### `save_client_registration`
+
+```python
+save_client_registration(self, client_info: dict[str, Any]) -> None
+```
+
+Save dynamic client registration information.
+
+**Args:**
+- `client_info`: Client registration data (client_id, redirect_uris, etc.)
+
+
+#### `get_client_registration`
+
+```python
+get_client_registration(self) -> dict[str, Any] | None
+```
+
+Retrieve client registration information.
+
+**Returns:**
+- Client info dict if registered, None otherwise
+
+
+#### `save_pkce_state`
+
+```python
+save_pkce_state(self, state: str, pkce_data: dict[str, Any], ttl: timedelta) -> None
+```
+
+Save PKCE state for an OAuth flow.
+
+**Args:**
+- `state`: OAuth state parameter (routing key)
+- `pkce_data`: PKCE verifier and code challenge data
+- `ttl`: Time-to-live for the PKCE state
+
+
+#### `get_pkce_state`
+
+```python
+get_pkce_state(self, state: str) -> dict[str, Any] | None
+```
+
+Retrieve PKCE state for an OAuth flow.
+
+**Args:**
+- `state`: OAuth state parameter
+
+**Returns:**
+- PKCE data dict if present, None if not found or expired
+
+
+#### `delete_pkce_state`
+
+```python
+delete_pkce_state(self, state: str) -> None
+```
+
+Delete PKCE state after token exchange.
+
+**Args:**
+- `state`: OAuth state parameter
+
+
+#### `save_auth_server_metadata`
+
+```python
+save_auth_server_metadata(self, metadata: dict[str, Any], ttl: timedelta | None = None) -> None
+```
+
+Cache authorization server metadata.
+
+**Args:**
+- `metadata`: AS metadata (authorization_endpoint, token_endpoint, etc.)
+- `ttl`: Time-to-live for the cached metadata (default\: 1 hour)
+
+
+#### `get_auth_server_metadata`
+
+```python
+get_auth_server_metadata(self) -> dict[str, Any] | None
+```
+
+Retrieve cached authorization server metadata.
+
+**Returns:**
+- AS metadata dict if cached, None otherwise
+
+
+### `APIKeyStorage`
+
+
+Type-safe facade for API key strategy storage.
+
+Storage hierarchy:
+```
+api_key/
+ └─ key # The API key value
+```
+
+
+**Methods:**
+
+#### `save_key`
+
+```python
+save_key(self, api_key: str) -> None
+```
+
+Save API key.
+
+**Args:**
+- `api_key`: The API key value
+
+
+#### `get_key`
+
+```python
+get_key(self) -> str | None
+```
+
+Retrieve API key.
+
+**Returns:**
+- API key string if present, None otherwise
+
+
+#### `delete_key`
+
+```python
+delete_key(self) -> None
+```
+
+Delete stored API key.
+
+
+### `AuthStateStorage`
+
+
+Type-safe facade for auth coordinator state management.
+
+Encapsulates storage keys for pending auth state and completion routing,
+providing a clean API without magic strings.
+
+Storage hierarchy:
+```
+auth_coordinator/
+ ├─ pending_auth:{context_id}:{server_name} # Pending auth metadata
+ └─ completion:{routing_key} # Completion routing metadata
+```
+
+
+**Methods:**
+
+#### `mark_auth_pending`
+
+```python
+mark_auth_pending(self, context_id: str, server_name: str, metadata: dict[str, Any], ttl: timedelta | None = None) -> None
+```
+
+Mark authentication as pending for a context/server.
+
+Called by strategies when they initiate an auth flow that requires
+user interaction (e.g., OAuth redirect). Stores metadata so sessions
+can detect pending auth and retrieve details.
+
+**Args:**
+- `context_id`: Context identifier
+- `server_name`: Server name requiring auth
+- `metadata`: Strategy-specific metadata (e.g., authorization_url, state)
+- `ttl`: Optional expiration time
+
+
+#### `get_auth_status`
+
+```python
+get_auth_status(self, context_id: str, server_name: str) -> dict[str, Any] | None
+```
+
+Get pending authentication metadata for a context/server.
+
+**Args:**
+- `context_id`: Context identifier
+- `server_name`: Server name to check
+
+**Returns:**
+- Auth metadata if pending, None otherwise
+
+
+#### `clear_auth_status`
+
+```python
+clear_auth_status(self, context_id: str, server_name: str) -> None
+```
+
+Clear pending authentication state.
+
+Called by strategies when auth flow completes (successfully or not).
+
+**Args:**
+- `context_id`: Context identifier
+- `server_name`: Server name
+
+
+#### `register_completion_route`
+
+```python
+register_completion_route(self, routing_key: str, metadata: dict[str, Any], ttl: timedelta | None = None) -> None
+```
+
+Register completion routing metadata for stateless execution.
+
+Used in stateless environments where a different process invocation
+will handle the completion. Stores metadata needed to route the completion
+to the right handler and reconstruct necessary state.
+
+**Args:**
+- `routing_key`: Key to route completion (e.g., OAuth state parameter)
+- `metadata`: Routing metadata including\:
+- handler_name\: Name of registered completion handler
+- storage_namespace\: Storage namespace path for strategy data
+- context_id\: Context identifier
+- server_name\: Server name for auth pending cleanup
+- handler_kwargs\: Optional kwargs for completion handler
+- `ttl`: Expiration time for routing metadata
+
+
+#### `get_completion_route`
+
+```python
+get_completion_route(self, routing_key: str) -> dict[str, Any] | None
+```
+
+Get completion routing metadata.
+
+**Args:**
+- `routing_key`: Routing key (e.g., OAuth state parameter)
+
+**Returns:**
+- Routing metadata dict if registered, None if not found or expired
+
+
+#### `delete_completion_route`
+
+```python
+delete_completion_route(self, routing_key: str) -> None
+```
+
+Delete completion routing metadata after completion.
+
+**Args:**
+- `routing_key`: Routing key to clean up
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-strategies-__init__.mdx b/docs/sdk/keycardai-mcp-client-auth-strategies-__init__.mdx
new file mode 100644
index 0000000..41bb85e
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-strategies-__init__.mdx
@@ -0,0 +1,9 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `keycardai.mcp.client.auth.strategies`
+
+
+Authentication strategies.
diff --git a/docs/sdk/keycardai-mcp-client-auth-strategies-oauth.mdx b/docs/sdk/keycardai-mcp-client-auth-strategies-oauth.mdx
new file mode 100644
index 0000000..7e14294
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-strategies-oauth.mdx
@@ -0,0 +1,219 @@
+---
+title: oauth
+sidebarTitle: oauth
+---
+
+# `keycardai.mcp.client.auth.strategies.oauth`
+
+
+Authentication strategies for MCP client connections.
+
+## Functions
+
+### `create_auth_strategy`
+
+```python
+create_auth_strategy(auth_config: dict[str, Any] | None, server_name: str, connection_storage: NamespacedStorage, context: Any, coordinator: 'AuthCoordinator') -> AuthStrategy
+```
+
+
+Factory function to create auth strategy from configuration.
+
+**Args:**
+- `auth_config`: Auth configuration dict with 'type' and strategy-specific params
+- `server_name`: Name of the server (for OAuth client naming)
+- `connection_storage`: Connection's storage namespace (will create strategy sub-namespace)
+- `context`: Context for identity
+- `coordinator`: Auth coordinator for callbacks
+
+**Returns:**
+- AuthStrategy instance
+
+**Examples:**
+
+>>> create_auth_strategy(None, "server1", storage, ctx, coord)
+NoAuthStrategy(...)
+>>> create_auth_strategy({"type": "oauth"}, "slack", storage, ctx, coord)
+OAuthStrategy(server_name="slack", ...)
+>>> create_auth_strategy({"type": "api_key", "key": "secret"}, "api", storage, ctx, coord)
+ApiKeyStrategy(api_key="secret", ...)
+
+
+## Classes
+
+### `AuthStrategy`
+
+
+Protocol for authentication strategies.
+
+Each strategy manages its own storage namespace and implements
+the complete authentication lifecycle.
+
+
+**Methods:**
+
+#### `get_strategy_namespace`
+
+```python
+get_strategy_namespace(self) -> str
+```
+
+Return the storage namespace for this strategy.
+
+**Returns:**
+- Namespace identifier (e.g., 'oauth', 'api_key', 'none')
+
+
+#### `get_auth_metadata`
+
+```python
+get_auth_metadata(self) -> dict[str, Any]
+```
+
+Get authentication metadata to add to requests.
+
+**Returns:**
+- Dict with headers and other auth metadata
+
+
+#### `handle_challenge`
+
+```python
+handle_challenge(self, challenge: Any, resource_url: str) -> bool
+```
+
+Handle authentication challenge.
+
+**Args:**
+- `challenge`: Challenge response (e.g., 401 HTTP response)
+- `resource_url`: URL of the protected resource
+
+**Returns:**
+- True if challenge was handled, False otherwise
+
+
+### `NoAuthStrategy`
+
+
+No authentication required.
+
+
+**Methods:**
+
+#### `get_strategy_namespace`
+
+```python
+get_strategy_namespace(self) -> str
+```
+
+Return namespace for this strategy.
+
+
+#### `get_auth_metadata`
+
+```python
+get_auth_metadata(self) -> dict[str, Any]
+```
+
+No auth metadata.
+
+
+#### `handle_challenge`
+
+```python
+handle_challenge(self, challenge: Any, resource_url: str) -> bool
+```
+
+No challenge handling.
+
+
+### `ApiKeyStrategy`
+
+
+API key authentication strategy.
+
+
+**Methods:**
+
+#### `get_strategy_namespace`
+
+```python
+get_strategy_namespace(self) -> str
+```
+
+Return namespace for this strategy.
+
+
+#### `get_auth_metadata`
+
+```python
+get_auth_metadata(self) -> dict[str, Any]
+```
+
+Return API key header.
+
+
+#### `handle_challenge`
+
+```python
+handle_challenge(self, challenge: Any, resource_url: str) -> bool
+```
+
+No challenge handling for API keys.
+
+
+### `OAuthStrategy`
+
+
+OAuth 2.0 PKCE authentication strategy.
+
+Orchestrates OAuth flow using specialized services:
+- Discovery: Find resource and authorization server metadata
+- Registration: Register OAuth client dynamically
+- Flow: Initiate authorization flow with PKCE
+- Exchange: Exchange authorization code for tokens
+
+Each server connection gets its own instance with isolated storage.
+
+
+**Methods:**
+
+#### `get_strategy_namespace`
+
+```python
+get_strategy_namespace(self) -> str
+```
+
+Return namespace for this strategy.
+
+
+#### `get_auth_metadata`
+
+```python
+get_auth_metadata(self) -> dict[str, Any]
+```
+
+Get OAuth access token if available.
+
+
+#### `handle_challenge`
+
+```python
+handle_challenge(self, challenge: Response, resource_url: str) -> bool
+```
+
+Handle 401 challenge by orchestrating OAuth flow.
+
+Coordinates all OAuth services to complete the authentication flow:
+1. Discovery: Find resource and authorization server metadata
+2. Registration: Register OAuth client (or reuse existing)
+3. Flow: Initiate PKCE authorization flow
+4. Routing: Register callback handler for token exchange
+
+**Args:**
+- `challenge`: HTTP 401 response containing OAuth challenge
+- `resource_url`: URL of the protected resource
+
+**Returns:**
+- True if challenge was handled successfully, False otherwise
+
diff --git a/docs/sdk/keycardai-mcp-client-auth-transports.mdx b/docs/sdk/keycardai-mcp-client-auth-transports.mdx
new file mode 100644
index 0000000..63ed028
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-auth-transports.mdx
@@ -0,0 +1,42 @@
+---
+title: transports
+sidebarTitle: transports
+---
+
+# `keycardai.mcp.client.auth.transports`
+
+
+HTTP transport adapters for authentication strategies.
+
+## Classes
+
+### `HttpxAuth`
+
+
+Adapts AuthStrategy to httpx.Auth interface.
+
+This is a thin adapter that translates between httpx's auth flow
+and our transport-agnostic AuthStrategy protocol.
+
+All business logic (OAuth discovery, token management, etc.) is
+delegated to the AuthStrategy implementation.
+
+Note: Strategy already has storage and coordinator from its constructor.
+
+
+**Methods:**
+
+#### `async_auth_flow`
+
+```python
+async_auth_flow(self, request: Request) -> AsyncGenerator[Request, Response]
+```
+
+httpx auth flow - delegates to AuthStrategy.
+
+This method:
+1. Adds auth metadata to the request (if available)
+2. Sends the request
+3. Handles auth challenges (401/403) via the strategy
+4. Retries if the strategy handled the challenge
+
diff --git a/docs/sdk/keycardai-mcp-client-client.mdx b/docs/sdk/keycardai-mcp-client-client.mdx
new file mode 100644
index 0000000..944b7b0
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-client.mdx
@@ -0,0 +1,121 @@
+---
+title: client
+sidebarTitle: client
+---
+
+# `keycardai.mcp.client.client`
+
+## Classes
+
+### `Client`
+
+
+MCP client for connecting to servers.
+
+
+**Methods:**
+
+#### `add_server`
+
+```python
+add_server(self, server_name: str, server_config: Any) -> None
+```
+
+Add a server connection to this client.
+
+**Args:**
+- `server_name`: Name of the server
+- `server_config`: Server configuration (including URL and auth config)
+
+
+#### `connect`
+
+```python
+connect(self, server: str | None = None, force_reconnect: bool = False) -> None
+```
+
+Connect to servers.
+
+This method attempts to connect to the specified server(s). Connection failures
+are communicated via session status, not exceptions. Check session.status or
+session.is_operational after calling to determine outcome.
+
+**Args:**
+- `server`: Optional server name. If None, connects to all servers.
+- `force_reconnect`: If True, disconnect and reconnect even if already connected.
+
+
+#### `requires_auth`
+
+```python
+requires_auth(self, server_name: str | None = None) -> bool
+```
+
+#### `get_auth_challenges`
+
+```python
+get_auth_challenges(self, server_name: str | None = None) -> list[AuthChallenge]
+```
+
+Get all pending auth challenges across sessions.
+
+An auth challenge is created when a server requires authentication
+that hasn't been completed yet. The challenge details are strategy-specific.
+
+**Args:**
+- `server_name`: Optional server name to check. If None, checks all sessions.
+
+**Returns:**
+- List of auth challenges. Each AuthChallenge contains strategy-specific fields
+- plus a 'server' field indicating which server needs auth.
+- Empty list if no challenges pending.
+
+
+#### `disconnect`
+
+```python
+disconnect(self) -> None
+```
+
+#### `list_tools`
+
+```python
+list_tools(self, server_name: str | None = None) -> list[ToolInfo]
+```
+
+List all available tools with their server information.
+
+Each tool is returned with explicit information about which server provides it.
+This makes it easy to iterate over tools while knowing their provenance.
+
+Automatically handles pagination by fetching all pages.
+
+**Args:**
+- `server_name`: Optional server name. If provided, lists tools only from that server.
+ If None, lists tools from all servers.
+
+**Returns:**
+- List of ToolInfo objects, each containing a tool and its server name.
+
+
+#### `call_tool`
+
+```python
+call_tool(self, tool_name: str, arguments: dict[str, Any], server_name: str | None = None) -> CallToolResult
+```
+
+Call a tool on an MCP server.
+
+**Args:**
+- `tool_name`: Name of the tool to call
+- `arguments`: Arguments to pass to the tool
+- `server_name`: Optional server name. If None, auto-discovers which server has the tool.
+
+**Returns:**
+- CallToolResult containing the tool's response with content and error status.
+- The content is a list of content items (text, images, embedded resources).
+
+**Raises:**
+- `ToolNotFoundException`: If the tool is not found on any server (when server_name is None)
+ or if the specified server is not connected/available
+
diff --git a/docs/sdk/keycardai-mcp-client-connection-__init__.mdx b/docs/sdk/keycardai-mcp-client-connection-__init__.mdx
new file mode 100644
index 0000000..f1bebe9
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-connection-__init__.mdx
@@ -0,0 +1,9 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `keycardai.mcp.client.connection`
+
+
+Connection module for MCP client.
diff --git a/docs/sdk/keycardai-mcp-client-connection-base.mdx b/docs/sdk/keycardai-mcp-client-connection-base.mdx
new file mode 100644
index 0000000..b372a7a
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-connection-base.mdx
@@ -0,0 +1,101 @@
+---
+title: base
+sidebarTitle: base
+---
+
+# `keycardai.mcp.client.connection.base`
+
+
+
+Base connection abstraction for MCP client.
+
+Provides the Connection base class that all transport implementations extend.
+
+
+## Classes
+
+### `ConnectionError`
+
+
+Raised when a connection cannot be established.
+
+
+### `Connection`
+
+
+Base class for MCP server connections.
+
+Manages the lifecycle of a connection:
+- start(): Begin connection (async, returns read/write streams)
+- stop(): Terminate connection
+- connect(): Implementation-specific connection logic (abstract)
+- disconnect(): Implementation-specific cleanup (optional)
+
+
+**Methods:**
+
+#### `start`
+
+```python
+start(self) -> tuple[Any, Any]
+```
+
+Start the connection.
+
+**Returns:**
+- Tuple of (read_stream, write_stream)
+
+**Raises:**
+- `ConnectionError`: If connection fails to establish
+- `Exception`: Any exception raised during connection establishment
+
+
+#### `stop`
+
+```python
+stop(self) -> None
+```
+
+Stop the connection and clean up resources.
+
+
+#### `connect_task`
+
+```python
+connect_task(self) -> None
+```
+
+Background task that manages connection lifecycle.
+
+This task:
+1. Calls connect() to establish connection
+2. Signals ready when connection is established
+3. Waits for stop signal
+4. Calls disconnect() on cleanup
+
+
+#### `connect`
+
+```python
+connect(self) -> tuple[Any, Any]
+```
+
+Establish the connection and return (read_stream, write_stream).
+Must be implemented by subclasses.
+
+**Returns:**
+- Tuple of (read_stream, write_stream)
+
+**Raises:**
+- `NotImplementedError`: If not implemented by subclass
+
+
+#### `disconnect`
+
+```python
+disconnect(self) -> None
+```
+
+Clean up connection resources.
+Optional - override in subclasses if cleanup is needed.
+
diff --git a/docs/sdk/keycardai-mcp-client-connection-factory.mdx b/docs/sdk/keycardai-mcp-client-connection-factory.mdx
new file mode 100644
index 0000000..56c83bd
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-connection-factory.mdx
@@ -0,0 +1,33 @@
+---
+title: factory
+sidebarTitle: factory
+---
+
+# `keycardai.mcp.client.connection.factory`
+
+
+
+Connection factory for creating appropriate connection types.
+
+
+## Functions
+
+### `create_connection`
+
+```python
+create_connection(server_name: str, server_config: dict[str, Any], context: Context, coordinator: 'AuthCoordinator', server_storage: 'NamespacedStorage') -> 'Connection | None'
+```
+
+
+Factory function to create appropriate connection type.
+
+**Args:**
+- `server_name`: Name of the server
+- `server_config`: Server configuration
+- `context`: Context for identity (not for storage - use server_storage)
+- `coordinator`: Auth coordinator for callbacks
+- `server_storage`: Pre-scoped storage namespace for this server
+
+**Returns:**
+- Connection instance or None if transport not supported
+
diff --git a/docs/sdk/keycardai-mcp-client-connection-http.mdx b/docs/sdk/keycardai-mcp-client-connection-http.mdx
new file mode 100644
index 0000000..07194e8
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-connection-http.mdx
@@ -0,0 +1,44 @@
+---
+title: http
+sidebarTitle: http
+---
+
+# `keycardai.mcp.client.connection.http`
+
+
+
+HTTP connection implementation for MCP client.
+
+Provides StreamableHttpConnection which uses httpx for HTTP-based MCP connections.
+
+
+## Classes
+
+### `StreamableHttpConnection`
+
+
+HTTP-based MCP connection with per-server authentication.
+
+Each connection instance represents a connection to a single server
+with its own isolated auth strategy and storage.
+
+
+**Methods:**
+
+#### `connect`
+
+```python
+connect(self) -> tuple[Any, Any]
+```
+
+Establish HTTP connection with auth adapter.
+
+
+#### `disconnect`
+
+```python
+disconnect(self) -> None
+```
+
+Disconnect from HTTP server.
+
diff --git a/docs/sdk/keycardai-mcp-client-context.mdx b/docs/sdk/keycardai-mcp-client-context.mdx
new file mode 100644
index 0000000..b4de2dc
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-context.mdx
@@ -0,0 +1,37 @@
+---
+title: context
+sidebarTitle: context
+---
+
+# `keycardai.mcp.client.context`
+
+
+Core context primitives for MCP client.
+
+## Classes
+
+### `Context`
+
+
+Represents an isolated execution context.
+
+Provides:
+- Unique identifier (e.g., "user:alice")
+- Namespaced storage (isolated from other contexts)
+- Reference to auth coordinator
+- Type-safe storage path builder
+
+
+**Methods:**
+
+#### `storage_path`
+
+```python
+storage_path(self) -> StoragePathBuilder
+```
+
+Create a storage path builder starting from this context's storage.
+
+**Returns:**
+- StoragePathBuilder for fluent namespace navigation
+
diff --git a/docs/sdk/keycardai-mcp-client-exceptions.mdx b/docs/sdk/keycardai-mcp-client-exceptions.mdx
new file mode 100644
index 0000000..d07f6d9
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-exceptions.mdx
@@ -0,0 +1,44 @@
+---
+title: exceptions
+sidebarTitle: exceptions
+---
+
+# `keycardai.mcp.client.exceptions`
+
+
+Exception classes for MCP client.
+
+This module defines all custom exceptions used throughout the MCP client package,
+providing clear error types and documentation for different failure scenarios.
+
+
+## Classes
+
+### `MCPClientError`
+
+
+Base exception for all MCP client errors.
+
+This is the base class for all exceptions raised by the MCP client package.
+It provides a common interface for error handling and allows catching all
+MCP client-related errors with a single except clause.
+
+
+### `ClientConfigurationError`
+
+
+Raised when Client is misconfigured.
+
+This exception is raised during Client initialization when
+the provided configuration is invalid or incomplete.
+
+
+### `ToolNotFoundException`
+
+
+Raised when a requested tool is not found on any server.
+
+This exception is raised when attempting to call a tool that doesn't exist
+on any of the configured servers. It's typically raised during auto-discovery
+when no server provides the requested tool.
+
diff --git a/docs/sdk/keycardai-mcp-client-integrations-__init__.mdx b/docs/sdk/keycardai-mcp-client-integrations-__init__.mdx
new file mode 100644
index 0000000..e8f4e7f
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-integrations-__init__.mdx
@@ -0,0 +1,9 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `keycardai.mcp.client.integrations`
+
+
+MCP client integrations for agent frameworks.
diff --git a/docs/sdk/keycardai-mcp-client-integrations-auth_tools.mdx b/docs/sdk/keycardai-mcp-client-integrations-auth_tools.mdx
new file mode 100644
index 0000000..681677f
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-integrations-auth_tools.mdx
@@ -0,0 +1,119 @@
+---
+title: auth_tools
+sidebarTitle: auth_tools
+---
+
+# `keycardai.mcp.client.integrations.auth_tools`
+
+
+
+Auth tool handlers for customizable authentication flow.
+
+Provides base classes and implementations for handling authentication
+challenges in different environments (Slack, CLI, web apps, etc.).
+
+
+## Classes
+
+### `AuthToolHandler`
+
+
+Base class for custom authentication tool handlers.
+
+Subclass this to implement custom authentication flows for your
+application (e.g., sending Slack messages, showing web UI, etc.).
+
+The handler receives auth challenges and is responsible for
+presenting them to the user in the appropriate way.
+
+
+**Methods:**
+
+#### `handle_auth_request`
+
+```python
+handle_auth_request(self, service: str, reason: str, challenge: dict[str, Any]) -> str
+```
+
+Handle an authentication request.
+
+This method is called when the agent determines that a service
+needs authentication. Implement this to send auth links to users
+via your preferred channel (Slack DM, email, web notification, etc.).
+
+**Args:**
+- `service`: Service name requesting auth (e.g., "google-mcp")
+- `reason`: User-friendly explanation from the agent
+ (e.g., "To send messages to Slack channels")
+- `challenge`: Full challenge dict containing\:
+- authorization_url\: URL for user to authorize
+- server\: Server name
+- Other challenge-specific fields
+
+**Returns:**
+- Simple status message for the agent (e.g., "Auth flow initiated").
+- This is NOT shown to the end user - it's just for the agent
+- to know the auth process started.
+
+
+### `DefaultAuthToolHandler`
+
+
+Default handler that returns auth info for the agent to display.
+
+This is the fallback when no custom handler is provided. It returns
+a formatted message that the agent can show to the user.
+
+Note: This is less ideal than a custom handler because it requires
+the agent to relay the message, which may not work well in all UIs.
+
+
+**Methods:**
+
+#### `handle_auth_request`
+
+```python
+handle_auth_request(self, service: str, reason: str, challenge: dict[str, Any]) -> str
+```
+
+Return formatted auth message for agent to display.
+
+
+### `SlackAuthToolHandler`
+
+
+Slack-specific handler that sends auth links via Slack messages.
+
+This handler sends authentication links directly to the user
+in their Slack thread, bypassing the agent message flow.
+
+
+**Methods:**
+
+#### `handle_auth_request`
+
+```python
+handle_auth_request(self, service: str, reason: str, challenge: dict[str, Any]) -> str
+```
+
+Send auth link via Slack message.
+
+
+### `ConsoleAuthToolHandler`
+
+
+Console/CLI handler that prints auth links to stdout.
+
+Useful for development, testing, or CLI applications.
+
+
+**Methods:**
+
+#### `handle_auth_request`
+
+```python
+handle_auth_request(self, service: str, reason: str, challenge: dict[str, Any]) -> str
+```
+
+Print auth link to console.
+
diff --git a/docs/sdk/keycardai-mcp-client-integrations-langchain_agents.mdx b/docs/sdk/keycardai-mcp-client-integrations-langchain_agents.mdx
new file mode 100644
index 0000000..ee1bcec
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-integrations-langchain_agents.mdx
@@ -0,0 +1,138 @@
+---
+title: langchain_agents
+sidebarTitle: langchain_agents
+---
+
+# `keycardai.mcp.client.integrations.langchain_agents`
+
+
+
+LangChain agents adapter for KeycardAI MCP client.
+
+Provides a clean API for integrating MCP tools with LangChain agents:
+- Automatic auth detection and handling
+- System prompt generation with auth context
+- MCP tools converted to LangChain tools
+- Auth request tools for agent
+
+
+## Functions
+
+### `create_client`
+
+```python
+create_client(mcp_client: Client, auth_tool_handler: AuthToolHandler | None = None, auth_hook_closure: Callable[[], Awaitable[None]] | None = None) -> LangChainClient
+```
+
+
+Get LangChain agents adapter for MCP client.
+
+Use as context manager for automatic lifecycle management.
+
+**Args:**
+- `mcp_client`: KeycardAI MCP client
+- `auth_tool_handler`: Optional custom handler for auth requests.
+Subclass AuthToolHandler to customize how auth links are sent.
+Built-in options\: SlackAuthToolHandler, ConsoleAuthToolHandler
+Default\: DefaultAuthToolHandler (returns message for agent)
+- `auth_hook_closure`: Optional async function called when auth is needed
+
+**Returns:**
+- LangChain client adapter
+
+Example - Console/CLI:
+ >>> from keycardai.mcp.client.integrations.auth_tools import ConsoleAuthToolHandler
+ >>>
+ >>> handler = ConsoleAuthToolHandler()
+ >>> async with langchain_agents.get_client(mcp_client, auth_tool_handler=handler) as client:
+ ... # Auth links will be printed to console
+
+Example - With memory/checkpointing:
+ >>> from langgraph.checkpoint.memory import InMemorySaver
+ >>> from langchain.agents import create_agent
+ >>>
+ >>> async with langchain_agents.get_client(mcp_client) as client:
+ ... agent = create_agent(
+ ... model="claude-sonnet-4-5-20250929",
+ ... tools=await client.get_tools() + await client.get_auth_tools(),
+ ... system_prompt=client.get_system_prompt("Be helpful"),
+ ... checkpointer=InMemorySaver(),
+ ... )
+ ... # Use with thread_id for conversation memory
+ ... result = agent.invoke(
+ ... {"messages": [{"role": "user", "content": "Hi, my name is Bob"}]},
+ ... {"configurable": {"thread_id": "123"}},
+ ... )
+
+
+## Classes
+
+### `LangChainClient`
+
+
+LangChain agents adapter for MCP client.
+
+Wraps MCP client to provide:
+- get_system_prompt(): Instructions with auth awareness
+- get_tools(): MCP tools converted to LangChain tools
+- get_auth_tools(): Tools for requesting authentication
+
+
+**Methods:**
+
+#### `get_system_prompt`
+
+```python
+get_system_prompt(self, base_instructions: str) -> str
+```
+
+Generate system prompt with auth awareness.
+
+ If services need auth, adds instructions about using auth tool.
+ Otherwise, returns base instructions unchanged.
+
+ Args:
+ base_instructions: Your base agent instructions
+
+ Returns:
+ System prompt (possibly augmented with auth instructions)
+
+ Example:
+ >>> prompt = client.get_system_prompt("You are a helpful assistant")
+ >>> # If auth needed:
+ >>> # "You are a helpful assistant
+
+**AUTH REQUIRED**..."
+ >>> # If authenticated:
+ >>> # "You are a helpful assistant"
+
+
+#### `get_tools`
+
+```python
+get_tools(self) -> list[StructuredTool]
+```
+
+Get MCP tools converted to LangChain tools.
+
+Only returns tools from servers that are authenticated.
+Servers requiring auth are excluded until authorization completes.
+
+**Returns:**
+- List of LangChain StructuredTool objects
+
+
+#### `get_auth_tools`
+
+```python
+get_auth_tools(self) -> list[StructuredTool]
+```
+
+Get authentication request tools for the agent.
+
+Returns a tool that allows the agent to request user authentication
+when needed. If all services are authenticated, returns empty list.
+
+**Returns:**
+- List with one auth request tool (or empty if no auth needed)
+
diff --git a/docs/sdk/keycardai-mcp-client-integrations-openai_agents.mdx b/docs/sdk/keycardai-mcp-client-integrations-openai_agents.mdx
new file mode 100644
index 0000000..cb1a39e
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-integrations-openai_agents.mdx
@@ -0,0 +1,224 @@
+---
+title: openai_agents
+sidebarTitle: openai_agents
+---
+
+# `keycardai.mcp.client.integrations.openai_agents`
+
+
+
+OpenAI agents SDK adapter for KeycardAI MCP client.
+
+Provides a clean API for integrating MCP tools with OpenAI agents:
+- Automatic auth detection and handling
+- System prompt generation with auth context
+- Filtered MCP servers (only authenticated)
+- Auth request tools for agent
+
+
+## Functions
+
+### `create_client`
+
+```python
+create_client(mcp_client: Client, auth_tool_handler: AuthToolHandler | None = None, auth_hook_closure: Callable[[], Awaitable[None]] | None = None, tool_result_parser: Callable[[Any], CallToolResult] | None = None) -> OpenAIAgentsClient
+```
+
+
+Get OpenAI agents adapter for MCP client.
+
+Use as context manager for automatic lifecycle management.
+
+**Args:**
+- `mcp_client`: KeycardAI MCP client
+- `auth_tool_handler`: Optional custom handler for auth requests.
+Subclass AuthToolHandler to customize how auth links are sent.
+Built-in options\: SlackAuthToolHandler, ConsoleAuthToolHandler
+Default\: DefaultAuthToolHandler (returns message for agent)
+- `auth_hook_closure`: Optional async function called when auth is needed
+
+**Returns:**
+- OpenAI agents client adapter
+
+Example - Console/CLI:
+ >>> from keycardai.mcp.client.integrations.auth_tools import ConsoleAuthToolHandler
+ >>>
+ >>> handler = ConsoleAuthToolHandler()
+ >>> async with get_client(mcp_client, auth_tool_handler=handler) as client:
+ ... # Auth links will be printed to console
+
+
+## Classes
+
+### `OpenAIMCPServer`
+
+
+OpenAI-compatible wrapper for an MCP server.
+
+This is what gets passed to Agent(mcp_servers=[...])
+
+Wraps a single MCP server to provide the interface expected by
+OpenAI agents SDK for MCP server integration.
+
+Implements the MCPServer protocol from agents.mcp.
+
+
+**Methods:**
+
+#### `name`
+
+```python
+name(self) -> str
+```
+
+A readable name for the server.
+
+
+#### `connect`
+
+```python
+connect(self)
+```
+
+Connect to the server.
+
+Note: Connection is managed by the MCP client, so this is a no-op.
+The client should already be connected before creating this adapter.
+
+
+#### `cleanup`
+
+```python
+cleanup(self)
+```
+
+Cleanup the server.
+
+Note: Cleanup is managed by the MCP client, so this is a no-op.
+
+
+#### `list_tools`
+
+```python
+list_tools(self, run_context: Any | None = None, agent: Any | None = None) -> list[Tool]
+```
+
+List the tools available on the server.
+
+**Args:**
+- `run_context`: Optional run context (not used in this implementation)
+- `agent`: Optional agent reference (not used in this implementation)
+
+**Returns:**
+- List of tools available on this server
+
+
+#### `call_tool`
+
+```python
+call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None) -> CallToolResult
+```
+
+Invoke a tool on the server.
+
+**Args:**
+- `tool_name`: Name of the tool to call
+- `arguments`: Arguments to pass to the tool (can be None)
+
+**Returns:**
+- Result from the tool call wrapped in CallToolResult
+
+
+#### `list_prompts`
+
+```python
+list_prompts(self) -> ListPromptsResult
+```
+
+List the prompts available on the server.
+
+Note: Not implemented - MCP client doesn't expose prompt listing yet.
+Returns empty list.
+
+
+#### `get_prompt`
+
+```python
+get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult
+```
+
+Get a specific prompt from the server.
+
+Note: Not implemented - MCP client doesn't expose prompts yet.
+Raises UserError.
+
+
+### `OpenAIAgentsClient`
+
+
+OpenAI agents adapter for MCP client.
+
+Wraps MCP client to provide:
+- get_system_prompt(): Instructions with auth awareness
+- get_mcp_servers(): Filtered authenticated servers
+- get_auth_tools(): Tools for requesting authentication
+
+
+**Methods:**
+
+#### `get_system_prompt`
+
+```python
+get_system_prompt(self, base_instructions: str) -> str
+```
+
+Generate system prompt with auth awareness.
+
+ If services need auth, adds instructions about using auth tool.
+ Otherwise, returns base instructions unchanged.
+
+ Args:
+ base_instructions: Your base agent instructions
+
+ Returns:
+ System prompt (possibly augmented with auth instructions)
+
+ Example:
+ >>> prompt = client.get_system_prompt("You are a helpful Slack bot")
+ >>> # If auth needed:
+ >>> # "You are a helpful Slack bot
+
+**AUTH REQUIRED**..."
+ >>> # If authenticated:
+ >>> # "You are a helpful Slack bot"
+
+
+#### `get_mcp_servers`
+
+```python
+get_mcp_servers(self) -> list[OpenAIMCPServer]
+```
+
+Get MCP servers for OpenAI agent.
+
+Only returns servers that are authenticated and have tools available.
+Servers requiring auth are excluded until authorization completes.
+
+**Returns:**
+- List of MCP server objects for OpenAI Agent
+
+
+#### `get_auth_tools`
+
+```python
+get_auth_tools(self) -> list[FunctionTool]
+```
+
+Get authentication request tools for the agent.
+
+Returns a tool that allows the agent to request user authentication
+when needed. If all services are authenticated, returns empty list.
+
+**Returns:**
+- List with one auth request tool (or empty if no auth needed)
+
diff --git a/docs/sdk/keycardai-mcp-client-logging_config.mdx b/docs/sdk/keycardai-mcp-client-logging_config.mdx
new file mode 100644
index 0000000..02ea55b
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-logging_config.mdx
@@ -0,0 +1,70 @@
+---
+title: logging_config
+sidebarTitle: logging_config
+---
+
+# `keycardai.mcp.client.logging_config`
+
+
+
+Logging configuration for MCP client.
+
+The library uses standard Python logging without forcing configuration.
+Users can configure logging however they prefer, or use the environment
+variables provided here for convenience.
+
+Environment Variables:
+ MCP_LOG_LEVEL: Set log level for all mcp.client loggers (DEBUG, INFO, WARNING, ERROR, CRITICAL)
+ MCP_LOG_FORMAT: Custom format string (optional)
+
+Example usage in application code:
+ import logging
+ logging.basicConfig(level=logging.INFO)
+
+ # Or with custom configuration:
+ import logging
+ logger = logging.getLogger('keycardai.mcp.client')
+ logger.setLevel(logging.DEBUG)
+ handler = logging.StreamHandler()
+ handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
+ logger.addHandler(handler)
+
+
+## Functions
+
+### `get_logger`
+
+```python
+get_logger(name: str) -> logging.Logger
+```
+
+
+Get a logger for the given module name.
+
+This function creates loggers that respect MCP_LOG_LEVEL environment variable
+if set, but doesn't force any configuration on the application.
+
+**Args:**
+- `name`: Module name (usually __name__)
+
+**Returns:**
+- Configured logger instance
+
+
+### `configure_logging`
+
+```python
+configure_logging(level: int | str = logging.WARNING, format_string: str | None = None, handler: logging.Handler | None = None) -> None
+```
+
+
+Configure logging for all MCP client loggers.
+
+This is a convenience function for applications that want simple logging setup.
+Advanced users should configure logging directly using the logging module.
+
+**Args:**
+- `level`: Log level (logging.DEBUG, logging.INFO, etc. or string like "DEBUG")
+- `format_string`: Custom format string for log messages
+- `handler`: Custom handler (if None, uses StreamHandler)
+
diff --git a/docs/sdk/keycardai-mcp-client-manager.mdx b/docs/sdk/keycardai-mcp-client-manager.mdx
new file mode 100644
index 0000000..04e14e4
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-manager.mdx
@@ -0,0 +1,37 @@
+---
+title: manager
+sidebarTitle: manager
+---
+
+# `keycardai.mcp.client.manager`
+
+## Classes
+
+### `ClientManager`
+
+
+Factory for creating multiple clients with shared coordinator.
+
+Use when you need multi-context/multi-user scenarios.
+All clients share the same coordinator (e.g., one local callback server for all users).
+
+
+**Methods:**
+
+#### `get_client`
+
+```python
+get_client(self, context_id: str, metadata: dict[str, Any] | None = None) -> Client
+```
+
+Get or create a client for a specific context.
+
+All clients share the same coordinator but have isolated storage.
+
+**Args:**
+- `context_id`: Identifier for the client context (e.g., "user\:alice", "task\:123")
+- `metadata`: Optional metadata dict to attach to context (e.g., user info, session data)
+
+**Returns:**
+- Client instance for the given context
+
diff --git a/docs/sdk/keycardai-mcp-client-session.mdx b/docs/sdk/keycardai-mcp-client-session.mdx
new file mode 100644
index 0000000..9f05cbe
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-session.mdx
@@ -0,0 +1,213 @@
+---
+title: session
+sidebarTitle: session
+---
+
+# `keycardai.mcp.client.session`
+
+## Classes
+
+### `SessionStatus`
+
+
+Comprehensive status tracking for Session lifecycle.
+
+States are organized into categories:
+- Initial: INITIALIZING
+- Active Connection: CONNECTING, AUTHENTICATING, AUTH_PENDING, CONNECTED
+- Disconnection: DISCONNECTING, DISCONNECTED
+- Failure States: AUTH_FAILED, CONNECTION_FAILED, SERVER_UNREACHABLE, FAILED
+- Recovery: RECONNECTING
+
+See SESSION_STATUS_DESIGN.md for detailed state transitions and sequence diagrams.
+
+
+### `SessionStatusCategory`
+
+
+Helper for checking status categories.
+
+
+### `Session`
+
+
+Session represents a connection to a single MCP server.
+
+Each session has its own connection with isolated auth and storage.
+
+**Connection Status Lifecycle:**
+
+Sessions track their connection state through a comprehensive status lifecycle.
+The `connect()` method does NOT raise exceptions for connection failures. Instead,
+it sets the appropriate session status. Callers should check `session.status` or
+`session.is_operational` after calling `connect()` to determine the outcome.
+
+Status states include:
+- INITIALIZING, CONNECTING, AUTHENTICATING, AUTH_PENDING, CONNECTED
+- DISCONNECTING, DISCONNECTED
+- AUTH_FAILED, CONNECTION_FAILED, SERVER_UNREACHABLE, FAILED
+
+Use properties like `is_operational`, `is_failed`, `requires_user_action`, and
+`can_retry` to check the session state.
+
+**Auto-Reconnection on Auth Completion:**
+
+When a session enters AUTH_PENDING state (e.g., waiting for OAuth), it subscribes
+to the coordinator's completion events. When authentication completes, the session
+automatically reconnects without requiring manual intervention. This enables the
+non-blocking auth pattern where users can poll `session.requires_user_action`.
+
+**Forwarding to ClientSession:**
+
+This class automatically forwards all methods from mcp.ClientSession via __getattr__,
+allowing it to stay in sync with upstream MCP SDK changes.
+
+For IDE autocomplete, see session.pyi type stub which defines all available methods.
+
+
+**Methods:**
+
+#### `connected`
+
+```python
+connected(self) -> bool
+```
+
+Check if session is connected (backward compatibility).
+
+
+#### `is_operational`
+
+```python
+is_operational(self) -> bool
+```
+
+Can perform operations (call tools, list resources, etc.)
+
+
+#### `is_connecting`
+
+```python
+is_connecting(self) -> bool
+```
+
+Currently attempting to connect.
+
+
+#### `requires_user_action`
+
+```python
+requires_user_action(self) -> bool
+```
+
+Requires user action to proceed (e.g., OAuth).
+
+
+#### `can_retry`
+
+```python
+can_retry(self) -> bool
+```
+
+Can attempt reconnection.
+
+
+#### `is_failed`
+
+```python
+is_failed(self) -> bool
+```
+
+In a failure state.
+
+
+#### `on_completion_handled`
+
+```python
+on_completion_handled(self, event: CompletionEvent) -> None
+```
+
+Handle auth completion notification from coordinator (CompletionSubscriber protocol).
+
+When OAuth completes for this session, automatically reconnect.
+This enables the non-blocking auth pattern where callers can poll
+`session.requires_user_action` and the session will automatically
+become operational when auth completes.
+
+**Args:**
+- `event`: Completion event from coordinator
+
+
+#### `connect`
+
+```python
+connect(self, _retry_after_auth: bool = True) -> None
+```
+
+Connect to the server.
+
+This method does not raise exceptions for connection failures. Instead, it sets
+the appropriate session status. Callers should check session.status or
+session.is_operational after calling this method.
+
+Possible outcomes:
+- status=CONNECTED: Successfully connected and ready
+- status=AUTH_PENDING: Requires user authentication (check get_auth_challenge)
+- status=SERVER_UNREACHABLE: Server not reachable
+- status=CONNECTION_FAILED: Connection failed
+- status=AUTH_FAILED: Authentication failed
+- status=FAILED: Other failure
+
+**Args:**
+- `_retry_after_auth`: Internal flag to retry once after auth challenge completes
+
+
+#### `disconnect`
+
+```python
+disconnect(self) -> None
+```
+
+Gracefully disconnect from the server.
+
+
+#### `requires_auth`
+
+```python
+requires_auth(self) -> bool
+```
+
+Check if this session has a pending auth challenge.
+
+
+#### `get_auth_challenge`
+
+```python
+get_auth_challenge(self) -> dict[str, str] | None
+```
+
+Get pending auth challenge for this session.
+
+An auth challenge is created by the auth strategy when authentication
+is required but not yet complete (e.g., waiting for OAuth callback).
+
+**Returns:**
+- Dict with challenge details (strategy-specific) or None if no pending challenge.
+- For OAuth: {'authorization_url': str, 'state': str}
+- For other strategies: may contain different fields
+
+
+#### `check_connection_health`
+
+```python
+check_connection_health(self) -> bool
+```
+
+Proactively check if the connection is still alive.
+
+This method sends a ping to the server to verify the connection is operational.
+If the connection is dead, it updates the session status accordingly.
+
+**Returns:**
+- True if connection is healthy, False if connection is dead
+
diff --git a/docs/sdk/keycardai-mcp-client-storage-__init__.mdx b/docs/sdk/keycardai-mcp-client-storage-__init__.mdx
new file mode 100644
index 0000000..fc8c2ef
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-storage-__init__.mdx
@@ -0,0 +1,9 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `keycardai.mcp.client.storage`
+
+
+Storage abstractions for MCP client.
diff --git a/docs/sdk/keycardai-mcp-client-storage-backend.mdx b/docs/sdk/keycardai-mcp-client-storage-backend.mdx
new file mode 100644
index 0000000..b48c0d9
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-storage-backend.mdx
@@ -0,0 +1,149 @@
+---
+title: backend
+sidebarTitle: backend
+---
+
+# `keycardai.mcp.client.storage.backend`
+
+
+Storage backend interface.
+
+## Classes
+
+### `StorageBackend`
+
+
+Low-level storage backend interface.
+
+Provides generic key-value operations with TTL support.
+All backends must implement these core operations.
+
+
+**Methods:**
+
+#### `get`
+
+```python
+get(self, key: str) -> Any | None
+```
+
+Get value by key.
+
+Returns None if key doesn't exist or has expired.
+
+
+#### `set`
+
+```python
+set(self, key: str, value: Any, ttl: timedelta | None = None) -> None
+```
+
+Set value with optional TTL.
+
+**Args:**
+- `key`: Storage key
+- `value`: Value to store (must be JSON-serializable)
+- `ttl`: Time to live (None = no expiration)
+
+
+#### `delete`
+
+```python
+delete(self, key: str) -> bool
+```
+
+Delete key.
+
+Returns True if deleted, False if key didn't exist.
+
+
+#### `exists`
+
+```python
+exists(self, key: str) -> bool
+```
+
+Check if key exists and is not expired.
+
+
+#### `get_many`
+
+```python
+get_many(self, keys: list[str]) -> dict[str, Any]
+```
+
+Get multiple values.
+
+Default implementation calls get() for each key.
+Backends can override for efficiency.
+
+
+#### `set_many`
+
+```python
+set_many(self, items: dict[str, Any], ttl: timedelta | None = None) -> None
+```
+
+Set multiple values.
+
+Default implementation calls set() for each item.
+Backends can override for efficiency.
+
+
+#### `delete_many`
+
+```python
+delete_many(self, keys: list[str]) -> int
+```
+
+Delete multiple keys.
+
+Returns count of deleted keys.
+
+
+#### `increment`
+
+```python
+increment(self, key: str, amount: int = 1, ttl: timedelta | None = None) -> int
+```
+
+Atomically increment counter.
+
+Default implementation is not atomic.
+Backends with native atomic support should override.
+
+Returns new value.
+
+
+#### `list_keys`
+
+```python
+list_keys(self, prefix: str | None = None, limit: int | None = None) -> list[str]
+```
+
+List keys, optionally filtered by prefix.
+
+**Args:**
+- `prefix`: Key prefix filter
+- `limit`: Maximum number of keys to return
+
+
+#### `delete_prefix`
+
+```python
+delete_prefix(self, prefix: str) -> int
+```
+
+Delete all keys matching prefix.
+
+Returns count of deleted keys.
+
+
+#### `close`
+
+```python
+close(self) -> None
+```
+
+Close backend and release resources.
+
diff --git a/docs/sdk/keycardai-mcp-client-storage-backends-__init__.mdx b/docs/sdk/keycardai-mcp-client-storage-backends-__init__.mdx
new file mode 100644
index 0000000..e4c87fa
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-storage-backends-__init__.mdx
@@ -0,0 +1,9 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `keycardai.mcp.client.storage.backends`
+
+
+Storage backend implementations.
diff --git a/docs/sdk/keycardai-mcp-client-storage-backends-memory.mdx b/docs/sdk/keycardai-mcp-client-storage-backends-memory.mdx
new file mode 100644
index 0000000..330ccf3
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-storage-backends-memory.mdx
@@ -0,0 +1,81 @@
+---
+title: memory
+sidebarTitle: memory
+---
+
+# `keycardai.mcp.client.storage.backends.memory`
+
+
+In-memory storage backend.
+
+## Classes
+
+### `InMemoryBackend`
+
+
+In-memory storage with TTL support.
+
+⚠️ WARNING: State is lost when process ends!
+Only use for:
+- Local development
+- Testing
+- Single long-running processes
+
+For stateless environments, use RedisBackend or DynamoDBBackend.
+
+
+**Methods:**
+
+#### `get`
+
+```python
+get(self, key: str) -> Any | None
+```
+
+#### `set`
+
+```python
+set(self, key: str, value: Any, ttl: timedelta | None = None) -> None
+```
+
+#### `delete`
+
+```python
+delete(self, key: str) -> bool
+```
+
+#### `exists`
+
+```python
+exists(self, key: str) -> bool
+```
+
+#### `increment`
+
+```python
+increment(self, key: str, amount: int = 1, ttl: timedelta | None = None) -> int
+```
+
+Atomic increment using lock.
+
+
+#### `list_keys`
+
+```python
+list_keys(self, prefix: str | None = None, limit: int | None = None) -> list[str]
+```
+
+#### `delete_prefix`
+
+```python
+delete_prefix(self, prefix: str) -> int
+```
+
+#### `close`
+
+```python
+close(self) -> None
+```
+
+Clear all data.
+
diff --git a/docs/sdk/keycardai-mcp-client-storage-backends-sqlite.mdx b/docs/sdk/keycardai-mcp-client-storage-backends-sqlite.mdx
new file mode 100644
index 0000000..d524da7
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-storage-backends-sqlite.mdx
@@ -0,0 +1,123 @@
+---
+title: sqlite
+sidebarTitle: sqlite
+---
+
+# `keycardai.mcp.client.storage.backends.sqlite`
+
+
+SQLite file-based storage backend.
+
+## Classes
+
+### `SQLiteBackend`
+
+
+SQLite file-based storage with TTL support.
+
+Stores data persistently in a SQLite database file.
+Suitable for:
+- Local development with persistence
+- Single-instance applications
+- Testing with persistent state
+
+For distributed systems, use RedisBackend or DynamoDBBackend.
+
+
+**Methods:**
+
+#### `get`
+
+```python
+get(self, key: str) -> Any | None
+```
+
+#### `set`
+
+```python
+set(self, key: str, value: Any, ttl: timedelta | None = None) -> None
+```
+
+#### `delete`
+
+```python
+delete(self, key: str) -> bool
+```
+
+#### `exists`
+
+```python
+exists(self, key: str) -> bool
+```
+
+#### `get_many`
+
+```python
+get_many(self, keys: list[str]) -> dict[str, Any]
+```
+
+Optimized batch get using SQL IN clause.
+
+
+#### `set_many`
+
+```python
+set_many(self, items: dict[str, Any], ttl: timedelta | None = None) -> None
+```
+
+Optimized batch set using transaction.
+
+
+#### `delete_many`
+
+```python
+delete_many(self, keys: list[str]) -> int
+```
+
+Optimized batch delete using SQL IN clause.
+
+
+#### `increment`
+
+```python
+increment(self, key: str, amount: int = 1, ttl: timedelta | None = None) -> int
+```
+
+Atomic increment using database transaction.
+
+
+#### `list_keys`
+
+```python
+list_keys(self, prefix: str | None = None, limit: int | None = None) -> list[str]
+```
+
+#### `delete_prefix`
+
+```python
+delete_prefix(self, prefix: str) -> int
+```
+
+Optimized prefix delete using SQL LIKE.
+
+
+#### `cleanup_expired`
+
+```python
+cleanup_expired(self) -> int
+```
+
+Remove all expired entries.
+
+This is a maintenance operation that can be called periodically.
+Returns count of deleted entries.
+
+
+#### `close`
+
+```python
+close(self) -> None
+```
+
+Close database connection.
+
diff --git a/docs/sdk/keycardai-mcp-client-storage-namespaced.mdx b/docs/sdk/keycardai-mcp-client-storage-namespaced.mdx
new file mode 100644
index 0000000..5f5d18e
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-storage-namespaced.mdx
@@ -0,0 +1,123 @@
+---
+title: namespaced
+sidebarTitle: namespaced
+---
+
+# `keycardai.mcp.client.storage.namespaced`
+
+
+Namespaced storage wrapper.
+
+## Classes
+
+### `NamespacedStorage`
+
+
+Wraps a backend with namespace isolation.
+
+Provides:
+- Automatic key prefixing for isolation
+- Hierarchical namespaces (user:alice:server:slack)
+- Easy bulk cleanup by namespace
+
+
+**Methods:**
+
+#### `get`
+
+```python
+get(self, key: str) -> Any | None
+```
+
+Get value from this namespace.
+
+
+#### `set`
+
+```python
+set(self, key: str, value: Any, ttl: timedelta | None = None) -> None
+```
+
+Set value in this namespace.
+
+
+#### `delete`
+
+```python
+delete(self, key: str) -> bool
+```
+
+Delete key from this namespace.
+
+
+#### `exists`
+
+```python
+exists(self, key: str) -> bool
+```
+
+Check if key exists in this namespace.
+
+
+#### `get_many`
+
+```python
+get_many(self, keys: list[str]) -> dict[str, Any]
+```
+
+Get multiple values (returns logical keys).
+
+
+#### `set_many`
+
+```python
+set_many(self, items: dict[str, Any], ttl: timedelta | None = None) -> None
+```
+
+Set multiple values.
+
+
+#### `delete_many`
+
+```python
+delete_many(self, keys: list[str]) -> int
+```
+
+Delete multiple keys from this namespace.
+
+
+#### `increment`
+
+```python
+increment(self, key: str, amount: int = 1, ttl: timedelta | None = None) -> int
+```
+
+Atomically increment counter.
+
+
+#### `list_keys`
+
+```python
+list_keys(self, limit: int | None = None) -> list[str]
+```
+
+List all keys in this namespace (without prefix).
+
+
+#### `clear`
+
+```python
+clear(self) -> int
+```
+
+Delete all keys in this namespace. Returns count.
+
+
+#### `get_namespace`
+
+```python
+get_namespace(self, sub_namespace: str) -> 'NamespacedStorage'
+```
+
+Create a sub-namespace.
+
diff --git a/docs/sdk/keycardai-mcp-client-storage-path_builder.mdx b/docs/sdk/keycardai-mcp-client-storage-path_builder.mdx
new file mode 100644
index 0000000..49d0708
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-storage-path_builder.mdx
@@ -0,0 +1,112 @@
+---
+title: path_builder
+sidebarTitle: path_builder
+---
+
+# `keycardai.mcp.client.storage.path_builder`
+
+
+Storage path builder for type-safe namespace navigation.
+
+## Classes
+
+### `StoragePathBuilder`
+
+
+Builder for type-safe storage namespace navigation.
+
+Provides a fluent interface for navigating through storage namespaces
+with named methods instead of magic strings.
+
+
+**Methods:**
+
+#### `for_server`
+
+```python
+for_server(self, server_name: str) -> 'StoragePathBuilder'
+```
+
+Navigate to server namespace.
+
+**Args:**
+- `server_name`: Server identifier (e.g., "slack", "github")
+
+**Returns:**
+- Self for method chaining
+
+
+#### `for_connection`
+
+```python
+for_connection(self) -> 'StoragePathBuilder'
+```
+
+Navigate to connection namespace.
+
+**Returns:**
+- Self for method chaining
+
+
+#### `for_oauth`
+
+```python
+for_oauth(self) -> 'StoragePathBuilder'
+```
+
+Navigate to OAuth strategy namespace.
+
+**Returns:**
+- Self for method chaining
+
+
+#### `for_api_key`
+
+```python
+for_api_key(self) -> 'StoragePathBuilder'
+```
+
+Navigate to API key strategy namespace.
+
+**Returns:**
+- Self for method chaining
+
+
+#### `for_namespace`
+
+```python
+for_namespace(self, namespace: str) -> 'StoragePathBuilder'
+```
+
+Navigate to a custom namespace.
+
+**Args:**
+- `namespace`: Custom namespace identifier
+
+**Returns:**
+- Self for method chaining
+
+
+#### `build`
+
+```python
+build(self) -> NamespacedStorage
+```
+
+Get the final storage namespace.
+
+**Returns:**
+- NamespacedStorage at the built path
+
+
+#### `get_full_path`
+
+```python
+get_full_path(self) -> str
+```
+
+Get the full namespace path (for logging/debugging).
+
+**Returns:**
+- Colon-separated namespace path
+
diff --git a/docs/sdk/keycardai-mcp-client-types.mdx b/docs/sdk/keycardai-mcp-client-types.mdx
new file mode 100644
index 0000000..3b1d036
--- /dev/null
+++ b/docs/sdk/keycardai-mcp-client-types.mdx
@@ -0,0 +1,34 @@
+---
+title: types
+sidebarTitle: types
+---
+
+# `keycardai.mcp.client.types`
+
+
+Type definitions for MCP client public interfaces.
+
+## Classes
+
+### `AuthChallenge`
+
+
+Authentication challenge details for a server requiring authentication.
+
+This is returned when a server requires authentication that hasn't been completed yet.
+The exact fields present depend on the authentication strategy configured for the server.
+
+Strategy-specific fields may include (but are not limited to):
+ - authorization_url: For browser-based auth flows
+ - state: For CSRF protection in auth flows
+ - Additional fields as defined by the authentication strategy
+
+
+### `ToolInfo`
+
+
+Information about a tool and which server provides it.
+
+This type makes it explicit which server a tool belongs to,
+useful when you need to track tool provenance across multiple servers.
+
diff --git a/docs/sdk/keycardai-mcp-integrations-fastmcp-__init__.mdx b/docs/sdk/keycardai-mcp-integrations-fastmcp-__init__.mdx
index 0aa97ff..3ced62b 100644
--- a/docs/sdk/keycardai-mcp-integrations-fastmcp-__init__.mdx
+++ b/docs/sdk/keycardai-mcp-integrations-fastmcp-__init__.mdx
@@ -69,8 +69,8 @@ Advanced Configuration:
zone_url="https://keycard.cloud",
mcp_base_url="https://my-server.com",
application_credential=ClientSecret({
- "tenant1": ("id1", "secret1"),
- "tenant2": ("id2", "secret2"),
+ "zone1": ("id1", "secret1"),
+ "zone2": ("id2", "secret2"),
})
)
diff --git a/docs/sdk/keycardai-mcp-integrations-fastmcp-provider.mdx b/docs/sdk/keycardai-mcp-integrations-fastmcp-provider.mdx
index 949765d..9716f1d 100644
--- a/docs/sdk/keycardai-mcp-integrations-fastmcp-provider.mdx
+++ b/docs/sdk/keycardai-mcp-integrations-fastmcp-provider.mdx
@@ -14,9 +14,56 @@ creates a RemoteAuthProvider instance with automatic Keycard zone discovery
and JWT token verification.
+## Functions
+
+### `introspect`
+
+```python
+introspect(self, message, *args, **kwargs)
+```
+
+
+Log at INTROSPECT level - most detailed debugging including token info.
+
+
+### `get_token_debug_info`
+
+```python
+get_token_debug_info(access_token: str) -> dict[str, Any]
+```
+
+
+Extract non-sensitive debugging information from a JWT access token.
+
+This function safely extracts only non-sensitive claims from a JWT token
+for debugging and logging purposes. It does NOT verify the token signature
+and only returns issuer, audience, subject, and scope information.
+
+**Important:** This is a debug function that NEVER raises exceptions. If token
+parsing fails, it returns an error indicator in the result dict instead.
+
+**Security Note:** This function is designed for internal debugging and logging.
+While it includes subject (user identifier), it excludes:
+- The actual token string
+- Custom claims that might contain PII
+
+**Args:**
+- `access_token`: JWT access token string (without Bearer prefix)
+
+**Returns:**
+- Dictionary with token information:
+- - issuer (str): Token issuer (if present)
+- - audience (str | list\[str]): Token audience (if present)
+- - subject (str): Token subject/user identifier (if present)
+- - expires_at (int): Token expiration time as Unix timestamp (if present)
+- - issued_at (int): Token issuance time as Unix timestamp (if present)
+- - scopes (list\[str]): List of scopes from the token (if present)
+- - error (str): Error message if token parsing failed
+
+
## Classes
-### `AccessContext`
+### `AccessContext`
Context object that provides access to exchanged tokens for specific resources.
@@ -27,7 +74,7 @@ allowing partial success scenarios where some resources succeed while others fai
**Methods:**
-#### `set_bulk_tokens`
+#### `set_bulk_tokens`
```python
set_bulk_tokens(self, access_tokens: dict[str, TokenResponse])
@@ -36,7 +83,7 @@ set_bulk_tokens(self, access_tokens: dict[str, TokenResponse])
Set access tokens for resources.
-#### `set_token`
+#### `set_token`
```python
set_token(self, resource: str, token: TokenResponse)
@@ -45,7 +92,7 @@ set_token(self, resource: str, token: TokenResponse)
Set token for the specified resource.
-#### `set_resource_error`
+#### `set_resource_error`
```python
set_resource_error(self, resource: str, error: dict[str, str])
@@ -54,7 +101,7 @@ set_resource_error(self, resource: str, error: dict[str, str])
Set error for a specific resource.
-#### `set_error`
+#### `set_error`
```python
set_error(self, error: dict[str, str])
@@ -63,7 +110,7 @@ set_error(self, error: dict[str, str])
Set error that affects all resources.
-#### `has_resource_error`
+#### `has_resource_error`
```python
has_resource_error(self, resource: str) -> bool
@@ -72,7 +119,7 @@ has_resource_error(self, resource: str) -> bool
Check if a specific resource has an error.
-#### `has_error`
+#### `has_error`
```python
has_error(self) -> bool
@@ -81,7 +128,7 @@ has_error(self) -> bool
Check if there's a global error.
-#### `has_errors`
+#### `has_errors`
```python
has_errors(self) -> bool
@@ -90,7 +137,7 @@ has_errors(self) -> bool
Check if there are any errors (global or resource-specific).
-#### `get_errors`
+#### `get_errors`
```python
get_errors(self) -> dict[str, Any] | None
@@ -99,7 +146,7 @@ get_errors(self) -> dict[str, Any] | None
Get global errors if any.
-#### `get_error`
+#### `get_error`
```python
get_error(self) -> dict[str, str] | None
@@ -108,7 +155,7 @@ get_error(self) -> dict[str, str] | None
Get global error if any.
-#### `get_resource_errors`
+#### `get_resource_errors`
```python
get_resource_errors(self, resource: str) -> dict[str, str] | None
@@ -117,7 +164,7 @@ get_resource_errors(self, resource: str) -> dict[str, str] | None
Get error for a specific resource.
-#### `get_status`
+#### `get_status`
```python
get_status(self) -> str
@@ -126,7 +173,7 @@ get_status(self) -> str
Get overall status of the access context.
-#### `get_successful_resources`
+#### `get_successful_resources`
```python
get_successful_resources(self) -> list[str]
@@ -135,7 +182,7 @@ get_successful_resources(self) -> list[str]
Get list of resources that have successful tokens.
-#### `get_failed_resources`
+#### `get_failed_resources`
```python
get_failed_resources(self) -> list[str]
@@ -144,7 +191,7 @@ get_failed_resources(self) -> list[str]
Get list of resources that have errors.
-#### `access`
+#### `access`
```python
access(self, resource: str) -> TokenResponse
@@ -162,7 +209,7 @@ Get token response for the specified resource.
- `ResourceAccessError`: If resource was not granted or has an error
-### `AuthProvider`
+### `AuthProvider`
Keycard authentication provider for FastMCP.
@@ -177,7 +224,7 @@ Advanced use cases:
**Methods:**
-#### `get_jwt_token_verifier`
+#### `get_jwt_token_verifier`
```python
get_jwt_token_verifier(self) -> JWTVerifier
@@ -185,20 +232,17 @@ get_jwt_token_verifier(self) -> JWTVerifier
Create a JWT token verifier for Keycard zone tokens.
-Discovers Keycard zone metadata and creates a JWTVerifier configured
-with the zone's JWKS URI and issuer information.
+Creates a JWTVerifier configured with the zone's JWKS URI and issuer
+information that was discovered during AuthProvider initialization.
-This method uses eager discovery of the zone metadata, and performs HTTP calls using the initialized client.
+Note: Zone metadata discovery happens in __init__. This method only
+creates the verifier object with already-discovered values.
**Returns:**
- Configured JWT token verifier for the Keycard zone
-**Raises:**
-- `MetadataDiscoveryError`: If zone metadata discovery fails
-- `JWKSValidationError`: If JWKS URI is not available
-
-#### `get_remote_auth_provider`
+#### `get_remote_auth_provider`
```python
get_remote_auth_provider(self) -> RemoteAuthProvider
@@ -206,16 +250,17 @@ get_remote_auth_provider(self) -> RemoteAuthProvider
Get a RemoteAuthProvider instance configured for Keycard authentication.
-This method uses eager discovery of the zone metadata, and performs HTTP calls using the initialized client.
+Creates a RemoteAuthProvider using the zone configuration that was
+discovered and validated during AuthProvider initialization.
+
+Note: Zone metadata discovery and validation happens in __init__. This method
+only creates the RemoteAuthProvider object with already-validated configuration.
**Returns:**
- Configured authentication provider for use with FastMCP
-**Raises:**
-- `MetadataDiscoveryError`: If zone metadata discovery fails or JWKS URI is not available
-
-#### `grant`
+#### `grant`
```python
grant(self, resources: str | list[str])
diff --git a/docs/sdk/keycardai-mcp-server-auth-application_credentials.mdx b/docs/sdk/keycardai-mcp-server-auth-application_credentials.mdx
index 521b8ec..80023e4 100644
--- a/docs/sdk/keycardai-mcp-server-auth-application_credentials.mdx
+++ b/docs/sdk/keycardai-mcp-server-auth-application_credentials.mdx
@@ -186,7 +186,7 @@ providing stronger security than shared secrets.
**Methods:**
-#### `get_http_client_auth`
+#### `get_http_client_auth`
```python
get_http_client_auth(self) -> AuthStrategy
@@ -201,7 +201,7 @@ Returns NoneAuth since WebIdentity uses client assertions in the request body
- NoneAuth instance for no HTTP client authentication
-#### `set_client_config`
+#### `set_client_config`
```python
set_client_config(self, config: ClientConfig, auth_info: dict[str, str]) -> ClientConfig
@@ -228,7 +228,7 @@ Sets up the client configuration with:
- `KeyError`: If required fields are not in auth_info
-#### `get_jwks`
+#### `get_jwks`
```python
get_jwks(self) -> JsonWebKeySet
@@ -240,7 +240,7 @@ Get JWKS for public key distribution.
- JsonWebKeySet containing the public keys
-#### `prepare_token_exchange_request`
+#### `prepare_token_exchange_request`
```python
prepare_token_exchange_request(self, client: AsyncClient, subject_token: str, resource: str, auth_info: dict[str, str] | None = None) -> TokenExchangeRequest
@@ -261,10 +261,10 @@ it in the token exchange request for client authentication.
- TokenExchangeRequest with JWT client assertion
**Raises:**
-- `KeyError`: If auth_info doesn't contain "resource_client_id"
+- `ValueError`: If auth_info doesn't contain "resource_client_id"
-### `EKSWorkloadIdentity`
+### `EKSWorkloadIdentity`
EKS workload identity provider using mounted tokens.
@@ -276,17 +276,15 @@ via initialization parameters or environment variables.
The token is read fresh on each token exchange request, allowing for token rotation
without requiring application restart.
-**Environment Variables:**
-
-When configured via `KEYCARD_APPLICATION_CREDENTIAL_TYPE="eks_workload_identity"`, the token file path is discovered from:
-1. `KEYCARD_EKS_WORKLOAD_IDENTITY_TOKEN_FILE` - Custom token file path (highest priority)
-2. `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` - AWS EKS default location
-3. `AWS_WEB_IDENTITY_TOKEN_FILE` - AWS fallback location
+Environment Variable Discovery (when token_file_path is not provided):
+ 1. KEYCARD_EKS_WORKLOAD_IDENTITY_TOKEN_FILE - Custom token file path (highest priority)
+ 2. AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE - AWS EKS default location
+ 3. AWS_WEB_IDENTITY_TOKEN_FILE - AWS fallback location
**Methods:**
-#### `get_http_client_auth`
+#### `get_http_client_auth`
```python
get_http_client_auth(self) -> AuthStrategy
@@ -301,7 +299,7 @@ body (EKS token) rather than HTTP client authentication.
- NoneAuth instance for no HTTP client authentication
-#### `set_client_config`
+#### `set_client_config`
```python
set_client_config(self, config: ClientConfig, auth_info: dict[str, str]) -> ClientConfig
@@ -320,7 +318,7 @@ token is provided in the token exchange request itself.
- Unmodified ClientConfig
-#### `prepare_token_exchange_request`
+#### `prepare_token_exchange_request`
```python
prepare_token_exchange_request(self, client: AsyncClient, subject_token: str, resource: str, auth_info: dict[str, str] | None = None) -> TokenExchangeRequest
diff --git a/docs/sdk/keycardai-mcp-server-auth-provider.mdx b/docs/sdk/keycardai-mcp-server-auth-provider.mdx
index 7272faf..d68a4a2 100644
--- a/docs/sdk/keycardai-mcp-server-auth-provider.mdx
+++ b/docs/sdk/keycardai-mcp-server-auth-provider.mdx
@@ -7,7 +7,7 @@ sidebarTitle: provider
## Classes
-### `AccessContext`
+### `AccessContext`
Context object that provides access to exchanged tokens for specific resources.
@@ -18,7 +18,7 @@ allowing partial success scenarios where some resources succeed while others fai
**Methods:**
-#### `set_bulk_tokens`
+#### `set_bulk_tokens`
```python
set_bulk_tokens(self, access_tokens: dict[str, TokenResponse])
@@ -27,7 +27,7 @@ set_bulk_tokens(self, access_tokens: dict[str, TokenResponse])
Set access tokens for resources.
-#### `set_token`
+#### `set_token`
```python
set_token(self, resource: str, token: TokenResponse)
@@ -36,7 +36,7 @@ set_token(self, resource: str, token: TokenResponse)
Set token for the specified resource.
-#### `set_resource_error`
+#### `set_resource_error`
```python
set_resource_error(self, resource: str, error: dict[str, str])
@@ -45,7 +45,7 @@ set_resource_error(self, resource: str, error: dict[str, str])
Set error for a specific resource.
-#### `set_error`
+#### `set_error`
```python
set_error(self, error: dict[str, str])
@@ -54,7 +54,7 @@ set_error(self, error: dict[str, str])
Set error that affects all resources.
-#### `has_resource_error`
+#### `has_resource_error`
```python
has_resource_error(self, resource: str) -> bool
@@ -63,7 +63,7 @@ has_resource_error(self, resource: str) -> bool
Check if a specific resource has an error.
-#### `has_error`
+#### `has_error`
```python
has_error(self) -> bool
@@ -72,7 +72,7 @@ has_error(self) -> bool
Check if there's a global error.
-#### `has_errors`
+#### `has_errors`
```python
has_errors(self) -> bool
@@ -81,7 +81,7 @@ has_errors(self) -> bool
Check if there are any errors (global or resource-specific).
-#### `get_errors`
+#### `get_errors`
```python
get_errors(self) -> dict[str, Any] | None
@@ -90,7 +90,7 @@ get_errors(self) -> dict[str, Any] | None
Get global errors if any.
-#### `get_error`
+#### `get_error`
```python
get_error(self) -> dict[str, str] | None
@@ -99,7 +99,7 @@ get_error(self) -> dict[str, str] | None
Get global error if any.
-#### `get_resource_errors`
+#### `get_resource_errors`
```python
get_resource_errors(self, resource: str) -> dict[str, str] | None
@@ -108,7 +108,7 @@ get_resource_errors(self, resource: str) -> dict[str, str] | None
Get error for a specific resource.
-#### `get_status`
+#### `get_status`
```python
get_status(self) -> str
@@ -117,7 +117,7 @@ get_status(self) -> str
Get overall status of the access context.
-#### `get_successful_resources`
+#### `get_successful_resources`
```python
get_successful_resources(self) -> list[str]
@@ -126,7 +126,7 @@ get_successful_resources(self) -> list[str]
Get list of resources that have successful tokens.
-#### `get_failed_resources`
+#### `get_failed_resources`
```python
get_failed_resources(self) -> list[str]
@@ -135,7 +135,7 @@ get_failed_resources(self) -> list[str]
Get list of resources that have errors.
-#### `access`
+#### `access`
```python
access(self, resource: str) -> TokenResponse
@@ -153,7 +153,7 @@ Get token response for the specified resource.
- `ResourceAccessError`: If resource was not granted or has an error
-### `AuthProvider`
+### `AuthProvider`
Keycard authentication provider with token exchange capabilities.
@@ -164,7 +164,7 @@ This provider handles both authentication (token verification) and authorization
**Methods:**
-#### `get_auth_settings`
+#### `get_auth_settings`
```python
get_auth_settings(self) -> AuthSettings
@@ -173,7 +173,7 @@ get_auth_settings(self) -> AuthSettings
Get authentication settings for the MCP server.
-#### `get_token_verifier`
+#### `get_token_verifier`
```python
get_token_verifier(self, enable_multi_zone: bool | None = None) -> TokenVerifier
@@ -182,7 +182,7 @@ get_token_verifier(self, enable_multi_zone: bool | None = None) -> TokenVerifier
Get a token verifier for the MCP server.
-#### `grant`
+#### `grant`
```python
grant(self, resources: str | list[str])
@@ -213,7 +213,7 @@ Error handling:
- Provides detailed error messages for debugging
-#### `get_mcp_router`
+#### `get_mcp_router`
```python
get_mcp_router(self, mcp_app: ASGIApp) -> Sequence[Route]
@@ -231,7 +231,7 @@ including OAuth metadata endpoints and the main MCP application with authenticat
- Sequence of routes including metadata mount and protected MCP mount
-#### `app`
+#### `app`
```python
app(self, mcp_app: FastMCP, middleware: list[Middleware] | None = None) -> ASGIApp
diff --git a/docs/sdk/keycardai-mcp-server-exceptions.mdx b/docs/sdk/keycardai-mcp-server-exceptions.mdx
index 71171a7..36bd26d 100644
--- a/docs/sdk/keycardai-mcp-server-exceptions.mdx
+++ b/docs/sdk/keycardai-mcp-server-exceptions.mdx
@@ -33,103 +33,103 @@ This exception is raised during AuthProvider initialization when
the provided configuration is invalid or incomplete.
-### `OAuthClientConfigurationError`
+### `OAuthClientConfigurationError`
Raised when OAuth client is misconfigured.
-### `MetadataDiscoveryError`
+### `MetadataDiscoveryError`
Raised when Keycard zone metadata discovery fails.
-### `JWKSInitializationError`
+### `JWKSInitializationError`
Raised when JWKS initialization fails.
-### `JWKSValidationError`
+### `JWKSValidationError`
Raised when JWKS URI validation fails.
-### `JWKSDiscoveryError`
+### `JWKSDiscoveryError`
JWKS discovery failed, typically due to invalid zone_id or unreachable endpoint.
-### `TokenValidationError`
+### `TokenValidationError`
Token validation failed due to invalid token format, signature, or claims.
-### `TokenExchangeError`
+### `TokenExchangeError`
Raised when OAuth token exchange fails.
-### `UnsupportedAlgorithmError`
+### `UnsupportedAlgorithmError`
JWT algorithm is not supported by the verifier.
-### `VerifierConfigError`
+### `VerifierConfigError`
Token verifier configuration is invalid.
-### `CacheError`
+### `CacheError`
JWKS cache operation failed.
-### `MissingContextError`
+### `MissingContextError`
Raised when grant decorator encounters a missing context error.
-### `MissingAccessContextError`
+### `MissingAccessContextError`
Raised when grant decorator encounters a missing AccessContext error.
-### `ResourceAccessError`
+### `ResourceAccessError`
Raised when accessing a resource token fails.
-### `AuthProviderInternalError`
+### `AuthProviderInternalError`
Raised when an internal error occurs in AuthProvider that requires support assistance.
-### `AuthProviderRemoteError`
+### `AuthProviderRemoteError`
Raised when AuthProvider cannot connect to or validate the Keycard zone.
-### `ClientInitializationError`
+### `ClientInitializationError`
Raised when OAuth client initialization fails.
-### `EKSWorkloadIdentityConfigurationError`
+### `EKSWorkloadIdentityConfigurationError`
Raised when EKS Workload Identity is misconfigured at initialization.
@@ -139,7 +139,7 @@ the token file is not accessible or the configuration is invalid. This indicates
a configuration problem that prevents the provider from starting.
-### `EKSWorkloadIdentityRuntimeError`
+### `EKSWorkloadIdentityRuntimeError`
Raised when EKS Workload Identity token cannot be read at runtime.
@@ -149,7 +149,7 @@ cannot be read. This indicates a runtime problem (e.g., token file was deleted,
permissions changed, or token rotation failed) rather than a configuration issue.
-### `ClientSecretConfigurationError`
+### `ClientSecretConfigurationError`
Raised when ClientSecret credential provider is misconfigured.
diff --git a/docs/sdk/keycardai-mcp-server-handlers-jwks.mdx b/docs/sdk/keycardai-mcp-server-handlers-jwks.mdx
index 454e25a..20d68e8 100644
--- a/docs/sdk/keycardai-mcp-server-handlers-jwks.mdx
+++ b/docs/sdk/keycardai-mcp-server-handlers-jwks.mdx
@@ -21,8 +21,11 @@ jwks_endpoint(jwks: JsonWebKeySet) -> Callable
```
-Create a JWKS endpoint that returns a dummy RSA key for testing.
+Create a JWKS endpoint that serves the provided JSON Web Key Set.
+
+**Args:**
+- `jwks`: JSON Web Key Set to serve at this endpoint
**Returns:**
-- Callable endpoint that serves JWKS data
+- Callable endpoint that serves the JWKS data
diff --git a/docs/sdk/keycardai-oauth-types-models.mdx b/docs/sdk/keycardai-oauth-types-models.mdx
index c6c30a7..f1851f1 100644
--- a/docs/sdk/keycardai-oauth-types-models.mdx
+++ b/docs/sdk/keycardai-oauth-types-models.mdx
@@ -44,7 +44,26 @@ OAuth 2.0 Token Revocation Request as defined in RFC 7009 Section 2.1.
Reference: https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
-### `ClientRegistrationRequest`
+### `OAuthClientMetadata`
+
+
+Base OAuth 2.0 Client Metadata fields (RFC 7591 Section 2).
+
+Common metadata fields shared across client registration requests,
+responses, and client information representations.
+
+Reference: https://datatracker.ietf.org/doc/html/rfc7591#section-2
+
+
+### `OAuthClientMetadataFull`
+
+
+OAuth 2.0 Client Metadata fields (RFC 7591 Section 2).
+
+Reference: https://datatracker.ietf.org/doc/html/rfc7591#section-2
+
+
+### `ClientRegistrationRequest`
Dynamic Client Registration Request as defined in RFC 7591 Section 2.
@@ -52,7 +71,7 @@ Dynamic Client Registration Request as defined in RFC 7591 Section 2.
Reference: https://datatracker.ietf.org/doc/html/rfc7591#section-2
-### `ClientRegistrationResponse`
+### `ClientRegistrationResponse`
RFC 7591 Dynamic Client Registration Response.
@@ -61,7 +80,7 @@ Preserves all RFC 7591 fields plus vendor extensions and response metadata.
Reference: https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1
-### `PushedAuthorizationRequest`
+### `PushedAuthorizationRequest`
Pushed Authorization Request as defined in RFC 9126 Section 2.
@@ -69,7 +88,7 @@ Pushed Authorization Request as defined in RFC 9126 Section 2.
Reference: https://datatracker.ietf.org/doc/html/rfc9126#section-2
-### `ServerMetadataRequest`
+### `ServerMetadataRequest`
OAuth 2.0 Authorization Server Metadata discovery request as defined in RFC 8414.
@@ -77,7 +96,7 @@ OAuth 2.0 Authorization Server Metadata discovery request as defined in RFC 8414
Reference: https://datatracker.ietf.org/doc/html/rfc8414#section-3
-### `AuthorizationServerMetadata`
+### `AuthorizationServerMetadata`
OAuth 2.0 Authorization Server Metadata (RFC 8414).
@@ -88,7 +107,7 @@ with support for all standard and optional fields.
Reference: https://datatracker.ietf.org/doc/html/rfc8414#section-2
-### `JsonWebKey`
+### `JsonWebKey`
JSON Web Key (JWK) as defined in RFC 7517 Section 4.
@@ -96,7 +115,7 @@ JSON Web Key (JWK) as defined in RFC 7517 Section 4.
Reference: https://datatracker.ietf.org/doc/html/rfc7517#section-4
-### `JsonWebKeySet`
+### `JsonWebKeySet`
JSON Web Key Set (JWKS) as defined in RFC 7517 Section 5.
@@ -104,19 +123,19 @@ JSON Web Key Set (JWKS) as defined in RFC 7517 Section 5.
Reference: https://datatracker.ietf.org/doc/html/rfc7517#section-5
-### `PKCE`
+### `PKCE`
RFC 7636 PKCE Challenge with S256 method support.
-### `Endpoints`
+### `Endpoints`
Type-safe endpoint configuration for unified client.
-### `ClientConfig`
+### `ClientConfig`
Comprehensive client configuration with enterprise defaults.
diff --git a/docs/welcome.mdx b/docs/welcome.mdx
index fb4dba4..787840d 100644
--- a/docs/welcome.mdx
+++ b/docs/welcome.mdx
@@ -23,51 +23,62 @@ Choose the package that best fits your needs:
- For MCP server functionality: `keycardai-mcp`
- For FastMCP integration: `keycardai-mcp-fastmcp`
-## Installation
+## Quick Start
-Install the packages you need using `pip` or `uv`:
+Create your first authenticated MCP server in 4 simple steps:
-### Using pip
+### Step 1: Create Project
```bash
-# OAuth package only
-pip install keycardai-oauth
-
-# MCP package only
-pip install keycardai-mcp
-
-# FastMCP integration
-pip install keycardai-mcp-fastmcp
+uv init --package my-mcp-server
+cd my-mcp-server
```
-### Using uv
+### Step 2: Install Dependencies
```bash
-# OAuth package only
-uv add keycardai-oauth
+uv add keycardai-mcp-fastmcp fastmcp
+```
-# MCP package only
-uv add keycardai-mcp
+### Step 3: Create Your Server
-# FastMCP integration
-uv add keycardai-mcp-fastmcp
+Create `src/my_mcp_server/__init__.py`:
+
+```python
+from keycardai.mcp.integrations.fastmcp import AuthProvider
+from fastmcp import FastMCP
+
+# Configure Keycard authentication
+auth_provider = AuthProvider(
+ zone_id="your-zone-id", # Get this from console.keycard.ai
+ mcp_server_name="My Server",
+ mcp_base_url="http://localhost:8000/"
+)
+
+# Create authenticated MCP server
+auth = auth_provider.get_remote_auth_provider()
+mcp = FastMCP("My Server", auth=auth)
+
+@mcp.tool()
+def hello_world(name: str) -> str:
+ """Say hello to someone."""
+ return f"Hello, {name}!"
+
+def main():
+ """Entry point for the MCP server."""
+ mcp.run(transport="streamable-http")
```
-## Quick Example
+### Step 4: Run Your Server
-```python
-from keycardai.oauth import AsyncClient
-
-async def main():
- client = AsyncClient(
- base_url="https://your-keycard-zone.com",
- client_id="your-client-id"
- )
-
- # Your OAuth operations here
- pass
+```bash
+uv run my-mcp-server
```
+🎉 Your authenticated MCP server is now running on `http://localhost:8000`!
+
+See the [FastMCP Integration example](examples/fastmcp-integration) for advanced features like delegated access to external APIs.
+
## Development
This project uses:
diff --git a/packages/mcp-fastmcp/src/keycardai/mcp/integrations/fastmcp/provider.py b/packages/mcp-fastmcp/src/keycardai/mcp/integrations/fastmcp/provider.py
index 40507df..66ee3f5 100644
--- a/packages/mcp-fastmcp/src/keycardai/mcp/integrations/fastmcp/provider.py
+++ b/packages/mcp-fastmcp/src/keycardai/mcp/integrations/fastmcp/provider.py
@@ -85,24 +85,24 @@ def get_token_debug_info(access_token: str) -> dict[str, Any]:
This function safely extracts only non-sensitive claims from a JWT token
for debugging and logging purposes. It does NOT verify the token signature
- and only returns issuer, audience, and scope information.
+ and only returns issuer, audience, subject, and scope information.
**Important:** This is a debug function that NEVER raises exceptions. If token
parsing fails, it returns an error indicator in the result dict instead.
- **Security Note:** This function is designed to be safe for logging/debugging
- in production environments. It explicitly excludes sensitive information like:
+ **Security Note:** This function is designed for internal debugging and logging.
+ While it includes subject (user identifier), it excludes:
- The actual token string
- - Subject (user identifier)
- Custom claims that might contain PII
Args:
access_token: JWT access token string (without Bearer prefix)
Returns:
- Dictionary with non-sensitive token information:
+ Dictionary with token information:
- issuer (str): Token issuer (if present)
- audience (str | list[str]): Token audience (if present)
+ - subject (str): Token subject/user identifier (if present)
- expires_at (int): Token expiration time as Unix timestamp (if present)
- issued_at (int): Token issuance time as Unix timestamp (if present)
- scopes (list[str]): List of scopes from the token (if present)
@@ -112,7 +112,7 @@ def get_token_debug_info(access_token: str) -> dict[str, Any]:
>>> token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
>>> debug_info = get_token_debug_info(token)
>>> logger.info(f"Token info: {debug_info}")
- # Success: {"issuer": "https://auth.example.com", "audience": "api.example.com", "expires_at": 1700000000, "issued_at": 1699996400, "scopes": ["read", "write"]}
+ # Success: {"issuer": "https://auth.example.com", "audience": "api.example.com", "subject": "user123", "expires_at": 1700000000, "issued_at": 1699996400, "scopes": ["read", "write"]}
# Failure: {"error": "Failed to parse token"}
"""
try:
@@ -532,17 +532,14 @@ def _discover_jwks_uri(self, client: Client) -> str | None:
def get_jwt_token_verifier(self) -> JWTVerifier:
"""Create a JWT token verifier for Keycard zone tokens.
- Discovers Keycard zone metadata and creates a JWTVerifier configured
- with the zone's JWKS URI and issuer information.
+ Creates a JWTVerifier configured with the zone's JWKS URI and issuer
+ information that was discovered during AuthProvider initialization.
- This method uses eager discovery of the zone metadata, and performs HTTP calls using the initialized client.
+ Note: Zone metadata discovery happens in __init__. This method only
+ creates the verifier object with already-discovered values.
Returns:
JWTVerifier: Configured JWT token verifier for the Keycard zone
-
- Raises:
- MetadataDiscoveryError: If zone metadata discovery fails
- JWKSValidationError: If JWKS URI is not available
"""
return JWTVerifier(
jwks_uri=self.jwks_uri,
@@ -554,13 +551,14 @@ def get_jwt_token_verifier(self) -> JWTVerifier:
def get_remote_auth_provider(self) -> RemoteAuthProvider:
"""Get a RemoteAuthProvider instance configured for Keycard authentication.
- This method uses eager discovery of the zone metadata, and performs HTTP calls using the initialized client.
+ Creates a RemoteAuthProvider using the zone configuration that was
+ discovered and validated during AuthProvider initialization.
+
+ Note: Zone metadata discovery and validation happens in __init__. This method
+ only creates the RemoteAuthProvider object with already-validated configuration.
Returns:
RemoteAuthProvider: Configured authentication provider for use with FastMCP
-
- Raises:
- MetadataDiscoveryError: If zone metadata discovery fails or JWKS URI is not available
"""
authorization_servers = [AnyHttpUrl(self.zone_url)]
diff --git a/packages/mcp/src/keycardai/mcp/client/auth/oauth/discovery.py b/packages/mcp/src/keycardai/mcp/client/auth/oauth/discovery.py
index fcd6cc2..ef704c2 100644
--- a/packages/mcp/src/keycardai/mcp/client/auth/oauth/discovery.py
+++ b/packages/mcp/src/keycardai/mcp/client/auth/oauth/discovery.py
@@ -52,7 +52,8 @@ async def discover_resource(self, challenge_response: Response) -> dict[str, Any
Resource metadata including authorization_servers list
Raises:
- ValueError: If discovery fails or no authorization servers found
+ ValueError: If no authorization servers found in discovery response
+ httpx.HTTPStatusError: If discovery request fails
Example:
>>> service = OAuthDiscoveryService(storage)
@@ -103,7 +104,8 @@ async def discover_auth_server(
- etc.
Raises:
- ValueError: If discovery fails or required fields missing
+ ValueError: If no authorization servers found, required fields missing,
+ or all discovery attempts fail
Example:
>>> metadata = await service.discover_auth_server(resource_metadata)
diff --git a/packages/mcp/src/keycardai/mcp/client/auth/oauth/registration.py b/packages/mcp/src/keycardai/mcp/client/auth/oauth/registration.py
index 5317f85..f04ef1b 100644
--- a/packages/mcp/src/keycardai/mcp/client/auth/oauth/registration.py
+++ b/packages/mcp/src/keycardai/mcp/client/auth/oauth/registration.py
@@ -66,7 +66,8 @@ async def get_or_register_client(
- etc.
Raises:
- ValueError: If registration endpoint not found or registration fails
+ ValueError: If registration endpoint not found
+ httpx.HTTPStatusError: If registration request fails
Example:
>>> service = OAuthClientRegistrationService(storage, "My App")
@@ -111,7 +112,8 @@ async def _register_new_client(
Client registration information
Raises:
- ValueError: If registration endpoint missing or registration fails
+ ValueError: If registration endpoint is missing
+ httpx.HTTPStatusError: If registration request fails
"""
registration_endpoint = auth_server_metadata.get("registration_endpoint")
if not registration_endpoint:
diff --git a/packages/mcp/src/keycardai/mcp/client/client.py b/packages/mcp/src/keycardai/mcp/client/client.py
index 470fa38..d26f0d3 100644
--- a/packages/mcp/src/keycardai/mcp/client/client.py
+++ b/packages/mcp/src/keycardai/mcp/client/client.py
@@ -252,7 +252,8 @@ async def call_tool(self, tool_name: str, arguments: dict[str, Any], server_name
The content is a list of content items (text, images, embedded resources).
Raises:
- ToolNotFoundException: If server_name is None and the tool is not found on any server
+ ToolNotFoundException: If the tool is not found on any server (when server_name is None)
+ or if the specified server is not connected/available
Example:
result = await client.call_tool("fetch_data", {"url": "https://api.example.com"})
diff --git a/packages/mcp/src/keycardai/mcp/client/connection/base.py b/packages/mcp/src/keycardai/mcp/client/connection/base.py
index 42b3b82..91c3d68 100644
--- a/packages/mcp/src/keycardai/mcp/client/connection/base.py
+++ b/packages/mcp/src/keycardai/mcp/client/connection/base.py
@@ -41,7 +41,8 @@ async def start(self) -> tuple[Any, Any]:
Tuple of (read_stream, write_stream)
Raises:
- ConnectionError: If connection fails
+ ConnectionError: If connection fails to establish
+ Exception: Any exception raised during connection establishment
"""
self._ready.clear()
self._done.clear()
diff --git a/packages/mcp/src/keycardai/mcp/server/auth/application_credentials.py b/packages/mcp/src/keycardai/mcp/server/auth/application_credentials.py
index 430f0b5..04356d1 100644
--- a/packages/mcp/src/keycardai/mcp/server/auth/application_credentials.py
+++ b/packages/mcp/src/keycardai/mcp/server/auth/application_credentials.py
@@ -279,9 +279,6 @@ def __init__(
- str: Single audience for all zones
- dict: Zone-specific audience mapping (zone_id -> audience)
- None: Use issuer as audience
-
- Raises:
- RuntimeError: If key pair bootstrap fails
"""
# Initialize storage
if storage is not None:
@@ -379,7 +376,7 @@ async def prepare_token_exchange_request(
TokenExchangeRequest with JWT client assertion
Raises:
- KeyError: If auth_info doesn't contain "resource_client_id"
+ ValueError: If auth_info doesn't contain "resource_client_id"
"""
if not auth_info or "resource_client_id" not in auth_info:
raise ValueError("auth_info with 'resource_client_id' is required for WebIdentity")
diff --git a/packages/mcp/src/keycardai/mcp/server/handlers/jwks.py b/packages/mcp/src/keycardai/mcp/server/handlers/jwks.py
index bc24a8d..9263bd9 100644
--- a/packages/mcp/src/keycardai/mcp/server/handlers/jwks.py
+++ b/packages/mcp/src/keycardai/mcp/server/handlers/jwks.py
@@ -13,10 +13,13 @@
def jwks_endpoint(jwks: JsonWebKeySet) -> Callable:
- """Create a JWKS endpoint that returns a dummy RSA key for testing.
+ """Create a JWKS endpoint that serves the provided JSON Web Key Set.
+
+ Args:
+ jwks: JSON Web Key Set to serve at this endpoint
Returns:
- Callable endpoint that serves JWKS data
+ Callable endpoint that serves the JWKS data
"""
def wrapper(request: Request) -> JSONResponse:
return JSONResponse(
diff --git a/pyproject.toml b/pyproject.toml
index b27af67..4eb1474 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,7 +51,7 @@ Issues = "https://github.com/keycardai/python-sdk/issues"
[tool.uv.workspace]
members = [
".",
- "packages/*",
+ "packages/*"
]
# Exclude any packages that shouldn't be part of the workspace
exclude = []
diff --git a/uv.lock b/uv.lock
index 7cc76c6..b066275 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,5 @@
version = 1
-revision = 3
+revision = 2
requires-python = ">=3.10"
[manifest]
@@ -8,6 +8,9 @@ members = [
"keycardai-mcp",
"keycardai-mcp-fastmcp",
"keycardai-oauth",
+ "my-mcp-client",
+ "my-mcp-server",
+ "my-secure-mcp",
]
[[package]]
@@ -1813,6 +1816,49 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" },
]
+[[package]]
+name = "my-mcp-client"
+version = "0.1.0"
+source = { editable = "docs-testing/my-mcp-client" }
+dependencies = [
+ { name = "keycardai-mcp" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "keycardai-mcp", editable = "packages/mcp" }]
+
+[[package]]
+name = "my-mcp-server"
+version = "0.1.0"
+source = { editable = "docs-testing/my-mcp-server" }
+dependencies = [
+ { name = "fastmcp" },
+ { name = "keycardai-mcp-fastmcp" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "fastmcp", specifier = ">=2.13.0" },
+ { name = "keycardai-mcp-fastmcp", editable = "packages/mcp-fastmcp" },
+]
+
+[[package]]
+name = "my-secure-mcp"
+version = "0.1.0"
+source = { editable = "docs-testing/my-secure-mcp" }
+dependencies = [
+ { name = "fastmcp" },
+ { name = "keycardai-mcp" },
+ { name = "uvicorn" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "fastmcp", specifier = ">=2.13.0" },
+ { name = "keycardai-mcp", editable = "packages/mcp" },
+ { name = "uvicorn", specifier = ">=0.35.0" },
+]
+
[[package]]
name = "mypy"
version = "1.17.1"