Skip to content

Commit 60e5c40

Browse files
authored
Merge pull request #70 from keycardai/age-17/delegated-access-examples
feat(examples): add delegated access examples with @grant decorator
2 parents 251be78 + 3a0b083 commit 60e5c40

File tree

15 files changed

+3281
-84
lines changed

15 files changed

+3281
-84
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.10
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# GitHub API Integration with Keycard Delegated Access
2+
3+
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.
4+
5+
## Why Keycard?
6+
7+
Keycard lets you securely connect your AI IDE or agent to external resources. With delegated access, your MCP server can:
8+
9+
- **Exchange user tokens** for API-specific access tokens via OAuth 2.0 Token Exchange
10+
- **Access external APIs** on behalf of authenticated users with proper scopes
11+
- **Maintain audit trails** of all delegated operations
12+
13+
## Prerequisites
14+
15+
Before running this example, set up Keycard for delegated access:
16+
17+
### 1. Sign up at [keycard.ai](https://keycard.ai)
18+
19+
### 2. Create a Zone
20+
21+
A zone is your authentication boundary. Create one in the Keycard console.
22+
23+
### 3. Configure an Identity Provider
24+
25+
Set up an identity provider (Google, Microsoft, etc.) for user authentication.
26+
27+
### 4. Create a GitHub App
28+
29+
Create a GitHub App in your organization (or personal account) to enable delegated access:
30+
31+
1. Go to your GitHub organization → **Settings****Developer settings****GitHub Apps**
32+
2. Click **New GitHub App**
33+
3. Configure the app with the permissions your MCP server needs (e.g., **Repository** access, **User** profile read)
34+
4. Generate a **Client Secret**
35+
5. Note down the **Client ID** and **Client Secret** — you'll use these to configure the credential provider in Keycard
36+
37+
### 5. Configure a Credential Provider in Keycard
38+
39+
Set up the GitHub credential provider so Keycard can obtain tokens on behalf of users:
40+
41+
1. In your Keycard zone, go to **Providers**
42+
2. Add a new GitHub credential provider using the **Client ID** and **Client Secret** from the GitHub App you created in step 4
43+
3. This credential provider is what Keycard uses to issue GitHub API tokens on behalf of authenticated users
44+
45+
### 6. Configure GitHub as an API Resource
46+
47+
Add GitHub as an API resource in your zone:
48+
49+
1. In your Keycard zone, go to **Resources**
50+
2. Add a new API resource for GitHub:
51+
- **Resource URL**: `https://api.github.com`
52+
- **Credential Provider**: Select the GitHub credential provider you created in step 5
53+
- **Scopes**: `read:user`, `repo` (adjust based on your needs)
54+
55+
### 7. Create an Application
56+
57+
Create an application that will represent your MCP server:
58+
59+
1. Go to **Applications** in your zone
60+
2. Create a new application
61+
- **Identifier**: Set this to match your `MCP_SERVER_URL` (e.g., `http://localhost:8000/`)
62+
3. Add the **GitHub API** resource as a dependency of this application
63+
4. Generate **Application Credentials** (Client ID and Client Secret)
64+
- These are what you'll use for `KEYCARD_CLIENT_ID` and `KEYCARD_CLIENT_SECRET`
65+
66+
### 8. Create an MCP Server Resource
67+
68+
Register your MCP server with Keycard:
69+
70+
1. Go to **Resources** and add a new MCP Server resource
71+
2. Set the URL to your server's MCP endpoint: `http://localhost:8000/mcp`
72+
3. Configure the resource:
73+
- **Provided by**: Select the application you created in step 7
74+
- **Credential Provider**: Keycard STS Zone Provider
75+
76+
> **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.
77+
78+
See [Delegated Access Setup](https://docs.keycard.ai/build-with-keycard/delegated-access) for detailed instructions.
79+
80+
## Quick Start
81+
82+
### 1. Set Up Tunneling (for local development)
83+
84+
Delegated access requires Keycard to reach your server. For local development, set up a tunnel:
85+
86+
```bash
87+
# Using ngrok
88+
ngrok http 8000
89+
90+
# Or using Cloudflare Tunnel
91+
cloudflared tunnel --url http://localhost:8000
92+
```
93+
94+
Use the public URL from your tunnel as `MCP_SERVER_URL`.
95+
96+
### 2. Set Environment Variables
97+
98+
```bash
99+
export KEYCARD_ZONE_ID="your-zone-id"
100+
export KEYCARD_CLIENT_ID="your-client-id"
101+
export KEYCARD_CLIENT_SECRET="your-client-secret"
102+
export MCP_SERVER_URL="https://your-tunnel-url.ngrok.io/" # Must be publicly reachable
103+
```
104+
105+
### 3. Install Dependencies
106+
107+
```bash
108+
cd packages/mcp-fastmcp/examples/delegated_access
109+
uv sync
110+
```
111+
112+
### 4. Run the Server
113+
114+
```bash
115+
uv run python main.py
116+
```
117+
118+
The server will start on `http://localhost:8000`.
119+
120+
### 5. Verify the Server
121+
122+
Check that OAuth metadata is being served:
123+
124+
```bash
125+
curl http://localhost:8000/.well-known/oauth-authorization-server
126+
```
127+
128+
You should see JSON with `issuer`, `authorization_endpoint`, and other OAuth metadata.
129+
130+
## Testing with MCP Client
131+
132+
1. Connect to your server using an MCP-compatible client (e.g., Cursor, Claude Desktop)
133+
2. Authenticate through your configured identity provider
134+
3. When prompted by Keycard, authorize GitHub access
135+
4. Call the `get_github_user` or `list_github_repos` tools
136+
5. Verify GitHub user data is returned
137+
138+
## How It Works
139+
140+
### Token Exchange Flow
141+
142+
```
143+
User MCP Server Keycard GitHub
144+
│ │ │ │
145+
│──── Authenticate ──────►│ │ │
146+
│ │◄── User Token ───────│ │
147+
│ │ │ │
148+
│──── Call Tool ─────────►│ │ │
149+
│ │── Exchange Token ───►│ │
150+
│ │◄─ GitHub Token ──────│ │
151+
│ │ │ │
152+
│ │──────────────────────┼── API Request ───────►│
153+
│ │◄─────────────────────┼── API Response ───────│
154+
│◄─── Tool Result ────────│ │ │
155+
```
156+
157+
1. User authenticates to your MCP server via Keycard
158+
2. When a tool with `@grant` is called, Keycard exchanges the user's token
159+
3. The exchanged token has the scopes configured for the external resource
160+
4. Your server uses this token to call GitHub API on behalf of the user
161+
162+
## Error Handling
163+
164+
The example demonstrates comprehensive error handling patterns:
165+
166+
| Method | Description |
167+
|--------|-------------|
168+
| `has_errors()` | Check for any errors (global or resource-specific) |
169+
| `get_errors()` | Get all error details as a dictionary |
170+
| `has_resource_error(url)` | Check for errors on a specific resource |
171+
| `get_resource_errors(url)` | Get errors for a specific resource |
172+
| `has_error()` | Check for global errors only |
173+
| `get_error()` | Get global error details |
174+
175+
## Environment Variables Reference
176+
177+
| Variable | Required | Description |
178+
|----------|----------|-------------|
179+
| `KEYCARD_ZONE_ID` | Yes | Your Keycard zone ID |
180+
| `KEYCARD_CLIENT_ID` | Yes | Client ID from application credentials |
181+
| `KEYCARD_CLIENT_SECRET` | Yes | Client secret from application credentials |
182+
| `MCP_SERVER_URL` | Yes | Server URL (must be publicly reachable for delegated access) |
183+
184+
## Learn More
185+
186+
- [Keycard Documentation](https://docs.keycard.ai)
187+
- [Delegated Access Guide](https://docs.keycard.ai/build-with-keycard/delegated-access)
188+
- [FastMCP Documentation](https://docs.fastmcp.com)
189+
- [GitHub API Documentation](https://docs.github.com/rest)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""GitHub API Integration with Keycard Delegated Access.
2+
3+
This example demonstrates how to use the @grant decorator to request
4+
token exchange for accessing external APIs (GitHub) on behalf of
5+
authenticated users.
6+
7+
Key concepts demonstrated:
8+
- AuthProvider setup with ClientSecret credentials
9+
- @grant decorator for requesting token exchange
10+
- AccessContext for accessing exchanged tokens
11+
- Comprehensive error handling patterns
12+
"""
13+
14+
import os
15+
16+
import httpx
17+
from fastmcp import Context, FastMCP
18+
19+
from keycardai.mcp.integrations.fastmcp import AccessContext, AuthProvider, ClientSecret
20+
21+
# Configure Keycard authentication with client credentials for delegated access
22+
# Get your zone_id and client credentials from console.keycard.ai
23+
auth_provider = AuthProvider(
24+
zone_id=os.getenv("KEYCARD_ZONE_ID", "your-zone-id"),
25+
mcp_server_name="GitHub API Server",
26+
mcp_base_url=os.getenv("MCP_SERVER_URL", "http://localhost:8000/"),
27+
# ClientSecret enables token exchange for delegated access
28+
application_credential=ClientSecret(
29+
(
30+
os.getenv("KEYCARD_CLIENT_ID", "your-client-id"),
31+
os.getenv("KEYCARD_CLIENT_SECRET", "your-client-secret"),
32+
)
33+
),
34+
)
35+
36+
# Get the RemoteAuthProvider for FastMCP
37+
auth = auth_provider.get_remote_auth_provider()
38+
39+
# Create authenticated FastMCP server
40+
mcp = FastMCP("GitHub API Server", auth=auth)
41+
42+
43+
@mcp.tool()
44+
@auth_provider.grant("https://api.github.com")
45+
async def get_github_user(ctx: Context) -> dict:
46+
"""Get the authenticated GitHub user's profile.
47+
48+
Demonstrates:
49+
- Basic @grant decorator usage
50+
- Error checking with has_errors()
51+
- Token access via AccessContext
52+
53+
Args:
54+
ctx: FastMCP context with Keycard authentication state
55+
56+
Returns:
57+
User profile data or error details
58+
"""
59+
# Get access context from FastMCP context namespace
60+
access_context: AccessContext = ctx.get_state("keycardai")
61+
62+
# Check for any errors (global or resource-specific)
63+
if access_context.has_errors():
64+
errors = access_context.get_errors()
65+
return {"error": "Token exchange failed", "details": errors}
66+
67+
# Get the exchanged token for GitHub API
68+
token = access_context.access("https://api.github.com").access_token
69+
70+
# Call GitHub API with delegated token
71+
async with httpx.AsyncClient() as client:
72+
response = await client.get(
73+
"https://api.github.com/user",
74+
headers={
75+
"Authorization": f"Bearer {token}",
76+
"Accept": "application/vnd.github.v3+json",
77+
},
78+
)
79+
80+
if response.status_code != 200:
81+
return {
82+
"error": f"GitHub API error: {response.status_code}",
83+
"details": response.text,
84+
}
85+
86+
user_data = response.json()
87+
return {
88+
"login": user_data.get("login"),
89+
"name": user_data.get("name"),
90+
"email": user_data.get("email"),
91+
"public_repos": user_data.get("public_repos"),
92+
"followers": user_data.get("followers"),
93+
}
94+
95+
96+
@mcp.tool()
97+
@auth_provider.grant("https://api.github.com")
98+
async def list_github_repos(ctx: Context, per_page: int = 5) -> dict:
99+
"""List the authenticated user's GitHub repositories.
100+
101+
Demonstrates:
102+
- Resource-specific error checking with has_resource_error()
103+
- Getting resource-specific errors with get_resource_errors()
104+
- Parameterized API calls
105+
106+
Args:
107+
ctx: FastMCP context with Keycard authentication state
108+
per_page: Number of repositories to return (default: 5)
109+
110+
Returns:
111+
List of repositories or error details
112+
"""
113+
access_context: AccessContext = ctx.get_state("keycardai")
114+
115+
# Check for resource-specific error (alternative to has_errors())
116+
if access_context.has_resource_error("https://api.github.com"):
117+
resource_errors = access_context.get_resource_errors("https://api.github.com")
118+
return {
119+
"error": "Token exchange failed for GitHub API",
120+
"resource_errors": resource_errors,
121+
}
122+
123+
# Check for global errors (e.g., no auth token available)
124+
if access_context.has_error():
125+
return {"error": "Global token error", "details": access_context.get_error()}
126+
127+
token = access_context.access("https://api.github.com").access_token
128+
129+
async with httpx.AsyncClient() as client:
130+
response = await client.get(
131+
"https://api.github.com/user/repos",
132+
headers={
133+
"Authorization": f"Bearer {token}",
134+
"Accept": "application/vnd.github.v3+json",
135+
},
136+
params={"per_page": per_page, "sort": "updated"},
137+
)
138+
139+
if response.status_code != 200:
140+
return {
141+
"error": f"GitHub API error: {response.status_code}",
142+
"details": response.text,
143+
}
144+
145+
repos = response.json()
146+
return {
147+
"count": len(repos),
148+
"repositories": [
149+
{
150+
"name": repo.get("name"),
151+
"full_name": repo.get("full_name"),
152+
"private": repo.get("private"),
153+
"html_url": repo.get("html_url"),
154+
}
155+
for repo in repos
156+
],
157+
}
158+
159+
160+
def main():
161+
"""Entry point for the MCP server."""
162+
mcp.run(transport="streamable-http")
163+
164+
165+
if __name__ == "__main__":
166+
main()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[project]
2+
name = "delegated-access-example"
3+
version = "0.1.0"
4+
description = "GitHub API integration with Keycard delegated access using the @grant decorator"
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
dependencies = [
8+
"keycardai-mcp-fastmcp",
9+
"fastmcp>=2.13.0,<3.0.0",
10+
"httpx>=0.27.0,<1.0.0",
11+
]
12+
13+
[tool.uv.sources]
14+
keycardai-mcp-fastmcp = { path = "../../", editable = true }
15+
16+
[project.scripts]
17+
delegated-access-server = "main:main"

0 commit comments

Comments
 (0)