Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.10
171 changes: 171 additions & 0 deletions packages/mcp-fastmcp/examples/delegated_access/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# GitHub API Integration with Keycard Delegated Access

A complete example demonstrating how to use the `@grant` decorator for token exchange, enabling your MCP server to access external APIs (GitHub) on behalf of authenticated users.

## Why Keycard?

Keycard lets you securely connect your AI IDE or agent to external resources. With delegated access, your MCP server can:

- **Exchange user tokens** for API-specific access tokens via OAuth 2.0 Token Exchange
- **Access external APIs** on behalf of authenticated users with proper scopes
- **Maintain audit trails** of all delegated operations

## Prerequisites

Before running this example, set up Keycard for delegated access:

### 1. Sign up at [keycard.ai](https://keycard.ai)

### 2. Create a Zone

A zone is your authentication boundary. Create one in the Keycard console.

### 3. Configure an Identity Provider

Set up an identity provider (Google, Microsoft, etc.) for user authentication.

### 4. Configure GitHub as an API Resource

Add GitHub as an API resource in your zone:

1. In your Keycard zone, go to **Resources**
2. Add a new API resource for GitHub:
- **Resource URL**: `https://api.github.com`
- **OAuth Provider**: GitHub (or your GitHub OAuth App)
- **Scopes**: `read:user`, `repo` (adjust based on your needs)

### 5. Create an Application

Create an application that will represent your MCP server:

1. Go to **Applications** in your zone
2. Create a new application
- **Identifier**: Set this to match your `MCP_SERVER_URL` (e.g., `http://localhost:8000/`)
3. Add **GitHub API** as a dependency of this application
4. Generate **Application Credentials** (Client ID and Client Secret)
- These are what you'll use for `KEYCARD_CLIENT_ID` and `KEYCARD_CLIENT_SECRET`

### 6. Create an MCP Server Resource

Register your MCP server with Keycard:

1. Go to **Resources** and add a new MCP Server resource
2. Set the URL to your server's MCP endpoint: `http://localhost:8000/mcp`
3. Configure the resource:
- **Provided by**: Select the application you created in step 5
- **Credential Provider**: Keycard STS Zone Provider

> **Note:** Delegated token exchange requires Keycard to reach your MCP server. For local development, use a tunneling service (e.g., ngrok, Cloudflare Tunnel) or host the server on a publicly accessible URL.

See [Delegated Access Setup](https://docs.keycard.ai/build-with-keycard/delegated-access) for detailed instructions.

## Quick Start

### 1. Set Up Tunneling (for local development)

Delegated access requires Keycard to reach your server. For local development, set up a tunnel:

```bash
# Using ngrok
ngrok http 8000

# Or using Cloudflare Tunnel
cloudflared tunnel --url http://localhost:8000
```

Use the public URL from your tunnel as `MCP_SERVER_URL`.

### 2. Set Environment Variables

```bash
export KEYCARD_ZONE_ID="your-zone-id"
export KEYCARD_CLIENT_ID="your-client-id"
export KEYCARD_CLIENT_SECRET="your-client-secret"
export MCP_SERVER_URL="https://your-tunnel-url.ngrok.io/" # Must be publicly reachable
```

### 3. Install Dependencies

```bash
cd packages/mcp-fastmcp/examples/delegated_access
uv sync
```

### 4. Run the Server

```bash
uv run python main.py
```

The server will start on `http://localhost:8000`.

### 5. Verify the Server

Check that OAuth metadata is being served:

```bash
curl http://localhost:8000/.well-known/oauth-authorization-server
```

You should see JSON with `issuer`, `authorization_endpoint`, and other OAuth metadata.

## Testing with MCP Client

1. Connect to your server using an MCP-compatible client (e.g., Cursor, Claude Desktop)
2. Authenticate through your configured identity provider
3. When prompted by Keycard, authorize GitHub access
4. Call the `get_github_user` or `list_github_repos` tools
5. Verify GitHub user data is returned

## How It Works

### Token Exchange Flow

```
User MCP Server Keycard GitHub
│ │ │ │
│──── Authenticate ──────►│ │ │
│ │◄── User Token ───────│ │
│ │ │ │
│──── Call Tool ─────────►│ │ │
│ │── Exchange Token ───►│ │
│ │◄─ GitHub Token ──────│ │
│ │ │ │
│ │──────────────────────┼── API Request ───────►│
│ │◄─────────────────────┼── API Response ───────│
│◄─── Tool Result ────────│ │ │
```

1. User authenticates to your MCP server via Keycard
2. When a tool with `@grant` is called, Keycard exchanges the user's token
3. The exchanged token has the scopes configured for the external resource
4. Your server uses this token to call GitHub API on behalf of the user

## Error Handling

The example demonstrates comprehensive error handling patterns:

| Method | Description |
|--------|-------------|
| `has_errors()` | Check for any errors (global or resource-specific) |
| `get_errors()` | Get all error details as a dictionary |
| `has_resource_error(url)` | Check for errors on a specific resource |
| `get_resource_errors(url)` | Get errors for a specific resource |
| `has_error()` | Check for global errors only |
| `get_error()` | Get global error details |

## Environment Variables Reference

| Variable | Required | Description |
|----------|----------|-------------|
| `KEYCARD_ZONE_ID` | Yes | Your Keycard zone ID |
| `KEYCARD_CLIENT_ID` | Yes | Client ID from application credentials |
| `KEYCARD_CLIENT_SECRET` | Yes | Client secret from application credentials |
| `MCP_SERVER_URL` | Yes | Server URL (must be publicly reachable for delegated access) |

## Learn More

- [Keycard Documentation](https://docs.keycard.ai)
- [Delegated Access Guide](https://docs.keycard.ai/build-with-keycard/delegated-access)
- [FastMCP Documentation](https://docs.fastmcp.com)
- [GitHub API Documentation](https://docs.github.com/rest)
166 changes: 166 additions & 0 deletions packages/mcp-fastmcp/examples/delegated_access/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""GitHub API Integration with Keycard Delegated Access.

This example demonstrates how to use the @grant decorator to request
token exchange for accessing external APIs (GitHub) on behalf of
authenticated users.

Key concepts demonstrated:
- AuthProvider setup with ClientSecret credentials
- @grant decorator for requesting token exchange
- AccessContext for accessing exchanged tokens
- Comprehensive error handling patterns
"""

import os

import httpx
from fastmcp import Context, FastMCP

from keycardai.mcp.integrations.fastmcp import AccessContext, AuthProvider, ClientSecret

# Configure Keycard authentication with client credentials for delegated access
# Get your zone_id and client credentials from console.keycard.ai
auth_provider = AuthProvider(
zone_id=os.getenv("KEYCARD_ZONE_ID", "your-zone-id"),
mcp_server_name="GitHub API Server",
mcp_base_url=os.getenv("MCP_SERVER_URL", "http://localhost:8000/"),
# ClientSecret enables token exchange for delegated access
application_credential=ClientSecret(
(
os.getenv("KEYCARD_CLIENT_ID", "your-client-id"),
os.getenv("KEYCARD_CLIENT_SECRET", "your-client-secret"),
)
),
)

# Get the RemoteAuthProvider for FastMCP
auth = auth_provider.get_remote_auth_provider()

# Create authenticated FastMCP server
mcp = FastMCP("GitHub API Server", auth=auth)


@mcp.tool()
@auth_provider.grant("https://api.github.com")
async def get_github_user(ctx: Context) -> dict:
"""Get the authenticated GitHub user's profile.

Demonstrates:
- Basic @grant decorator usage
- Error checking with has_errors()
- Token access via AccessContext

Args:
ctx: FastMCP context with Keycard authentication state

Returns:
User profile data or error details
"""
# Get access context from FastMCP context namespace
access_context: AccessContext = ctx.get_state("keycardai")

# Check for any errors (global or resource-specific)
if access_context.has_errors():
errors = access_context.get_errors()
return {"error": "Token exchange failed", "details": errors}

# Get the exchanged token for GitHub API
token = access_context.access("https://api.github.com").access_token

# Call GitHub API with delegated token
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.github.com/user",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
},
)

if response.status_code != 200:
return {
"error": f"GitHub API error: {response.status_code}",
"details": response.text,
}

user_data = response.json()
return {
"login": user_data.get("login"),
"name": user_data.get("name"),
"email": user_data.get("email"),
"public_repos": user_data.get("public_repos"),
"followers": user_data.get("followers"),
}


@mcp.tool()
@auth_provider.grant("https://api.github.com")
async def list_github_repos(ctx: Context, per_page: int = 5) -> dict:
"""List the authenticated user's GitHub repositories.

Demonstrates:
- Resource-specific error checking with has_resource_error()
- Getting resource-specific errors with get_resource_errors()
- Parameterized API calls

Args:
ctx: FastMCP context with Keycard authentication state
per_page: Number of repositories to return (default: 5)

Returns:
List of repositories or error details
"""
access_context: AccessContext = ctx.get_state("keycardai")

# Check for resource-specific error (alternative to has_errors())
if access_context.has_resource_error("https://api.github.com"):
resource_errors = access_context.get_resource_errors("https://api.github.com")
return {
"error": "Token exchange failed for GitHub API",
"resource_errors": resource_errors,
}

# Check for global errors (e.g., no auth token available)
if access_context.has_error():
return {"error": "Global token error", "details": access_context.get_error()}

token = access_context.access("https://api.github.com").access_token

async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.github.com/user/repos",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
},
params={"per_page": per_page, "sort": "updated"},
)

if response.status_code != 200:
return {
"error": f"GitHub API error: {response.status_code}",
"details": response.text,
}

repos = response.json()
return {
"count": len(repos),
"repositories": [
{
"name": repo.get("name"),
"full_name": repo.get("full_name"),
"private": repo.get("private"),
"html_url": repo.get("html_url"),
}
for repo in repos
],
}


def main():
"""Entry point for the MCP server."""
mcp.run(transport="streamable-http")


if __name__ == "__main__":
main()
17 changes: 17 additions & 0 deletions packages/mcp-fastmcp/examples/delegated_access/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[project]
name = "delegated-access-example"
version = "0.1.0"
description = "GitHub API integration with Keycard delegated access using the @grant decorator"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"keycardai-mcp-fastmcp",
"fastmcp>=2.13.0,<3.0.0",
"httpx>=0.27.0,<1.0.0",
]

[tool.uv.sources]
keycardai-mcp-fastmcp = { path = "../../", editable = true }

[project.scripts]
delegated-access-server = "main:main"
Loading