Skip to content

Commit 9ee8469

Browse files
committed
docs(keycardai-mcp-fastmcp): document access context features
1 parent 82540e1 commit 9ee8469

File tree

2 files changed

+225
-4
lines changed

2 files changed

+225
-4
lines changed

packages/mcp-fastmcp/README.md

Lines changed: 171 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ A Python package that provides seamless integration between KeyCard and FastMCP
44

55
## Installation
66

7+
```bash
8+
uv add keycardai-mcp-fastmcp
9+
```
10+
11+
or
12+
713
```bash
814
pip install keycardai-mcp-fastmcp
915
```
@@ -15,7 +21,7 @@ Add KeyCard authentication to your existing FastMCP server:
1521
### Install the Package
1622

1723
```bash
18-
pip install keycardai-mcp-fastmcp
24+
uv add keycardai-mcp-fastmcp
1925
```
2026

2127
### Get Your KeyCard Zone ID
@@ -35,7 +41,7 @@ from keycardai.mcp.integrations.fastmcp import AuthProvider
3541
auth_provider = AuthProvider(
3642
zone_id="your-zone-id", # Get this from keycard.ai
3743
mcp_server_name="My Secure FastMCP Server",
38-
mcp_server_url="http://127.0.0.1:8000/"
44+
mcp_base_url="http://127.0.0.1:8000/" # Note: trailing slash will be added automatically
3945
)
4046

4147
# Get the RemoteAuthProvider for FastMCP
@@ -48,12 +54,42 @@ mcp = FastMCP("My Secure FastMCP Server", auth=auth)
4854
def hello_world(name: str) -> str:
4955
return f"Hello, {name}!"
5056

57+
if __name__ == "__main__":
58+
mcp.run(transport="streamable-http")
59+
```
60+
61+
### Add access delegation to tool calls
62+
63+
```python
64+
from fastmcp import FastMCP, Context
65+
from keycardai.mcp.integrations.fastmcp import AuthProvider, AccessContext
66+
67+
# Configure KeyCard authentication (recommended: use zone_id)
68+
auth_provider = AuthProvider(
69+
zone_id="your-zone-id", # Get this from keycard.ai
70+
mcp_server_name="My Secure FastMCP Server",
71+
mcp_base_url="http://127.0.0.1:8000/" # Note: trailing slash will be added automatically
72+
)
73+
74+
# Get the RemoteAuthProvider for FastMCP
75+
auth = auth_provider.get_remote_auth_provider()
76+
77+
# Create authenticated FastMCP server
78+
mcp = FastMCP("My Secure FastMCP Server", auth=auth)
79+
5180
# Example with token exchange for external API access
5281
@mcp.tool()
5382
@auth_provider.grant("https://api.example.com")
5483
def call_external_api(ctx: Context, query: str) -> str:
84+
# Get access context to check token exchange status
85+
access_context: AccessContext = ctx.get_state("keycardai")
86+
87+
# Check for errors before accessing token
88+
if access_context.has_errors():
89+
return f"Error: Failed to obtain access token - {access_context.get_errors()}"
90+
5591
# Access delegated token through context namespace
56-
token = ctx.get_state("keycardai").access("https://api.example.com").access_token
92+
token = access_context.access("https://api.example.com").access_token
5793
# Use token to call external API
5894
return f"Results for {query}"
5995

@@ -63,6 +99,138 @@ if __name__ == "__main__":
6399

64100
### 🎉 Your FastMCP server is now protected with KeyCard authentication! 🎉
65101

102+
## Working with AccessContext
103+
104+
When using the `@grant()` decorator, tokens are made available through the `AccessContext` object. This object provides robust error handling and status checking for token exchange operations.
105+
106+
The `@grant()` decorator avoids raising exceptions. Instead, it exposes error information via associated metadata.
107+
You can check if the context encountered errors by calling the `has_errors()` method.
108+
109+
### Basic Usage
110+
111+
```python
112+
from keycardai.mcp.integrations.fastmcp import AccessContext
113+
114+
@mcp.tool()
115+
@auth_provider.grant("https://api.example.com")
116+
def my_tool(ctx: Context, user_id: str) -> str:
117+
# Get the access context
118+
access_context: AccessContext = ctx.get_state("keycardai")
119+
120+
# Always check for errors first
121+
if access_context.has_errors():
122+
# Handle the error case
123+
errors = access_context.get_errors()
124+
return f"Authentication failed: {errors}"
125+
126+
# Access the token for the specific resource
127+
token = access_context.access("https://api.example.com").access_token
128+
129+
# Use the token in your API calls
130+
headers = {"Authorization": f"Bearer {token}"}
131+
# Make your API request...
132+
return f"Success for user {user_id}"
133+
```
134+
135+
### Multiple Resources
136+
137+
You can request tokens for multiple resources in a single decorator:
138+
139+
```python
140+
@mcp.tool()
141+
@auth_provider.grant(["https://api.example.com", "https://other-api.com"])
142+
def multi_resource_tool(ctx: Context) -> str:
143+
access_context: AccessContext = ctx.get_state("keycardai")
144+
145+
# Check overall status
146+
status = access_context.get_status() # "success", "partial_error", or "error"
147+
148+
if status == "error":
149+
# Global error - no tokens available
150+
return f"Global error: {access_context.get_error()}"
151+
152+
elif status == "partial_error":
153+
# Some resources succeeded, others failed
154+
successful = access_context.get_successful_resources()
155+
failed = access_context.get_failed_resources()
156+
157+
# Work with successful resources only
158+
for resource in successful:
159+
token = access_context.access(resource).access_token
160+
# Use token...
161+
162+
return f"Partial success: {len(successful)} succeeded, {len(failed)} failed"
163+
164+
else: # status == "success"
165+
# All resources succeeded
166+
token1 = access_context.access("https://api.example.com").access_token
167+
token2 = access_context.access("https://other-api.com").access_token
168+
# Use both tokens...
169+
return "All resources accessed successfully"
170+
```
171+
172+
### Error Handling Methods
173+
174+
The `AccessContext` provides several methods for checking errors:
175+
176+
```python
177+
# Check if there are any errors (global or resource-specific)
178+
if access_context.has_errors():
179+
# Handle any error case
180+
181+
# Check for global errors only
182+
if access_context.has_error():
183+
global_error = access_context.get_error()
184+
185+
# Check for specific resource errors
186+
if access_context.has_resource_error("https://api.example.com"):
187+
resource_error = access_context.get_resource_errors("https://api.example.com")
188+
189+
# Get all errors (global + resource-specific)
190+
all_errors = access_context.get_errors()
191+
192+
# Get status summary
193+
status = access_context.get_status() # "success", "partial_error", or "error"
194+
195+
# Get lists of successful/failed resources
196+
successful_resources = access_context.get_successful_resources()
197+
failed_resources = access_context.get_failed_resources()
198+
```
199+
200+
## Important Configuration Notes
201+
202+
### URL Slash Requirement
203+
204+
⚠️ **Important**: The `mcp_base_url` parameter will automatically have a trailing slash (`/`) appended if not present. This is required for proper JWT audience validation with FastMCP.
205+
206+
**When configuring your KeyCard Resource**, ensure the resource URL in your KeyCard zone settings matches exactly, including the trailing slash:
207+
208+
```python
209+
# This configuration...
210+
auth_provider = AuthProvider(
211+
zone_id="your-zone-id",
212+
mcp_base_url="http://localhost:8000" # No trailing slash
213+
)
214+
215+
# Will become "http://localhost:8000/" internally
216+
# So your KeyCard Resource must be configured as: http://localhost:8000/
217+
```
218+
219+
### Client Credentials for Token Exchange
220+
221+
To enable token exchange (required for the `@grant` decorator), provide client credentials:
222+
223+
```python
224+
from keycardai.oauth.http.auth import BasicAuth
225+
226+
auth_provider = AuthProvider(
227+
zone_id="your-zone-id",
228+
mcp_server_name="My FastMCP Service",
229+
mcp_base_url="http://localhost:8000/",
230+
auth=BasicAuth("your_client_id", "your_client_secret")
231+
)
232+
```
233+
66234
## Examples
67235

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

uv.lock

Lines changed: 54 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)