keycardai-mcp


Namekeycardai-mcp JSON
Version 0.9.0 PyPI version JSON
download
home_pageNone
SummaryA Python SDK for Model Context Protocol (MCP) functionality with simplified authentication and authorization
upload_time2025-10-20 13:37:01
maintainerNone
docs_urlNone
authorNone
requires_python>=3.10
licenseMIT
keywords ai authentication authorization llm mcp model-context-protocol
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Keycard MCP SDK

A comprehensive Python SDK for Model Context Protocol (MCP) functionality that simplifies authentication and authorization concerns for developers working with AI/LLM integrations.

## Requirements

- **Python 3.9 or greater**
- Virtual environment (recommended)

## Setup Guide

### Option 1: Using uv (Recommended)

If you have [uv](https://docs.astral.sh/uv/) installed:

```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
```

### Option 2: Using Standard Python

```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
```

## Installation

```bash
pip install keycardai-mcp
```

## Quick Start

Add Keycard authentication to your existing MCP server:

### Install the Package

```bash
pip install keycardai-mcp
```

### 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

# Your existing MCP server
mcp = FastMCP("My Secure MCP Server")

@mcp.tool()
def my_protected_tool(data: str) -> str:
    return f"Processed: {data}"

# Add Keycard authentication
access = AuthProvider(
    zone_id="your_zone_id_here",
    mcp_server_name="My Secure MCP Server",
)

# Create authenticated app
app = access.app(mcp)
```

### Run with Authentication

```bash
pip install uvicorn
uvicorn server:app
```

### 🎉 Your MCP server is now protected with Keycard authentication! 🎉

## Features

- ✅ **OAuth 2.0 Authentication**: Secure your MCP server with industry-standard OAuth flows
- ✅ **Easy Integration**: Add authentication with just a few lines of code
- ✅ **Multi-Zone Support**: Support multiple Keycard zones in one application
- ✅ **Token Exchange**: Automatic delegated token exchange for accessing external APIs
- ✅ **Production Ready**: Battle-tested security patterns and error handling

### Delegated Access

Keycard allows MCP servers to access other resources on behalf of users with automatic consent and secure token exchange.

#### Setup Protected Resources

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** to provide authentication method

#### Add Delegation to Your Tools

```python
from mcp.server.fastmcp import FastMCP, Context
from keycardai.mcp.server.auth import AuthProvider, AccessContext, ClientSecret
import os

# Configure your provider with client credentials
access = AuthProvider(
    zone_id="your_zone_id",
    mcp_server_name="My MCP Server",
    application_credential=ClientSecret((
        os.getenv("KEYCARD_CLIENT_ID"),
        os.getenv("KEYCARD_CLIENT_SECRET")
    ))
)

mcp = FastMCP("My MCP Server")

@mcp.tool()
@access.grant("https://protected-api")
def protected_tool(ctx: Context, access_context: AccessContext, name: str) -> str:
    # Use the access_context to call external APIs on behalf of the user
    token = access_context.access("https://protected-api").access_token
    # Make authenticated API calls...
    return f"Protected data for {name}"

app = access.app(mcp)
```

### Lowlevel Integration

For advanced use cases requiring direct control over the MCP server lifecycle, you can integrate Keycard with the lowlevel MCP server API.

#### Requirements

When using lowlevel integration with Keycard:

1. **Function Parameters**: Functions decorated with `@auth.grant()` must accept `RequestContext` and `AccessContext` parameters:
   ```python
   @auth.grant("https://protected-api")
   def echo_handler(arguments: dict[str, Any], ctx: RequestContext, access_context: AccessContext) -> list[TextContent]:
       # Your implementation
   ```

2. **RequestContext Responsibility**: Unlike FastMCP which automatically injects the context, lowlevel servers require you to manually pass the `RequestContext` from `server.request_context` to your handler functions.
    ```python
    @server.call_tool()
    async def handle_call_tool(name: str, arguments: dict[str, Any] | None = None) -> list[TextContent]:
        # Pass server.request_context to the tool call. 
        return await echo_handler(arguments, server.request_context)
    ```

3. **ASGI Integration**: Use `auth.get_mcp_router()` to create authenticated routes that wrap your MCP transport or session manager.

#### Option 1: Using StreamableHTTPServerTransport

```python
import uvicorn
import asyncio
from contextlib import asynccontextmanager
from starlette.applications import Starlette
from starlette.types import Scope, Receive, Send
from mcp.server.lowlevel import Server
from mcp.shared.context import RequestContext
from mcp.types import Tool, TextContent
from mcp.server.streamable_http import StreamableHTTPServerTransport
from typing import Any

from keycardai.mcp.server.auth import AuthProvider, AccessContext

# Configure Keycard authentication
auth = AuthProvider(
    zone_id="your_zone_id",
    mcp_server_name="lowlevel-mcp",
    enable_multi_zone=True,
)

class StreamableHTTPASGIApp:
    def __init__(self, transport: StreamableHTTPServerTransport):
        self.transport = transport

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        await self.transport.handle_request(scope, receive, send)

# Create MCP server and transport
server = Server("lowlevel-mcp")
transport = StreamableHTTPServerTransport()

# Define protected tool with delegated access
@auth.grant("https://protected-api")
def echo_handler(arguments: dict[str, Any], ctx: RequestContext, access_context: AccessContext) -> list[TextContent]:
    if access_context.has_errors():
        return [TextContent(type="text", text=f"Error: {access_context.get_errors()}")]
    
    # Access external API with delegated token
    token = access_context.access("https://protected-api").access_token
    return [TextContent(type="text", text=f"Echo: {arguments['message']}")]

# Register tools
@server.list_tools()
async def handle_list_tools() -> list[Tool]:
    return [Tool(
        name="echo",
        description="Echo a message with protected access",
        inputSchema={
            "type": "object",
            "properties": {"message": {"type": "string"}},
            "required": ["message"]
        }
    )]

@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any] | None = None) -> list[TextContent]:
    if name == "echo":
        # Pass RequestContext from server to decorated handler
        return await echo_handler(arguments, server.request_context)
    raise Exception(f"Unknown tool: {name}")

@asynccontextmanager
async def lifespan(app):
    async with transport.connect() as (read_stream, write_stream):
        server_task = asyncio.create_task(server.run(
            read_stream, write_stream, server.create_initialization_options()
        ))
        try:
            yield
        finally:
            server_task.cancel()

# Create authenticated ASGI app
app = Starlette(
    routes=auth.get_mcp_router(StreamableHTTPASGIApp(transport)),
    lifespan=lifespan,
)
```

#### Option 2: Using StreamableHTTPSessionManager

```python
import uvicorn
import asyncio
from starlette.applications import Starlette
from starlette.types import Scope, Receive, Send
from mcp.server.lowlevel import Server
from mcp.shared.context import RequestContext
from mcp.types import Tool, TextContent
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from typing import Any

from keycardai.mcp.server.auth import AuthProvider, AccessContext

# Configure Keycard authentication
auth = AuthProvider(
    zone_id="your_zone_id",
    mcp_server_name="lowlevel-mcp",
    enable_multi_zone=True,
)

class StreamableHTTPASGIApp:
    def __init__(self, session_manager: StreamableHTTPSessionManager):
        self.session_manager = session_manager

    async def __call__(self, scope: Scope, receive: Send, send: Send) -> None:
        await self.session_manager.handle_request(scope, receive, send)

# Create MCP server and session manager
server = Server("lowlevel-mcp")
session_manager = StreamableHTTPSessionManager(
    app=server,
    stateless=True,
)

# Define protected tool with delegated access
@auth.grant("https://protected-api")
def echo_handler(arguments: dict[str, Any], ctx: RequestContext, access_context: AccessContext) -> list[TextContent]:
    if access_context.has_errors():
        return [TextContent(type="text", text=f"Error: {access_context.get_errors()}")]
    
    # Access external API with delegated token
    token = access_context.access("https://protected-api").access_token
    return [TextContent(type="text", text=f"Echo: {arguments['message']}")]

# Register tools
@server.list_tools()
async def handle_list_tools() -> list[Tool]:
    return [Tool(
        name="echo",
        description="Echo a message with protected access",
        inputSchema={
            "type": "object",
            "properties": {"message": {"type": "string"}},
            "required": ["message"]
        }
    )]

@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any] | None = None) -> list[TextContent]:
    if name == "echo":
        # Pass RequestContext from server to decorated handler
        return await echo_handler(arguments, server.request_context)
    raise Exception(f"Unknown tool: {name}")

# Create authenticated ASGI app
app = Starlette(
    routes=auth.get_mcp_router(StreamableHTTPASGIApp(session_manager)),
    lifespan=lambda app: session_manager.run(),
)

# Run the server
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

Both approaches provide full control over the MCP server lifecycle while maintaining Keycard's authentication and delegated access capabilities.

### Error Handling

The Keycard MCP package implements a robust error handling system that allows functions to continue execution even when delegation processes fail. This is achieved through the `AccessContext` object, which manages both successful tokens and error states without raising exceptions.

#### How AccessContext Manages Errors

The `AccessContext` serves as a centralized error management system during the OAuth token delegation process:

**Error Types:**
- **Global Errors**: Affect all resources (e.g., missing authentication, configuration issues)
- **Resource-Specific Errors**: Affect individual resources during token exchange

The `@grant` decorator automatically handles all error scenarios and populates the `AccessContext` with appropriate error information, ensuring your functions can always execute and handle errors gracefully.

#### Error Scenarios

The `@grant` decorator handles multiple error scenarios automatically:

- **Authentication Errors**: Missing or invalid authentication tokens
- **Configuration Errors**: Server misconfiguration or missing zone information  
- **Token Exchange Errors**: Failures when exchanging tokens for specific resources

All errors include descriptive messages to help with debugging and user-friendly error handling.

#### Usage Patterns

**Basic Error Checking:**
```python
@provider.grant("https://api.example.com")
def my_tool(access_ctx: AccessContext, ctx: Context, user_id: str):
    # Check for any errors first
    if access_ctx.has_errors():
        error_info = access_ctx.get_errors()
        return {"error": "Token delegation failed", "details": error_info}
    
    # Proceed with successful token
    token = access_ctx.access("https://api.example.com").access_token
    return call_external_api(token, user_id)
```

**Partial Success Handling:**
```python
@provider.grant(["https://api1.com", "https://api2.com"])
def multi_resource_tool(access_ctx: AccessContext, ctx: Context):
    results = {}
    
    # Handle successful resources
    for resource in access_ctx.get_successful_resources():
        token = access_ctx.access(resource).access_token
        results[resource] = call_api(resource, token)
    
    # Handle failed resources
    for resource in access_ctx.get_failed_resources():
        error = access_ctx.get_resource_errors(resource)
        results[resource] = {"error": error["error"]}
    
    return results
```

**Status-Based Handling:**
```python
@provider.grant("https://api.example.com")
def status_aware_tool(access_ctx: AccessContext, ctx: Context):
    status = access_ctx.get_status()  # "success", "partial_error", or "error"
    
    if status == "error":
        return {"status": "failed", "reason": access_ctx.get_error()}
    elif status == "partial_error":
        return {"status": "partial", "details": access_ctx.get_errors()}
    else:
        token = access_ctx.access("https://api.example.com").access_token
        return {"status": "success", "data": call_api(token)}
```

## FAQ

### How to test the MCP server with modelcontexprotocol/inspector?

When testing your MCP server with the [modelcontexprotocol/inspector](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.

You can use Starlette's built-in `CORSMiddleware` to configure CORS settings:

```python
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware

middleware = [
    Middleware(
        CORSMiddleware,
        allow_origins=["*"],  # Allow all origins for testing
        allow_credentials=True,
        allow_methods=["*"],  # Allow all HTTP methods
        allow_headers=["*"],  # Allow all headers
    )
]

app = access.app(mcp, middleware=middleware)
```

**Important Security Note:** The configuration above uses permissive CORS settings (`allow_origins=["*"]`) which should **only be used for local development and testing**. In production environments, you should restrict `allow_origins` to specific domains that need access to your MCP server.

For production use, consider more restrictive settings:

```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
    )
]
```

## Examples

For complete examples and advanced usage patterns, see our [documentation](https://docs.keycard.ai).

## License

MIT License - see [LICENSE](https://github.com/keycardai/python-sdk/blob/main/LICENSE) file for details.

## Support

- 📖 [Documentation](https://docs.keycard.ai)
- 🐛 [Issue Tracker](https://github.com/keycardai/python-sdk/issues)
- 📧 [Support Email](mailto:support@keycard.ai)

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "keycardai-mcp",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "ai, authentication, authorization, llm, mcp, model-context-protocol",
    "author": null,
    "author_email": "Keycard <support@keycard.ai>",
    "download_url": "https://files.pythonhosted.org/packages/ca/10/5fbc9bd0b078e8930be3e2e67d395e6395eb30b261befea9ed68d8f5f8c1/keycardai_mcp-0.9.0.tar.gz",
    "platform": null,
    "description": "# Keycard MCP SDK\n\nA comprehensive Python SDK for Model Context Protocol (MCP) functionality that simplifies authentication and authorization concerns for developers working with AI/LLM integrations.\n\n## Requirements\n\n- **Python 3.9 or greater**\n- Virtual environment (recommended)\n\n## Setup Guide\n\n### Option 1: Using uv (Recommended)\n\nIf you have [uv](https://docs.astral.sh/uv/) installed:\n\n```bash\n# Create a new project with uv\nuv init my-mcp-project\ncd my-mcp-project\n\n# Create and activate virtual environment\nuv venv\nsource .venv/bin/activate  # On Windows: .venv\\Scripts\\activate\n```\n\n### Option 2: Using Standard Python\n\n```bash\n# Create project directory\nmkdir my-mcp-project\ncd my-mcp-project\n\n# Create and activate virtual environment\npython3 -m venv .venv\nsource .venv/bin/activate  # On Windows: .venv\\Scripts\\activate\n\n# Upgrade pip (recommended)\npip install --upgrade pip\n```\n\n## Installation\n\n```bash\npip install keycardai-mcp\n```\n\n## Quick Start\n\nAdd Keycard authentication to your existing MCP server:\n\n### Install the Package\n\n```bash\npip install keycardai-mcp\n```\n\n### Get Your Keycard Zone ID\n\n1. Sign up at [keycard.ai](https://keycard.ai)\n2. Navigate to Zone Settings to get your zone ID\n3. Configure your preferred identity provider (Google, Microsoft, etc.)\n4. Create an MCP resource in your zone\n\n### Add Authentication to Your MCP Server\n\n```python\nfrom mcp.server.fastmcp import FastMCP\nfrom keycardai.mcp.server.auth import AuthProvider\n\n# Your existing MCP server\nmcp = FastMCP(\"My Secure MCP Server\")\n\n@mcp.tool()\ndef my_protected_tool(data: str) -> str:\n    return f\"Processed: {data}\"\n\n# Add Keycard authentication\naccess = AuthProvider(\n    zone_id=\"your_zone_id_here\",\n    mcp_server_name=\"My Secure MCP Server\",\n)\n\n# Create authenticated app\napp = access.app(mcp)\n```\n\n### Run with Authentication\n\n```bash\npip install uvicorn\nuvicorn server:app\n```\n\n### \ud83c\udf89 Your MCP server is now protected with Keycard authentication! \ud83c\udf89\n\n## Features\n\n- \u2705 **OAuth 2.0 Authentication**: Secure your MCP server with industry-standard OAuth flows\n- \u2705 **Easy Integration**: Add authentication with just a few lines of code\n- \u2705 **Multi-Zone Support**: Support multiple Keycard zones in one application\n- \u2705 **Token Exchange**: Automatic delegated token exchange for accessing external APIs\n- \u2705 **Production Ready**: Battle-tested security patterns and error handling\n\n### Delegated Access\n\nKeycard allows MCP servers to access other resources on behalf of users with automatic consent and secure token exchange.\n\n#### Setup Protected Resources\n\n1. **Configure credential provider** (e.g., Google Workspace)\n2. **Configure protected resource** (e.g., Google Drive API)  \n3. **Set MCP server dependencies** to allow delegated access\n4. **Create client secret identity** to provide authentication method\n\n#### Add Delegation to Your Tools\n\n```python\nfrom mcp.server.fastmcp import FastMCP, Context\nfrom keycardai.mcp.server.auth import AuthProvider, AccessContext, ClientSecret\nimport os\n\n# Configure your provider with client credentials\naccess = AuthProvider(\n    zone_id=\"your_zone_id\",\n    mcp_server_name=\"My MCP Server\",\n    application_credential=ClientSecret((\n        os.getenv(\"KEYCARD_CLIENT_ID\"),\n        os.getenv(\"KEYCARD_CLIENT_SECRET\")\n    ))\n)\n\nmcp = FastMCP(\"My MCP Server\")\n\n@mcp.tool()\n@access.grant(\"https://protected-api\")\ndef protected_tool(ctx: Context, access_context: AccessContext, name: str) -> str:\n    # Use the access_context to call external APIs on behalf of the user\n    token = access_context.access(\"https://protected-api\").access_token\n    # Make authenticated API calls...\n    return f\"Protected data for {name}\"\n\napp = access.app(mcp)\n```\n\n### Lowlevel Integration\n\nFor advanced use cases requiring direct control over the MCP server lifecycle, you can integrate Keycard with the lowlevel MCP server API.\n\n#### Requirements\n\nWhen using lowlevel integration with Keycard:\n\n1. **Function Parameters**: Functions decorated with `@auth.grant()` must accept `RequestContext` and `AccessContext` parameters:\n   ```python\n   @auth.grant(\"https://protected-api\")\n   def echo_handler(arguments: dict[str, Any], ctx: RequestContext, access_context: AccessContext) -> list[TextContent]:\n       # Your implementation\n   ```\n\n2. **RequestContext Responsibility**: Unlike FastMCP which automatically injects the context, lowlevel servers require you to manually pass the `RequestContext` from `server.request_context` to your handler functions.\n    ```python\n    @server.call_tool()\n    async def handle_call_tool(name: str, arguments: dict[str, Any] | None = None) -> list[TextContent]:\n        # Pass server.request_context to the tool call. \n        return await echo_handler(arguments, server.request_context)\n    ```\n\n3. **ASGI Integration**: Use `auth.get_mcp_router()` to create authenticated routes that wrap your MCP transport or session manager.\n\n#### Option 1: Using StreamableHTTPServerTransport\n\n```python\nimport uvicorn\nimport asyncio\nfrom contextlib import asynccontextmanager\nfrom starlette.applications import Starlette\nfrom starlette.types import Scope, Receive, Send\nfrom mcp.server.lowlevel import Server\nfrom mcp.shared.context import RequestContext\nfrom mcp.types import Tool, TextContent\nfrom mcp.server.streamable_http import StreamableHTTPServerTransport\nfrom typing import Any\n\nfrom keycardai.mcp.server.auth import AuthProvider, AccessContext\n\n# Configure Keycard authentication\nauth = AuthProvider(\n    zone_id=\"your_zone_id\",\n    mcp_server_name=\"lowlevel-mcp\",\n    enable_multi_zone=True,\n)\n\nclass StreamableHTTPASGIApp:\n    def __init__(self, transport: StreamableHTTPServerTransport):\n        self.transport = transport\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n        await self.transport.handle_request(scope, receive, send)\n\n# Create MCP server and transport\nserver = Server(\"lowlevel-mcp\")\ntransport = StreamableHTTPServerTransport()\n\n# Define protected tool with delegated access\n@auth.grant(\"https://protected-api\")\ndef echo_handler(arguments: dict[str, Any], ctx: RequestContext, access_context: AccessContext) -> list[TextContent]:\n    if access_context.has_errors():\n        return [TextContent(type=\"text\", text=f\"Error: {access_context.get_errors()}\")]\n    \n    # Access external API with delegated token\n    token = access_context.access(\"https://protected-api\").access_token\n    return [TextContent(type=\"text\", text=f\"Echo: {arguments['message']}\")]\n\n# Register tools\n@server.list_tools()\nasync def handle_list_tools() -> list[Tool]:\n    return [Tool(\n        name=\"echo\",\n        description=\"Echo a message with protected access\",\n        inputSchema={\n            \"type\": \"object\",\n            \"properties\": {\"message\": {\"type\": \"string\"}},\n            \"required\": [\"message\"]\n        }\n    )]\n\n@server.call_tool()\nasync def handle_call_tool(name: str, arguments: dict[str, Any] | None = None) -> list[TextContent]:\n    if name == \"echo\":\n        # Pass RequestContext from server to decorated handler\n        return await echo_handler(arguments, server.request_context)\n    raise Exception(f\"Unknown tool: {name}\")\n\n@asynccontextmanager\nasync def lifespan(app):\n    async with transport.connect() as (read_stream, write_stream):\n        server_task = asyncio.create_task(server.run(\n            read_stream, write_stream, server.create_initialization_options()\n        ))\n        try:\n            yield\n        finally:\n            server_task.cancel()\n\n# Create authenticated ASGI app\napp = Starlette(\n    routes=auth.get_mcp_router(StreamableHTTPASGIApp(transport)),\n    lifespan=lifespan,\n)\n```\n\n#### Option 2: Using StreamableHTTPSessionManager\n\n```python\nimport uvicorn\nimport asyncio\nfrom starlette.applications import Starlette\nfrom starlette.types import Scope, Receive, Send\nfrom mcp.server.lowlevel import Server\nfrom mcp.shared.context import RequestContext\nfrom mcp.types import Tool, TextContent\nfrom mcp.server.streamable_http_manager import StreamableHTTPSessionManager\nfrom typing import Any\n\nfrom keycardai.mcp.server.auth import AuthProvider, AccessContext\n\n# Configure Keycard authentication\nauth = AuthProvider(\n    zone_id=\"your_zone_id\",\n    mcp_server_name=\"lowlevel-mcp\",\n    enable_multi_zone=True,\n)\n\nclass StreamableHTTPASGIApp:\n    def __init__(self, session_manager: StreamableHTTPSessionManager):\n        self.session_manager = session_manager\n\n    async def __call__(self, scope: Scope, receive: Send, send: Send) -> None:\n        await self.session_manager.handle_request(scope, receive, send)\n\n# Create MCP server and session manager\nserver = Server(\"lowlevel-mcp\")\nsession_manager = StreamableHTTPSessionManager(\n    app=server,\n    stateless=True,\n)\n\n# Define protected tool with delegated access\n@auth.grant(\"https://protected-api\")\ndef echo_handler(arguments: dict[str, Any], ctx: RequestContext, access_context: AccessContext) -> list[TextContent]:\n    if access_context.has_errors():\n        return [TextContent(type=\"text\", text=f\"Error: {access_context.get_errors()}\")]\n    \n    # Access external API with delegated token\n    token = access_context.access(\"https://protected-api\").access_token\n    return [TextContent(type=\"text\", text=f\"Echo: {arguments['message']}\")]\n\n# Register tools\n@server.list_tools()\nasync def handle_list_tools() -> list[Tool]:\n    return [Tool(\n        name=\"echo\",\n        description=\"Echo a message with protected access\",\n        inputSchema={\n            \"type\": \"object\",\n            \"properties\": {\"message\": {\"type\": \"string\"}},\n            \"required\": [\"message\"]\n        }\n    )]\n\n@server.call_tool()\nasync def handle_call_tool(name: str, arguments: dict[str, Any] | None = None) -> list[TextContent]:\n    if name == \"echo\":\n        # Pass RequestContext from server to decorated handler\n        return await echo_handler(arguments, server.request_context)\n    raise Exception(f\"Unknown tool: {name}\")\n\n# Create authenticated ASGI app\napp = Starlette(\n    routes=auth.get_mcp_router(StreamableHTTPASGIApp(session_manager)),\n    lifespan=lambda app: session_manager.run(),\n)\n\n# Run the server\nif __name__ == \"__main__\":\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n```\n\nBoth approaches provide full control over the MCP server lifecycle while maintaining Keycard's authentication and delegated access capabilities.\n\n### Error Handling\n\nThe Keycard MCP package implements a robust error handling system that allows functions to continue execution even when delegation processes fail. This is achieved through the `AccessContext` object, which manages both successful tokens and error states without raising exceptions.\n\n#### How AccessContext Manages Errors\n\nThe `AccessContext` serves as a centralized error management system during the OAuth token delegation process:\n\n**Error Types:**\n- **Global Errors**: Affect all resources (e.g., missing authentication, configuration issues)\n- **Resource-Specific Errors**: Affect individual resources during token exchange\n\nThe `@grant` decorator automatically handles all error scenarios and populates the `AccessContext` with appropriate error information, ensuring your functions can always execute and handle errors gracefully.\n\n#### Error Scenarios\n\nThe `@grant` decorator handles multiple error scenarios automatically:\n\n- **Authentication Errors**: Missing or invalid authentication tokens\n- **Configuration Errors**: Server misconfiguration or missing zone information  \n- **Token Exchange Errors**: Failures when exchanging tokens for specific resources\n\nAll errors include descriptive messages to help with debugging and user-friendly error handling.\n\n#### Usage Patterns\n\n**Basic Error Checking:**\n```python\n@provider.grant(\"https://api.example.com\")\ndef my_tool(access_ctx: AccessContext, ctx: Context, user_id: str):\n    # Check for any errors first\n    if access_ctx.has_errors():\n        error_info = access_ctx.get_errors()\n        return {\"error\": \"Token delegation failed\", \"details\": error_info}\n    \n    # Proceed with successful token\n    token = access_ctx.access(\"https://api.example.com\").access_token\n    return call_external_api(token, user_id)\n```\n\n**Partial Success Handling:**\n```python\n@provider.grant([\"https://api1.com\", \"https://api2.com\"])\ndef multi_resource_tool(access_ctx: AccessContext, ctx: Context):\n    results = {}\n    \n    # Handle successful resources\n    for resource in access_ctx.get_successful_resources():\n        token = access_ctx.access(resource).access_token\n        results[resource] = call_api(resource, token)\n    \n    # Handle failed resources\n    for resource in access_ctx.get_failed_resources():\n        error = access_ctx.get_resource_errors(resource)\n        results[resource] = {\"error\": error[\"error\"]}\n    \n    return results\n```\n\n**Status-Based Handling:**\n```python\n@provider.grant(\"https://api.example.com\")\ndef status_aware_tool(access_ctx: AccessContext, ctx: Context):\n    status = access_ctx.get_status()  # \"success\", \"partial_error\", or \"error\"\n    \n    if status == \"error\":\n        return {\"status\": \"failed\", \"reason\": access_ctx.get_error()}\n    elif status == \"partial_error\":\n        return {\"status\": \"partial\", \"details\": access_ctx.get_errors()}\n    else:\n        token = access_ctx.access(\"https://api.example.com\").access_token\n        return {\"status\": \"success\", \"data\": call_api(token)}\n```\n\n## FAQ\n\n### How to test the MCP server with modelcontexprotocol/inspector?\n\nWhen 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.\n\nYou can use Starlette's built-in `CORSMiddleware` to configure CORS settings:\n\n```python\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.cors import CORSMiddleware\n\nmiddleware = [\n    Middleware(\n        CORSMiddleware,\n        allow_origins=[\"*\"],  # Allow all origins for testing\n        allow_credentials=True,\n        allow_methods=[\"*\"],  # Allow all HTTP methods\n        allow_headers=[\"*\"],  # Allow all headers\n    )\n]\n\napp = access.app(mcp, middleware=middleware)\n```\n\n**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.\n\nFor production use, consider more restrictive settings:\n\n```python\nmiddleware = [\n    Middleware(\n        CORSMiddleware,\n        allow_origins=[\"https://yourdomain.com\"],  # Specific allowed origins\n        allow_credentials=True,\n        allow_methods=[\"GET\", \"POST\"],  # Only required methods\n        allow_headers=[\"Authorization\", \"Content-Type\"],  # Only required headers\n    )\n]\n```\n\n## Examples\n\nFor complete examples and advanced usage patterns, see our [documentation](https://docs.keycard.ai).\n\n## License\n\nMIT License - see [LICENSE](https://github.com/keycardai/python-sdk/blob/main/LICENSE) file for details.\n\n## Support\n\n- \ud83d\udcd6 [Documentation](https://docs.keycard.ai)\n- \ud83d\udc1b [Issue Tracker](https://github.com/keycardai/python-sdk/issues)\n- \ud83d\udce7 [Support Email](mailto:support@keycard.ai)\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "A Python SDK for Model Context Protocol (MCP) functionality with simplified authentication and authorization",
    "version": "0.9.0",
    "project_urls": {
        "Documentation": "https://docs.keycardai.com",
        "Homepage": "https://github.com/keycardai/python-sdk",
        "Issues": "https://github.com/keycardai/python-sdk/issues",
        "Repository": "https://github.com/keycardai/python-sdk"
    },
    "split_keywords": [
        "ai",
        " authentication",
        " authorization",
        " llm",
        " mcp",
        " model-context-protocol"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "450f93cbf92fa777abee9c42f0b5f1711f92c5bae81330870e82b97491966190",
                "md5": "fe7fe07fac2e97911312ccc59b691c5c",
                "sha256": "b89cebc3fa2bc308b2a5212a992b254e27268ed5b94965d07560a04bbfd5a56f"
            },
            "downloads": -1,
            "filename": "keycardai_mcp-0.9.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "fe7fe07fac2e97911312ccc59b691c5c",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 39531,
            "upload_time": "2025-10-20T13:37:00",
            "upload_time_iso_8601": "2025-10-20T13:37:00.797465Z",
            "url": "https://files.pythonhosted.org/packages/45/0f/93cbf92fa777abee9c42f0b5f1711f92c5bae81330870e82b97491966190/keycardai_mcp-0.9.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "ca105fbc9bd0b078e8930be3e2e67d395e6395eb30b261befea9ed68d8f5f8c1",
                "md5": "3cd5d8bc873d0ba2506e463f67df1489",
                "sha256": "7d62ddf4763cdf57c28fb2034ce82be481f026e2c04febc58999d5def3d33149"
            },
            "downloads": -1,
            "filename": "keycardai_mcp-0.9.0.tar.gz",
            "has_sig": false,
            "md5_digest": "3cd5d8bc873d0ba2506e463f67df1489",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 59411,
            "upload_time": "2025-10-20T13:37:01",
            "upload_time_iso_8601": "2025-10-20T13:37:01.667244Z",
            "url": "https://files.pythonhosted.org/packages/ca/10/5fbc9bd0b078e8930be3e2e67d395e6395eb30b261befea9ed68d8f5f8c1/keycardai_mcp-0.9.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-20 13:37:01",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "keycardai",
    "github_project": "python-sdk",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "keycardai-mcp"
}
        
Elapsed time: 1.29198s