diff --git a/.gitignore b/.gitignore
index 7bbea59d0c..0c636d5a03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,6 +70,7 @@ dmypy.json
/PLAN.md
/TODO.md
/STATUS.md
+plans/
# Common FastMCP test files
/test.py
diff --git a/docs/integrations/authkit.mdx b/docs/integrations/authkit.mdx
index d16a4db3b4..e99f783516 100644
--- a/docs/integrations/authkit.mdx
+++ b/docs/integrations/authkit.mdx
@@ -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.
+
+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.
+
## Configuration
### Prerequisites
@@ -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:
@@ -93,4 +98,4 @@ auth = AuthKitProvider(
)
mcp = FastMCP(name="AuthKit Secured App", auth=auth)
-```
\ No newline at end of file
+```
diff --git a/docs/integrations/supabase.mdx b/docs/integrations/supabase.mdx
index 68b23c89dc..9ffda444d4 100644
--- a/docs/integrations/supabase.mdx
+++ b/docs/integrations/supabase.mdx
@@ -11,21 +11,43 @@ 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.
+
+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.
+
+
+## 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`:
@@ -33,12 +55,9 @@ Create your FastMCP server using the `SupabaseProvider`:
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)
@@ -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:
@@ -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)
@@ -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
@@ -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)
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx b/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
index a36e35e427..d1908f9cd9 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
@@ -44,7 +44,7 @@ https://docs.scalekit.com/mcp/overview/
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx b/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx
index 7210566bfd..5772a4fa3f 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx
@@ -50,7 +50,7 @@ https://supabase.com/docs/guides/auth/jwts
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
index afd05187f3..28fb9c0868 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
@@ -85,7 +85,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/examples/auth/authkit_dcr/README.md b/examples/auth/authkit_dcr/README.md
index 884f59ac1e..8082461996 100644
--- a/examples/auth/authkit_dcr/README.md
+++ b/examples/auth/authkit_dcr/README.md
@@ -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:
diff --git a/examples/auth/authkit_dcr/client.py b/examples/auth/authkit_dcr/client.py
index 5f1f39bb2f..562637bec1 100644
--- a/examples/auth/authkit_dcr/client.py
+++ b/examples/auth/authkit_dcr/client.py
@@ -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__":
diff --git a/examples/auth/authkit_dcr/server.py b/examples/auth/authkit_dcr/server.py
index c05a676d80..8974376d2b 100644
--- a/examples/auth/authkit_dcr/server.py
+++ b/examples/auth/authkit_dcr/server.py
@@ -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 "",
base_url="http://localhost:8000",
)
diff --git a/examples/auth/github_oauth/client.py b/examples/auth/github_oauth/client.py
index a7ab5c47ee..7158583bc6 100644
--- a/examples/auth/github_oauth/client.py
+++ b/examples/auth/github_oauth/client.py
@@ -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!")
diff --git a/src/fastmcp/server/auth/providers/scalekit.py b/src/fastmcp/server/auth/providers/scalekit.py
index e434e95d9c..d55ec14fd7 100644
--- a/src/fastmcp/server/auth/providers/scalekit.py
+++ b/src/fastmcp/server/auth/providers/scalekit.py
@@ -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:
diff --git a/src/fastmcp/server/auth/providers/supabase.py b/src/fastmcp/server/auth/providers/supabase.py
index 513c025558..b6253e46c2 100644
--- a/src/fastmcp/server/auth/providers/supabase.py
+++ b/src/fastmcp/server/auth/providers/supabase.py
@@ -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,
)
diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py
index f34f770293..4354d405bd 100644
--- a/src/fastmcp/server/auth/providers/workos.py
+++ b/src/fastmcp/server/auth/providers/workos.py
@@ -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,
):
@@ -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
"""
@@ -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,
)
diff --git a/tests/server/auth/providers/test_scalekit.py b/tests/server/auth/providers/test_scalekit.py
index b112e1c68f..a47840682f 100644
--- a/tests/server/auth/providers/test_scalekit.py
+++ b/tests/server/auth/providers/test_scalekit.py
@@ -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."""