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"