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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ dmypy.json
/PLAN.md
/TODO.md
/STATUS.md
plans/

# Common FastMCP test files
/test.py
Expand Down
15 changes: 10 additions & 5 deletions docs/integrations/authkit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { VersionBadge } from "/snippets/version-badge.mdx"

This guide shows you how to secure your FastMCP server using WorkOS's **AuthKit**, a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where AuthKit handles user login and your FastMCP server validates the tokens.

<Warning>
AuthKit does not currently support [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators, so FastMCP cannot validate that tokens were issued for the specific resource server. If you need resource-specific audience validation, consider using [WorkOSProvider](/integrations/workos) (OAuth proxy pattern) instead.
</Warning>

## Configuration
### Prerequisites
Expand Down Expand Up @@ -62,21 +65,23 @@ To test your server, you can use the `fastmcp` CLI to run it locally. Assuming y
fastmcp run server.py --transport http --port 8000
```

Now, you can use a FastMCP client to test that you can reach your server after authenticating:
AuthKit defaults DCR clients to `client_secret_basic` for token exchange, which conflicts with how some MCP clients send credentials. To avoid token exchange errors, register as a public client by setting `token_endpoint_auth_method` to `"none"`:

```python
```python client.py
from fastmcp import Client
from fastmcp.client.auth import OAuth
import asyncio

auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})

async def main():
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
async with Client("http://localhost:8000/mcp", auth=auth) as client:
assert await client.ping()

if __name__ == "__main__":
asyncio.run(main())
```


## Production Configuration

For production deployments, load sensitive configuration from environment variables:
Expand All @@ -93,4 +98,4 @@ auth = AuthKitProvider(
)

mcp = FastMCP(name="AuthKit Secured App", auth=auth)
```
```
46 changes: 29 additions & 17 deletions docs/integrations/supabase.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,53 @@ import { VersionBadge } from "/snippets/version-badge.mdx"

This guide shows you how to secure your FastMCP server using **Supabase Auth**. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where Supabase handles user authentication and your FastMCP server validates the tokens.

<Warning>
Supabase Auth does not currently support [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators, so FastMCP cannot validate that tokens were issued for the specific resource server.
</Warning>

## Consent UI Requirement

Supabase's OAuth Server delegates the user consent screen to your application. When an MCP client initiates authorization, Supabase authenticates the user and then redirects to your application at a configured callback URL (e.g., `https://your-app.com/oauth/callback?authorization_id=...`). Your application must host a page that calls Supabase's `approveAuthorization()` or `denyAuthorization()` APIs to complete the flow.

`SupabaseProvider` handles the resource server side (token verification and metadata), but you are responsible for building and hosting the consent UI separately. See [Supabase's OAuth Server documentation](https://supabase.com/docs/guides/auth/oauth-server/getting-started) for details on implementing the authorization page.

## Configuration

### Prerequisites

Before you begin, you will need:
1. A **[Supabase Account](https://supabase.com/)** with a project or a self-hosted **Supabase Auth** instance
2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
2. **OAuth Server enabled** in your Supabase Dashboard (Authentication → OAuth Server)
3. **Dynamic Client Registration enabled** in the same settings
4. A **consent UI** hosted at your configured authorization path (see above)
5. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)

### Step 1: Get Supabase Project URL
### Step 1: Enable Supabase OAuth Server

In your Supabase Dashboard:
1. Go to **Authentication → OAuth Server**
2. Enable the **OAuth Server**
3. Set your **Site URL** to where your consent UI is hosted
4. Set the **Authorization Path** (e.g., `/oauth/callback`)
5. Enable **Allow Dynamic OAuth Apps** for MCP client registration

### Step 2: Get Supabase Project URL

In your Supabase Dashboard:
1. Go to **Project Settings**
2. Copy your **Project URL** (e.g., `https://abc123.supabase.co`)

### Step 2: FastMCP Configuration
### Step 3: FastMCP Configuration

Create your FastMCP server using the `SupabaseProvider`:

```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.supabase import SupabaseProvider

# Configure Supabase Auth
auth = SupabaseProvider(
project_url="https://abc123.supabase.co",
base_url="http://localhost:8000",
# Optional: customize auth_route for self-hosted Supabase Auth with custom routes
# auth_route="/my/auth/route"
)

mcp = FastMCP("Supabase Protected Server", auth=auth)
Expand All @@ -62,8 +81,6 @@ Start your FastMCP server with HTTP transport to enable OAuth flows:
fastmcp run server.py --transport http --port 8000
```

Your server is now running and protected by Supabase authentication.

### Testing with a Client

Create a test client that authenticates with your Supabase-protected server:
Expand All @@ -73,12 +90,9 @@ from fastmcp import Client
import asyncio

async def main():
# The client will automatically handle Supabase OAuth
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
# First-time connection will open Supabase login in your browser
print("✓ Authenticated with Supabase!")
print("Authenticated with Supabase!")

# Test the protected tool
result = await client.call_tool("protected_tool", {"message": "Hello!"})
print(result)

Expand All @@ -87,9 +101,9 @@ if __name__ == "__main__":
```

When you run the client for the first time:
1. Your browser will open to Supabase's authorization page
2. After you authorize, you'll be redirected back
3. The client receives the token and can make authenticated requests
1. Your browser will open to Supabase's authorization endpoint
2. After authenticating, Supabase redirects to your consent UI
3. After you approve, the client receives the token and can make authenticated requests

## Production Configuration

Expand All @@ -100,11 +114,9 @@ import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.supabase import SupabaseProvider

# Load configuration from environment variables
auth = SupabaseProvider(
project_url=os.environ["SUPABASE_PROJECT_URL"],
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
auth_route=os.environ.get("SUPABASE_AUTH_ROUTE", "/auth/v1"), # Optional: for custom routes
)

mcp = FastMCP(name="Supabase Secured App", auth=auth)
Expand Down
2 changes: 1 addition & 1 deletion docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ https://docs.scalekit.com/mcp/overview/

**Methods:**

#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/scalekit.py#L144" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/scalekit.py#L145" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
Expand Down
2 changes: 1 addition & 1 deletion docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ https://supabase.com/docs/guides/auth/jwts

**Methods:**

#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/supabase.py#L120" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/supabase.py#L126" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Manual edit to auto-generated file will be overwritten

docs/python-sdk/** is updated automatically by a bot. This anchor correction will be overwritten on the next bot commit. The fix should emerge naturally from the source code change, not be patched here manually.

As per coding guidelines, "Do not manually modify docs/python-sdk/** - a bot automatically updates these files via commits added to PRs."


```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
Expand Down
2 changes: 1 addition & 1 deletion docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification

**Methods:**

#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L290" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L301" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
Expand Down
2 changes: 1 addition & 1 deletion examples/auth/authkit_dcr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Demonstrates FastMCP server protection with AuthKit Dynamic Client Registration.
1. Set your AuthKit domain:

```bash
export FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN="https://your-app.authkit.app"
export AUTHKIT_DOMAIN="https://your-app.authkit.app"
```

2. Run the server:
Expand Down
25 changes: 13 additions & 12 deletions examples/auth/authkit_dcr/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,24 @@
import asyncio

from fastmcp.client import Client
from fastmcp.client.auth import OAuth

SERVER_URL = "http://127.0.0.1:8000/mcp"


async def main():
try:
async with Client(SERVER_URL, auth="oauth") as client:
assert await client.ping()
print("✅ Successfully authenticated!")

tools = await client.list_tools()
print(f"🔧 Available tools ({len(tools)}):")
for tool in tools:
print(f" - {tool.name}: {tool.description}")
except Exception as e:
print(f"❌ Authentication failed: {e}")
raise
# AuthKit defaults DCR clients to client_secret_basic, which conflicts
# with how MCP SDKs send credentials. Force "none" to register as a
# public client and avoid token exchange errors.
auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})
async with Client(SERVER_URL, auth=auth) as client:
assert await client.ping()
print("Successfully authenticated!")

tools = await client.list_tools()
print(f"Available tools ({len(tools)}):")
for tool in tools:
print(f" - {tool.name}: {tool.description}")


if __name__ == "__main__":
Expand Down
3 changes: 1 addition & 2 deletions examples/auth/authkit_dcr/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
from fastmcp.server.auth.providers.workos import AuthKitProvider

auth = AuthKitProvider(
authkit_domain=os.getenv("FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN")
or "",
authkit_domain=os.getenv("AUTHKIT_DOMAIN") or "",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep AuthKit env var name consistent with example docs

The server example now reads AUTHKIT_DOMAIN, but both the example README (examples/auth/authkit_dcr/README.md) and this file’s own header still instruct users to export FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN; following those instructions leaves authkit_domain empty at runtime and the example fails to start. Please either keep reading the documented variable (or both names for compatibility) or update the accompanying docs in the same change.

Useful? React with 👍 / 👎.

base_url="http://localhost:8000",
)

Expand Down
8 changes: 1 addition & 7 deletions examples/auth/github_oauth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,7 @@

async def main():
try:
async with Client(
SERVER_URL,
auth=OAuth(
# Replace with your own CIMD document URL
client_metadata_url="https://www.jlowin.dev/mcp-client.json",
),
) as client:
async with Client(SERVER_URL, auth=OAuth()) as client:
assert await client.ping()
print("✅ Successfully authenticated!")

Expand Down
1 change: 1 addition & 0 deletions src/fastmcp/server/auth/providers/scalekit.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def __init__(
jwks_uri=f"{self.environment_url}/keys",
issuer=self.environment_url,
algorithm="RS256",
audience=self.resource_id,
required_scopes=self.required_scopes or None,
)
else:
Expand Down
6 changes: 6 additions & 0 deletions src/fastmcp/server/auth/providers/supabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,16 @@ def __init__(

# Create default JWT verifier if none provided
if token_verifier is None:
logger.warning(
"SupabaseProvider cannot validate token audience for the specific resource "
"because Supabase Auth does not support RFC 8707 resource indicators. "
"This may leave the server vulnerable to cross-server token replay."
)
token_verifier = JWTVerifier(
jwks_uri=f"{self.project_url}/{self.auth_route}/.well-known/jwks.json",
issuer=f"{self.project_url}/{self.auth_route}",
algorithm=algorithm,
audience="authenticated",
required_scopes=parsed_scopes,
)

Expand Down
11 changes: 11 additions & 0 deletions src/fastmcp/server/auth/providers/workos.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def __init__(
*,
authkit_domain: AnyHttpUrl | str,
base_url: AnyHttpUrl | str,
client_id: str | None = None,
required_scopes: list[str] | None = None,
token_verifier: TokenVerifier | None = None,
):
Expand All @@ -260,6 +261,9 @@ def __init__(
Args:
authkit_domain: Your AuthKit domain (e.g., "https://your-app.authkit.app")
base_url: Public URL of this FastMCP server
client_id: Your WorkOS project client ID (e.g., "client_01ABC..."). Used to
validate the JWT audience claim. Found in your WorkOS Dashboard under
API Keys. This is the project-level client ID, not individual MCP client IDs.
required_scopes: Optional list of scopes to require for all requests
token_verifier: Optional token verifier. If None, creates JWT verifier for AuthKit
"""
Expand All @@ -273,10 +277,17 @@ def __init__(

# Create default JWT verifier if none provided
if token_verifier is None:
logger.warning(
"AuthKitProvider cannot validate token audience for the specific resource "
"because AuthKit does not support RFC 8707 resource indicators. "
"This may leave the server vulnerable to cross-server token replay. "
"Consider using WorkOSProvider (OAuth proxy) for audience-bound tokens."
)
token_verifier = JWTVerifier(
jwks_uri=f"{self.authkit_domain}/oauth2/jwks",
issuer=self.authkit_domain,
algorithm="RS256",
audience=client_id,
required_scopes=parsed_scopes,
)

Expand Down
2 changes: 1 addition & 1 deletion tests/server/auth/providers/test_scalekit.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def test_jwt_verifier_configured_correctly(self):
assert isinstance(provider.token_verifier, JWTVerifier)
assert provider.token_verifier.jwks_uri == "https://my-env.scalekit.com/keys"
assert provider.token_verifier.issuer == "https://my-env.scalekit.com"
assert provider.token_verifier.audience is None
assert provider.token_verifier.audience == "sk_resource_456"

def test_required_scopes_hooks_into_verifier(self):
"""Token verifier should enforce required scopes when provided."""
Expand Down