diff --git a/README.md b/README.md
index e144942e..d7e3aea1 100644
--- a/README.md
+++ b/README.md
@@ -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)
@@ -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
### 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.
-### Configure Your prefered Identity Provider
+### Configure Your Preferred Identity Provider
@@ -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
@@ -115,7 +115,7 @@ pip install uvicorn
```
```bash
-uvicorn server:app
+python -m uvicorn server:app
```
### Authenticate in client
@@ -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.
+
+
+
+#### Configure protected resource
+
+Configure a protected resource, for example the Google Drive API.
+
+
+
+#### 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.
+
+
+
+#### Give the MCP server identity secret
+
+In order for the MCP server to securely perform exchanges, it requires an identity secret.
+
+
+
+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
+
+
+
## Overview
diff --git a/docs/images/cursor_delegated_access_example.png b/docs/images/cursor_delegated_access_example.png
new file mode 100644
index 00000000..d65111e3
Binary files /dev/null and b/docs/images/cursor_delegated_access_example.png differ
diff --git a/docs/images/keycard_credential_provider_config.png b/docs/images/keycard_credential_provider_config.png
new file mode 100644
index 00000000..598f51b7
Binary files /dev/null and b/docs/images/keycard_credential_provider_config.png differ
diff --git a/docs/images/keycard_identity_configuration.png b/docs/images/keycard_identity_configuration.png
new file mode 100644
index 00000000..60445eb2
Binary files /dev/null and b/docs/images/keycard_identity_configuration.png differ
diff --git a/docs/images/keycard_resource_create.png b/docs/images/keycard_resource_create.png
new file mode 100644
index 00000000..41b107be
Binary files /dev/null and b/docs/images/keycard_resource_create.png differ
diff --git a/docs/images/keycard_set_dependency.png b/docs/images/keycard_set_dependency.png
new file mode 100644
index 00000000..3ff07c03
Binary files /dev/null and b/docs/images/keycard_set_dependency.png differ
diff --git a/packages/mcp/README.md b/packages/mcp/README.md
index ae12fc14..cb641dbe 100644
--- a/packages/mcp/README.md
+++ b/packages/mcp/README.md
@@ -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
@@ -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
diff --git a/packages/mcp/src/keycardai/mcp/server/auth/provider.py b/packages/mcp/src/keycardai/mcp/server/auth/provider.py
index 2f39ed5a..cec4bb7f 100644
--- a/packages/mcp/src/keycardai/mcp/server/auth/provider.py
+++ b/packages/mcp/src/keycardai/mcp/server/auth/provider.py
@@ -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}"}
@@ -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
@@ -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:
@@ -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 {
diff --git a/uv.lock b/uv.lock
index e64f7626..58bb6032 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,5 @@
version = 1
-revision = 3
+revision = 2
requires-python = ">=3.10"
[manifest]
@@ -7,6 +7,7 @@ members = [
"keycardai",
"keycardai-mcp",
"keycardai-mcp-fastmcp",
+ "keycardai-mcp-slack",
"keycardai-oauth",
]
@@ -869,6 +870,24 @@ requires-dist = [
{ name = "pydantic-settings", specifier = ">=2.7.1" },
]
+[[package]]
+name = "keycardai-mcp-slack"
+source = { editable = "packages/mcp-slack" }
+dependencies = [
+ { name = "httpx" },
+ { name = "keycardai-oauth" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "httpx", specifier = ">=0.27.2" },
+ { name = "keycardai-oauth", editable = "packages/oauth" },
+ { name = "pydantic", specifier = ">=2.11.7" },
+ { name = "pydantic-settings", specifier = ">=2.7.1" },
+]
+
[[package]]
name = "keycardai-oauth"
source = { editable = "packages/oauth" }