From 0229a79e7ee9673a59824053b947f5cda5acc2b0 Mon Sep 17 00:00:00 2001 From: Larry-Osakwe Date: Mon, 23 Feb 2026 16:10:19 -0800 Subject: [PATCH] Fix docs DX: consistent patterns, narrative guides, CI smoke tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Customer reported broken AccessContext patterns in docs — this overhaul fixes the root cause (duplicated code in MDX files) and adds CI guardrails so examples can't silently drift from the SDK API again. - Rewrite root README with value prop, decision table, correct patterns - Convert docs/ MDX files to narrative-only guides (no code duplication) - Fix delegated_access example: add missing ctx: Context parameter - Fix stale AccessContext = None docstring in provider.py - Add smoke tests for all package examples (mcp, mcp-fastmcp, oauth) - Rename docs nav group from "Examples" to "Guides" --- .gitignore | 3 + README.md | 418 ++++++------------ docs/docs.json | 2 +- docs/examples/fastmcp-integration.mdx | 146 ++---- docs/examples/mcp-client-usage.mdx | 76 +--- docs/examples/mcp-server-auth.mdx | 146 ++---- docs/welcome.mdx | 28 +- packages/mcp-fastmcp/tests/test_examples.py | 80 ++++ packages/mcp/README.md | 12 +- .../mcp/examples/delegated_access/main.py | 11 +- .../src/keycardai/mcp/server/auth/provider.py | 2 +- packages/mcp/tests/test_examples.py | 49 ++ .../discover_server_metadata/README.md | 32 ++ packages/oauth/tests/test_examples.py | 40 ++ 14 files changed, 452 insertions(+), 593 deletions(-) create mode 100644 packages/mcp-fastmcp/tests/test_examples.py create mode 100644 packages/mcp/tests/test_examples.py create mode 100644 packages/oauth/tests/test_examples.py diff --git a/.gitignore b/.gitignore index 1701c8b..6a381fb 100644 --- a/.gitignore +++ b/.gitignore @@ -188,3 +188,6 @@ Thumbs.db # Local development .local/ + +# Claude Code +CLAUDE.md diff --git a/README.md b/README.md index 95f0b45..3ef906b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,18 @@ # Keycard Python SDK -A collection of Python packages for Keycard services, organized as a uv workspace. +**Keycard handles OAuth, identity, and access so your MCP servers don't have to.** Add authentication, authorization, and delegated API access to any MCP server with a few lines of Python — no token plumbing, no auth middleware, no security footguns. -## Requirements +- **Drop-in auth** for MCP servers (OAuth 2.0, PKCE, token exchange — handled for you) +- **Delegated access** — call Google, GitHub, Slack APIs on behalf of your users with automatic token exchange +- **Works with both** the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) and the [FastMCP](https://github.com/jlowin/fastmcp) framework -- **Python 3.10 or greater** -- Virtual environment (recommended) +## Which Package? + +| You want... | Install | Guide | +|---|---|---| +| Auth for MCP servers (using the `mcp` SDK) | `pip install keycardai-mcp` | [Quick Start](#quick-start-standard-mcp) | +| Auth for FastMCP servers | `pip install keycardai-mcp-fastmcp` | [Quick Start](#quick-start-fastmcp) | +| OAuth 2.0 client only | `pip install keycardai-oauth` | [Package docs](packages/oauth/) | ## Known Limitations & Non-Goals @@ -83,242 +90,188 @@ Packages will graduate to `1.0.0` when: 3. **Commit messages** use `!` suffix (e.g., `feat!:`) for breaking changes 4. **Release notes** highlight breaking changes prominently -## Setup Guide - -### Option 1: Using uv (Recommended) +> **`mcp` vs `fastmcp`:** The `mcp` SDK includes a built-in `FastMCP` class (`from mcp.server.fastmcp import FastMCP`), while `fastmcp` is a separate standalone framework (`from fastmcp import FastMCP`). They're different libraries. `keycardai-mcp` wraps the former; `keycardai-mcp-fastmcp` wraps the latter. -If you have [uv](https://docs.astral.sh/uv/) installed: +## Prerequisites -```bash -# Create a new project with uv -uv init my-mcp-project -cd my-mcp-project - -# Create and activate virtual environment -uv venv -source .venv/bin/activate # On Windows: .venv\Scripts\activate -``` +1. **Python 3.10+** and a virtual environment +2. Sign up at [keycard.ai](https://keycard.ai) and get your **zone ID** from Zone Settings +3. Configure an identity provider (Google, Microsoft, etc.) and create an MCP resource in your zone -### Option 2: Using Standard Python +## Quick Start: FastMCP ```bash -# Create project directory -mkdir my-mcp-project -cd my-mcp-project - -# Create and activate virtual environment -python3 -m venv .venv -source .venv/bin/activate # On Windows: .venv\Scripts\activate - -# Upgrade pip (recommended) -pip install --upgrade pip +uv add keycardai-mcp-fastmcp fastmcp ``` -## Quick Start - -Choose the integration that best fits your MCP setup: +```python +from fastmcp import FastMCP +from keycardai.mcp.integrations.fastmcp import AuthProvider -## Quick Start with keycardai-mcp (Standard MCP) +# Configure Keycard authentication +auth_provider = AuthProvider( + zone_id="your-zone-id", # From console.keycard.ai + mcp_server_name="My Server", + mcp_base_url="http://localhost:8000/" +) -For standard MCP servers using the official MCP Python SDK: +# Create authenticated MCP server +auth = auth_provider.get_remote_auth_provider() +mcp = FastMCP("My Server", auth=auth) -### Install the Package +@mcp.tool() +def hello_world(name: str) -> str: + """Say hello to someone.""" + return f"Hello, {name}!" -```bash -pip install keycardai-mcp +if __name__ == "__main__": + mcp.run(transport="streamable-http") ``` -or +See the [FastMCP examples](packages/mcp-fastmcp/examples/) for runnable projects. + +## Quick Start: Standard MCP ```bash -uv add keycardai-mcp +pip install keycardai-mcp uvicorn ``` -### Get Your Keycard Zone ID - -1. Sign up at [keycard.ai](https://keycard.ai) -2. Navigate to Zone Settings to get your zone ID -3. Configure your preferred identity provider (Google, Microsoft, etc.) -4. Create an MCP resource in your zone - -### Add Authentication to Your MCP Server - ```python from mcp.server.fastmcp import FastMCP from keycardai.mcp.server.auth import AuthProvider +import uvicorn -# Your existing MCP server -mcp = FastMCP("My Secure MCP Server") +# Your MCP server +mcp = FastMCP("My Server") @mcp.tool() -def my_protected_tool(data: str) -> str: - return f"Processed: {data}" +def hello_world(name: str) -> str: + """Say hello to someone.""" + return f"Hello, {name}!" # Add Keycard authentication -access = AuthProvider( - zone_id="your_zone_id_here", - mcp_server_name="My Secure MCP Server", - application_credential=ClientSecret(( - os.getenv("KEYCARD_CLIENT_ID"), - os.getenv("KEYCARD_CLIENT_SECRET") - )) +auth_provider = AuthProvider( + zone_id="your-zone-id", # From console.keycard.ai + mcp_server_name="My Server", + mcp_server_url="http://localhost:8000/" ) -# Create authenticated app -app = access.app(mcp) +# Wrap with auth and run +app = auth_provider.app(mcp) +uvicorn.run(app, host="0.0.0.0", port=8000) ``` -### Run with Authentication +See the [MCP examples](packages/mcp/examples/) for runnable projects. -```bash -pip install uvicorn -uvicorn server:app -``` +## Delegated Access + +Delegated access lets your MCP tools call external APIs (Google Calendar, GitHub, Slack, etc.) on behalf of authenticated users via automatic token exchange. -### Add Delegated Access (Optional) +**Setup:** Get client credentials from [console.keycard.ai](https://console.keycard.ai), then set `KEYCARD_CLIENT_ID` and `KEYCARD_CLIENT_SECRET` as environment variables. + +### FastMCP ```python import os -from mcp.server.fastmcp import FastMCP, Context -from keycardai.mcp.server.auth import AuthProvider, AccessContext, ClientSecret +import httpx +from fastmcp import FastMCP, Context +from keycardai.mcp.integrations.fastmcp import AuthProvider, AccessContext, ClientSecret -# Configure your provider with client credentials -access = AuthProvider( - zone_id="your_zone_id", - mcp_server_name="My MCP Server", +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") )) ) -mcp = FastMCP("My MCP Server") +auth = auth_provider.get_remote_auth_provider() +mcp = FastMCP("My Server", auth=auth) @mcp.tool() -@access.grant("https://protected-api") -def protected_tool(ctx: Context, access_context: AccessContext, name: str) -> str: - # Use the access_context to call external APIs on behalf of the user - token = access_context.access("https://protected-api").access_token - # Make authenticated API calls... - return f"Protected data for {name}" - -app = access.app(mcp) -``` - -## Quick Start with keycardai-mcp-fastmcp (FastMCP) - -For FastMCP servers using the FastMCP framework: - -### Install the Package - -```bash -pip install keycardai-mcp-fastmcp -``` - -or - -```bash -uv add keycardai-mcp-fastmcp -``` - -### Get Your Keycard Zone ID - -1. Sign up at [keycard.ai](https://keycard.ai) -2. Navigate to Zone Settings to get your zone ID -3. Configure your preferred identity provider (Google, Microsoft, etc.) -4. Create an MCP resource in your zone - -### Add Authentication to Your FastMCP Server - -```python -from fastmcp import FastMCP, Context -from keycardai.mcp.integrations.fastmcp import AuthProvider - -# Configure Keycard authentication -auth_provider = AuthProvider( - zone_id="your-zone-id", # Get this from keycard.ai - mcp_server_name="My Secure FastMCP Server", - mcp_base_url="http://127.0.0.1:8000/" -) +@auth_provider.grant("https://www.googleapis.com/calendar/v3") +async def get_calendar_events(ctx: Context) -> dict: + """Get the user's calendar events with delegated access.""" + # Retrieve access context from FastMCP context + access_context: AccessContext = ctx.get_state("keycardai") -# Get the RemoteAuthProvider for FastMCP -auth = auth_provider.get_remote_auth_provider() + if access_context.has_errors(): + return {"error": f"Token exchange failed: {access_context.get_errors()}"} -# Create authenticated FastMCP server -mcp = FastMCP("My Secure FastMCP Server", auth=auth) + token = access_context.access("https://www.googleapis.com/calendar/v3").access_token -@mcp.tool() -def hello_world(name: str) -> str: - return f"Hello, {name}!" - -if __name__ == "__main__": - mcp.run(transport="streamable-http") + 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() ``` -### Add Delegated Access (Optional) +### Standard MCP ```python import os -from fastmcp import FastMCP, Context -from keycardai.mcp.integrations.fastmcp import ( - AuthProvider, - AccessContext, - ClientSecret -) +import httpx +from mcp.server.fastmcp import FastMCP, Context +from keycardai.mcp.server.auth import AuthProvider, AccessContext, ClientSecret -# Configure Keycard authentication with client credentials for delegated access auth_provider = AuthProvider( zone_id="your-zone-id", - mcp_server_name="My Secure FastMCP Server", - mcp_base_url="http://127.0.0.1:8000/", + mcp_server_name="My Server", + mcp_server_url="http://localhost:8000/", application_credential=ClientSecret(( os.getenv("KEYCARD_CLIENT_ID"), os.getenv("KEYCARD_CLIENT_SECRET") )) ) -# Get the RemoteAuthProvider for FastMCP -auth = auth_provider.get_remote_auth_provider() - -# Create authenticated FastMCP server -mcp = FastMCP("My Secure FastMCP Server", auth=auth) +mcp = FastMCP("My Server") -# Example with token exchange for external API access @mcp.tool() -@auth_provider.grant("https://api.example.com") -def call_external_api(ctx: Context, query: str) -> str: - # Get access context to check token exchange status - access_context: AccessContext = ctx.get_state("keycardai") - - # Check for errors before accessing token - if access_context.has_errors(): - return f"Error: Failed to obtain access token - {access_context.get_errors()}" +@auth_provider.grant("https://www.googleapis.com/calendar/v3") +async def get_calendar_events(access_ctx: AccessContext, ctx: Context) -> dict: + """Get the user's calendar events with delegated access.""" + # @grant requires both AccessContext (for tokens) and Context (for request state) + if access_ctx.has_errors(): + return {"error": f"Token exchange failed: {access_ctx.get_errors()}"} + + token = access_ctx.access("https://www.googleapis.com/calendar/v3").access_token + + 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() + +app = auth_provider.app(mcp) +``` - # Access delegated token through context namespace - token = access_context.access("https://api.example.com").access_token - # Use token to call external API - return f"Results for {query}" +> **Key difference:** In `keycardai-mcp`, the `@grant` decorator requires both `access_ctx: AccessContext` and `ctx: Context` as function parameters. In `keycardai-mcp-fastmcp`, `AccessContext` is retrieved from the FastMCP `Context` via `ctx.get_state("keycardai")`. -if __name__ == "__main__": - mcp.run(transport="streamable-http") -``` +For complete delegated access examples with error handling patterns, see: +- [FastMCP delegated access example](packages/mcp-fastmcp/examples/delegated_access/) +- [Standard MCP delegated access example](packages/mcp/examples/delegated_access/) -### Configure Your AI Client +## Connecting Your AI Client -Configure the remote MCP in your AI client, like [Cursor](https://cursor.com/?from=home): +Configure the remote MCP in your AI client (e.g., [Cursor](https://cursor.com)): ```json { "mcpServers": { - "my-secure-server": { + "my-server": { "url": "http://localhost:8000/mcp" } } } ``` -### 🎉 Your MCP server is now protected with Keycard authentication! 🎉 - ## Using FastAPI Mounting a FastMCP server into a larger FastAPI service introduces a few @@ -368,7 +321,7 @@ app.mount("/mcp-server/mcp", mcp_app) ### Custom, Non Standards Compliant, Approach -> ![WARNING] +> [!WARNING] > **This is not advised.** Only follow this if you know for sure you need > flexibility outside of what the spec requires. @@ -383,8 +336,8 @@ from fastapi import FastAPI from keycardai.mcp.server.routers.metadata import well_known_metadata_mount auth_provider = AuthProvider( - zone_id="your-zone-id", # Get this from keycard.ai - mcp_server_name="My Secure FastMCP Server", + zone_id="your-zone-id", + mcp_server_name="My Server", mcp_base_url="http://127.0.0.1:8000/" ) @@ -422,8 +375,8 @@ from keycardai.mcp.server.routers.metadata import ( ) auth_provider = AuthProvider( - zone_id="your-zone-id", # Get this from keycard.ai - mcp_server_name="My Secure FastMCP Server", + zone_id="your-zone-id", + mcp_server_name="My Server", mcp_base_url="http://127.0.0.1:8000/" ) @@ -456,104 +409,13 @@ which will produce the following endpoints /my/custom/path/to/well-known/oauth-authorization-server ``` -## Features - -### Delegated Access - -Keycard allows MCP servers to access other resources on behalf of users with automatic consent and secure token exchange. - -#### Setup Protected Resources - -1. **Configure credential provider** (e.g., Google Workspace) -2. **Configure protected resource** (e.g., Google Drive API) -3. **Set MCP server dependencies** to allow delegated access -4. **Create client secret identity** for secure authentication - -## Overview - -This workspace contains multiple Python packages that provide various Keycard functionality: - -- **keycardai-oauth**: OAuth 2.0 implementation with support for RFC 8693 (Token Exchange) -- **keycardai-mcp**: Core MCP (Model Context Protocol) integration utilities for standard MCP servers -- **keycardai-mcp-fastmcp**: FastMCP-specific integration package with decorators and middleware - -## Installation - -### For Standard MCP Servers - -If you're using the official MCP Python SDK: - -```bash -pip install keycardai-mcp -``` - -or - -```bash -uv add keycardai-mcp -``` - -### For FastMCP Servers - -If you're using the FastMCP framework: - -```bash -pip install keycardai-mcp-fastmcp -``` - -or - -```bash -uv add keycardai-mcp-fastmcp -``` - -### For OAuth Functionality Only - -If you only need OAuth capabilities: - -```bash -pip install keycardai-oauth -``` - -### Install from Source - -```bash -git clone git@github.com:keycardai/python-sdk.git -cd python-sdk - -# Install specific packages as needed -pip install ./packages/oauth -pip install ./packages/mcp -pip install ./packages/mcp-fastmcp -``` - -## Documentation - -Comprehensive documentation is available at our [documentation site](https://docs.keycard.ai), including: - -- API reference for all packages -- Usage examples and tutorials -- Integration guides -- Architecture decisions - -## Examples - -Each package includes practical examples in their respective `examples/` directories: - -- **OAuth examples**: Anonymous token exchange, server discovery, dynamic registration -- **MCP examples**: Google API integration with delegated token exchange - -For detailed examples and usage patterns, see our [documentation](https://docs.keycard.ai). - ## FAQ -### How to test the MCP server with modelcontexprotocol/inspector? +### How to test the MCP server with modelcontextprotocol/inspector? -When testing your MCP server with the [modelcontexprotocol/inspector](https://github.com/modelcontextprotocol/inspector), you may need to configure CORS (Cross-Origin Resource Sharing) to allow the inspector's web interface to access your protected endpoints from localhost. +When testing your MCP server with the [modelcontextprotocol/inspector](https://github.com/modelcontextprotocol/inspector), you may need to configure CORS to allow the inspector's web interface to access your protected endpoints from localhost. -**Note:** This applies specifically to the `keycardai-mcp` package. When using `keycardai-mcp-fastmcp`, no middleware is currently required as FastMCP permits access to metadata endpoints by default. - -You can use Starlette's built-in `CORSMiddleware` to configure CORS settings: +**Note:** This applies specifically to `keycardai-mcp`. When using `keycardai-mcp-fastmcp`, no middleware is currently required as FastMCP permits access to metadata endpoints by default. ```python from starlette.middleware import Middleware @@ -562,31 +424,23 @@ from starlette.middleware.cors import CORSMiddleware middleware = [ Middleware( CORSMiddleware, - allow_origins=["*"], # Allow all origins for testing + allow_origins=["*"], # For local dev only allow_credentials=True, - allow_methods=["*"], # Allow all HTTP methods - allow_headers=["*"], # Allow all headers + allow_methods=["*"], + allow_headers=["*"], ) ] -app = access.app(mcp, middleware=middleware) +app = auth_provider.app(mcp, middleware=middleware) ``` -**Important Security Note:** The configuration above uses permissive CORS settings (`allow_origins=["*"]`) which should **only be used for local development and testing**. In production environments, you should restrict `allow_origins` to specific domains that need access to your MCP server. +**Important:** The `allow_origins=["*"]` setting is for **local development only**. In production, restrict to specific domains. -For production use, consider more restrictive settings: +## Documentation -```python -middleware = [ - Middleware( - CORSMiddleware, - allow_origins=["https://yourdomain.com"], # Specific allowed origins - allow_credentials=True, - allow_methods=["GET", "POST"], # Only required methods - allow_headers=["Authorization", "Content-Type"], # Only required headers - ) -] -``` +- [Full documentation](https://docs.keycard.ai) — API reference, tutorials, integration guides +- [Package docs](packages/) — Detailed README for each package +- [Examples](packages/mcp/examples/) — Runnable example projects ## License @@ -594,8 +448,6 @@ This project is licensed under the MIT License - see the LICENSE file for detail ## Support -For questions, issues, or support: - - GitHub Issues: [https://github.com/keycardai/python-sdk/issues](https://github.com/keycardai/python-sdk/issues) -- Documentation: [https://docs.keycardai.com](https://docs.keycard.ai/) +- Documentation: [https://docs.keycard.ai](https://docs.keycard.ai/) - Email: support@keycard.ai diff --git a/docs/docs.json b/docs/docs.json index 034686e..ccdf9fe 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -32,7 +32,7 @@ ] }, { - "group": "Examples", + "group": "Guides", "pages": [ "examples/fastmcp-integration", "examples/mcp-client-usage", diff --git a/docs/examples/fastmcp-integration.mdx b/docs/examples/fastmcp-integration.mdx index bad9f21..af3ac57 100644 --- a/docs/examples/fastmcp-integration.mdx +++ b/docs/examples/fastmcp-integration.mdx @@ -1,141 +1,63 @@ --- title: "FastMCP Integration" -description: "How to use Keycard OAuth with FastMCP servers for automated token exchange" +description: "How Keycard authentication works with FastMCP servers" --- # FastMCP Integration -A step-by-step guide to create an authenticated MCP server with FastMCP and Keycard. +This guide explains how Keycard adds authentication and delegated access to MCP servers built with the [FastMCP](https://github.com/jlowin/fastmcp) framework (`keycardai-mcp-fastmcp` package). -## Quick Start +For the official MCP Python SDK, see the [MCP Server Auth guide](mcp-server-auth). -### Step 1: Create Project +## When to use this package -```bash -uv init --package my-mcp-server -cd my-mcp-server -``` +Use `keycardai-mcp-fastmcp` when your server imports from `fastmcp` (the standalone FastMCP framework). This is different from `mcp.server.fastmcp` (the MCP SDK's built-in FastMCP class) — see the [README](https://github.com/keycardai/python-sdk#which-package) for a comparison. -### Step 2: Install Dependencies +## How authentication works -```bash -uv add keycardai-mcp-fastmcp fastmcp -``` +`AuthProvider` creates a `RemoteAuthProvider` that plugs directly into FastMCP's auth system. You pass it to `FastMCP("My Server", auth=auth)` and FastMCP handles the rest — OAuth metadata, token validation, and session management. -### Step 3: Create Your Server +The setup is two steps: +1. Create your `AuthProvider` with a zone ID +2. Call `auth_provider.get_remote_auth_provider()` and pass the result to FastMCP -Create `src/my_mcp_server/__init__.py`: +Your server runs with `mcp.run(transport="streamable-http")` — no separate ASGI server needed. -```python -from keycardai.mcp.integrations.fastmcp import AuthProvider -from fastmcp import FastMCP +## How delegated access works -# 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/" -) +Delegated access lets your MCP tools call external APIs (Google Calendar, GitHub, Slack, etc.) on behalf of authenticated users. -# Get auth provider for FastMCP -auth = auth_provider.get_remote_auth_provider() +The `@grant` decorator handles token exchange automatically. When a user calls a granted tool: -# Create authenticated MCP server -mcp = FastMCP("My Server", auth=auth) +1. Keycard exchanges the user's token for a scoped token targeting the external API +2. The exchanged token is stored in FastMCP's context state under the `"keycardai"` namespace +3. Your function retrieves it via `ctx.get_state("keycardai")` +4. You extract the token and call the external API -@mcp.tool() -def hello_world(name: str) -> str: - """Say hello to someone.""" - return f"Hello, {name}!" +If the exchange fails, the error is set on the `AccessContext` — no exceptions are thrown. Always check `access_context.has_errors()` before using tokens. -def main(): - """Entry point for the MCP server.""" - mcp.run(transport="streamable-http") -``` +### Key difference from standard MCP -### Step 4: Run Your Server +In `keycardai-mcp-fastmcp`, `AccessContext` is **retrieved from context state**: -```bash -uv run my-mcp-server +``` +def my_tool(ctx: Context) -> dict: + access_context = ctx.get_state("keycardai") ``` -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: +In `keycardai-mcp`, `AccessContext` is a **function parameter** injected by `@grant`: -```bash -export KEYCARD_CLIENT_ID="your-client-id" -export KEYCARD_CLIENT_SECRET="your-client-secret" ``` - -### Step 2: Add Delegated Access - -Update `src/my_mcp_server/__init__.py`: - -```python -import os -from fastmcp import FastMCP, Context -from keycardai.mcp.integrations.fastmcp import AuthProvider, AccessContext, ClientSecret -import httpx - -# Configure with client credentials for delegated access -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") - )) -) - -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}!" - -# 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.""" - access_context: AccessContext = ctx.get_state("keycardai") - - if access_context.has_errors(): - return {"error": f"Token exchange failed: {access_context.get_errors()}"} - - # Get delegated token for Google Calendar API - token = access_context.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}"}, - params={"maxResults": maxResults} - ) - response.raise_for_status() - return response.json() - -def main(): - """Entry point for the MCP server.""" - mcp.run(transport="streamable-http") +def my_tool(access_ctx: AccessContext, ctx: Context) -> dict: ``` -### Step 3: Run with Credentials +## Get started -```bash -export KEYCARD_CLIENT_ID="your-client-id" -export KEYCARD_CLIENT_SECRET="your-client-secret" -uv run my-mcp-server -``` +Runnable examples with full setup instructions: + +- **[Hello World Server](https://github.com/keycardai/python-sdk/tree/main/packages/mcp-fastmcp/examples/hello_world_server)** — Basic authenticated FastMCP server in ~50 lines +- **[Delegated Access](https://github.com/keycardai/python-sdk/tree/main/packages/mcp-fastmcp/examples/delegated_access)** — GitHub API integration with comprehensive error handling patterns + +## Reference +- [keycardai-mcp-fastmcp README](https://github.com/keycardai/python-sdk/tree/main/packages/mcp-fastmcp) — Full API docs, AccessContext usage, testing utilities, troubleshooting diff --git a/docs/examples/mcp-client-usage.mdx b/docs/examples/mcp-client-usage.mdx index fb76937..8a3598d 100644 --- a/docs/examples/mcp-client-usage.mdx +++ b/docs/examples/mcp-client-usage.mdx @@ -1,70 +1,30 @@ --- -title: "MCP Client Usage" -description: "How to use the Keycard MCP client to connect to authenticated MCP servers" +title: "MCP Client" +description: "How to connect to authenticated MCP servers using the Keycard MCP client" --- -# MCP Client Usage +# MCP Client -A step-by-step guide to connect to authenticated MCP servers using the Keycard MCP client. +The Keycard MCP client connects to authenticated MCP servers with built-in OAuth support, multi-server management, and AI agent integrations. -## Quick Start +## What the client handles -### Step 1: Create Project +- **OAuth 2.0 flows** — Automatic browser-based authentication when connecting to protected servers +- **Multi-server connections** — Connect to multiple MCP servers with independent session and status tracking +- **Graceful failures** — Connection issues are reported via session status, not exceptions. Your app stays running even if one server is down. +- **AI agent integrations** — Pre-built integrations with LangChain and OpenAI Agents SDK -```bash -uv init --package my-mcp-client -cd my-mcp-client -``` +## How it works -### Step 2: Install Dependencies +The `Client` accepts a dictionary of server configurations. Each server specifies a URL, transport type, and auth method. When you enter the async context (`async with Client(servers) as client:`), the client: -```bash -uv add keycardai-mcp -``` +1. Opens your browser for OAuth authentication (if needed) +2. Establishes connections to all configured servers +3. Tracks each server's session status independently +4. Provides `list_tools()` and `call_tool()` across all connected servers -### Step 3: Create Your Client +## Get started -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). +For complete documentation, configuration examples, and advanced usage patterns (web applications, event-driven architectures, AI agent integrations): +- **[MCP Client README](https://github.com/keycardai/python-sdk/blob/main/packages/mcp/src/keycardai/mcp/client/README.md)** — Full client documentation diff --git a/docs/examples/mcp-server-auth.mdx b/docs/examples/mcp-server-auth.mdx index be8998c..e3de42e 100644 --- a/docs/examples/mcp-server-auth.mdx +++ b/docs/examples/mcp-server-auth.mdx @@ -1,142 +1,64 @@ --- title: "MCP Server Authentication" -description: "How to add Keycard authentication to your MCP server using the server AuthProvider" +description: "How Keycard authentication works with standard MCP servers" --- # MCP Server Authentication -A step-by-step guide to add Keycard authentication to any MCP server. +This guide explains how Keycard adds authentication and delegated access to MCP servers built with the official [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) (`keycardai-mcp` package). -## Quick Start +For the FastMCP framework, see the [FastMCP Integration guide](fastmcp-integration). -### Step 1: Create Project +## When to use this package -```bash -uv init --package my-secure-mcp -cd my-secure-mcp -``` +Use `keycardai-mcp` when your server imports from `mcp.server.fastmcp` (the MCP SDK's built-in FastMCP class). This is different from the standalone `fastmcp` framework — see the [README](https://github.com/keycardai/python-sdk#which-package) for a comparison. -### Step 2: Install Dependencies +## How authentication works -```bash -uv add keycardai-mcp fastmcp uvicorn -``` +`AuthProvider` wraps your MCP server with OAuth 2.0 middleware. You create your `FastMCP` server as usual, then call `auth_provider.app(mcp)` to get a Starlette application with authentication baked in. The resulting app: -### Step 3: Create Your Server +- Serves OAuth metadata at `/.well-known/oauth-protected-resource` +- Validates bearer tokens on every MCP request +- Optionally handles dynamic client registration +- Runs with any ASGI server (uvicorn, etc.) -Create `src/my_secure_mcp/__init__.py`: +## How delegated access works -```python -from mcp.server.fastmcp import FastMCP -from keycardai.mcp.server.auth import AuthProvider -import uvicorn +Delegated access lets your MCP tools call external APIs (Google Calendar, GitHub, Slack, etc.) on behalf of authenticated users. -# Create your MCP server -mcp = FastMCP("My Secure MCP Server") +The `@grant` decorator handles token exchange automatically. When a user calls a granted tool: -@mcp.tool() -def my_protected_tool(data: str) -> str: - """A tool that requires authentication.""" - return f"Processed: {data}" +1. Keycard exchanges the user's token for a scoped token targeting the external API +2. The exchanged token is injected into an `AccessContext` object +3. Your function receives `AccessContext` as a parameter, alongside `Context` +4. You extract the token and call the external API -# Add Keycard authentication -auth_provider = AuthProvider( - zone_id="your-zone-id", # Get this from console.keycard.ai - mcp_server_name="My Secure MCP Server" -) +Both `AccessContext` and `Context` are **required** function parameters when using `@grant` with this package. `AccessContext` carries the exchanged tokens; `Context` provides the MCP request state that the decorator needs to perform the exchange. -# Create authenticated app -app = auth_provider.app(mcp) +If the exchange fails, the error is set on the `AccessContext` — no exceptions are thrown. Always check `access_ctx.has_errors()` before using tokens. -def main(): - """Entry point for the MCP server.""" - uvicorn.run(app, host="0.0.0.0", port=8000) -``` +### Key difference from FastMCP integration -### Step 4: Run Your Server +In `keycardai-mcp`, `AccessContext` is a **function parameter** injected by `@grant`: -```bash -uv run my-secure-mcp +``` +def my_tool(access_ctx: AccessContext, ctx: Context) -> dict: ``` -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: +In `keycardai-mcp-fastmcp`, `AccessContext` is **retrieved from context state**: -```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) +def my_tool(ctx: Context) -> dict: + access_context = ctx.get_state("keycardai") ``` -### Step 3: Run with Credentials +## Get started -```bash -export KEYCARD_CLIENT_ID="your-client-id" -export KEYCARD_CLIENT_SECRET="your-client-secret" -uv run my-secure-mcp -``` +Runnable examples with full setup instructions: + +- **[Hello World Server](https://github.com/keycardai/python-sdk/tree/main/packages/mcp/examples/hello_world_server)** — Basic authenticated MCP server in ~50 lines +- **[Delegated Access](https://github.com/keycardai/python-sdk/tree/main/packages/mcp/examples/delegated_access)** — GitHub API integration with comprehensive error handling patterns -For more details on configuration options and credential types, see the [MCP package documentation](https://github.com/keycardai/python-sdk/tree/main/packages/mcp). +## Reference +- [keycardai-mcp README](https://github.com/keycardai/python-sdk/tree/main/packages/mcp) — Full API docs, zone configuration, credential types, lowlevel integration diff --git a/docs/welcome.mdx b/docs/welcome.mdx index 787840d..5133eea 100644 --- a/docs/welcome.mdx +++ b/docs/welcome.mdx @@ -1,27 +1,23 @@ --- title: "Welcome to Keycard Python SDK" -description: "Python SDK for Keycard OAuth 2.0 and Model Context Protocol (MCP) integration" +description: "Python SDK for Keycard — OAuth, identity, and access for MCP servers" --- # Keycard Python SDK -The Python SDK for Keycard OAuth 2.0 and Model Context Protocol (MCP) integration. +**Keycard handles OAuth, identity, and access so your MCP servers don't have to.** Add authentication, authorization, and delegated API access to any MCP server with a few lines of Python. -## Packages - -This SDK is organized into separate packages: - -- **keycardai-oauth**: Core OAuth 2.0 client functionality -- **keycardai-mcp**: Model Context Protocol base functionality -- **keycardai-mcp-fastmcp**: FastMCP-specific integration with automated token exchange +- **Drop-in auth** for MCP servers (OAuth 2.0, PKCE, token exchange — handled for you) +- **Delegated access** — call Google, GitHub, Slack APIs on behalf of your users with automatic token exchange +- **Works with both** the MCP Python SDK and the FastMCP framework -## Getting Started - -Choose the package that best fits your needs: +## Packages -- For OAuth 2.0 operations: `keycardai-oauth` -- For MCP server functionality: `keycardai-mcp` -- For FastMCP integration: `keycardai-mcp-fastmcp` +| You want... | Install | Guide | +|---|---|---| +| Auth for MCP servers (using the `mcp` SDK) | `pip install keycardai-mcp` | [MCP Server Auth](examples/mcp-server-auth) | +| Auth for FastMCP servers | `pip install keycardai-mcp-fastmcp` | [FastMCP Integration](examples/fastmcp-integration) | +| OAuth 2.0 client only | `pip install keycardai-oauth` | [OAuth package docs](https://github.com/keycardai/python-sdk/tree/main/packages/oauth) | ## Quick Start @@ -75,7 +71,7 @@ def main(): uv run my-mcp-server ``` -🎉 Your authenticated MCP server is now running on `http://localhost:8000`! +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. diff --git a/packages/mcp-fastmcp/tests/test_examples.py b/packages/mcp-fastmcp/tests/test_examples.py new file mode 100644 index 0000000..c3bd20c --- /dev/null +++ b/packages/mcp-fastmcp/tests/test_examples.py @@ -0,0 +1,80 @@ +"""Smoke tests for package examples. + +Verifies that each example in packages/mcp-fastmcp/examples/ imports cleanly +and exposes the expected objects. The @grant decorator validates function +signatures at import time, so a successful import confirms the example +is compatible with the current SDK API. +""" + +import importlib.util +import sys +from pathlib import Path +from unittest.mock import Mock, patch + +EXAMPLES_DIR = Path(__file__).parent.parent / "examples" + +# Patch target: where AuthProvider.__init__ looks up DefaultClientFactory +_FACTORY_PATCH_TARGET = "keycardai.mcp.integrations.fastmcp.provider.DefaultClientFactory" + + +def _mock_client_factory(): + """Create a mock DefaultClientFactory that skips real HTTP calls. + + AuthProvider.__init__ calls _discover_jwks_uri which hits the zone URL. + This mock returns fake metadata so examples can be imported without + network access. + """ + factory = Mock() + mock_metadata = Mock() + mock_metadata.jwks_uri = "https://test.keycard.cloud/.well-known/jwks.json" + + mock_client = Mock() + mock_client.discover_server_metadata.return_value = mock_metadata + + mock_async_client = Mock() + mock_async_client.config = Mock() + mock_async_client.config.client_id = "test_client_id" + + factory.return_value.create_client.return_value = mock_client + factory.return_value.create_async_client.return_value = mock_async_client + return factory + + +def _load_example(example_name: str): + """Load an example module by name from the examples directory.""" + module_path = EXAMPLES_DIR / example_name / "main.py" + module_name = f"example_{example_name.replace('/', '_')}" + + # Remove from sys.modules if previously loaded to get a clean import + sys.modules.pop(module_name, None) + + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_hello_world_example_loads(): + """The hello_world_server example imports and exposes auth_provider, auth, and mcp.""" + with patch(_FACTORY_PATCH_TARGET, _mock_client_factory()): + mod = _load_example("hello_world_server") + assert hasattr(mod, "auth_provider") + assert hasattr(mod, "auth") + assert hasattr(mod, "mcp") + assert hasattr(mod, "main") + assert callable(mod.main) + + +def test_delegated_access_example_loads(): + """The delegated_access example imports and exposes auth_provider, auth, mcp, and tool functions.""" + with patch(_FACTORY_PATCH_TARGET, _mock_client_factory()): + mod = _load_example("delegated_access") + assert hasattr(mod, "auth_provider") + assert hasattr(mod, "auth") + assert hasattr(mod, "mcp") + # FastMCP's @mcp.tool() wraps functions in FunctionTool objects (not callable), + # but their existence confirms @grant validated the signatures at import time. + assert hasattr(mod, "get_github_user") + assert hasattr(mod, "list_github_repos") + assert hasattr(mod, "main") + assert callable(mod.main) diff --git a/packages/mcp/README.md b/packages/mcp/README.md index b22d0b4..8c02d52 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -75,13 +75,13 @@ def my_protected_tool(data: str) -> str: return f"Processed: {data}" # Add Keycard authentication -access = AuthProvider( +auth_provider = AuthProvider( zone_id="your_zone_id_here", mcp_server_name="My Secure MCP Server", ) # Create authenticated app -app = access.app(mcp) +app = auth_provider.app(mcp) ``` ### Run with Authentication @@ -357,7 +357,7 @@ from keycardai.mcp.server.auth import AuthProvider, AccessContext, ClientSecret import os # Configure your provider with client credentials -access = AuthProvider( +auth_provider = AuthProvider( zone_id="your_zone_id", mcp_server_name="My MCP Server", application_credential=ClientSecret(( @@ -369,14 +369,14 @@ access = AuthProvider( mcp = FastMCP("My MCP Server") @mcp.tool() -@access.grant("https://protected-api") +@auth_provider.grant("https://protected-api") def protected_tool(ctx: Context, access_context: AccessContext, name: str) -> str: # Use the access_context to call external APIs on behalf of the user token = access_context.access("https://protected-api").access_token # Make authenticated API calls... return f"Protected data for {name}" -app = access.app(mcp) +app = auth_provider.app(mcp) ``` ### Lowlevel Integration @@ -661,7 +661,7 @@ middleware = [ ) ] -app = access.app(mcp, middleware=middleware) +app = auth_provider.app(mcp, middleware=middleware) ``` **Important Security Note:** The configuration above uses permissive CORS settings (`allow_origins=["*"]`) which should **only be used for local development and testing**. In production environments, you should restrict `allow_origins` to specific domains that need access to your MCP server. diff --git a/packages/mcp/examples/delegated_access/main.py b/packages/mcp/examples/delegated_access/main.py index 9c9c2ad..68680e8 100644 --- a/packages/mcp/examples/delegated_access/main.py +++ b/packages/mcp/examples/delegated_access/main.py @@ -6,13 +6,14 @@ Key differences from FastMCP integration: - AccessContext is passed as a function parameter (not retrieved from ctx.get_state()) +- Both AccessContext and Context parameters are required by @grant - Server startup uses uvicorn.run(auth_provider.app(mcp)) - More control over the Starlette application Key concepts demonstrated: - AuthProvider setup with ClientSecret credentials - @grant decorator for requesting token exchange -- AccessContext as function parameter for accessing exchanged tokens +- AccessContext and Context as function parameters - Comprehensive error handling patterns """ @@ -20,7 +21,7 @@ import httpx import uvicorn -from mcp.server.fastmcp import FastMCP +from mcp.server.fastmcp import Context, FastMCP from keycardai.mcp.server.auth import AccessContext, AuthProvider, ClientSecret @@ -45,7 +46,7 @@ @mcp.tool() @auth_provider.grant("https://api.github.com") -async def get_github_user(access_ctx: AccessContext) -> dict: +async def get_github_user(access_ctx: AccessContext, ctx: Context) -> dict: """Get the authenticated GitHub user's profile. Demonstrates: @@ -55,6 +56,7 @@ async def get_github_user(access_ctx: AccessContext) -> dict: Args: access_ctx: AccessContext with exchanged tokens (injected by @grant) + ctx: MCP Context providing request state (required by @grant) Returns: User profile data or error details @@ -95,7 +97,7 @@ async def get_github_user(access_ctx: AccessContext) -> dict: @mcp.tool() @auth_provider.grant("https://api.github.com") -async def list_github_repos(access_ctx: AccessContext, per_page: int = 5) -> dict: +async def list_github_repos(access_ctx: AccessContext, ctx: Context, per_page: int = 5) -> dict: """List the authenticated user's GitHub repositories. Demonstrates: @@ -105,6 +107,7 @@ async def list_github_repos(access_ctx: AccessContext, per_page: int = 5) -> dic Args: access_ctx: AccessContext with exchanged tokens (injected by @grant) + ctx: MCP Context providing request state (required by @grant) per_page: Number of repositories to return (default: 5) Returns: diff --git a/packages/mcp/src/keycardai/mcp/server/auth/provider.py b/packages/mcp/src/keycardai/mcp/server/auth/provider.py index f4fe975..807b459 100644 --- a/packages/mcp/src/keycardai/mcp/server/auth/provider.py +++ b/packages/mcp/src/keycardai/mcp/server/auth/provider.py @@ -185,7 +185,7 @@ class AuthProvider: ) @provider.grant("https://api.example.com") - async def my_tool(ctx, access_ctx: AccessContext = None): + async def my_tool(access_ctx: AccessContext, ctx: Context): token = access_ctx.access("https://api.example.com").access_token # Use token to call API ``` diff --git a/packages/mcp/tests/test_examples.py b/packages/mcp/tests/test_examples.py new file mode 100644 index 0000000..f51d06b --- /dev/null +++ b/packages/mcp/tests/test_examples.py @@ -0,0 +1,49 @@ +"""Smoke tests for package examples. + +Verifies that each example in packages/mcp/examples/ imports cleanly +and exposes the expected objects. The @grant decorator validates function +signatures at import time, so a successful import confirms the example +is compatible with the current SDK API. +""" + +import importlib.util +import sys +from pathlib import Path + +EXAMPLES_DIR = Path(__file__).parent.parent / "examples" + + +def _load_example(example_name: str): + """Load an example module by name from the examples directory.""" + module_path = EXAMPLES_DIR / example_name / "main.py" + module_name = f"example_{example_name.replace('/', '_')}" + + # Remove from sys.modules if previously loaded to get a clean import + sys.modules.pop(module_name, None) + + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_hello_world_example_loads(): + """The hello_world_server example imports and exposes auth_provider and mcp.""" + mod = _load_example("hello_world_server") + assert hasattr(mod, "auth_provider") + assert hasattr(mod, "mcp") + assert hasattr(mod, "main") + assert callable(mod.main) + + +def test_delegated_access_example_loads(): + """The delegated_access example imports and exposes auth_provider, mcp, and tool functions.""" + mod = _load_example("delegated_access") + assert hasattr(mod, "auth_provider") + assert hasattr(mod, "mcp") + assert hasattr(mod, "get_github_user") + assert hasattr(mod, "list_github_repos") + assert callable(mod.get_github_user) + assert callable(mod.list_github_repos) + assert hasattr(mod, "main") + assert callable(mod.main) diff --git a/packages/oauth/examples/discover_server_metadata/README.md b/packages/oauth/examples/discover_server_metadata/README.md index e69de29..dd55954 100644 --- a/packages/oauth/examples/discover_server_metadata/README.md +++ b/packages/oauth/examples/discover_server_metadata/README.md @@ -0,0 +1,32 @@ +# Discover Server Metadata Example + +Demonstrates OAuth 2.0 Authorization Server Metadata discovery (RFC 8414) using the Keycard OAuth client. + +## What it does + +Connects to a Keycard zone and retrieves its OAuth server metadata, which includes: +- Authorization endpoint +- Token endpoint +- Supported grant types, scopes, and response types +- Registration endpoint (if dynamic client registration is enabled) + +## Usage + +### Environment variable: +```bash +export ZONE_URL=http://kq0sohre3tpcywxjnog16iipay.localdev.keycard.sh +uv run python main.py +``` + +### Install and run: +```bash +uv sync +export ZONE_URL=http://kq0sohre3tpcywxjnog16iipay.localdev.keycard.sh +uv run python main.py +``` + +## Requirements + +- Python 3.10+ +- keycardai-oauth package +- Access to a Keycard authorization server diff --git a/packages/oauth/tests/test_examples.py b/packages/oauth/tests/test_examples.py new file mode 100644 index 0000000..ebdbc77 --- /dev/null +++ b/packages/oauth/tests/test_examples.py @@ -0,0 +1,40 @@ +"""Smoke tests for package examples. + +Verifies that each example in packages/oauth/examples/ imports cleanly +and exposes the expected objects. A successful import confirms the example +is compatible with the current SDK API. +""" + +import importlib.util +import sys +from pathlib import Path + +EXAMPLES_DIR = Path(__file__).parent.parent / "examples" + + +def _load_example(example_name: str): + """Load an example module by name from the examples directory.""" + module_path = EXAMPLES_DIR / example_name / "main.py" + module_name = f"example_{example_name.replace('/', '_')}" + + # Remove from sys.modules if previously loaded to get a clean import + sys.modules.pop(module_name, None) + + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_discover_server_metadata_example_loads(): + """The discover_server_metadata example imports and exposes a main function.""" + mod = _load_example("discover_server_metadata") + assert hasattr(mod, "main") + assert callable(mod.main) + + +def test_dynamic_client_registration_example_loads(): + """The dynamic_client_registration example imports and exposes a main function.""" + mod = _load_example("dynamic_client_registration") + assert hasattr(mod, "main") + assert callable(mod.main)