Skip to content
Merged
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
91 changes: 82 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ if __name__ == "__main__":
python server.py
```

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

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

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

### Test the remote server with client,
### Test the remote server with client

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

### Signup to Keycard and get your zone identifier

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

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

### Configure Your prefered Identity Provider
### Configure Your Preferred Identity Provider

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

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

# Create starlett app to handle authorization flows
# Create Starlette app to handle authorization flows
app = access.app(mcp)
```

### Run Your Server

The authorization flows require additonal handlers to advertise the metadata.
The authorization flows require additional handlers to advertise the metadata.

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
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.

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

```bash
uv add uvicorn
Expand All @@ -115,7 +115,7 @@ pip install uvicorn
```

```bash
uvicorn server:app
python -m uvicorn server:app
```

### Authenticate in client
Expand All @@ -125,6 +125,79 @@ uvicorn server:app

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

## Features

### Delegated Access

You can use Keycard to allow MCP servers to access other resources on behalf of the user.

It automatically requests user consent and performs necessary secure exchanges to provide granular access to resources.

#### Configure credential provider

Configure a credential provider for your resource, for example Google Workspace.

<img src="docs/images/keycard_credential_provider_config.png" alt="Keycard Credential Provider Configuration" width="400">

#### Configure protected resource

Configure a protected resource, for example the Google Drive API.

<img src="docs/images/keycard_resource_create.png" alt="Keycard Resource Creation" width="400">

#### Allow access from MCP to protected resource

To allow the MCP server to make delegated calls to the API, set the dependency on the MCP server for the protected resource.

<img src="docs/images/keycard_set_dependency.png" alt="Keycard Set Dependency" width="400">

#### Give the MCP server identity secret

In order for the MCP server to securely perform exchanges, it requires an identity secret.

<img src="docs/images/keycard_identity_configuration.png" alt="Keycard Resource Identity" width="400">

Note: Keep the client_id and client_secret safe. We will use them in the next steps.

#### Add delegation control to tool calls

Note: For demonstration, we will print a different message when access is granted.
In real use cases, you would use the token to make requests to downstream APIs.

```python
from mcp.server.fastmcp import FastMCP, Context

from keycardai.mcp.server.auth import AuthProvider, AccessContext, BasicAuth

# From the zone setting above
zone_id = "90zqtq5lvtobrmyl3b0i0k2z1q"

access = AuthProvider(
zone_id = zone_id,
mcp_server_name="Hello World Mcp",
auth=BasicAuth(os.getenv("KEYCARD_CLIENT_ID"), os.getenv("KEYCARD_CLIENT_SECRET")))
)

mcp = FastMCP("Minimal MCP")

protected_resource_identifier = "https://protected-api"

@mcp.tool()
@access.grant(protected_resource_identifier)
def hello_world(ctx: Context, access_context: AccessContext, name: str) -> str:
msg = f"Hello, {name}!"
if access_context.access(protected_resource_identifier).access_token:
msg = f"Hello, {name}! I can see you have extra access"
return msg

# Create Starlette app to handle authorization flows
app = access.app(mcp)
```

#### Use obtained access to make API calls on behalf of users

<img src="docs/images/cursor_delegated_access_example.png" alt="Keycard Set Dependency" width="400">


## Overview

Expand Down
Binary file added docs/images/cursor_delegated_access_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/keycard_identity_configuration.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/keycard_resource_create.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/keycard_set_dependency.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 43 additions & 2 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pip install keycardai-mcp

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

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

### Delegated Access

KeyCard allows MCP servers to access other resources on behalf of users with automatic consent and secure token exchange.

#### Setup Protected Resources

1. **Configure credential provider** (e.g., Google Workspace)
2. **Configure protected resource** (e.g., Google Drive API)
3. **Set MCP server dependencies** to allow delegated access
4. **Create client secret identity** to provide authentication method

#### Add Delegation to Your Tools

```python
from mcp.server.fastmcp import FastMCP, Context
from keycardai.mcp.server.auth import AuthProvider, AccessContext, BasicAuth
import os

# Configure your provider
access = AuthProvider(
zone_id="your_zone_id",
mcp_server_name="My MCP Server",
auth=BasicAuth(
os.getenv("KEYCARD_CLIENT_ID"),
os.getenv("KEYCARD_CLIENT_SECRET")
)
)

mcp = FastMCP("My MCP Server")

@mcp.tool()
@access.grant("https://protected-api")
def protected_tool(ctx: Context, access_context: AccessContext, name: str) -> str:
# Use the access_context to call external APIs on behalf of the user
token = access_context.access("https://protected-api").access_token
# Make authenticated API calls...
return f"Protected data for {name}"

app = access.app(mcp)
```

## Examples

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

## License

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

## Support

Expand Down
26 changes: 23 additions & 3 deletions packages/mcp/src/keycardai/mcp/server/auth/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,18 @@ def grant(self, resources: str | list[str]):

Usage:
```python
from mcp.server.fastmcp import Context
# Async function
@provider.grant("https://api.example.com")
async def my_tool(ctx: AccessContext, user_id: str):
async def my_async_tool(ctx: AccessContext, request_ctx: Context, user_id: str):
token = ctx.access("https://api.example.com").access_token
# Use token to call the external API
headers = {"Authorization": f"Bearer {token}"}
# ... make API call

# Sync function (also supported)
@provider.grant("https://api.example.com")
def my_sync_tool(ctx: AccessContext, request_ctx: Context, user_id: str):
token = ctx.access("https://api.example.com").access_token
# Use token to call the external API
headers = {"Authorization": f"Bearer {token}"}
Expand All @@ -322,7 +332,11 @@ async def my_tool(ctx: AccessContext, user_id: str):

The decorated function must:
- Have a parameter annotated with `AccessContext` type (e.g., `my_ctx: AccessContext = None`)
- Be async (token exchange is async)
- Have a parameter annotated with `Context` type from FastMCP (e.g., `request_ctx: Context`)
- Can be either async or sync (the decorator handles both cases)

Note: The `Context` parameter is required for accessing request authentication information.
Without it, the decorator cannot extract the user's authentication token.

Error handling:
- Returns structured error response if token exchange fails
Expand All @@ -344,6 +358,9 @@ def decorator(func: Callable) -> Callable:

new_sig = original_sig.replace(parameters=new_params)

# mcp.server.fastmcp always run in async mode
is_async_func = inspect.iscoroutinefunction(func)

@wraps(func)
async def wrapper(*args, **kwargs) -> Any:
try:
Expand Down Expand Up @@ -407,7 +424,10 @@ async def wrapper(*args, **kwargs) -> Any:
if access_ctx_param_name:
kwargs[access_ctx_param_name] = access_ctx

return await func(*args, **kwargs)
if is_async_func:
return await func(*args, **kwargs)
else:
return func(*args, **kwargs)

except Exception as e:
return {
Expand Down
21 changes: 20 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.