Skip to content

Commit 5a68542

Browse files
Merge pull request #12 from keycardai/fix/tool-func-types
Fix/tool func types
2 parents b939c4e + 9927a7f commit 5a68542

9 files changed

Lines changed: 168 additions & 15 deletions

File tree

README.md

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ if __name__ == "__main__":
3939
python server.py
4040
```
4141

42-
For more detail refer to the [mcp](https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#streamable-http-transport) documentation
42+
For more details, refer to the [mcp](https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#streamable-http-transport) documentation.
4343

4444
### Configure the remote MCP in your AI client, like [Cursor](https://cursor.com/?from=home)
4545

@@ -53,17 +53,17 @@ For more detail refer to the [mcp](https://github.com/modelcontextprotocol/pytho
5353
}
5454
```
5555

56-
### Test the remote server with client,
56+
### Test the remote server with client
5757

5858
<img src="docs/images/cursor_hello_world_agent_call.png" alt="Cursor Hello World Agent Call" width="500">
5959

6060
### Signup to Keycard and get your zone identifier
6161

62-
Refer to [docs](https://docs.keycard.ai/) on how to signup. Navigate to Zone Settings to obtain the zone id
62+
Refer to [docs](https://docs.keycard.ai/) on how to sign up. Navigate to Zone Settings to obtain the zone ID.
6363

6464
<img src="docs/images/keycard_zone_information.png" alt="Keycard ZoneId Information" width="400">
6565

66-
### Configure Your prefered Identity Provider
66+
### Configure Your Preferred Identity Provider
6767

6868
<img src="docs/images/keycard_identity_provider_config.png" alt="Keycard Identity Provider Configuration" width="400">
6969

@@ -92,17 +92,17 @@ mcp = FastMCP("Minimal MCP")
9292
def hello_world(name: str) -> str:
9393
return f"Hello, {name}!"
9494

95-
# Create starlett app to handle authorization flows
95+
# Create Starlette app to handle authorization flows
9696
app = access.app(mcp)
9797
```
9898

9999
### Run Your Server
100100

101-
The authorization flows require additonal handlers to advertise the metadata.
101+
The authorization flows require additional handlers to advertise the metadata.
102102

103-
This is implemented using underlying starlett application, for more information refer to official [mcp](https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#streamablehttp-servers) documentation
103+
This is implemented using the underlying Starlette application. For more information, refer to the official [mcp](https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#streamablehttp-servers) documentation.
104104

105-
You can use any async server, for example [uvicorn](https://www.uvicorn.org/)
105+
You can use any async server, for example [uvicorn](https://www.uvicorn.org/):
106106

107107
```bash
108108
uv add uvicorn
@@ -115,7 +115,7 @@ pip install uvicorn
115115
```
116116

117117
```bash
118-
uvicorn server:app
118+
python -m uvicorn server:app
119119
```
120120

121121
### Authenticate in client
@@ -125,6 +125,79 @@ uvicorn server:app
125125

126126
### 🎉 Your MCP server is now running with KeyCard authentication! 🎉
127127

128+
## Features
129+
130+
### Delegated Access
131+
132+
You can use Keycard to allow MCP servers to access other resources on behalf of the user.
133+
134+
It automatically requests user consent and performs necessary secure exchanges to provide granular access to resources.
135+
136+
#### Configure credential provider
137+
138+
Configure a credential provider for your resource, for example Google Workspace.
139+
140+
<img src="docs/images/keycard_credential_provider_config.png" alt="Keycard Credential Provider Configuration" width="400">
141+
142+
#### Configure protected resource
143+
144+
Configure a protected resource, for example the Google Drive API.
145+
146+
<img src="docs/images/keycard_resource_create.png" alt="Keycard Resource Creation" width="400">
147+
148+
#### Allow access from MCP to protected resource
149+
150+
To allow the MCP server to make delegated calls to the API, set the dependency on the MCP server for the protected resource.
151+
152+
<img src="docs/images/keycard_set_dependency.png" alt="Keycard Set Dependency" width="400">
153+
154+
#### Give the MCP server identity secret
155+
156+
In order for the MCP server to securely perform exchanges, it requires an identity secret.
157+
158+
<img src="docs/images/keycard_identity_configuration.png" alt="Keycard Resource Identity" width="400">
159+
160+
Note: Keep the client_id and client_secret safe. We will use them in the next steps.
161+
162+
#### Add delegation control to tool calls
163+
164+
Note: For demonstration, we will print a different message when access is granted.
165+
In real use cases, you would use the token to make requests to downstream APIs.
166+
167+
```python
168+
from mcp.server.fastmcp import FastMCP, Context
169+
170+
from keycardai.mcp.server.auth import AuthProvider, AccessContext, BasicAuth
171+
172+
# From the zone setting above
173+
zone_id = "90zqtq5lvtobrmyl3b0i0k2z1q"
174+
175+
access = AuthProvider(
176+
zone_id = zone_id,
177+
mcp_server_name="Hello World Mcp",
178+
auth=BasicAuth(os.getenv("KEYCARD_CLIENT_ID"), os.getenv("KEYCARD_CLIENT_SECRET")))
179+
)
180+
181+
mcp = FastMCP("Minimal MCP")
182+
183+
protected_resource_identifier = "https://protected-api"
184+
185+
@mcp.tool()
186+
@access.grant(protected_resource_identifier)
187+
def hello_world(ctx: Context, access_context: AccessContext, name: str) -> str:
188+
msg = f"Hello, {name}!"
189+
if access_context.access(protected_resource_identifier).access_token:
190+
msg = f"Hello, {name}! I can see you have extra access"
191+
return msg
192+
193+
# Create Starlette app to handle authorization flows
194+
app = access.app(mcp)
195+
```
196+
197+
#### Use obtained access to make API calls on behalf of users
198+
199+
<img src="docs/images/cursor_delegated_access_example.png" alt="Keycard Set Dependency" width="400">
200+
128201

129202
## Overview
130203

55.5 KB
Loading
68.7 KB
Loading
54.4 KB
Loading
56.1 KB
Loading
49.4 KB
Loading

packages/mcp/README.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pip install keycardai-mcp
2222

2323
1. Sign up at [keycard.ai](https://keycard.ai)
2424
2. Navigate to Zone Settings to get your zone ID
25-
3. Configure your preferred identity provider
25+
3. Configure your preferred identity provider (Google, Microsoft, etc.)
2626
4. Create an MCP resource in your zone
2727

2828
### Add Authentication to Your MCP Server
@@ -65,13 +65,54 @@ uvicorn server:app
6565
-**Token Exchange**: Automatic delegated token exchange for accessing external APIs
6666
-**Production Ready**: Battle-tested security patterns and error handling
6767

68+
### Delegated Access
69+
70+
KeyCard allows MCP servers to access other resources on behalf of users with automatic consent and secure token exchange.
71+
72+
#### Setup Protected Resources
73+
74+
1. **Configure credential provider** (e.g., Google Workspace)
75+
2. **Configure protected resource** (e.g., Google Drive API)
76+
3. **Set MCP server dependencies** to allow delegated access
77+
4. **Create client secret identity** to provide authentication method
78+
79+
#### Add Delegation to Your Tools
80+
81+
```python
82+
from mcp.server.fastmcp import FastMCP, Context
83+
from keycardai.mcp.server.auth import AuthProvider, AccessContext, BasicAuth
84+
import os
85+
86+
# Configure your provider
87+
access = AuthProvider(
88+
zone_id="your_zone_id",
89+
mcp_server_name="My MCP Server",
90+
auth=BasicAuth(
91+
os.getenv("KEYCARD_CLIENT_ID"),
92+
os.getenv("KEYCARD_CLIENT_SECRET")
93+
)
94+
)
95+
96+
mcp = FastMCP("My MCP Server")
97+
98+
@mcp.tool()
99+
@access.grant("https://protected-api")
100+
def protected_tool(ctx: Context, access_context: AccessContext, name: str) -> str:
101+
# Use the access_context to call external APIs on behalf of the user
102+
token = access_context.access("https://protected-api").access_token
103+
# Make authenticated API calls...
104+
return f"Protected data for {name}"
105+
106+
app = access.app(mcp)
107+
```
108+
68109
## Examples
69110

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

72113
## License
73114

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

76117
## Support
77118

packages/mcp/src/keycardai/mcp/server/auth/provider.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,18 @@ def grant(self, resources: str | list[str]):
312312
313313
Usage:
314314
```python
315+
from mcp.server.fastmcp import Context
316+
# Async function
315317
@provider.grant("https://api.example.com")
316-
async def my_tool(ctx: AccessContext, user_id: str):
318+
async def my_async_tool(ctx: AccessContext, request_ctx: Context, user_id: str):
319+
token = ctx.access("https://api.example.com").access_token
320+
# Use token to call the external API
321+
headers = {"Authorization": f"Bearer {token}"}
322+
# ... make API call
323+
324+
# Sync function (also supported)
325+
@provider.grant("https://api.example.com")
326+
def my_sync_tool(ctx: AccessContext, request_ctx: Context, user_id: str):
317327
token = ctx.access("https://api.example.com").access_token
318328
# Use token to call the external API
319329
headers = {"Authorization": f"Bearer {token}"}
@@ -322,7 +332,11 @@ async def my_tool(ctx: AccessContext, user_id: str):
322332
323333
The decorated function must:
324334
- Have a parameter annotated with `AccessContext` type (e.g., `my_ctx: AccessContext = None`)
325-
- Be async (token exchange is async)
335+
- Have a parameter annotated with `Context` type from FastMCP (e.g., `request_ctx: Context`)
336+
- Can be either async or sync (the decorator handles both cases)
337+
338+
Note: The `Context` parameter is required for accessing request authentication information.
339+
Without it, the decorator cannot extract the user's authentication token.
326340
327341
Error handling:
328342
- Returns structured error response if token exchange fails
@@ -344,6 +358,9 @@ def decorator(func: Callable) -> Callable:
344358

345359
new_sig = original_sig.replace(parameters=new_params)
346360

361+
# mcp.server.fastmcp always run in async mode
362+
is_async_func = inspect.iscoroutinefunction(func)
363+
347364
@wraps(func)
348365
async def wrapper(*args, **kwargs) -> Any:
349366
try:
@@ -407,7 +424,10 @@ async def wrapper(*args, **kwargs) -> Any:
407424
if access_ctx_param_name:
408425
kwargs[access_ctx_param_name] = access_ctx
409426

410-
return await func(*args, **kwargs)
427+
if is_async_func:
428+
return await func(*args, **kwargs)
429+
else:
430+
return func(*args, **kwargs)
411431

412432
except Exception as e:
413433
return {

uv.lock

Lines changed: 20 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)