@@ -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
814pip 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
3541auth_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)
4854def 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" )
5483def 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
68236For complete examples and advanced usage patterns, see our [ documentation] ( https://docs.keycard.ai ) .
0 commit comments