Skip to content

Commit 043e2ee

Browse files
committed
feat(keycardai-mcp-fastmcp): automatic app cred discovery
1 parent 3945e56 commit 043e2ee

3 files changed

Lines changed: 421 additions & 6 deletions

File tree

packages/mcp-fastmcp/README.md

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,13 +252,28 @@ auth_provider = AuthProvider(
252252
# So your Keycard Resource must be configured as: http://localhost:8000/
253253
```
254254

255-
### Client Credentials for Token Exchange
255+
### Application Credentials for Token Exchange
256256

257-
To enable token exchange (required for the `@grant` decorator), provide application credentials:
257+
To enable token exchange (required for the `@grant` decorator), you need to configure application credentials. The SDK supports multiple credential types and provides automatic discovery via environment variables.
258+
259+
#### Credential Types
260+
261+
The SDK supports three types of application credentials:
262+
263+
1. **ClientSecret** - OAuth client credentials (client_id/client_secret) issued by Keycard
264+
2. **WebIdentity** - Private key JWT authentication for MCP servers
265+
3. **EKSWorkloadIdentity** - AWS EKS Pod Identity for Kubernetes deployments
266+
267+
#### Configuration Methods
268+
269+
##### 1. Explicit Configuration (Recommended for Production)
270+
271+
Explicitly provide credentials when creating the `AuthProvider`:
258272

259273
```python
260-
from keycardai.mcp.integrations.fastmcp import ClientSecret
274+
from keycardai.mcp.integrations.fastmcp import AuthProvider, ClientSecret
261275

276+
# Client Secret credentials
262277
auth_provider = AuthProvider(
263278
zone_id="your-zone-id",
264279
mcp_server_name="My FastMCP Service",
@@ -267,6 +282,121 @@ auth_provider = AuthProvider(
267282
)
268283
```
269284

285+
```python
286+
from keycardai.mcp.integrations.fastmcp import AuthProvider, WebIdentity
287+
288+
# Web Identity (Private Key JWT)
289+
auth_provider = AuthProvider(
290+
zone_id="your-zone-id",
291+
mcp_server_name="My FastMCP Service",
292+
mcp_base_url="http://localhost:8000/",
293+
application_credential=WebIdentity(
294+
mcp_server_name="My FastMCP Service",
295+
storage_dir="./mcp_keys" # Directory for key storage
296+
)
297+
)
298+
```
299+
300+
```python
301+
from keycardai.mcp.integrations.fastmcp import AuthProvider, EKSWorkloadIdentity
302+
303+
# EKS Workload Identity
304+
auth_provider = AuthProvider(
305+
zone_id="your-zone-id",
306+
mcp_server_name="My FastMCP Service",
307+
mcp_base_url="http://localhost:8000/",
308+
application_credential=EKSWorkloadIdentity()
309+
)
310+
```
311+
312+
##### 2. Environment Variable Discovery (Convenient for Development)
313+
314+
The SDK automatically discovers credentials from environment variables, making it easy to configure without code changes:
315+
316+
**Option A: Client Credentials**
317+
```bash
318+
export KEYCARD_CLIENT_ID="your_client_id"
319+
export KEYCARD_CLIENT_SECRET="your_client_secret"
320+
```
321+
322+
**Option B: Explicit Credential Type**
323+
```bash
324+
# For EKS Workload Identity
325+
export KEYCARD_APPLICATION_CREDENTIAL_TYPE="eks_workload_identity"
326+
export AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE="/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
327+
328+
# For Web Identity
329+
export KEYCARD_APPLICATION_CREDENTIAL_TYPE="web_identity"
330+
export KEYCARD_WEB_IDENTITY_KEY_STORAGE_DIR="./mcp_keys" # Optional: defaults to "./mcp_keys"
331+
```
332+
333+
**Option C: Automatic EKS Detection**
334+
```bash
335+
# SDK automatically detects EKS when this environment variable is present
336+
export AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE="/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
337+
```
338+
339+
With environment variables configured, you can create the `AuthProvider` without explicit credentials:
340+
341+
```python
342+
from keycardai.mcp.integrations.fastmcp import AuthProvider
343+
344+
# Credentials automatically discovered from environment variables
345+
auth_provider = AuthProvider(
346+
zone_id="your-zone-id",
347+
mcp_server_name="My FastMCP Service",
348+
mcp_base_url="http://localhost:8000/"
349+
)
350+
```
351+
352+
#### Configuration Precedence
353+
354+
When multiple configuration methods are present, the SDK follows this precedence order (highest to lowest):
355+
356+
1. **Explicit `application_credential` parameter** - Always takes priority
357+
2. **`KEYCARD_CLIENT_ID` + `KEYCARD_CLIENT_SECRET`** - Client credentials via environment
358+
3. **`KEYCARD_APPLICATION_CREDENTIAL_TYPE`** - Explicit credential type selection
359+
4. **`AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE`** - Automatic EKS detection
360+
5. **None** - No credentials configured (token exchange disabled)
361+
362+
**Example:**
363+
```python
364+
# Even with environment variables set, explicit parameter takes priority
365+
import os
366+
os.environ["KEYCARD_CLIENT_ID"] = "env_client_id"
367+
os.environ["KEYCARD_CLIENT_SECRET"] = "env_secret"
368+
369+
auth_provider = AuthProvider(
370+
zone_id="your-zone-id",
371+
mcp_server_name="My FastMCP Service",
372+
mcp_base_url="http://localhost:8000/",
373+
# This takes priority over environment variables
374+
application_credential=WebIdentity(mcp_server_name="My FastMCP Service")
375+
)
376+
```
377+
378+
#### Supported Credential Types via Environment Variables
379+
380+
| Environment Variable | Value | Resulting Credential Type |
381+
|---------------------|-------|---------------------------|
382+
| `KEYCARD_APPLICATION_CREDENTIAL_TYPE` | `"eks_workload_identity"` | `EKSWorkloadIdentity` |
383+
| `KEYCARD_APPLICATION_CREDENTIAL_TYPE` | `"web_identity"` | `WebIdentity` |
384+
| `KEYCARD_APPLICATION_CREDENTIAL_TYPE` | `"unknown_type"` | ❌ Raises `AuthProviderConfigurationError` |
385+
386+
#### Additional Environment Variables
387+
388+
| Environment Variable | Purpose | Used By | Default Value |
389+
|---------------------|---------|---------|---------------|
390+
| `KEYCARD_CLIENT_ID` | OAuth client identifier | `ClientSecret` | None |
391+
| `KEYCARD_CLIENT_SECRET` | OAuth client secret | `ClientSecret` | None |
392+
| `KEYCARD_APPLICATION_CREDENTIAL_TYPE` | Explicit credential type selection | All | None |
393+
| `KEYCARD_WEB_IDENTITY_KEY_STORAGE_DIR` | Directory for private key storage | `WebIdentity` | `"./mcp_keys"` |
394+
| `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` | Path to EKS token file | `EKSWorkloadIdentity` | None |
395+
396+
#### Running Without Application Credentials
397+
398+
If no application credentials are configured, the `AuthProvider` will work for basic authentication but the `@grant` decorator will be unable to perform token exchange. This is useful for MCP servers that only need user authentication without delegated access to external resources.
399+
270400
## Testing
271401

272402
This section provides comprehensive guidance on testing your FastMCP servers that use Keycard authentication. The examples show how to use the `mock_access_context` utility to easily mock authentication without needing to understand the internal SDK implementation.

packages/mcp-fastmcp/src/keycardai/mcp/integrations/fastmcp/provider.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from __future__ import annotations
1010

1111
import inspect
12+
import os
1213
from collections.abc import Callable
1314
from functools import wraps
1415
from typing import Any
@@ -19,7 +20,12 @@
1920
from fastmcp.server.auth import RemoteAuthProvider
2021
from fastmcp.server.auth.providers.jwt import JWTVerifier
2122
from fastmcp.server.dependencies import get_access_token
22-
from keycardai.mcp.server.auth import ApplicationCredential
23+
from keycardai.mcp.server.auth import (
24+
ApplicationCredential,
25+
ClientSecret,
26+
EKSWorkloadIdentity,
27+
WebIdentity,
28+
)
2329
from keycardai.mcp.server.auth.client_factory import ClientFactory, DefaultClientFactory
2430
from keycardai.mcp.server.exceptions import (
2531
AuthProviderConfigurationError,
@@ -265,7 +271,7 @@ def __init__(
265271
self._is_custom_factory = client_factory is not None
266272

267273
# Initialize application credential provider
268-
self.application_credential = application_credential
274+
self.application_credential = self._discover_application_credential(application_credential)
269275

270276
# Get the auth strategy for the HTTP client doing the token exchange
271277
if self.application_credential is not None:
@@ -288,6 +294,45 @@ def __init__(
288294
zone_url=self.zone_url,
289295
) from e
290296

297+
def _discover_application_credential(self, application_credential: ApplicationCredential | None) -> ApplicationCredential | None:
298+
"""Discover the application credential from the provided parameters.
299+
300+
Args:
301+
application_credential: Application credential to discover
302+
303+
Returns:
304+
ApplicationCredential: The discovered application credential
305+
"""
306+
if application_credential is not None:
307+
return application_credential
308+
309+
# discover environment variables
310+
client_id = os.getenv("KEYCARD_CLIENT_ID")
311+
client_secret = os.getenv("KEYCARD_CLIENT_SECRET")
312+
if client_id and client_secret:
313+
return ClientSecret((client_id, client_secret))
314+
315+
application_credential_type = os.getenv("KEYCARD_APPLICATION_CREDENTIAL_TYPE")
316+
if application_credential_type == "eks_workload_identity":
317+
return EKSWorkloadIdentity()
318+
elif application_credential_type == "web_identity":
319+
key_storage_dir = os.getenv("KEYCARD_WEB_IDENTITY_KEY_STORAGE_DIR")
320+
return WebIdentity(
321+
mcp_server_name=self.mcp_server_name,
322+
storage_dir=key_storage_dir,
323+
)
324+
elif application_credential_type is not None:
325+
raise AuthProviderConfigurationError(
326+
message=f"Unknown application credential type: {application_credential_type}. Supported types: eks_workload_identity, web_identity"
327+
)
328+
329+
# detect workload identity from environment variables
330+
workload_identity_token = os.getenv(EKSWorkloadIdentity.default_env_var_name)
331+
if workload_identity_token:
332+
return EKSWorkloadIdentity()
333+
334+
return None
335+
291336
def _handle_client_creation_error(self, auth, exception: Exception | None = None) -> None:
292337
"""Handle client creation errors with appropriate exception type.
293338

0 commit comments

Comments
 (0)