diff --git a/docs/deployment/http.mdx b/docs/deployment/http.mdx
index 54132eb0e..8400b39c2 100644
--- a/docs/deployment/http.mdx
+++ b/docs/deployment/http.mdx
@@ -478,7 +478,7 @@ When mounting an OAuth-protected server under a path prefix, declare your URLs u
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from starlette.applications import Starlette
from starlette.routing import Mount
@@ -540,7 +540,7 @@ Here's a complete working example showing all the pieces together:
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from starlette.applications import Starlette
from starlette.routing import Mount
import uvicorn
diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx
index 476349a01..8d54a592f 100644
--- a/docs/development/v3-notes/v3-features.mdx
+++ b/docs/development/v3-notes/v3-features.mdx
@@ -97,7 +97,7 @@ async def my_tool(
For Azure/Entra, the new `fastmcp[azure]` extra adds `EntraOBOToken`, which handles the On-Behalf-Of token exchange declaratively:
```python
-from fastmcp.server.auth.providers.azure import EntraOBOToken
+from fastmcp.server.plugins.auth.azure.provider import EntraOBOToken
@mcp.tool()
async def get_emails(
diff --git a/docs/getting-started/upgrading/from-fastmcp-2.mdx b/docs/getting-started/upgrading/from-fastmcp-2.mdx
index 1e659e76a..4b461aa5d 100644
--- a/docs/getting-started/upgrading/from-fastmcp-2.mdx
+++ b/docs/getting-started/upgrading/from-fastmcp-2.mdx
@@ -252,7 +252,7 @@ auth = GitHubProvider()
# After (v3) — pass values explicitly
import os
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id=os.environ["GITHUB_CLIENT_ID"],
diff --git a/docs/integrations/auth0.mdx b/docs/integrations/auth0.mdx
index 65f9d3873..bf66feee5 100644
--- a/docs/integrations/auth0.mdx
+++ b/docs/integrations/auth0.mdx
@@ -47,7 +47,7 @@ Create an Application in your Auth0 settings to get the credentials needed for a
- If you want to use a custom callback path (e.g., `/auth/auth0/callback`), make sure to set the same path in both your Auth0 Application settings and the `redirect_path` parameter when configuring the Auth0Provider.
+ If you want to use a custom callback path (e.g., `/auth/auth0/callback`), make sure to set the same path in both your Auth0 Application settings and the `redirect_path` parameter when configuring the Auth0 plugin.
@@ -76,23 +76,25 @@ Create an Application in your Auth0 settings to get the credentials needed for a
### Step 2: FastMCP Configuration
-Create your FastMCP server using the `Auth0Provider`.
+Create your FastMCP server using the `Auth0Auth` plugin.
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.auth0 import Auth0Provider
+from fastmcp.server.plugins.auth.auth0 import Auth0Auth
-# The Auth0Provider utilizes Auth0 OIDC configuration
-auth_provider = Auth0Provider(
- config_url="https://.../.well-known/openid-configuration", # Your Auth0 configuration URL
- client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB", # Your Auth0 application Client ID
- client_secret="vPYqbjemq...", # Your Auth0 application Client Secret
- audience="https://...", # Your Auth0 API audience
- base_url="http://localhost:8000", # Must match your application configuration
- # redirect_path="/auth/callback" # Default value, customize if needed
+# The Auth0 plugin utilizes Auth0 OIDC configuration
+auth_plugin = Auth0Auth(
+ Auth0Auth.Config(
+ config_url="https://.../.well-known/openid-configuration", # Your Auth0 configuration URL
+ client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB", # Your Auth0 application Client ID
+ client_secret="vPYqbjemq...", # Your Auth0 application Client Secret
+ audience="https://...", # Your Auth0 API audience
+ base_url="http://localhost:8000", # Must match your application configuration
+ # redirect_path="/auth/callback" # Default value, customize if needed
+ )
)
-mcp = FastMCP(name="Auth0 Secured App", auth=auth_provider)
+mcp = FastMCP(name="Auth0 Secured App", plugins=[auth_plugin])
# Add a protected tool to test authentication
@mcp.tool
@@ -157,21 +159,21 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.auth0 import Auth0Provider
+from fastmcp.server.plugins.auth.auth0 import Auth0Auth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
-auth_provider = Auth0Provider(
- config_url="https://.../.well-known/openid-configuration",
- client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB",
- client_secret="vPYqbjemq...",
- audience="https://...",
- base_url="https://your-production-domain.com",
-
- # Production token management
- jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+auth_plugin = Auth0Auth(
+ Auth0Auth.Config(
+ config_url="https://.../.well-known/openid-configuration",
+ client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB",
+ client_secret="vPYqbjemq...",
+ audience="https://...",
+ base_url="https://your-production-domain.com",
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ ),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@@ -181,7 +183,7 @@ auth_provider = Auth0Provider(
)
)
-mcp = FastMCP(name="Production Auth0 App", auth=auth_provider)
+mcp = FastMCP(name="Production Auth0 App", plugins=[auth_plugin])
```
diff --git a/docs/integrations/authkit.mdx b/docs/integrations/authkit.mdx
index c77175201..c530779ff 100644
--- a/docs/integrations/authkit.mdx
+++ b/docs/integrations/authkit.mdx
@@ -44,20 +44,22 @@ In the WorkOS Dashboard, go to **Connect → Configuration** and configure:
### Step 2: FastMCP Configuration
-Create your FastMCP server file and use the `AuthKitProvider` to handle all the OAuth integration automatically:
+Create your FastMCP server file and use the `AuthKitAuth` plugin to handle the OAuth integration automatically:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import AuthKitProvider
+from fastmcp.server.plugins.auth.authkit import AuthKitAuth
-# AuthKitProvider automatically discovers WorkOS endpoints, configures JWT
+# AuthKitAuth automatically discovers WorkOS endpoints, configures JWT
# validation, and binds the token audience to this server's resource URL.
-auth_provider = AuthKitProvider(
- authkit_domain="https://your-project-12345.authkit.app",
- base_url="http://127.0.0.1:8000", # Use your actual server URL
+auth_plugin = AuthKitAuth(
+ AuthKitAuth.Config(
+ authkit_domain="https://your-project-12345.authkit.app",
+ base_url="http://127.0.0.1:8000", # Use your actual server URL
+ )
)
-mcp = FastMCP(name="AuthKit Secured App", auth=auth_provider)
+mcp = FastMCP(name="AuthKit Secured App", plugins=[auth_plugin])
```
When the server starts, it logs the resource URL it is validating against. Paste that URL into your Dashboard's **MCP resource indicators** list.
@@ -94,13 +96,15 @@ For production deployments, load sensitive configuration from environment variab
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import AuthKitProvider
+from fastmcp.server.plugins.auth.authkit import AuthKitAuth
# Load configuration from environment variables
-auth = AuthKitProvider(
- authkit_domain=os.environ.get("AUTHKIT_DOMAIN"),
- base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+auth_plugin = AuthKitAuth(
+ AuthKitAuth.Config(
+ authkit_domain=os.environ.get("AUTHKIT_DOMAIN"),
+ base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+ )
)
-mcp = FastMCP(name="AuthKit Secured App", auth=auth)
+mcp = FastMCP(name="AuthKit Secured App", plugins=[auth_plugin])
```
diff --git a/docs/integrations/aws-cognito.mdx b/docs/integrations/aws-cognito.mdx
index b7df29222..a7be4e16e 100644
--- a/docs/integrations/aws-cognito.mdx
+++ b/docs/integrations/aws-cognito.mdx
@@ -116,24 +116,26 @@ Set up AWS Cognito user pool with an app client to get the credentials needed fo
### Step 2: FastMCP Configuration
-Create your FastMCP server using the `AWSCognitoProvider`, which handles AWS Cognito's JWT tokens and user claims automatically:
+Create your FastMCP server using the `AWSCognitoAuth` plugin, which handles AWS Cognito's JWT tokens and user claims automatically:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.aws import AWSCognitoProvider
+from fastmcp.server.plugins.auth.aws import AWSCognitoAuth
from fastmcp.server.dependencies import get_access_token
-# The AWSCognitoProvider handles JWT validation and user claims
-auth_provider = AWSCognitoProvider(
- user_pool_id="eu-central-1_XXXXXXXXX", # Your AWS Cognito user pool ID
- aws_region="eu-central-1", # AWS region (defaults to eu-central-1)
- client_id="your-app-client-id", # Your app client ID
- client_secret="your-app-client-secret", # Your app client Secret
- base_url="http://localhost:8000", # Must match your callback URL
- # redirect_path="/auth/callback" # Default value, customize if needed
+# The AWSCognitoAuth plugin handles JWT validation and user claims
+auth_plugin = AWSCognitoAuth(
+ AWSCognitoAuth.Config(
+ user_pool_id="eu-central-1_XXXXXXXXX", # Your AWS Cognito user pool ID
+ aws_region="eu-central-1", # AWS region (defaults to eu-central-1)
+ client_id="your-app-client-id", # Your app client ID
+ client_secret="your-app-client-secret", # Your app client Secret
+ base_url="http://localhost:8000", # Must match your callback URL
+ # redirect_path="/auth/callback" # Default value, customize if needed
+ )
)
-mcp = FastMCP(name="AWS Cognito Secured App", auth=auth_provider)
+mcp = FastMCP(name="AWS Cognito Secured App", plugins=[auth_plugin])
# Add a protected tool to test authentication
@mcp.tool
@@ -204,21 +206,21 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.aws import AWSCognitoProvider
+from fastmcp.server.plugins.auth.aws import AWSCognitoAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
-auth_provider = AWSCognitoProvider(
- user_pool_id="eu-central-1_XXXXXXXXX",
- aws_region="eu-central-1",
- client_id="your-app-client-id",
- client_secret="your-app-client-secret",
- base_url="https://your-production-domain.com",
-
- # Production token management
- jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+auth_plugin = AWSCognitoAuth(
+ AWSCognitoAuth.Config(
+ user_pool_id="eu-central-1_XXXXXXXXX",
+ aws_region="eu-central-1",
+ client_id="your-app-client-id",
+ client_secret="your-app-client-secret",
+ base_url="https://your-production-domain.com",
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ ),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@@ -228,7 +230,7 @@ auth_provider = AWSCognitoProvider(
)
)
-mcp = FastMCP(name="Production AWS Cognito App", auth=auth_provider)
+mcp = FastMCP(name="Production AWS Cognito App", plugins=[auth_plugin])
```
@@ -275,4 +277,4 @@ Perfect for enterprise environments with:
- **Multi-Factor Authentication (MFA)**: Leverage AWS Cognito's built-in MFA
- **User Groups**: Role-based access control through AWS Cognito groups
- **Custom Attributes**: Access custom user attributes defined in your AWS Cognito user pool
-- **Compliance**: Meet enterprise security and compliance requirements
\ No newline at end of file
+- **Compliance**: Meet enterprise security and compliance requirements
diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx
index 4376a38ce..95bd159a4 100644
--- a/docs/integrations/azure.mdx
+++ b/docs/integrations/azure.mdx
@@ -46,7 +46,7 @@ Create an App registration in Azure Portal to get the credentials needed for aut
- If you want to use a custom callback path (e.g., `/auth/azure/callback`), make sure to set the same path in both your Azure App registration and the `redirect_path` parameter when configuring the AzureProvider.
+ If you want to use a custom callback path (e.g., `/auth/azure/callback`), make sure to set the same path in both your Azure App registration and the `redirect_path` parameter when configuring the Azure plugin.
- **Expose an API**: Configure your Application ID URI and define scopes
@@ -74,7 +74,7 @@ Create an App registration in Azure Portal to get the credentials needed for aut
- In FastMCP's `AzureProvider`, set `identifier_uri` to your Application ID URI (optional; defaults to `api://{client_id}`) and set `required_scopes` to the unprefixed scope names (e.g., `read`, `write`). During authorization, FastMCP automatically prefixes scopes with your `identifier_uri`.
+ In FastMCP's Azure plugin, set `identifier_uri` to your Application ID URI (optional; defaults to `api://{client_id}`) and set `required_scopes` to the unprefixed scope names (e.g., `read`, `write`). During authorization, FastMCP automatically prefixes scopes with your `identifier_uri`.
@@ -109,28 +109,30 @@ Create an App registration in Azure Portal to get the credentials needed for aut
### Step 2: FastMCP Configuration
-Create your FastMCP server using the `AzureProvider`, which handles Azure's OAuth flow automatically:
+Create your FastMCP server using the `AzureAuth` plugin, which handles Azure's OAuth flow automatically:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.azure import AzureProvider
+from fastmcp.server.plugins.auth.azure import AzureAuth
-# The AzureProvider handles Azure's token format and validation
-auth_provider = AzureProvider(
- client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149", # Your Azure App Client ID
- client_secret="your-client-secret", # Your Azure App Client Secret
- tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5", # Your Azure Tenant ID (REQUIRED)
- base_url="http://localhost:8000", # Must match your App registration
- required_scopes=["your-scope"], # At least one scope REQUIRED - name of scope from your App
- # identifier_uri defaults to api://{client_id}
- # identifier_uri="api://your-api-id",
- # Optional: request additional upstream scopes in the authorize request
- # additional_authorize_scopes=["User.Read", "openid", "email"],
- # redirect_path="/auth/callback" # Default value, customize if needed
- # base_authority="login.microsoftonline.us" # For Azure Government (default: login.microsoftonline.com)
+# The AzureAuth plugin handles Azure's token format and validation
+auth_plugin = AzureAuth(
+ AzureAuth.Config(
+ client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149", # Your Azure App Client ID
+ client_secret="your-client-secret", # Your Azure App Client Secret
+ tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5", # Your Azure Tenant ID (REQUIRED)
+ base_url="http://localhost:8000", # Must match your App registration
+ required_scopes=["your-scope"], # At least one scope REQUIRED - name of scope from your App
+ # identifier_uri defaults to api://{client_id}
+ # identifier_uri="api://your-api-id",
+ # Optional: request additional upstream scopes in the authorize request
+ # additional_authorize_scopes=["User.Read", "openid", "email"],
+ # redirect_path="/auth/callback" # Default value, customize if needed
+ # base_authority="login.microsoftonline.us" # For Azure Government (default: login.microsoftonline.com)
+ )
)
-mcp = FastMCP(name="Azure Secured App", auth=auth_provider)
+mcp = FastMCP(name="Azure Secured App", plugins=[auth_plugin])
# Add a protected tool to test authentication
@mcp.tool
@@ -139,7 +141,7 @@ async def get_user_info() -> dict:
from fastmcp.server.dependencies import get_access_token
token = get_access_token()
- # The AzureProvider stores user data in token claims
+ # The Azure plugin stores user data in token claims
return {
"azure_id": token.claims.get("sub"),
"email": token.claims.get("email"),
@@ -250,21 +252,21 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.azure import AzureProvider
+from fastmcp.server.plugins.auth.azure import AzureAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
-auth_provider = AzureProvider(
- client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149",
- client_secret="your-client-secret",
- tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5",
- base_url="https://your-production-domain.com",
- required_scopes=["your-scope"],
-
- # Production token management
- jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+auth_plugin = AzureAuth(
+ AzureAuth.Config(
+ client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149",
+ client_secret="your-client-secret",
+ tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5",
+ base_url="https://your-production-domain.com",
+ required_scopes=["your-scope"],
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ ),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@@ -274,7 +276,7 @@ auth_provider = AzureProvider(
)
)
-mcp = FastMCP(name="Production Azure App", auth=auth_provider)
+mcp = FastMCP(name="Production Azure App", plugins=[auth_plugin])
```
@@ -287,7 +289,7 @@ For complete details on these parameters, see the [OAuth Proxy documentation](/s
-For deployments where your server only needs to **validate incoming tokens** — such as Azure Container Apps with Managed Identity — use `AzureJWTVerifier` with `RemoteAuthProvider` instead of the full `AzureProvider`.
+For deployments where your server only needs to **validate incoming tokens** — such as Azure Container Apps with Managed Identity — use `AzureJWTVerifier` with `RemoteAuthProvider` instead of the full Azure auth plugin.
This pattern is ideal when:
- Your infrastructure handles authentication (e.g., Managed Identity)
@@ -297,7 +299,7 @@ This pattern is ideal when:
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth import RemoteAuthProvider
-from fastmcp.server.auth.providers.azure import AzureJWTVerifier
+from fastmcp.server.plugins.auth.azure.provider import AzureJWTVerifier
from pydantic import AnyHttpUrl
tenant_id = "your-tenant-id"
@@ -371,29 +373,31 @@ OBO requires additional configuration in your Azure App registration beyond basi
-### Configure AzureProvider for OBO
+### Configure AzureAuth for OBO
The `additional_authorize_scopes` parameter tells Azure which downstream API permissions to include during the initial authorization. These scopes establish what your server can request through OBO later.
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.azure import AzureProvider
+from fastmcp.server.plugins.auth.azure import AzureAuth
-auth_provider = AzureProvider(
- client_id="your-client-id",
- client_secret="your-client-secret",
- tenant_id="your-tenant-id",
- base_url="http://localhost:8000",
- required_scopes=["mcp-access"], # Your API scope
- # Include Graph scopes for OBO
- additional_authorize_scopes=[
- "https://graph.microsoft.com/Mail.Read",
- "https://graph.microsoft.com/User.Read",
- "offline_access", # Enables refresh tokens
- ],
+auth_plugin = AzureAuth(
+ AzureAuth.Config(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ tenant_id="your-tenant-id",
+ base_url="http://localhost:8000",
+ required_scopes=["mcp-access"], # Your API scope
+ # Include Graph scopes for OBO
+ additional_authorize_scopes=[
+ "https://graph.microsoft.com/Mail.Read",
+ "https://graph.microsoft.com/User.Read",
+ "offline_access", # Enables refresh tokens
+ ],
+ )
)
-mcp = FastMCP(name="Graph-Enabled Server", auth=auth_provider)
+mcp = FastMCP(name="Graph-Enabled Server", plugins=[auth_plugin])
```
Scopes listed in `additional_authorize_scopes` are requested during the initial OAuth flow but aren't validated on incoming tokens. They establish permission for your server to later exchange the user's token for downstream API access.
@@ -408,22 +412,25 @@ The `EntraOBOToken` dependency handles the complete OBO flow automatically. Decl
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.azure import AzureProvider, EntraOBOToken
+from fastmcp.server.plugins.auth.azure import AzureAuth
+from fastmcp.server.plugins.auth.azure.provider import EntraOBOToken
import httpx
-auth_provider = AzureProvider(
- client_id="your-client-id",
- client_secret="your-client-secret",
- tenant_id="your-tenant-id",
- base_url="http://localhost:8000",
- required_scopes=["mcp-access"],
- additional_authorize_scopes=[
- "https://graph.microsoft.com/Mail.Read",
- "https://graph.microsoft.com/User.Read",
- ],
+auth_plugin = AzureAuth(
+ AzureAuth.Config(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ tenant_id="your-tenant-id",
+ base_url="http://localhost:8000",
+ required_scopes=["mcp-access"],
+ additional_authorize_scopes=[
+ "https://graph.microsoft.com/Mail.Read",
+ "https://graph.microsoft.com/User.Read",
+ ],
+ )
)
-mcp = FastMCP(name="Email Reader", auth=auth_provider)
+mcp = FastMCP(name="Email Reader", plugins=[auth_plugin])
@mcp.tool
async def get_recent_emails(
diff --git a/docs/integrations/descope.mdx b/docs/integrations/descope.mdx
index bfb6cd9c8..304784e1e 100644
--- a/docs/integrations/descope.mdx
+++ b/docs/integrations/descope.mdx
@@ -54,21 +54,23 @@ SERVER_URL=http://localhost:3000 # Your server's base URL
### Step 3: FastMCP Configuration
-Create your FastMCP server file and use the DescopeProvider to handle all the OAuth integration automatically:
+Create your FastMCP server file and use the `DescopeAuth` plugin to handle the OAuth integration automatically:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.descope import DescopeProvider
+from fastmcp.server.plugins.auth.descope import DescopeAuth
-# The DescopeProvider automatically discovers Descope endpoints
+# The DescopeAuth plugin automatically discovers Descope endpoints
# and configures JWT token validation
-auth_provider = DescopeProvider(
- config_url="https://.../.well-known/openid-configuration", # Your MCP Server .well-known URL
- base_url=SERVER_URL, # Your server's public URL
+auth_plugin = DescopeAuth(
+ DescopeAuth.Config(
+ config_url="https://.../.well-known/openid-configuration", # Your MCP Server .well-known URL
+ base_url=SERVER_URL, # Your server's public URL
+ )
)
-# Create FastMCP server with auth
-mcp = FastMCP(name="My Descope Protected Server", auth=auth_provider)
+# Create FastMCP server with auth plugin
+mcp = FastMCP(name="My Descope Protected Server", plugins=[auth_plugin])
```
@@ -101,13 +103,15 @@ For production deployments, load configuration from environment variables:
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.descope import DescopeProvider
+from fastmcp.server.plugins.auth.descope import DescopeAuth
# Load configuration from environment variables
-auth = DescopeProvider(
- config_url=os.environ.get("DESCOPE_CONFIG_URL"),
- base_url=os.environ.get("BASE_URL", "https://your-server.com")
+auth_plugin = DescopeAuth(
+ DescopeAuth.Config(
+ config_url=os.environ.get("DESCOPE_CONFIG_URL"),
+ base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+ )
)
-mcp = FastMCP(name="My Descope Protected Server", auth=auth)
+mcp = FastMCP(name="My Descope Protected Server", plugins=[auth_plugin])
```
diff --git a/docs/integrations/discord.mdx b/docs/integrations/discord.mdx
index 5d6c643b7..19d943e11 100644
--- a/docs/integrations/discord.mdx
+++ b/docs/integrations/discord.mdx
@@ -56,19 +56,21 @@ Create an application in the Discord Developer Portal to get the credentials nee
### Step 2: FastMCP Configuration
-Create your FastMCP server using the `DiscordProvider`, which handles Discord's OAuth flow automatically:
+Create your FastMCP server using the `DiscordAuth` plugin, which handles Discord's OAuth flow automatically:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.discord import DiscordProvider
+from fastmcp.server.plugins.auth.discord import DiscordAuth
-auth_provider = DiscordProvider(
- client_id="12345", # Your Discord Application Client ID
- client_secret="your-client-secret", # Your Discord OAuth Client Secret
- base_url="http://localhost:8000", # Must match your OAuth configuration
+auth_plugin = DiscordAuth(
+ DiscordAuth.Config(
+ client_id="12345", # Your Discord Application Client ID
+ client_secret="your-client-secret", # Your Discord OAuth Client Secret
+ base_url="http://localhost:8000", # Must match your OAuth configuration
+ )
)
-mcp = FastMCP(name="Discord Secured App", auth=auth_provider)
+mcp = FastMCP(name="Discord Secured App", plugins=[auth_plugin])
@mcp.tool
async def get_user_info() -> dict:
@@ -138,11 +140,13 @@ Discord OAuth supports several scopes for accessing different types of user data
To request additional scopes:
```python
-auth_provider = DiscordProvider(
- client_id="...",
- client_secret="...",
- base_url="http://localhost:8000",
- required_scopes=["identify", "email"],
+auth_plugin = DiscordAuth(
+ DiscordAuth.Config(
+ client_id="...",
+ client_secret="...",
+ base_url="http://localhost:8000",
+ required_scopes=["identify", "email"],
+ )
)
```
@@ -153,17 +157,18 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.discord import DiscordProvider
+from fastmcp.server.plugins.auth.discord import DiscordAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
-auth_provider = DiscordProvider(
- client_id="12345",
- client_secret=os.environ["DISCORD_CLIENT_SECRET"],
- base_url="https://your-production-domain.com",
-
- jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+auth_plugin = DiscordAuth(
+ DiscordAuth.Config(
+ client_id="12345",
+ client_secret=os.environ["DISCORD_CLIENT_SECRET"],
+ base_url="https://your-production-domain.com",
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ ),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@@ -173,7 +178,7 @@ auth_provider = DiscordProvider(
)
)
-mcp = FastMCP(name="Production Discord App", auth=auth_provider)
+mcp = FastMCP(name="Production Discord App", plugins=[auth_plugin])
```
diff --git a/docs/integrations/github.mdx b/docs/integrations/github.mdx
index d493eb1ef..530788b6b 100644
--- a/docs/integrations/github.mdx
+++ b/docs/integrations/github.mdx
@@ -42,7 +42,7 @@ Create an OAuth App in your GitHub settings to get the credentials needed for au
- If you want to use a custom callback path (e.g., `/auth/github/callback`), make sure to set the same path in both your GitHub OAuth App settings and the `redirect_path` parameter when configuring the GitHubProvider.
+ If you want to use a custom callback path (e.g., `/auth/github/callback`), make sure to set the same path in both your GitHub OAuth App settings and the `redirect_path` parameter when configuring the GitHub plugin.
@@ -60,21 +60,23 @@ Create an OAuth App in your GitHub settings to get the credentials needed for au
### Step 2: FastMCP Configuration
-Create your FastMCP server using the `GitHubProvider`, which handles GitHub's OAuth quirks automatically:
+Create your FastMCP server using the `GitHubAuth` plugin, which handles GitHub's OAuth quirks automatically:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github import GitHubAuth
-# The GitHubProvider handles GitHub's token format and validation
-auth_provider = GitHubProvider(
- client_id="Ov23liAbcDefGhiJkLmN", # Your GitHub OAuth App Client ID
- client_secret="github_pat_...", # Your GitHub OAuth App Client Secret
- base_url="http://localhost:8000", # Must match your OAuth App configuration
- # redirect_path="/auth/callback" # Default value, customize if needed
+# The GitHubAuth plugin handles GitHub's token format and validation
+auth_plugin = GitHubAuth(
+ GitHubAuth.Config(
+ client_id="Ov23liAbcDefGhiJkLmN", # Your GitHub OAuth App Client ID
+ client_secret="github_pat_...", # Your GitHub OAuth App Client Secret
+ base_url="http://localhost:8000", # Must match your OAuth App configuration
+ # redirect_path="/auth/callback" # Default value, customize if needed
+ )
)
-mcp = FastMCP(name="GitHub Secured App", auth=auth_provider)
+mcp = FastMCP(name="GitHub Secured App", plugins=[auth_plugin])
# Add a protected tool to test authentication
@mcp.tool
@@ -83,7 +85,7 @@ async def get_user_info() -> dict:
from fastmcp.server.dependencies import get_access_token
token = get_access_token()
- # The GitHubProvider stores user data in token claims
+ # The GitHub auth plugin stores user data in token claims
return {
"github_user": token.claims.get("login"),
"name": token.claims.get("name"),
@@ -143,19 +145,19 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github import GitHubAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
-auth_provider = GitHubProvider(
- client_id="Ov23liAbcDefGhiJkLmN",
- client_secret="github_pat_...",
- base_url="https://your-production-domain.com",
-
- # Production token management
- jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+auth_plugin = GitHubAuth(
+ GitHubAuth.Config(
+ client_id="Ov23liAbcDefGhiJkLmN",
+ client_secret="github_pat_...",
+ base_url="https://your-production-domain.com",
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ ),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@@ -165,7 +167,7 @@ auth_provider = GitHubProvider(
)
)
-mcp = FastMCP(name="Production GitHub App", auth=auth_provider)
+mcp = FastMCP(name="Production GitHub App", plugins=[auth_plugin])
```
diff --git a/docs/integrations/google.mdx b/docs/integrations/google.mdx
index 17d49d12f..df0d8152f 100644
--- a/docs/integrations/google.mdx
+++ b/docs/integrations/google.mdx
@@ -45,7 +45,7 @@ Create an OAuth 2.0 Client ID in your Google Cloud Console to get the credential
- If you want to use a custom callback path (e.g., `/auth/google/callback`), make sure to set the same path in both your Google OAuth Client settings and the `redirect_path` parameter when configuring the GoogleProvider.
+ If you want to use a custom callback path (e.g., `/auth/google/callback`), make sure to set the same path in both your Google OAuth Client settings and the `redirect_path` parameter when configuring the Google plugin.
@@ -65,25 +65,27 @@ Create an OAuth 2.0 Client ID in your Google Cloud Console to get the credential
### Step 2: FastMCP Configuration
-Create your FastMCP server using the `GoogleProvider`, which handles Google's OAuth flow automatically:
+Create your FastMCP server using the `GoogleAuth` plugin, which handles Google's OAuth flow automatically:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.google import GoogleProvider
+from fastmcp.server.plugins.auth.google import GoogleAuth
-# The GoogleProvider handles Google's token format and validation
-auth_provider = GoogleProvider(
- client_id="123456789.apps.googleusercontent.com", # Your Google OAuth Client ID
- client_secret="GOCSPX-abc123...", # Your Google OAuth Client Secret
- base_url="http://localhost:8000", # Must match your OAuth configuration
- required_scopes=[ # Request user information
- "openid",
- "https://www.googleapis.com/auth/userinfo.email",
- ],
- # redirect_path="/auth/callback" # Default value, customize if needed
+# The GoogleAuth plugin handles Google's token format and validation
+auth_plugin = GoogleAuth(
+ GoogleAuth.Config(
+ client_id="123456789.apps.googleusercontent.com", # Your Google OAuth Client ID
+ client_secret="GOCSPX-abc123...", # Your Google OAuth Client Secret
+ base_url="http://localhost:8000", # Must match your OAuth configuration
+ required_scopes=[ # Request user information
+ "openid",
+ "https://www.googleapis.com/auth/userinfo.email",
+ ],
+ # redirect_path="/auth/callback" # Default value, customize if needed
+ )
)
-mcp = FastMCP(name="Google Secured App", auth=auth_provider)
+mcp = FastMCP(name="Google Secured App", plugins=[auth_plugin])
# Add a protected tool to test authentication
@mcp.tool
@@ -92,7 +94,7 @@ async def get_user_info() -> dict:
from fastmcp.server.dependencies import get_access_token
token = get_access_token()
- # The GoogleProvider stores user data in token claims
+ # The Google auth plugin stores user data in token claims
return {
"google_id": token.claims.get("sub"),
"email": token.claims.get("email"),
@@ -156,20 +158,20 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.google import GoogleProvider
+from fastmcp.server.plugins.auth.google import GoogleAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
-auth_provider = GoogleProvider(
- client_id="123456789.apps.googleusercontent.com",
- client_secret="GOCSPX-abc123...",
- base_url="https://your-production-domain.com",
- required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"],
-
- # Production token management
- jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+auth_plugin = GoogleAuth(
+ GoogleAuth.Config(
+ client_id="123456789.apps.googleusercontent.com",
+ client_secret="GOCSPX-abc123...",
+ base_url="https://your-production-domain.com",
+ required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"],
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ ),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@@ -179,11 +181,11 @@ auth_provider = GoogleProvider(
)
)
-mcp = FastMCP(name="Production Google App", auth=auth_provider)
+mcp = FastMCP(name="Production Google App", plugins=[auth_plugin])
```
Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
-
\ No newline at end of file
+
diff --git a/docs/integrations/keycloak.mdx b/docs/integrations/keycloak.mdx
index 22d61f132..6ec42d3e2 100644
--- a/docs/integrations/keycloak.mdx
+++ b/docs/integrations/keycloak.mdx
@@ -27,22 +27,24 @@ Before you begin, you will need:
### FastMCP Configuration
-Create your FastMCP server and use `KeycloakAuthProvider` to handle OAuth:
+Create your FastMCP server and use the `KeycloakAuth` plugin to handle OAuth:
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+from fastmcp.server.plugins.auth.keycloak import KeycloakAuth
from fastmcp.server.dependencies import get_access_token
-auth = KeycloakAuthProvider(
- realm_url=os.getenv("KEYCLOAK_REALM_URL") or "http://localhost:8080/realms/myrealm",
- base_url="http://localhost:8000",
- # audience="http://localhost:8000", # Recommended for production
+auth_plugin = KeycloakAuth(
+ KeycloakAuth.Config(
+ realm_url=os.getenv("KEYCLOAK_REALM_URL") or "http://localhost:8080/realms/myrealm",
+ base_url="http://localhost:8000",
+ # audience="http://localhost:8000", # Recommended for production
+ )
)
-mcp = FastMCP("Keycloak Example Server", auth=auth)
+mcp = FastMCP("Keycloak Example Server", plugins=[auth_plugin])
@mcp.tool
@@ -124,7 +126,7 @@ async def admin_only_tool() -> str:
```python
from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+from fastmcp.server.plugins.auth.keycloak import KeycloakAuth
custom_verifier = JWTVerifier(
jwks_uri="http://localhost:8080/realms/myrealm/protocol/openid-connect/certs",
@@ -133,9 +135,11 @@ custom_verifier = JWTVerifier(
required_scopes=["api:read", "api:write"],
)
-auth = KeycloakAuthProvider(
- realm_url="http://localhost:8080/realms/myrealm",
- base_url="http://localhost:8000",
+auth_plugin = KeycloakAuth(
+ KeycloakAuth.Config(
+ realm_url="http://localhost:8080/realms/myrealm",
+ base_url="http://localhost:8000",
+ ),
token_verifier=custom_verifier,
)
```
diff --git a/docs/integrations/oci.mdx b/docs/integrations/oci.mdx
index 02fa36dae..baff973fb 100644
--- a/docs/integrations/oci.mdx
+++ b/docs/integrations/oci.mdx
@@ -87,7 +87,7 @@ Follow the Steps as mentioned below to create an OAuth client.
Click on "Submit" button to update OAuth configuration for the client application.
**Note: You don't need to do any special configuration to support PKCE for the OAuth client.**
Make sure to Activate the client application.
- Note down client ID and client secret for the application. You'll use these values when configuring the OCIProvider in your code.
+ Note down client ID and client secret for the application. You'll use these values when configuring the OCI plugin in your code.
@@ -209,7 +209,7 @@ For production deployments with persistent token management across server restar
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.oci import OCIProvider
+from fastmcp.server.plugins.auth.oci import OCIAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
@@ -217,14 +217,14 @@ from cryptography.fernet import Fernet
# Load configuration from environment
# Production setup with encrypted persistent token storage
-auth_provider = OCIProvider(
- config_url=os.environ.get("OCI_CONFIG_URL"),
- client_id=os.environ.get("OCI_CLIENT_ID"),
- client_secret=os.environ.get("OCI_CLIENT_SECRET"),
- base_url=os.environ.get("BASE_URL", "https://your-production-domain.com"),
-
- # Production token management
- jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+auth_plugin = OCIAuth(
+ OCIAuth.Config(
+ config_url=os.environ.get("OCI_CONFIG_URL"),
+ client_id=os.environ.get("OCI_CLIENT_ID"),
+ client_secret=os.environ.get("OCI_CLIENT_SECRET"),
+ base_url=os.environ.get("BASE_URL", "https://your-production-domain.com"),
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ ),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@@ -234,7 +234,7 @@ auth_provider = OCIProvider(
)
)
-mcp = FastMCP(name="Production OCI App", auth=auth_provider)
+mcp = FastMCP(name="Production OCI App", plugins=[auth_plugin])
```
@@ -245,4 +245,4 @@ For complete details on these parameters, see the [OAuth Proxy documentation](/s
The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
-
\ No newline at end of file
+
diff --git a/docs/integrations/propelauth.mdx b/docs/integrations/propelauth.mdx
index 7f21d2010..8fbafe3d7 100644
--- a/docs/integrations/propelauth.mdx
+++ b/docs/integrations/propelauth.mdx
@@ -67,22 +67,24 @@ SERVER_URL=http://localhost:8000 # Your server's base U
### Step 3: FastMCP Configuration
-Create your FastMCP server file and use the PropelAuthProvider to handle all the OAuth integration automatically:
+Create your FastMCP server file and use the `PropelAuth` plugin to handle the OAuth integration automatically:
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
+from fastmcp.server.plugins.auth.propelauth import PropelAuth
-auth_provider = PropelAuthProvider(
- auth_url=os.environ["PROPELAUTH_AUTH_URL"],
- introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
- introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
- base_url=os.environ["SERVER_URL"],
- required_scopes=["read:user_data"], # Optional scope enforcement
+auth_plugin = PropelAuth(
+ PropelAuth.Config(
+ auth_url=os.environ["PROPELAUTH_AUTH_URL"],
+ introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
+ introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
+ base_url=os.environ["SERVER_URL"],
+ required_scopes=["read:user_data"], # Optional scope enforcement
+ )
)
-mcp = FastMCP(name="My PropelAuth Protected Server", auth=auth_provider)
+mcp = FastMCP(name="My PropelAuth Protected Server", plugins=[auth_plugin])
```
## Testing
@@ -114,18 +116,20 @@ You can use `get_access_token()` inside your tools to identify the authenticated
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
+from fastmcp.server.plugins.auth.propelauth import PropelAuth
from fastmcp.server.dependencies import get_access_token
-auth = PropelAuthProvider(
- auth_url=os.environ["PROPELAUTH_AUTH_URL"],
- introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
- introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
- base_url=os.environ["SERVER_URL"],
- required_scopes=["read:user_data"],
+auth_plugin = PropelAuth(
+ PropelAuth.Config(
+ auth_url=os.environ["PROPELAUTH_AUTH_URL"],
+ introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
+ introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
+ base_url=os.environ["SERVER_URL"],
+ required_scopes=["read:user_data"],
+ )
)
-mcp = FastMCP(name="My PropelAuth Protected Server", auth=auth)
+mcp = FastMCP(name="My PropelAuth Protected Server", plugins=[auth_plugin])
@mcp.tool
def whoami() -> dict:
@@ -139,26 +143,26 @@ def whoami() -> dict:
## Advanced Configuration
-The `PropelAuthProvider` supports optional overrides for token introspection behavior, including caching and request timeouts:
+The `PropelAuth` plugin supports optional overrides for token introspection behavior, including caching and request timeouts:
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
+from fastmcp.server.plugins.auth.propelauth import PropelAuth
-auth = PropelAuthProvider(
- auth_url=os.environ["PROPELAUTH_AUTH_URL"],
- introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
- introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
- base_url=os.environ.get("BASE_URL", "https://your-server.com"),
- required_scopes=["read:user_data"],
- resource="https://your-server.com/mcp", # Restrict to tokens intended for this server (RFC 8707)
- token_introspection_overrides={
- "cache_ttl_seconds": 300, # Cache introspection results for 5 minutes
- "max_cache_size": 1000, # Maximum cached tokens
- "timeout_seconds": 15, # HTTP request timeout
- },
+auth_plugin = PropelAuth(
+ PropelAuth.Config(
+ auth_url=os.environ["PROPELAUTH_AUTH_URL"],
+ introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
+ introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
+ base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+ required_scopes=["read:user_data"],
+ resource="https://your-server.com/mcp", # Restrict to tokens intended for this server (RFC 8707)
+ introspection_cache_ttl_seconds=300, # Cache introspection results for 5 minutes
+ introspection_max_cache_size=1000, # Maximum cached tokens
+ introspection_timeout_seconds=15, # HTTP request timeout
+ )
)
-mcp = FastMCP(name="My PropelAuth Protected Server", auth=auth)
+mcp = FastMCP(name="My PropelAuth Protected Server", plugins=[auth_plugin])
```
diff --git a/docs/integrations/scalekit.mdx b/docs/integrations/scalekit.mdx
index 191b81ca2..0812d3ec0 100644
--- a/docs/integrations/scalekit.mdx
+++ b/docs/integrations/scalekit.mdx
@@ -43,24 +43,26 @@ BASE_URL=http://localhost:8000/
### Step 2: Add auth to FastMCP server
-Create your FastMCP server file and use the ScalekitProvider to handle all the OAuth integration automatically:
+Create your FastMCP server file and use the `ScalekitAuth` plugin to handle the OAuth integration automatically:
> **Warning:** The legacy `mcp_url` and `client_id` parameters are deprecated and will be removed in a future release. Use `base_url` instead of `mcp_url` and remove `client_id` from your configuration.
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.scalekit import ScalekitProvider
+from fastmcp.server.plugins.auth.scalekit import ScalekitAuth
# Discovers Scalekit endpoints and set up JWT token validation
-auth_provider = ScalekitProvider(
- environment_url=SCALEKIT_ENVIRONMENT_URL, # Scalekit environment URL
- resource_id=SCALEKIT_RESOURCE_ID, # Resource server ID
- base_url=SERVER_URL, # Public MCP endpoint
- required_scopes=["read"], # Optional scope enforcement
+auth_plugin = ScalekitAuth(
+ ScalekitAuth.Config(
+ environment_url=SCALEKIT_ENVIRONMENT_URL, # Scalekit environment URL
+ resource_id=SCALEKIT_RESOURCE_ID, # Resource server ID
+ base_url=SERVER_URL, # Public MCP endpoint
+ required_scopes=["read"], # Optional scope enforcement
+ )
)
-# Create FastMCP server with auth
-mcp = FastMCP(name="My Scalekit Protected Server", auth=auth_provider)
+# Create FastMCP server with auth plugin
+mcp = FastMCP(name="My Scalekit Protected Server", plugins=[auth_plugin])
@mcp.tool
def auth_status() -> dict:
@@ -95,16 +97,18 @@ For production deployments, load configuration from environment variables:
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.scalekit import ScalekitProvider
+from fastmcp.server.plugins.auth.scalekit import ScalekitAuth
# Load configuration from environment variables
-auth = ScalekitProvider(
- environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"),
- resource_id=os.environ.get("SCALEKIT_RESOURCE_ID"),
- base_url=os.environ.get("BASE_URL", "https://your-server.com")
+auth_plugin = ScalekitAuth(
+ ScalekitAuth.Config(
+ environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"),
+ resource_id=os.environ.get("SCALEKIT_RESOURCE_ID"),
+ base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+ )
)
-mcp = FastMCP(name="My Scalekit Protected Server", auth=auth)
+mcp = FastMCP(name="My Scalekit Protected Server", plugins=[auth_plugin])
@mcp.tool
def protected_action() -> str:
diff --git a/docs/integrations/supabase.mdx b/docs/integrations/supabase.mdx
index 9ffda444d..ffd8b0e5b 100644
--- a/docs/integrations/supabase.mdx
+++ b/docs/integrations/supabase.mdx
@@ -19,7 +19,7 @@ Supabase Auth does not currently support [RFC 8707](https://www.rfc-editor.org/r
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.
+The Supabase auth plugin 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
@@ -49,18 +49,20 @@ In your Supabase Dashboard:
### Step 3: FastMCP Configuration
-Create your FastMCP server using the `SupabaseProvider`:
+Create your FastMCP server using the `SupabaseAuth` plugin:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.supabase import SupabaseProvider
+from fastmcp.server.plugins.auth.supabase import SupabaseAuth
-auth = SupabaseProvider(
- project_url="https://abc123.supabase.co",
- base_url="http://localhost:8000",
+auth_plugin = SupabaseAuth(
+ SupabaseAuth.Config(
+ project_url="https://abc123.supabase.co",
+ base_url="http://localhost:8000",
+ )
)
-mcp = FastMCP("Supabase Protected Server", auth=auth)
+mcp = FastMCP("Supabase Protected Server", plugins=[auth_plugin])
@mcp.tool
def protected_tool(message: str) -> str:
@@ -112,12 +114,14 @@ For production deployments, load configuration from environment variables:
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.supabase import SupabaseProvider
+from fastmcp.server.plugins.auth.supabase import SupabaseAuth
-auth = SupabaseProvider(
- project_url=os.environ["SUPABASE_PROJECT_URL"],
- base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+auth_plugin = SupabaseAuth(
+ SupabaseAuth.Config(
+ project_url=os.environ["SUPABASE_PROJECT_URL"],
+ base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+ )
)
-mcp = FastMCP(name="Supabase Secured App", auth=auth)
+mcp = FastMCP(name="Supabase Secured App", plugins=[auth_plugin])
```
diff --git a/docs/integrations/workos.mdx b/docs/integrations/workos.mdx
index 4f13a5512..406f82e49 100644
--- a/docs/integrations/workos.mdx
+++ b/docs/integrations/workos.mdx
@@ -56,22 +56,24 @@ The callback URL must match exactly. The default path is `/auth/callback`, but y
### Step 2: FastMCP Configuration
-Create your FastMCP server using the `WorkOSProvider`:
+Create your FastMCP server using the `WorkOSAuth` plugin:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import WorkOSProvider
+from fastmcp.server.plugins.auth.workos import WorkOSAuth
# Configure WorkOS OAuth
-auth = WorkOSProvider(
- client_id="client_YOUR_CLIENT_ID",
- client_secret="YOUR_CLIENT_SECRET",
- authkit_domain="https://your-app.authkit.app",
- base_url="http://localhost:8000",
- required_scopes=["openid", "profile", "email"]
+auth_plugin = WorkOSAuth(
+ WorkOSAuth.Config(
+ client_id="client_YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ authkit_domain="https://your-app.authkit.app",
+ base_url="http://localhost:8000",
+ required_scopes=["openid", "profile", "email"],
+ )
)
-mcp = FastMCP("WorkOS Protected Server", auth=auth)
+mcp = FastMCP("WorkOS Protected Server", plugins=[auth_plugin])
@mcp.tool
def protected_tool(message: str) -> str:
@@ -134,21 +136,21 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import WorkOSProvider
+from fastmcp.server.plugins.auth.workos import WorkOSAuth
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
-auth = WorkOSProvider(
- client_id="client_YOUR_CLIENT_ID",
- client_secret="YOUR_CLIENT_SECRET",
- authkit_domain="https://your-app.authkit.app",
- base_url="https://your-production-domain.com",
- required_scopes=["openid", "profile", "email"],
-
- # Production token management
- jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+auth_plugin = WorkOSAuth(
+ WorkOSAuth.Config(
+ client_id="client_YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ authkit_domain="https://your-app.authkit.app",
+ base_url="https://your-production-domain.com",
+ required_scopes=["openid", "profile", "email"],
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ ),
client_storage=FernetEncryptionWrapper(
key_value=RedisStore(
host=os.environ["REDIS_HOST"],
@@ -158,7 +160,7 @@ auth = WorkOSProvider(
)
)
-mcp = FastMCP(name="Production WorkOS App", auth=auth)
+mcp = FastMCP(name="Production WorkOS App", plugins=[auth_plugin])
```
@@ -197,4 +199,4 @@ OAuth callback path
API request timeout
-
\ No newline at end of file
+
diff --git a/docs/servers/auth/authentication.mdx b/docs/servers/auth/authentication.mdx
index d37c57f36..b4ce3e270 100644
--- a/docs/servers/auth/authentication.mdx
+++ b/docs/servers/auth/authentication.mdx
@@ -106,7 +106,7 @@ For example, the built-in `AuthKitProvider` uses WorkOS AuthKit, which fully sup
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import AuthKitProvider
+from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
auth = AuthKitProvider(
authkit_domain="https://your-project.authkit.app",
@@ -136,7 +136,7 @@ For example, the built-in `GitHubProvider` extends `OAuthProxy` to work with Git
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="Ov23li...", # Your GitHub OAuth App ID
@@ -221,7 +221,7 @@ For production deployments, load sensitive values like client secrets from envir
```python
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
# Load secrets from environment variables
auth = GitHubProvider(
diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx
index aff1267ff..3a4f9a00e 100644
--- a/docs/servers/auth/oauth-proxy.mdx
+++ b/docs/servers/auth/oauth-proxy.mdx
@@ -355,7 +355,7 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
FastMCP includes pre-configured providers for common services:
```python
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="your-github-app-id",
@@ -690,7 +690,7 @@ For production deployments, load sensitive credentials from environment variable
```python
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
# Load secrets from environment variables
auth = GitHubProvider(
diff --git a/docs/servers/auth/oidc-proxy.mdx b/docs/servers/auth/oidc-proxy.mdx
index d33cd611a..138f39bbe 100644
--- a/docs/servers/auth/oidc-proxy.mdx
+++ b/docs/servers/auth/oidc-proxy.mdx
@@ -218,7 +218,7 @@ auth = OIDCProxy(
FastMCP includes pre-configured OIDC providers for common services:
```python
-from fastmcp.server.auth.providers.auth0 import Auth0Provider
+from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
auth = Auth0Provider(
config_url="https://.../.well-known/openid-configuration",
@@ -262,7 +262,7 @@ For production deployments, load sensitive credentials from environment variable
```python
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.auth0 import Auth0Provider
+from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
# Load secrets from environment variables
auth = Auth0Provider(
diff --git a/docs/servers/auth/plugins.mdx b/docs/servers/auth/plugins.mdx
index 0bd0f2c52..b5496f59c 100644
--- a/docs/servers/auth/plugins.mdx
+++ b/docs/servers/auth/plugins.mdx
@@ -10,68 +10,53 @@ Use an auth plugin when you want authentication to be configured alongside other
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.plugins.auth import GitHubAuth
+from fastmcp.server.plugins.auth.github import GitHubAuth
mcp = FastMCP(
"GitHub Protected Server",
plugins=[
GitHubAuth(
- {
- "client_id": "your-github-client-id",
- "client_secret": "your-github-client-secret",
- "base_url": "https://your-server.com",
- }
+ GitHubAuth.Config(
+ client_id="your-github-client-id",
+ client_secret="your-github-client-secret",
+ base_url="https://your-server.com",
+ )
)
],
)
```
-The provider APIs remain available and are still the most direct option in Python code:
-
-```python
-from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
-
-auth = GitHubProvider(
- client_id="your-github-client-id",
- client_secret="your-github-client-secret",
- base_url="https://your-server.com",
-)
-
-mcp = FastMCP("GitHub Protected Server", auth=auth)
-```
+Provider APIs remain available in each plugin's explicit `.provider` module for advanced direct auth wiring, but integrations should prefer the plugin form.
## Included Plugins
-Import first-party auth plugins from `fastmcp.server.plugins.auth`:
+Each first-party auth plugin lives in its own module under `fastmcp.server.plugins.auth`, mirroring the provider package:
```python
-from fastmcp.server.plugins.auth import (
- Auth0Auth,
- AuthKitAuth,
- AWSCognitoAuth,
- AzureAuth,
- ClerkAuth,
- DescopeAuth,
- DiscordAuth,
- GitHubAuth,
- GoogleAuth,
- KeycloakAuth,
- OCIAuth,
- PropelAuth,
- ScalekitAuth,
- SupabaseAuth,
- WorkOSAuth,
-)
+from fastmcp.server.plugins.auth.auth0 import Auth0Auth
+from fastmcp.server.plugins.auth.authkit import AuthKitAuth
+from fastmcp.server.plugins.auth.aws import AWSCognitoAuth
+from fastmcp.server.plugins.auth.azure import AzureAuth
+from fastmcp.server.plugins.auth.clerk import ClerkAuth
+from fastmcp.server.plugins.auth.descope import DescopeAuth
+from fastmcp.server.plugins.auth.discord import DiscordAuth
+from fastmcp.server.plugins.auth.github import GitHubAuth
+from fastmcp.server.plugins.auth.google import GoogleAuth
+from fastmcp.server.plugins.auth.keycloak import KeycloakAuth
+from fastmcp.server.plugins.auth.oci import OCIAuth
+from fastmcp.server.plugins.auth.propelauth import PropelAuth
+from fastmcp.server.plugins.auth.scalekit import ScalekitAuth
+from fastmcp.server.plugins.auth.supabase import SupabaseAuth
+from fastmcp.server.plugins.auth.workos import WorkOSAuth
```
-Each plugin accepts a matching `*AuthConfig` model or a plain dictionary. Config fields mirror the wrapped provider's constructor wherever the value can be represented as JSON. Python-only objects such as custom token verifiers, HTTP clients, and client storage are passed as constructor keyword arguments:
+Each plugin exposes its serializable configuration model as `Plugin.Config`. Config fields mirror the wrapped provider's constructor wherever the value can be represented as JSON. Python-only objects such as custom token verifiers, HTTP clients, and client storage are passed as constructor keyword arguments:
```python
-from fastmcp.server.plugins.auth import SupabaseAuth, SupabaseAuthConfig
+from fastmcp.server.plugins.auth.supabase import SupabaseAuth
auth_plugin = SupabaseAuth(
- SupabaseAuthConfig(
+ SupabaseAuth.Config(
project_url="https://abc123.supabase.co",
base_url="https://your-server.com",
required_scopes=["read"],
diff --git a/docs/servers/auth/remote-oauth.mdx b/docs/servers/auth/remote-oauth.mdx
index 7c94a937f..75f90f0c8 100644
--- a/docs/servers/auth/remote-oauth.mdx
+++ b/docs/servers/auth/remote-oauth.mdx
@@ -197,7 +197,7 @@ WorkOS AuthKit provides an excellent example of remote OAuth integration. The `A
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import AuthKitProvider
+from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
auth = AuthKitProvider(
authkit_domain="https://your-project.authkit.app",
diff --git a/docs/servers/storage-backends.mdx b/docs/servers/storage-backends.mdx
index d13ce176d..b12c6dfb2 100644
--- a/docs/servers/storage-backends.mdx
+++ b/docs/servers/storage-backends.mdx
@@ -111,7 +111,7 @@ For OAuth token storage:
```python
import os
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.redis import RedisStore
auth = GitHubProvider(
@@ -163,7 +163,7 @@ By default, FastMCP automatically manages keys and storage based on your platfor
No configuration needed:
```python
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="your-id",
@@ -178,7 +178,7 @@ For production deployments, configure explicit keys and persistent network-acces
```python
import os
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
diff --git a/docs/v2/deployment/http.mdx b/docs/v2/deployment/http.mdx
index fa91c65e0..eb91f378f 100644
--- a/docs/v2/deployment/http.mdx
+++ b/docs/v2/deployment/http.mdx
@@ -468,7 +468,7 @@ When mounting an OAuth-protected server under a path prefix, declare your URLs u
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from starlette.applications import Starlette
from starlette.routing import Mount
@@ -530,7 +530,7 @@ Here's a complete working example showing all the pieces together:
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from starlette.applications import Starlette
from starlette.routing import Mount
import uvicorn
diff --git a/docs/v2/integrations/auth0.mdx b/docs/v2/integrations/auth0.mdx
index 9e5b186fc..fafd164e4 100644
--- a/docs/v2/integrations/auth0.mdx
+++ b/docs/v2/integrations/auth0.mdx
@@ -81,7 +81,7 @@ Create your FastMCP server using the `Auth0Provider`.
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.auth0 import Auth0Provider
+from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
# The Auth0Provider utilizes Auth0 OIDC configuration
auth_provider = Auth0Provider(
@@ -158,7 +158,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.auth0 import Auth0Provider
+from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@@ -205,7 +205,7 @@ Setting this environment variable allows the Auth0 provider to be used automatic
-Set to `fastmcp.server.auth.providers.auth0.Auth0Provider` to use Auth0 authentication.
+Set to `fastmcp.server.plugins.auth.auth0.provider.Auth0Provider` to use Auth0 authentication.
@@ -250,7 +250,7 @@ Comma-, space-, or JSON-separated list of required AUth0 scopes (e.g., `openid e
Example `.env` file:
```bash
# Use the Auth0 provider
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.auth0.Auth0Provider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.auth0.provider.Auth0Provider
# Auth0 configuration and credentials
FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL=https://.../.well-known/openid-configuration
diff --git a/docs/v2/integrations/authkit.mdx b/docs/v2/integrations/authkit.mdx
index aa7799a35..a87822991 100644
--- a/docs/v2/integrations/authkit.mdx
+++ b/docs/v2/integrations/authkit.mdx
@@ -43,7 +43,7 @@ Create your FastMCP server file and use the `AuthKitProvider` to handle all the
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import AuthKitProvider
+from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
# The AuthKitProvider automatically discovers WorkOS endpoints
# and configures JWT token validation
@@ -90,7 +90,7 @@ Setting this environment variable allows the AuthKit provider to be used automat
-Set to `fastmcp.server.auth.providers.workos.AuthKitProvider` to use AuthKit authentication.
+Set to `fastmcp.server.plugins.auth.authkit.provider.AuthKitProvider` to use AuthKit authentication.
@@ -115,7 +115,7 @@ Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid p
Example `.env` file:
```bash
# Use the AuthKit provider
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.workos.AuthKitProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.authkit.provider.AuthKitProvider
# AuthKit configuration
FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN=https://your-project-12345.authkit.app
diff --git a/docs/v2/integrations/aws-cognito.mdx b/docs/v2/integrations/aws-cognito.mdx
index 4f39e1953..523d15ff2 100644
--- a/docs/v2/integrations/aws-cognito.mdx
+++ b/docs/v2/integrations/aws-cognito.mdx
@@ -121,7 +121,7 @@ Create your FastMCP server using the `AWSCognitoProvider`, which handles AWS Cog
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.aws import AWSCognitoProvider
+from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
from fastmcp.server.dependencies import get_access_token
# The AWSCognitoProvider handles JWT validation and user claims
@@ -205,7 +205,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.aws import AWSCognitoProvider
+from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@@ -248,7 +248,7 @@ Setting this environment variable allows the AWS Cognito provider to be used aut
-Set to `fastmcp.server.auth.providers.aws.AWSCognitoProvider` to use AWS Cognito authentication.
+Set to `fastmcp.server.plugins.auth.aws.provider.AWSCognitoProvider` to use AWS Cognito authentication.
@@ -293,7 +293,7 @@ Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid e
Example `.env` file:
```bash
# Use the AWS Cognito provider
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.aws.AWSCognitoProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.aws.provider.AWSCognitoProvider
# AWS Cognito credentials
FASTMCP_SERVER_AUTH_AWS_COGNITO_USER_POOL_ID=eu-central-1_XXXXXXXXX
diff --git a/docs/v2/integrations/azure.mdx b/docs/v2/integrations/azure.mdx
index 208b3f2ed..a0a33fab8 100644
--- a/docs/v2/integrations/azure.mdx
+++ b/docs/v2/integrations/azure.mdx
@@ -114,7 +114,7 @@ Create your FastMCP server using the `AzureProvider`, which handles Azure's OAut
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.azure import AzureProvider
+from fastmcp.server.plugins.auth.azure.provider import AzureProvider
# The AzureProvider handles Azure's token format and validation
auth_provider = AzureProvider(
@@ -247,7 +247,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.azure import AzureProvider
+from fastmcp.server.plugins.auth.azure.provider import AzureProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@@ -292,7 +292,7 @@ Setting this environment variable allows the Azure provider to be used automatic
-Set to `fastmcp.server.auth.providers.azure.AzureProvider` to use Azure authentication.
+Set to `fastmcp.server.plugins.auth.azure.provider.AzureProvider` to use Azure authentication.
@@ -360,7 +360,7 @@ This setting affects all Azure OAuth endpoints (authorization, token, issuer, JW
Example `.env` file:
```bash
# Use the Azure provider
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.azure.AzureProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.azure.provider.AzureProvider
# Azure OAuth credentials
FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID=835f09b6-0f0f-40cc-85cb-f32c5829a149
diff --git a/docs/v2/integrations/descope.mdx b/docs/v2/integrations/descope.mdx
index 14bade5f4..8193275d6 100644
--- a/docs/v2/integrations/descope.mdx
+++ b/docs/v2/integrations/descope.mdx
@@ -59,7 +59,7 @@ Create your FastMCP server file and use the DescopeProvider to handle all the OA
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.descope import DescopeProvider
+from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
# The DescopeProvider automatically discovers Descope endpoints
# and configures JWT token validation
@@ -105,7 +105,7 @@ Setting this environment variable allows the Descope provider to be used automat
- Set to `fastmcp.server.auth.providers.descope.DescopeProvider` to use
+ Set to `fastmcp.server.plugins.auth.descope.provider.DescopeProvider` to use
Descope authentication.
@@ -129,7 +129,7 @@ Example `.env` file:
```bash
# Use the Descope provider
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.descope.DescopeProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.descope.provider.DescopeProvider
# Descope configuration
FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration
diff --git a/docs/v2/integrations/discord.mdx b/docs/v2/integrations/discord.mdx
index b9154e3d6..0e9377d06 100644
--- a/docs/v2/integrations/discord.mdx
+++ b/docs/v2/integrations/discord.mdx
@@ -61,7 +61,7 @@ Create your FastMCP server using the `DiscordProvider`, which handles Discord's
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.discord import DiscordProvider
+from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
auth_provider = DiscordProvider(
client_id="12345", # Your Discord Application Client ID
@@ -154,7 +154,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.discord import DiscordProvider
+from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@@ -193,7 +193,7 @@ Setting this environment variable allows the Discord provider to be used automat
-Set to `fastmcp.server.auth.providers.discord.DiscordProvider` to use Discord authentication.
+Set to `fastmcp.server.plugins.auth.discord.provider.DiscordProvider` to use Discord authentication.
@@ -233,7 +233,7 @@ HTTP request timeout for Discord API calls
Example `.env` file:
```bash
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.discord.DiscordProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.discord.provider.DiscordProvider
FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID=12345
FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET=your-client-secret
diff --git a/docs/v2/integrations/github.mdx b/docs/v2/integrations/github.mdx
index 5c920063c..3a5cdef11 100644
--- a/docs/v2/integrations/github.mdx
+++ b/docs/v2/integrations/github.mdx
@@ -65,7 +65,7 @@ Create your FastMCP server using the `GitHubProvider`, which handles GitHub's OA
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
# The GitHubProvider handles GitHub's token format and validation
auth_provider = GitHubProvider(
@@ -144,7 +144,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@@ -187,7 +187,7 @@ Setting this environment variable allows the GitHub provider to be used automati
-Set to `fastmcp.server.auth.providers.github.GitHubProvider` to use GitHub authentication.
+Set to `fastmcp.server.plugins.auth.github.provider.GitHubProvider` to use GitHub authentication.
@@ -228,7 +228,7 @@ HTTP request timeout for GitHub API calls
Example `.env` file:
```bash
# Use the GitHub provider
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.github.provider.GitHubProvider
# GitHub OAuth credentials
FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID=Ov23liAbcDefGhiJkLmN
diff --git a/docs/v2/integrations/google.mdx b/docs/v2/integrations/google.mdx
index 3285765e1..853fd1a38 100644
--- a/docs/v2/integrations/google.mdx
+++ b/docs/v2/integrations/google.mdx
@@ -70,7 +70,7 @@ Create your FastMCP server using the `GoogleProvider`, which handles Google's OA
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.google import GoogleProvider
+from fastmcp.server.plugins.auth.google.provider import GoogleProvider
# The GoogleProvider handles Google's token format and validation
auth_provider = GoogleProvider(
@@ -157,7 +157,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.google import GoogleProvider
+from fastmcp.server.plugins.auth.google.provider import GoogleProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@@ -201,7 +201,7 @@ Setting this environment variable allows the Google provider to be used automati
-Set to `fastmcp.server.auth.providers.google.GoogleProvider` to use Google authentication.
+Set to `fastmcp.server.plugins.auth.google.provider.GoogleProvider` to use Google authentication.
@@ -242,7 +242,7 @@ HTTP request timeout for Google API calls
Example `.env` file:
```bash
# Use the Google provider
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.google.GoogleProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.google.provider.GoogleProvider
# Google OAuth credentials
FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID=123456789.apps.googleusercontent.com
diff --git a/docs/v2/integrations/oci.mdx b/docs/v2/integrations/oci.mdx
index 282a51e17..426089a03 100644
--- a/docs/v2/integrations/oci.mdx
+++ b/docs/v2/integrations/oci.mdx
@@ -213,7 +213,7 @@ For production deployments with persistent token management across server restar
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.oci import OCIProvider
+from fastmcp.server.plugins.auth.oci.provider import OCIProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
@@ -265,7 +265,7 @@ Setting this environment variable allows the OCI provider to be used automatical
-Set to `fastmcp.server.auth.providers.oci.OCIProvider` to use OCI IAM authentication.
+Set to `fastmcp.server.plugins.auth.oci.provider.OCIProvider` to use OCI IAM authentication.
@@ -303,7 +303,7 @@ Redirect path configured in your OCI IAM Integrated Application
Example `.env` file:
```bash
# Use the OCI IAM provider
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.oci.OCIProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.oci.provider.OCIProvider
# OCI IAM configuration and credentials
FASTMCP_SERVER_AUTH_OCI_IAM_GUID=idcs-asaacasd1111.....
diff --git a/docs/v2/integrations/scalekit.mdx b/docs/v2/integrations/scalekit.mdx
index abe41a2ba..bb4470ea4 100644
--- a/docs/v2/integrations/scalekit.mdx
+++ b/docs/v2/integrations/scalekit.mdx
@@ -50,7 +50,7 @@ Create your FastMCP server file and use the ScalekitProvider to handle all the O
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.scalekit import ScalekitProvider
+from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
# Discovers Scalekit endpoints and set up JWT token validation
auth_provider = ScalekitProvider(
@@ -95,7 +95,7 @@ Setting this environment variable allows the Scalekit provider to be used automa
-Set to `fastmcp.server.auth.providers.scalekit.ScalekitProvider` to use Scalekit authentication.
+Set to `fastmcp.server.plugins.auth.scalekit.provider.ScalekitProvider` to use Scalekit authentication.
@@ -127,7 +127,7 @@ Example `.env`:
```bash
# Use the Scalekit provider
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.scalekit.ScalekitProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.scalekit.provider.ScalekitProvider
# Scalekit configuration
FASTMCP_SERVER_AUTH_SCALEKITPROVIDER_ENVIRONMENT_URL=https://your-env.scalekit.com
diff --git a/docs/v2/integrations/supabase.mdx b/docs/v2/integrations/supabase.mdx
index 94a57e0a2..8a4875df7 100644
--- a/docs/v2/integrations/supabase.mdx
+++ b/docs/v2/integrations/supabase.mdx
@@ -32,7 +32,7 @@ Create your FastMCP server using the `SupabaseProvider`:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.supabase import SupabaseProvider
+from fastmcp.server.plugins.auth.supabase.provider import SupabaseProvider
# Configure Supabase Auth
auth = SupabaseProvider(
@@ -101,7 +101,7 @@ Setting this environment variable allows the Supabase provider to be used automa
-Set to `fastmcp.server.auth.providers.supabase.SupabaseProvider` to use Supabase authentication.
+Set to `fastmcp.server.plugins.auth.supabase.provider.SupabaseProvider` to use Supabase authentication.
@@ -130,7 +130,7 @@ Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid e
Example `.env` file:
```bash
# Use the Supabase provider
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.supabase.SupabaseProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.supabase.provider.SupabaseProvider
# Supabase configuration
FASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL=https://abc123.supabase.co
diff --git a/docs/v2/integrations/workos.mdx b/docs/v2/integrations/workos.mdx
index b8c01a1d6..0f317dcfe 100644
--- a/docs/v2/integrations/workos.mdx
+++ b/docs/v2/integrations/workos.mdx
@@ -61,7 +61,7 @@ Create your FastMCP server using the `WorkOSProvider`:
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import WorkOSProvider
+from fastmcp.server.plugins.auth.workos.provider import WorkOSProvider
# Configure WorkOS OAuth
auth = WorkOSProvider(
@@ -135,7 +135,7 @@ For production deployments with persistent token management across server restar
```python server.py
import os
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import WorkOSProvider
+from fastmcp.server.plugins.auth.workos.provider import WorkOSProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
@@ -180,7 +180,7 @@ Setting this environment variable allows the WorkOS provider to be used automati
-Set to `fastmcp.server.auth.providers.workos.WorkOSProvider` to use WorkOS authentication.
+Set to `fastmcp.server.plugins.auth.workos.provider.WorkOSProvider` to use WorkOS authentication.
@@ -232,7 +232,7 @@ FASTMCP_SERVER_AUTH_WORKOS_BASE_URL=https://your-server.com
FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES=["openid","profile","email"]
# Optional: Automatically provision WorkOS auth for all servers
-FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.workos.WorkOSProvider
+FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.workos.provider.WorkOSProvider
```
With environment variables set, you can either:
@@ -240,14 +240,14 @@ With environment variables set, you can either:
**Option 1: Manual instantiation (env vars provide defaults)**
```python server.py
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import WorkOSProvider
+from fastmcp.server.plugins.auth.workos.provider import WorkOSProvider
# Env vars provide default values for WorkOSProvider()
auth = WorkOSProvider() # Uses env var defaults
mcp = FastMCP(name="WorkOS Protected Server", auth=auth)
```
-**Option 2: Automatic provisioning (requires FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.workos.WorkOSProvider)**
+**Option 2: Automatic provisioning (requires FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.workos.provider.WorkOSProvider)**
```python server.py
from fastmcp import FastMCP
diff --git a/docs/v2/servers/auth/authentication.mdx b/docs/v2/servers/auth/authentication.mdx
index c6b829bfe..8821760c5 100644
--- a/docs/v2/servers/auth/authentication.mdx
+++ b/docs/v2/servers/auth/authentication.mdx
@@ -106,7 +106,7 @@ For example, the built-in `AuthKitProvider` uses WorkOS AuthKit, which fully sup
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import AuthKitProvider
+from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
auth = AuthKitProvider(
authkit_domain="https://your-project.authkit.app",
@@ -136,7 +136,7 @@ For example, the built-in `GitHubProvider` extends `OAuthProxy` to work with Git
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="Ov23li...", # Your GitHub OAuth App ID
@@ -202,11 +202,11 @@ Authentication providers are configured by specifying the full module path to th
The full module path to the authentication provider class. Examples:
-- `fastmcp.server.auth.providers.github.GitHubProvider` - GitHub OAuth
-- `fastmcp.server.auth.providers.google.GoogleProvider` - Google OAuth
+- `fastmcp.server.plugins.auth.github.provider.GitHubProvider` - GitHub OAuth
+- `fastmcp.server.plugins.auth.google.provider.GoogleProvider` - Google OAuth
- `fastmcp.server.auth.providers.jwt.JWTVerifier` - JWT token verification
-- `fastmcp.server.auth.providers.workos.WorkOSProvider` - WorkOS OAuth
-- `fastmcp.server.auth.providers.workos.AuthKitProvider` - WorkOS AuthKit
+- `fastmcp.server.plugins.auth.workos.provider.WorkOSProvider` - WorkOS OAuth
+- `fastmcp.server.plugins.auth.authkit.provider.AuthKitProvider` - WorkOS AuthKit
- `mycompany.auth.CustomProvider` - Your custom provider class
@@ -214,12 +214,12 @@ When using providers like GitHub or Google, you'll need to set provider-specific
```bash
# GitHub OAuth
-export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider
+export FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.github.provider.GitHubProvider
export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID="Ov23li..."
export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET="github_pat_..."
# Google OAuth
-export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.google.GoogleProvider
+export FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.google.provider.GoogleProvider
export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID="123456.apps.googleusercontent.com"
export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET="GOCSPX-..."
```
diff --git a/docs/v2/servers/auth/oauth-proxy.mdx b/docs/v2/servers/auth/oauth-proxy.mdx
index eef3bce1c..f82090a32 100644
--- a/docs/v2/servers/auth/oauth-proxy.mdx
+++ b/docs/v2/servers/auth/oauth-proxy.mdx
@@ -345,7 +345,7 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
FastMCP includes pre-configured providers for common services:
```python
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="your-github-app-id",
@@ -590,7 +590,7 @@ For production deployments, configure the OAuth proxy through environment variab
```bash
# Specify the provider implementation
-export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.github.GitHubProvider
+export FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.github.provider.GitHubProvider
# Provider-specific credentials
export FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID="Ov23li..."
diff --git a/docs/v2/servers/auth/oidc-proxy.mdx b/docs/v2/servers/auth/oidc-proxy.mdx
index 750298298..33f37db23 100644
--- a/docs/v2/servers/auth/oidc-proxy.mdx
+++ b/docs/v2/servers/auth/oidc-proxy.mdx
@@ -218,7 +218,7 @@ auth = OIDCProxy(
FastMCP includes pre-configured OIDC providers for common services:
```python
-from fastmcp.server.auth.providers.auth0 import Auth0Provider
+from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
auth = Auth0Provider(
config_url="https://.../.well-known/openid-configuration",
@@ -247,7 +247,7 @@ For production deployments, configure the OIDC proxy through environment variabl
```bash
# Specify the provider implementation
-export FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.auth0.Auth0Provider
+export FASTMCP_SERVER_AUTH=fastmcp.server.plugins.auth.auth0.provider.Auth0Provider
# Provider-specific credentials
export FASTMCP_SERVER_AUTH_AUTH0_CONFIG_URL=https://.../.well-known/openid-configuration
diff --git a/docs/v2/servers/auth/remote-oauth.mdx b/docs/v2/servers/auth/remote-oauth.mdx
index 39c43c559..b1bd1be36 100644
--- a/docs/v2/servers/auth/remote-oauth.mdx
+++ b/docs/v2/servers/auth/remote-oauth.mdx
@@ -183,7 +183,7 @@ WorkOS AuthKit provides an excellent example of remote OAuth integration. The `A
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.workos import AuthKitProvider
+from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
auth = AuthKitProvider(
authkit_domain="https://your-project.authkit.app",
diff --git a/docs/v2/servers/storage-backends.mdx b/docs/v2/servers/storage-backends.mdx
index 25b8580b0..2cfd08321 100644
--- a/docs/v2/servers/storage-backends.mdx
+++ b/docs/v2/servers/storage-backends.mdx
@@ -57,7 +57,7 @@ middleware = ResponseCachingMiddleware(
Or with OAuth token storage:
```python
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.disk import DiskStore
auth = GitHubProvider(
@@ -110,7 +110,7 @@ For OAuth token storage:
```python
import os
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.redis import RedisStore
auth = GitHubProvider(
@@ -162,7 +162,7 @@ By default, FastMCP automatically manages keys and storage based on your platfor
No configuration needed:
```python
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
auth = GitHubProvider(
client_id="your-id",
@@ -177,7 +177,7 @@ For production deployments, configure explicit keys and persistent network-acces
```python
import os
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from key_value.aio.stores.redis import RedisStore
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
from cryptography.fernet import Fernet
diff --git a/src/fastmcp/server/auth/providers/auth0.py b/src/fastmcp/server/auth/providers/auth0.py
index 28c50d060..ab716d3bb 100644
--- a/src/fastmcp/server/auth/providers/auth0.py
+++ b/src/fastmcp/server/auth/providers/auth0.py
@@ -1,135 +1,20 @@
-"""Auth0 OAuth provider for FastMCP.
+"""Backward compatibility shim for Auth0 auth provider."""
-This module provides a complete Auth0 integration that's ready to use with
-just the configuration URL, client ID, client secret, audience, and base URL.
+from __future__ import annotations
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.auth0 import Auth0Provider
+import warnings
- # Simple Auth0 OAuth protection
- auth = Auth0Provider(
- config_url="https://auth0.config.url",
- client_id="your-auth0-client-id",
- client_secret="your-auth0-client-secret",
- audience="your-auth0-api-audience",
- base_url="http://localhost:8000",
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
+
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.auth0 is deprecated. "
+ "Import from fastmcp.server.plugins.auth.auth0.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
)
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-"""
+from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
-from typing import Literal
-
-from key_value.aio.protocols import AsyncKeyValue
-from pydantic import AnyHttpUrl
-
-from fastmcp.server.auth.oidc_proxy import OIDCProxy
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
-
-logger = get_logger(__name__)
-
-
-class Auth0Provider(OIDCProxy):
- """An Auth0 provider implementation for FastMCP.
-
- This provider is a complete Auth0 integration that's ready to use with
- just the configuration URL, client ID, client secret, audience, and base URL.
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.auth0 import Auth0Provider
-
- # Simple Auth0 OAuth protection
- auth = Auth0Provider(
- config_url="https://auth0.config.url",
- client_id="your-auth0-client-id",
- client_secret="your-auth0-client-secret",
- audience="your-auth0-api-audience",
- base_url="http://localhost:8000",
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- config_url: AnyHttpUrl | str,
- client_id: str,
- client_secret: str,
- audience: str,
- base_url: AnyHttpUrl | str,
- resource_base_url: AnyHttpUrl | str | None = None,
- issuer_url: AnyHttpUrl | str | None = None,
- required_scopes: list[str] | None = None,
- redirect_path: str | None = None,
- allowed_client_redirect_uris: list[str] | None = None,
- client_storage: AsyncKeyValue | None = None,
- jwt_signing_key: str | bytes | None = None,
- require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_csp_policy: str | None = None,
- forward_resource: bool = True,
- ) -> None:
- """Initialize Auth0 OAuth provider.
-
- Args:
- config_url: Auth0 config URL
- client_id: Auth0 application client id
- client_secret: Auth0 application client secret
- audience: Auth0 API audience
- base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
- resource_base_url: Optional public base URL for the protected resource metadata
- and token audience. Defaults to ``base_url``.
- issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
- to avoid 404s during discovery when mounting under a path.
- required_scopes: Required Auth0 scopes (defaults to ["openid"])
- redirect_path: Redirect path configured in Auth0 application
- allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
- If None (default), all URIs are allowed. If empty list, no URIs are allowed.
- client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, an encrypted file store will be created in the data directory
- (derived from `platformdirs`).
- jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
- they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
- provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
- require_authorization_consent: Whether to require user consent before authorizing clients (default True).
- When True, users see a consent screen before being redirected to Auth0.
- When False, authorization proceeds directly without user confirmation.
- When "external", the built-in consent screen is skipped but no warning is
- logged, indicating that consent is handled externally (e.g. by the upstream IdP).
- SECURITY WARNING: Only set to False for local development or testing environments.
- """
- # Parse scopes if provided as string
- auth0_required_scopes = (
- parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
- )
-
- super().__init__(
- config_url=config_url,
- client_id=client_id,
- client_secret=client_secret,
- audience=audience,
- base_url=base_url,
- resource_base_url=resource_base_url,
- issuer_url=issuer_url,
- redirect_path=redirect_path,
- required_scopes=auth0_required_scopes,
- allowed_client_redirect_uris=allowed_client_redirect_uris,
- client_storage=client_storage,
- jwt_signing_key=jwt_signing_key,
- require_authorization_consent=require_authorization_consent,
- consent_csp_policy=consent_csp_policy,
- forward_resource=forward_resource,
- )
-
- logger.debug(
- "Initialized Auth0 OAuth provider for client %s with scopes: %s",
- client_id,
- auth0_required_scopes,
- )
+__all__ = ["Auth0Provider"]
diff --git a/src/fastmcp/server/auth/providers/aws.py b/src/fastmcp/server/auth/providers/aws.py
index 6837dc5b5..5a3b63d23 100644
--- a/src/fastmcp/server/auth/providers/aws.py
+++ b/src/fastmcp/server/auth/providers/aws.py
@@ -1,229 +1,23 @@
-"""AWS Cognito OAuth provider for FastMCP.
-
-This module provides a complete AWS Cognito OAuth integration that's ready to use
-with a user pool ID, domain prefix, client ID and client secret. It handles all
-the complexity of AWS Cognito's OAuth flow, token validation, and user management.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider
-
- # Simple AWS Cognito OAuth protection
- auth = AWSCognitoProvider(
- user_pool_id="your-user-pool-id",
- aws_region="eu-central-1",
- client_id="your-cognito-client-id",
- client_secret="your-cognito-client-secret"
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-"""
+"""Backward compatibility shim for AWS Cognito auth provider."""
from __future__ import annotations
-from typing import Literal
+import warnings
-from key_value.aio.protocols import AsyncKeyValue
-from pydantic import AnyHttpUrl
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-from fastmcp.server.auth.auth import AccessToken
-from fastmcp.server.auth.oidc_proxy import OIDCProxy
-from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.aws is deprecated. "
+ "Import from fastmcp.server.plugins.auth.aws.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
-logger = get_logger(__name__)
+from fastmcp.server.plugins.auth.aws.provider import (
+ AWSCognitoProvider,
+ AWSCognitoTokenVerifier,
+)
-
-class AWSCognitoTokenVerifier(JWTVerifier):
- """Token verifier for Cognito access tokens.
-
- Cognito access tokens use a ``client_id`` claim instead of the
- standard ``aud`` claim. This subclass passes ``audience=None``
- to the parent (skipping the ``aud`` check) and validates the
- ``client_id`` claim directly.
- """
-
- def __init__(self, *, audience: str | list[str] | None = None, **kwargs):
- self._expected_client_id = audience
- super().__init__(audience=None, **kwargs)
-
- async def verify_token(self, token: str) -> AccessToken | None:
- """Verify token and filter claims to Cognito-specific subset."""
- access_token = await super().verify_token(token)
- if not access_token:
- return None
-
- # Validate client_id claim (Cognito's equivalent of aud)
- if self._expected_client_id:
- token_client_id = access_token.claims.get("client_id")
- if isinstance(self._expected_client_id, list):
- valid = token_client_id in self._expected_client_id
- else:
- valid = token_client_id == self._expected_client_id
- if not valid:
- self.logger.debug(
- "Token validation failed: client_id mismatch (expected %s, got %s)",
- self._expected_client_id,
- token_client_id,
- )
- return None
-
- # Filter claims to Cognito-specific subset
- cognito_claims = {
- "sub": access_token.claims.get("sub"),
- "username": access_token.claims.get("username"),
- "cognito:groups": access_token.claims.get("cognito:groups", []),
- }
-
- return AccessToken(
- token=access_token.token,
- client_id=access_token.client_id,
- scopes=access_token.scopes,
- expires_at=access_token.expires_at,
- claims=cognito_claims,
- )
-
-
-class AWSCognitoProvider(OIDCProxy):
- """Complete AWS Cognito OAuth provider for FastMCP.
-
- This provider makes it trivial to add AWS Cognito OAuth protection to any
- FastMCP server using OIDC Discovery. Just provide your Cognito User Pool details,
- client credentials, and a base URL, and you're ready to go.
-
- Features:
- - Automatic OIDC Discovery from AWS Cognito User Pool
- - Automatic JWT token validation via Cognito's public keys
- - Cognito-specific claim filtering (sub, username, cognito:groups)
- - Support for Cognito User Pools
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider
-
- auth = AWSCognitoProvider(
- user_pool_id="eu-central-1_XXXXXXXXX",
- aws_region="eu-central-1",
- client_id="your-cognito-client-id",
- client_secret="your-cognito-client-secret",
- base_url="https://my-server.com",
- redirect_path="/custom/callback",
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- user_pool_id: str,
- client_id: str,
- client_secret: str,
- base_url: AnyHttpUrl | str,
- resource_base_url: AnyHttpUrl | str | None = None,
- aws_region: str = "eu-central-1",
- issuer_url: AnyHttpUrl | str | None = None,
- redirect_path: str = "/auth/callback",
- required_scopes: list[str] | None = None,
- allowed_client_redirect_uris: list[str] | None = None,
- client_storage: AsyncKeyValue | None = None,
- jwt_signing_key: str | bytes | None = None,
- require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_csp_policy: str | None = None,
- forward_resource: bool = True,
- ):
- """Initialize AWS Cognito OAuth provider.
-
- Args:
- user_pool_id: Your Cognito User Pool ID (e.g., "eu-central-1_XXXXXXXXX")
- client_id: Cognito app client ID
- client_secret: Cognito app client secret
- base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
- resource_base_url: Optional public base URL for the protected resource metadata
- and token audience. Defaults to ``base_url``.
- aws_region: AWS region where your User Pool is located (defaults to "eu-central-1")
- issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
- to avoid 404s during discovery when mounting under a path.
- redirect_path: Redirect path configured in Cognito app (defaults to "/auth/callback")
- required_scopes: Required Cognito scopes (defaults to ["openid"])
- allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
- If None (default), all URIs are allowed. If empty list, no URIs are allowed.
- client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, an encrypted file store will be created in the data directory
- (derived from `platformdirs`).
- jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
- they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
- provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
- require_authorization_consent: Whether to require user consent before authorizing clients (default True).
- When True, users see a consent screen before being redirected to AWS Cognito.
- When False, authorization proceeds directly without user confirmation.
- When "external", the built-in consent screen is skipped but no warning is
- logged, indicating that consent is handled externally (e.g. by the upstream IdP).
- SECURITY WARNING: Only set to False for local development or testing environments.
- """
- # Parse scopes if provided as string
- required_scopes_final = (
- parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
- )
-
- # Construct OIDC discovery URL
- config_url = f"https://cognito-idp.{aws_region}.amazonaws.com/{user_pool_id}/.well-known/openid-configuration"
-
- # Store Cognito-specific info for claim filtering
- self.user_pool_id = user_pool_id
- self.aws_region = aws_region
- self.client_id = client_id
-
- # Initialize OIDC proxy with Cognito discovery
- super().__init__(
- config_url=config_url,
- client_id=client_id,
- client_secret=client_secret,
- algorithm="RS256",
- required_scopes=required_scopes_final,
- base_url=base_url,
- resource_base_url=resource_base_url,
- issuer_url=issuer_url,
- redirect_path=redirect_path,
- allowed_client_redirect_uris=allowed_client_redirect_uris,
- client_storage=client_storage,
- jwt_signing_key=jwt_signing_key,
- require_authorization_consent=require_authorization_consent,
- consent_csp_policy=consent_csp_policy,
- forward_resource=forward_resource,
- )
-
- logger.debug(
- "Initialized AWS Cognito OAuth provider for client %s with scopes: %s",
- client_id,
- required_scopes_final,
- )
-
- def get_token_verifier(
- self,
- *,
- algorithm: str | None = None,
- audience: str | None = None,
- required_scopes: list[str] | None = None,
- timeout_seconds: int | None = None,
- ) -> AWSCognitoTokenVerifier:
- """Creates a Cognito-specific token verifier with claim filtering.
-
- Args:
- algorithm: Optional token verifier algorithm
- audience: Optional token verifier audience
- required_scopes: Optional token verifier required_scopes
- timeout_seconds: HTTP request timeout in seconds
- """
- return AWSCognitoTokenVerifier(
- issuer=str(self.oidc_config.issuer),
- audience=audience or self.client_id,
- algorithm=algorithm,
- jwks_uri=str(self.oidc_config.jwks_uri),
- required_scopes=required_scopes,
- )
+__all__ = ["AWSCognitoProvider", "AWSCognitoTokenVerifier"]
diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py
index 0f0763b97..a4f1d0dab 100644
--- a/src/fastmcp/server/auth/providers/azure.py
+++ b/src/fastmcp/server/auth/providers/azure.py
@@ -1,768 +1,24 @@
-"""Azure (Microsoft Entra) OAuth provider for FastMCP.
-
-This provider implements Azure/Microsoft Entra ID OAuth authentication
-using the OAuth Proxy pattern for non-DCR OAuth flows.
-"""
+"""Backward compatibility shim for Azure auth provider."""
from __future__ import annotations
-import hashlib
-from collections import OrderedDict
-from typing import TYPE_CHECKING, Any, Literal, cast
-
-import httpx
-from key_value.aio.protocols import AsyncKeyValue
-
-from fastmcp.dependencies import Dependency
-from fastmcp.server.auth.auth import MultiAuth
-from fastmcp.server.auth.oauth_proxy import OAuthProxy
-from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.utilities.auth import decode_jwt_payload, parse_scopes
-from fastmcp.utilities.logging import get_logger
-
-if TYPE_CHECKING:
- from azure.identity.aio import OnBehalfOfCredential
- from mcp.server.auth.provider import AuthorizationParams
- from mcp.shared.auth import OAuthClientInformationFull
- from pydantic import AnyHttpUrl
-
- from fastmcp.server.auth.auth import AuthProvider
-
-logger = get_logger(__name__)
-
-# Standard OIDC scopes that should never be prefixed with identifier_uri.
-# Per Microsoft docs: https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc
-# "OIDC scopes are requested as simple string identifiers without resource prefixes"
-OIDC_SCOPES = frozenset({"openid", "profile", "email", "offline_access"})
-
-
-class AzureProvider(OAuthProxy):
- """Azure (Microsoft Entra) OAuth provider for FastMCP.
-
- This provider implements Azure/Microsoft Entra ID authentication using the
- OAuth Proxy pattern. It supports both organizational accounts and personal
- Microsoft accounts depending on the tenant configuration.
-
- Scope Handling:
- - required_scopes: Provide unprefixed scope names (e.g., ["read", "write"])
- → Automatically prefixed with identifier_uri during initialization
- → Validated on all tokens and advertised to MCP clients
- - additional_authorize_scopes: Provide full format (e.g., ["User.Read"])
- → NOT prefixed, NOT validated, NOT advertised to clients
- → Used to request Microsoft Graph or other upstream API permissions
-
- Features:
- - OAuth proxy to Azure/Microsoft identity platform
- - JWT validation using tenant issuer and JWKS
- - Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
- - Custom API scopes and Microsoft Graph scopes in a single provider
-
- Setup:
- 1. Create an App registration in Azure Portal
- 2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
- 3. Add an Application ID URI under "Expose an API" (defaults to api://{client_id})
- 4. Add custom scopes (e.g., "read", "write") under "Expose an API"
- 5. Set access token version to 2 in the App manifest: "requestedAccessTokenVersion": 2
- 6. Create a client secret
- 7. Get Application (client) ID, Directory (tenant) ID, and client secret
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.azure import AzureProvider
-
- # Standard Azure (Public Cloud)
- auth = AzureProvider(
- client_id="your-client-id",
- client_secret="your-client-secret",
- tenant_id="your-tenant-id",
- required_scopes=["read", "write"], # Unprefixed scope names
- additional_authorize_scopes=["User.Read", "Mail.Read"], # Optional Graph scopes
- base_url="http://localhost:8000",
- # identifier_uri defaults to api://{client_id}
- )
-
- # Azure Government
- auth_gov = AzureProvider(
- client_id="your-client-id",
- client_secret="your-client-secret",
- tenant_id="your-tenant-id",
- required_scopes=["read", "write"],
- base_authority="login.microsoftonline.us", # Override for Azure Gov
- base_url="http://localhost:8000",
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- client_id: str,
- client_secret: str | None = None,
- tenant_id: str,
- required_scopes: list[str],
- base_url: str,
- resource_base_url: AnyHttpUrl | str | None = None,
- identifier_uri: str | None = None,
- issuer_url: str | None = None,
- redirect_path: str | None = None,
- additional_authorize_scopes: list[str] | None = None,
- allowed_client_redirect_uris: list[str] | None = None,
- client_storage: AsyncKeyValue | None = None,
- jwt_signing_key: str | bytes | None = None,
- require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_csp_policy: str | None = None,
- forward_resource: bool = True,
- base_authority: str = "login.microsoftonline.com",
- http_client: httpx.AsyncClient | None = None,
- enable_cimd: bool = True,
- ) -> None:
- """Initialize Azure OAuth provider.
-
- Args:
- client_id: Azure application (client) ID from your App registration
- client_secret: Azure client secret from your App registration. Optional when
- using alternative credentials (e.g., managed identity with a custom
- _create_upstream_oauth_client override). When omitted, jwt_signing_key
- must be provided.
- tenant_id: Azure tenant ID (specific tenant GUID, "organizations", or "consumers")
- identifier_uri: Optional Application ID URI for your custom API (defaults to api://{client_id}).
- This URI is automatically prefixed to all required_scopes during initialization.
- Example: identifier_uri="api://my-api" + required_scopes=["read"]
- → tokens validated for "api://my-api/read"
- base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
- resource_base_url: Optional public base URL for the protected resource metadata
- and token audience. Defaults to ``base_url``.
- issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
- to avoid 404s during discovery when mounting under a path.
- redirect_path: Redirect path configured in Azure App registration (defaults to "/auth/callback")
- base_authority: Azure authority base URL (defaults to "login.microsoftonline.com").
- For Azure Government, use "login.microsoftonline.us".
- required_scopes: Custom API scope names WITHOUT prefix (e.g., ["read", "write"]).
- - Automatically prefixed with identifier_uri during initialization
- - Validated on all tokens
- - Advertised in Protected Resource Metadata
- - Must match scope names defined in Azure Portal under "Expose an API"
- Example: ["read", "write"] → validates tokens containing ["api://xxx/read", "api://xxx/write"]
- additional_authorize_scopes: Microsoft Graph or other upstream scopes in full format.
- - NOT prefixed with identifier_uri
- - NOT validated on tokens
- - NOT advertised to MCP clients
- - Used to request additional permissions from Azure (e.g., Graph API access)
- Example: ["User.Read", "Mail.Read"]
- These scopes allow your FastMCP server to call Microsoft Graph APIs using the
- upstream Azure token, but MCP clients are unaware of them.
- Note: "offline_access" is automatically included to obtain refresh tokens.
- allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
- If None (default), all URIs are allowed. If empty list, no URIs are allowed.
- client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, an encrypted file store will be created in the data directory
- (derived from `platformdirs`).
- jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
- they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
- provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
- require_authorization_consent: Whether to require user consent before authorizing clients (default True).
- When True, users see a consent screen before being redirected to Azure.
- When False, authorization proceeds directly without user confirmation.
- When "external", the built-in consent screen is skipped but no warning is
- logged, indicating that consent is handled externally (e.g. by the upstream IdP).
- SECURITY WARNING: Only set to False for local development or testing environments.
- http_client: Optional httpx.AsyncClient for connection pooling in JWKS fetches.
- When provided, the client is reused for JWT key fetches and the caller
- is responsible for its lifecycle. When None (default), a fresh client is created per fetch.
- enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
- client IDs (default True). Set to False to disable.
- """
- # Parse scopes if provided as string
- parsed_required_scopes = parse_scopes(required_scopes)
- parsed_additional_scopes: list[str] = (
- parse_scopes(additional_authorize_scopes) or []
- if additional_authorize_scopes
- else []
- )
-
- # Always include offline_access to get refresh tokens from Azure
- if "offline_access" not in parsed_additional_scopes:
- parsed_additional_scopes = [*parsed_additional_scopes, "offline_access"]
-
- # Store Azure-specific config for OBO credential creation
- self._tenant_id = tenant_id
- self._base_authority = base_authority
-
- # Cache of OBO credentials keyed by hash of user assertion token.
- # Reusing credentials allows the Azure SDK's internal token cache
- # to avoid redundant OBO exchanges for the same user + scopes.
- self._obo_credentials: OrderedDict[str, OnBehalfOfCredential] = OrderedDict()
- self._obo_max_credentials: int = 128
-
- # Apply defaults
- self.identifier_uri = identifier_uri or f"api://{client_id}"
- self.additional_authorize_scopes: list[str] = parsed_additional_scopes
-
- # Always validate tokens against the app's API client ID using JWT
- issuer = f"https://{base_authority}/{tenant_id}/v2.0"
- jwks_uri = f"https://{base_authority}/{tenant_id}/discovery/v2.0/keys"
-
- # Azure access tokens only include custom API scopes in the `scp` claim,
- # NOT standard OIDC scopes (openid, profile, email, offline_access).
- # Filter out OIDC scopes from validation - they'll still be sent to Azure
- # during authorization (handled by _prefix_scopes_for_azure).
- validation_scopes = [
- s for s in (parsed_required_scopes or []) if s not in OIDC_SCOPES
- ]
- if not validation_scopes:
- raise ValueError(
- "AzureProvider requires at least one non-OIDC scope in "
- "required_scopes (e.g., 'read', 'write'). OIDC scopes like "
- "'openid', 'profile', 'email', and 'offline_access' are not "
- "included in Azure access token claims and cannot be used for "
- "scope enforcement."
- )
-
- token_verifier = JWTVerifier(
- jwks_uri=jwks_uri,
- issuer=issuer,
- audience=[client_id, self.identifier_uri],
- algorithm="RS256",
- required_scopes=validation_scopes, # Only validate non-OIDC scopes
- http_client=http_client,
- )
-
- # Build Azure OAuth endpoints with tenant
- authorization_endpoint = (
- f"https://{base_authority}/{tenant_id}/oauth2/v2.0/authorize"
- )
- token_endpoint = f"https://{base_authority}/{tenant_id}/oauth2/v2.0/token"
-
- # Initialize OAuth proxy with Azure endpoints
- # Remember there's hooks called, such as _prepare_scopes_for_token_exchange
- # and _prepare_scopes_for_upstream_refresh
- super().__init__(
- upstream_authorization_endpoint=authorization_endpoint,
- upstream_token_endpoint=token_endpoint,
- upstream_client_id=client_id,
- upstream_client_secret=client_secret,
- token_verifier=token_verifier,
- base_url=base_url,
- resource_base_url=resource_base_url,
- redirect_path=redirect_path,
- issuer_url=issuer_url or base_url, # Default to base_url if not specified
- allowed_client_redirect_uris=allowed_client_redirect_uris,
- client_storage=client_storage,
- jwt_signing_key=jwt_signing_key,
- require_authorization_consent=require_authorization_consent,
- consent_csp_policy=consent_csp_policy,
- forward_resource=forward_resource,
- valid_scopes=parsed_required_scopes,
- enable_cimd=enable_cimd,
- )
-
- authority_info = ""
- if base_authority != "login.microsoftonline.com":
- authority_info = f" using authority {base_authority}"
- logger.info(
- "Initialized Azure OAuth provider for client %s with tenant %s%s%s",
- client_id,
- tenant_id,
- f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
- authority_info,
- )
-
- async def authorize(
- self,
- client: OAuthClientInformationFull,
- params: AuthorizationParams,
- ) -> str:
- """Start OAuth transaction and redirect to Azure AD.
-
- Override parent's authorize method to filter out the 'resource' parameter
- which is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use
- scopes to determine the resource/audience instead of a separate parameter.
-
- Args:
- client: OAuth client information
- params: Authorization parameters from the client
-
- Returns:
- Authorization URL to redirect the user to Azure AD
- """
- # Clear the resource parameter that Azure AD v2.0 doesn't support
- # This parameter comes from RFC 8707 (OAuth 2.0 Resource Indicators)
- # but Azure AD v2.0 uses scopes instead to determine the audience
- params_to_use = params
- if hasattr(params, "resource"):
- original_resource = getattr(params, "resource", None)
- if original_resource is not None:
- params_to_use = params.model_copy(update={"resource": None})
- if original_resource:
- logger.debug(
- "Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
- original_resource,
- )
- # Don't modify the scopes in params - they stay unprefixed for MCP clients
- # We'll prefix them when building the Azure authorization URL (in _build_upstream_authorize_url)
- auth_url = await super().authorize(client, params_to_use)
- separator = "&" if "?" in auth_url else "?"
- return f"{auth_url}{separator}prompt=select_account"
-
- def _prefix_scopes_for_azure(self, scopes: list[str]) -> list[str]:
- """Prefix unprefixed custom API scopes with identifier_uri for Azure.
-
- This helper centralizes the scope prefixing logic used in both
- authorization and token refresh flows.
-
- Scopes that are NOT prefixed:
- - Standard OIDC scopes (openid, profile, email, offline_access)
- - Fully-qualified URIs (contain "://")
- - Scopes with path component (contain "/")
-
- Note: Microsoft Graph scopes (e.g., User.Read) should be passed via
- `additional_authorize_scopes` or use fully-qualified format
- (e.g., https://graph.microsoft.com/User.Read).
-
- Args:
- scopes: List of scopes, may be prefixed or unprefixed
-
- Returns:
- List of scopes with identifier_uri prefix applied where needed
- """
- prefixed = []
- for scope in scopes:
- if scope in OIDC_SCOPES:
- # Standard OIDC scopes - never prefix
- prefixed.append(scope)
- elif "://" in scope or "/" in scope:
- # Already fully-qualified (e.g., "api://xxx/read" or
- # "https://graph.microsoft.com/User.Read")
- prefixed.append(scope)
- else:
- # Unprefixed custom API scope - prefix with identifier_uri
- prefixed.append(f"{self.identifier_uri}/{scope}")
- return prefixed
-
- def _build_upstream_authorize_url(
- self, txn_id: str, transaction: dict[str, Any]
- ) -> str:
- """Build Azure authorization URL with prefixed scopes.
-
- Overrides parent to prefix scopes with identifier_uri before sending to Azure,
- while keeping unprefixed scopes in the transaction for MCP clients.
- """
- # Get unprefixed scopes from transaction
- unprefixed_scopes = transaction.get("scopes") or self.required_scopes or []
-
- # Prefix scopes for Azure authorization request
- prefixed_scopes = self._prefix_scopes_for_azure(unprefixed_scopes)
-
- # Add Microsoft Graph scopes (not validated, not prefixed)
- if self.additional_authorize_scopes:
- prefixed_scopes.extend(self.additional_authorize_scopes)
-
- # Temporarily modify transaction dict for parent's URL building
- modified_transaction = transaction.copy()
- modified_transaction["scopes"] = prefixed_scopes
-
- # Let parent build the URL with prefixed scopes
- return super()._build_upstream_authorize_url(txn_id, modified_transaction)
-
- def _prepare_scopes_for_token_exchange(self, scopes: list[str]) -> list[str]:
- """Prepare scopes for Azure authorization code exchange.
-
- Azure requires scopes during token exchange (AADSTS28003 error if missing).
- Azure only allows ONE resource per token request (AADSTS28000), so we only
- include scopes for this API plus OIDC scopes.
-
- Args:
- scopes: Scopes from the authorization request (unprefixed)
-
- Returns:
- List of scopes for Azure token endpoint
- """
- # Prefix scopes for this API
- prefixed_scopes = self._prefix_scopes_for_azure(scopes or [])
-
- # Add OIDC scopes only (not other API scopes) to avoid AADSTS28000
- if self.additional_authorize_scopes:
- prefixed_scopes.extend(
- s for s in self.additional_authorize_scopes if s in OIDC_SCOPES
- )
-
- deduplicated = list(dict.fromkeys(prefixed_scopes))
- logger.debug("Token exchange scopes: %s", deduplicated)
- return deduplicated
-
- def _prepare_scopes_for_upstream_refresh(self, scopes: list[str]) -> list[str]:
- """Prepare scopes for Azure token refresh.
-
- Azure requires fully-qualified scopes and only allows ONE resource per
- token request (AADSTS28000). We include scopes for this API plus OIDC scopes.
-
- Args:
- scopes: Base scopes from RefreshToken (unprefixed, e.g., ["read"])
-
- Returns:
- Deduplicated list of scopes formatted for Azure token endpoint
- """
- logger.debug("Base scopes from storage: %s", scopes)
-
- # Filter out any additional_authorize_scopes that may have been stored
- additional_scopes_set = set(self.additional_authorize_scopes or [])
- base_scopes = [s for s in scopes if s not in additional_scopes_set]
-
- # Prefix base scopes with identifier_uri for Azure
- prefixed_scopes = self._prefix_scopes_for_azure(base_scopes)
-
- # Add OIDC scopes only (not other API scopes) to avoid AADSTS28000
- if self.additional_authorize_scopes:
- prefixed_scopes.extend(
- s for s in self.additional_authorize_scopes if s in OIDC_SCOPES
- )
-
- deduplicated_scopes = list(dict.fromkeys(prefixed_scopes))
- logger.debug("Scopes for Azure token endpoint: %s", deduplicated_scopes)
- return deduplicated_scopes
-
- async def _extract_upstream_claims(
- self, idp_tokens: dict[str, Any]
- ) -> dict[str, Any] | None:
- """Extract claims from Azure token response to embed in FastMCP JWT.
-
- Decodes the Azure access token (which is a JWT) to extract user identity
- claims. This allows gateways to inspect upstream identity information by
- decoding the FastMCP JWT without needing server-side storage lookups.
-
- Azure access tokens contain claims like:
- - sub: Subject identifier (unique per user per application)
- - oid: Object ID (unique user identifier across Azure AD)
- - tid: Tenant ID
- - azp: Authorized party (client ID that requested the token)
- - name: Display name
- - given_name: First name
- - family_name: Last name
- - preferred_username: User principal name (email format)
- - upn: User Principal Name
- - email: Email address (if available)
- - roles: Application roles assigned to the user
- - groups: Group memberships (if configured)
-
- Args:
- idp_tokens: Full token response from Azure, containing access_token
- and potentially id_token.
-
- Returns:
- Dict of extracted claims, or None if extraction fails.
- """
- access_token = idp_tokens.get("access_token")
- if not access_token:
- return None
-
- try:
- # Azure access tokens are JWTs - decode without verification
- # (already validated by token_verifier during token exchange)
- payload = decode_jwt_payload(access_token)
-
- # Extract useful identity claims
- claims: dict[str, Any] = {}
- claim_keys = [
- "sub",
- "oid",
- "tid",
- "azp",
- "name",
- "given_name",
- "family_name",
- "preferred_username",
- "upn",
- "email",
- "roles",
- "groups",
- ]
- for claim in claim_keys:
- if claim in payload:
- claims[claim] = payload[claim]
-
- if claims:
- logger.debug(
- "Extracted %d Azure claims for embedding in FastMCP JWT",
- len(claims),
- )
- return claims
-
- return None
-
- except Exception as e:
- logger.debug("Failed to extract Azure claims: %s", e)
- return None
-
- async def get_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential:
- """Get a cached or new OnBehalfOfCredential for OBO token exchange.
-
- Credentials are cached by user assertion so the Azure SDK's internal
- token cache can avoid redundant OBO exchanges when the same user
- calls multiple tools with the same scopes.
-
- Args:
- user_assertion: The user's access token to exchange via OBO.
-
- Returns:
- A configured OnBehalfOfCredential ready for get_token() calls.
-
- Raises:
- ImportError: If azure-identity is not installed (requires fastmcp[azure]).
- """
- _require_azure_identity("OBO token exchange")
- from azure.identity.aio import OnBehalfOfCredential
-
- key = hashlib.sha256(user_assertion.encode()).hexdigest()
-
- if key in self._obo_credentials:
- self._obo_credentials.move_to_end(key)
- return self._obo_credentials[key]
-
- obo_kwargs: dict[str, Any] = {
- "tenant_id": self._tenant_id,
- "client_id": self._upstream_client_id,
- "user_assertion": user_assertion,
- "authority": f"https://{self._base_authority}",
- }
- if self._upstream_client_secret is not None:
- obo_kwargs["client_secret"] = (
- self._upstream_client_secret.get_secret_value()
- )
- else:
- raise ValueError(
- "OBO token exchange requires either a client_secret or a subclass "
- "that overrides get_obo_credential() to provide alternative credentials "
- "(e.g., client_assertion_func for managed identity)."
- )
- credential = OnBehalfOfCredential(**obo_kwargs)
- self._obo_credentials[key] = credential
-
- # Evict oldest if over capacity
- while len(self._obo_credentials) > self._obo_max_credentials:
- _, evicted = self._obo_credentials.popitem(last=False)
- await evicted.close()
-
- return credential
-
- async def close_obo_credentials(self) -> None:
- """Close all cached OBO credentials."""
- credentials = list(self._obo_credentials.values())
- self._obo_credentials.clear()
- for credential in credentials:
- try:
- await credential.close()
- except Exception:
- logger.debug("Error closing OBO credential", exc_info=True)
-
-
-class AzureJWTVerifier(JWTVerifier):
- """JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
-
- Auto-configures JWKS URI, issuer, audience, and scope handling from your
- Azure app registration details. Designed for Managed Identity and other
- token-verification-only scenarios where AzureProvider's full OAuth proxy
- isn't needed.
-
- Handles Azure's scope format automatically:
- - Validates tokens using short-form scopes (what Azure puts in ``scp`` claims)
- - Advertises full-URI scopes in OAuth metadata (what clients need to request)
-
- Example::
-
- from fastmcp.server.auth import RemoteAuthProvider
- from fastmcp.server.auth.providers.azure import AzureJWTVerifier
- from pydantic import AnyHttpUrl
-
- verifier = AzureJWTVerifier(
- client_id="your-client-id",
- tenant_id="your-tenant-id",
- required_scopes=["access_as_user"],
- )
-
- auth = RemoteAuthProvider(
- token_verifier=verifier,
- authorization_servers=[
- AnyHttpUrl("https://login.microsoftonline.com/your-tenant-id/v2.0")
- ],
- base_url="https://my-server.com",
- )
- """
-
- def __init__(
- self,
- *,
- client_id: str,
- tenant_id: str,
- required_scopes: list[str] | None = None,
- identifier_uri: str | None = None,
- base_authority: str = "login.microsoftonline.com",
- ):
- """Initialize Azure JWT verifier.
-
- Args:
- client_id: Azure application (client) ID from your App registration
- tenant_id: Azure tenant ID (specific tenant GUID, "organizations", or "consumers").
- For multi-tenant apps ("organizations" or "consumers"), issuer validation
- is skipped since Azure tokens carry the actual tenant GUID as issuer.
- required_scopes: Scope names as they appear in Azure Portal under "Expose an API"
- (e.g., ["access_as_user", "read"]). These are validated against
- the short-form scopes in token ``scp`` claims, and automatically
- prefixed with identifier_uri for OAuth metadata.
- identifier_uri: Application ID URI (defaults to ``api://{client_id}``).
- Used to prefix scopes in OAuth metadata so clients know the full
- scope URIs to request from Azure.
- base_authority: Azure authority base URL (defaults to "login.microsoftonline.com").
- For Azure Government, use "login.microsoftonline.us".
- """
- self._identifier_uri = identifier_uri or f"api://{client_id}"
-
- # For multi-tenant apps, Azure tokens carry the actual tenant GUID as
- # issuer, not the literal "organizations" or "consumers" string. Skip
- # issuer validation for these — audience still protects against wrong-app tokens.
- multi_tenant_values = {"organizations", "consumers", "common"}
- issuer: str | None = (
- None
- if tenant_id in multi_tenant_values
- else f"https://{base_authority}/{tenant_id}/v2.0"
- )
-
- super().__init__(
- jwks_uri=f"https://{base_authority}/{tenant_id}/discovery/v2.0/keys",
- issuer=issuer,
- audience=[client_id, self._identifier_uri],
- algorithm="RS256",
- required_scopes=required_scopes,
- )
-
- @property
- def scopes_supported(self) -> list[str]:
- """Return scopes with Azure URI prefix for OAuth metadata.
-
- Azure tokens contain short-form scopes (e.g., ``read``) in the ``scp``
- claim, but clients must request full URI scopes (e.g.,
- ``api://client-id/read``) from the Azure authorization endpoint. This
- property returns the full-URI form for OAuth metadata while
- ``required_scopes`` retains the short form for token validation.
- """
- if not self.required_scopes:
- return []
- prefixed = []
- for scope in self.required_scopes:
- if scope in OIDC_SCOPES or "://" in scope or "/" in scope:
- prefixed.append(scope)
- else:
- prefixed.append(f"{self._identifier_uri}/{scope}")
- return prefixed
-
-
-# --- Dependency injection support ---
-# These require fastmcp[azure] extra for azure-identity
-
-
-def _require_azure_identity(feature: str) -> None:
- """Raise ImportError with install instructions if azure-identity is not available."""
- try:
- import azure.identity # noqa: F401
- except ImportError as e:
- raise ImportError(
- f"{feature} requires the `azure` extra. "
- "Install with: pip install 'fastmcp[azure]'"
- ) from e
-
-
-def _find_azure_provider(auth: AuthProvider | None) -> AzureProvider | None:
- """Extract an AzureProvider from an auth provider, unwrapping MultiAuth if needed."""
- if isinstance(auth, AzureProvider):
- return auth
-
- if isinstance(auth, MultiAuth) and isinstance(auth.server, AzureProvider):
- return auth.server
-
- return None
-
-
-class _EntraOBOToken(Dependency[str]):
- """Dependency that performs OBO token exchange for Microsoft Entra.
-
- Uses azure.identity's OnBehalfOfCredential for async-native OBO,
- with automatic token caching and refresh. Credentials are cached on
- the AzureProvider so repeated tool calls reuse existing credentials
- and benefit from the Azure SDK's internal token cache.
- """
-
- def __init__(self, scopes: list[str]):
- self.scopes = scopes
-
- async def __aenter__(self) -> str:
- _require_azure_identity("EntraOBOToken")
-
- from fastmcp.server.dependencies import get_access_token, get_server
-
- access_token = get_access_token()
- if access_token is None:
- raise RuntimeError(
- "No access token available. Cannot perform OBO exchange."
- )
-
- server = get_server()
- azure_provider = _find_azure_provider(server.auth)
- if azure_provider is None:
- raise RuntimeError(
- "EntraOBOToken requires an AzureProvider as the auth provider. "
- f"Current provider: {type(server.auth).__name__}"
- )
-
- credential = await azure_provider.get_obo_credential(
- user_assertion=access_token.token,
- )
-
- result = await credential.get_token(*self.scopes)
- return result.token
-
-
-def EntraOBOToken(scopes: list[str]) -> str:
- """Exchange the user's Entra token for a downstream API token via OBO.
-
- This dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,
- allowing your MCP server to call downstream APIs (like Microsoft Graph) on
- behalf of the authenticated user.
-
- Args:
- scopes: The scopes to request for the downstream API. For Microsoft Graph,
- use scopes like ["https://graph.microsoft.com/Mail.Read"] or
- ["https://graph.microsoft.com/.default"].
-
- Returns:
- A dependency that resolves to the downstream API access token string
-
- Raises:
- ImportError: If fastmcp[azure] is not installed
- RuntimeError: If no access token is available, provider is not Azure,
- or OBO exchange fails
-
- Example:
- ```python
- from fastmcp.server.auth.providers.azure import EntraOBOToken
- import httpx
-
- @mcp.tool()
- async def get_my_emails(
- graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"])
- ):
- async with httpx.AsyncClient() as client:
- resp = await client.get(
- "https://graph.microsoft.com/v1.0/me/messages",
- headers={"Authorization": f"Bearer {graph_token}"}
- )
- return resp.json()
- ```
-
- Note:
- For OBO to work, ensure the scopes are included in the AzureProvider's
- `additional_authorize_scopes` parameter, and that admin consent has been
- granted for those scopes in your Entra app registration.
- """
- return cast(str, _EntraOBOToken(scopes))
+import warnings
+
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
+
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.azure is deprecated. "
+ "Import from fastmcp.server.plugins.auth.azure.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
+
+from fastmcp.server.plugins.auth.azure.provider import (
+ AzureJWTVerifier,
+ AzureProvider,
+ EntraOBOToken,
+)
+
+__all__ = ["AzureJWTVerifier", "AzureProvider", "EntraOBOToken"]
diff --git a/src/fastmcp/server/auth/providers/clerk.py b/src/fastmcp/server/auth/providers/clerk.py
index b17261573..3ab6d5e93 100644
--- a/src/fastmcp/server/auth/providers/clerk.py
+++ b/src/fastmcp/server/auth/providers/clerk.py
@@ -1,388 +1,23 @@
-"""Clerk OAuth provider for FastMCP.
-
-This module provides a complete Clerk OAuth integration that's ready to use
-with a Clerk domain, client ID, and client secret. It handles all the complexity
-of Clerk's OAuth/OIDC flow, token validation, and user management.
-
-Clerk uses standard OIDC endpoints derived from the instance domain
-(e.g., ``https://.clerk.accounts.dev``). Token verification is
-performed via the introspection endpoint (RFC 7662) for security-critical
-checks (active status, audience, scopes), followed by the userinfo endpoint
-for profile enrichment. Userinfo failure is non-fatal.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.clerk import ClerkProvider
-
- auth = ClerkProvider(
- domain="saving-primate-16.clerk.accounts.dev",
- client_id="your-clerk-client-id",
- client_secret="your-clerk-client-secret",
- base_url="https://my-server.com",
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-"""
+"""Backward compatibility shim for Clerk auth provider."""
from __future__ import annotations
-import contextlib
-from typing import Literal
+import warnings
-import httpx
-from key_value.aio.protocols import AsyncKeyValue
-from pydantic import AnyHttpUrl
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-from fastmcp.server.auth import TokenVerifier
-from fastmcp.server.auth.auth import AccessToken
-from fastmcp.server.auth.oauth_proxy import OAuthProxy
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.clerk is deprecated. "
+ "Import from fastmcp.server.plugins.auth.clerk.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
-logger = get_logger(__name__)
+from fastmcp.server.plugins.auth.clerk.provider import (
+ ClerkProvider,
+ ClerkTokenVerifier,
+)
-
-class ClerkTokenVerifier(TokenVerifier):
- """Token verifier for Clerk OAuth tokens.
-
- Clerk issues standard OIDC tokens. Verification uses the introspection
- endpoint (RFC 7662) as the primary security gate — it confirms the token
- is active and provides metadata (scopes, expiry, audience). The userinfo
- endpoint is called second for profile enrichment (name, email, picture)
- and its failure is non-fatal.
-
- When a ``client_id`` is configured, the audience from introspection is
- validated against it. When ``required_scopes`` are configured,
- introspection must return the token's scopes — the verifier will not
- assume scopes when introspection is unavailable.
- """
-
- def __init__(
- self,
- *,
- domain: str,
- client_id: str | None = None,
- client_secret: str | None = None,
- required_scopes: list[str] | None = None,
- timeout_seconds: int = 10,
- http_client: httpx.AsyncClient | None = None,
- ):
- """Initialize the Clerk token verifier.
-
- Args:
- domain: Clerk instance domain (e.g., "saving-primate-16.clerk.accounts.dev")
- client_id: Clerk OAuth client ID, used for introspection endpoint authentication
- client_secret: Clerk OAuth client secret, used for introspection endpoint authentication
- required_scopes: Required OAuth scopes (e.g., ["openid", "email", "profile"])
- timeout_seconds: HTTP request timeout
- http_client: Optional httpx.AsyncClient for connection pooling. When provided,
- the client is reused across calls and the caller is responsible for its
- lifecycle. When None (default), a fresh client is created per call.
- """
- super().__init__(required_scopes=required_scopes)
- self.domain = domain.rstrip("/")
- self._client_id = client_id
- self._client_secret = client_secret
- self.timeout_seconds = timeout_seconds
- self._http_client = http_client
-
- self._userinfo_url = f"https://{self.domain}/oauth/userinfo"
- self._introspection_url = f"https://{self.domain}/oauth/token_info"
-
- async def verify_token(self, token: str) -> AccessToken | None:
- """Verify a Clerk OAuth token via introspection and userinfo.
-
- Calls the introspection endpoint first to validate the token and
- retrieve auth metadata (active status, scopes, expiry, audience).
- If the token passes security checks, the userinfo endpoint is called
- for profile enrichment. Userinfo failure is non-fatal.
-
- When a ``client_id`` is configured, the token's audience must match it.
- When ``required_scopes`` are configured, introspection must confirm
- them; tokens are rejected if scope information is unavailable.
- """
- try:
- async with (
- contextlib.nullcontext(self._http_client)
- if self._http_client is not None
- else httpx.AsyncClient(timeout=self.timeout_seconds)
- ) as client:
- # Step 1: Validate token via introspection (RFC 7662).
- # Security-critical checks (active, audience, scopes) come first.
- introspect_data_payload: dict = {"token": token}
- introspect_kwargs: dict = {
- "data": introspect_data_payload,
- "headers": {"User-Agent": "FastMCP-Clerk-OAuth"},
- }
-
- if self._client_id and self._client_secret:
- introspect_kwargs["auth"] = (
- self._client_id,
- self._client_secret,
- )
- elif self._client_id:
- introspect_data_payload["client_id"] = self._client_id
-
- introspect_response = await client.post(
- self._introspection_url,
- **introspect_kwargs,
- )
-
- if introspect_response.status_code != 200:
- logger.debug(
- "Clerk introspection failed: %d",
- introspect_response.status_code,
- )
- return None
-
- introspect_data = introspect_response.json()
-
- # RFC 7662 requires the 'active' field in the response.
- # A missing field indicates a malformed response — reject.
- if "active" not in introspect_data or not introspect_data["active"]:
- logger.debug(
- "Clerk introspection: token inactive or missing 'active' field"
- )
- return None
-
- scope_str = introspect_data.get("scope", "")
- token_scopes = scope_str.split() if scope_str else []
-
- aud = introspect_data.get("aud") or introspect_data.get("client_id")
-
- expires_at: int | None = None
- exp = introspect_data.get("exp")
- if exp is not None:
- with contextlib.suppress(ValueError, TypeError):
- expires_at = int(exp)
-
- if self._client_id and aud != self._client_id:
- logger.debug(
- "Clerk token audience mismatch: got %s, expected %s",
- aud,
- self._client_id,
- )
- return None
-
- if self.required_scopes:
- if not token_scopes:
- logger.debug(
- "Clerk token missing scope information; "
- "cannot verify required scopes %s",
- self.required_scopes,
- )
- return None
- token_scopes_set = set(token_scopes)
- required_scopes_set = set(self.required_scopes)
- if not required_scopes_set.issubset(token_scopes_set):
- logger.debug(
- "Clerk token missing required scopes. Has %s, needs %s",
- token_scopes_set,
- required_scopes_set,
- )
- return None
-
- # Step 2: Fetch user profile via userinfo.
- # Enriches the token with profile data (name, email, picture).
- sub = introspect_data.get("sub")
- user_data: dict = {}
- try:
- userinfo_response = await client.get(
- self._userinfo_url,
- headers={
- "Authorization": f"Bearer {token}",
- "User-Agent": "FastMCP-Clerk-OAuth",
- },
- )
- if userinfo_response.status_code == 200:
- user_data = userinfo_response.json()
- if not sub:
- sub = user_data.get("sub")
- except Exception as e:
- logger.debug("Clerk userinfo call failed: %s", e)
-
- if not sub:
- logger.debug("Clerk token missing 'sub' claim")
- return None
-
- access_token = AccessToken(
- token=token,
- client_id=aud or sub,
- scopes=token_scopes,
- expires_at=expires_at,
- claims={
- "sub": sub,
- "aud": aud,
- "email": user_data.get("email"),
- "email_verified": user_data.get("email_verified"),
- "name": user_data.get("name"),
- "picture": user_data.get("picture"),
- "given_name": user_data.get("given_name"),
- "family_name": user_data.get("family_name"),
- "preferred_username": user_data.get("preferred_username"),
- "iss": user_data.get("iss"),
- "clerk_user_data": user_data or None,
- },
- )
- logger.debug("Clerk token verified successfully for sub=%s", sub)
- return access_token
-
- except httpx.RequestError as e:
- logger.debug("Failed to verify Clerk token: %s", e)
- return None
- except Exception as e:
- logger.debug("Clerk token verification error: %s", e)
- return None
-
-
-class ClerkProvider(OAuthProxy):
- """Complete Clerk OAuth provider for FastMCP.
-
- This provider makes it trivial to add Clerk OAuth protection to any
- FastMCP server. Provide your Clerk instance domain, OAuth app credentials,
- and a base URL, and you're ready to go.
-
- Clerk uses standard OIDC endpoints derived from the instance domain.
- All endpoint URLs are constructed automatically from the domain parameter.
-
- Features:
- - Transparent OAuth proxy to Clerk
- - Automatic token validation via Clerk's userinfo & introspection APIs
- - User information extraction from Clerk's OIDC claims
- - PKCE support (S256)
- - Minimal configuration required
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.clerk import ClerkProvider
-
- auth = ClerkProvider(
- domain="saving-primate-16.clerk.accounts.dev",
- client_id="your-clerk-client-id",
- client_secret="your-clerk-client-secret",
- base_url="https://my-server.com",
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- domain: str,
- client_id: str,
- client_secret: str | None = None,
- base_url: AnyHttpUrl | str,
- resource_base_url: AnyHttpUrl | str | None = None,
- issuer_url: AnyHttpUrl | str | None = None,
- redirect_path: str | None = None,
- required_scopes: list[str] | None = None,
- valid_scopes: list[str] | None = None,
- timeout_seconds: int = 10,
- allowed_client_redirect_uris: list[str] | None = None,
- client_storage: AsyncKeyValue | None = None,
- jwt_signing_key: str | bytes | None = None,
- require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_csp_policy: str | None = None,
- forward_resource: bool = True,
- extra_authorize_params: dict[str, str] | None = None,
- http_client: httpx.AsyncClient | None = None,
- enable_cimd: bool = True,
- ):
- """Initialize Clerk OAuth provider.
-
- Args:
- domain: Clerk instance domain (e.g., "saving-primate-16.clerk.accounts.dev").
- This is used to derive all OAuth/OIDC endpoint URLs.
- client_id: Clerk OAuth application client ID
- client_secret: Clerk OAuth application client secret.
- Optional for PKCE public clients. When omitted, jwt_signing_key must be provided.
- base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
- resource_base_url: Optional public base URL for the protected resource metadata
- and token audience. Defaults to ``base_url``.
- issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
- to avoid 404s during discovery when mounting under a path.
- redirect_path: Redirect path configured in Clerk OAuth app (defaults to "/auth/callback")
- required_scopes: Required Clerk scopes (defaults to ["openid", "email", "profile"]).
- Clerk supports: "openid", "email", "profile", "public_metadata",
- "private_metadata", "offline_access".
- valid_scopes: All scopes that clients are allowed to request, advertised through
- well-known endpoints. Defaults to required_scopes if not provided.
- timeout_seconds: HTTP request timeout for Clerk API calls (defaults to 10)
- allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
- If None (default), all URIs are allowed. If empty list, no URIs are allowed.
- client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, an encrypted file store will be created in the data directory
- (derived from ``platformdirs``).
- jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes
- are provided, they will be used as is. If a string is provided, it will be derived
- into a 32-byte key. If not provided, the upstream client secret will be used to
- derive a 32-byte key using PBKDF2.
- require_authorization_consent: Whether to require user consent before authorizing
- clients (default True). When "external", the built-in consent screen is skipped
- but no warning is logged, indicating that consent is handled externally by Clerk.
- consent_csp_policy: Custom CSP policy for the consent page.
- extra_authorize_params: Additional parameters to forward to Clerk's authorization
- endpoint. Example: {"prompt": "login"} to force re-authentication.
- http_client: Optional httpx.AsyncClient for connection pooling in token verification.
- When provided, the client is reused across verify_token calls and the caller
- is responsible for its lifecycle. When None (default), a fresh client is created
- per call.
- enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
- client IDs (default True). Set to False to disable.
- """
- domain = domain.rstrip("/")
-
- required_scopes_final = (
- parse_scopes(required_scopes)
- if required_scopes is not None
- else ["openid", "email", "profile"]
- )
-
- parsed_valid_scopes = (
- parse_scopes(valid_scopes) if valid_scopes is not None else None
- )
-
- token_verifier = ClerkTokenVerifier(
- domain=domain,
- client_id=client_id,
- client_secret=client_secret,
- required_scopes=required_scopes_final,
- timeout_seconds=timeout_seconds,
- http_client=http_client,
- )
-
- extra_authorize_params_final = (
- dict(extra_authorize_params) if extra_authorize_params else {}
- )
-
- super().__init__(
- upstream_authorization_endpoint=f"https://{domain}/oauth/authorize",
- upstream_token_endpoint=f"https://{domain}/oauth/token",
- upstream_client_id=client_id,
- upstream_client_secret=client_secret,
- token_verifier=token_verifier,
- base_url=base_url,
- resource_base_url=resource_base_url,
- redirect_path=redirect_path,
- issuer_url=issuer_url or base_url,
- allowed_client_redirect_uris=allowed_client_redirect_uris,
- client_storage=client_storage,
- jwt_signing_key=jwt_signing_key,
- require_authorization_consent=require_authorization_consent,
- consent_csp_policy=consent_csp_policy,
- forward_resource=forward_resource,
- extra_authorize_params=extra_authorize_params_final or None,
- valid_scopes=parsed_valid_scopes,
- enable_cimd=enable_cimd,
- )
-
- logger.debug(
- "Initialized Clerk OAuth provider for domain %s with scopes: %s",
- domain,
- required_scopes_final,
- )
+__all__ = ["ClerkProvider", "ClerkTokenVerifier"]
diff --git a/src/fastmcp/server/auth/providers/descope.py b/src/fastmcp/server/auth/providers/descope.py
index 3bdccf8d5..028152dde 100644
--- a/src/fastmcp/server/auth/providers/descope.py
+++ b/src/fastmcp/server/auth/providers/descope.py
@@ -1,209 +1,20 @@
-"""Descope authentication provider for FastMCP.
-
-This module provides DescopeProvider - a complete authentication solution that integrates
-with Descope's OAuth 2.1 and OpenID Connect services, supporting Dynamic Client Registration (DCR)
-for seamless MCP client authentication.
-"""
+"""Backward compatibility shim for Descope auth provider."""
from __future__ import annotations
-from urllib.parse import urlparse
+import warnings
-import httpx
-from pydantic import AnyHttpUrl
-from starlette.responses import JSONResponse
-from starlette.routing import Route
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
-from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.descope is deprecated. "
+ "Import from fastmcp.server.plugins.auth.descope.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
-logger = get_logger(__name__)
+from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
-
-class DescopeProvider(RemoteAuthProvider):
- """Descope metadata provider for DCR (Dynamic Client Registration).
-
- This provider implements Descope integration using metadata forwarding.
- This is the recommended approach for Descope DCR
- as it allows Descope to handle the OAuth flow directly while FastMCP acts
- as a resource server.
-
- IMPORTANT SETUP REQUIREMENTS:
-
- 1. Create an MCP Server in Descope Console:
- - Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console
- - Create a new MCP Server
- - Ensure that **Dynamic Client Registration (DCR)** is enabled
- - Note your Well-Known URL
-
- 2. Note your Well-Known URL:
- - Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers)
- - Format: ``https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration``
-
- For detailed setup instructions, see:
- https://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr
-
- Example:
- ```python
- from fastmcp.server.auth.providers.descope import DescopeProvider
-
- # Create Descope metadata provider (JWT verifier created automatically)
- descope_auth = DescopeProvider(
- config_url="https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration",
- base_url="https://your-fastmcp-server.com",
- )
-
- # Use with FastMCP
- mcp = FastMCP("My App", auth=descope_auth)
- ```
- """
-
- def __init__(
- self,
- *,
- base_url: AnyHttpUrl | str,
- config_url: AnyHttpUrl | str | None = None,
- project_id: str | None = None,
- descope_base_url: AnyHttpUrl | str | None = None,
- required_scopes: list[str] | None = None,
- scopes_supported: list[str] | None = None,
- resource_name: str | None = None,
- resource_documentation: AnyHttpUrl | None = None,
- token_verifier: TokenVerifier | None = None,
- ):
- """Initialize Descope metadata provider.
-
- Args:
- base_url: Public URL of this FastMCP server
- config_url: Your Descope Well-Known URL (e.g., "https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration")
- This is the new recommended way. If provided, project_id and descope_base_url are ignored.
- project_id: Your Descope Project ID (e.g., "P2abc123"). Used with descope_base_url for backwards compatibility.
- descope_base_url: Your Descope base URL (e.g., "https://api.descope.com"). Used with project_id for backwards compatibility.
- required_scopes: Optional list of scopes that must be present in validated tokens.
- These scopes will be included in the protected resource metadata.
- scopes_supported: Optional list of scopes to advertise in OAuth metadata.
- If None, uses required_scopes. Use this when the scopes clients should
- request differ from the scopes enforced on tokens.
- resource_name: Optional name for the protected resource metadata.
- resource_documentation: Optional documentation URL for the protected resource.
- token_verifier: Optional token verifier. If None, creates JWT verifier for Descope
- """
- self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
-
- # Parse scopes if provided as string
- parsed_scopes = (
- parse_scopes(required_scopes) if required_scopes is not None else None
- )
-
- # Determine which API is being used
- if config_url is not None:
- # New API: use config_url
- # Strip /.well-known/openid-configuration from config_url if present
- issuer_url = str(config_url)
- if issuer_url.endswith("/.well-known/openid-configuration"):
- issuer_url = issuer_url[: -len("/.well-known/openid-configuration")]
-
- # Parse the issuer URL to extract descope_base_url and project_id for other uses
- parsed_url = urlparse(issuer_url)
- path_parts = parsed_url.path.strip("/").split("/")
-
- # Extract project_id from path (format: /v1/apps/agentic/P.../M...)
- if "agentic" in path_parts:
- agentic_index = path_parts.index("agentic")
- if agentic_index + 1 < len(path_parts):
- self.project_id = path_parts[agentic_index + 1]
- else:
- raise ValueError(
- f"Could not extract project_id from config_url: {issuer_url}"
- )
- else:
- raise ValueError(
- f"Could not find 'agentic' in config_url path: {issuer_url}"
- )
-
- # Extract descope_base_url (scheme + netloc)
- self.descope_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}".rstrip(
- "/"
- )
- elif project_id is not None and descope_base_url is not None:
- # Old API: use project_id and descope_base_url
- self.project_id = project_id
- descope_base_url_str = str(descope_base_url).rstrip("/")
- # Ensure descope_base_url has a scheme
- if not descope_base_url_str.startswith(("http://", "https://")):
- descope_base_url_str = f"https://{descope_base_url_str}"
- self.descope_base_url = descope_base_url_str
- # Old issuer format
- issuer_url = f"{self.descope_base_url}/v1/apps/{self.project_id}"
- else:
- raise ValueError(
- "Either config_url (new API) or both project_id and descope_base_url (old API) must be provided"
- )
-
- # Create default JWT verifier if none provided
- if token_verifier is None:
- token_verifier = JWTVerifier(
- jwks_uri=f"{self.descope_base_url}/{self.project_id}/.well-known/jwks.json",
- issuer=issuer_url,
- algorithm="RS256",
- audience=self.project_id,
- required_scopes=parsed_scopes,
- )
-
- # Initialize RemoteAuthProvider with Descope as the authorization server
- super().__init__(
- token_verifier=token_verifier,
- authorization_servers=[AnyHttpUrl(issuer_url)],
- base_url=self.base_url,
- scopes_supported=scopes_supported,
- resource_name=resource_name,
- resource_documentation=resource_documentation,
- )
-
- def get_routes(
- self,
- mcp_path: str | None = None,
- ) -> list[Route]:
- """Get OAuth routes including Descope authorization server metadata forwarding.
-
- This returns the standard protected resource routes plus an authorization server
- metadata endpoint that forwards Descope's OAuth metadata to clients.
-
- Args:
- mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
- This is used to advertise the resource URL in metadata.
- """
- # Get the standard protected resource routes from RemoteAuthProvider
- routes = super().get_routes(mcp_path)
-
- async def oauth_authorization_server_metadata(request):
- """Forward Descope OAuth authorization server metadata with FastMCP customizations."""
- try:
- async with httpx.AsyncClient() as client:
- response = await client.get(
- f"{self.descope_base_url}/v1/apps/{self.project_id}/.well-known/oauth-authorization-server"
- )
- response.raise_for_status()
- metadata = response.json()
- return JSONResponse(metadata)
- except Exception as e:
- return JSONResponse(
- {
- "error": "server_error",
- "error_description": f"Failed to fetch Descope metadata: {e}",
- },
- status_code=500,
- )
-
- # Add Descope authorization server metadata forwarding
- routes.append(
- Route(
- "/.well-known/oauth-authorization-server",
- endpoint=oauth_authorization_server_metadata,
- methods=["GET"],
- )
- )
-
- return routes
+__all__ = ["DescopeProvider"]
diff --git a/src/fastmcp/server/auth/providers/discord.py b/src/fastmcp/server/auth/providers/discord.py
index edf407ae1..8cba6c8e1 100644
--- a/src/fastmcp/server/auth/providers/discord.py
+++ b/src/fastmcp/server/auth/providers/discord.py
@@ -1,288 +1,23 @@
-"""Discord OAuth provider for FastMCP.
-
-This module provides a complete Discord OAuth integration that's ready to use
-with just a client ID and client secret. It handles all the complexity of
-Discord's OAuth flow, token validation, and user management.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.discord import DiscordProvider
-
- # Simple Discord OAuth protection
- auth = DiscordProvider(
- client_id="your-discord-client-id",
- client_secret="your-discord-client-secret"
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-"""
+"""Backward compatibility shim for Discord auth provider."""
from __future__ import annotations
-import contextlib
-import time
-from datetime import datetime
-from typing import Literal
+import warnings
-import httpx
-from key_value.aio.protocols import AsyncKeyValue
-from pydantic import AnyHttpUrl
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-from fastmcp.server.auth import TokenVerifier
-from fastmcp.server.auth.auth import AccessToken
-from fastmcp.server.auth.oauth_proxy import OAuthProxy
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.discord is deprecated. "
+ "Import from fastmcp.server.plugins.auth.discord.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
-logger = get_logger(__name__)
+from fastmcp.server.plugins.auth.discord.provider import (
+ DiscordProvider,
+ DiscordTokenVerifier,
+)
-
-class DiscordTokenVerifier(TokenVerifier):
- """Token verifier for Discord OAuth tokens.
-
- Discord OAuth tokens are opaque (not JWTs), so we verify them
- by calling Discord's tokeninfo API to check if they're valid and get user info.
- """
-
- def __init__(
- self,
- *,
- expected_client_id: str,
- required_scopes: list[str] | None = None,
- timeout_seconds: int = 10,
- http_client: httpx.AsyncClient | None = None,
- ):
- """Initialize the Discord token verifier.
-
- Args:
- expected_client_id: Expected Discord OAuth client ID for audience binding
- required_scopes: Required OAuth scopes (e.g., ['email'])
- timeout_seconds: HTTP request timeout
- http_client: Optional httpx.AsyncClient for connection pooling. When provided,
- the client is reused across calls and the caller is responsible for its
- lifecycle. When None (default), a fresh client is created per call.
- """
- super().__init__(required_scopes=required_scopes)
- self.expected_client_id = expected_client_id
- self.timeout_seconds = timeout_seconds
- self._http_client = http_client
-
- async def verify_token(self, token: str) -> AccessToken | None:
- """Verify Discord OAuth token by calling Discord's tokeninfo API."""
- try:
- async with (
- contextlib.nullcontext(self._http_client)
- if self._http_client is not None
- else httpx.AsyncClient(timeout=self.timeout_seconds)
- ) as client:
- # Use Discord's tokeninfo endpoint to validate the token
- headers = {
- "Authorization": f"Bearer {token}",
- "User-Agent": "FastMCP-Discord-OAuth",
- }
- response = await client.get(
- "https://discord.com/api/oauth2/@me",
- headers=headers,
- )
-
- if response.status_code != 200:
- logger.debug(
- "Discord token verification failed: %d",
- response.status_code,
- )
- return None
-
- token_info = response.json()
-
- # Check if token is expired (Discord returns ISO timestamp)
- expires_str = token_info.get("expires")
- expires_at = None
- if expires_str:
- expires_dt = datetime.fromisoformat(
- expires_str.replace("Z", "+00:00")
- )
- expires_at = int(expires_dt.timestamp())
- if expires_at <= int(time.time()):
- logger.debug("Discord token has expired")
- return None
-
- token_scopes = token_info.get("scopes", [])
-
- # Check required scopes
- if self.required_scopes:
- token_scopes_set = set(token_scopes)
- required_scopes_set = set(self.required_scopes)
- if not required_scopes_set.issubset(token_scopes_set):
- logger.debug(
- "Discord token missing required scopes. Has %d, needs %d",
- len(token_scopes_set),
- len(required_scopes_set),
- )
- return None
-
- user_data = token_info.get("user", {})
- application = token_info.get("application") or {}
- client_id = str(application.get("id", "unknown"))
- if client_id != self.expected_client_id:
- logger.debug(
- "Discord token app ID mismatch: expected %s, got %s",
- self.expected_client_id,
- client_id,
- )
- return None
-
- # Create AccessToken with Discord user info
- access_token = AccessToken(
- token=token,
- client_id=client_id,
- scopes=token_scopes,
- expires_at=expires_at,
- claims={
- "sub": user_data.get("id"),
- "username": user_data.get("username"),
- "discriminator": user_data.get("discriminator"),
- "avatar": user_data.get("avatar"),
- "email": user_data.get("email"),
- "verified": user_data.get("verified"),
- "locale": user_data.get("locale"),
- "discord_user": user_data,
- "discord_token_info": token_info,
- },
- )
- logger.debug("Discord token verified successfully")
- return access_token
-
- except httpx.RequestError as e:
- logger.debug("Failed to verify Discord token: %s", e)
- return None
- except Exception as e:
- logger.debug("Discord token verification error: %s", e)
- return None
-
-
-class DiscordProvider(OAuthProxy):
- """Complete Discord OAuth provider for FastMCP.
-
- This provider makes it trivial to add Discord OAuth protection to any
- FastMCP server. Just provide your Discord OAuth app credentials and
- a base URL, and you're ready to go.
-
- Features:
- - Transparent OAuth proxy to Discord
- - Automatic token validation via Discord's API
- - User information extraction from Discord APIs
- - Minimal configuration required
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.discord import DiscordProvider
-
- auth = DiscordProvider(
- client_id="123456789",
- client_secret="discord-client-secret-abc123...",
- base_url="https://my-server.com"
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- client_id: str,
- client_secret: str,
- base_url: AnyHttpUrl | str,
- resource_base_url: AnyHttpUrl | str | None = None,
- issuer_url: AnyHttpUrl | str | None = None,
- redirect_path: str | None = None,
- required_scopes: list[str] | None = None,
- timeout_seconds: int = 10,
- allowed_client_redirect_uris: list[str] | None = None,
- client_storage: AsyncKeyValue | None = None,
- jwt_signing_key: str | bytes | None = None,
- require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_csp_policy: str | None = None,
- forward_resource: bool = True,
- http_client: httpx.AsyncClient | None = None,
- enable_cimd: bool = True,
- ):
- """Initialize Discord OAuth provider.
-
- Args:
- client_id: Discord OAuth client ID (e.g., "123456789")
- client_secret: Discord OAuth client secret (e.g., "S....")
- base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
- resource_base_url: Optional public base URL for the protected resource metadata
- and token audience. Defaults to ``base_url``.
- issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
- to avoid 404s during discovery when mounting under a path.
- redirect_path: Redirect path configured in Discord OAuth app (defaults to "/auth/callback")
- required_scopes: Required Discord scopes (defaults to ["identify"]). Common scopes include:
- - "identify" for profile info (default)
- - "email" for email access
- - "guilds" for server membership info
- timeout_seconds: HTTP request timeout for Discord API calls (defaults to 10)
- allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
- If None (default), all URIs are allowed. If empty list, no URIs are allowed.
- client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, an encrypted file store will be created in the data directory
- (derived from `platformdirs`).
- jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
- they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
- provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
- require_authorization_consent: Whether to require user consent before authorizing clients (default True).
- When True, users see a consent screen before being redirected to Discord.
- When False, authorization proceeds directly without user confirmation.
- When "external", the built-in consent screen is skipped but no warning is
- logged, indicating that consent is handled externally (e.g. by the upstream IdP).
- SECURITY WARNING: Only set to False for local development or testing environments.
- http_client: Optional httpx.AsyncClient for connection pooling in token verification.
- When provided, the client is reused across verify_token calls and the caller
- is responsible for its lifecycle. When None (default), a fresh client is created per call.
- enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
- client IDs (default True). Set to False to disable.
- """
- # Parse scopes if provided as string
- required_scopes_final = (
- parse_scopes(required_scopes)
- if required_scopes is not None
- else ["identify"]
- )
-
- # Create Discord token verifier
- token_verifier = DiscordTokenVerifier(
- expected_client_id=client_id,
- required_scopes=required_scopes_final,
- timeout_seconds=timeout_seconds,
- http_client=http_client,
- )
-
- # Initialize OAuth proxy with Discord endpoints
- super().__init__(
- upstream_authorization_endpoint="https://discord.com/oauth2/authorize",
- upstream_token_endpoint="https://discord.com/api/oauth2/token",
- upstream_client_id=client_id,
- upstream_client_secret=client_secret,
- token_verifier=token_verifier,
- base_url=base_url,
- resource_base_url=resource_base_url,
- redirect_path=redirect_path,
- issuer_url=issuer_url or base_url, # Default to base_url if not specified
- allowed_client_redirect_uris=allowed_client_redirect_uris,
- client_storage=client_storage,
- jwt_signing_key=jwt_signing_key,
- require_authorization_consent=require_authorization_consent,
- consent_csp_policy=consent_csp_policy,
- forward_resource=forward_resource,
- enable_cimd=enable_cimd,
- )
-
- logger.debug(
- "Initialized Discord OAuth provider for client %s with scopes: %s",
- client_id,
- required_scopes_final,
- )
+__all__ = ["DiscordProvider", "DiscordTokenVerifier"]
diff --git a/src/fastmcp/server/auth/providers/github.py b/src/fastmcp/server/auth/providers/github.py
index 57ee16799..928534421 100644
--- a/src/fastmcp/server/auth/providers/github.py
+++ b/src/fastmcp/server/auth/providers/github.py
@@ -1,303 +1,23 @@
-"""GitHub OAuth provider for FastMCP.
-
-This module provides a complete GitHub OAuth integration that's ready to use
-with just a client ID and client secret. It handles all the complexity of
-GitHub's OAuth flow, token validation, and user management.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.github import GitHubProvider
-
- # Simple GitHub OAuth protection
- auth = GitHubProvider(
- client_id="your-github-client-id",
- client_secret="your-github-client-secret"
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-"""
+"""Backward compatibility shim for GitHub auth provider."""
from __future__ import annotations
-import contextlib
-from typing import Literal
+import warnings
-import httpx
-from key_value.aio.protocols import AsyncKeyValue
-from pydantic import AnyHttpUrl
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-from fastmcp.server.auth import TokenVerifier
-from fastmcp.server.auth.auth import AccessToken
-from fastmcp.server.auth.oauth_proxy import OAuthProxy
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
-from fastmcp.utilities.token_cache import TokenCache
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.github is deprecated. "
+ "Import from fastmcp.server.plugins.auth.github.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
-logger = get_logger(__name__)
+from fastmcp.server.plugins.auth.github.provider import (
+ GitHubProvider,
+ GitHubTokenVerifier,
+)
-
-class GitHubTokenVerifier(TokenVerifier):
- """Token verifier for GitHub OAuth tokens.
-
- GitHub OAuth tokens are opaque (not JWTs), so we verify them
- by calling GitHub's API to check if they're valid and get user info.
-
- Caching is disabled by default. Set ``cache_ttl_seconds`` to a positive
- integer to cache successful verification results and avoid repeated
- GitHub API calls for the same token.
- """
-
- def __init__(
- self,
- *,
- required_scopes: list[str] | None = None,
- timeout_seconds: int = 10,
- cache_ttl_seconds: int | None = None,
- max_cache_size: int | None = None,
- http_client: httpx.AsyncClient | None = None,
- ):
- """Initialize the GitHub token verifier.
-
- Args:
- required_scopes: Required OAuth scopes (e.g., ['user:email'])
- timeout_seconds: HTTP request timeout
- cache_ttl_seconds: How long to cache verification results in seconds.
- Caching is disabled by default (None). Set to a positive integer
- to enable (e.g., 300 for 5 minutes).
- max_cache_size: Maximum number of tokens to cache. Default: 10 000.
- http_client: Optional httpx.AsyncClient for connection pooling. When provided,
- the client is reused across calls and the caller is responsible for its
- lifecycle. When None (default), a fresh client is created per call.
- """
- super().__init__(required_scopes=required_scopes)
- self.timeout_seconds = timeout_seconds
- self._http_client = http_client
- self._cache = TokenCache(
- ttl_seconds=cache_ttl_seconds,
- max_size=max_cache_size,
- )
-
- async def verify_token(self, token: str) -> AccessToken | None:
- """Verify GitHub OAuth token by calling GitHub API."""
- is_cached, cached_result = self._cache.get(token)
- if is_cached:
- logger.debug("GitHub token cache hit")
- return cached_result
-
- try:
- async with (
- contextlib.nullcontext(self._http_client)
- if self._http_client is not None
- else httpx.AsyncClient(timeout=self.timeout_seconds)
- ) as client:
- # Get token info from GitHub API
- response = await client.get(
- "https://api.github.com/user",
- headers={
- "Authorization": f"Bearer {token}",
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "FastMCP-GitHub-OAuth",
- },
- )
-
- if response.status_code != 200:
- logger.debug(
- "GitHub token verification failed: %d - %s",
- response.status_code,
- response.text[:200],
- )
- return None
-
- user_data = response.json()
-
- # Get token scopes from GitHub API
- # GitHub includes scopes in the X-OAuth-Scopes header
- scopes_response = await client.get(
- "https://api.github.com/user/repos", # Any authenticated endpoint
- headers={
- "Authorization": f"Bearer {token}",
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "FastMCP-GitHub-OAuth",
- },
- )
-
- # Extract scopes from X-OAuth-Scopes header if available
- scopes_verified = scopes_response.status_code == 200
- oauth_scopes_header = scopes_response.headers.get("x-oauth-scopes", "")
- token_scopes = [
- scope.strip()
- for scope in oauth_scopes_header.split(",")
- if scope.strip()
- ]
-
- # If no scopes in header, assume basic scopes based on successful user API call
- if not token_scopes:
- token_scopes = ["user"] # Basic scope if we can access user info
-
- # Check required scopes
- if self.required_scopes:
- token_scopes_set = set(token_scopes)
- required_scopes_set = set(self.required_scopes)
- if not required_scopes_set.issubset(token_scopes_set):
- logger.debug(
- "GitHub token missing required scopes. Has %d, needs %d",
- len(token_scopes_set),
- len(required_scopes_set),
- )
- return None
-
- # Create AccessToken with GitHub user info
- result = AccessToken(
- token=token,
- client_id=str(user_data.get("id", "unknown")), # Use GitHub user ID
- scopes=token_scopes,
- expires_at=None, # GitHub tokens don't typically expire
- claims={
- "sub": str(user_data["id"]),
- "login": user_data.get("login"),
- "name": user_data.get("name"),
- "email": user_data.get("email"),
- "avatar_url": user_data.get("avatar_url"),
- "github_user_data": user_data,
- },
- )
- if scopes_verified:
- self._cache.set(token, result)
- return result
-
- except httpx.RequestError as e:
- logger.debug("Failed to verify GitHub token: %s", e)
- return None
- except Exception as e:
- logger.debug("GitHub token verification error: %s", e)
- return None
-
-
-class GitHubProvider(OAuthProxy):
- """Complete GitHub OAuth provider for FastMCP.
-
- This provider makes it trivial to add GitHub OAuth protection to any
- FastMCP server. Just provide your GitHub OAuth app credentials and
- a base URL, and you're ready to go.
-
- Features:
- - Transparent OAuth proxy to GitHub
- - Automatic token validation via GitHub API
- - User information extraction
- - Minimal configuration required
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.github import GitHubProvider
-
- auth = GitHubProvider(
- client_id="Ov23li...",
- client_secret="abc123...",
- base_url="https://my-server.com"
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- client_id: str,
- client_secret: str,
- base_url: AnyHttpUrl | str,
- resource_base_url: AnyHttpUrl | str | None = None,
- issuer_url: AnyHttpUrl | str | None = None,
- redirect_path: str | None = None,
- required_scopes: list[str] | None = None,
- timeout_seconds: int = 10,
- cache_ttl_seconds: int | None = None,
- max_cache_size: int | None = None,
- allowed_client_redirect_uris: list[str] | None = None,
- client_storage: AsyncKeyValue | None = None,
- jwt_signing_key: str | bytes | None = None,
- require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_csp_policy: str | None = None,
- forward_resource: bool = True,
- http_client: httpx.AsyncClient | None = None,
- enable_cimd: bool = True,
- ):
- """Initialize GitHub OAuth provider.
-
- Args:
- client_id: GitHub OAuth app client ID (e.g., "Ov23li...")
- client_secret: GitHub OAuth app client secret
- base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
- resource_base_url: Optional public base URL for the protected resource metadata
- and token audience. Defaults to ``base_url``.
- issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
- to avoid 404s during discovery when mounting under a path.
- redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
- required_scopes: Required GitHub scopes (defaults to ["user"])
- timeout_seconds: HTTP request timeout for GitHub API calls (defaults to 10)
- cache_ttl_seconds: How long to cache token verification results in seconds.
- Caching is disabled by default (None). Set to a positive integer to
- enable (e.g., 300 for 5 minutes).
- max_cache_size: Maximum number of tokens to cache. Default: 10 000.
- allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
- If None (default), all URIs are allowed. If empty list, no URIs are allowed.
- client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, an encrypted file store will be created in the data directory
- (derived from `platformdirs`).
- jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
- they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
- provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
- require_authorization_consent: Whether to require user consent before authorizing clients (default True).
- When True, users see a consent screen before being redirected to GitHub.
- When False, authorization proceeds directly without user confirmation.
- When "external", the built-in consent screen is skipped but no warning is
- logged, indicating that consent is handled externally (e.g. by the upstream IdP).
- SECURITY WARNING: Only set to False for local development or testing environments.
- http_client: Optional httpx.AsyncClient for connection pooling in token verification.
- When provided, the client is reused across verify_token calls and the caller
- is responsible for its lifecycle. When None (default), a fresh client is created per call.
- enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
- client IDs (default True). Set to False to disable.
- """
- # Parse scopes if provided as string
- required_scopes_final = (
- parse_scopes(required_scopes) if required_scopes is not None else ["user"]
- )
-
- # Create GitHub token verifier
- token_verifier = GitHubTokenVerifier(
- required_scopes=required_scopes_final,
- timeout_seconds=timeout_seconds,
- cache_ttl_seconds=cache_ttl_seconds,
- max_cache_size=max_cache_size,
- http_client=http_client,
- )
-
- # Initialize OAuth proxy with GitHub endpoints
- super().__init__(
- upstream_authorization_endpoint="https://github.com/login/oauth/authorize",
- upstream_token_endpoint="https://github.com/login/oauth/access_token",
- upstream_client_id=client_id,
- upstream_client_secret=client_secret,
- token_verifier=token_verifier,
- base_url=base_url,
- resource_base_url=resource_base_url,
- redirect_path=redirect_path,
- issuer_url=issuer_url or base_url, # Default to base_url if not specified
- allowed_client_redirect_uris=allowed_client_redirect_uris,
- client_storage=client_storage,
- jwt_signing_key=jwt_signing_key,
- require_authorization_consent=require_authorization_consent,
- consent_csp_policy=consent_csp_policy,
- forward_resource=forward_resource,
- enable_cimd=enable_cimd,
- )
-
- logger.debug(
- "Initialized GitHub OAuth provider for client %s with scopes: %s",
- client_id,
- required_scopes_final,
- )
+__all__ = ["GitHubProvider", "GitHubTokenVerifier"]
diff --git a/src/fastmcp/server/auth/providers/google.py b/src/fastmcp/server/auth/providers/google.py
index 55dcad17c..bf54eec47 100644
--- a/src/fastmcp/server/auth/providers/google.py
+++ b/src/fastmcp/server/auth/providers/google.py
@@ -1,365 +1,23 @@
-"""Google OAuth provider for FastMCP.
-
-This module provides a complete Google OAuth integration that's ready to use
-with just a client ID and client secret. It handles all the complexity of
-Google's OAuth flow, token validation, and user management.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.google import GoogleProvider
-
- # Simple Google OAuth protection
- auth = GoogleProvider(
- client_id="your-google-client-id.apps.googleusercontent.com",
- client_secret="your-google-client-secret"
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-"""
+"""Backward compatibility shim for Google auth provider."""
from __future__ import annotations
-import contextlib
-import time
-from typing import Literal
+import warnings
-import httpx
-from key_value.aio.protocols import AsyncKeyValue
-from pydantic import AnyHttpUrl
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-from fastmcp.server.auth import TokenVerifier
-from fastmcp.server.auth.auth import AccessToken
-from fastmcp.server.auth.oauth_proxy import OAuthProxy
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.google is deprecated. "
+ "Import from fastmcp.server.plugins.auth.google.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
-logger = get_logger(__name__)
+from fastmcp.server.plugins.auth.google.provider import (
+ GoogleProvider,
+ GoogleTokenVerifier,
+)
-
-GOOGLE_SCOPE_ALIASES: dict[str, str] = {
- "email": "https://www.googleapis.com/auth/userinfo.email",
- "profile": "https://www.googleapis.com/auth/userinfo.profile",
-}
-
-
-def _normalize_google_scope(scope: str) -> str:
- """Normalize a Google scope shorthand to its canonical full URI.
-
- Google accepts shorthand scopes like "email" and "profile" in authorization
- requests, but returns the full URI form in token responses. This normalizes
- to the full URI so comparisons work regardless of which form was used.
- """
- return GOOGLE_SCOPE_ALIASES.get(scope, scope)
-
-
-class GoogleTokenVerifier(TokenVerifier):
- """Token verifier for Google OAuth tokens.
-
- Google OAuth tokens are opaque (not JWTs), so we verify them by calling
- Google's tokeninfo endpoint with the access token as a query parameter.
- This returns the OAuth app ID (``aud``), granted scopes, and expiry time.
- User profile data (name, picture, etc.) is fetched separately from the
- v2 userinfo endpoint when the token is valid.
- """
-
- def __init__(
- self,
- *,
- required_scopes: list[str] | None = None,
- timeout_seconds: int = 10,
- http_client: httpx.AsyncClient | None = None,
- ):
- """Initialize the Google token verifier.
-
- Args:
- required_scopes: Required OAuth scopes (e.g., ['openid', 'https://www.googleapis.com/auth/userinfo.email'])
- timeout_seconds: HTTP request timeout
- http_client: Optional httpx.AsyncClient for connection pooling. When provided,
- the client is reused across calls and the caller is responsible for its
- lifecycle. When None (default), a fresh client is created per call.
- """
- normalized = (
- [_normalize_google_scope(s) for s in required_scopes]
- if required_scopes
- else required_scopes
- )
- super().__init__(required_scopes=normalized)
- self.timeout_seconds = timeout_seconds
- self._http_client = http_client
-
- async def verify_token(self, token: str) -> AccessToken | None:
- """Verify a Google OAuth token using the tokeninfo endpoint.
-
- Calls ``https://oauth2.googleapis.com/tokeninfo?access_token=TOKEN``
- to validate the token and retrieve the OAuth app ID (``aud``), granted
- scopes, and expiry time. On success, fetches user profile data from
- the v2 userinfo endpoint to populate name, picture, and locale claims.
- """
- try:
- async with (
- contextlib.nullcontext(self._http_client)
- if self._http_client is not None
- else httpx.AsyncClient(timeout=self.timeout_seconds)
- ) as client:
- # Step 1: Verify token via tokeninfo endpoint.
- # Returns aud (OAuth app ID), scope (space-separated), expires_in, sub, email.
- response = await client.get(
- "https://oauth2.googleapis.com/tokeninfo",
- params={"access_token": token},
- headers={"User-Agent": "FastMCP-Google-OAuth"},
- )
-
- if response.status_code != 200:
- logger.debug(
- "Google token verification failed: %d",
- response.status_code,
- )
- return None
-
- token_data = response.json()
-
- # aud is the OAuth app ID (client_id / audience)
- aud = token_data.get("aud")
- if not aud:
- logger.debug("Google tokeninfo missing 'aud' claim")
- return None
-
- # sub is required (unique Google user ID)
- sub = token_data.get("sub")
- if not sub:
- logger.debug("Google tokeninfo missing 'sub' claim")
- return None
-
- # Parse scopes directly from the tokeninfo response (space-separated)
- scope_str = token_data.get("scope", "")
- token_scopes = scope_str.split() if scope_str else []
-
- # Check required scopes
- if self.required_scopes:
- token_scopes_set = set(token_scopes)
- required_scopes_set = set(self.required_scopes)
- if not required_scopes_set.issubset(token_scopes_set):
- logger.debug(
- "Google token missing required scopes. Has %d, needs %d",
- len(token_scopes_set),
- len(required_scopes_set),
- )
- return None
-
- # Compute expiry from expires_in (seconds until expiry)
- expires_at: int | None = None
- expires_in = token_data.get("expires_in")
- if expires_in is not None:
- with contextlib.suppress(ValueError, TypeError):
- expires_at = int(time.time()) + int(expires_in)
-
- # Step 2: Fetch user profile from v2 userinfo endpoint.
- # tokeninfo provides auth data; userinfo provides name, picture, locale.
- user_data: dict = {}
- try:
- userinfo_response = await client.get(
- "https://www.googleapis.com/oauth2/v2/userinfo",
- headers={
- "Authorization": f"Bearer {token}",
- "User-Agent": "FastMCP-Google-OAuth",
- },
- )
- if userinfo_response.status_code == 200:
- user_data = userinfo_response.json()
- except Exception as e:
- logger.debug("Failed to fetch Google user profile: %s", e)
-
- access_token = AccessToken(
- token=token,
- client_id=sub,
- scopes=token_scopes,
- expires_at=expires_at,
- claims={
- "sub": sub,
- "aud": aud,
- "email": token_data.get("email") or user_data.get("email"),
- "email_verified": token_data.get("email_verified")
- or user_data.get("verified_email"),
- "name": user_data.get("name"),
- "picture": user_data.get("picture"),
- "given_name": user_data.get("given_name"),
- "family_name": user_data.get("family_name"),
- "locale": user_data.get("locale"),
- "google_user_data": user_data or None,
- },
- )
- logger.debug("Google token verified successfully")
- return access_token
-
- except httpx.RequestError as e:
- logger.debug("Failed to verify Google token: %s", e)
- return None
- except Exception as e:
- logger.debug("Google token verification error: %s", e)
- return None
-
-
-class GoogleProvider(OAuthProxy):
- """Complete Google OAuth provider for FastMCP.
-
- This provider makes it trivial to add Google OAuth protection to any
- FastMCP server. Just provide your Google OAuth app credentials and
- a base URL, and you're ready to go.
-
- Features:
- - Transparent OAuth proxy to Google
- - Automatic token validation via Google's tokeninfo API
- - User information extraction from Google APIs
- - Minimal configuration required
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.google import GoogleProvider
-
- auth = GoogleProvider(
- client_id="123456789.apps.googleusercontent.com",
- client_secret="GOCSPX-abc123...",
- base_url="https://my-server.com"
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- client_id: str,
- client_secret: str | None = None,
- base_url: AnyHttpUrl | str,
- resource_base_url: AnyHttpUrl | str | None = None,
- issuer_url: AnyHttpUrl | str | None = None,
- redirect_path: str | None = None,
- required_scopes: list[str] | None = None,
- valid_scopes: list[str] | None = None,
- timeout_seconds: int = 10,
- allowed_client_redirect_uris: list[str] | None = None,
- client_storage: AsyncKeyValue | None = None,
- jwt_signing_key: str | bytes | None = None,
- require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_csp_policy: str | None = None,
- forward_resource: bool = True,
- extra_authorize_params: dict[str, str] | None = None,
- http_client: httpx.AsyncClient | None = None,
- enable_cimd: bool = True,
- ):
- """Initialize Google OAuth provider.
-
- Args:
- client_id: Google OAuth client ID (e.g., "123456789.apps.googleusercontent.com")
- client_secret: Google OAuth client secret (e.g., "GOCSPX-abc123...").
- Optional for PKCE public clients (e.g., native apps). When omitted,
- jwt_signing_key must be provided.
- base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
- resource_base_url: Optional public base URL for the protected resource metadata
- and token audience. Defaults to ``base_url``.
- issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
- to avoid 404s during discovery when mounting under a path.
- redirect_path: Redirect path configured in Google OAuth app (defaults to "/auth/callback")
- required_scopes: Required Google scopes (defaults to ["openid"]). Common scopes include:
- - "openid" for OpenID Connect (default)
- - "https://www.googleapis.com/auth/userinfo.email" for email access
- - "https://www.googleapis.com/auth/userinfo.profile" for profile info
- Google scope shorthands like "email" and "profile" are automatically
- normalized to their full URI forms for token verification.
- valid_scopes: All scopes that clients are allowed to request, advertised through
- well-known endpoints. Defaults to required_scopes if not provided. Use this
- when you want clients to be able to request additional scopes beyond the
- required minimum. Shorthands are normalized to full URI forms.
- timeout_seconds: HTTP request timeout for Google API calls (defaults to 10)
- allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
- If None (default), all URIs are allowed. If empty list, no URIs are allowed.
- client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, an encrypted file store will be created in the data directory
- (derived from `platformdirs`).
- jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
- they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
- provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
- require_authorization_consent: Whether to require user consent before authorizing clients (default True).
- When True, users see a consent screen before being redirected to Google.
- When False, authorization proceeds directly without user confirmation.
- When "external", the built-in consent screen is skipped but no warning is
- logged, indicating that consent is handled externally (e.g. by Google's own consent).
- SECURITY WARNING: Only set to False for local development or testing environments.
- extra_authorize_params: Additional parameters to forward to Google's authorization endpoint.
- By default, GoogleProvider sets {"access_type": "offline", "prompt": "consent"} to ensure
- refresh tokens are returned. You can override these defaults or add additional parameters.
- Example: {"prompt": "select_account"} to let users choose their Google account.
- http_client: Optional httpx.AsyncClient for connection pooling in token verification.
- When provided, the client is reused across verify_token calls and the caller
- is responsible for its lifecycle. When None (default), a fresh client is created per call.
- enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
- client IDs (default True). Set to False to disable.
- """
- # Parse scopes if provided as string
- # Google requires at least one scope - openid is the minimal OIDC scope
- required_scopes_final = (
- parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
- )
-
- # Normalize valid_scopes if provided
- parsed_valid_scopes = (
- parse_scopes(valid_scopes) if valid_scopes is not None else None
- )
- valid_scopes_final = (
- [_normalize_google_scope(s) for s in parsed_valid_scopes]
- if parsed_valid_scopes is not None
- else None
- )
-
- # Create Google token verifier
- # Normalization of shorthand scopes (e.g. "email" -> full URI) happens
- # inside GoogleTokenVerifier so required_scopes match what Google returns.
- token_verifier = GoogleTokenVerifier(
- required_scopes=required_scopes_final,
- timeout_seconds=timeout_seconds,
- http_client=http_client,
- )
-
- # Set Google-specific defaults for extra authorize params
- # access_type=offline ensures refresh tokens are returned
- # prompt=consent forces consent screen to get refresh token (Google only issues on first auth otherwise)
- google_defaults = {
- "access_type": "offline",
- "prompt": "consent",
- }
- # User-provided params override defaults
- if extra_authorize_params:
- google_defaults.update(extra_authorize_params)
- extra_authorize_params_final = google_defaults
-
- # Initialize OAuth proxy with Google endpoints
- super().__init__(
- upstream_authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
- upstream_token_endpoint="https://oauth2.googleapis.com/token",
- upstream_client_id=client_id,
- upstream_client_secret=client_secret,
- token_verifier=token_verifier,
- base_url=base_url,
- resource_base_url=resource_base_url,
- redirect_path=redirect_path,
- issuer_url=issuer_url or base_url, # Default to base_url if not specified
- allowed_client_redirect_uris=allowed_client_redirect_uris,
- client_storage=client_storage,
- jwt_signing_key=jwt_signing_key,
- require_authorization_consent=require_authorization_consent,
- consent_csp_policy=consent_csp_policy,
- forward_resource=forward_resource,
- extra_authorize_params=extra_authorize_params_final,
- valid_scopes=valid_scopes_final,
- enable_cimd=enable_cimd,
- )
-
- logger.debug(
- "Initialized Google OAuth provider for client %s with scopes: %s",
- client_id,
- required_scopes_final,
- )
+__all__ = ["GoogleProvider", "GoogleTokenVerifier"]
diff --git a/src/fastmcp/server/auth/providers/keycloak.py b/src/fastmcp/server/auth/providers/keycloak.py
index d018bc4b0..10b5af41f 100644
--- a/src/fastmcp/server/auth/providers/keycloak.py
+++ b/src/fastmcp/server/auth/providers/keycloak.py
@@ -1,74 +1,22 @@
-"""Keycloak authentication provider for FastMCP."""
+"""Backward compatibility shim for Keycloak auth provider."""
from __future__ import annotations
-from pydantic import AnyHttpUrl
+import warnings
-from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
-from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-logger = get_logger(__name__)
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.keycloak is deprecated. "
+ "Import from fastmcp.server.plugins.auth.keycloak.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
+from fastmcp.server.plugins.auth.keycloak.provider import (
+ KeycloakAuthProvider,
+)
-class KeycloakAuthProvider(RemoteAuthProvider):
- """Keycloak authentication provider using Dynamic Client Registration (DCR).
-
- Requires Keycloak 26.6.0 or later, which includes the fix for DCR compatibility
- with MCP clients (https://github.com/keycloak/keycloak/pull/45309).
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
-
- auth = KeycloakAuthProvider(
- realm_url="https://keycloak.example.com/realms/myrealm",
- base_url="https://my-mcp-server.example.com",
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- realm_url: AnyHttpUrl | str,
- base_url: AnyHttpUrl | str,
- required_scopes: list[str] | str | None = None,
- audience: str | list[str] | None = None,
- token_verifier: TokenVerifier | None = None,
- ):
- """Initialize the Keycloak auth provider.
-
- Args:
- realm_url: Keycloak realm URL (e.g., "https://keycloak.example.com/realms/myrealm")
- base_url: Public URL of this FastMCP server
- required_scopes: Scopes to require on incoming tokens. Defaults to
- ["openid"], which ensures the `sub` claim (user identifier) is
- present in the access token. Override to require additional scopes.
- audience: Optional audience(s) for JWT validation. Recommended for production.
- token_verifier: Optional custom token verifier. Defaults to a JWTVerifier
- configured for Keycloak's JWKS endpoint and issuer.
- """
- self.realm_url = str(realm_url).rstrip("/")
- parsed_scopes = (
- parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
- )
-
- if token_verifier is None:
- token_verifier = JWTVerifier(
- jwks_uri=f"{self.realm_url}/protocol/openid-connect/certs",
- issuer=self.realm_url,
- algorithm="RS256",
- required_scopes=parsed_scopes,
- audience=audience,
- )
-
- super().__init__(
- token_verifier=token_verifier,
- authorization_servers=[AnyHttpUrl(self.realm_url)],
- base_url=AnyHttpUrl(str(base_url).rstrip("/")),
- )
+__all__ = ["KeycloakAuthProvider"]
diff --git a/src/fastmcp/server/auth/providers/oci.py b/src/fastmcp/server/auth/providers/oci.py
index ae765299d..4f633a3bb 100644
--- a/src/fastmcp/server/auth/providers/oci.py
+++ b/src/fastmcp/server/auth/providers/oci.py
@@ -1,180 +1,20 @@
-"""OCI OIDC provider for FastMCP.
+"""Backward compatibility shim for OCI auth provider."""
-The pull request for the provider is submitted to fastmcp.
+from __future__ import annotations
-This module provides OIDC Implementation to integrate MCP servers with OCI.
-You only need OCI Identity Domain's discovery URL, client ID, client secret, and base URL.
+import warnings
-Post Authentication, you get OCI IAM domain access token. That is not authorized to invoke OCI control plane.
-You need to exchange the IAM domain access token for OCI UPST token to invoke OCI control plane APIs.
-The sample code below has get_oci_signer function that returns OCI TokenExchangeSigner object.
-You can use the signer object to create OCI service object.
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.oci import OCIProvider
- from fastmcp.server.dependencies import get_access_token
- from fastmcp.utilities.logging import get_logger
-
- import os
-
- import oci
- from oci.auth.signers import TokenExchangeSigner
-
- logger = get_logger(__name__)
-
- # Load configuration from environment
- config_url = os.environ.get("OCI_CONFIG_URL") # OCI IAM Domain OIDC discovery URL
- client_id = os.environ.get("OCI_CLIENT_ID") # Client ID configured for the OCI IAM Domain Integrated Application
- client_secret = os.environ.get("OCI_CLIENT_SECRET") # Client secret configured for the OCI IAM Domain Integrated Application
- iam_guid = os.environ.get("OCI_IAM_GUID") # IAM GUID configured for the OCI IAM Domain
-
- # Simple OCI OIDC protection
- auth = OCIProvider(
- config_url=config_url, # config URL is the OCI IAM Domain OIDC discovery URL
- client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
- client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application
- required_scopes=["openid", "profile", "email"],
- redirect_path="/auth/callback",
- base_url="http://localhost:8000",
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.oci is deprecated. "
+ "Import from fastmcp.server.plugins.auth.oci.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
)
- # NOTE: For production use, replace this with a thread-safe cache implementation
- # such as threading.Lock-protected dict or a proper caching library
- _global_token_cache = {} # In memory cache for OCI session token signer
+from fastmcp.server.plugins.auth.oci.provider import OCIProvider
- def get_oci_signer() -> TokenExchangeSigner:
-
- authntoken = get_access_token()
- tokenID = authntoken.claims.get("jti")
- token = authntoken.token
-
- # Check if the signer exists for the token ID in memory cache
- cached_signer = _global_token_cache.get(tokenID)
- logger.debug(f"Global cached signer: {cached_signer}")
- if cached_signer:
- logger.debug(f"Using globally cached signer for token ID: {tokenID}")
- return cached_signer
-
- # If the signer is not yet created for the token then create new OCI signer object
- logger.debug(f"Creating new signer for token ID: {tokenID}")
- signer = TokenExchangeSigner(
- jwt_or_func=token,
- oci_domain_id=iam_guid.split(".")[0] if iam_guid else None, # This is same as IAM GUID configured for the OCI IAM Domain
- client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
- client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application
- )
- logger.debug(f"Signer {signer} created for token ID: {tokenID}")
-
- #Cache the signer object in memory cache
- _global_token_cache[tokenID] = signer
- logger.debug(f"Signer cached for token ID: {tokenID}")
-
- return signer
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-"""
-
-from typing import Literal
-
-from key_value.aio.protocols import AsyncKeyValue
-from pydantic import AnyHttpUrl
-
-from fastmcp.server.auth.oidc_proxy import OIDCProxy
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
-
-logger = get_logger(__name__)
-
-
-class OCIProvider(OIDCProxy):
- """An OCI IAM Domain provider implementation for FastMCP.
-
- This provider is a complete OCI integration that's ready to use with
- just the configuration URL, client ID, client secret, and base URL.
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.oci import OCIProvider
-
- import os
-
- # Load configuration from environment
- auth = OCIProvider(
- config_url=os.environ.get("OCI_CONFIG_URL"), # OCI IAM Domain OIDC discovery URL
- client_id=os.environ.get("OCI_CLIENT_ID"), # Client ID configured for the OCI IAM Domain Integrated Application
- client_secret=os.environ.get("OCI_CLIENT_SECRET"), # Client secret configured for the OCI IAM Domain Integrated Application
- base_url="http://localhost:8000",
- required_scopes=["openid", "profile", "email"],
- redirect_path="/auth/callback",
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- config_url: AnyHttpUrl | str,
- client_id: str,
- client_secret: str,
- base_url: AnyHttpUrl | str,
- resource_base_url: AnyHttpUrl | str | None = None,
- audience: str | None = None,
- issuer_url: AnyHttpUrl | str | None = None,
- required_scopes: list[str] | None = None,
- redirect_path: str | None = None,
- allowed_client_redirect_uris: list[str] | None = None,
- client_storage: AsyncKeyValue | None = None,
- jwt_signing_key: str | bytes | None = None,
- require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_csp_policy: str | None = None,
- forward_resource: bool = True,
- ) -> None:
- """Initialize OCI OIDC provider.
-
- Args:
- config_url: OCI OIDC Discovery URL
- client_id: OCI IAM Domain Integrated Application client id
- client_secret: OCI Integrated Application client secret
- base_url: Public URL where OIDC endpoints will be accessible (includes any mount path)
- resource_base_url: Optional public base URL for the protected resource metadata
- and token audience. Defaults to ``base_url``.
- audience: OCI API audience (optional)
- issuer_url: Issuer URL for OCI IAM Domain metadata. This will override issuer URL from the discovery URL.
- required_scopes: Required OCI scopes (defaults to ["openid"])
- redirect_path: Redirect path configured in OCI IAM Domain Integrated Application. The default is "/auth/callback".
- allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
- """
- # Parse scopes if provided as string
- oci_required_scopes = (
- parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
- )
-
- super().__init__(
- config_url=config_url,
- client_id=client_id,
- client_secret=client_secret,
- audience=audience,
- base_url=base_url,
- resource_base_url=resource_base_url,
- issuer_url=issuer_url,
- redirect_path=redirect_path,
- required_scopes=oci_required_scopes,
- allowed_client_redirect_uris=allowed_client_redirect_uris,
- client_storage=client_storage,
- jwt_signing_key=jwt_signing_key,
- require_authorization_consent=require_authorization_consent,
- consent_csp_policy=consent_csp_policy,
- forward_resource=forward_resource,
- )
-
- logger.debug(
- "Initialized OCI OAuth provider for client %s with scopes: %s",
- client_id,
- oci_required_scopes,
- )
+__all__ = ["OCIProvider"]
diff --git a/src/fastmcp/server/auth/providers/propelauth.py b/src/fastmcp/server/auth/providers/propelauth.py
index 82e55e172..26902841e 100644
--- a/src/fastmcp/server/auth/providers/propelauth.py
+++ b/src/fastmcp/server/auth/providers/propelauth.py
@@ -1,234 +1,23 @@
-"""PropelAuth authentication provider for FastMCP.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
-
- auth = PropelAuthProvider(
- auth_url="https://auth.yourdomain.com",
- introspection_client_id="your-client-id",
- introspection_client_secret="your-client-secret",
- base_url="https://your-fastmcp-server.com",
- required_scopes=["read:user_data"],
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
-"""
+"""Backward compatibility shim for PropelAuth auth provider."""
from __future__ import annotations
-from typing import TypedDict
+import warnings
-import httpx
-from pydantic import AnyHttpUrl, SecretStr
-from starlette.responses import JSONResponse
-from starlette.routing import Route
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-from fastmcp.server.auth import AccessToken, RemoteAuthProvider
-from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
-from fastmcp.utilities.logging import get_logger
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.propelauth is deprecated. "
+ "Import from fastmcp.server.plugins.auth.propelauth.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
-logger = get_logger(__name__)
+from fastmcp.server.plugins.auth.propelauth.provider import (
+ PropelAuthProvider,
+ PropelAuthTokenIntrospectionOverrides,
+)
-
-class PropelAuthTokenIntrospectionOverrides(TypedDict, total=False):
- timeout_seconds: int
- cache_ttl_seconds: int | None
- max_cache_size: int | None
- http_client: httpx.AsyncClient | None
-
-
-class PropelAuthProvider(RemoteAuthProvider):
- """PropelAuth resource server provider using OAuth 2.1 token introspection.
-
- This provider validates access tokens via PropelAuth's introspection endpoint
- and forwards authorization server metadata for OAuth discovery.
-
- Setup:
- 1. Enable MCP authentication in the PropelAuth Dashboard
- 2. Configure scopes on the MCP page
- 3. Select which redirect URIs to enable by picking which clients you support
- 4. Generate introspection credentials (Client ID + Client Secret)
-
- For detailed setup instructions, see:
- https://docs.propelauth.com/mcp-authentication/overview
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
-
- auth = PropelAuthProvider(
- auth_url="https://auth.yourdomain.com",
- introspection_client_id="your-client-id",
- introspection_client_secret="your-client-secret",
- base_url="https://your-fastmcp-server.com",
- required_scopes=["read:user_data"],
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- auth_url: AnyHttpUrl | str,
- introspection_client_id: str,
- introspection_client_secret: str | SecretStr,
- base_url: AnyHttpUrl | str,
- required_scopes: list[str] | None = None,
- scopes_supported: list[str] | None = None,
- resource_name: str | None = None,
- resource_documentation: AnyHttpUrl | None = None,
- resource: AnyHttpUrl | str | None = None,
- token_introspection_overrides: (
- PropelAuthTokenIntrospectionOverrides | None
- ) = None,
- ):
- """Initialize PropelAuth provider.
-
- Args:
- auth_url: Your PropelAuth Auth URL (from the Backend Integration page)
- introspection_client_id: Introspection Client ID from the PropelAuth Dashboard
- introspection_client_secret: Introspection Client Secret from the PropelAuth Dashboard
- base_url: Public URL of this FastMCP server
- required_scopes: Optional list of scopes that must be present in tokens
- scopes_supported: Optional list of scopes to advertise in OAuth metadata.
- If None, uses required_scopes. Use this when the scopes clients should
- request differ from the scopes enforced on tokens.
- resource_name: Optional name for the protected resource metadata.
- resource_documentation: Optional documentation URL for the protected resource.
- resource: Optional resource URI (RFC 8707) identifying this MCP server.
- Use this when multiple MCP servers share the same PropelAuth
- authorization server (e.g. ``resource="https://api.example.com/mcp"``),
- so only tokens intended for this MCP server are accepted.
- token_introspection_overrides: Optional overrides for the underlying
- IntrospectionTokenVerifier (timeout, caching, http_client)
- """
- normalized_auth_url = str(auth_url).rstrip("/")
- introspection_url = f"{normalized_auth_url}/oauth/2.1/introspect"
- authorization_server_url = AnyHttpUrl(f"{normalized_auth_url}/oauth/2.1")
-
- if resource is None:
- self._resource = None
- logger.debug(
- "PropelAuthProvider: no resource configured, audience checking disabled"
- )
- else:
- self._resource = str(resource)
-
- token_verifier = self._create_token_verifier(
- introspection_url=introspection_url,
- client_id=introspection_client_id,
- client_secret=introspection_client_secret,
- required_scopes=required_scopes,
- introspection_overrides=token_introspection_overrides,
- )
-
- self._normalized_auth_url = normalized_auth_url
- super().__init__(
- token_verifier=token_verifier,
- authorization_servers=[authorization_server_url],
- base_url=base_url,
- scopes_supported=scopes_supported,
- resource_name=resource_name,
- resource_documentation=resource_documentation,
- )
-
- def get_routes(
- self,
- mcp_path: str | None = None,
- ) -> list[Route]:
- """Get routes for this provider.
-
- Includes the standard routes from the RemoteAuthProvider (protected resource metadata routes (RFC 9728)),
- and creates an authorization server metadata route that forwards to PropelAuth's route
-
- Args:
- mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
- This is used to advertise the resource URL in metadata.
- """
- routes = super().get_routes(mcp_path)
-
- async def oauth_authorization_server_metadata(request):
- """Forward PropelAuth OAuth authorization server metadata"""
- try:
- async with httpx.AsyncClient() as client:
- response = await client.get(
- f"{self._normalized_auth_url}/.well-known/oauth-authorization-server/oauth/2.1"
- )
- response.raise_for_status()
- metadata = response.json()
- return JSONResponse(metadata)
- except Exception as e:
- return JSONResponse(
- {
- "error": "server_error",
- "error_description": f"Failed to fetch PropelAuth metadata: {e}",
- },
- status_code=500,
- )
-
- routes.append(
- Route(
- "/.well-known/oauth-authorization-server",
- endpoint=oauth_authorization_server_metadata,
- methods=["GET"],
- )
- )
-
- return routes
-
- async def verify_token(self, token: str) -> AccessToken | None:
- """Verify token and check the ``aud`` claim against the configured resource."""
- result = await super().verify_token(token)
- if result is None or self._resource is None:
- return result
-
- aud = result.claims.get("aud")
- if aud != self._resource:
- logger.debug(
- "PropelAuthProvider: token audience %r does not match resource %s",
- aud,
- self._resource,
- )
- return None
-
- return result
-
- def _create_token_verifier(
- self,
- introspection_url: str,
- client_id: str,
- client_secret: str | SecretStr,
- required_scopes: list[str] | None,
- introspection_overrides: PropelAuthTokenIntrospectionOverrides | None,
- ) -> IntrospectionTokenVerifier:
- # Being defensive here, check for only the fields we are expecting
- safe_overrides: PropelAuthTokenIntrospectionOverrides = {}
- if introspection_overrides is not None:
- if "timeout_seconds" in introspection_overrides:
- safe_overrides["timeout_seconds"] = introspection_overrides[
- "timeout_seconds"
- ]
- if "cache_ttl_seconds" in introspection_overrides:
- safe_overrides["cache_ttl_seconds"] = introspection_overrides[
- "cache_ttl_seconds"
- ]
- if "max_cache_size" in introspection_overrides:
- safe_overrides["max_cache_size"] = introspection_overrides[
- "max_cache_size"
- ]
- if "http_client" in introspection_overrides:
- safe_overrides["http_client"] = introspection_overrides["http_client"]
-
- return IntrospectionTokenVerifier(
- introspection_url=introspection_url,
- client_id=client_id,
- client_secret=client_secret,
- required_scopes=required_scopes,
- **safe_overrides,
- )
+__all__ = ["PropelAuthProvider", "PropelAuthTokenIntrospectionOverrides"]
diff --git a/src/fastmcp/server/auth/providers/scalekit.py b/src/fastmcp/server/auth/providers/scalekit.py
index ffcacc9c5..fd4b5a88f 100644
--- a/src/fastmcp/server/auth/providers/scalekit.py
+++ b/src/fastmcp/server/auth/providers/scalekit.py
@@ -1,212 +1,20 @@
-"""Scalekit authentication provider for FastMCP.
-
-This module provides ScalekitProvider - a complete authentication solution that integrates
-with Scalekit's OAuth 2.1 and OpenID Connect services, supporting Resource Server
-authentication for seamless MCP client authentication.
-"""
+"""Backward compatibility shim for Scalekit auth provider."""
from __future__ import annotations
-import httpx
-from pydantic import AnyHttpUrl
-from starlette.responses import JSONResponse
-from starlette.routing import Route
+import warnings
-from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
-from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-logger = get_logger(__name__)
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.scalekit is deprecated. "
+ "Import from fastmcp.server.plugins.auth.scalekit.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
+from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
-class ScalekitProvider(RemoteAuthProvider):
- """Scalekit resource server provider for OAuth 2.1 authentication.
-
- This provider implements Scalekit integration using resource server pattern.
- FastMCP acts as a protected resource server that validates access tokens issued
- by Scalekit's authorization server.
-
- IMPORTANT SETUP REQUIREMENTS:
-
- 1. Create an MCP Server in Scalekit Dashboard:
- - Go to your [Scalekit Dashboard](https://app.scalekit.com/)
- - Navigate to MCP Servers section
- - Register a new MCP Server with appropriate scopes
- - Ensure the Resource Identifier matches exactly what you configure as MCP URL
- - Note the Resource ID
-
- 2. Environment Configuration:
- - Set SCALEKIT_ENVIRONMENT_URL (e.g., https://your-env.scalekit.com)
- - Set SCALEKIT_RESOURCE_ID from your created resource
- - Set BASE_URL to your FastMCP server's public URL
-
- For detailed setup instructions, see:
- https://docs.scalekit.com/mcp/overview/
-
- Example:
- ```python
- from fastmcp.server.auth.providers.scalekit import ScalekitProvider
-
- # Create Scalekit resource server provider
- scalekit_auth = ScalekitProvider(
- environment_url="https://your-env.scalekit.com",
- resource_id="sk_resource_...",
- base_url="https://your-fastmcp-server.com",
- )
-
- # Use with FastMCP
- mcp = FastMCP("My App", auth=scalekit_auth)
- ```
- """
-
- def __init__(
- self,
- *,
- environment_url: AnyHttpUrl | str,
- resource_id: str,
- base_url: AnyHttpUrl | str | None = None,
- mcp_url: AnyHttpUrl | str | None = None,
- client_id: str | None = None,
- required_scopes: list[str] | None = None,
- scopes_supported: list[str] | None = None,
- resource_name: str | None = None,
- resource_documentation: AnyHttpUrl | None = None,
- token_verifier: TokenVerifier | None = None,
- ):
- """Initialize Scalekit resource server provider.
-
- Args:
- environment_url: Your Scalekit environment URL (e.g., "https://your-env.scalekit.com")
- resource_id: Your Scalekit resource ID
- base_url: Public URL of this FastMCP server (or use mcp_url for backwards compatibility)
- mcp_url: Deprecated alias for base_url. Will be removed in a future release.
- client_id: Deprecated parameter, no longer required. Will be removed in a future release.
- required_scopes: Optional list of scopes that must be present in tokens
- scopes_supported: Optional list of scopes to advertise in OAuth metadata.
- If None, uses required_scopes. Use this when the scopes clients should
- request differ from the scopes enforced on tokens.
- resource_name: Optional name for the protected resource metadata.
- resource_documentation: Optional documentation URL for the protected resource.
- token_verifier: Optional token verifier. If None, creates JWT verifier for Scalekit
- """
- # Resolve base_url from mcp_url if needed (backwards compatibility)
- resolved_base_url = base_url or mcp_url
- if not resolved_base_url:
- raise ValueError("Either base_url or mcp_url must be provided")
-
- if mcp_url is not None:
- logger.warning(
- "ScalekitProvider parameter 'mcp_url' is deprecated and will be removed in a future release. "
- "Rename it to 'base_url'."
- )
-
- if client_id is not None:
- logger.warning(
- "ScalekitProvider no longer requires 'client_id'. The parameter is accepted only for backward "
- "compatibility and will be removed in a future release."
- )
-
- self.environment_url = str(environment_url).rstrip("/")
- self.resource_id = resource_id
- parsed_scopes = (
- parse_scopes(required_scopes) if required_scopes is not None else []
- )
- self.required_scopes = parsed_scopes
- base_url_value = str(resolved_base_url)
-
- logger.debug(
- "Initializing ScalekitProvider: environment_url=%s resource_id=%s base_url=%s required_scopes=%s",
- self.environment_url,
- self.resource_id,
- base_url_value,
- self.required_scopes,
- )
-
- # Create default JWT verifier if none provided
- if token_verifier is None:
- logger.debug(
- "Creating default JWTVerifier for Scalekit: jwks_uri=%s issuer=%s required_scopes=%s",
- f"{self.environment_url}/keys",
- self.environment_url,
- self.required_scopes,
- )
- token_verifier = JWTVerifier(
- 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:
- logger.debug("Using custom token verifier for ScalekitProvider")
-
- # Initialize RemoteAuthProvider with Scalekit as the authorization server
- super().__init__(
- token_verifier=token_verifier,
- authorization_servers=[
- AnyHttpUrl(f"{self.environment_url}/resources/{self.resource_id}")
- ],
- base_url=base_url_value,
- scopes_supported=scopes_supported,
- resource_name=resource_name,
- resource_documentation=resource_documentation,
- )
-
- def get_routes(
- self,
- mcp_path: str | None = None,
- ) -> list[Route]:
- """Get OAuth routes including Scalekit authorization server metadata forwarding.
-
- This returns the standard protected resource routes plus an authorization server
- metadata endpoint that forwards Scalekit's OAuth metadata to clients.
-
- Args:
- mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
- This is used to advertise the resource URL in metadata.
- """
- # Get the standard protected resource routes from RemoteAuthProvider
- routes = super().get_routes(mcp_path)
- logger.debug(
- "Preparing Scalekit metadata routes: mcp_path=%s resource_id=%s",
- mcp_path,
- self.resource_id,
- )
-
- async def oauth_authorization_server_metadata(request):
- """Forward Scalekit OAuth authorization server metadata with FastMCP customizations."""
- try:
- metadata_url = f"{self.environment_url}/.well-known/oauth-authorization-server/resources/{self.resource_id}"
- logger.debug(
- "Fetching Scalekit OAuth metadata: metadata_url=%s", metadata_url
- )
- async with httpx.AsyncClient() as client:
- response = await client.get(metadata_url)
- response.raise_for_status()
- metadata = response.json()
- logger.debug(
- "Scalekit metadata fetched successfully: metadata_keys=%s",
- list(metadata.keys()),
- )
- return JSONResponse(metadata)
- except Exception as e:
- logger.error(f"Failed to fetch Scalekit metadata: {e}")
- return JSONResponse(
- {
- "error": "server_error",
- "error_description": f"Failed to fetch Scalekit metadata: {e}",
- },
- status_code=500,
- )
-
- # Add Scalekit authorization server metadata forwarding
- routes.append(
- Route(
- "/.well-known/oauth-authorization-server",
- endpoint=oauth_authorization_server_metadata,
- methods=["GET"],
- )
- )
-
- return routes
+__all__ = ["ScalekitProvider"]
diff --git a/src/fastmcp/server/auth/providers/supabase.py b/src/fastmcp/server/auth/providers/supabase.py
index 527d701ee..08d5a3cd7 100644
--- a/src/fastmcp/server/auth/providers/supabase.py
+++ b/src/fastmcp/server/auth/providers/supabase.py
@@ -1,181 +1,20 @@
-"""Supabase authentication provider for FastMCP.
-
-This module provides SupabaseProvider - a complete authentication solution that integrates
-with Supabase Auth's JWT verification, supporting Dynamic Client Registration (DCR)
-for seamless MCP client authentication.
-"""
+"""Backward compatibility shim for Supabase auth provider."""
from __future__ import annotations
-from typing import Literal
+import warnings
-import httpx
-from pydantic import AnyHttpUrl
-from starlette.responses import JSONResponse
-from starlette.routing import Route
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
-from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.supabase is deprecated. "
+ "Import from fastmcp.server.plugins.auth.supabase.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
-logger = get_logger(__name__)
+from fastmcp.server.plugins.auth.supabase.provider import SupabaseProvider
-
-class SupabaseProvider(RemoteAuthProvider):
- """Supabase metadata provider for DCR (Dynamic Client Registration).
-
- This provider implements Supabase Auth integration using metadata forwarding.
- This approach allows Supabase to handle the OAuth flow directly while FastMCP acts
- as a resource server, verifying JWTs issued by Supabase Auth.
-
- IMPORTANT SETUP REQUIREMENTS:
-
- 1. Supabase Project Setup:
- - Create a Supabase project at https://supabase.com
- - Note your project URL (e.g., "https://abc123.supabase.co")
- - Configure your JWT algorithm in Supabase Auth settings (RS256 or ES256)
- - Asymmetric keys (RS256/ES256) are recommended for production
-
- 2. JWT Verification:
- - FastMCP verifies JWTs using the JWKS endpoint at {project_url}{auth_route}/.well-known/jwks.json
- - JWTs are issued by {project_url}{auth_route}
- - Default auth_route is "/auth/v1" (can be customized for self-hosted setups)
- - Tokens are cached for up to 10 minutes by Supabase's edge servers
- - Algorithm must match your Supabase Auth configuration
-
- 3. Authorization:
- - Supabase uses Row Level Security (RLS) policies for database authorization
- - OAuth-level scopes are an upcoming feature in Supabase Auth
- - Both approaches will be supported once scope handling is available
-
- For detailed setup instructions, see:
- https://supabase.com/docs/guides/auth/jwts
-
- Example:
- ```python
- from fastmcp.server.auth.providers.supabase import SupabaseProvider
-
- # Create Supabase metadata provider (JWT verifier created automatically)
- supabase_auth = SupabaseProvider(
- project_url="https://abc123.supabase.co",
- base_url="https://your-fastmcp-server.com",
- algorithm="ES256", # Match your Supabase Auth configuration
- )
-
- # Use with FastMCP
- mcp = FastMCP("My App", auth=supabase_auth)
- ```
- """
-
- def __init__(
- self,
- *,
- project_url: AnyHttpUrl | str,
- base_url: AnyHttpUrl | str,
- auth_route: str = "/auth/v1",
- algorithm: Literal["RS256", "ES256"] = "ES256",
- required_scopes: list[str] | None = None,
- scopes_supported: list[str] | None = None,
- resource_name: str | None = None,
- resource_documentation: AnyHttpUrl | None = None,
- token_verifier: TokenVerifier | None = None,
- ):
- """Initialize Supabase metadata provider.
-
- Args:
- project_url: Your Supabase project URL (e.g., "https://abc123.supabase.co")
- base_url: Public URL of this FastMCP server
- auth_route: Supabase Auth route. Defaults to "/auth/v1". Can be customized
- for self-hosted Supabase Auth setups using custom routes.
- algorithm: JWT signing algorithm (RS256 or ES256). Must match your
- Supabase Auth configuration. Defaults to ES256.
- required_scopes: Optional list of scopes to require for all requests.
- Note: Supabase currently uses RLS policies for authorization. OAuth-level
- scopes are an upcoming feature.
- scopes_supported: Optional list of scopes to advertise in OAuth metadata.
- If None, uses required_scopes. Use this when the scopes clients should
- request differ from the scopes enforced on tokens.
- resource_name: Optional name for the protected resource metadata.
- resource_documentation: Optional documentation URL for the protected resource.
- token_verifier: Optional token verifier. If None, creates JWT verifier for Supabase
- """
- self.project_url = str(project_url).rstrip("/")
- self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
- self.auth_route = auth_route.strip("/")
-
- # Parse scopes if provided as string
- parsed_scopes = (
- parse_scopes(required_scopes) if required_scopes is not None else None
- )
-
- # 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,
- )
-
- # Initialize RemoteAuthProvider with Supabase as the authorization server
- super().__init__(
- token_verifier=token_verifier,
- authorization_servers=[AnyHttpUrl(f"{self.project_url}/{self.auth_route}")],
- base_url=self.base_url,
- scopes_supported=scopes_supported,
- resource_name=resource_name,
- resource_documentation=resource_documentation,
- )
-
- def get_routes(
- self,
- mcp_path: str | None = None,
- ) -> list[Route]:
- """Get OAuth routes including Supabase authorization server metadata forwarding.
-
- This returns the standard protected resource routes plus an authorization server
- metadata endpoint that forwards Supabase's OAuth metadata to clients.
-
- Args:
- mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
- This is used to advertise the resource URL in metadata.
- """
- # Get the standard protected resource routes from RemoteAuthProvider
- routes = super().get_routes(mcp_path)
-
- async def oauth_authorization_server_metadata(request):
- """Forward Supabase OAuth authorization server metadata with FastMCP customizations."""
- try:
- async with httpx.AsyncClient() as client:
- response = await client.get(
- f"{self.project_url}/{self.auth_route}/.well-known/oauth-authorization-server"
- )
- response.raise_for_status()
- metadata = response.json()
- return JSONResponse(metadata)
- except Exception as e:
- return JSONResponse(
- {
- "error": "server_error",
- "error_description": f"Failed to fetch Supabase metadata: {e}",
- },
- status_code=500,
- )
-
- # Add Supabase authorization server metadata forwarding
- routes.append(
- Route(
- "/.well-known/oauth-authorization-server",
- endpoint=oauth_authorization_server_metadata,
- methods=["GET"],
- )
- )
-
- return routes
+__all__ = ["SupabaseProvider"]
diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py
index 91ab83dc1..0c9dcb855 100644
--- a/src/fastmcp/server/auth/providers/workos.py
+++ b/src/fastmcp/server/auth/providers/workos.py
@@ -1,428 +1,25 @@
-"""WorkOS authentication providers for FastMCP.
-
-This module provides two WorkOS authentication strategies:
-
-1. WorkOSProvider - OAuth proxy for WorkOS Connect applications (non-DCR)
-2. AuthKitProvider - DCR-compliant provider for WorkOS AuthKit
-
-Choose based on your WorkOS setup and authentication requirements.
-"""
+"""Backward compatibility shim for WorkOS auth providers."""
from __future__ import annotations
-import contextlib
-from typing import Literal
+import warnings
-import httpx
-from key_value.aio.protocols import AsyncKeyValue
-from pydantic import AnyHttpUrl
-from starlette.responses import JSONResponse
-from starlette.routing import Route
+from fastmcp import settings
+from fastmcp.exceptions import FastMCPDeprecationWarning
-from fastmcp.server.auth import AccessToken, RemoteAuthProvider, TokenVerifier
-from fastmcp.server.auth.oauth_proxy import OAuthProxy
-from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.utilities.auth import parse_scopes
-from fastmcp.utilities.logging import get_logger
+if settings.deprecation_warnings:
+ warnings.warn(
+ "fastmcp.server.auth.providers.workos is deprecated. "
+ "Import from fastmcp.server.plugins.auth.workos.provider or "
+ "fastmcp.server.plugins.auth.authkit.provider instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
-logger = get_logger(__name__)
+from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
+from fastmcp.server.plugins.auth.workos.provider import (
+ WorkOSProvider,
+ WorkOSTokenVerifier,
+)
-
-class WorkOSTokenVerifier(TokenVerifier):
- """Token verifier for WorkOS OAuth tokens.
-
- WorkOS AuthKit tokens are opaque, so we verify them by calling
- the /oauth2/userinfo endpoint to check validity and get user info.
- """
-
- def __init__(
- self,
- *,
- authkit_domain: str,
- required_scopes: list[str] | None = None,
- timeout_seconds: int = 10,
- http_client: httpx.AsyncClient | None = None,
- ):
- """Initialize the WorkOS token verifier.
-
- Args:
- authkit_domain: WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
- required_scopes: Required OAuth scopes
- timeout_seconds: HTTP request timeout
- http_client: Optional httpx.AsyncClient for connection pooling. When provided,
- the client is reused across calls and the caller is responsible for its
- lifecycle. When None (default), a fresh client is created per call.
- """
- super().__init__(required_scopes=required_scopes)
- self.authkit_domain = authkit_domain.rstrip("/")
- self.timeout_seconds = timeout_seconds
- self._http_client = http_client
-
- async def verify_token(self, token: str) -> AccessToken | None:
- """Verify WorkOS OAuth token by calling userinfo endpoint."""
- try:
- async with (
- contextlib.nullcontext(self._http_client)
- if self._http_client is not None
- else httpx.AsyncClient(timeout=self.timeout_seconds)
- ) as client:
- # Use WorkOS AuthKit userinfo endpoint to validate token
- response = await client.get(
- f"{self.authkit_domain}/oauth2/userinfo",
- headers={
- "Authorization": f"Bearer {token}",
- "User-Agent": "FastMCP-WorkOS-OAuth",
- },
- )
-
- if response.status_code != 200:
- logger.debug(
- "WorkOS token verification failed: %d - %s",
- response.status_code,
- response.text[:200],
- )
- return None
-
- user_data = response.json()
- token_scopes = (
- parse_scopes(user_data.get("scope") or user_data.get("scopes"))
- or []
- )
-
- if self.required_scopes and not all(
- scope in token_scopes for scope in self.required_scopes
- ):
- logger.debug(
- "WorkOS token missing required scopes. required=%s actual=%s",
- self.required_scopes,
- token_scopes,
- )
- return None
-
- # Create AccessToken with WorkOS user info
- return AccessToken(
- token=token,
- client_id=str(user_data.get("sub", "unknown")),
- scopes=token_scopes,
- expires_at=None, # Will be set from token introspection if needed
- claims={
- "sub": user_data.get("sub"),
- "email": user_data.get("email"),
- "email_verified": user_data.get("email_verified"),
- "name": user_data.get("name"),
- "given_name": user_data.get("given_name"),
- "family_name": user_data.get("family_name"),
- },
- )
-
- except httpx.RequestError as e:
- logger.debug("Failed to verify WorkOS token: %s", e)
- return None
- except Exception as e:
- logger.debug("WorkOS token verification error: %s", e)
- return None
-
-
-class WorkOSProvider(OAuthProxy):
- """Complete WorkOS OAuth provider for FastMCP.
-
- This provider implements WorkOS AuthKit OAuth using the OAuth Proxy pattern.
- It provides OAuth2 authentication for users through WorkOS Connect applications.
-
- Features:
- - Transparent OAuth proxy to WorkOS AuthKit
- - Automatic token validation via userinfo endpoint
- - User information extraction from ID tokens
- - Support for standard OAuth scopes (openid, profile, email)
-
- Setup Requirements:
- 1. Create a WorkOS Connect application in your dashboard
- 2. Note your AuthKit domain (e.g., "https://your-app.authkit.app")
- 3. Configure redirect URI as: http://localhost:8000/auth/callback
- 4. Note your Client ID and Client Secret
-
- Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.workos import WorkOSProvider
-
- auth = WorkOSProvider(
- client_id="client_123",
- client_secret="sk_test_456",
- authkit_domain="https://your-app.authkit.app",
- base_url="http://localhost:8000"
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
- """
-
- def __init__(
- self,
- *,
- client_id: str,
- client_secret: str,
- authkit_domain: str,
- base_url: AnyHttpUrl | str,
- resource_base_url: AnyHttpUrl | str | None = None,
- issuer_url: AnyHttpUrl | str | None = None,
- redirect_path: str | None = None,
- required_scopes: list[str] | None = None,
- timeout_seconds: int = 10,
- allowed_client_redirect_uris: list[str] | None = None,
- client_storage: AsyncKeyValue | None = None,
- jwt_signing_key: str | bytes | None = None,
- require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_csp_policy: str | None = None,
- forward_resource: bool = True,
- http_client: httpx.AsyncClient | None = None,
- enable_cimd: bool = True,
- ):
- """Initialize WorkOS OAuth provider.
-
- Args:
- client_id: WorkOS client ID
- client_secret: WorkOS client secret
- authkit_domain: Your WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
- base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
- resource_base_url: Optional public base URL for the protected resource metadata
- and token audience. Defaults to ``base_url``.
- issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
- to avoid 404s during discovery when mounting under a path.
- redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback")
- required_scopes: Required OAuth scopes (no default)
- timeout_seconds: HTTP request timeout for WorkOS API calls (defaults to 10)
- allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
- If None (default), all URIs are allowed. If empty list, no URIs are allowed.
- client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
- If None, an encrypted file store will be created in the data directory
- (derived from `platformdirs`).
- jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
- they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
- provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
- require_authorization_consent: Whether to require user consent before authorizing clients (default True).
- When True, users see a consent screen before being redirected to WorkOS.
- When False, authorization proceeds directly without user confirmation.
- When "external", the built-in consent screen is skipped but no warning is
- logged, indicating that consent is handled externally (e.g. by the upstream IdP).
- SECURITY WARNING: Only set to False for local development or testing environments.
- http_client: Optional httpx.AsyncClient for connection pooling in token verification.
- When provided, the client is reused across verify_token calls and the caller
- is responsible for its lifecycle. When None (default), a fresh client is created per call.
- enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
- client IDs (default True). Set to False to disable.
- """
- # Apply defaults and ensure authkit_domain is a full URL
- authkit_domain_str = authkit_domain
- if not authkit_domain_str.startswith(("http://", "https://")):
- authkit_domain_str = f"https://{authkit_domain_str}"
- authkit_domain_final = authkit_domain_str.rstrip("/")
- scopes_final = (
- parse_scopes(required_scopes) if required_scopes is not None else []
- )
-
- # Create WorkOS token verifier
- token_verifier = WorkOSTokenVerifier(
- authkit_domain=authkit_domain_final,
- required_scopes=scopes_final,
- timeout_seconds=timeout_seconds,
- http_client=http_client,
- )
-
- # Initialize OAuth proxy with WorkOS AuthKit endpoints
- super().__init__(
- upstream_authorization_endpoint=f"{authkit_domain_final}/oauth2/authorize",
- upstream_token_endpoint=f"{authkit_domain_final}/oauth2/token",
- upstream_client_id=client_id,
- upstream_client_secret=client_secret,
- token_verifier=token_verifier,
- base_url=base_url,
- resource_base_url=resource_base_url,
- redirect_path=redirect_path,
- issuer_url=issuer_url or base_url, # Default to base_url if not specified
- allowed_client_redirect_uris=allowed_client_redirect_uris,
- client_storage=client_storage,
- jwt_signing_key=jwt_signing_key,
- require_authorization_consent=require_authorization_consent,
- consent_csp_policy=consent_csp_policy,
- forward_resource=forward_resource,
- enable_cimd=enable_cimd,
- )
-
- logger.debug(
- "Initialized WorkOS OAuth provider for client %s with AuthKit domain %s",
- client_id,
- authkit_domain_final,
- )
-
-
-class AuthKitProvider(RemoteAuthProvider):
- """AuthKit metadata provider for DCR (Dynamic Client Registration).
-
- This provider implements AuthKit integration using metadata forwarding
- instead of OAuth proxying. This is the recommended approach for WorkOS DCR
- as it allows WorkOS to handle the OAuth flow directly while FastMCP acts
- as a resource server.
-
- IMPORTANT SETUP REQUIREMENTS:
-
- 1. Enable Dynamic Client Registration in WorkOS Dashboard:
- - Go to Applications → Configuration
- - Toggle "Dynamic Client Registration" to enabled
-
- 2. Configure your FastMCP server URL as a callback:
- - Add your server URL to the Redirects tab in WorkOS dashboard
- - Example: https://your-fastmcp-server.com/oauth2/callback
-
- For detailed setup instructions, see:
- https://workos.com/docs/authkit/mcp/integrating/token-verification
-
- Token audience is bound to this server automatically: when the MCP
- mount path becomes known (typically at ``http_app()`` construction),
- ``JWTVerifier.audience`` is set to the resource URL advertised in
- ``.well-known/oauth-protected-resource``. Enable Resource Indicators
- (RFC 8707) in your WorkOS Dashboard and list that same URL — AuthKit
- will then mint tokens with the matching ``aud`` claim.
-
- Example:
- ```python
- from fastmcp.server.auth.providers.workos import AuthKitProvider
-
- workos_auth = AuthKitProvider(
- authkit_domain="https://your-workos-domain.authkit.app",
- base_url="https://your-fastmcp-server.com",
- )
-
- mcp = FastMCP("My App", auth=workos_auth)
- ```
- """
-
- def __init__(
- self,
- *,
- authkit_domain: AnyHttpUrl | str,
- base_url: AnyHttpUrl | str,
- resource_base_url: AnyHttpUrl | str | None = None,
- required_scopes: list[str] | None = None,
- scopes_supported: list[str] | None = None,
- resource_name: str | None = None,
- resource_documentation: AnyHttpUrl | None = None,
- token_verifier: TokenVerifier | None = None,
- ):
- """Initialize AuthKit metadata provider.
-
- Args:
- authkit_domain: Your AuthKit domain (e.g., "https://your-app.authkit.app")
- base_url: Public URL of this FastMCP server
- resource_base_url: Optional public base URL for the protected resource.
- When provided, this URL is advertised in protected resource metadata
- instead of ``base_url``. Useful when OAuth callbacks and the protected
- MCP resource live under different public URLs.
- required_scopes: Optional list of scopes to require for all requests
- scopes_supported: Optional list of scopes to advertise in OAuth metadata.
- If None, uses required_scopes. Use this when the scopes clients should
- request differ from the scopes enforced on tokens.
- resource_name: Optional name for the protected resource metadata.
- resource_documentation: Optional documentation URL for the protected resource.
- token_verifier: Optional token verifier. If provided, it is used as-is and
- audience auto-wiring is skipped — the caller is responsible for setting
- an appropriate ``audience``. If None (default), a ``JWTVerifier`` is
- created with audience bound to this server's resource URL.
- """
- self.authkit_domain = str(authkit_domain).rstrip("/")
- self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
-
- # Parse scopes if provided as string
- parsed_scopes = (
- parse_scopes(required_scopes) if required_scopes is not None else None
- )
-
- # When no custom verifier is provided, we own the JWTVerifier and can
- # bind its audience to our resource URL once set_mcp_path() is called.
- self._auto_bind_audience = token_verifier is None
- if token_verifier is None:
- token_verifier = JWTVerifier(
- jwks_uri=f"{self.authkit_domain}/oauth2/jwks",
- issuer=self.authkit_domain,
- algorithm="RS256",
- required_scopes=parsed_scopes,
- )
-
- # Initialize RemoteAuthProvider with AuthKit as the authorization server
- super().__init__(
- token_verifier=token_verifier,
- authorization_servers=[AnyHttpUrl(self.authkit_domain)],
- base_url=self.base_url,
- resource_base_url=resource_base_url,
- scopes_supported=scopes_supported,
- resource_name=resource_name,
- resource_documentation=resource_documentation,
- )
-
- def set_mcp_path(self, mcp_path: str | None) -> None:
- """Bind the default verifier's audience to this server's resource URL.
-
- AuthKit with Resource Indicators (RFC 8707) mints tokens whose ``aud``
- claim equals the resource URL the client requested — which is the URL
- we advertise in ``.well-known/oauth-protected-resource``. Binding the
- audience here keeps validation in lock-step with what clients are sent.
- """
- super().set_mcp_path(mcp_path)
- if (
- self._auto_bind_audience
- and self._resource_url is not None
- and isinstance(self.token_verifier, JWTVerifier)
- ):
- resource_url = str(self._resource_url)
- self.token_verifier.audience = resource_url
- logger.info(
- "AuthKit tokens will be validated against aud=%s. "
- "Configure this URL as a Resource Indicator in the WorkOS Dashboard.",
- resource_url,
- )
-
- def get_routes(
- self,
- mcp_path: str | None = None,
- ) -> list[Route]:
- """Get OAuth routes including AuthKit authorization server metadata forwarding.
-
- This returns the standard protected resource routes plus an authorization server
- metadata endpoint that forwards AuthKit's OAuth metadata to clients.
-
- Args:
- mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
- This is used to advertise the resource URL in metadata.
- """
- # Get the standard protected resource routes from RemoteAuthProvider
- routes = super().get_routes(mcp_path)
-
- async def oauth_authorization_server_metadata(request):
- """Forward AuthKit OAuth authorization server metadata with FastMCP customizations."""
- try:
- async with httpx.AsyncClient() as client:
- response = await client.get(
- f"{self.authkit_domain}/.well-known/oauth-authorization-server"
- )
- response.raise_for_status()
- metadata = response.json()
- return JSONResponse(metadata)
- except Exception as e:
- return JSONResponse(
- {
- "error": "server_error",
- "error_description": f"Failed to fetch AuthKit metadata: {e}",
- },
- status_code=500,
- )
-
- # Add AuthKit authorization server metadata forwarding
- routes.append(
- Route(
- "/.well-known/oauth-authorization-server",
- endpoint=oauth_authorization_server_metadata,
- methods=["GET"],
- )
- )
-
- return routes
+__all__ = ["AuthKitProvider", "WorkOSProvider", "WorkOSTokenVerifier"]
diff --git a/src/fastmcp/server/plugins/auth/__init__.py b/src/fastmcp/server/plugins/auth/__init__.py
index f78bb69f2..3fa68acda 100644
--- a/src/fastmcp/server/plugins/auth/__init__.py
+++ b/src/fastmcp/server/plugins/auth/__init__.py
@@ -1,67 +1,3 @@
-"""Auth plugins for FastMCP."""
+"""Auth plugin namespace for FastMCP."""
-from fastmcp.server.plugins.auth.providers import (
- Auth0Auth,
- Auth0AuthConfig,
- AuthKitAuth,
- AuthKitAuthConfig,
- AWSCognitoAuth,
- AWSCognitoAuthConfig,
- AzureAuth,
- AzureAuthConfig,
- ClerkAuth,
- ClerkAuthConfig,
- DescopeAuth,
- DescopeAuthConfig,
- DiscordAuth,
- DiscordAuthConfig,
- GitHubAuth,
- GitHubAuthConfig,
- GoogleAuth,
- GoogleAuthConfig,
- KeycloakAuth,
- KeycloakAuthConfig,
- OCIAuth,
- OCIAuthConfig,
- PropelAuth,
- PropelAuthConfig,
- ScalekitAuth,
- ScalekitAuthConfig,
- SupabaseAuth,
- SupabaseAuthConfig,
- WorkOSAuth,
- WorkOSAuthConfig,
-)
-
-__all__ = [
- "AWSCognitoAuth",
- "AWSCognitoAuthConfig",
- "Auth0Auth",
- "Auth0AuthConfig",
- "AuthKitAuth",
- "AuthKitAuthConfig",
- "AzureAuth",
- "AzureAuthConfig",
- "ClerkAuth",
- "ClerkAuthConfig",
- "DescopeAuth",
- "DescopeAuthConfig",
- "DiscordAuth",
- "DiscordAuthConfig",
- "GitHubAuth",
- "GitHubAuthConfig",
- "GoogleAuth",
- "GoogleAuthConfig",
- "KeycloakAuth",
- "KeycloakAuthConfig",
- "OCIAuth",
- "OCIAuthConfig",
- "PropelAuth",
- "PropelAuthConfig",
- "ScalekitAuth",
- "ScalekitAuthConfig",
- "SupabaseAuth",
- "SupabaseAuthConfig",
- "WorkOSAuth",
- "WorkOSAuthConfig",
-]
+__all__: list[str] = []
diff --git a/src/fastmcp/server/plugins/auth/_base.py b/src/fastmcp/server/plugins/auth/_base.py
new file mode 100644
index 000000000..f5a7f93f5
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/_base.py
@@ -0,0 +1,65 @@
+"""Shared primitives for first-party auth plugins."""
+
+from __future__ import annotations
+
+from typing import Any, Generic, Literal, TypeVar
+
+from pydantic import AnyHttpUrl, BaseModel, ConfigDict
+
+from fastmcp.server.plugins.base import Plugin
+
+ConsentMode = bool | Literal["remember", "external"]
+Algorithm = Literal["RS256", "ES256"]
+ConfigT = TypeVar("ConfigT", bound=BaseModel)
+
+
+class AuthPlugin(Plugin[ConfigT], Generic[ConfigT]):
+ def _require(self, *fields: str) -> None:
+ missing = [field for field in fields if getattr(self.config, field) is None]
+ if missing:
+ names = ", ".join(f"`{field}`" for field in missing)
+ raise ValueError(f"{type(self).__name__} requires {names}.")
+
+ def _require_one(self, *fields: str) -> None:
+ if not any(getattr(self.config, field) is not None for field in fields):
+ names = " or ".join(f"`{field}`" for field in fields)
+ raise ValueError(f"{type(self).__name__} requires {names}.")
+
+ def _kwargs(self, *fields: str) -> dict[str, Any]:
+ return {
+ field: getattr(self.config, field)
+ for field in fields
+ if getattr(self.config, field) is not None
+ }
+
+
+class PluginConfig(BaseModel):
+ model_config = ConfigDict(extra="forbid")
+
+
+class OAuthProxyConfig(PluginConfig):
+ base_url: AnyHttpUrl | str | None = None
+ resource_base_url: AnyHttpUrl | str | None = None
+ issuer_url: AnyHttpUrl | str | None = None
+ redirect_path: str | None = None
+ required_scopes: list[str] | None = None
+ allowed_client_redirect_uris: list[str] | None = None
+ jwt_signing_key: str | None = None
+ require_authorization_consent: ConsentMode = True
+ consent_csp_policy: str | None = None
+ forward_resource: bool = True
+
+
+class OAuthProviderConfig(OAuthProxyConfig):
+ client_id: str | None = None
+ client_secret: str | None = None
+ timeout_seconds: int = 10
+ enable_cimd: bool = True
+
+
+class RemoteAuthConfig(PluginConfig):
+ base_url: AnyHttpUrl | str | None = None
+ required_scopes: list[str] | None = None
+ scopes_supported: list[str] | None = None
+ resource_name: str | None = None
+ resource_documentation: AnyHttpUrl | None = None
diff --git a/src/fastmcp/server/plugins/auth/auth0/__init__.py b/src/fastmcp/server/plugins/auth/auth0/__init__.py
new file mode 100644
index 000000000..f80363cdb
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/auth0/__init__.py
@@ -0,0 +1,5 @@
+"""Auth0 auth plugin."""
+
+from fastmcp.server.plugins.auth.auth0.plugin import Auth0Auth
+
+__all__ = ["Auth0Auth"]
diff --git a/src/fastmcp/server/plugins/auth/auth0/plugin.py b/src/fastmcp/server/plugins/auth/auth0/plugin.py
new file mode 100644
index 000000000..accacf678
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/auth0/plugin.py
@@ -0,0 +1,63 @@
+"""Auth0 auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+from key_value.aio.protocols import AsyncKeyValue
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import AuthProvider
+from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProxyConfig
+from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class Auth0AuthConfig(OAuthProxyConfig):
+ """Config model for the Auth0 auth plugin."""
+
+ config_url: AnyHttpUrl | str | None = None
+ client_id: str | None = None
+ client_secret: str | None = None
+ audience: str | None = None
+
+
+class Auth0Auth(AuthPlugin[Auth0AuthConfig]):
+ """Contribute an `Auth0Provider` as the server's auth provider."""
+
+ Config: ClassVar[type[Auth0AuthConfig]] = Auth0AuthConfig
+
+ meta = PluginMeta(name="auth0-auth")
+
+ def __init__(
+ self,
+ config: Auth0AuthConfig | dict[str, Any] | None = None,
+ *,
+ client_storage: AsyncKeyValue | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._client_storage = client_storage
+
+ def auth(self) -> AuthProvider | None:
+ self._require(
+ "config_url", "client_id", "client_secret", "audience", "base_url"
+ )
+ return Auth0Provider(
+ **self._kwargs(
+ "config_url",
+ "client_id",
+ "client_secret",
+ "audience",
+ "base_url",
+ "resource_base_url",
+ "issuer_url",
+ "required_scopes",
+ "redirect_path",
+ "allowed_client_redirect_uris",
+ "jwt_signing_key",
+ "require_authorization_consent",
+ "consent_csp_policy",
+ "forward_resource",
+ ),
+ client_storage=self._client_storage,
+ )
diff --git a/src/fastmcp/server/plugins/auth/auth0/provider.py b/src/fastmcp/server/plugins/auth/auth0/provider.py
new file mode 100644
index 000000000..ab1abe6fa
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/auth0/provider.py
@@ -0,0 +1,135 @@
+"""Auth0 OAuth provider for FastMCP.
+
+This module provides a complete Auth0 integration that's ready to use with
+just the configuration URL, client ID, client secret, audience, and base URL.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
+
+ # Simple Auth0 OAuth protection
+ auth = Auth0Provider(
+ config_url="https://auth0.config.url",
+ client_id="your-auth0-client-id",
+ client_secret="your-auth0-client-secret",
+ audience="your-auth0-api-audience",
+ base_url="http://localhost:8000",
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+"""
+
+from typing import Literal
+
+from key_value.aio.protocols import AsyncKeyValue
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth.oidc_proxy import OIDCProxy
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class Auth0Provider(OIDCProxy):
+ """An Auth0 provider implementation for FastMCP.
+
+ This provider is a complete Auth0 integration that's ready to use with
+ just the configuration URL, client ID, client secret, audience, and base URL.
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
+
+ # Simple Auth0 OAuth protection
+ auth = Auth0Provider(
+ config_url="https://auth0.config.url",
+ client_id="your-auth0-client-id",
+ client_secret="your-auth0-client-secret",
+ audience="your-auth0-api-audience",
+ base_url="http://localhost:8000",
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ config_url: AnyHttpUrl | str,
+ client_id: str,
+ client_secret: str,
+ audience: str,
+ base_url: AnyHttpUrl | str,
+ resource_base_url: AnyHttpUrl | str | None = None,
+ issuer_url: AnyHttpUrl | str | None = None,
+ required_scopes: list[str] | None = None,
+ redirect_path: str | None = None,
+ allowed_client_redirect_uris: list[str] | None = None,
+ client_storage: AsyncKeyValue | None = None,
+ jwt_signing_key: str | bytes | None = None,
+ require_authorization_consent: bool | Literal["remember", "external"] = True,
+ consent_csp_policy: str | None = None,
+ forward_resource: bool = True,
+ ) -> None:
+ """Initialize Auth0 OAuth provider.
+
+ Args:
+ config_url: Auth0 config URL
+ client_id: Auth0 application client id
+ client_secret: Auth0 application client secret
+ audience: Auth0 API audience
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ resource_base_url: Optional public base URL for the protected resource metadata
+ and token audience. Defaults to ``base_url``.
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
+ required_scopes: Required Auth0 scopes (defaults to ["openid"])
+ redirect_path: Redirect path configured in Auth0 application
+ allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
+ If None (default), all URIs are allowed. If empty list, no URIs are allowed.
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
+ When True, users see a consent screen before being redirected to Auth0.
+ When False, authorization proceeds directly without user confirmation.
+ When "external", the built-in consent screen is skipped but no warning is
+ logged, indicating that consent is handled externally (e.g. by the upstream IdP).
+ SECURITY WARNING: Only set to False for local development or testing environments.
+ """
+ # Parse scopes if provided as string
+ auth0_required_scopes = (
+ parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
+ )
+
+ super().__init__(
+ config_url=config_url,
+ client_id=client_id,
+ client_secret=client_secret,
+ audience=audience,
+ base_url=base_url,
+ resource_base_url=resource_base_url,
+ issuer_url=issuer_url,
+ redirect_path=redirect_path,
+ required_scopes=auth0_required_scopes,
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
+ client_storage=client_storage,
+ jwt_signing_key=jwt_signing_key,
+ require_authorization_consent=require_authorization_consent,
+ consent_csp_policy=consent_csp_policy,
+ forward_resource=forward_resource,
+ )
+
+ logger.debug(
+ "Initialized Auth0 OAuth provider for client %s with scopes: %s",
+ client_id,
+ auth0_required_scopes,
+ )
diff --git a/src/fastmcp/server/plugins/auth/authkit/__init__.py b/src/fastmcp/server/plugins/auth/authkit/__init__.py
new file mode 100644
index 000000000..6fa1b75ac
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/authkit/__init__.py
@@ -0,0 +1,5 @@
+"""WorkOS AuthKit auth plugin."""
+
+from fastmcp.server.plugins.auth.authkit.plugin import AuthKitAuth
+
+__all__ = ["AuthKitAuth"]
diff --git a/src/fastmcp/server/plugins/auth/authkit/plugin.py b/src/fastmcp/server/plugins/auth/authkit/plugin.py
new file mode 100644
index 000000000..38fc8cb11
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/authkit/plugin.py
@@ -0,0 +1,51 @@
+"""WorkOS AuthKit auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import AuthProvider, TokenVerifier
+from fastmcp.server.plugins.auth._base import AuthPlugin, RemoteAuthConfig
+from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class AuthKitAuthConfig(RemoteAuthConfig):
+ """Config model for the WorkOS AuthKit auth plugin."""
+
+ authkit_domain: AnyHttpUrl | str | None = None
+ resource_base_url: AnyHttpUrl | str | None = None
+
+
+class AuthKitAuth(AuthPlugin[AuthKitAuthConfig]):
+ """Contribute an `AuthKitProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[AuthKitAuthConfig]] = AuthKitAuthConfig
+
+ meta = PluginMeta(name="authkit-auth")
+
+ def __init__(
+ self,
+ config: AuthKitAuthConfig | dict[str, Any] | None = None,
+ *,
+ token_verifier: TokenVerifier | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._token_verifier = token_verifier
+
+ def auth(self) -> AuthProvider | None:
+ self._require("authkit_domain", "base_url")
+ return AuthKitProvider(
+ **self._kwargs(
+ "authkit_domain",
+ "base_url",
+ "resource_base_url",
+ "required_scopes",
+ "scopes_supported",
+ "resource_name",
+ "resource_documentation",
+ ),
+ token_verifier=self._token_verifier,
+ )
diff --git a/src/fastmcp/server/plugins/auth/authkit/provider.py b/src/fastmcp/server/plugins/auth/authkit/provider.py
new file mode 100644
index 000000000..de6498278
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/authkit/provider.py
@@ -0,0 +1,186 @@
+"""WorkOS AuthKit provider."""
+
+from __future__ import annotations
+
+import httpx
+from pydantic import AnyHttpUrl
+from starlette.responses import JSONResponse
+from starlette.routing import Route
+
+from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class AuthKitProvider(RemoteAuthProvider):
+ """AuthKit metadata provider for DCR (Dynamic Client Registration).
+
+ This provider implements AuthKit integration using metadata forwarding
+ instead of OAuth proxying. This is the recommended approach for WorkOS DCR
+ as it allows WorkOS to handle the OAuth flow directly while FastMCP acts
+ as a resource server.
+
+ IMPORTANT SETUP REQUIREMENTS:
+
+ 1. Enable Dynamic Client Registration in WorkOS Dashboard:
+ - Go to Applications -> Configuration
+ - Toggle "Dynamic Client Registration" to enabled
+
+ 2. Configure your FastMCP server URL as a callback:
+ - Add your server URL to the Redirects tab in WorkOS dashboard
+ - Example: https://your-fastmcp-server.com/oauth2/callback
+
+ For detailed setup instructions, see:
+ https://workos.com/docs/authkit/mcp/integrating/token-verification
+
+ Token audience is bound to this server automatically: when the MCP
+ mount path becomes known (typically at ``http_app()`` construction),
+ ``JWTVerifier.audience`` is set to the resource URL advertised in
+ ``.well-known/oauth-protected-resource``. Enable Resource Indicators
+ (RFC 8707) in your WorkOS Dashboard and list that same URL — AuthKit
+ will then mint tokens with the matching ``aud`` claim.
+
+ Example:
+ ```python
+ from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
+
+ workos_auth = AuthKitProvider(
+ authkit_domain="https://your-workos-domain.authkit.app",
+ base_url="https://your-fastmcp-server.com",
+ )
+
+ mcp = FastMCP("My App", auth=workos_auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ authkit_domain: AnyHttpUrl | str,
+ base_url: AnyHttpUrl | str,
+ resource_base_url: AnyHttpUrl | str | None = None,
+ required_scopes: list[str] | None = None,
+ scopes_supported: list[str] | None = None,
+ resource_name: str | None = None,
+ resource_documentation: AnyHttpUrl | None = None,
+ token_verifier: TokenVerifier | None = None,
+ ):
+ """Initialize AuthKit metadata provider.
+
+ Args:
+ authkit_domain: Your AuthKit domain (e.g., "https://your-app.authkit.app")
+ base_url: Public URL of this FastMCP server
+ resource_base_url: Optional public base URL for the protected resource.
+ When provided, this URL is advertised in protected resource metadata
+ instead of ``base_url``. Useful when OAuth callbacks and the protected
+ MCP resource live under different public URLs.
+ required_scopes: Optional list of scopes to require for all requests
+ scopes_supported: Optional list of scopes to advertise in OAuth metadata.
+ If None, uses required_scopes. Use this when the scopes clients should
+ request differ from the scopes enforced on tokens.
+ resource_name: Optional name for the protected resource metadata.
+ resource_documentation: Optional documentation URL for the protected resource.
+ token_verifier: Optional token verifier. If provided, it is used as-is and
+ audience auto-wiring is skipped — the caller is responsible for setting
+ an appropriate ``audience``. If None (default), a ``JWTVerifier`` is
+ created with audience bound to this server's resource URL.
+ """
+ self.authkit_domain = str(authkit_domain).rstrip("/")
+ self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
+
+ parsed_scopes = (
+ parse_scopes(required_scopes) if required_scopes is not None else None
+ )
+
+ # When no custom verifier is provided, we own the JWTVerifier and can
+ # bind its audience to our resource URL once set_mcp_path() is called.
+ self._auto_bind_audience = token_verifier is None
+ if token_verifier is None:
+ token_verifier = JWTVerifier(
+ jwks_uri=f"{self.authkit_domain}/oauth2/jwks",
+ issuer=self.authkit_domain,
+ algorithm="RS256",
+ required_scopes=parsed_scopes,
+ )
+
+ super().__init__(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl(self.authkit_domain)],
+ base_url=self.base_url,
+ resource_base_url=resource_base_url,
+ scopes_supported=scopes_supported,
+ resource_name=resource_name,
+ resource_documentation=resource_documentation,
+ )
+
+ def set_mcp_path(self, mcp_path: str | None) -> None:
+ """Bind the default verifier's audience to this server's resource URL.
+
+ AuthKit with Resource Indicators (RFC 8707) mints tokens whose ``aud``
+ claim equals the resource URL the client requested — which is the URL
+ we advertise in ``.well-known/oauth-protected-resource``. Binding the
+ audience here keeps validation in lock-step with what clients are sent.
+ """
+ super().set_mcp_path(mcp_path)
+ if (
+ self._auto_bind_audience
+ and self._resource_url is not None
+ and isinstance(self.token_verifier, JWTVerifier)
+ ):
+ resource_url = str(self._resource_url)
+ self.token_verifier.audience = resource_url
+ logger.info(
+ "AuthKit tokens will be validated against aud=%s. "
+ "Configure this URL as a Resource Indicator in the WorkOS Dashboard.",
+ resource_url,
+ )
+
+ def get_routes(
+ self,
+ mcp_path: str | None = None,
+ ) -> list[Route]:
+ """Get OAuth routes including AuthKit authorization server metadata forwarding.
+
+ This returns the standard protected resource routes plus an authorization server
+ metadata endpoint that forwards AuthKit's OAuth metadata to clients.
+
+ Args:
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
+ This is used to advertise the resource URL in metadata.
+ """
+ routes = super().get_routes(mcp_path)
+
+ async def oauth_authorization_server_metadata(request):
+ """Forward AuthKit OAuth authorization server metadata with FastMCP customizations."""
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"{self.authkit_domain}/.well-known/oauth-authorization-server"
+ )
+ response.raise_for_status()
+ metadata = response.json()
+ return JSONResponse(metadata)
+ except Exception as e:
+ return JSONResponse(
+ {
+ "error": "server_error",
+ "error_description": f"Failed to fetch AuthKit metadata: {e}",
+ },
+ status_code=500,
+ )
+
+ routes.append(
+ Route(
+ "/.well-known/oauth-authorization-server",
+ endpoint=oauth_authorization_server_metadata,
+ methods=["GET"],
+ )
+ )
+
+ return routes
+
+
+__all__ = ["AuthKitProvider"]
diff --git a/src/fastmcp/server/plugins/auth/aws/__init__.py b/src/fastmcp/server/plugins/auth/aws/__init__.py
new file mode 100644
index 000000000..d9f6677ff
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/aws/__init__.py
@@ -0,0 +1,5 @@
+"""AWS Cognito auth plugin."""
+
+from fastmcp.server.plugins.auth.aws.plugin import AWSCognitoAuth
+
+__all__ = ["AWSCognitoAuth"]
diff --git a/src/fastmcp/server/plugins/auth/aws/plugin.py b/src/fastmcp/server/plugins/auth/aws/plugin.py
new file mode 100644
index 000000000..922c3485a
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/aws/plugin.py
@@ -0,0 +1,61 @@
+"""AWS Cognito auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+from key_value.aio.protocols import AsyncKeyValue
+
+from fastmcp.server.auth import AuthProvider
+from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProxyConfig
+from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class AWSCognitoAuthConfig(OAuthProxyConfig):
+ """Config model for the AWS Cognito auth plugin."""
+
+ user_pool_id: str | None = None
+ client_id: str | None = None
+ client_secret: str | None = None
+ aws_region: str = "eu-central-1"
+ redirect_path: str | None = "/auth/callback"
+
+
+class AWSCognitoAuth(AuthPlugin[AWSCognitoAuthConfig]):
+ """Contribute an `AWSCognitoProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[AWSCognitoAuthConfig]] = AWSCognitoAuthConfig
+
+ meta = PluginMeta(name="aws-cognito-auth")
+
+ def __init__(
+ self,
+ config: AWSCognitoAuthConfig | dict[str, Any] | None = None,
+ *,
+ client_storage: AsyncKeyValue | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._client_storage = client_storage
+
+ def auth(self) -> AuthProvider | None:
+ self._require("user_pool_id", "client_id", "client_secret", "base_url")
+ return AWSCognitoProvider(
+ **self._kwargs(
+ "user_pool_id",
+ "client_id",
+ "client_secret",
+ "base_url",
+ "resource_base_url",
+ "aws_region",
+ "issuer_url",
+ "redirect_path",
+ "required_scopes",
+ "allowed_client_redirect_uris",
+ "jwt_signing_key",
+ "require_authorization_consent",
+ "consent_csp_policy",
+ "forward_resource",
+ ),
+ client_storage=self._client_storage,
+ )
diff --git a/src/fastmcp/server/plugins/auth/aws/provider.py b/src/fastmcp/server/plugins/auth/aws/provider.py
new file mode 100644
index 000000000..6e8dbb74e
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/aws/provider.py
@@ -0,0 +1,229 @@
+"""AWS Cognito OAuth provider for FastMCP.
+
+This module provides a complete AWS Cognito OAuth integration that's ready to use
+with a user pool ID, domain prefix, client ID and client secret. It handles all
+the complexity of AWS Cognito's OAuth flow, token validation, and user management.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
+
+ # Simple AWS Cognito OAuth protection
+ auth = AWSCognitoProvider(
+ user_pool_id="your-user-pool-id",
+ aws_region="eu-central-1",
+ client_id="your-cognito-client-id",
+ client_secret="your-cognito-client-secret"
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+"""
+
+from __future__ import annotations
+
+from typing import Literal
+
+from key_value.aio.protocols import AsyncKeyValue
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth.auth import AccessToken
+from fastmcp.server.auth.oidc_proxy import OIDCProxy
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class AWSCognitoTokenVerifier(JWTVerifier):
+ """Token verifier for Cognito access tokens.
+
+ Cognito access tokens use a ``client_id`` claim instead of the
+ standard ``aud`` claim. This subclass passes ``audience=None``
+ to the parent (skipping the ``aud`` check) and validates the
+ ``client_id`` claim directly.
+ """
+
+ def __init__(self, *, audience: str | list[str] | None = None, **kwargs):
+ self._expected_client_id = audience
+ super().__init__(audience=None, **kwargs)
+
+ async def verify_token(self, token: str) -> AccessToken | None:
+ """Verify token and filter claims to Cognito-specific subset."""
+ access_token = await super().verify_token(token)
+ if not access_token:
+ return None
+
+ # Validate client_id claim (Cognito's equivalent of aud)
+ if self._expected_client_id:
+ token_client_id = access_token.claims.get("client_id")
+ if isinstance(self._expected_client_id, list):
+ valid = token_client_id in self._expected_client_id
+ else:
+ valid = token_client_id == self._expected_client_id
+ if not valid:
+ self.logger.debug(
+ "Token validation failed: client_id mismatch (expected %s, got %s)",
+ self._expected_client_id,
+ token_client_id,
+ )
+ return None
+
+ # Filter claims to Cognito-specific subset
+ cognito_claims = {
+ "sub": access_token.claims.get("sub"),
+ "username": access_token.claims.get("username"),
+ "cognito:groups": access_token.claims.get("cognito:groups", []),
+ }
+
+ return AccessToken(
+ token=access_token.token,
+ client_id=access_token.client_id,
+ scopes=access_token.scopes,
+ expires_at=access_token.expires_at,
+ claims=cognito_claims,
+ )
+
+
+class AWSCognitoProvider(OIDCProxy):
+ """Complete AWS Cognito OAuth provider for FastMCP.
+
+ This provider makes it trivial to add AWS Cognito OAuth protection to any
+ FastMCP server using OIDC Discovery. Just provide your Cognito User Pool details,
+ client credentials, and a base URL, and you're ready to go.
+
+ Features:
+ - Automatic OIDC Discovery from AWS Cognito User Pool
+ - Automatic JWT token validation via Cognito's public keys
+ - Cognito-specific claim filtering (sub, username, cognito:groups)
+ - Support for Cognito User Pools
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
+
+ auth = AWSCognitoProvider(
+ user_pool_id="eu-central-1_XXXXXXXXX",
+ aws_region="eu-central-1",
+ client_id="your-cognito-client-id",
+ client_secret="your-cognito-client-secret",
+ base_url="https://my-server.com",
+ redirect_path="/custom/callback",
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ user_pool_id: str,
+ client_id: str,
+ client_secret: str,
+ base_url: AnyHttpUrl | str,
+ resource_base_url: AnyHttpUrl | str | None = None,
+ aws_region: str = "eu-central-1",
+ issuer_url: AnyHttpUrl | str | None = None,
+ redirect_path: str = "/auth/callback",
+ required_scopes: list[str] | None = None,
+ allowed_client_redirect_uris: list[str] | None = None,
+ client_storage: AsyncKeyValue | None = None,
+ jwt_signing_key: str | bytes | None = None,
+ require_authorization_consent: bool | Literal["remember", "external"] = True,
+ consent_csp_policy: str | None = None,
+ forward_resource: bool = True,
+ ):
+ """Initialize AWS Cognito OAuth provider.
+
+ Args:
+ user_pool_id: Your Cognito User Pool ID (e.g., "eu-central-1_XXXXXXXXX")
+ client_id: Cognito app client ID
+ client_secret: Cognito app client secret
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ resource_base_url: Optional public base URL for the protected resource metadata
+ and token audience. Defaults to ``base_url``.
+ aws_region: AWS region where your User Pool is located (defaults to "eu-central-1")
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
+ redirect_path: Redirect path configured in Cognito app (defaults to "/auth/callback")
+ required_scopes: Required Cognito scopes (defaults to ["openid"])
+ allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
+ If None (default), all URIs are allowed. If empty list, no URIs are allowed.
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
+ When True, users see a consent screen before being redirected to AWS Cognito.
+ When False, authorization proceeds directly without user confirmation.
+ When "external", the built-in consent screen is skipped but no warning is
+ logged, indicating that consent is handled externally (e.g. by the upstream IdP).
+ SECURITY WARNING: Only set to False for local development or testing environments.
+ """
+ # Parse scopes if provided as string
+ required_scopes_final = (
+ parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
+ )
+
+ # Construct OIDC discovery URL
+ config_url = f"https://cognito-idp.{aws_region}.amazonaws.com/{user_pool_id}/.well-known/openid-configuration"
+
+ # Store Cognito-specific info for claim filtering
+ self.user_pool_id = user_pool_id
+ self.aws_region = aws_region
+ self.client_id = client_id
+
+ # Initialize OIDC proxy with Cognito discovery
+ super().__init__(
+ config_url=config_url,
+ client_id=client_id,
+ client_secret=client_secret,
+ algorithm="RS256",
+ required_scopes=required_scopes_final,
+ base_url=base_url,
+ resource_base_url=resource_base_url,
+ issuer_url=issuer_url,
+ redirect_path=redirect_path,
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
+ client_storage=client_storage,
+ jwt_signing_key=jwt_signing_key,
+ require_authorization_consent=require_authorization_consent,
+ consent_csp_policy=consent_csp_policy,
+ forward_resource=forward_resource,
+ )
+
+ logger.debug(
+ "Initialized AWS Cognito OAuth provider for client %s with scopes: %s",
+ client_id,
+ required_scopes_final,
+ )
+
+ def get_token_verifier(
+ self,
+ *,
+ algorithm: str | None = None,
+ audience: str | None = None,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int | None = None,
+ ) -> AWSCognitoTokenVerifier:
+ """Creates a Cognito-specific token verifier with claim filtering.
+
+ Args:
+ algorithm: Optional token verifier algorithm
+ audience: Optional token verifier audience
+ required_scopes: Optional token verifier required_scopes
+ timeout_seconds: HTTP request timeout in seconds
+ """
+ return AWSCognitoTokenVerifier(
+ issuer=str(self.oidc_config.issuer),
+ audience=audience or self.client_id,
+ algorithm=algorithm,
+ jwks_uri=str(self.oidc_config.jwks_uri),
+ required_scopes=required_scopes,
+ )
diff --git a/src/fastmcp/server/plugins/auth/azure/__init__.py b/src/fastmcp/server/plugins/auth/azure/__init__.py
new file mode 100644
index 000000000..c15183a2b
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/azure/__init__.py
@@ -0,0 +1,5 @@
+"""Azure auth plugin."""
+
+from fastmcp.server.plugins.auth.azure.plugin import AzureAuth
+
+__all__ = ["AzureAuth"]
diff --git a/src/fastmcp/server/plugins/auth/azure/plugin.py b/src/fastmcp/server/plugins/auth/azure/plugin.py
new file mode 100644
index 000000000..96e36274f
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/azure/plugin.py
@@ -0,0 +1,69 @@
+"""Azure auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+
+from fastmcp.server.auth import AuthProvider
+from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
+from fastmcp.server.plugins.auth.azure.provider import AzureProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class AzureAuthConfig(OAuthProviderConfig):
+ """Config model for the Azure auth plugin."""
+
+ tenant_id: str | None = None
+ required_scopes: list[str] | None = None
+ identifier_uri: str | None = None
+ additional_authorize_scopes: list[str] | None = None
+ base_authority: str = "login.microsoftonline.com"
+
+
+class AzureAuth(AuthPlugin[AzureAuthConfig]):
+ """Contribute an `AzureProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[AzureAuthConfig]] = AzureAuthConfig
+
+ meta = PluginMeta(name="azure-auth")
+
+ def __init__(
+ self,
+ config: AzureAuthConfig | dict[str, Any] | None = None,
+ *,
+ client_storage: AsyncKeyValue | None = None,
+ http_client: httpx.AsyncClient | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._client_storage = client_storage
+ self._http_client = http_client
+
+ def auth(self) -> AuthProvider | None:
+ self._require("client_id", "tenant_id", "required_scopes", "base_url")
+ self._require_one("client_secret", "jwt_signing_key")
+ return AzureProvider(
+ **self._kwargs(
+ "client_id",
+ "client_secret",
+ "tenant_id",
+ "required_scopes",
+ "base_url",
+ "resource_base_url",
+ "identifier_uri",
+ "issuer_url",
+ "redirect_path",
+ "additional_authorize_scopes",
+ "allowed_client_redirect_uris",
+ "jwt_signing_key",
+ "require_authorization_consent",
+ "consent_csp_policy",
+ "forward_resource",
+ "base_authority",
+ "enable_cimd",
+ ),
+ client_storage=self._client_storage,
+ http_client=self._http_client,
+ )
diff --git a/src/fastmcp/server/plugins/auth/azure/provider.py b/src/fastmcp/server/plugins/auth/azure/provider.py
new file mode 100644
index 000000000..8854665cd
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/azure/provider.py
@@ -0,0 +1,768 @@
+"""Azure (Microsoft Entra) OAuth provider for FastMCP.
+
+This provider implements Azure/Microsoft Entra ID OAuth authentication
+using the OAuth Proxy pattern for non-DCR OAuth flows.
+"""
+
+from __future__ import annotations
+
+import hashlib
+from collections import OrderedDict
+from typing import TYPE_CHECKING, Any, Literal, cast
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+
+from fastmcp.dependencies import Dependency
+from fastmcp.server.auth.auth import MultiAuth
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.utilities.auth import decode_jwt_payload, parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+if TYPE_CHECKING:
+ from azure.identity.aio import OnBehalfOfCredential
+ from mcp.server.auth.provider import AuthorizationParams
+ from mcp.shared.auth import OAuthClientInformationFull
+ from pydantic import AnyHttpUrl
+
+ from fastmcp.server.auth.auth import AuthProvider
+
+logger = get_logger(__name__)
+
+# Standard OIDC scopes that should never be prefixed with identifier_uri.
+# Per Microsoft docs: https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc
+# "OIDC scopes are requested as simple string identifiers without resource prefixes"
+OIDC_SCOPES = frozenset({"openid", "profile", "email", "offline_access"})
+
+
+class AzureProvider(OAuthProxy):
+ """Azure (Microsoft Entra) OAuth provider for FastMCP.
+
+ This provider implements Azure/Microsoft Entra ID authentication using the
+ OAuth Proxy pattern. It supports both organizational accounts and personal
+ Microsoft accounts depending on the tenant configuration.
+
+ Scope Handling:
+ - required_scopes: Provide unprefixed scope names (e.g., ["read", "write"])
+ → Automatically prefixed with identifier_uri during initialization
+ → Validated on all tokens and advertised to MCP clients
+ - additional_authorize_scopes: Provide full format (e.g., ["User.Read"])
+ → NOT prefixed, NOT validated, NOT advertised to clients
+ → Used to request Microsoft Graph or other upstream API permissions
+
+ Features:
+ - OAuth proxy to Azure/Microsoft identity platform
+ - JWT validation using tenant issuer and JWKS
+ - Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
+ - Custom API scopes and Microsoft Graph scopes in a single provider
+
+ Setup:
+ 1. Create an App registration in Azure Portal
+ 2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
+ 3. Add an Application ID URI under "Expose an API" (defaults to api://{client_id})
+ 4. Add custom scopes (e.g., "read", "write") under "Expose an API"
+ 5. Set access token version to 2 in the App manifest: "requestedAccessTokenVersion": 2
+ 6. Create a client secret
+ 7. Get Application (client) ID, Directory (tenant) ID, and client secret
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.azure.provider import AzureProvider
+
+ # Standard Azure (Public Cloud)
+ auth = AzureProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ tenant_id="your-tenant-id",
+ required_scopes=["read", "write"], # Unprefixed scope names
+ additional_authorize_scopes=["User.Read", "Mail.Read"], # Optional Graph scopes
+ base_url="http://localhost:8000",
+ # identifier_uri defaults to api://{client_id}
+ )
+
+ # Azure Government
+ auth_gov = AzureProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ tenant_id="your-tenant-id",
+ required_scopes=["read", "write"],
+ base_authority="login.microsoftonline.us", # Override for Azure Gov
+ base_url="http://localhost:8000",
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ client_id: str,
+ client_secret: str | None = None,
+ tenant_id: str,
+ required_scopes: list[str],
+ base_url: str,
+ resource_base_url: AnyHttpUrl | str | None = None,
+ identifier_uri: str | None = None,
+ issuer_url: str | None = None,
+ redirect_path: str | None = None,
+ additional_authorize_scopes: list[str] | None = None,
+ allowed_client_redirect_uris: list[str] | None = None,
+ client_storage: AsyncKeyValue | None = None,
+ jwt_signing_key: str | bytes | None = None,
+ require_authorization_consent: bool | Literal["remember", "external"] = True,
+ consent_csp_policy: str | None = None,
+ forward_resource: bool = True,
+ base_authority: str = "login.microsoftonline.com",
+ http_client: httpx.AsyncClient | None = None,
+ enable_cimd: bool = True,
+ ) -> None:
+ """Initialize Azure OAuth provider.
+
+ Args:
+ client_id: Azure application (client) ID from your App registration
+ client_secret: Azure client secret from your App registration. Optional when
+ using alternative credentials (e.g., managed identity with a custom
+ _create_upstream_oauth_client override). When omitted, jwt_signing_key
+ must be provided.
+ tenant_id: Azure tenant ID (specific tenant GUID, "organizations", or "consumers")
+ identifier_uri: Optional Application ID URI for your custom API (defaults to api://{client_id}).
+ This URI is automatically prefixed to all required_scopes during initialization.
+ Example: identifier_uri="api://my-api" + required_scopes=["read"]
+ → tokens validated for "api://my-api/read"
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ resource_base_url: Optional public base URL for the protected resource metadata
+ and token audience. Defaults to ``base_url``.
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
+ redirect_path: Redirect path configured in Azure App registration (defaults to "/auth/callback")
+ base_authority: Azure authority base URL (defaults to "login.microsoftonline.com").
+ For Azure Government, use "login.microsoftonline.us".
+ required_scopes: Custom API scope names WITHOUT prefix (e.g., ["read", "write"]).
+ - Automatically prefixed with identifier_uri during initialization
+ - Validated on all tokens
+ - Advertised in Protected Resource Metadata
+ - Must match scope names defined in Azure Portal under "Expose an API"
+ Example: ["read", "write"] → validates tokens containing ["api://xxx/read", "api://xxx/write"]
+ additional_authorize_scopes: Microsoft Graph or other upstream scopes in full format.
+ - NOT prefixed with identifier_uri
+ - NOT validated on tokens
+ - NOT advertised to MCP clients
+ - Used to request additional permissions from Azure (e.g., Graph API access)
+ Example: ["User.Read", "Mail.Read"]
+ These scopes allow your FastMCP server to call Microsoft Graph APIs using the
+ upstream Azure token, but MCP clients are unaware of them.
+ Note: "offline_access" is automatically included to obtain refresh tokens.
+ allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
+ If None (default), all URIs are allowed. If empty list, no URIs are allowed.
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
+ When True, users see a consent screen before being redirected to Azure.
+ When False, authorization proceeds directly without user confirmation.
+ When "external", the built-in consent screen is skipped but no warning is
+ logged, indicating that consent is handled externally (e.g. by the upstream IdP).
+ SECURITY WARNING: Only set to False for local development or testing environments.
+ http_client: Optional httpx.AsyncClient for connection pooling in JWKS fetches.
+ When provided, the client is reused for JWT key fetches and the caller
+ is responsible for its lifecycle. When None (default), a fresh client is created per fetch.
+ enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
+ client IDs (default True). Set to False to disable.
+ """
+ # Parse scopes if provided as string
+ parsed_required_scopes = parse_scopes(required_scopes)
+ parsed_additional_scopes: list[str] = (
+ parse_scopes(additional_authorize_scopes) or []
+ if additional_authorize_scopes
+ else []
+ )
+
+ # Always include offline_access to get refresh tokens from Azure
+ if "offline_access" not in parsed_additional_scopes:
+ parsed_additional_scopes = [*parsed_additional_scopes, "offline_access"]
+
+ # Store Azure-specific config for OBO credential creation
+ self._tenant_id = tenant_id
+ self._base_authority = base_authority
+
+ # Cache of OBO credentials keyed by hash of user assertion token.
+ # Reusing credentials allows the Azure SDK's internal token cache
+ # to avoid redundant OBO exchanges for the same user + scopes.
+ self._obo_credentials: OrderedDict[str, OnBehalfOfCredential] = OrderedDict()
+ self._obo_max_credentials: int = 128
+
+ # Apply defaults
+ self.identifier_uri = identifier_uri or f"api://{client_id}"
+ self.additional_authorize_scopes: list[str] = parsed_additional_scopes
+
+ # Always validate tokens against the app's API client ID using JWT
+ issuer = f"https://{base_authority}/{tenant_id}/v2.0"
+ jwks_uri = f"https://{base_authority}/{tenant_id}/discovery/v2.0/keys"
+
+ # Azure access tokens only include custom API scopes in the `scp` claim,
+ # NOT standard OIDC scopes (openid, profile, email, offline_access).
+ # Filter out OIDC scopes from validation - they'll still be sent to Azure
+ # during authorization (handled by _prefix_scopes_for_azure).
+ validation_scopes = [
+ s for s in (parsed_required_scopes or []) if s not in OIDC_SCOPES
+ ]
+ if not validation_scopes:
+ raise ValueError(
+ "AzureProvider requires at least one non-OIDC scope in "
+ "required_scopes (e.g., 'read', 'write'). OIDC scopes like "
+ "'openid', 'profile', 'email', and 'offline_access' are not "
+ "included in Azure access token claims and cannot be used for "
+ "scope enforcement."
+ )
+
+ token_verifier = JWTVerifier(
+ jwks_uri=jwks_uri,
+ issuer=issuer,
+ audience=[client_id, self.identifier_uri],
+ algorithm="RS256",
+ required_scopes=validation_scopes, # Only validate non-OIDC scopes
+ http_client=http_client,
+ )
+
+ # Build Azure OAuth endpoints with tenant
+ authorization_endpoint = (
+ f"https://{base_authority}/{tenant_id}/oauth2/v2.0/authorize"
+ )
+ token_endpoint = f"https://{base_authority}/{tenant_id}/oauth2/v2.0/token"
+
+ # Initialize OAuth proxy with Azure endpoints
+ # Remember there's hooks called, such as _prepare_scopes_for_token_exchange
+ # and _prepare_scopes_for_upstream_refresh
+ super().__init__(
+ upstream_authorization_endpoint=authorization_endpoint,
+ upstream_token_endpoint=token_endpoint,
+ upstream_client_id=client_id,
+ upstream_client_secret=client_secret,
+ token_verifier=token_verifier,
+ base_url=base_url,
+ resource_base_url=resource_base_url,
+ redirect_path=redirect_path,
+ issuer_url=issuer_url or base_url, # Default to base_url if not specified
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
+ client_storage=client_storage,
+ jwt_signing_key=jwt_signing_key,
+ require_authorization_consent=require_authorization_consent,
+ consent_csp_policy=consent_csp_policy,
+ forward_resource=forward_resource,
+ valid_scopes=parsed_required_scopes,
+ enable_cimd=enable_cimd,
+ )
+
+ authority_info = ""
+ if base_authority != "login.microsoftonline.com":
+ authority_info = f" using authority {base_authority}"
+ logger.info(
+ "Initialized Azure OAuth provider for client %s with tenant %s%s%s",
+ client_id,
+ tenant_id,
+ f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
+ authority_info,
+ )
+
+ async def authorize(
+ self,
+ client: OAuthClientInformationFull,
+ params: AuthorizationParams,
+ ) -> str:
+ """Start OAuth transaction and redirect to Azure AD.
+
+ Override parent's authorize method to filter out the 'resource' parameter
+ which is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use
+ scopes to determine the resource/audience instead of a separate parameter.
+
+ Args:
+ client: OAuth client information
+ params: Authorization parameters from the client
+
+ Returns:
+ Authorization URL to redirect the user to Azure AD
+ """
+ # Clear the resource parameter that Azure AD v2.0 doesn't support
+ # This parameter comes from RFC 8707 (OAuth 2.0 Resource Indicators)
+ # but Azure AD v2.0 uses scopes instead to determine the audience
+ params_to_use = params
+ if hasattr(params, "resource"):
+ original_resource = getattr(params, "resource", None)
+ if original_resource is not None:
+ params_to_use = params.model_copy(update={"resource": None})
+ if original_resource:
+ logger.debug(
+ "Filtering out 'resource' parameter '%s' for Azure AD v2.0 (use scopes instead)",
+ original_resource,
+ )
+ # Don't modify the scopes in params - they stay unprefixed for MCP clients
+ # We'll prefix them when building the Azure authorization URL (in _build_upstream_authorize_url)
+ auth_url = await super().authorize(client, params_to_use)
+ separator = "&" if "?" in auth_url else "?"
+ return f"{auth_url}{separator}prompt=select_account"
+
+ def _prefix_scopes_for_azure(self, scopes: list[str]) -> list[str]:
+ """Prefix unprefixed custom API scopes with identifier_uri for Azure.
+
+ This helper centralizes the scope prefixing logic used in both
+ authorization and token refresh flows.
+
+ Scopes that are NOT prefixed:
+ - Standard OIDC scopes (openid, profile, email, offline_access)
+ - Fully-qualified URIs (contain "://")
+ - Scopes with path component (contain "/")
+
+ Note: Microsoft Graph scopes (e.g., User.Read) should be passed via
+ `additional_authorize_scopes` or use fully-qualified format
+ (e.g., https://graph.microsoft.com/User.Read).
+
+ Args:
+ scopes: List of scopes, may be prefixed or unprefixed
+
+ Returns:
+ List of scopes with identifier_uri prefix applied where needed
+ """
+ prefixed = []
+ for scope in scopes:
+ if scope in OIDC_SCOPES:
+ # Standard OIDC scopes - never prefix
+ prefixed.append(scope)
+ elif "://" in scope or "/" in scope:
+ # Already fully-qualified (e.g., "api://xxx/read" or
+ # "https://graph.microsoft.com/User.Read")
+ prefixed.append(scope)
+ else:
+ # Unprefixed custom API scope - prefix with identifier_uri
+ prefixed.append(f"{self.identifier_uri}/{scope}")
+ return prefixed
+
+ def _build_upstream_authorize_url(
+ self, txn_id: str, transaction: dict[str, Any]
+ ) -> str:
+ """Build Azure authorization URL with prefixed scopes.
+
+ Overrides parent to prefix scopes with identifier_uri before sending to Azure,
+ while keeping unprefixed scopes in the transaction for MCP clients.
+ """
+ # Get unprefixed scopes from transaction
+ unprefixed_scopes = transaction.get("scopes") or self.required_scopes or []
+
+ # Prefix scopes for Azure authorization request
+ prefixed_scopes = self._prefix_scopes_for_azure(unprefixed_scopes)
+
+ # Add Microsoft Graph scopes (not validated, not prefixed)
+ if self.additional_authorize_scopes:
+ prefixed_scopes.extend(self.additional_authorize_scopes)
+
+ # Temporarily modify transaction dict for parent's URL building
+ modified_transaction = transaction.copy()
+ modified_transaction["scopes"] = prefixed_scopes
+
+ # Let parent build the URL with prefixed scopes
+ return super()._build_upstream_authorize_url(txn_id, modified_transaction)
+
+ def _prepare_scopes_for_token_exchange(self, scopes: list[str]) -> list[str]:
+ """Prepare scopes for Azure authorization code exchange.
+
+ Azure requires scopes during token exchange (AADSTS28003 error if missing).
+ Azure only allows ONE resource per token request (AADSTS28000), so we only
+ include scopes for this API plus OIDC scopes.
+
+ Args:
+ scopes: Scopes from the authorization request (unprefixed)
+
+ Returns:
+ List of scopes for Azure token endpoint
+ """
+ # Prefix scopes for this API
+ prefixed_scopes = self._prefix_scopes_for_azure(scopes or [])
+
+ # Add OIDC scopes only (not other API scopes) to avoid AADSTS28000
+ if self.additional_authorize_scopes:
+ prefixed_scopes.extend(
+ s for s in self.additional_authorize_scopes if s in OIDC_SCOPES
+ )
+
+ deduplicated = list(dict.fromkeys(prefixed_scopes))
+ logger.debug("Token exchange scopes: %s", deduplicated)
+ return deduplicated
+
+ def _prepare_scopes_for_upstream_refresh(self, scopes: list[str]) -> list[str]:
+ """Prepare scopes for Azure token refresh.
+
+ Azure requires fully-qualified scopes and only allows ONE resource per
+ token request (AADSTS28000). We include scopes for this API plus OIDC scopes.
+
+ Args:
+ scopes: Base scopes from RefreshToken (unprefixed, e.g., ["read"])
+
+ Returns:
+ Deduplicated list of scopes formatted for Azure token endpoint
+ """
+ logger.debug("Base scopes from storage: %s", scopes)
+
+ # Filter out any additional_authorize_scopes that may have been stored
+ additional_scopes_set = set(self.additional_authorize_scopes or [])
+ base_scopes = [s for s in scopes if s not in additional_scopes_set]
+
+ # Prefix base scopes with identifier_uri for Azure
+ prefixed_scopes = self._prefix_scopes_for_azure(base_scopes)
+
+ # Add OIDC scopes only (not other API scopes) to avoid AADSTS28000
+ if self.additional_authorize_scopes:
+ prefixed_scopes.extend(
+ s for s in self.additional_authorize_scopes if s in OIDC_SCOPES
+ )
+
+ deduplicated_scopes = list(dict.fromkeys(prefixed_scopes))
+ logger.debug("Scopes for Azure token endpoint: %s", deduplicated_scopes)
+ return deduplicated_scopes
+
+ async def _extract_upstream_claims(
+ self, idp_tokens: dict[str, Any]
+ ) -> dict[str, Any] | None:
+ """Extract claims from Azure token response to embed in FastMCP JWT.
+
+ Decodes the Azure access token (which is a JWT) to extract user identity
+ claims. This allows gateways to inspect upstream identity information by
+ decoding the FastMCP JWT without needing server-side storage lookups.
+
+ Azure access tokens contain claims like:
+ - sub: Subject identifier (unique per user per application)
+ - oid: Object ID (unique user identifier across Azure AD)
+ - tid: Tenant ID
+ - azp: Authorized party (client ID that requested the token)
+ - name: Display name
+ - given_name: First name
+ - family_name: Last name
+ - preferred_username: User principal name (email format)
+ - upn: User Principal Name
+ - email: Email address (if available)
+ - roles: Application roles assigned to the user
+ - groups: Group memberships (if configured)
+
+ Args:
+ idp_tokens: Full token response from Azure, containing access_token
+ and potentially id_token.
+
+ Returns:
+ Dict of extracted claims, or None if extraction fails.
+ """
+ access_token = idp_tokens.get("access_token")
+ if not access_token:
+ return None
+
+ try:
+ # Azure access tokens are JWTs - decode without verification
+ # (already validated by token_verifier during token exchange)
+ payload = decode_jwt_payload(access_token)
+
+ # Extract useful identity claims
+ claims: dict[str, Any] = {}
+ claim_keys = [
+ "sub",
+ "oid",
+ "tid",
+ "azp",
+ "name",
+ "given_name",
+ "family_name",
+ "preferred_username",
+ "upn",
+ "email",
+ "roles",
+ "groups",
+ ]
+ for claim in claim_keys:
+ if claim in payload:
+ claims[claim] = payload[claim]
+
+ if claims:
+ logger.debug(
+ "Extracted %d Azure claims for embedding in FastMCP JWT",
+ len(claims),
+ )
+ return claims
+
+ return None
+
+ except Exception as e:
+ logger.debug("Failed to extract Azure claims: %s", e)
+ return None
+
+ async def get_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential:
+ """Get a cached or new OnBehalfOfCredential for OBO token exchange.
+
+ Credentials are cached by user assertion so the Azure SDK's internal
+ token cache can avoid redundant OBO exchanges when the same user
+ calls multiple tools with the same scopes.
+
+ Args:
+ user_assertion: The user's access token to exchange via OBO.
+
+ Returns:
+ A configured OnBehalfOfCredential ready for get_token() calls.
+
+ Raises:
+ ImportError: If azure-identity is not installed (requires fastmcp[azure]).
+ """
+ _require_azure_identity("OBO token exchange")
+ from azure.identity.aio import OnBehalfOfCredential
+
+ key = hashlib.sha256(user_assertion.encode()).hexdigest()
+
+ if key in self._obo_credentials:
+ self._obo_credentials.move_to_end(key)
+ return self._obo_credentials[key]
+
+ obo_kwargs: dict[str, Any] = {
+ "tenant_id": self._tenant_id,
+ "client_id": self._upstream_client_id,
+ "user_assertion": user_assertion,
+ "authority": f"https://{self._base_authority}",
+ }
+ if self._upstream_client_secret is not None:
+ obo_kwargs["client_secret"] = (
+ self._upstream_client_secret.get_secret_value()
+ )
+ else:
+ raise ValueError(
+ "OBO token exchange requires either a client_secret or a subclass "
+ "that overrides get_obo_credential() to provide alternative credentials "
+ "(e.g., client_assertion_func for managed identity)."
+ )
+ credential = OnBehalfOfCredential(**obo_kwargs)
+ self._obo_credentials[key] = credential
+
+ # Evict oldest if over capacity
+ while len(self._obo_credentials) > self._obo_max_credentials:
+ _, evicted = self._obo_credentials.popitem(last=False)
+ await evicted.close()
+
+ return credential
+
+ async def close_obo_credentials(self) -> None:
+ """Close all cached OBO credentials."""
+ credentials = list(self._obo_credentials.values())
+ self._obo_credentials.clear()
+ for credential in credentials:
+ try:
+ await credential.close()
+ except Exception:
+ logger.debug("Error closing OBO credential", exc_info=True)
+
+
+class AzureJWTVerifier(JWTVerifier):
+ """JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
+
+ Auto-configures JWKS URI, issuer, audience, and scope handling from your
+ Azure app registration details. Designed for Managed Identity and other
+ token-verification-only scenarios where AzureProvider's full OAuth proxy
+ isn't needed.
+
+ Handles Azure's scope format automatically:
+ - Validates tokens using short-form scopes (what Azure puts in ``scp`` claims)
+ - Advertises full-URI scopes in OAuth metadata (what clients need to request)
+
+ Example::
+
+ from fastmcp.server.auth import RemoteAuthProvider
+ from fastmcp.server.plugins.auth.azure.provider import AzureJWTVerifier
+ from pydantic import AnyHttpUrl
+
+ verifier = AzureJWTVerifier(
+ client_id="your-client-id",
+ tenant_id="your-tenant-id",
+ required_scopes=["access_as_user"],
+ )
+
+ auth = RemoteAuthProvider(
+ token_verifier=verifier,
+ authorization_servers=[
+ AnyHttpUrl("https://login.microsoftonline.com/your-tenant-id/v2.0")
+ ],
+ base_url="https://my-server.com",
+ )
+ """
+
+ def __init__(
+ self,
+ *,
+ client_id: str,
+ tenant_id: str,
+ required_scopes: list[str] | None = None,
+ identifier_uri: str | None = None,
+ base_authority: str = "login.microsoftonline.com",
+ ):
+ """Initialize Azure JWT verifier.
+
+ Args:
+ client_id: Azure application (client) ID from your App registration
+ tenant_id: Azure tenant ID (specific tenant GUID, "organizations", or "consumers").
+ For multi-tenant apps ("organizations" or "consumers"), issuer validation
+ is skipped since Azure tokens carry the actual tenant GUID as issuer.
+ required_scopes: Scope names as they appear in Azure Portal under "Expose an API"
+ (e.g., ["access_as_user", "read"]). These are validated against
+ the short-form scopes in token ``scp`` claims, and automatically
+ prefixed with identifier_uri for OAuth metadata.
+ identifier_uri: Application ID URI (defaults to ``api://{client_id}``).
+ Used to prefix scopes in OAuth metadata so clients know the full
+ scope URIs to request from Azure.
+ base_authority: Azure authority base URL (defaults to "login.microsoftonline.com").
+ For Azure Government, use "login.microsoftonline.us".
+ """
+ self._identifier_uri = identifier_uri or f"api://{client_id}"
+
+ # For multi-tenant apps, Azure tokens carry the actual tenant GUID as
+ # issuer, not the literal "organizations" or "consumers" string. Skip
+ # issuer validation for these — audience still protects against wrong-app tokens.
+ multi_tenant_values = {"organizations", "consumers", "common"}
+ issuer: str | None = (
+ None
+ if tenant_id in multi_tenant_values
+ else f"https://{base_authority}/{tenant_id}/v2.0"
+ )
+
+ super().__init__(
+ jwks_uri=f"https://{base_authority}/{tenant_id}/discovery/v2.0/keys",
+ issuer=issuer,
+ audience=[client_id, self._identifier_uri],
+ algorithm="RS256",
+ required_scopes=required_scopes,
+ )
+
+ @property
+ def scopes_supported(self) -> list[str]:
+ """Return scopes with Azure URI prefix for OAuth metadata.
+
+ Azure tokens contain short-form scopes (e.g., ``read``) in the ``scp``
+ claim, but clients must request full URI scopes (e.g.,
+ ``api://client-id/read``) from the Azure authorization endpoint. This
+ property returns the full-URI form for OAuth metadata while
+ ``required_scopes`` retains the short form for token validation.
+ """
+ if not self.required_scopes:
+ return []
+ prefixed = []
+ for scope in self.required_scopes:
+ if scope in OIDC_SCOPES or "://" in scope or "/" in scope:
+ prefixed.append(scope)
+ else:
+ prefixed.append(f"{self._identifier_uri}/{scope}")
+ return prefixed
+
+
+# --- Dependency injection support ---
+# These require fastmcp[azure] extra for azure-identity
+
+
+def _require_azure_identity(feature: str) -> None:
+ """Raise ImportError with install instructions if azure-identity is not available."""
+ try:
+ import azure.identity # noqa: F401
+ except ImportError as e:
+ raise ImportError(
+ f"{feature} requires the `azure` extra. "
+ "Install with: pip install 'fastmcp[azure]'"
+ ) from e
+
+
+def _find_azure_provider(auth: AuthProvider | None) -> AzureProvider | None:
+ """Extract an AzureProvider from an auth provider, unwrapping MultiAuth if needed."""
+ if isinstance(auth, AzureProvider):
+ return auth
+
+ if isinstance(auth, MultiAuth) and isinstance(auth.server, AzureProvider):
+ return auth.server
+
+ return None
+
+
+class _EntraOBOToken(Dependency[str]):
+ """Dependency that performs OBO token exchange for Microsoft Entra.
+
+ Uses azure.identity's OnBehalfOfCredential for async-native OBO,
+ with automatic token caching and refresh. Credentials are cached on
+ the AzureProvider so repeated tool calls reuse existing credentials
+ and benefit from the Azure SDK's internal token cache.
+ """
+
+ def __init__(self, scopes: list[str]):
+ self.scopes = scopes
+
+ async def __aenter__(self) -> str:
+ _require_azure_identity("EntraOBOToken")
+
+ from fastmcp.server.dependencies import get_access_token, get_server
+
+ access_token = get_access_token()
+ if access_token is None:
+ raise RuntimeError(
+ "No access token available. Cannot perform OBO exchange."
+ )
+
+ server = get_server()
+ azure_provider = _find_azure_provider(server.auth)
+ if azure_provider is None:
+ raise RuntimeError(
+ "EntraOBOToken requires an AzureProvider as the auth provider. "
+ f"Current provider: {type(server.auth).__name__}"
+ )
+
+ credential = await azure_provider.get_obo_credential(
+ user_assertion=access_token.token,
+ )
+
+ result = await credential.get_token(*self.scopes)
+ return result.token
+
+
+def EntraOBOToken(scopes: list[str]) -> str:
+ """Exchange the user's Entra token for a downstream API token via OBO.
+
+ This dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,
+ allowing your MCP server to call downstream APIs (like Microsoft Graph) on
+ behalf of the authenticated user.
+
+ Args:
+ scopes: The scopes to request for the downstream API. For Microsoft Graph,
+ use scopes like ["https://graph.microsoft.com/Mail.Read"] or
+ ["https://graph.microsoft.com/.default"].
+
+ Returns:
+ A dependency that resolves to the downstream API access token string
+
+ Raises:
+ ImportError: If fastmcp[azure] is not installed
+ RuntimeError: If no access token is available, provider is not Azure,
+ or OBO exchange fails
+
+ Example:
+ ```python
+ from fastmcp.server.plugins.auth.azure.provider import EntraOBOToken
+ import httpx
+
+ @mcp.tool()
+ async def get_my_emails(
+ graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"])
+ ):
+ async with httpx.AsyncClient() as client:
+ resp = await client.get(
+ "https://graph.microsoft.com/v1.0/me/messages",
+ headers={"Authorization": f"Bearer {graph_token}"}
+ )
+ return resp.json()
+ ```
+
+ Note:
+ For OBO to work, ensure the scopes are included in the AzureProvider's
+ `additional_authorize_scopes` parameter, and that admin consent has been
+ granted for those scopes in your Entra app registration.
+ """
+ return cast(str, _EntraOBOToken(scopes))
diff --git a/src/fastmcp/server/plugins/auth/clerk/__init__.py b/src/fastmcp/server/plugins/auth/clerk/__init__.py
new file mode 100644
index 000000000..22b004aef
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/clerk/__init__.py
@@ -0,0 +1,5 @@
+"""Clerk auth plugin."""
+
+from fastmcp.server.plugins.auth.clerk.plugin import ClerkAuth
+
+__all__ = ["ClerkAuth"]
diff --git a/src/fastmcp/server/plugins/auth/clerk/plugin.py b/src/fastmcp/server/plugins/auth/clerk/plugin.py
new file mode 100644
index 000000000..f0ef4c4b1
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/clerk/plugin.py
@@ -0,0 +1,67 @@
+"""Clerk auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+
+from fastmcp.server.auth import AuthProvider
+from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
+from fastmcp.server.plugins.auth.clerk.provider import ClerkProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class ClerkAuthConfig(OAuthProviderConfig):
+ """Config model for the Clerk auth plugin."""
+
+ domain: str | None = None
+ valid_scopes: list[str] | None = None
+ extra_authorize_params: dict[str, str] | None = None
+
+
+class ClerkAuth(AuthPlugin[ClerkAuthConfig]):
+ """Contribute a `ClerkProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[ClerkAuthConfig]] = ClerkAuthConfig
+
+ meta = PluginMeta(name="clerk-auth")
+
+ def __init__(
+ self,
+ config: ClerkAuthConfig | dict[str, Any] | None = None,
+ *,
+ client_storage: AsyncKeyValue | None = None,
+ http_client: httpx.AsyncClient | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._client_storage = client_storage
+ self._http_client = http_client
+
+ def auth(self) -> AuthProvider | None:
+ self._require("domain", "client_id", "base_url")
+ self._require_one("client_secret", "jwt_signing_key")
+ return ClerkProvider(
+ **self._kwargs(
+ "domain",
+ "client_id",
+ "client_secret",
+ "base_url",
+ "resource_base_url",
+ "issuer_url",
+ "redirect_path",
+ "required_scopes",
+ "valid_scopes",
+ "timeout_seconds",
+ "allowed_client_redirect_uris",
+ "jwt_signing_key",
+ "require_authorization_consent",
+ "consent_csp_policy",
+ "forward_resource",
+ "extra_authorize_params",
+ "enable_cimd",
+ ),
+ client_storage=self._client_storage,
+ http_client=self._http_client,
+ )
diff --git a/src/fastmcp/server/plugins/auth/clerk/provider.py b/src/fastmcp/server/plugins/auth/clerk/provider.py
new file mode 100644
index 000000000..b7888bb47
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/clerk/provider.py
@@ -0,0 +1,388 @@
+"""Clerk OAuth provider for FastMCP.
+
+This module provides a complete Clerk OAuth integration that's ready to use
+with a Clerk domain, client ID, and client secret. It handles all the complexity
+of Clerk's OAuth/OIDC flow, token validation, and user management.
+
+Clerk uses standard OIDC endpoints derived from the instance domain
+(e.g., ``https://.clerk.accounts.dev``). Token verification is
+performed via the introspection endpoint (RFC 7662) for security-critical
+checks (active status, audience, scopes), followed by the userinfo endpoint
+for profile enrichment. Userinfo failure is non-fatal.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.clerk.provider import ClerkProvider
+
+ auth = ClerkProvider(
+ domain="saving-primate-16.clerk.accounts.dev",
+ client_id="your-clerk-client-id",
+ client_secret="your-clerk-client-secret",
+ base_url="https://my-server.com",
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+"""
+
+from __future__ import annotations
+
+import contextlib
+from typing import Literal
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import TokenVerifier
+from fastmcp.server.auth.auth import AccessToken
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class ClerkTokenVerifier(TokenVerifier):
+ """Token verifier for Clerk OAuth tokens.
+
+ Clerk issues standard OIDC tokens. Verification uses the introspection
+ endpoint (RFC 7662) as the primary security gate — it confirms the token
+ is active and provides metadata (scopes, expiry, audience). The userinfo
+ endpoint is called second for profile enrichment (name, email, picture)
+ and its failure is non-fatal.
+
+ When a ``client_id`` is configured, the audience from introspection is
+ validated against it. When ``required_scopes`` are configured,
+ introspection must return the token's scopes — the verifier will not
+ assume scopes when introspection is unavailable.
+ """
+
+ def __init__(
+ self,
+ *,
+ domain: str,
+ client_id: str | None = None,
+ client_secret: str | None = None,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ http_client: httpx.AsyncClient | None = None,
+ ):
+ """Initialize the Clerk token verifier.
+
+ Args:
+ domain: Clerk instance domain (e.g., "saving-primate-16.clerk.accounts.dev")
+ client_id: Clerk OAuth client ID, used for introspection endpoint authentication
+ client_secret: Clerk OAuth client secret, used for introspection endpoint authentication
+ required_scopes: Required OAuth scopes (e.g., ["openid", "email", "profile"])
+ timeout_seconds: HTTP request timeout
+ http_client: Optional httpx.AsyncClient for connection pooling. When provided,
+ the client is reused across calls and the caller is responsible for its
+ lifecycle. When None (default), a fresh client is created per call.
+ """
+ super().__init__(required_scopes=required_scopes)
+ self.domain = domain.rstrip("/")
+ self._client_id = client_id
+ self._client_secret = client_secret
+ self.timeout_seconds = timeout_seconds
+ self._http_client = http_client
+
+ self._userinfo_url = f"https://{self.domain}/oauth/userinfo"
+ self._introspection_url = f"https://{self.domain}/oauth/token_info"
+
+ async def verify_token(self, token: str) -> AccessToken | None:
+ """Verify a Clerk OAuth token via introspection and userinfo.
+
+ Calls the introspection endpoint first to validate the token and
+ retrieve auth metadata (active status, scopes, expiry, audience).
+ If the token passes security checks, the userinfo endpoint is called
+ for profile enrichment. Userinfo failure is non-fatal.
+
+ When a ``client_id`` is configured, the token's audience must match it.
+ When ``required_scopes`` are configured, introspection must confirm
+ them; tokens are rejected if scope information is unavailable.
+ """
+ try:
+ async with (
+ contextlib.nullcontext(self._http_client)
+ if self._http_client is not None
+ else httpx.AsyncClient(timeout=self.timeout_seconds)
+ ) as client:
+ # Step 1: Validate token via introspection (RFC 7662).
+ # Security-critical checks (active, audience, scopes) come first.
+ introspect_data_payload: dict = {"token": token}
+ introspect_kwargs: dict = {
+ "data": introspect_data_payload,
+ "headers": {"User-Agent": "FastMCP-Clerk-OAuth"},
+ }
+
+ if self._client_id and self._client_secret:
+ introspect_kwargs["auth"] = (
+ self._client_id,
+ self._client_secret,
+ )
+ elif self._client_id:
+ introspect_data_payload["client_id"] = self._client_id
+
+ introspect_response = await client.post(
+ self._introspection_url,
+ **introspect_kwargs,
+ )
+
+ if introspect_response.status_code != 200:
+ logger.debug(
+ "Clerk introspection failed: %d",
+ introspect_response.status_code,
+ )
+ return None
+
+ introspect_data = introspect_response.json()
+
+ # RFC 7662 requires the 'active' field in the response.
+ # A missing field indicates a malformed response — reject.
+ if "active" not in introspect_data or not introspect_data["active"]:
+ logger.debug(
+ "Clerk introspection: token inactive or missing 'active' field"
+ )
+ return None
+
+ scope_str = introspect_data.get("scope", "")
+ token_scopes = scope_str.split() if scope_str else []
+
+ aud = introspect_data.get("aud") or introspect_data.get("client_id")
+
+ expires_at: int | None = None
+ exp = introspect_data.get("exp")
+ if exp is not None:
+ with contextlib.suppress(ValueError, TypeError):
+ expires_at = int(exp)
+
+ if self._client_id and aud != self._client_id:
+ logger.debug(
+ "Clerk token audience mismatch: got %s, expected %s",
+ aud,
+ self._client_id,
+ )
+ return None
+
+ if self.required_scopes:
+ if not token_scopes:
+ logger.debug(
+ "Clerk token missing scope information; "
+ "cannot verify required scopes %s",
+ self.required_scopes,
+ )
+ return None
+ token_scopes_set = set(token_scopes)
+ required_scopes_set = set(self.required_scopes)
+ if not required_scopes_set.issubset(token_scopes_set):
+ logger.debug(
+ "Clerk token missing required scopes. Has %s, needs %s",
+ token_scopes_set,
+ required_scopes_set,
+ )
+ return None
+
+ # Step 2: Fetch user profile via userinfo.
+ # Enriches the token with profile data (name, email, picture).
+ sub = introspect_data.get("sub")
+ user_data: dict = {}
+ try:
+ userinfo_response = await client.get(
+ self._userinfo_url,
+ headers={
+ "Authorization": f"Bearer {token}",
+ "User-Agent": "FastMCP-Clerk-OAuth",
+ },
+ )
+ if userinfo_response.status_code == 200:
+ user_data = userinfo_response.json()
+ if not sub:
+ sub = user_data.get("sub")
+ except Exception as e:
+ logger.debug("Clerk userinfo call failed: %s", e)
+
+ if not sub:
+ logger.debug("Clerk token missing 'sub' claim")
+ return None
+
+ access_token = AccessToken(
+ token=token,
+ client_id=aud or sub,
+ scopes=token_scopes,
+ expires_at=expires_at,
+ claims={
+ "sub": sub,
+ "aud": aud,
+ "email": user_data.get("email"),
+ "email_verified": user_data.get("email_verified"),
+ "name": user_data.get("name"),
+ "picture": user_data.get("picture"),
+ "given_name": user_data.get("given_name"),
+ "family_name": user_data.get("family_name"),
+ "preferred_username": user_data.get("preferred_username"),
+ "iss": user_data.get("iss"),
+ "clerk_user_data": user_data or None,
+ },
+ )
+ logger.debug("Clerk token verified successfully for sub=%s", sub)
+ return access_token
+
+ except httpx.RequestError as e:
+ logger.debug("Failed to verify Clerk token: %s", e)
+ return None
+ except Exception as e:
+ logger.debug("Clerk token verification error: %s", e)
+ return None
+
+
+class ClerkProvider(OAuthProxy):
+ """Complete Clerk OAuth provider for FastMCP.
+
+ This provider makes it trivial to add Clerk OAuth protection to any
+ FastMCP server. Provide your Clerk instance domain, OAuth app credentials,
+ and a base URL, and you're ready to go.
+
+ Clerk uses standard OIDC endpoints derived from the instance domain.
+ All endpoint URLs are constructed automatically from the domain parameter.
+
+ Features:
+ - Transparent OAuth proxy to Clerk
+ - Automatic token validation via Clerk's userinfo & introspection APIs
+ - User information extraction from Clerk's OIDC claims
+ - PKCE support (S256)
+ - Minimal configuration required
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.clerk.provider import ClerkProvider
+
+ auth = ClerkProvider(
+ domain="saving-primate-16.clerk.accounts.dev",
+ client_id="your-clerk-client-id",
+ client_secret="your-clerk-client-secret",
+ base_url="https://my-server.com",
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ domain: str,
+ client_id: str,
+ client_secret: str | None = None,
+ base_url: AnyHttpUrl | str,
+ resource_base_url: AnyHttpUrl | str | None = None,
+ issuer_url: AnyHttpUrl | str | None = None,
+ redirect_path: str | None = None,
+ required_scopes: list[str] | None = None,
+ valid_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ allowed_client_redirect_uris: list[str] | None = None,
+ client_storage: AsyncKeyValue | None = None,
+ jwt_signing_key: str | bytes | None = None,
+ require_authorization_consent: bool | Literal["remember", "external"] = True,
+ consent_csp_policy: str | None = None,
+ forward_resource: bool = True,
+ extra_authorize_params: dict[str, str] | None = None,
+ http_client: httpx.AsyncClient | None = None,
+ enable_cimd: bool = True,
+ ):
+ """Initialize Clerk OAuth provider.
+
+ Args:
+ domain: Clerk instance domain (e.g., "saving-primate-16.clerk.accounts.dev").
+ This is used to derive all OAuth/OIDC endpoint URLs.
+ client_id: Clerk OAuth application client ID
+ client_secret: Clerk OAuth application client secret.
+ Optional for PKCE public clients. When omitted, jwt_signing_key must be provided.
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ resource_base_url: Optional public base URL for the protected resource metadata
+ and token audience. Defaults to ``base_url``.
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
+ redirect_path: Redirect path configured in Clerk OAuth app (defaults to "/auth/callback")
+ required_scopes: Required Clerk scopes (defaults to ["openid", "email", "profile"]).
+ Clerk supports: "openid", "email", "profile", "public_metadata",
+ "private_metadata", "offline_access".
+ valid_scopes: All scopes that clients are allowed to request, advertised through
+ well-known endpoints. Defaults to required_scopes if not provided.
+ timeout_seconds: HTTP request timeout for Clerk API calls (defaults to 10)
+ allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
+ If None (default), all URIs are allowed. If empty list, no URIs are allowed.
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
+ If None, an encrypted file store will be created in the data directory
+ (derived from ``platformdirs``).
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes
+ are provided, they will be used as is. If a string is provided, it will be derived
+ into a 32-byte key. If not provided, the upstream client secret will be used to
+ derive a 32-byte key using PBKDF2.
+ require_authorization_consent: Whether to require user consent before authorizing
+ clients (default True). When "external", the built-in consent screen is skipped
+ but no warning is logged, indicating that consent is handled externally by Clerk.
+ consent_csp_policy: Custom CSP policy for the consent page.
+ extra_authorize_params: Additional parameters to forward to Clerk's authorization
+ endpoint. Example: {"prompt": "login"} to force re-authentication.
+ http_client: Optional httpx.AsyncClient for connection pooling in token verification.
+ When provided, the client is reused across verify_token calls and the caller
+ is responsible for its lifecycle. When None (default), a fresh client is created
+ per call.
+ enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
+ client IDs (default True). Set to False to disable.
+ """
+ domain = domain.rstrip("/")
+
+ required_scopes_final = (
+ parse_scopes(required_scopes)
+ if required_scopes is not None
+ else ["openid", "email", "profile"]
+ )
+
+ parsed_valid_scopes = (
+ parse_scopes(valid_scopes) if valid_scopes is not None else None
+ )
+
+ token_verifier = ClerkTokenVerifier(
+ domain=domain,
+ client_id=client_id,
+ client_secret=client_secret,
+ required_scopes=required_scopes_final,
+ timeout_seconds=timeout_seconds,
+ http_client=http_client,
+ )
+
+ extra_authorize_params_final = (
+ dict(extra_authorize_params) if extra_authorize_params else {}
+ )
+
+ super().__init__(
+ upstream_authorization_endpoint=f"https://{domain}/oauth/authorize",
+ upstream_token_endpoint=f"https://{domain}/oauth/token",
+ upstream_client_id=client_id,
+ upstream_client_secret=client_secret,
+ token_verifier=token_verifier,
+ base_url=base_url,
+ resource_base_url=resource_base_url,
+ redirect_path=redirect_path,
+ issuer_url=issuer_url or base_url,
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
+ client_storage=client_storage,
+ jwt_signing_key=jwt_signing_key,
+ require_authorization_consent=require_authorization_consent,
+ consent_csp_policy=consent_csp_policy,
+ forward_resource=forward_resource,
+ extra_authorize_params=extra_authorize_params_final or None,
+ valid_scopes=parsed_valid_scopes,
+ enable_cimd=enable_cimd,
+ )
+
+ logger.debug(
+ "Initialized Clerk OAuth provider for domain %s with scopes: %s",
+ domain,
+ required_scopes_final,
+ )
diff --git a/src/fastmcp/server/plugins/auth/descope/__init__.py b/src/fastmcp/server/plugins/auth/descope/__init__.py
new file mode 100644
index 000000000..14b30571b
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/descope/__init__.py
@@ -0,0 +1,5 @@
+"""Descope auth plugin."""
+
+from fastmcp.server.plugins.auth.descope.plugin import DescopeAuth
+
+__all__ = ["DescopeAuth"]
diff --git a/src/fastmcp/server/plugins/auth/descope/plugin.py b/src/fastmcp/server/plugins/auth/descope/plugin.py
new file mode 100644
index 000000000..9b3c9df48
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/descope/plugin.py
@@ -0,0 +1,55 @@
+"""Descope auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import AuthProvider, TokenVerifier
+from fastmcp.server.plugins.auth._base import AuthPlugin, RemoteAuthConfig
+from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class DescopeAuthConfig(RemoteAuthConfig):
+ """Config model for the Descope auth plugin."""
+
+ config_url: AnyHttpUrl | str | None = None
+ project_id: str | None = None
+ descope_base_url: AnyHttpUrl | str | None = None
+
+
+class DescopeAuth(AuthPlugin[DescopeAuthConfig]):
+ """Contribute a `DescopeProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[DescopeAuthConfig]] = DescopeAuthConfig
+
+ meta = PluginMeta(name="descope-auth")
+
+ def __init__(
+ self,
+ config: DescopeAuthConfig | dict[str, Any] | None = None,
+ *,
+ token_verifier: TokenVerifier | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._token_verifier = token_verifier
+
+ def auth(self) -> AuthProvider | None:
+ self._require("base_url")
+ if self.config.config_url is None:
+ self._require("project_id", "descope_base_url")
+ return DescopeProvider(
+ **self._kwargs(
+ "base_url",
+ "config_url",
+ "project_id",
+ "descope_base_url",
+ "required_scopes",
+ "scopes_supported",
+ "resource_name",
+ "resource_documentation",
+ ),
+ token_verifier=self._token_verifier,
+ )
diff --git a/src/fastmcp/server/plugins/auth/descope/provider.py b/src/fastmcp/server/plugins/auth/descope/provider.py
new file mode 100644
index 000000000..64fc7c021
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/descope/provider.py
@@ -0,0 +1,209 @@
+"""Descope authentication provider for FastMCP.
+
+This module provides DescopeProvider - a complete authentication solution that integrates
+with Descope's OAuth 2.1 and OpenID Connect services, supporting Dynamic Client Registration (DCR)
+for seamless MCP client authentication.
+"""
+
+from __future__ import annotations
+
+from urllib.parse import urlparse
+
+import httpx
+from pydantic import AnyHttpUrl
+from starlette.responses import JSONResponse
+from starlette.routing import Route
+
+from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class DescopeProvider(RemoteAuthProvider):
+ """Descope metadata provider for DCR (Dynamic Client Registration).
+
+ This provider implements Descope integration using metadata forwarding.
+ This is the recommended approach for Descope DCR
+ as it allows Descope to handle the OAuth flow directly while FastMCP acts
+ as a resource server.
+
+ IMPORTANT SETUP REQUIREMENTS:
+
+ 1. Create an MCP Server in Descope Console:
+ - Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console
+ - Create a new MCP Server
+ - Ensure that **Dynamic Client Registration (DCR)** is enabled
+ - Note your Well-Known URL
+
+ 2. Note your Well-Known URL:
+ - Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers)
+ - Format: ``https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration``
+
+ For detailed setup instructions, see:
+ https://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr
+
+ Example:
+ ```python
+ from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
+
+ # Create Descope metadata provider (JWT verifier created automatically)
+ descope_auth = DescopeProvider(
+ config_url="https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration",
+ base_url="https://your-fastmcp-server.com",
+ )
+
+ # Use with FastMCP
+ mcp = FastMCP("My App", auth=descope_auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ base_url: AnyHttpUrl | str,
+ config_url: AnyHttpUrl | str | None = None,
+ project_id: str | None = None,
+ descope_base_url: AnyHttpUrl | str | None = None,
+ required_scopes: list[str] | None = None,
+ scopes_supported: list[str] | None = None,
+ resource_name: str | None = None,
+ resource_documentation: AnyHttpUrl | None = None,
+ token_verifier: TokenVerifier | None = None,
+ ):
+ """Initialize Descope metadata provider.
+
+ Args:
+ base_url: Public URL of this FastMCP server
+ config_url: Your Descope Well-Known URL (e.g., "https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration")
+ This is the new recommended way. If provided, project_id and descope_base_url are ignored.
+ project_id: Your Descope Project ID (e.g., "P2abc123"). Used with descope_base_url for backwards compatibility.
+ descope_base_url: Your Descope base URL (e.g., "https://api.descope.com"). Used with project_id for backwards compatibility.
+ required_scopes: Optional list of scopes that must be present in validated tokens.
+ These scopes will be included in the protected resource metadata.
+ scopes_supported: Optional list of scopes to advertise in OAuth metadata.
+ If None, uses required_scopes. Use this when the scopes clients should
+ request differ from the scopes enforced on tokens.
+ resource_name: Optional name for the protected resource metadata.
+ resource_documentation: Optional documentation URL for the protected resource.
+ token_verifier: Optional token verifier. If None, creates JWT verifier for Descope
+ """
+ self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
+
+ # Parse scopes if provided as string
+ parsed_scopes = (
+ parse_scopes(required_scopes) if required_scopes is not None else None
+ )
+
+ # Determine which API is being used
+ if config_url is not None:
+ # New API: use config_url
+ # Strip /.well-known/openid-configuration from config_url if present
+ issuer_url = str(config_url)
+ if issuer_url.endswith("/.well-known/openid-configuration"):
+ issuer_url = issuer_url[: -len("/.well-known/openid-configuration")]
+
+ # Parse the issuer URL to extract descope_base_url and project_id for other uses
+ parsed_url = urlparse(issuer_url)
+ path_parts = parsed_url.path.strip("/").split("/")
+
+ # Extract project_id from path (format: /v1/apps/agentic/P.../M...)
+ if "agentic" in path_parts:
+ agentic_index = path_parts.index("agentic")
+ if agentic_index + 1 < len(path_parts):
+ self.project_id = path_parts[agentic_index + 1]
+ else:
+ raise ValueError(
+ f"Could not extract project_id from config_url: {issuer_url}"
+ )
+ else:
+ raise ValueError(
+ f"Could not find 'agentic' in config_url path: {issuer_url}"
+ )
+
+ # Extract descope_base_url (scheme + netloc)
+ self.descope_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}".rstrip(
+ "/"
+ )
+ elif project_id is not None and descope_base_url is not None:
+ # Old API: use project_id and descope_base_url
+ self.project_id = project_id
+ descope_base_url_str = str(descope_base_url).rstrip("/")
+ # Ensure descope_base_url has a scheme
+ if not descope_base_url_str.startswith(("http://", "https://")):
+ descope_base_url_str = f"https://{descope_base_url_str}"
+ self.descope_base_url = descope_base_url_str
+ # Old issuer format
+ issuer_url = f"{self.descope_base_url}/v1/apps/{self.project_id}"
+ else:
+ raise ValueError(
+ "Either config_url (new API) or both project_id and descope_base_url (old API) must be provided"
+ )
+
+ # Create default JWT verifier if none provided
+ if token_verifier is None:
+ token_verifier = JWTVerifier(
+ jwks_uri=f"{self.descope_base_url}/{self.project_id}/.well-known/jwks.json",
+ issuer=issuer_url,
+ algorithm="RS256",
+ audience=self.project_id,
+ required_scopes=parsed_scopes,
+ )
+
+ # Initialize RemoteAuthProvider with Descope as the authorization server
+ super().__init__(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl(issuer_url)],
+ base_url=self.base_url,
+ scopes_supported=scopes_supported,
+ resource_name=resource_name,
+ resource_documentation=resource_documentation,
+ )
+
+ def get_routes(
+ self,
+ mcp_path: str | None = None,
+ ) -> list[Route]:
+ """Get OAuth routes including Descope authorization server metadata forwarding.
+
+ This returns the standard protected resource routes plus an authorization server
+ metadata endpoint that forwards Descope's OAuth metadata to clients.
+
+ Args:
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
+ This is used to advertise the resource URL in metadata.
+ """
+ # Get the standard protected resource routes from RemoteAuthProvider
+ routes = super().get_routes(mcp_path)
+
+ async def oauth_authorization_server_metadata(request):
+ """Forward Descope OAuth authorization server metadata with FastMCP customizations."""
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"{self.descope_base_url}/v1/apps/{self.project_id}/.well-known/oauth-authorization-server"
+ )
+ response.raise_for_status()
+ metadata = response.json()
+ return JSONResponse(metadata)
+ except Exception as e:
+ return JSONResponse(
+ {
+ "error": "server_error",
+ "error_description": f"Failed to fetch Descope metadata: {e}",
+ },
+ status_code=500,
+ )
+
+ # Add Descope authorization server metadata forwarding
+ routes.append(
+ Route(
+ "/.well-known/oauth-authorization-server",
+ endpoint=oauth_authorization_server_metadata,
+ methods=["GET"],
+ )
+ )
+
+ return routes
diff --git a/src/fastmcp/server/plugins/auth/discord/__init__.py b/src/fastmcp/server/plugins/auth/discord/__init__.py
new file mode 100644
index 000000000..4fea3f539
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/discord/__init__.py
@@ -0,0 +1,5 @@
+"""Discord auth plugin."""
+
+from fastmcp.server.plugins.auth.discord.plugin import DiscordAuth
+
+__all__ = ["DiscordAuth"]
diff --git a/src/fastmcp/server/plugins/auth/discord/plugin.py b/src/fastmcp/server/plugins/auth/discord/plugin.py
new file mode 100644
index 000000000..eedeb78fe
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/discord/plugin.py
@@ -0,0 +1,59 @@
+"""Discord auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+
+from fastmcp.server.auth import AuthProvider
+from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
+from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class DiscordAuthConfig(OAuthProviderConfig):
+ """Config model for the Discord auth plugin."""
+
+
+class DiscordAuth(AuthPlugin[DiscordAuthConfig]):
+ """Contribute a `DiscordProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[DiscordAuthConfig]] = DiscordAuthConfig
+
+ meta = PluginMeta(name="discord-auth")
+
+ def __init__(
+ self,
+ config: DiscordAuthConfig | dict[str, Any] | None = None,
+ *,
+ client_storage: AsyncKeyValue | None = None,
+ http_client: httpx.AsyncClient | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._client_storage = client_storage
+ self._http_client = http_client
+
+ def auth(self) -> AuthProvider | None:
+ self._require("client_id", "client_secret", "base_url")
+ return DiscordProvider(
+ **self._kwargs(
+ "client_id",
+ "client_secret",
+ "base_url",
+ "resource_base_url",
+ "issuer_url",
+ "redirect_path",
+ "required_scopes",
+ "timeout_seconds",
+ "allowed_client_redirect_uris",
+ "jwt_signing_key",
+ "require_authorization_consent",
+ "consent_csp_policy",
+ "forward_resource",
+ "enable_cimd",
+ ),
+ client_storage=self._client_storage,
+ http_client=self._http_client,
+ )
diff --git a/src/fastmcp/server/plugins/auth/discord/provider.py b/src/fastmcp/server/plugins/auth/discord/provider.py
new file mode 100644
index 000000000..dc8f72974
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/discord/provider.py
@@ -0,0 +1,288 @@
+"""Discord OAuth provider for FastMCP.
+
+This module provides a complete Discord OAuth integration that's ready to use
+with just a client ID and client secret. It handles all the complexity of
+Discord's OAuth flow, token validation, and user management.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
+
+ # Simple Discord OAuth protection
+ auth = DiscordProvider(
+ client_id="your-discord-client-id",
+ client_secret="your-discord-client-secret"
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+"""
+
+from __future__ import annotations
+
+import contextlib
+import time
+from datetime import datetime
+from typing import Literal
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import TokenVerifier
+from fastmcp.server.auth.auth import AccessToken
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class DiscordTokenVerifier(TokenVerifier):
+ """Token verifier for Discord OAuth tokens.
+
+ Discord OAuth tokens are opaque (not JWTs), so we verify them
+ by calling Discord's tokeninfo API to check if they're valid and get user info.
+ """
+
+ def __init__(
+ self,
+ *,
+ expected_client_id: str,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ http_client: httpx.AsyncClient | None = None,
+ ):
+ """Initialize the Discord token verifier.
+
+ Args:
+ expected_client_id: Expected Discord OAuth client ID for audience binding
+ required_scopes: Required OAuth scopes (e.g., ['email'])
+ timeout_seconds: HTTP request timeout
+ http_client: Optional httpx.AsyncClient for connection pooling. When provided,
+ the client is reused across calls and the caller is responsible for its
+ lifecycle. When None (default), a fresh client is created per call.
+ """
+ super().__init__(required_scopes=required_scopes)
+ self.expected_client_id = expected_client_id
+ self.timeout_seconds = timeout_seconds
+ self._http_client = http_client
+
+ async def verify_token(self, token: str) -> AccessToken | None:
+ """Verify Discord OAuth token by calling Discord's tokeninfo API."""
+ try:
+ async with (
+ contextlib.nullcontext(self._http_client)
+ if self._http_client is not None
+ else httpx.AsyncClient(timeout=self.timeout_seconds)
+ ) as client:
+ # Use Discord's tokeninfo endpoint to validate the token
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "User-Agent": "FastMCP-Discord-OAuth",
+ }
+ response = await client.get(
+ "https://discord.com/api/oauth2/@me",
+ headers=headers,
+ )
+
+ if response.status_code != 200:
+ logger.debug(
+ "Discord token verification failed: %d",
+ response.status_code,
+ )
+ return None
+
+ token_info = response.json()
+
+ # Check if token is expired (Discord returns ISO timestamp)
+ expires_str = token_info.get("expires")
+ expires_at = None
+ if expires_str:
+ expires_dt = datetime.fromisoformat(
+ expires_str.replace("Z", "+00:00")
+ )
+ expires_at = int(expires_dt.timestamp())
+ if expires_at <= int(time.time()):
+ logger.debug("Discord token has expired")
+ return None
+
+ token_scopes = token_info.get("scopes", [])
+
+ # Check required scopes
+ if self.required_scopes:
+ token_scopes_set = set(token_scopes)
+ required_scopes_set = set(self.required_scopes)
+ if not required_scopes_set.issubset(token_scopes_set):
+ logger.debug(
+ "Discord token missing required scopes. Has %d, needs %d",
+ len(token_scopes_set),
+ len(required_scopes_set),
+ )
+ return None
+
+ user_data = token_info.get("user", {})
+ application = token_info.get("application") or {}
+ client_id = str(application.get("id", "unknown"))
+ if client_id != self.expected_client_id:
+ logger.debug(
+ "Discord token app ID mismatch: expected %s, got %s",
+ self.expected_client_id,
+ client_id,
+ )
+ return None
+
+ # Create AccessToken with Discord user info
+ access_token = AccessToken(
+ token=token,
+ client_id=client_id,
+ scopes=token_scopes,
+ expires_at=expires_at,
+ claims={
+ "sub": user_data.get("id"),
+ "username": user_data.get("username"),
+ "discriminator": user_data.get("discriminator"),
+ "avatar": user_data.get("avatar"),
+ "email": user_data.get("email"),
+ "verified": user_data.get("verified"),
+ "locale": user_data.get("locale"),
+ "discord_user": user_data,
+ "discord_token_info": token_info,
+ },
+ )
+ logger.debug("Discord token verified successfully")
+ return access_token
+
+ except httpx.RequestError as e:
+ logger.debug("Failed to verify Discord token: %s", e)
+ return None
+ except Exception as e:
+ logger.debug("Discord token verification error: %s", e)
+ return None
+
+
+class DiscordProvider(OAuthProxy):
+ """Complete Discord OAuth provider for FastMCP.
+
+ This provider makes it trivial to add Discord OAuth protection to any
+ FastMCP server. Just provide your Discord OAuth app credentials and
+ a base URL, and you're ready to go.
+
+ Features:
+ - Transparent OAuth proxy to Discord
+ - Automatic token validation via Discord's API
+ - User information extraction from Discord APIs
+ - Minimal configuration required
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
+
+ auth = DiscordProvider(
+ client_id="123456789",
+ client_secret="discord-client-secret-abc123...",
+ base_url="https://my-server.com"
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ client_id: str,
+ client_secret: str,
+ base_url: AnyHttpUrl | str,
+ resource_base_url: AnyHttpUrl | str | None = None,
+ issuer_url: AnyHttpUrl | str | None = None,
+ redirect_path: str | None = None,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ allowed_client_redirect_uris: list[str] | None = None,
+ client_storage: AsyncKeyValue | None = None,
+ jwt_signing_key: str | bytes | None = None,
+ require_authorization_consent: bool | Literal["remember", "external"] = True,
+ consent_csp_policy: str | None = None,
+ forward_resource: bool = True,
+ http_client: httpx.AsyncClient | None = None,
+ enable_cimd: bool = True,
+ ):
+ """Initialize Discord OAuth provider.
+
+ Args:
+ client_id: Discord OAuth client ID (e.g., "123456789")
+ client_secret: Discord OAuth client secret (e.g., "S....")
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ resource_base_url: Optional public base URL for the protected resource metadata
+ and token audience. Defaults to ``base_url``.
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
+ redirect_path: Redirect path configured in Discord OAuth app (defaults to "/auth/callback")
+ required_scopes: Required Discord scopes (defaults to ["identify"]). Common scopes include:
+ - "identify" for profile info (default)
+ - "email" for email access
+ - "guilds" for server membership info
+ timeout_seconds: HTTP request timeout for Discord API calls (defaults to 10)
+ allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
+ If None (default), all URIs are allowed. If empty list, no URIs are allowed.
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
+ When True, users see a consent screen before being redirected to Discord.
+ When False, authorization proceeds directly without user confirmation.
+ When "external", the built-in consent screen is skipped but no warning is
+ logged, indicating that consent is handled externally (e.g. by the upstream IdP).
+ SECURITY WARNING: Only set to False for local development or testing environments.
+ http_client: Optional httpx.AsyncClient for connection pooling in token verification.
+ When provided, the client is reused across verify_token calls and the caller
+ is responsible for its lifecycle. When None (default), a fresh client is created per call.
+ enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
+ client IDs (default True). Set to False to disable.
+ """
+ # Parse scopes if provided as string
+ required_scopes_final = (
+ parse_scopes(required_scopes)
+ if required_scopes is not None
+ else ["identify"]
+ )
+
+ # Create Discord token verifier
+ token_verifier = DiscordTokenVerifier(
+ expected_client_id=client_id,
+ required_scopes=required_scopes_final,
+ timeout_seconds=timeout_seconds,
+ http_client=http_client,
+ )
+
+ # Initialize OAuth proxy with Discord endpoints
+ super().__init__(
+ upstream_authorization_endpoint="https://discord.com/oauth2/authorize",
+ upstream_token_endpoint="https://discord.com/api/oauth2/token",
+ upstream_client_id=client_id,
+ upstream_client_secret=client_secret,
+ token_verifier=token_verifier,
+ base_url=base_url,
+ resource_base_url=resource_base_url,
+ redirect_path=redirect_path,
+ issuer_url=issuer_url or base_url, # Default to base_url if not specified
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
+ client_storage=client_storage,
+ jwt_signing_key=jwt_signing_key,
+ require_authorization_consent=require_authorization_consent,
+ consent_csp_policy=consent_csp_policy,
+ forward_resource=forward_resource,
+ enable_cimd=enable_cimd,
+ )
+
+ logger.debug(
+ "Initialized Discord OAuth provider for client %s with scopes: %s",
+ client_id,
+ required_scopes_final,
+ )
diff --git a/src/fastmcp/server/plugins/auth/github/__init__.py b/src/fastmcp/server/plugins/auth/github/__init__.py
new file mode 100644
index 000000000..5bc2c1159
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/github/__init__.py
@@ -0,0 +1,5 @@
+"""GitHub auth plugin."""
+
+from fastmcp.server.plugins.auth.github.plugin import GitHubAuth
+
+__all__ = ["GitHubAuth"]
diff --git a/src/fastmcp/server/plugins/auth/github/plugin.py b/src/fastmcp/server/plugins/auth/github/plugin.py
new file mode 100644
index 000000000..e11f4f4e4
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/github/plugin.py
@@ -0,0 +1,64 @@
+"""GitHub auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+
+from fastmcp.server.auth import AuthProvider
+from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class GitHubAuthConfig(OAuthProviderConfig):
+ """Config model for the GitHub auth plugin."""
+
+ cache_ttl_seconds: int | None = None
+ max_cache_size: int | None = None
+
+
+class GitHubAuth(AuthPlugin[GitHubAuthConfig]):
+ """Contribute a `GitHubProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[GitHubAuthConfig]] = GitHubAuthConfig
+
+ meta = PluginMeta(name="github-auth")
+
+ def __init__(
+ self,
+ config: GitHubAuthConfig | dict[str, Any] | None = None,
+ *,
+ client_storage: AsyncKeyValue | None = None,
+ http_client: httpx.AsyncClient | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._client_storage = client_storage
+ self._http_client = http_client
+
+ def auth(self) -> AuthProvider | None:
+ self._require("client_id", "client_secret", "base_url")
+ return GitHubProvider(
+ **self._kwargs(
+ "client_id",
+ "client_secret",
+ "base_url",
+ "resource_base_url",
+ "issuer_url",
+ "redirect_path",
+ "required_scopes",
+ "timeout_seconds",
+ "cache_ttl_seconds",
+ "max_cache_size",
+ "allowed_client_redirect_uris",
+ "jwt_signing_key",
+ "require_authorization_consent",
+ "consent_csp_policy",
+ "forward_resource",
+ "enable_cimd",
+ ),
+ client_storage=self._client_storage,
+ http_client=self._http_client,
+ )
diff --git a/src/fastmcp/server/plugins/auth/github/provider.py b/src/fastmcp/server/plugins/auth/github/provider.py
new file mode 100644
index 000000000..2197acf72
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/github/provider.py
@@ -0,0 +1,303 @@
+"""GitHub OAuth provider for FastMCP.
+
+This module provides a complete GitHub OAuth integration that's ready to use
+with just a client ID and client secret. It handles all the complexity of
+GitHub's OAuth flow, token validation, and user management.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.github.provider import GitHubProvider
+
+ # Simple GitHub OAuth protection
+ auth = GitHubProvider(
+ client_id="your-github-client-id",
+ client_secret="your-github-client-secret"
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+"""
+
+from __future__ import annotations
+
+import contextlib
+from typing import Literal
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import TokenVerifier
+from fastmcp.server.auth.auth import AccessToken
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+from fastmcp.utilities.token_cache import TokenCache
+
+logger = get_logger(__name__)
+
+
+class GitHubTokenVerifier(TokenVerifier):
+ """Token verifier for GitHub OAuth tokens.
+
+ GitHub OAuth tokens are opaque (not JWTs), so we verify them
+ by calling GitHub's API to check if they're valid and get user info.
+
+ Caching is disabled by default. Set ``cache_ttl_seconds`` to a positive
+ integer to cache successful verification results and avoid repeated
+ GitHub API calls for the same token.
+ """
+
+ def __init__(
+ self,
+ *,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ cache_ttl_seconds: int | None = None,
+ max_cache_size: int | None = None,
+ http_client: httpx.AsyncClient | None = None,
+ ):
+ """Initialize the GitHub token verifier.
+
+ Args:
+ required_scopes: Required OAuth scopes (e.g., ['user:email'])
+ timeout_seconds: HTTP request timeout
+ cache_ttl_seconds: How long to cache verification results in seconds.
+ Caching is disabled by default (None). Set to a positive integer
+ to enable (e.g., 300 for 5 minutes).
+ max_cache_size: Maximum number of tokens to cache. Default: 10 000.
+ http_client: Optional httpx.AsyncClient for connection pooling. When provided,
+ the client is reused across calls and the caller is responsible for its
+ lifecycle. When None (default), a fresh client is created per call.
+ """
+ super().__init__(required_scopes=required_scopes)
+ self.timeout_seconds = timeout_seconds
+ self._http_client = http_client
+ self._cache = TokenCache(
+ ttl_seconds=cache_ttl_seconds,
+ max_size=max_cache_size,
+ )
+
+ async def verify_token(self, token: str) -> AccessToken | None:
+ """Verify GitHub OAuth token by calling GitHub API."""
+ is_cached, cached_result = self._cache.get(token)
+ if is_cached:
+ logger.debug("GitHub token cache hit")
+ return cached_result
+
+ try:
+ async with (
+ contextlib.nullcontext(self._http_client)
+ if self._http_client is not None
+ else httpx.AsyncClient(timeout=self.timeout_seconds)
+ ) as client:
+ # Get token info from GitHub API
+ response = await client.get(
+ "https://api.github.com/user",
+ headers={
+ "Authorization": f"Bearer {token}",
+ "Accept": "application/vnd.github.v3+json",
+ "User-Agent": "FastMCP-GitHub-OAuth",
+ },
+ )
+
+ if response.status_code != 200:
+ logger.debug(
+ "GitHub token verification failed: %d - %s",
+ response.status_code,
+ response.text[:200],
+ )
+ return None
+
+ user_data = response.json()
+
+ # Get token scopes from GitHub API
+ # GitHub includes scopes in the X-OAuth-Scopes header
+ scopes_response = await client.get(
+ "https://api.github.com/user/repos", # Any authenticated endpoint
+ headers={
+ "Authorization": f"Bearer {token}",
+ "Accept": "application/vnd.github.v3+json",
+ "User-Agent": "FastMCP-GitHub-OAuth",
+ },
+ )
+
+ # Extract scopes from X-OAuth-Scopes header if available
+ scopes_verified = scopes_response.status_code == 200
+ oauth_scopes_header = scopes_response.headers.get("x-oauth-scopes", "")
+ token_scopes = [
+ scope.strip()
+ for scope in oauth_scopes_header.split(",")
+ if scope.strip()
+ ]
+
+ # If no scopes in header, assume basic scopes based on successful user API call
+ if not token_scopes:
+ token_scopes = ["user"] # Basic scope if we can access user info
+
+ # Check required scopes
+ if self.required_scopes:
+ token_scopes_set = set(token_scopes)
+ required_scopes_set = set(self.required_scopes)
+ if not required_scopes_set.issubset(token_scopes_set):
+ logger.debug(
+ "GitHub token missing required scopes. Has %d, needs %d",
+ len(token_scopes_set),
+ len(required_scopes_set),
+ )
+ return None
+
+ # Create AccessToken with GitHub user info
+ result = AccessToken(
+ token=token,
+ client_id=str(user_data.get("id", "unknown")), # Use GitHub user ID
+ scopes=token_scopes,
+ expires_at=None, # GitHub tokens don't typically expire
+ claims={
+ "sub": str(user_data["id"]),
+ "login": user_data.get("login"),
+ "name": user_data.get("name"),
+ "email": user_data.get("email"),
+ "avatar_url": user_data.get("avatar_url"),
+ "github_user_data": user_data,
+ },
+ )
+ if scopes_verified:
+ self._cache.set(token, result)
+ return result
+
+ except httpx.RequestError as e:
+ logger.debug("Failed to verify GitHub token: %s", e)
+ return None
+ except Exception as e:
+ logger.debug("GitHub token verification error: %s", e)
+ return None
+
+
+class GitHubProvider(OAuthProxy):
+ """Complete GitHub OAuth provider for FastMCP.
+
+ This provider makes it trivial to add GitHub OAuth protection to any
+ FastMCP server. Just provide your GitHub OAuth app credentials and
+ a base URL, and you're ready to go.
+
+ Features:
+ - Transparent OAuth proxy to GitHub
+ - Automatic token validation via GitHub API
+ - User information extraction
+ - Minimal configuration required
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.github.provider import GitHubProvider
+
+ auth = GitHubProvider(
+ client_id="Ov23li...",
+ client_secret="abc123...",
+ base_url="https://my-server.com"
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ client_id: str,
+ client_secret: str,
+ base_url: AnyHttpUrl | str,
+ resource_base_url: AnyHttpUrl | str | None = None,
+ issuer_url: AnyHttpUrl | str | None = None,
+ redirect_path: str | None = None,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ cache_ttl_seconds: int | None = None,
+ max_cache_size: int | None = None,
+ allowed_client_redirect_uris: list[str] | None = None,
+ client_storage: AsyncKeyValue | None = None,
+ jwt_signing_key: str | bytes | None = None,
+ require_authorization_consent: bool | Literal["remember", "external"] = True,
+ consent_csp_policy: str | None = None,
+ forward_resource: bool = True,
+ http_client: httpx.AsyncClient | None = None,
+ enable_cimd: bool = True,
+ ):
+ """Initialize GitHub OAuth provider.
+
+ Args:
+ client_id: GitHub OAuth app client ID (e.g., "Ov23li...")
+ client_secret: GitHub OAuth app client secret
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ resource_base_url: Optional public base URL for the protected resource metadata
+ and token audience. Defaults to ``base_url``.
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
+ redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
+ required_scopes: Required GitHub scopes (defaults to ["user"])
+ timeout_seconds: HTTP request timeout for GitHub API calls (defaults to 10)
+ cache_ttl_seconds: How long to cache token verification results in seconds.
+ Caching is disabled by default (None). Set to a positive integer to
+ enable (e.g., 300 for 5 minutes).
+ max_cache_size: Maximum number of tokens to cache. Default: 10 000.
+ allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
+ If None (default), all URIs are allowed. If empty list, no URIs are allowed.
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
+ When True, users see a consent screen before being redirected to GitHub.
+ When False, authorization proceeds directly without user confirmation.
+ When "external", the built-in consent screen is skipped but no warning is
+ logged, indicating that consent is handled externally (e.g. by the upstream IdP).
+ SECURITY WARNING: Only set to False for local development or testing environments.
+ http_client: Optional httpx.AsyncClient for connection pooling in token verification.
+ When provided, the client is reused across verify_token calls and the caller
+ is responsible for its lifecycle. When None (default), a fresh client is created per call.
+ enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
+ client IDs (default True). Set to False to disable.
+ """
+ # Parse scopes if provided as string
+ required_scopes_final = (
+ parse_scopes(required_scopes) if required_scopes is not None else ["user"]
+ )
+
+ # Create GitHub token verifier
+ token_verifier = GitHubTokenVerifier(
+ required_scopes=required_scopes_final,
+ timeout_seconds=timeout_seconds,
+ cache_ttl_seconds=cache_ttl_seconds,
+ max_cache_size=max_cache_size,
+ http_client=http_client,
+ )
+
+ # Initialize OAuth proxy with GitHub endpoints
+ super().__init__(
+ upstream_authorization_endpoint="https://github.com/login/oauth/authorize",
+ upstream_token_endpoint="https://github.com/login/oauth/access_token",
+ upstream_client_id=client_id,
+ upstream_client_secret=client_secret,
+ token_verifier=token_verifier,
+ base_url=base_url,
+ resource_base_url=resource_base_url,
+ redirect_path=redirect_path,
+ issuer_url=issuer_url or base_url, # Default to base_url if not specified
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
+ client_storage=client_storage,
+ jwt_signing_key=jwt_signing_key,
+ require_authorization_consent=require_authorization_consent,
+ consent_csp_policy=consent_csp_policy,
+ forward_resource=forward_resource,
+ enable_cimd=enable_cimd,
+ )
+
+ logger.debug(
+ "Initialized GitHub OAuth provider for client %s with scopes: %s",
+ client_id,
+ required_scopes_final,
+ )
diff --git a/src/fastmcp/server/plugins/auth/google/__init__.py b/src/fastmcp/server/plugins/auth/google/__init__.py
new file mode 100644
index 000000000..e2b72a12e
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/google/__init__.py
@@ -0,0 +1,5 @@
+"""Google auth plugin."""
+
+from fastmcp.server.plugins.auth.google.plugin import GoogleAuth
+
+__all__ = ["GoogleAuth"]
diff --git a/src/fastmcp/server/plugins/auth/google/plugin.py b/src/fastmcp/server/plugins/auth/google/plugin.py
new file mode 100644
index 000000000..381b82ac7
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/google/plugin.py
@@ -0,0 +1,65 @@
+"""Google auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+
+from fastmcp.server.auth import AuthProvider
+from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
+from fastmcp.server.plugins.auth.google.provider import GoogleProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class GoogleAuthConfig(OAuthProviderConfig):
+ """Config model for the Google auth plugin."""
+
+ valid_scopes: list[str] | None = None
+ extra_authorize_params: dict[str, str] | None = None
+
+
+class GoogleAuth(AuthPlugin[GoogleAuthConfig]):
+ """Contribute a `GoogleProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[GoogleAuthConfig]] = GoogleAuthConfig
+
+ meta = PluginMeta(name="google-auth")
+
+ def __init__(
+ self,
+ config: GoogleAuthConfig | dict[str, Any] | None = None,
+ *,
+ client_storage: AsyncKeyValue | None = None,
+ http_client: httpx.AsyncClient | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._client_storage = client_storage
+ self._http_client = http_client
+
+ def auth(self) -> AuthProvider | None:
+ self._require("client_id", "base_url")
+ self._require_one("client_secret", "jwt_signing_key")
+ return GoogleProvider(
+ **self._kwargs(
+ "client_id",
+ "client_secret",
+ "base_url",
+ "resource_base_url",
+ "issuer_url",
+ "redirect_path",
+ "required_scopes",
+ "valid_scopes",
+ "timeout_seconds",
+ "allowed_client_redirect_uris",
+ "jwt_signing_key",
+ "require_authorization_consent",
+ "consent_csp_policy",
+ "forward_resource",
+ "extra_authorize_params",
+ "enable_cimd",
+ ),
+ client_storage=self._client_storage,
+ http_client=self._http_client,
+ )
diff --git a/src/fastmcp/server/plugins/auth/google/provider.py b/src/fastmcp/server/plugins/auth/google/provider.py
new file mode 100644
index 000000000..3f165397c
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/google/provider.py
@@ -0,0 +1,365 @@
+"""Google OAuth provider for FastMCP.
+
+This module provides a complete Google OAuth integration that's ready to use
+with just a client ID and client secret. It handles all the complexity of
+Google's OAuth flow, token validation, and user management.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.google.provider import GoogleProvider
+
+ # Simple Google OAuth protection
+ auth = GoogleProvider(
+ client_id="your-google-client-id.apps.googleusercontent.com",
+ client_secret="your-google-client-secret"
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+"""
+
+from __future__ import annotations
+
+import contextlib
+import time
+from typing import Literal
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import TokenVerifier
+from fastmcp.server.auth.auth import AccessToken
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+GOOGLE_SCOPE_ALIASES: dict[str, str] = {
+ "email": "https://www.googleapis.com/auth/userinfo.email",
+ "profile": "https://www.googleapis.com/auth/userinfo.profile",
+}
+
+
+def _normalize_google_scope(scope: str) -> str:
+ """Normalize a Google scope shorthand to its canonical full URI.
+
+ Google accepts shorthand scopes like "email" and "profile" in authorization
+ requests, but returns the full URI form in token responses. This normalizes
+ to the full URI so comparisons work regardless of which form was used.
+ """
+ return GOOGLE_SCOPE_ALIASES.get(scope, scope)
+
+
+class GoogleTokenVerifier(TokenVerifier):
+ """Token verifier for Google OAuth tokens.
+
+ Google OAuth tokens are opaque (not JWTs), so we verify them by calling
+ Google's tokeninfo endpoint with the access token as a query parameter.
+ This returns the OAuth app ID (``aud``), granted scopes, and expiry time.
+ User profile data (name, picture, etc.) is fetched separately from the
+ v2 userinfo endpoint when the token is valid.
+ """
+
+ def __init__(
+ self,
+ *,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ http_client: httpx.AsyncClient | None = None,
+ ):
+ """Initialize the Google token verifier.
+
+ Args:
+ required_scopes: Required OAuth scopes (e.g., ['openid', 'https://www.googleapis.com/auth/userinfo.email'])
+ timeout_seconds: HTTP request timeout
+ http_client: Optional httpx.AsyncClient for connection pooling. When provided,
+ the client is reused across calls and the caller is responsible for its
+ lifecycle. When None (default), a fresh client is created per call.
+ """
+ normalized = (
+ [_normalize_google_scope(s) for s in required_scopes]
+ if required_scopes
+ else required_scopes
+ )
+ super().__init__(required_scopes=normalized)
+ self.timeout_seconds = timeout_seconds
+ self._http_client = http_client
+
+ async def verify_token(self, token: str) -> AccessToken | None:
+ """Verify a Google OAuth token using the tokeninfo endpoint.
+
+ Calls ``https://oauth2.googleapis.com/tokeninfo?access_token=TOKEN``
+ to validate the token and retrieve the OAuth app ID (``aud``), granted
+ scopes, and expiry time. On success, fetches user profile data from
+ the v2 userinfo endpoint to populate name, picture, and locale claims.
+ """
+ try:
+ async with (
+ contextlib.nullcontext(self._http_client)
+ if self._http_client is not None
+ else httpx.AsyncClient(timeout=self.timeout_seconds)
+ ) as client:
+ # Step 1: Verify token via tokeninfo endpoint.
+ # Returns aud (OAuth app ID), scope (space-separated), expires_in, sub, email.
+ response = await client.get(
+ "https://oauth2.googleapis.com/tokeninfo",
+ params={"access_token": token},
+ headers={"User-Agent": "FastMCP-Google-OAuth"},
+ )
+
+ if response.status_code != 200:
+ logger.debug(
+ "Google token verification failed: %d",
+ response.status_code,
+ )
+ return None
+
+ token_data = response.json()
+
+ # aud is the OAuth app ID (client_id / audience)
+ aud = token_data.get("aud")
+ if not aud:
+ logger.debug("Google tokeninfo missing 'aud' claim")
+ return None
+
+ # sub is required (unique Google user ID)
+ sub = token_data.get("sub")
+ if not sub:
+ logger.debug("Google tokeninfo missing 'sub' claim")
+ return None
+
+ # Parse scopes directly from the tokeninfo response (space-separated)
+ scope_str = token_data.get("scope", "")
+ token_scopes = scope_str.split() if scope_str else []
+
+ # Check required scopes
+ if self.required_scopes:
+ token_scopes_set = set(token_scopes)
+ required_scopes_set = set(self.required_scopes)
+ if not required_scopes_set.issubset(token_scopes_set):
+ logger.debug(
+ "Google token missing required scopes. Has %d, needs %d",
+ len(token_scopes_set),
+ len(required_scopes_set),
+ )
+ return None
+
+ # Compute expiry from expires_in (seconds until expiry)
+ expires_at: int | None = None
+ expires_in = token_data.get("expires_in")
+ if expires_in is not None:
+ with contextlib.suppress(ValueError, TypeError):
+ expires_at = int(time.time()) + int(expires_in)
+
+ # Step 2: Fetch user profile from v2 userinfo endpoint.
+ # tokeninfo provides auth data; userinfo provides name, picture, locale.
+ user_data: dict = {}
+ try:
+ userinfo_response = await client.get(
+ "https://www.googleapis.com/oauth2/v2/userinfo",
+ headers={
+ "Authorization": f"Bearer {token}",
+ "User-Agent": "FastMCP-Google-OAuth",
+ },
+ )
+ if userinfo_response.status_code == 200:
+ user_data = userinfo_response.json()
+ except Exception as e:
+ logger.debug("Failed to fetch Google user profile: %s", e)
+
+ access_token = AccessToken(
+ token=token,
+ client_id=sub,
+ scopes=token_scopes,
+ expires_at=expires_at,
+ claims={
+ "sub": sub,
+ "aud": aud,
+ "email": token_data.get("email") or user_data.get("email"),
+ "email_verified": token_data.get("email_verified")
+ or user_data.get("verified_email"),
+ "name": user_data.get("name"),
+ "picture": user_data.get("picture"),
+ "given_name": user_data.get("given_name"),
+ "family_name": user_data.get("family_name"),
+ "locale": user_data.get("locale"),
+ "google_user_data": user_data or None,
+ },
+ )
+ logger.debug("Google token verified successfully")
+ return access_token
+
+ except httpx.RequestError as e:
+ logger.debug("Failed to verify Google token: %s", e)
+ return None
+ except Exception as e:
+ logger.debug("Google token verification error: %s", e)
+ return None
+
+
+class GoogleProvider(OAuthProxy):
+ """Complete Google OAuth provider for FastMCP.
+
+ This provider makes it trivial to add Google OAuth protection to any
+ FastMCP server. Just provide your Google OAuth app credentials and
+ a base URL, and you're ready to go.
+
+ Features:
+ - Transparent OAuth proxy to Google
+ - Automatic token validation via Google's tokeninfo API
+ - User information extraction from Google APIs
+ - Minimal configuration required
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.google.provider import GoogleProvider
+
+ auth = GoogleProvider(
+ client_id="123456789.apps.googleusercontent.com",
+ client_secret="GOCSPX-abc123...",
+ base_url="https://my-server.com"
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ client_id: str,
+ client_secret: str | None = None,
+ base_url: AnyHttpUrl | str,
+ resource_base_url: AnyHttpUrl | str | None = None,
+ issuer_url: AnyHttpUrl | str | None = None,
+ redirect_path: str | None = None,
+ required_scopes: list[str] | None = None,
+ valid_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ allowed_client_redirect_uris: list[str] | None = None,
+ client_storage: AsyncKeyValue | None = None,
+ jwt_signing_key: str | bytes | None = None,
+ require_authorization_consent: bool | Literal["remember", "external"] = True,
+ consent_csp_policy: str | None = None,
+ forward_resource: bool = True,
+ extra_authorize_params: dict[str, str] | None = None,
+ http_client: httpx.AsyncClient | None = None,
+ enable_cimd: bool = True,
+ ):
+ """Initialize Google OAuth provider.
+
+ Args:
+ client_id: Google OAuth client ID (e.g., "123456789.apps.googleusercontent.com")
+ client_secret: Google OAuth client secret (e.g., "GOCSPX-abc123...").
+ Optional for PKCE public clients (e.g., native apps). When omitted,
+ jwt_signing_key must be provided.
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ resource_base_url: Optional public base URL for the protected resource metadata
+ and token audience. Defaults to ``base_url``.
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
+ redirect_path: Redirect path configured in Google OAuth app (defaults to "/auth/callback")
+ required_scopes: Required Google scopes (defaults to ["openid"]). Common scopes include:
+ - "openid" for OpenID Connect (default)
+ - "https://www.googleapis.com/auth/userinfo.email" for email access
+ - "https://www.googleapis.com/auth/userinfo.profile" for profile info
+ Google scope shorthands like "email" and "profile" are automatically
+ normalized to their full URI forms for token verification.
+ valid_scopes: All scopes that clients are allowed to request, advertised through
+ well-known endpoints. Defaults to required_scopes if not provided. Use this
+ when you want clients to be able to request additional scopes beyond the
+ required minimum. Shorthands are normalized to full URI forms.
+ timeout_seconds: HTTP request timeout for Google API calls (defaults to 10)
+ allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
+ If None (default), all URIs are allowed. If empty list, no URIs are allowed.
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
+ When True, users see a consent screen before being redirected to Google.
+ When False, authorization proceeds directly without user confirmation.
+ When "external", the built-in consent screen is skipped but no warning is
+ logged, indicating that consent is handled externally (e.g. by Google's own consent).
+ SECURITY WARNING: Only set to False for local development or testing environments.
+ extra_authorize_params: Additional parameters to forward to Google's authorization endpoint.
+ By default, GoogleProvider sets {"access_type": "offline", "prompt": "consent"} to ensure
+ refresh tokens are returned. You can override these defaults or add additional parameters.
+ Example: {"prompt": "select_account"} to let users choose their Google account.
+ http_client: Optional httpx.AsyncClient for connection pooling in token verification.
+ When provided, the client is reused across verify_token calls and the caller
+ is responsible for its lifecycle. When None (default), a fresh client is created per call.
+ enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
+ client IDs (default True). Set to False to disable.
+ """
+ # Parse scopes if provided as string
+ # Google requires at least one scope - openid is the minimal OIDC scope
+ required_scopes_final = (
+ parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
+ )
+
+ # Normalize valid_scopes if provided
+ parsed_valid_scopes = (
+ parse_scopes(valid_scopes) if valid_scopes is not None else None
+ )
+ valid_scopes_final = (
+ [_normalize_google_scope(s) for s in parsed_valid_scopes]
+ if parsed_valid_scopes is not None
+ else None
+ )
+
+ # Create Google token verifier
+ # Normalization of shorthand scopes (e.g. "email" -> full URI) happens
+ # inside GoogleTokenVerifier so required_scopes match what Google returns.
+ token_verifier = GoogleTokenVerifier(
+ required_scopes=required_scopes_final,
+ timeout_seconds=timeout_seconds,
+ http_client=http_client,
+ )
+
+ # Set Google-specific defaults for extra authorize params
+ # access_type=offline ensures refresh tokens are returned
+ # prompt=consent forces consent screen to get refresh token (Google only issues on first auth otherwise)
+ google_defaults = {
+ "access_type": "offline",
+ "prompt": "consent",
+ }
+ # User-provided params override defaults
+ if extra_authorize_params:
+ google_defaults.update(extra_authorize_params)
+ extra_authorize_params_final = google_defaults
+
+ # Initialize OAuth proxy with Google endpoints
+ super().__init__(
+ upstream_authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
+ upstream_token_endpoint="https://oauth2.googleapis.com/token",
+ upstream_client_id=client_id,
+ upstream_client_secret=client_secret,
+ token_verifier=token_verifier,
+ base_url=base_url,
+ resource_base_url=resource_base_url,
+ redirect_path=redirect_path,
+ issuer_url=issuer_url or base_url, # Default to base_url if not specified
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
+ client_storage=client_storage,
+ jwt_signing_key=jwt_signing_key,
+ require_authorization_consent=require_authorization_consent,
+ consent_csp_policy=consent_csp_policy,
+ forward_resource=forward_resource,
+ extra_authorize_params=extra_authorize_params_final,
+ valid_scopes=valid_scopes_final,
+ enable_cimd=enable_cimd,
+ )
+
+ logger.debug(
+ "Initialized Google OAuth provider for client %s with scopes: %s",
+ client_id,
+ required_scopes_final,
+ )
diff --git a/src/fastmcp/server/plugins/auth/keycloak/__init__.py b/src/fastmcp/server/plugins/auth/keycloak/__init__.py
new file mode 100644
index 000000000..e5a5c1593
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/keycloak/__init__.py
@@ -0,0 +1,5 @@
+"""Keycloak auth plugin."""
+
+from fastmcp.server.plugins.auth.keycloak.plugin import KeycloakAuth
+
+__all__ = ["KeycloakAuth"]
diff --git a/src/fastmcp/server/plugins/auth/keycloak/plugin.py b/src/fastmcp/server/plugins/auth/keycloak/plugin.py
new file mode 100644
index 000000000..1399fe082
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/keycloak/plugin.py
@@ -0,0 +1,45 @@
+"""Keycloak auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import AuthProvider, TokenVerifier
+from fastmcp.server.plugins.auth._base import AuthPlugin, PluginConfig
+from fastmcp.server.plugins.auth.keycloak.provider import KeycloakAuthProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class KeycloakAuthConfig(PluginConfig):
+ """Config model for the Keycloak auth plugin."""
+
+ realm_url: AnyHttpUrl | str | None = None
+ base_url: AnyHttpUrl | str | None = None
+ required_scopes: list[str] | str | None = None
+ audience: str | list[str] | None = None
+
+
+class KeycloakAuth(AuthPlugin[KeycloakAuthConfig]):
+ """Contribute a `KeycloakAuthProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[KeycloakAuthConfig]] = KeycloakAuthConfig
+
+ meta = PluginMeta(name="keycloak-auth")
+
+ def __init__(
+ self,
+ config: KeycloakAuthConfig | dict[str, Any] | None = None,
+ *,
+ token_verifier: TokenVerifier | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._token_verifier = token_verifier
+
+ def auth(self) -> AuthProvider | None:
+ self._require("realm_url", "base_url")
+ return KeycloakAuthProvider(
+ **self._kwargs("realm_url", "base_url", "required_scopes", "audience"),
+ token_verifier=self._token_verifier,
+ )
diff --git a/src/fastmcp/server/plugins/auth/keycloak/provider.py b/src/fastmcp/server/plugins/auth/keycloak/provider.py
new file mode 100644
index 000000000..b4c2eead2
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/keycloak/provider.py
@@ -0,0 +1,74 @@
+"""Keycloak authentication provider for FastMCP."""
+
+from __future__ import annotations
+
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class KeycloakAuthProvider(RemoteAuthProvider):
+ """Keycloak authentication provider using Dynamic Client Registration (DCR).
+
+ Requires Keycloak 26.6.0 or later, which includes the fix for DCR compatibility
+ with MCP clients (https://github.com/keycloak/keycloak/pull/45309).
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.keycloak.provider import KeycloakAuthProvider
+
+ auth = KeycloakAuthProvider(
+ realm_url="https://keycloak.example.com/realms/myrealm",
+ base_url="https://my-mcp-server.example.com",
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ realm_url: AnyHttpUrl | str,
+ base_url: AnyHttpUrl | str,
+ required_scopes: list[str] | str | None = None,
+ audience: str | list[str] | None = None,
+ token_verifier: TokenVerifier | None = None,
+ ):
+ """Initialize the Keycloak auth provider.
+
+ Args:
+ realm_url: Keycloak realm URL (e.g., "https://keycloak.example.com/realms/myrealm")
+ base_url: Public URL of this FastMCP server
+ required_scopes: Scopes to require on incoming tokens. Defaults to
+ ["openid"], which ensures the `sub` claim (user identifier) is
+ present in the access token. Override to require additional scopes.
+ audience: Optional audience(s) for JWT validation. Recommended for production.
+ token_verifier: Optional custom token verifier. Defaults to a JWTVerifier
+ configured for Keycloak's JWKS endpoint and issuer.
+ """
+ self.realm_url = str(realm_url).rstrip("/")
+ parsed_scopes = (
+ parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
+ )
+
+ if token_verifier is None:
+ token_verifier = JWTVerifier(
+ jwks_uri=f"{self.realm_url}/protocol/openid-connect/certs",
+ issuer=self.realm_url,
+ algorithm="RS256",
+ required_scopes=parsed_scopes,
+ audience=audience,
+ )
+
+ super().__init__(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl(self.realm_url)],
+ base_url=AnyHttpUrl(str(base_url).rstrip("/")),
+ )
diff --git a/src/fastmcp/server/plugins/auth/oci/__init__.py b/src/fastmcp/server/plugins/auth/oci/__init__.py
new file mode 100644
index 000000000..db49aadf3
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/oci/__init__.py
@@ -0,0 +1,5 @@
+"""OCI auth plugin."""
+
+from fastmcp.server.plugins.auth.oci.plugin import OCIAuth
+
+__all__ = ["OCIAuth"]
diff --git a/src/fastmcp/server/plugins/auth/oci/plugin.py b/src/fastmcp/server/plugins/auth/oci/plugin.py
new file mode 100644
index 000000000..e319e4aac
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/oci/plugin.py
@@ -0,0 +1,61 @@
+"""OCI auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+from key_value.aio.protocols import AsyncKeyValue
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import AuthProvider
+from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProxyConfig
+from fastmcp.server.plugins.auth.oci.provider import OCIProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class OCIAuthConfig(OAuthProxyConfig):
+ """Config model for the OCI auth plugin."""
+
+ config_url: AnyHttpUrl | str | None = None
+ client_id: str | None = None
+ client_secret: str | None = None
+ audience: str | None = None
+
+
+class OCIAuth(AuthPlugin[OCIAuthConfig]):
+ """Contribute an `OCIProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[OCIAuthConfig]] = OCIAuthConfig
+
+ meta = PluginMeta(name="oci-auth")
+
+ def __init__(
+ self,
+ config: OCIAuthConfig | dict[str, Any] | None = None,
+ *,
+ client_storage: AsyncKeyValue | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._client_storage = client_storage
+
+ def auth(self) -> AuthProvider | None:
+ self._require("config_url", "client_id", "client_secret", "base_url")
+ return OCIProvider(
+ **self._kwargs(
+ "config_url",
+ "client_id",
+ "client_secret",
+ "base_url",
+ "resource_base_url",
+ "audience",
+ "issuer_url",
+ "required_scopes",
+ "redirect_path",
+ "allowed_client_redirect_uris",
+ "jwt_signing_key",
+ "require_authorization_consent",
+ "consent_csp_policy",
+ "forward_resource",
+ ),
+ client_storage=self._client_storage,
+ )
diff --git a/src/fastmcp/server/plugins/auth/oci/provider.py b/src/fastmcp/server/plugins/auth/oci/provider.py
new file mode 100644
index 000000000..fe3841ad6
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/oci/provider.py
@@ -0,0 +1,180 @@
+"""OCI OIDC provider for FastMCP.
+
+The pull request for the provider is submitted to fastmcp.
+
+This module provides OIDC Implementation to integrate MCP servers with OCI.
+You only need OCI Identity Domain's discovery URL, client ID, client secret, and base URL.
+
+Post Authentication, you get OCI IAM domain access token. That is not authorized to invoke OCI control plane.
+You need to exchange the IAM domain access token for OCI UPST token to invoke OCI control plane APIs.
+The sample code below has get_oci_signer function that returns OCI TokenExchangeSigner object.
+You can use the signer object to create OCI service object.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.oci.provider import OCIProvider
+ from fastmcp.server.dependencies import get_access_token
+ from fastmcp.utilities.logging import get_logger
+
+ import os
+
+ import oci
+ from oci.auth.signers import TokenExchangeSigner
+
+ logger = get_logger(__name__)
+
+ # Load configuration from environment
+ config_url = os.environ.get("OCI_CONFIG_URL") # OCI IAM Domain OIDC discovery URL
+ client_id = os.environ.get("OCI_CLIENT_ID") # Client ID configured for the OCI IAM Domain Integrated Application
+ client_secret = os.environ.get("OCI_CLIENT_SECRET") # Client secret configured for the OCI IAM Domain Integrated Application
+ iam_guid = os.environ.get("OCI_IAM_GUID") # IAM GUID configured for the OCI IAM Domain
+
+ # Simple OCI OIDC protection
+ auth = OCIProvider(
+ config_url=config_url, # config URL is the OCI IAM Domain OIDC discovery URL
+ client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
+ client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application
+ required_scopes=["openid", "profile", "email"],
+ redirect_path="/auth/callback",
+ base_url="http://localhost:8000",
+ )
+
+ # NOTE: For production use, replace this with a thread-safe cache implementation
+ # such as threading.Lock-protected dict or a proper caching library
+ _global_token_cache = {} # In memory cache for OCI session token signer
+
+ def get_oci_signer() -> TokenExchangeSigner:
+
+ authntoken = get_access_token()
+ tokenID = authntoken.claims.get("jti")
+ token = authntoken.token
+
+ # Check if the signer exists for the token ID in memory cache
+ cached_signer = _global_token_cache.get(tokenID)
+ logger.debug(f"Global cached signer: {cached_signer}")
+ if cached_signer:
+ logger.debug(f"Using globally cached signer for token ID: {tokenID}")
+ return cached_signer
+
+ # If the signer is not yet created for the token then create new OCI signer object
+ logger.debug(f"Creating new signer for token ID: {tokenID}")
+ signer = TokenExchangeSigner(
+ jwt_or_func=token,
+ oci_domain_id=iam_guid.split(".")[0] if iam_guid else None, # This is same as IAM GUID configured for the OCI IAM Domain
+ client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
+ client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application
+ )
+ logger.debug(f"Signer {signer} created for token ID: {tokenID}")
+
+ #Cache the signer object in memory cache
+ _global_token_cache[tokenID] = signer
+ logger.debug(f"Signer cached for token ID: {tokenID}")
+
+ return signer
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+"""
+
+from typing import Literal
+
+from key_value.aio.protocols import AsyncKeyValue
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth.oidc_proxy import OIDCProxy
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class OCIProvider(OIDCProxy):
+ """An OCI IAM Domain provider implementation for FastMCP.
+
+ This provider is a complete OCI integration that's ready to use with
+ just the configuration URL, client ID, client secret, and base URL.
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.oci.provider import OCIProvider
+
+ import os
+
+ # Load configuration from environment
+ auth = OCIProvider(
+ config_url=os.environ.get("OCI_CONFIG_URL"), # OCI IAM Domain OIDC discovery URL
+ client_id=os.environ.get("OCI_CLIENT_ID"), # Client ID configured for the OCI IAM Domain Integrated Application
+ client_secret=os.environ.get("OCI_CLIENT_SECRET"), # Client secret configured for the OCI IAM Domain Integrated Application
+ base_url="http://localhost:8000",
+ required_scopes=["openid", "profile", "email"],
+ redirect_path="/auth/callback",
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ config_url: AnyHttpUrl | str,
+ client_id: str,
+ client_secret: str,
+ base_url: AnyHttpUrl | str,
+ resource_base_url: AnyHttpUrl | str | None = None,
+ audience: str | None = None,
+ issuer_url: AnyHttpUrl | str | None = None,
+ required_scopes: list[str] | None = None,
+ redirect_path: str | None = None,
+ allowed_client_redirect_uris: list[str] | None = None,
+ client_storage: AsyncKeyValue | None = None,
+ jwt_signing_key: str | bytes | None = None,
+ require_authorization_consent: bool | Literal["remember", "external"] = True,
+ consent_csp_policy: str | None = None,
+ forward_resource: bool = True,
+ ) -> None:
+ """Initialize OCI OIDC provider.
+
+ Args:
+ config_url: OCI OIDC Discovery URL
+ client_id: OCI IAM Domain Integrated Application client id
+ client_secret: OCI Integrated Application client secret
+ base_url: Public URL where OIDC endpoints will be accessible (includes any mount path)
+ resource_base_url: Optional public base URL for the protected resource metadata
+ and token audience. Defaults to ``base_url``.
+ audience: OCI API audience (optional)
+ issuer_url: Issuer URL for OCI IAM Domain metadata. This will override issuer URL from the discovery URL.
+ required_scopes: Required OCI scopes (defaults to ["openid"])
+ redirect_path: Redirect path configured in OCI IAM Domain Integrated Application. The default is "/auth/callback".
+ allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
+ """
+ # Parse scopes if provided as string
+ oci_required_scopes = (
+ parse_scopes(required_scopes) if required_scopes is not None else ["openid"]
+ )
+
+ super().__init__(
+ config_url=config_url,
+ client_id=client_id,
+ client_secret=client_secret,
+ audience=audience,
+ base_url=base_url,
+ resource_base_url=resource_base_url,
+ issuer_url=issuer_url,
+ redirect_path=redirect_path,
+ required_scopes=oci_required_scopes,
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
+ client_storage=client_storage,
+ jwt_signing_key=jwt_signing_key,
+ require_authorization_consent=require_authorization_consent,
+ consent_csp_policy=consent_csp_policy,
+ forward_resource=forward_resource,
+ )
+
+ logger.debug(
+ "Initialized OCI OAuth provider for client %s with scopes: %s",
+ client_id,
+ oci_required_scopes,
+ )
diff --git a/src/fastmcp/server/plugins/auth/propelauth/__init__.py b/src/fastmcp/server/plugins/auth/propelauth/__init__.py
new file mode 100644
index 000000000..dc674d55f
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/propelauth/__init__.py
@@ -0,0 +1,5 @@
+"""PropelAuth auth plugin."""
+
+from fastmcp.server.plugins.auth.propelauth.plugin import PropelAuth
+
+__all__ = ["PropelAuth"]
diff --git a/src/fastmcp/server/plugins/auth/propelauth/plugin.py b/src/fastmcp/server/plugins/auth/propelauth/plugin.py
new file mode 100644
index 000000000..f58d14da7
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/propelauth/plugin.py
@@ -0,0 +1,77 @@
+"""PropelAuth auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+import httpx
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import AuthProvider
+from fastmcp.server.plugins.auth._base import AuthPlugin, RemoteAuthConfig
+from fastmcp.server.plugins.auth.propelauth.provider import (
+ PropelAuthProvider,
+ PropelAuthTokenIntrospectionOverrides,
+)
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class PropelAuthConfig(RemoteAuthConfig):
+ """Config model for the PropelAuth auth plugin."""
+
+ auth_url: AnyHttpUrl | str | None = None
+ introspection_client_id: str | None = None
+ introspection_client_secret: str | None = None
+ resource: AnyHttpUrl | str | None = None
+ introspection_timeout_seconds: int | None = None
+ introspection_cache_ttl_seconds: int | None = None
+ introspection_max_cache_size: int | None = None
+
+
+class PropelAuth(AuthPlugin[PropelAuthConfig]):
+ """Contribute a `PropelAuthProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[PropelAuthConfig]] = PropelAuthConfig
+
+ meta = PluginMeta(name="propelauth-auth")
+
+ def __init__(
+ self,
+ config: PropelAuthConfig | dict[str, Any] | None = None,
+ *,
+ http_client: httpx.AsyncClient | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._http_client = http_client
+
+ def auth(self) -> AuthProvider | None:
+ self._require(
+ "auth_url",
+ "introspection_client_id",
+ "introspection_client_secret",
+ "base_url",
+ )
+ overrides: PropelAuthTokenIntrospectionOverrides = {}
+ if self.config.introspection_timeout_seconds is not None:
+ overrides["timeout_seconds"] = self.config.introspection_timeout_seconds
+ if self.config.introspection_cache_ttl_seconds is not None:
+ overrides["cache_ttl_seconds"] = self.config.introspection_cache_ttl_seconds
+ if self.config.introspection_max_cache_size is not None:
+ overrides["max_cache_size"] = self.config.introspection_max_cache_size
+ if self._http_client is not None:
+ overrides["http_client"] = self._http_client
+
+ return PropelAuthProvider(
+ **self._kwargs(
+ "auth_url",
+ "introspection_client_id",
+ "introspection_client_secret",
+ "base_url",
+ "required_scopes",
+ "scopes_supported",
+ "resource_name",
+ "resource_documentation",
+ "resource",
+ ),
+ token_introspection_overrides=overrides or None,
+ )
diff --git a/src/fastmcp/server/plugins/auth/propelauth/provider.py b/src/fastmcp/server/plugins/auth/propelauth/provider.py
new file mode 100644
index 000000000..b7073b4da
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/propelauth/provider.py
@@ -0,0 +1,234 @@
+"""PropelAuth authentication provider for FastMCP.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.propelauth.provider import PropelAuthProvider
+
+ auth = PropelAuthProvider(
+ auth_url="https://auth.yourdomain.com",
+ introspection_client_id="your-client-id",
+ introspection_client_secret="your-client-secret",
+ base_url="https://your-fastmcp-server.com",
+ required_scopes=["read:user_data"],
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+"""
+
+from __future__ import annotations
+
+from typing import TypedDict
+
+import httpx
+from pydantic import AnyHttpUrl, SecretStr
+from starlette.responses import JSONResponse
+from starlette.routing import Route
+
+from fastmcp.server.auth import AccessToken, RemoteAuthProvider
+from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class PropelAuthTokenIntrospectionOverrides(TypedDict, total=False):
+ timeout_seconds: int
+ cache_ttl_seconds: int | None
+ max_cache_size: int | None
+ http_client: httpx.AsyncClient | None
+
+
+class PropelAuthProvider(RemoteAuthProvider):
+ """PropelAuth resource server provider using OAuth 2.1 token introspection.
+
+ This provider validates access tokens via PropelAuth's introspection endpoint
+ and forwards authorization server metadata for OAuth discovery.
+
+ Setup:
+ 1. Enable MCP authentication in the PropelAuth Dashboard
+ 2. Configure scopes on the MCP page
+ 3. Select which redirect URIs to enable by picking which clients you support
+ 4. Generate introspection credentials (Client ID + Client Secret)
+
+ For detailed setup instructions, see:
+ https://docs.propelauth.com/mcp-authentication/overview
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.propelauth.provider import PropelAuthProvider
+
+ auth = PropelAuthProvider(
+ auth_url="https://auth.yourdomain.com",
+ introspection_client_id="your-client-id",
+ introspection_client_secret="your-client-secret",
+ base_url="https://your-fastmcp-server.com",
+ required_scopes=["read:user_data"],
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ auth_url: AnyHttpUrl | str,
+ introspection_client_id: str,
+ introspection_client_secret: str | SecretStr,
+ base_url: AnyHttpUrl | str,
+ required_scopes: list[str] | None = None,
+ scopes_supported: list[str] | None = None,
+ resource_name: str | None = None,
+ resource_documentation: AnyHttpUrl | None = None,
+ resource: AnyHttpUrl | str | None = None,
+ token_introspection_overrides: (
+ PropelAuthTokenIntrospectionOverrides | None
+ ) = None,
+ ):
+ """Initialize PropelAuth provider.
+
+ Args:
+ auth_url: Your PropelAuth Auth URL (from the Backend Integration page)
+ introspection_client_id: Introspection Client ID from the PropelAuth Dashboard
+ introspection_client_secret: Introspection Client Secret from the PropelAuth Dashboard
+ base_url: Public URL of this FastMCP server
+ required_scopes: Optional list of scopes that must be present in tokens
+ scopes_supported: Optional list of scopes to advertise in OAuth metadata.
+ If None, uses required_scopes. Use this when the scopes clients should
+ request differ from the scopes enforced on tokens.
+ resource_name: Optional name for the protected resource metadata.
+ resource_documentation: Optional documentation URL for the protected resource.
+ resource: Optional resource URI (RFC 8707) identifying this MCP server.
+ Use this when multiple MCP servers share the same PropelAuth
+ authorization server (e.g. ``resource="https://api.example.com/mcp"``),
+ so only tokens intended for this MCP server are accepted.
+ token_introspection_overrides: Optional overrides for the underlying
+ IntrospectionTokenVerifier (timeout, caching, http_client)
+ """
+ normalized_auth_url = str(auth_url).rstrip("/")
+ introspection_url = f"{normalized_auth_url}/oauth/2.1/introspect"
+ authorization_server_url = AnyHttpUrl(f"{normalized_auth_url}/oauth/2.1")
+
+ if resource is None:
+ self._resource = None
+ logger.debug(
+ "PropelAuthProvider: no resource configured, audience checking disabled"
+ )
+ else:
+ self._resource = str(resource)
+
+ token_verifier = self._create_token_verifier(
+ introspection_url=introspection_url,
+ client_id=introspection_client_id,
+ client_secret=introspection_client_secret,
+ required_scopes=required_scopes,
+ introspection_overrides=token_introspection_overrides,
+ )
+
+ self._normalized_auth_url = normalized_auth_url
+ super().__init__(
+ token_verifier=token_verifier,
+ authorization_servers=[authorization_server_url],
+ base_url=base_url,
+ scopes_supported=scopes_supported,
+ resource_name=resource_name,
+ resource_documentation=resource_documentation,
+ )
+
+ def get_routes(
+ self,
+ mcp_path: str | None = None,
+ ) -> list[Route]:
+ """Get routes for this provider.
+
+ Includes the standard routes from the RemoteAuthProvider (protected resource metadata routes (RFC 9728)),
+ and creates an authorization server metadata route that forwards to PropelAuth's route
+
+ Args:
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
+ This is used to advertise the resource URL in metadata.
+ """
+ routes = super().get_routes(mcp_path)
+
+ async def oauth_authorization_server_metadata(request):
+ """Forward PropelAuth OAuth authorization server metadata"""
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"{self._normalized_auth_url}/.well-known/oauth-authorization-server/oauth/2.1"
+ )
+ response.raise_for_status()
+ metadata = response.json()
+ return JSONResponse(metadata)
+ except Exception as e:
+ return JSONResponse(
+ {
+ "error": "server_error",
+ "error_description": f"Failed to fetch PropelAuth metadata: {e}",
+ },
+ status_code=500,
+ )
+
+ routes.append(
+ Route(
+ "/.well-known/oauth-authorization-server",
+ endpoint=oauth_authorization_server_metadata,
+ methods=["GET"],
+ )
+ )
+
+ return routes
+
+ async def verify_token(self, token: str) -> AccessToken | None:
+ """Verify token and check the ``aud`` claim against the configured resource."""
+ result = await super().verify_token(token)
+ if result is None or self._resource is None:
+ return result
+
+ aud = result.claims.get("aud")
+ if aud != self._resource:
+ logger.debug(
+ "PropelAuthProvider: token audience %r does not match resource %s",
+ aud,
+ self._resource,
+ )
+ return None
+
+ return result
+
+ def _create_token_verifier(
+ self,
+ introspection_url: str,
+ client_id: str,
+ client_secret: str | SecretStr,
+ required_scopes: list[str] | None,
+ introspection_overrides: PropelAuthTokenIntrospectionOverrides | None,
+ ) -> IntrospectionTokenVerifier:
+ # Being defensive here, check for only the fields we are expecting
+ safe_overrides: PropelAuthTokenIntrospectionOverrides = {}
+ if introspection_overrides is not None:
+ if "timeout_seconds" in introspection_overrides:
+ safe_overrides["timeout_seconds"] = introspection_overrides[
+ "timeout_seconds"
+ ]
+ if "cache_ttl_seconds" in introspection_overrides:
+ safe_overrides["cache_ttl_seconds"] = introspection_overrides[
+ "cache_ttl_seconds"
+ ]
+ if "max_cache_size" in introspection_overrides:
+ safe_overrides["max_cache_size"] = introspection_overrides[
+ "max_cache_size"
+ ]
+ if "http_client" in introspection_overrides:
+ safe_overrides["http_client"] = introspection_overrides["http_client"]
+
+ return IntrospectionTokenVerifier(
+ introspection_url=introspection_url,
+ client_id=client_id,
+ client_secret=client_secret,
+ required_scopes=required_scopes,
+ **safe_overrides,
+ )
diff --git a/src/fastmcp/server/plugins/auth/providers.py b/src/fastmcp/server/plugins/auth/providers.py
deleted file mode 100644
index d25edcfb4..000000000
--- a/src/fastmcp/server/plugins/auth/providers.py
+++ /dev/null
@@ -1,792 +0,0 @@
-"""First-party auth plugins.
-
-These plugins are thin, JSON-configurable wrappers around FastMCP's
-existing auth providers. Python-only dependencies such as HTTP clients,
-token verifiers, and client storage stay as constructor arguments.
-"""
-
-from __future__ import annotations
-
-from typing import Any, Generic, Literal, TypeVar
-
-import httpx
-from key_value.aio.protocols import AsyncKeyValue
-from pydantic import AnyHttpUrl, BaseModel, ConfigDict
-
-from fastmcp.server.auth import AuthProvider, TokenVerifier
-from fastmcp.server.plugins.base import Plugin, PluginMeta
-
-ConsentMode = bool | Literal["remember", "external"]
-Algorithm = Literal["RS256", "ES256"]
-ConfigT = TypeVar("ConfigT", bound=BaseModel)
-
-
-class _AuthPlugin(Plugin[ConfigT], Generic[ConfigT]):
- def _require(self, *fields: str) -> None:
- missing = [field for field in fields if getattr(self.config, field) is None]
- if missing:
- names = ", ".join(f"`{field}`" for field in missing)
- raise ValueError(f"{type(self).__name__} requires {names}.")
-
- def _require_one(self, *fields: str) -> None:
- if not any(getattr(self.config, field) is not None for field in fields):
- names = " or ".join(f"`{field}`" for field in fields)
- raise ValueError(f"{type(self).__name__} requires {names}.")
-
- def _kwargs(self, *fields: str) -> dict[str, Any]:
- return {
- field: getattr(self.config, field)
- for field in fields
- if getattr(self.config, field) is not None
- }
-
-
-class _PluginConfig(BaseModel):
- model_config = ConfigDict(extra="forbid")
-
-
-class _OAuthProxyConfig(_PluginConfig):
- base_url: AnyHttpUrl | str | None = None
- resource_base_url: AnyHttpUrl | str | None = None
- issuer_url: AnyHttpUrl | str | None = None
- redirect_path: str | None = None
- required_scopes: list[str] | None = None
- allowed_client_redirect_uris: list[str] | None = None
- jwt_signing_key: str | None = None
- require_authorization_consent: ConsentMode = True
- consent_csp_policy: str | None = None
- forward_resource: bool = True
-
-
-class _OAuthProviderConfig(_OAuthProxyConfig):
- client_id: str | None = None
- client_secret: str | None = None
- timeout_seconds: int = 10
- enable_cimd: bool = True
-
-
-class _RemoteAuthConfig(_PluginConfig):
- base_url: AnyHttpUrl | str | None = None
- required_scopes: list[str] | None = None
- scopes_supported: list[str] | None = None
- resource_name: str | None = None
- resource_documentation: AnyHttpUrl | None = None
-
-
-class Auth0AuthConfig(_OAuthProxyConfig):
- """Config model for the Auth0 auth plugin."""
-
- config_url: AnyHttpUrl | str | None = None
- client_id: str | None = None
- client_secret: str | None = None
- audience: str | None = None
-
-
-class Auth0Auth(_AuthPlugin[Auth0AuthConfig]):
- """Contribute an `Auth0Provider` as the server's auth provider."""
-
- meta = PluginMeta(name="auth0-auth")
-
- def __init__(
- self,
- config: Auth0AuthConfig | dict[str, Any] | None = None,
- *,
- client_storage: AsyncKeyValue | None = None,
- ) -> None:
- super().__init__(config)
- self._client_storage = client_storage
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.auth0 import Auth0Provider
-
- self._require(
- "config_url", "client_id", "client_secret", "audience", "base_url"
- )
- return Auth0Provider(
- **self._kwargs(
- "config_url",
- "client_id",
- "client_secret",
- "audience",
- "base_url",
- "resource_base_url",
- "issuer_url",
- "required_scopes",
- "redirect_path",
- "allowed_client_redirect_uris",
- "jwt_signing_key",
- "require_authorization_consent",
- "consent_csp_policy",
- "forward_resource",
- ),
- client_storage=self._client_storage,
- )
-
-
-class AuthKitAuthConfig(_RemoteAuthConfig):
- """Config model for the WorkOS AuthKit auth plugin."""
-
- authkit_domain: AnyHttpUrl | str | None = None
- resource_base_url: AnyHttpUrl | str | None = None
-
-
-class AuthKitAuth(_AuthPlugin[AuthKitAuthConfig]):
- """Contribute an `AuthKitProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="authkit-auth")
-
- def __init__(
- self,
- config: AuthKitAuthConfig | dict[str, Any] | None = None,
- *,
- token_verifier: TokenVerifier | None = None,
- ) -> None:
- super().__init__(config)
- self._token_verifier = token_verifier
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.workos import AuthKitProvider
-
- self._require("authkit_domain", "base_url")
- return AuthKitProvider(
- **self._kwargs(
- "authkit_domain",
- "base_url",
- "resource_base_url",
- "required_scopes",
- "scopes_supported",
- "resource_name",
- "resource_documentation",
- ),
- token_verifier=self._token_verifier,
- )
-
-
-class AWSCognitoAuthConfig(_OAuthProxyConfig):
- """Config model for the AWS Cognito auth plugin."""
-
- user_pool_id: str | None = None
- client_id: str | None = None
- client_secret: str | None = None
- aws_region: str = "eu-central-1"
- redirect_path: str | None = "/auth/callback"
-
-
-class AWSCognitoAuth(_AuthPlugin[AWSCognitoAuthConfig]):
- """Contribute an `AWSCognitoProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="aws-cognito-auth")
-
- def __init__(
- self,
- config: AWSCognitoAuthConfig | dict[str, Any] | None = None,
- *,
- client_storage: AsyncKeyValue | None = None,
- ) -> None:
- super().__init__(config)
- self._client_storage = client_storage
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.aws import AWSCognitoProvider
-
- self._require("user_pool_id", "client_id", "client_secret", "base_url")
- return AWSCognitoProvider(
- **self._kwargs(
- "user_pool_id",
- "client_id",
- "client_secret",
- "base_url",
- "resource_base_url",
- "aws_region",
- "issuer_url",
- "redirect_path",
- "required_scopes",
- "allowed_client_redirect_uris",
- "jwt_signing_key",
- "require_authorization_consent",
- "consent_csp_policy",
- "forward_resource",
- ),
- client_storage=self._client_storage,
- )
-
-
-class AzureAuthConfig(_OAuthProviderConfig):
- """Config model for the Azure auth plugin."""
-
- tenant_id: str | None = None
- required_scopes: list[str] | None = None
- identifier_uri: str | None = None
- additional_authorize_scopes: list[str] | None = None
- base_authority: str = "login.microsoftonline.com"
-
-
-class AzureAuth(_AuthPlugin[AzureAuthConfig]):
- """Contribute an `AzureProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="azure-auth")
-
- def __init__(
- self,
- config: AzureAuthConfig | dict[str, Any] | None = None,
- *,
- client_storage: AsyncKeyValue | None = None,
- http_client: httpx.AsyncClient | None = None,
- ) -> None:
- super().__init__(config)
- self._client_storage = client_storage
- self._http_client = http_client
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.azure import AzureProvider
-
- self._require("client_id", "tenant_id", "required_scopes", "base_url")
- self._require_one("client_secret", "jwt_signing_key")
- return AzureProvider(
- **self._kwargs(
- "client_id",
- "client_secret",
- "tenant_id",
- "required_scopes",
- "base_url",
- "resource_base_url",
- "identifier_uri",
- "issuer_url",
- "redirect_path",
- "additional_authorize_scopes",
- "allowed_client_redirect_uris",
- "jwt_signing_key",
- "require_authorization_consent",
- "consent_csp_policy",
- "forward_resource",
- "base_authority",
- "enable_cimd",
- ),
- client_storage=self._client_storage,
- http_client=self._http_client,
- )
-
-
-class ClerkAuthConfig(_OAuthProviderConfig):
- """Config model for the Clerk auth plugin."""
-
- domain: str | None = None
- valid_scopes: list[str] | None = None
- extra_authorize_params: dict[str, str] | None = None
-
-
-class ClerkAuth(_AuthPlugin[ClerkAuthConfig]):
- """Contribute a `ClerkProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="clerk-auth")
-
- def __init__(
- self,
- config: ClerkAuthConfig | dict[str, Any] | None = None,
- *,
- client_storage: AsyncKeyValue | None = None,
- http_client: httpx.AsyncClient | None = None,
- ) -> None:
- super().__init__(config)
- self._client_storage = client_storage
- self._http_client = http_client
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.clerk import ClerkProvider
-
- self._require("domain", "client_id", "base_url")
- self._require_one("client_secret", "jwt_signing_key")
- return ClerkProvider(
- **self._kwargs(
- "domain",
- "client_id",
- "client_secret",
- "base_url",
- "resource_base_url",
- "issuer_url",
- "redirect_path",
- "required_scopes",
- "valid_scopes",
- "timeout_seconds",
- "allowed_client_redirect_uris",
- "jwt_signing_key",
- "require_authorization_consent",
- "consent_csp_policy",
- "forward_resource",
- "extra_authorize_params",
- "enable_cimd",
- ),
- client_storage=self._client_storage,
- http_client=self._http_client,
- )
-
-
-class DescopeAuthConfig(_RemoteAuthConfig):
- """Config model for the Descope auth plugin."""
-
- config_url: AnyHttpUrl | str | None = None
- project_id: str | None = None
- descope_base_url: AnyHttpUrl | str | None = None
-
-
-class DescopeAuth(_AuthPlugin[DescopeAuthConfig]):
- """Contribute a `DescopeProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="descope-auth")
-
- def __init__(
- self,
- config: DescopeAuthConfig | dict[str, Any] | None = None,
- *,
- token_verifier: TokenVerifier | None = None,
- ) -> None:
- super().__init__(config)
- self._token_verifier = token_verifier
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.descope import DescopeProvider
-
- self._require("base_url")
- if self.config.config_url is None:
- self._require("project_id", "descope_base_url")
- return DescopeProvider(
- **self._kwargs(
- "base_url",
- "config_url",
- "project_id",
- "descope_base_url",
- "required_scopes",
- "scopes_supported",
- "resource_name",
- "resource_documentation",
- ),
- token_verifier=self._token_verifier,
- )
-
-
-class DiscordAuthConfig(_OAuthProviderConfig):
- """Config model for the Discord auth plugin."""
-
-
-class DiscordAuth(_AuthPlugin[DiscordAuthConfig]):
- """Contribute a `DiscordProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="discord-auth")
-
- def __init__(
- self,
- config: DiscordAuthConfig | dict[str, Any] | None = None,
- *,
- client_storage: AsyncKeyValue | None = None,
- http_client: httpx.AsyncClient | None = None,
- ) -> None:
- super().__init__(config)
- self._client_storage = client_storage
- self._http_client = http_client
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.discord import DiscordProvider
-
- self._require("client_id", "client_secret", "base_url")
- return DiscordProvider(
- **self._kwargs(
- "client_id",
- "client_secret",
- "base_url",
- "resource_base_url",
- "issuer_url",
- "redirect_path",
- "required_scopes",
- "timeout_seconds",
- "allowed_client_redirect_uris",
- "jwt_signing_key",
- "require_authorization_consent",
- "consent_csp_policy",
- "forward_resource",
- "enable_cimd",
- ),
- client_storage=self._client_storage,
- http_client=self._http_client,
- )
-
-
-class GitHubAuthConfig(_OAuthProviderConfig):
- """Config model for the GitHub auth plugin."""
-
- cache_ttl_seconds: int | None = None
- max_cache_size: int | None = None
-
-
-class GitHubAuth(_AuthPlugin[GitHubAuthConfig]):
- """Contribute a `GitHubProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="github-auth")
-
- def __init__(
- self,
- config: GitHubAuthConfig | dict[str, Any] | None = None,
- *,
- client_storage: AsyncKeyValue | None = None,
- http_client: httpx.AsyncClient | None = None,
- ) -> None:
- super().__init__(config)
- self._client_storage = client_storage
- self._http_client = http_client
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.github import GitHubProvider
-
- self._require("client_id", "client_secret", "base_url")
- return GitHubProvider(
- **self._kwargs(
- "client_id",
- "client_secret",
- "base_url",
- "resource_base_url",
- "issuer_url",
- "redirect_path",
- "required_scopes",
- "timeout_seconds",
- "cache_ttl_seconds",
- "max_cache_size",
- "allowed_client_redirect_uris",
- "jwt_signing_key",
- "require_authorization_consent",
- "consent_csp_policy",
- "forward_resource",
- "enable_cimd",
- ),
- client_storage=self._client_storage,
- http_client=self._http_client,
- )
-
-
-class GoogleAuthConfig(_OAuthProviderConfig):
- """Config model for the Google auth plugin."""
-
- valid_scopes: list[str] | None = None
- extra_authorize_params: dict[str, str] | None = None
-
-
-class GoogleAuth(_AuthPlugin[GoogleAuthConfig]):
- """Contribute a `GoogleProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="google-auth")
-
- def __init__(
- self,
- config: GoogleAuthConfig | dict[str, Any] | None = None,
- *,
- client_storage: AsyncKeyValue | None = None,
- http_client: httpx.AsyncClient | None = None,
- ) -> None:
- super().__init__(config)
- self._client_storage = client_storage
- self._http_client = http_client
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.google import GoogleProvider
-
- self._require("client_id", "base_url")
- self._require_one("client_secret", "jwt_signing_key")
- return GoogleProvider(
- **self._kwargs(
- "client_id",
- "client_secret",
- "base_url",
- "resource_base_url",
- "issuer_url",
- "redirect_path",
- "required_scopes",
- "valid_scopes",
- "timeout_seconds",
- "allowed_client_redirect_uris",
- "jwt_signing_key",
- "require_authorization_consent",
- "consent_csp_policy",
- "forward_resource",
- "extra_authorize_params",
- "enable_cimd",
- ),
- client_storage=self._client_storage,
- http_client=self._http_client,
- )
-
-
-class KeycloakAuthConfig(_PluginConfig):
- """Config model for the Keycloak auth plugin."""
-
- realm_url: AnyHttpUrl | str | None = None
- base_url: AnyHttpUrl | str | None = None
- required_scopes: list[str] | str | None = None
- audience: str | list[str] | None = None
-
-
-class KeycloakAuth(_AuthPlugin[KeycloakAuthConfig]):
- """Contribute a `KeycloakAuthProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="keycloak-auth")
-
- def __init__(
- self,
- config: KeycloakAuthConfig | dict[str, Any] | None = None,
- *,
- token_verifier: TokenVerifier | None = None,
- ) -> None:
- super().__init__(config)
- self._token_verifier = token_verifier
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
-
- self._require("realm_url", "base_url")
- return KeycloakAuthProvider(
- **self._kwargs("realm_url", "base_url", "required_scopes", "audience"),
- token_verifier=self._token_verifier,
- )
-
-
-class OCIAuthConfig(_OAuthProxyConfig):
- """Config model for the OCI auth plugin."""
-
- config_url: AnyHttpUrl | str | None = None
- client_id: str | None = None
- client_secret: str | None = None
- audience: str | None = None
-
-
-class OCIAuth(_AuthPlugin[OCIAuthConfig]):
- """Contribute an `OCIProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="oci-auth")
-
- def __init__(
- self,
- config: OCIAuthConfig | dict[str, Any] | None = None,
- *,
- client_storage: AsyncKeyValue | None = None,
- ) -> None:
- super().__init__(config)
- self._client_storage = client_storage
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.oci import OCIProvider
-
- self._require("config_url", "client_id", "client_secret", "base_url")
- return OCIProvider(
- **self._kwargs(
- "config_url",
- "client_id",
- "client_secret",
- "base_url",
- "resource_base_url",
- "audience",
- "issuer_url",
- "required_scopes",
- "redirect_path",
- "allowed_client_redirect_uris",
- "jwt_signing_key",
- "require_authorization_consent",
- "consent_csp_policy",
- "forward_resource",
- ),
- client_storage=self._client_storage,
- )
-
-
-class PropelAuthConfig(_RemoteAuthConfig):
- """Config model for the PropelAuth auth plugin."""
-
- auth_url: AnyHttpUrl | str | None = None
- introspection_client_id: str | None = None
- introspection_client_secret: str | None = None
- resource: AnyHttpUrl | str | None = None
- introspection_timeout_seconds: int | None = None
- introspection_cache_ttl_seconds: int | None = None
- introspection_max_cache_size: int | None = None
-
-
-class PropelAuth(_AuthPlugin[PropelAuthConfig]):
- """Contribute a `PropelAuthProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="propelauth-auth")
-
- def __init__(
- self,
- config: PropelAuthConfig | dict[str, Any] | None = None,
- *,
- http_client: httpx.AsyncClient | None = None,
- ) -> None:
- super().__init__(config)
- self._http_client = http_client
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.propelauth import (
- PropelAuthProvider,
- PropelAuthTokenIntrospectionOverrides,
- )
-
- self._require(
- "auth_url",
- "introspection_client_id",
- "introspection_client_secret",
- "base_url",
- )
- overrides: PropelAuthTokenIntrospectionOverrides = {}
- if self.config.introspection_timeout_seconds is not None:
- overrides["timeout_seconds"] = self.config.introspection_timeout_seconds
- if self.config.introspection_cache_ttl_seconds is not None:
- overrides["cache_ttl_seconds"] = self.config.introspection_cache_ttl_seconds
- if self.config.introspection_max_cache_size is not None:
- overrides["max_cache_size"] = self.config.introspection_max_cache_size
- if self._http_client is not None:
- overrides["http_client"] = self._http_client
-
- return PropelAuthProvider(
- **self._kwargs(
- "auth_url",
- "introspection_client_id",
- "introspection_client_secret",
- "base_url",
- "required_scopes",
- "scopes_supported",
- "resource_name",
- "resource_documentation",
- "resource",
- ),
- token_introspection_overrides=overrides or None,
- )
-
-
-class ScalekitAuthConfig(_RemoteAuthConfig):
- """Config model for the Scalekit auth plugin."""
-
- environment_url: AnyHttpUrl | str | None = None
- resource_id: str | None = None
- mcp_url: AnyHttpUrl | str | None = None
- client_id: str | None = None
-
-
-class ScalekitAuth(_AuthPlugin[ScalekitAuthConfig]):
- """Contribute a `ScalekitProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="scalekit-auth")
-
- def __init__(
- self,
- config: ScalekitAuthConfig | dict[str, Any] | None = None,
- *,
- token_verifier: TokenVerifier | None = None,
- ) -> None:
- super().__init__(config)
- self._token_verifier = token_verifier
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.scalekit import ScalekitProvider
-
- self._require("environment_url", "resource_id")
- self._require_one("base_url", "mcp_url")
- return ScalekitProvider(
- **self._kwargs(
- "environment_url",
- "resource_id",
- "base_url",
- "mcp_url",
- "client_id",
- "required_scopes",
- "scopes_supported",
- "resource_name",
- "resource_documentation",
- ),
- token_verifier=self._token_verifier,
- )
-
-
-class SupabaseAuthConfig(_RemoteAuthConfig):
- """Config model for the Supabase auth plugin."""
-
- project_url: AnyHttpUrl | str | None = None
- auth_route: str = "/auth/v1"
- algorithm: Algorithm = "ES256"
-
-
-class SupabaseAuth(_AuthPlugin[SupabaseAuthConfig]):
- """Contribute a `SupabaseProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="supabase-auth")
-
- def __init__(
- self,
- config: SupabaseAuthConfig | dict[str, Any] | None = None,
- *,
- token_verifier: TokenVerifier | None = None,
- ) -> None:
- super().__init__(config)
- self._token_verifier = token_verifier
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.supabase import SupabaseProvider
-
- self._require("project_url", "base_url")
- return SupabaseProvider(
- **self._kwargs(
- "project_url",
- "base_url",
- "auth_route",
- "algorithm",
- "required_scopes",
- "scopes_supported",
- "resource_name",
- "resource_documentation",
- ),
- token_verifier=self._token_verifier,
- )
-
-
-class WorkOSAuthConfig(_OAuthProviderConfig):
- """Config model for the WorkOS auth plugin."""
-
- authkit_domain: str | None = None
-
-
-class WorkOSAuth(_AuthPlugin[WorkOSAuthConfig]):
- """Contribute a `WorkOSProvider` as the server's auth provider."""
-
- meta = PluginMeta(name="workos-auth")
-
- def __init__(
- self,
- config: WorkOSAuthConfig | dict[str, Any] | None = None,
- *,
- client_storage: AsyncKeyValue | None = None,
- http_client: httpx.AsyncClient | None = None,
- ) -> None:
- super().__init__(config)
- self._client_storage = client_storage
- self._http_client = http_client
-
- def auth(self) -> AuthProvider | None:
- from fastmcp.server.auth.providers.workos import WorkOSProvider
-
- self._require("client_id", "client_secret", "authkit_domain", "base_url")
- return WorkOSProvider(
- **self._kwargs(
- "client_id",
- "client_secret",
- "authkit_domain",
- "base_url",
- "resource_base_url",
- "issuer_url",
- "redirect_path",
- "required_scopes",
- "timeout_seconds",
- "allowed_client_redirect_uris",
- "jwt_signing_key",
- "require_authorization_consent",
- "consent_csp_policy",
- "forward_resource",
- "enable_cimd",
- ),
- client_storage=self._client_storage,
- http_client=self._http_client,
- )
diff --git a/src/fastmcp/server/plugins/auth/scalekit/__init__.py b/src/fastmcp/server/plugins/auth/scalekit/__init__.py
new file mode 100644
index 000000000..bad3fc3b1
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/scalekit/__init__.py
@@ -0,0 +1,5 @@
+"""Scalekit auth plugin."""
+
+from fastmcp.server.plugins.auth.scalekit.plugin import ScalekitAuth
+
+__all__ = ["ScalekitAuth"]
diff --git a/src/fastmcp/server/plugins/auth/scalekit/plugin.py b/src/fastmcp/server/plugins/auth/scalekit/plugin.py
new file mode 100644
index 000000000..ab4ac2553
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/scalekit/plugin.py
@@ -0,0 +1,56 @@
+"""Scalekit auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import AuthProvider, TokenVerifier
+from fastmcp.server.plugins.auth._base import AuthPlugin, RemoteAuthConfig
+from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class ScalekitAuthConfig(RemoteAuthConfig):
+ """Config model for the Scalekit auth plugin."""
+
+ environment_url: AnyHttpUrl | str | None = None
+ resource_id: str | None = None
+ mcp_url: AnyHttpUrl | str | None = None
+ client_id: str | None = None
+
+
+class ScalekitAuth(AuthPlugin[ScalekitAuthConfig]):
+ """Contribute a `ScalekitProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[ScalekitAuthConfig]] = ScalekitAuthConfig
+
+ meta = PluginMeta(name="scalekit-auth")
+
+ def __init__(
+ self,
+ config: ScalekitAuthConfig | dict[str, Any] | None = None,
+ *,
+ token_verifier: TokenVerifier | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._token_verifier = token_verifier
+
+ def auth(self) -> AuthProvider | None:
+ self._require("environment_url", "resource_id")
+ self._require_one("base_url", "mcp_url")
+ return ScalekitProvider(
+ **self._kwargs(
+ "environment_url",
+ "resource_id",
+ "base_url",
+ "mcp_url",
+ "client_id",
+ "required_scopes",
+ "scopes_supported",
+ "resource_name",
+ "resource_documentation",
+ ),
+ token_verifier=self._token_verifier,
+ )
diff --git a/src/fastmcp/server/plugins/auth/scalekit/provider.py b/src/fastmcp/server/plugins/auth/scalekit/provider.py
new file mode 100644
index 000000000..c64abada4
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/scalekit/provider.py
@@ -0,0 +1,212 @@
+"""Scalekit authentication provider for FastMCP.
+
+This module provides ScalekitProvider - a complete authentication solution that integrates
+with Scalekit's OAuth 2.1 and OpenID Connect services, supporting Resource Server
+authentication for seamless MCP client authentication.
+"""
+
+from __future__ import annotations
+
+import httpx
+from pydantic import AnyHttpUrl
+from starlette.responses import JSONResponse
+from starlette.routing import Route
+
+from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class ScalekitProvider(RemoteAuthProvider):
+ """Scalekit resource server provider for OAuth 2.1 authentication.
+
+ This provider implements Scalekit integration using resource server pattern.
+ FastMCP acts as a protected resource server that validates access tokens issued
+ by Scalekit's authorization server.
+
+ IMPORTANT SETUP REQUIREMENTS:
+
+ 1. Create an MCP Server in Scalekit Dashboard:
+ - Go to your [Scalekit Dashboard](https://app.scalekit.com/)
+ - Navigate to MCP Servers section
+ - Register a new MCP Server with appropriate scopes
+ - Ensure the Resource Identifier matches exactly what you configure as MCP URL
+ - Note the Resource ID
+
+ 2. Environment Configuration:
+ - Set SCALEKIT_ENVIRONMENT_URL (e.g., https://your-env.scalekit.com)
+ - Set SCALEKIT_RESOURCE_ID from your created resource
+ - Set BASE_URL to your FastMCP server's public URL
+
+ For detailed setup instructions, see:
+ https://docs.scalekit.com/mcp/overview/
+
+ Example:
+ ```python
+ from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
+
+ # Create Scalekit resource server provider
+ scalekit_auth = ScalekitProvider(
+ environment_url="https://your-env.scalekit.com",
+ resource_id="sk_resource_...",
+ base_url="https://your-fastmcp-server.com",
+ )
+
+ # Use with FastMCP
+ mcp = FastMCP("My App", auth=scalekit_auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ environment_url: AnyHttpUrl | str,
+ resource_id: str,
+ base_url: AnyHttpUrl | str | None = None,
+ mcp_url: AnyHttpUrl | str | None = None,
+ client_id: str | None = None,
+ required_scopes: list[str] | None = None,
+ scopes_supported: list[str] | None = None,
+ resource_name: str | None = None,
+ resource_documentation: AnyHttpUrl | None = None,
+ token_verifier: TokenVerifier | None = None,
+ ):
+ """Initialize Scalekit resource server provider.
+
+ Args:
+ environment_url: Your Scalekit environment URL (e.g., "https://your-env.scalekit.com")
+ resource_id: Your Scalekit resource ID
+ base_url: Public URL of this FastMCP server (or use mcp_url for backwards compatibility)
+ mcp_url: Deprecated alias for base_url. Will be removed in a future release.
+ client_id: Deprecated parameter, no longer required. Will be removed in a future release.
+ required_scopes: Optional list of scopes that must be present in tokens
+ scopes_supported: Optional list of scopes to advertise in OAuth metadata.
+ If None, uses required_scopes. Use this when the scopes clients should
+ request differ from the scopes enforced on tokens.
+ resource_name: Optional name for the protected resource metadata.
+ resource_documentation: Optional documentation URL for the protected resource.
+ token_verifier: Optional token verifier. If None, creates JWT verifier for Scalekit
+ """
+ # Resolve base_url from mcp_url if needed (backwards compatibility)
+ resolved_base_url = base_url or mcp_url
+ if not resolved_base_url:
+ raise ValueError("Either base_url or mcp_url must be provided")
+
+ if mcp_url is not None:
+ logger.warning(
+ "ScalekitProvider parameter 'mcp_url' is deprecated and will be removed in a future release. "
+ "Rename it to 'base_url'."
+ )
+
+ if client_id is not None:
+ logger.warning(
+ "ScalekitProvider no longer requires 'client_id'. The parameter is accepted only for backward "
+ "compatibility and will be removed in a future release."
+ )
+
+ self.environment_url = str(environment_url).rstrip("/")
+ self.resource_id = resource_id
+ parsed_scopes = (
+ parse_scopes(required_scopes) if required_scopes is not None else []
+ )
+ self.required_scopes = parsed_scopes
+ base_url_value = str(resolved_base_url)
+
+ logger.debug(
+ "Initializing ScalekitProvider: environment_url=%s resource_id=%s base_url=%s required_scopes=%s",
+ self.environment_url,
+ self.resource_id,
+ base_url_value,
+ self.required_scopes,
+ )
+
+ # Create default JWT verifier if none provided
+ if token_verifier is None:
+ logger.debug(
+ "Creating default JWTVerifier for Scalekit: jwks_uri=%s issuer=%s required_scopes=%s",
+ f"{self.environment_url}/keys",
+ self.environment_url,
+ self.required_scopes,
+ )
+ token_verifier = JWTVerifier(
+ 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:
+ logger.debug("Using custom token verifier for ScalekitProvider")
+
+ # Initialize RemoteAuthProvider with Scalekit as the authorization server
+ super().__init__(
+ token_verifier=token_verifier,
+ authorization_servers=[
+ AnyHttpUrl(f"{self.environment_url}/resources/{self.resource_id}")
+ ],
+ base_url=base_url_value,
+ scopes_supported=scopes_supported,
+ resource_name=resource_name,
+ resource_documentation=resource_documentation,
+ )
+
+ def get_routes(
+ self,
+ mcp_path: str | None = None,
+ ) -> list[Route]:
+ """Get OAuth routes including Scalekit authorization server metadata forwarding.
+
+ This returns the standard protected resource routes plus an authorization server
+ metadata endpoint that forwards Scalekit's OAuth metadata to clients.
+
+ Args:
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
+ This is used to advertise the resource URL in metadata.
+ """
+ # Get the standard protected resource routes from RemoteAuthProvider
+ routes = super().get_routes(mcp_path)
+ logger.debug(
+ "Preparing Scalekit metadata routes: mcp_path=%s resource_id=%s",
+ mcp_path,
+ self.resource_id,
+ )
+
+ async def oauth_authorization_server_metadata(request):
+ """Forward Scalekit OAuth authorization server metadata with FastMCP customizations."""
+ try:
+ metadata_url = f"{self.environment_url}/.well-known/oauth-authorization-server/resources/{self.resource_id}"
+ logger.debug(
+ "Fetching Scalekit OAuth metadata: metadata_url=%s", metadata_url
+ )
+ async with httpx.AsyncClient() as client:
+ response = await client.get(metadata_url)
+ response.raise_for_status()
+ metadata = response.json()
+ logger.debug(
+ "Scalekit metadata fetched successfully: metadata_keys=%s",
+ list(metadata.keys()),
+ )
+ return JSONResponse(metadata)
+ except Exception as e:
+ logger.error(f"Failed to fetch Scalekit metadata: {e}")
+ return JSONResponse(
+ {
+ "error": "server_error",
+ "error_description": f"Failed to fetch Scalekit metadata: {e}",
+ },
+ status_code=500,
+ )
+
+ # Add Scalekit authorization server metadata forwarding
+ routes.append(
+ Route(
+ "/.well-known/oauth-authorization-server",
+ endpoint=oauth_authorization_server_metadata,
+ methods=["GET"],
+ )
+ )
+
+ return routes
diff --git a/src/fastmcp/server/plugins/auth/supabase.py b/src/fastmcp/server/plugins/auth/supabase.py
deleted file mode 100644
index debbcbac0..000000000
--- a/src/fastmcp/server/plugins/auth/supabase.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""Supabase auth plugin."""
-
-from fastmcp.server.plugins.auth.providers import SupabaseAuth, SupabaseAuthConfig
-
-__all__ = ["SupabaseAuth", "SupabaseAuthConfig"]
diff --git a/src/fastmcp/server/plugins/auth/supabase/__init__.py b/src/fastmcp/server/plugins/auth/supabase/__init__.py
new file mode 100644
index 000000000..e2367f17c
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/supabase/__init__.py
@@ -0,0 +1,5 @@
+"""Supabase auth plugin."""
+
+from fastmcp.server.plugins.auth.supabase.plugin import SupabaseAuth
+
+__all__ = ["SupabaseAuth"]
diff --git a/src/fastmcp/server/plugins/auth/supabase/plugin.py b/src/fastmcp/server/plugins/auth/supabase/plugin.py
new file mode 100644
index 000000000..8ecf979e9
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/supabase/plugin.py
@@ -0,0 +1,53 @@
+"""Supabase auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import AuthProvider, TokenVerifier
+from fastmcp.server.plugins.auth._base import Algorithm, AuthPlugin, RemoteAuthConfig
+from fastmcp.server.plugins.auth.supabase.provider import SupabaseProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class SupabaseAuthConfig(RemoteAuthConfig):
+ """Config model for the Supabase auth plugin."""
+
+ project_url: AnyHttpUrl | str | None = None
+ auth_route: str = "/auth/v1"
+ algorithm: Algorithm = "ES256"
+
+
+class SupabaseAuth(AuthPlugin[SupabaseAuthConfig]):
+ """Contribute a `SupabaseProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[SupabaseAuthConfig]] = SupabaseAuthConfig
+
+ meta = PluginMeta(name="supabase-auth")
+
+ def __init__(
+ self,
+ config: SupabaseAuthConfig | dict[str, Any] | None = None,
+ *,
+ token_verifier: TokenVerifier | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._token_verifier = token_verifier
+
+ def auth(self) -> AuthProvider | None:
+ self._require("project_url", "base_url")
+ return SupabaseProvider(
+ **self._kwargs(
+ "project_url",
+ "base_url",
+ "auth_route",
+ "algorithm",
+ "required_scopes",
+ "scopes_supported",
+ "resource_name",
+ "resource_documentation",
+ ),
+ token_verifier=self._token_verifier,
+ )
diff --git a/src/fastmcp/server/plugins/auth/supabase/provider.py b/src/fastmcp/server/plugins/auth/supabase/provider.py
new file mode 100644
index 000000000..1631df35a
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/supabase/provider.py
@@ -0,0 +1,181 @@
+"""Supabase authentication provider for FastMCP.
+
+This module provides SupabaseProvider - a complete authentication solution that integrates
+with Supabase Auth's JWT verification, supporting Dynamic Client Registration (DCR)
+for seamless MCP client authentication.
+"""
+
+from __future__ import annotations
+
+from typing import Literal
+
+import httpx
+from pydantic import AnyHttpUrl
+from starlette.responses import JSONResponse
+from starlette.routing import Route
+
+from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class SupabaseProvider(RemoteAuthProvider):
+ """Supabase metadata provider for DCR (Dynamic Client Registration).
+
+ This provider implements Supabase Auth integration using metadata forwarding.
+ This approach allows Supabase to handle the OAuth flow directly while FastMCP acts
+ as a resource server, verifying JWTs issued by Supabase Auth.
+
+ IMPORTANT SETUP REQUIREMENTS:
+
+ 1. Supabase Project Setup:
+ - Create a Supabase project at https://supabase.com
+ - Note your project URL (e.g., "https://abc123.supabase.co")
+ - Configure your JWT algorithm in Supabase Auth settings (RS256 or ES256)
+ - Asymmetric keys (RS256/ES256) are recommended for production
+
+ 2. JWT Verification:
+ - FastMCP verifies JWTs using the JWKS endpoint at {project_url}{auth_route}/.well-known/jwks.json
+ - JWTs are issued by {project_url}{auth_route}
+ - Default auth_route is "/auth/v1" (can be customized for self-hosted setups)
+ - Tokens are cached for up to 10 minutes by Supabase's edge servers
+ - Algorithm must match your Supabase Auth configuration
+
+ 3. Authorization:
+ - Supabase uses Row Level Security (RLS) policies for database authorization
+ - OAuth-level scopes are an upcoming feature in Supabase Auth
+ - Both approaches will be supported once scope handling is available
+
+ For detailed setup instructions, see:
+ https://supabase.com/docs/guides/auth/jwts
+
+ Example:
+ ```python
+ from fastmcp.server.plugins.auth.supabase.provider import SupabaseProvider
+
+ # Create Supabase metadata provider (JWT verifier created automatically)
+ supabase_auth = SupabaseProvider(
+ project_url="https://abc123.supabase.co",
+ base_url="https://your-fastmcp-server.com",
+ algorithm="ES256", # Match your Supabase Auth configuration
+ )
+
+ # Use with FastMCP
+ mcp = FastMCP("My App", auth=supabase_auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ project_url: AnyHttpUrl | str,
+ base_url: AnyHttpUrl | str,
+ auth_route: str = "/auth/v1",
+ algorithm: Literal["RS256", "ES256"] = "ES256",
+ required_scopes: list[str] | None = None,
+ scopes_supported: list[str] | None = None,
+ resource_name: str | None = None,
+ resource_documentation: AnyHttpUrl | None = None,
+ token_verifier: TokenVerifier | None = None,
+ ):
+ """Initialize Supabase metadata provider.
+
+ Args:
+ project_url: Your Supabase project URL (e.g., "https://abc123.supabase.co")
+ base_url: Public URL of this FastMCP server
+ auth_route: Supabase Auth route. Defaults to "/auth/v1". Can be customized
+ for self-hosted Supabase Auth setups using custom routes.
+ algorithm: JWT signing algorithm (RS256 or ES256). Must match your
+ Supabase Auth configuration. Defaults to ES256.
+ required_scopes: Optional list of scopes to require for all requests.
+ Note: Supabase currently uses RLS policies for authorization. OAuth-level
+ scopes are an upcoming feature.
+ scopes_supported: Optional list of scopes to advertise in OAuth metadata.
+ If None, uses required_scopes. Use this when the scopes clients should
+ request differ from the scopes enforced on tokens.
+ resource_name: Optional name for the protected resource metadata.
+ resource_documentation: Optional documentation URL for the protected resource.
+ token_verifier: Optional token verifier. If None, creates JWT verifier for Supabase
+ """
+ self.project_url = str(project_url).rstrip("/")
+ self.base_url = AnyHttpUrl(str(base_url).rstrip("/"))
+ self.auth_route = auth_route.strip("/")
+
+ # Parse scopes if provided as string
+ parsed_scopes = (
+ parse_scopes(required_scopes) if required_scopes is not None else None
+ )
+
+ # 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,
+ )
+
+ # Initialize RemoteAuthProvider with Supabase as the authorization server
+ super().__init__(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl(f"{self.project_url}/{self.auth_route}")],
+ base_url=self.base_url,
+ scopes_supported=scopes_supported,
+ resource_name=resource_name,
+ resource_documentation=resource_documentation,
+ )
+
+ def get_routes(
+ self,
+ mcp_path: str | None = None,
+ ) -> list[Route]:
+ """Get OAuth routes including Supabase authorization server metadata forwarding.
+
+ This returns the standard protected resource routes plus an authorization server
+ metadata endpoint that forwards Supabase's OAuth metadata to clients.
+
+ Args:
+ mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp")
+ This is used to advertise the resource URL in metadata.
+ """
+ # Get the standard protected resource routes from RemoteAuthProvider
+ routes = super().get_routes(mcp_path)
+
+ async def oauth_authorization_server_metadata(request):
+ """Forward Supabase OAuth authorization server metadata with FastMCP customizations."""
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"{self.project_url}/{self.auth_route}/.well-known/oauth-authorization-server"
+ )
+ response.raise_for_status()
+ metadata = response.json()
+ return JSONResponse(metadata)
+ except Exception as e:
+ return JSONResponse(
+ {
+ "error": "server_error",
+ "error_description": f"Failed to fetch Supabase metadata: {e}",
+ },
+ status_code=500,
+ )
+
+ # Add Supabase authorization server metadata forwarding
+ routes.append(
+ Route(
+ "/.well-known/oauth-authorization-server",
+ endpoint=oauth_authorization_server_metadata,
+ methods=["GET"],
+ )
+ )
+
+ return routes
diff --git a/src/fastmcp/server/plugins/auth/workos/__init__.py b/src/fastmcp/server/plugins/auth/workos/__init__.py
new file mode 100644
index 000000000..962652a99
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/workos/__init__.py
@@ -0,0 +1,5 @@
+"""WorkOS auth plugin."""
+
+from fastmcp.server.plugins.auth.workos.plugin import WorkOSAuth
+
+__all__ = ["WorkOSAuth"]
diff --git a/src/fastmcp/server/plugins/auth/workos/plugin.py b/src/fastmcp/server/plugins/auth/workos/plugin.py
new file mode 100644
index 000000000..a28cf4526
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/workos/plugin.py
@@ -0,0 +1,62 @@
+"""WorkOS auth plugin."""
+
+from __future__ import annotations
+
+from typing import Any, ClassVar
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+
+from fastmcp.server.auth import AuthProvider
+from fastmcp.server.plugins.auth._base import AuthPlugin, OAuthProviderConfig
+from fastmcp.server.plugins.auth.workos.provider import WorkOSProvider
+from fastmcp.server.plugins.base import PluginMeta
+
+
+class WorkOSAuthConfig(OAuthProviderConfig):
+ """Config model for the WorkOS auth plugin."""
+
+ authkit_domain: str | None = None
+
+
+class WorkOSAuth(AuthPlugin[WorkOSAuthConfig]):
+ """Contribute a `WorkOSProvider` as the server's auth provider."""
+
+ Config: ClassVar[type[WorkOSAuthConfig]] = WorkOSAuthConfig
+
+ meta = PluginMeta(name="workos-auth")
+
+ def __init__(
+ self,
+ config: WorkOSAuthConfig | dict[str, Any] | None = None,
+ *,
+ client_storage: AsyncKeyValue | None = None,
+ http_client: httpx.AsyncClient | None = None,
+ ) -> None:
+ super().__init__(config)
+ self._client_storage = client_storage
+ self._http_client = http_client
+
+ def auth(self) -> AuthProvider | None:
+ self._require("client_id", "client_secret", "authkit_domain", "base_url")
+ return WorkOSProvider(
+ **self._kwargs(
+ "client_id",
+ "client_secret",
+ "authkit_domain",
+ "base_url",
+ "resource_base_url",
+ "issuer_url",
+ "redirect_path",
+ "required_scopes",
+ "timeout_seconds",
+ "allowed_client_redirect_uris",
+ "jwt_signing_key",
+ "require_authorization_consent",
+ "consent_csp_policy",
+ "forward_resource",
+ "enable_cimd",
+ ),
+ client_storage=self._client_storage,
+ http_client=self._http_client,
+ )
diff --git a/src/fastmcp/server/plugins/auth/workos/provider.py b/src/fastmcp/server/plugins/auth/workos/provider.py
new file mode 100644
index 000000000..16d94539b
--- /dev/null
+++ b/src/fastmcp/server/plugins/auth/workos/provider.py
@@ -0,0 +1,245 @@
+"""WorkOS OAuth authentication provider for FastMCP."""
+
+from __future__ import annotations
+
+import contextlib
+from typing import Literal
+
+import httpx
+from key_value.aio.protocols import AsyncKeyValue
+from pydantic import AnyHttpUrl
+
+from fastmcp.server.auth import AccessToken, TokenVerifier
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
+from fastmcp.utilities.auth import parse_scopes
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class WorkOSTokenVerifier(TokenVerifier):
+ """Token verifier for WorkOS OAuth tokens.
+
+ WorkOS AuthKit tokens are opaque, so we verify them by calling
+ the /oauth2/userinfo endpoint to check validity and get user info.
+ """
+
+ def __init__(
+ self,
+ *,
+ authkit_domain: str,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ http_client: httpx.AsyncClient | None = None,
+ ):
+ """Initialize the WorkOS token verifier.
+
+ Args:
+ authkit_domain: WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
+ required_scopes: Required OAuth scopes
+ timeout_seconds: HTTP request timeout
+ http_client: Optional httpx.AsyncClient for connection pooling. When provided,
+ the client is reused across calls and the caller is responsible for its
+ lifecycle. When None (default), a fresh client is created per call.
+ """
+ super().__init__(required_scopes=required_scopes)
+ self.authkit_domain = authkit_domain.rstrip("/")
+ self.timeout_seconds = timeout_seconds
+ self._http_client = http_client
+
+ async def verify_token(self, token: str) -> AccessToken | None:
+ """Verify WorkOS OAuth token by calling userinfo endpoint."""
+ try:
+ async with (
+ contextlib.nullcontext(self._http_client)
+ if self._http_client is not None
+ else httpx.AsyncClient(timeout=self.timeout_seconds)
+ ) as client:
+ # Use WorkOS AuthKit userinfo endpoint to validate token
+ response = await client.get(
+ f"{self.authkit_domain}/oauth2/userinfo",
+ headers={
+ "Authorization": f"Bearer {token}",
+ "User-Agent": "FastMCP-WorkOS-OAuth",
+ },
+ )
+
+ if response.status_code != 200:
+ logger.debug(
+ "WorkOS token verification failed: %d - %s",
+ response.status_code,
+ response.text[:200],
+ )
+ return None
+
+ user_data = response.json()
+ token_scopes = (
+ parse_scopes(user_data.get("scope") or user_data.get("scopes"))
+ or []
+ )
+
+ if self.required_scopes and not all(
+ scope in token_scopes for scope in self.required_scopes
+ ):
+ logger.debug(
+ "WorkOS token missing required scopes. required=%s actual=%s",
+ self.required_scopes,
+ token_scopes,
+ )
+ return None
+
+ # Create AccessToken with WorkOS user info
+ return AccessToken(
+ token=token,
+ client_id=str(user_data.get("sub", "unknown")),
+ scopes=token_scopes,
+ expires_at=None, # Will be set from token introspection if needed
+ claims={
+ "sub": user_data.get("sub"),
+ "email": user_data.get("email"),
+ "email_verified": user_data.get("email_verified"),
+ "name": user_data.get("name"),
+ "given_name": user_data.get("given_name"),
+ "family_name": user_data.get("family_name"),
+ },
+ )
+
+ except httpx.RequestError as e:
+ logger.debug("Failed to verify WorkOS token: %s", e)
+ return None
+ except Exception as e:
+ logger.debug("WorkOS token verification error: %s", e)
+ return None
+
+
+class WorkOSProvider(OAuthProxy):
+ """Complete WorkOS OAuth provider for FastMCP.
+
+ This provider implements WorkOS AuthKit OAuth using the OAuth Proxy pattern.
+ It provides OAuth2 authentication for users through WorkOS Connect applications.
+
+ Features:
+ - Transparent OAuth proxy to WorkOS AuthKit
+ - Automatic token validation via userinfo endpoint
+ - User information extraction from ID tokens
+ - Support for standard OAuth scopes (openid, profile, email)
+
+ Setup Requirements:
+ 1. Create a WorkOS Connect application in your dashboard
+ 2. Note your AuthKit domain (e.g., "https://your-app.authkit.app")
+ 3. Configure redirect URI as: http://localhost:8000/auth/callback
+ 4. Note your Client ID and Client Secret
+
+ Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.plugins.auth.workos.provider import WorkOSProvider
+
+ auth = WorkOSProvider(
+ client_id="client_123",
+ client_secret="sk_test_456",
+ authkit_domain="https://your-app.authkit.app",
+ base_url="http://localhost:8000"
+ )
+
+ mcp = FastMCP("My App", auth=auth)
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ client_id: str,
+ client_secret: str,
+ authkit_domain: str,
+ base_url: AnyHttpUrl | str,
+ resource_base_url: AnyHttpUrl | str | None = None,
+ issuer_url: AnyHttpUrl | str | None = None,
+ redirect_path: str | None = None,
+ required_scopes: list[str] | None = None,
+ timeout_seconds: int = 10,
+ allowed_client_redirect_uris: list[str] | None = None,
+ client_storage: AsyncKeyValue | None = None,
+ jwt_signing_key: str | bytes | None = None,
+ require_authorization_consent: bool | Literal["remember", "external"] = True,
+ consent_csp_policy: str | None = None,
+ forward_resource: bool = True,
+ http_client: httpx.AsyncClient | None = None,
+ enable_cimd: bool = True,
+ ):
+ """Initialize WorkOS OAuth provider.
+
+ Args:
+ client_id: WorkOS client ID
+ client_secret: WorkOS client secret
+ authkit_domain: Your WorkOS AuthKit domain (e.g., "https://your-app.authkit.app")
+ base_url: Public URL where OAuth endpoints will be accessible (includes any mount path)
+ resource_base_url: Optional public base URL for the protected resource metadata
+ and token audience. Defaults to ``base_url``.
+ issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
+ to avoid 404s during discovery when mounting under a path.
+ redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback")
+ required_scopes: Required OAuth scopes (no default)
+ timeout_seconds: HTTP request timeout for WorkOS API calls (defaults to 10)
+ allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
+ If None (default), all URIs are allowed. If empty list, no URIs are allowed.
+ client_storage: Storage backend for OAuth state (client registrations, encrypted tokens).
+ If None, an encrypted file store will be created in the data directory
+ (derived from `platformdirs`).
+ jwt_signing_key: Secret for signing FastMCP JWT tokens (any string or bytes). If bytes are provided,
+ they will be used as is. If a string is provided, it will be derived into a 32-byte key. If not
+ provided, the upstream client secret will be used to derive a 32-byte key using PBKDF2.
+ require_authorization_consent: Whether to require user consent before authorizing clients (default True).
+ When True, users see a consent screen before being redirected to WorkOS.
+ When False, authorization proceeds directly without user confirmation.
+ When "external", the built-in consent screen is skipped but no warning is
+ logged, indicating that consent is handled externally (e.g. by the upstream IdP).
+ SECURITY WARNING: Only set to False for local development or testing environments.
+ http_client: Optional httpx.AsyncClient for connection pooling in token verification.
+ When provided, the client is reused across verify_token calls and the caller
+ is responsible for its lifecycle. When None (default), a fresh client is created per call.
+ enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
+ client IDs (default True). Set to False to disable.
+ """
+ # Apply defaults and ensure authkit_domain is a full URL
+ authkit_domain_str = authkit_domain
+ if not authkit_domain_str.startswith(("http://", "https://")):
+ authkit_domain_str = f"https://{authkit_domain_str}"
+ authkit_domain_final = authkit_domain_str.rstrip("/")
+ scopes_final = (
+ parse_scopes(required_scopes) if required_scopes is not None else []
+ )
+
+ # Create WorkOS token verifier
+ token_verifier = WorkOSTokenVerifier(
+ authkit_domain=authkit_domain_final,
+ required_scopes=scopes_final,
+ timeout_seconds=timeout_seconds,
+ http_client=http_client,
+ )
+
+ # Initialize OAuth proxy with WorkOS AuthKit endpoints
+ super().__init__(
+ upstream_authorization_endpoint=f"{authkit_domain_final}/oauth2/authorize",
+ upstream_token_endpoint=f"{authkit_domain_final}/oauth2/token",
+ upstream_client_id=client_id,
+ upstream_client_secret=client_secret,
+ token_verifier=token_verifier,
+ base_url=base_url,
+ resource_base_url=resource_base_url,
+ redirect_path=redirect_path,
+ issuer_url=issuer_url or base_url, # Default to base_url if not specified
+ allowed_client_redirect_uris=allowed_client_redirect_uris,
+ client_storage=client_storage,
+ jwt_signing_key=jwt_signing_key,
+ require_authorization_consent=require_authorization_consent,
+ consent_csp_policy=consent_csp_policy,
+ forward_resource=forward_resource,
+ enable_cimd=enable_cimd,
+ )
+
+ logger.debug(
+ "Initialized WorkOS OAuth provider for client %s with AuthKit domain %s",
+ client_id,
+ authkit_domain_final,
+ )
diff --git a/src/fastmcp/server/plugins/base.py b/src/fastmcp/server/plugins/base.py
index 8851e22b1..74fc8e63b 100644
--- a/src/fastmcp/server/plugins/base.py
+++ b/src/fastmcp/server/plugins/base.py
@@ -378,6 +378,9 @@ class Plugin(Generic[C]):
def middleware(self):
# self.config is typed as PIIRedactorConfig
return [PIIMiddleware(self.config.patterns)]
+
+
+ plugin = PIIRedactor(PIIRedactor.Config(patterns=["email"]))
```
"""
@@ -396,6 +399,17 @@ class Plugin(Generic[C]):
for plugins that don't parameterize `Plugin`.
"""
+ Config: ClassVar[type[BaseModel]] = _EmptyConfig
+ """Public alias for the plugin's config model.
+
+ This lets users instantiate a plugin's config without importing the
+ implementation-specific config class separately:
+
+ ```python
+ SomePlugin(SomePlugin.Config(...))
+ ```
+ """
+
config: C
"""The validated config instance. Typed as `C`, the generic
parameter, so `self.config.` type-checks correctly."""
@@ -419,6 +433,7 @@ class Plugin(Generic[C]):
config_cls = _resolve_plugin_config_cls(cls)
if config_cls is not None:
cls._config_cls = config_cls
+ cls.Config = cls._config_cls
# Enforce the JSON-serializable contract on the resolved config.
# Every plugin config must round-trip through JSON so plugins
# can be loaded from config files, rendered by registry/Horizon
diff --git a/src/fastmcp/server/plugins/code_mode/plugin.py b/src/fastmcp/server/plugins/code_mode/plugin.py
index a557aee7d..72142d6ba 100644
--- a/src/fastmcp/server/plugins/code_mode/plugin.py
+++ b/src/fastmcp/server/plugins/code_mode/plugin.py
@@ -10,7 +10,7 @@ for servers with many tools.
from __future__ import annotations
-from typing import Any, Literal
+from typing import Any, ClassVar, Literal
from pydantic import BaseModel, ConfigDict
@@ -73,7 +73,6 @@ class CodeMode(Plugin[CodeModeConfig]):
```python
from fastmcp.server.plugins.code_mode import (
CodeMode,
- CodeModeConfig,
GetSchemas,
ListTools,
)
@@ -82,7 +81,7 @@ class CodeMode(Plugin[CodeModeConfig]):
"Server",
plugins=[
CodeMode(
- CodeModeConfig(execute_tool_name="run"),
+ CodeMode.Config(execute_tool_name="run"),
sandbox_provider=my_custom_sandbox,
discovery_tools=[ListTools(), GetSchemas()],
)
@@ -91,6 +90,8 @@ class CodeMode(Plugin[CodeModeConfig]):
```
"""
+ Config: ClassVar[type[CodeModeConfig]] = CodeModeConfig
+
# `meta` is auto-derived (name="code-mode", version=None) — the right
# answer for a bundled first-party plugin. Declare `meta` explicitly
# (or use `PluginMeta.from_package(...)`) if published separately.
diff --git a/src/fastmcp/server/plugins/openapi/__init__.py b/src/fastmcp/server/plugins/openapi/__init__.py
index d9ef7e1ca..aacb93eda 100644
--- a/src/fastmcp/server/plugins/openapi/__init__.py
+++ b/src/fastmcp/server/plugins/openapi/__init__.py
@@ -1,11 +1,11 @@
"""OpenAPI plugin — mount an OpenAPI spec as MCP tools/resources.
from fastmcp import FastMCP
- from fastmcp.server.plugins.openapi import OpenAPI, OpenAPIConfig
+ from fastmcp.server.plugins.openapi import OpenAPI
mcp = FastMCP(
"Petstore",
- plugins=[OpenAPI(OpenAPIConfig(spec=petstore_spec))],
+ plugins=[OpenAPI(OpenAPI.Config(spec=petstore_spec))],
)
Typed `RouteMap` + `MCPType` are re-exported for the Python-only
diff --git a/src/fastmcp/server/plugins/openapi/plugin.py b/src/fastmcp/server/plugins/openapi/plugin.py
index f1ca2227a..da6ccd177 100644
--- a/src/fastmcp/server/plugins/openapi/plugin.py
+++ b/src/fastmcp/server/plugins/openapi/plugin.py
@@ -14,7 +14,7 @@ from __future__ import annotations
import json
from pathlib import Path
-from typing import Any, Literal
+from typing import Any, ClassVar, Literal
import httpx
from pydantic import BaseModel, ConfigDict
@@ -123,21 +123,21 @@ class OpenAPI(Plugin[OpenAPIConfig]):
"""Mount an OpenAPI spec as an MCP server via a plugin.
Everything declarative (spec, base URL, headers, route mappings)
- goes in `OpenAPIConfig`. Python-only knobs — custom `httpx.AsyncClient`,
+ goes in `OpenAPI.Config`. Python-only knobs — custom `httpx.AsyncClient`,
route-mapping callables, component customization — go in `__init__`
kwargs.
Example:
```python
from fastmcp import FastMCP
- from fastmcp.server.plugins.openapi import OpenAPI, OpenAPIConfig
+ from fastmcp.server.plugins.openapi import OpenAPI
# Declarative (JSON-friendly):
mcp = FastMCP(
"Petstore",
plugins=[
OpenAPI(
- OpenAPIConfig(
+ OpenAPI.Config(
spec=petstore_spec,
base_url="https://api.example.com",
headers={"Authorization": "Bearer ..."},
@@ -152,7 +152,7 @@ class OpenAPI(Plugin[OpenAPIConfig]):
"Petstore",
plugins=[
OpenAPI(
- OpenAPIConfig(spec=petstore_spec),
+ OpenAPI.Config(spec=petstore_spec),
client=custom_client,
)
],
@@ -160,6 +160,8 @@ class OpenAPI(Plugin[OpenAPIConfig]):
```
"""
+ Config: ClassVar[type[OpenAPIConfig]] = OpenAPIConfig
+
# "OpenAPI" is a single technical term; the auto-kebab would split
# it into "open-api", which is uglier than the established spelling.
meta = PluginMeta(name="openapi")
diff --git a/src/fastmcp/server/plugins/prompts_as_tools/plugin.py b/src/fastmcp/server/plugins/prompts_as_tools/plugin.py
index a4b192a4c..a853a5483 100644
--- a/src/fastmcp/server/plugins/prompts_as_tools/plugin.py
+++ b/src/fastmcp/server/plugins/prompts_as_tools/plugin.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from typing import ClassVar
+
from pydantic import BaseModel, ConfigDict
from fastmcp.server.plugins.base import Plugin
@@ -37,5 +39,7 @@ class PromptsAsTools(Plugin[PromptsAsToolsConfig]):
```
"""
+ Config: ClassVar[type[PromptsAsToolsConfig]] = PromptsAsToolsConfig
+
def transforms(self) -> list[Transform]:
return [PromptsAsToolsTransform()]
diff --git a/src/fastmcp/server/plugins/resources_as_tools/plugin.py b/src/fastmcp/server/plugins/resources_as_tools/plugin.py
index c6948b793..add9df90f 100644
--- a/src/fastmcp/server/plugins/resources_as_tools/plugin.py
+++ b/src/fastmcp/server/plugins/resources_as_tools/plugin.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from typing import ClassVar
+
from pydantic import BaseModel, ConfigDict
from fastmcp.server.plugins.base import Plugin
@@ -39,5 +41,7 @@ class ResourcesAsTools(Plugin[ResourcesAsToolsConfig]):
```
"""
+ Config: ClassVar[type[ResourcesAsToolsConfig]] = ResourcesAsToolsConfig
+
def transforms(self) -> list[Transform]:
return [ResourcesAsToolsTransform()]
diff --git a/src/fastmcp/server/plugins/skills/__init__.py b/src/fastmcp/server/plugins/skills/__init__.py
index 518507a31..7cfba05f3 100644
--- a/src/fastmcp/server/plugins/skills/__init__.py
+++ b/src/fastmcp/server/plugins/skills/__init__.py
@@ -1,9 +1,9 @@
"""Skills plugin — expose agent skill folders as MCP resources.
from fastmcp import FastMCP
- from fastmcp.server.plugins.skills import Skills, SkillsConfig
+ from fastmcp.server.plugins.skills import Skills
- mcp = FastMCP("skills", plugins=[Skills(SkillsConfig(vendor="claude"))])
+ mcp = FastMCP("skills", plugins=[Skills(Skills.Config(vendor="claude"))])
The underlying `SkillProvider` and `SkillsDirectoryProvider` classes
live on `.skill_provider` and `.directory_provider` submodules for
diff --git a/src/fastmcp/server/plugins/skills/plugin.py b/src/fastmcp/server/plugins/skills/plugin.py
index 3176d123a..cfbb93c29 100644
--- a/src/fastmcp/server/plugins/skills/plugin.py
+++ b/src/fastmcp/server/plugins/skills/plugin.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from pathlib import Path
-from typing import Any, Literal
+from typing import Any, ClassVar, Literal
from pydantic import BaseModel, ConfigDict
@@ -14,7 +14,7 @@ from fastmcp.server.providers import Provider
# Vendor-name → list of skill-root paths. Captures the same preset
# paths the vendor subclasses (`ClaudeSkillsProvider`, `CursorSkillsProvider`,
-# etc.) used to hardcode. The dict lets `Skills(SkillsConfig(vendor="claude"))`
+# etc.) used to hardcode. The dict lets `Skills(Skills.Config(vendor="claude"))`
# replace seven separate subclass names with one plugin + an enum value.
VENDOR_PATHS: dict[str, list[Path]] = {
"claude": [Path.home() / ".claude" / "skills"],
@@ -93,28 +93,30 @@ class Skills(Plugin[SkillsConfig]):
Example:
```python
from fastmcp import FastMCP
- from fastmcp.server.plugins.skills import Skills, SkillsConfig
+ from fastmcp.server.plugins.skills import Skills
# Vendor preset — the common case:
mcp = FastMCP(
"skills",
- plugins=[Skills(SkillsConfig(vendor="claude"))],
+ plugins=[Skills(Skills.Config(vendor="claude"))],
)
# Custom directory:
mcp = FastMCP(
"skills",
- plugins=[Skills(SkillsConfig(directory="./skills"))],
+ plugins=[Skills(Skills.Config(directory="./skills"))],
)
# Single skill folder:
mcp = FastMCP(
"skills",
- plugins=[Skills(SkillsConfig(path="./skills/pdf-processing"))],
+ plugins=[Skills(Skills.Config(path="./skills/pdf-processing"))],
)
```
"""
+ Config: ClassVar[type[SkillsConfig]] = SkillsConfig
+
def providers(self) -> list[Provider]:
return [self._build_provider()]
diff --git a/src/fastmcp/server/plugins/tool_search/plugin.py b/src/fastmcp/server/plugins/tool_search/plugin.py
index cb1a08c69..936610512 100644
--- a/src/fastmcp/server/plugins/tool_search/plugin.py
+++ b/src/fastmcp/server/plugins/tool_search/plugin.py
@@ -8,7 +8,7 @@ user code should configure behavior through the plugin.
from __future__ import annotations
-from typing import Literal
+from typing import ClassVar, Literal
from pydantic import BaseModel, ConfigDict
@@ -51,7 +51,7 @@ class ToolSearch(Plugin[ToolSearchConfig]):
Example:
```python
from fastmcp import FastMCP
- from fastmcp.server.plugins.tool_search import ToolSearch, ToolSearchConfig
+ from fastmcp.server.plugins.tool_search import ToolSearch
# Default config:
mcp = FastMCP("Server", plugins=[ToolSearch()])
@@ -59,7 +59,7 @@ class ToolSearch(Plugin[ToolSearchConfig]):
# Typed config (IDE completion + static validation):
mcp = FastMCP(
"Server",
- plugins=[ToolSearch(ToolSearchConfig(strategy="regex", always_visible=["help"]))],
+ plugins=[ToolSearch(ToolSearch.Config(strategy="regex", always_visible=["help"]))],
)
# Dict config (useful for loading from JSON/YAML):
@@ -67,6 +67,8 @@ class ToolSearch(Plugin[ToolSearchConfig]):
```
"""
+ Config: ClassVar[type[ToolSearchConfig]] = ToolSearchConfig
+
# `meta` is intentionally omitted: the auto-derived default
# (`name="tool-search"`, `version=None`) is appropriate for a
# bundled first-party plugin with no independent release cadence.
diff --git a/tests/deprecated/test_auth_provider_imports.py b/tests/deprecated/test_auth_provider_imports.py
new file mode 100644
index 000000000..57d41fb32
--- /dev/null
+++ b/tests/deprecated/test_auth_provider_imports.py
@@ -0,0 +1,137 @@
+"""Test that deprecated auth provider import paths still work."""
+
+from __future__ import annotations
+
+import importlib
+import sys
+import warnings
+
+import pytest
+
+from fastmcp.exceptions import FastMCPDeprecationWarning
+from fastmcp.utilities.tests import temporary_settings
+
+AUTH_PROVIDER_SHIMS = [
+ (
+ "auth0",
+ "fastmcp.server.plugins.auth.auth0.provider",
+ ("Auth0Provider",),
+ ),
+ (
+ "aws",
+ "fastmcp.server.plugins.auth.aws.provider",
+ ("AWSCognitoProvider", "AWSCognitoTokenVerifier"),
+ ),
+ (
+ "azure",
+ "fastmcp.server.plugins.auth.azure.provider",
+ ("AzureJWTVerifier", "AzureProvider", "EntraOBOToken"),
+ ),
+ (
+ "clerk",
+ "fastmcp.server.plugins.auth.clerk.provider",
+ ("ClerkProvider", "ClerkTokenVerifier"),
+ ),
+ (
+ "descope",
+ "fastmcp.server.plugins.auth.descope.provider",
+ ("DescopeProvider",),
+ ),
+ (
+ "discord",
+ "fastmcp.server.plugins.auth.discord.provider",
+ ("DiscordProvider", "DiscordTokenVerifier"),
+ ),
+ (
+ "github",
+ "fastmcp.server.plugins.auth.github.provider",
+ ("GitHubProvider", "GitHubTokenVerifier"),
+ ),
+ (
+ "google",
+ "fastmcp.server.plugins.auth.google.provider",
+ ("GoogleProvider", "GoogleTokenVerifier"),
+ ),
+ (
+ "keycloak",
+ "fastmcp.server.plugins.auth.keycloak.provider",
+ ("KeycloakAuthProvider",),
+ ),
+ (
+ "oci",
+ "fastmcp.server.plugins.auth.oci.provider",
+ ("OCIProvider",),
+ ),
+ (
+ "propelauth",
+ "fastmcp.server.plugins.auth.propelauth.provider",
+ ("PropelAuthProvider", "PropelAuthTokenIntrospectionOverrides"),
+ ),
+ (
+ "scalekit",
+ "fastmcp.server.plugins.auth.scalekit.provider",
+ ("ScalekitProvider",),
+ ),
+ (
+ "supabase",
+ "fastmcp.server.plugins.auth.supabase.provider",
+ ("SupabaseProvider",),
+ ),
+ (
+ "workos",
+ "fastmcp.server.plugins.auth.workos.provider",
+ ("WorkOSProvider", "WorkOSTokenVerifier"),
+ ),
+ (
+ "workos",
+ "fastmcp.server.plugins.auth.authkit.provider",
+ ("AuthKitProvider",),
+ ),
+]
+
+
+@pytest.mark.parametrize(
+ ("legacy_name", "canonical_module_name", "export_names"),
+ AUTH_PROVIDER_SHIMS,
+)
+def test_deprecated_auth_provider_imports_still_work(
+ legacy_name: str,
+ canonical_module_name: str,
+ export_names: tuple[str, ...],
+):
+ legacy_module_name = f"fastmcp.server.auth.providers.{legacy_name}"
+ canonical_module = importlib.import_module(canonical_module_name)
+
+ sys.modules.pop(legacy_module_name, None)
+
+ with temporary_settings(deprecation_warnings=True):
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter("always")
+ legacy_module = importlib.import_module(legacy_module_name)
+
+ fastmcp_warns = [
+ w for w in caught if issubclass(w.category, FastMCPDeprecationWarning)
+ ]
+ assert any("fastmcp.server.plugins.auth" in str(w.message) for w in fastmcp_warns)
+
+ for export_name in export_names:
+ assert getattr(legacy_module, export_name) is getattr(
+ canonical_module, export_name
+ )
+
+
+@pytest.mark.parametrize(
+ "legacy_name",
+ sorted({legacy_name for legacy_name, _, _ in AUTH_PROVIDER_SHIMS}),
+)
+def test_deprecated_auth_provider_imports_are_silent_when_disabled(
+ legacy_name: str,
+):
+ legacy_module_name = f"fastmcp.server.auth.providers.{legacy_name}"
+
+ sys.modules.pop(legacy_module_name, None)
+
+ with temporary_settings(deprecation_warnings=False):
+ with warnings.catch_warnings():
+ warnings.simplefilter("error", FastMCPDeprecationWarning)
+ importlib.import_module(legacy_module_name)
diff --git a/tests/integration_tests/auth/test_github_provider_integration.py b/tests/integration_tests/auth/test_github_provider_integration.py
index 47d9c944a..8b742155c 100644
--- a/tests/integration_tests/auth/test_github_provider_integration.py
+++ b/tests/integration_tests/auth/test_github_provider_integration.py
@@ -24,7 +24,7 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.auth.auth import AccessToken
from fastmcp.server.auth.oauth_proxy.models import ClientCode
-from fastmcp.server.auth.providers.github import GitHubProvider
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
from fastmcp.utilities.tests import HeadlessOAuth, run_server_async
FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID = os.getenv("FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID")
diff --git a/tests/integration_tests/auth/test_keycloak_provider_integration.py b/tests/integration_tests/auth/test_keycloak_provider_integration.py
index 3b3a8fafb..3bfca6ab4 100644
--- a/tests/integration_tests/auth/test_keycloak_provider_integration.py
+++ b/tests/integration_tests/auth/test_keycloak_provider_integration.py
@@ -7,7 +7,7 @@ import httpx
import pytest
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+from fastmcp.server.plugins.auth.keycloak.provider import KeycloakAuthProvider
TEST_REALM_URL = "https://keycloak.example.com/realms/test"
TEST_BASE_URL = "https://fastmcp.example.com"
diff --git a/tests/server/auth/providers/test_auth0.py b/tests/server/auth/providers/test_auth0.py
index 2c8cb1b46..06220a550 100644
--- a/tests/server/auth/providers/test_auth0.py
+++ b/tests/server/auth/providers/test_auth0.py
@@ -5,8 +5,8 @@ from unittest.mock import patch
import pytest
from fastmcp.server.auth.oidc_proxy import OIDCConfiguration
-from fastmcp.server.auth.providers.auth0 import Auth0Provider
from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
TEST_CONFIG_URL = "https://example.com/.well-known/openid-configuration"
TEST_CLIENT_ID = "test-client-id"
diff --git a/tests/server/auth/providers/test_aws.py b/tests/server/auth/providers/test_aws.py
index 4f5232b53..36764840a 100644
--- a/tests/server/auth/providers/test_aws.py
+++ b/tests/server/auth/providers/test_aws.py
@@ -3,7 +3,7 @@
from contextlib import contextmanager
from unittest.mock import patch
-from fastmcp.server.auth.providers.aws import (
+from fastmcp.server.plugins.auth.aws.provider import (
AWSCognitoProvider,
)
diff --git a/tests/server/auth/providers/test_azure.py b/tests/server/auth/providers/test_azure.py
index 54b119ce9..b5ff43d19 100644
--- a/tests/server/auth/providers/test_azure.py
+++ b/tests/server/auth/providers/test_azure.py
@@ -8,8 +8,8 @@ from mcp.server.auth.provider import AuthorizationParams
from mcp.shared.auth import OAuthClientInformationFull
from pydantic import AnyUrl
-from fastmcp.server.auth.providers.azure import AzureProvider
from fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair
+from fastmcp.server.plugins.auth.azure.provider import AzureProvider
@pytest.fixture
diff --git a/tests/server/auth/providers/test_azure_scopes.py b/tests/server/auth/providers/test_azure_scopes.py
index 8ab35431d..e17c357f5 100644
--- a/tests/server/auth/providers/test_azure_scopes.py
+++ b/tests/server/auth/providers/test_azure_scopes.py
@@ -4,13 +4,13 @@ import pytest
from key_value.aio.stores.memory import MemoryStore
from fastmcp.server.auth.auth import MultiAuth
-from fastmcp.server.auth.providers.azure import (
+from fastmcp.server.auth.providers.jwt import RSAKeyPair, StaticTokenVerifier
+from fastmcp.server.plugins.auth.azure.provider import (
OIDC_SCOPES,
AzureJWTVerifier,
AzureProvider,
_find_azure_provider,
)
-from fastmcp.server.auth.providers.jwt import RSAKeyPair, StaticTokenVerifier
@pytest.fixture
@@ -771,13 +771,16 @@ class TestAzureOBOIntegration:
def test_entra_obo_token_is_importable(self):
"""Test that EntraOBOToken can be imported."""
- from fastmcp.server.auth.providers.azure import EntraOBOToken
+ from fastmcp.server.plugins.auth.azure.provider import EntraOBOToken
assert EntraOBOToken is not None
def test_entra_obo_token_creates_dependency(self):
"""Test that EntraOBOToken creates a dependency with scopes."""
- from fastmcp.server.auth.providers.azure import EntraOBOToken, _EntraOBOToken
+ from fastmcp.server.plugins.auth.azure.provider import (
+ EntraOBOToken,
+ _EntraOBOToken,
+ )
dep = EntraOBOToken(["https://graph.microsoft.com/User.Read"])
assert isinstance(dep, _EntraOBOToken)
@@ -786,7 +789,7 @@ class TestAzureOBOIntegration:
def test_entra_obo_token_is_dependency_instance(self):
"""Test that EntraOBOToken is a Dependency instance."""
from fastmcp.dependencies import Dependency
- from fastmcp.server.auth.providers.azure import _EntraOBOToken
+ from fastmcp.server.plugins.auth.azure.provider import _EntraOBOToken
dep = _EntraOBOToken(["scope"])
assert isinstance(dep, Dependency)
diff --git a/tests/server/auth/providers/test_clerk.py b/tests/server/auth/providers/test_clerk.py
index 323b36572..3aae0dbfc 100644
--- a/tests/server/auth/providers/test_clerk.py
+++ b/tests/server/auth/providers/test_clerk.py
@@ -7,7 +7,7 @@ import pytest
from key_value.aio.stores.memory import MemoryStore
from pytest_httpx import HTTPXMock
-from fastmcp.server.auth.providers.clerk import ClerkProvider, ClerkTokenVerifier
+from fastmcp.server.plugins.auth.clerk.provider import ClerkProvider, ClerkTokenVerifier
CLERK_DOMAIN = "test-instance.clerk.accounts.dev"
diff --git a/tests/server/auth/providers/test_descope.py b/tests/server/auth/providers/test_descope.py
index 7dcfc477f..2a0a44bb9 100644
--- a/tests/server/auth/providers/test_descope.py
+++ b/tests/server/auth/providers/test_descope.py
@@ -8,8 +8,8 @@ import pytest
from fastmcp import Client, FastMCP
from fastmcp.client.transports import StreamableHttpTransport
-from fastmcp.server.auth.providers.descope import DescopeProvider
from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
from fastmcp.utilities.tests import HeadlessOAuth, run_server_async
diff --git a/tests/server/auth/providers/test_discord.py b/tests/server/auth/providers/test_discord.py
index edf3ffdc7..244b09597 100644
--- a/tests/server/auth/providers/test_discord.py
+++ b/tests/server/auth/providers/test_discord.py
@@ -5,7 +5,10 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from key_value.aio.stores.memory import MemoryStore
-from fastmcp.server.auth.providers.discord import DiscordProvider, DiscordTokenVerifier
+from fastmcp.server.plugins.auth.discord.provider import (
+ DiscordProvider,
+ DiscordTokenVerifier,
+)
@pytest.fixture
@@ -120,7 +123,7 @@ class TestDiscordTokenVerifier:
mock_client.get.return_value = token_info_response
with patch(
- "fastmcp.server.auth.providers.discord.httpx.AsyncClient"
+ "fastmcp.server.plugins.auth.discord.provider.httpx.AsyncClient"
) as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
result = await verifier.verify_token("token")
diff --git a/tests/server/auth/providers/test_github.py b/tests/server/auth/providers/test_github.py
index a0cc4b9cd..81fed9bf1 100644
--- a/tests/server/auth/providers/test_github.py
+++ b/tests/server/auth/providers/test_github.py
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from key_value.aio.stores.memory import MemoryStore
-from fastmcp.server.auth.providers.github import (
+from fastmcp.server.plugins.auth.github.provider import (
GitHubProvider,
GitHubTokenVerifier,
)
@@ -142,7 +142,7 @@ class TestGitHubTokenVerifier:
# Patch the AsyncClient context manager
with patch(
- "fastmcp.server.auth.providers.github.httpx.AsyncClient"
+ "fastmcp.server.plugins.auth.github.provider.httpx.AsyncClient"
) as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
@@ -203,7 +203,7 @@ class TestGitHubTokenVerifierCaching:
mock_client = AsyncMock()
with patch(
- "fastmcp.server.auth.providers.github.httpx.AsyncClient"
+ "fastmcp.server.plugins.auth.github.provider.httpx.AsyncClient"
) as mock_cls:
mock_cls.return_value.__aenter__.return_value = mock_client
@@ -226,7 +226,7 @@ class TestGitHubTokenVerifierCaching:
mock_client = AsyncMock()
with patch(
- "fastmcp.server.auth.providers.github.httpx.AsyncClient"
+ "fastmcp.server.plugins.auth.github.provider.httpx.AsyncClient"
) as mock_cls:
mock_cls.return_value.__aenter__.return_value = mock_client
@@ -244,7 +244,7 @@ class TestGitHubTokenVerifierCaching:
mock_client = AsyncMock()
with patch(
- "fastmcp.server.auth.providers.github.httpx.AsyncClient"
+ "fastmcp.server.plugins.auth.github.provider.httpx.AsyncClient"
) as mock_cls:
mock_cls.return_value.__aenter__.return_value = mock_client
@@ -265,7 +265,7 @@ class TestGitHubTokenVerifierCaching:
mock_client = AsyncMock()
with patch(
- "fastmcp.server.auth.providers.github.httpx.AsyncClient"
+ "fastmcp.server.plugins.auth.github.provider.httpx.AsyncClient"
) as mock_cls:
mock_cls.return_value.__aenter__.return_value = mock_client
@@ -299,7 +299,7 @@ class TestGitHubTokenVerifierCaching:
scopes_response.headers = {}
with patch(
- "fastmcp.server.auth.providers.github.httpx.AsyncClient"
+ "fastmcp.server.plugins.auth.github.provider.httpx.AsyncClient"
) as mock_cls:
mock_cls.return_value.__aenter__.return_value = mock_client
diff --git a/tests/server/auth/providers/test_google.py b/tests/server/auth/providers/test_google.py
index 229f04ad3..25d525e02 100644
--- a/tests/server/auth/providers/test_google.py
+++ b/tests/server/auth/providers/test_google.py
@@ -7,7 +7,7 @@ import pytest
from key_value.aio.stores.memory import MemoryStore
from pytest_httpx import HTTPXMock
-from fastmcp.server.auth.providers.google import (
+from fastmcp.server.plugins.auth.google.provider import (
GOOGLE_SCOPE_ALIASES,
GoogleProvider,
GoogleTokenVerifier,
diff --git a/tests/server/auth/providers/test_http_client.py b/tests/server/auth/providers/test_http_client.py
index fa6126183..be9a3bfd7 100644
--- a/tests/server/auth/providers/test_http_client.py
+++ b/tests/server/auth/providers/test_http_client.py
@@ -212,14 +212,14 @@ class TestGitHubHttpClient:
"""Test http_client parameter on GitHubTokenVerifier."""
def test_stores_http_client(self):
- from fastmcp.server.auth.providers.github import GitHubTokenVerifier
+ from fastmcp.server.plugins.auth.github.provider import GitHubTokenVerifier
client = httpx.AsyncClient()
verifier = GitHubTokenVerifier(http_client=client)
assert verifier._http_client is client
async def test_uses_provided_client(self, httpx_mock: HTTPXMock):
- from fastmcp.server.auth.providers.github import GitHubTokenVerifier
+ from fastmcp.server.plugins.auth.github.provider import GitHubTokenVerifier
client = httpx.AsyncClient()
httpx_mock.add_response(
@@ -242,7 +242,7 @@ class TestDiscordHttpClient:
"""Test http_client parameter on DiscordTokenVerifier."""
def test_stores_http_client(self):
- from fastmcp.server.auth.providers.discord import DiscordTokenVerifier
+ from fastmcp.server.plugins.auth.discord.provider import DiscordTokenVerifier
client = httpx.AsyncClient()
verifier = DiscordTokenVerifier(
@@ -256,7 +256,7 @@ class TestGoogleHttpClient:
"""Test http_client parameter on GoogleTokenVerifier."""
def test_stores_http_client(self):
- from fastmcp.server.auth.providers.google import GoogleTokenVerifier
+ from fastmcp.server.plugins.auth.google.provider import GoogleTokenVerifier
client = httpx.AsyncClient()
verifier = GoogleTokenVerifier(http_client=client)
@@ -267,7 +267,7 @@ class TestWorkOSHttpClient:
"""Test http_client parameter on WorkOSTokenVerifier."""
def test_stores_http_client(self):
- from fastmcp.server.auth.providers.workos import WorkOSTokenVerifier
+ from fastmcp.server.plugins.auth.workos.provider import WorkOSTokenVerifier
client = httpx.AsyncClient()
verifier = WorkOSTokenVerifier(
@@ -281,7 +281,7 @@ class TestProviderHttpClientPassthrough:
"""Test that convenience providers pass http_client to their verifiers."""
def test_github_provider_threads_http_client(self):
- from fastmcp.server.auth.providers.github import (
+ from fastmcp.server.plugins.auth.github.provider import (
GitHubProvider,
GitHubTokenVerifier,
)
@@ -299,7 +299,7 @@ class TestProviderHttpClientPassthrough:
assert verifier._http_client is client
def test_discord_provider_threads_http_client(self):
- from fastmcp.server.auth.providers.discord import (
+ from fastmcp.server.plugins.auth.discord.provider import (
DiscordProvider,
DiscordTokenVerifier,
)
@@ -316,7 +316,7 @@ class TestProviderHttpClientPassthrough:
assert verifier._http_client is client
def test_google_provider_threads_http_client(self):
- from fastmcp.server.auth.providers.google import (
+ from fastmcp.server.plugins.auth.google.provider import (
GoogleProvider,
GoogleTokenVerifier,
)
@@ -333,7 +333,7 @@ class TestProviderHttpClientPassthrough:
assert verifier._http_client is client
def test_workos_provider_threads_http_client(self):
- from fastmcp.server.auth.providers.workos import (
+ from fastmcp.server.plugins.auth.workos.provider import (
WorkOSProvider,
WorkOSTokenVerifier,
)
@@ -351,8 +351,8 @@ class TestProviderHttpClientPassthrough:
assert verifier._http_client is client
def test_azure_provider_threads_http_client(self):
- from fastmcp.server.auth.providers.azure import AzureProvider
from fastmcp.server.auth.providers.jwt import JWTVerifier
+ from fastmcp.server.plugins.auth.azure.provider import AzureProvider
client = httpx.AsyncClient()
provider = AzureProvider(
diff --git a/tests/server/auth/providers/test_keycloak.py b/tests/server/auth/providers/test_keycloak.py
index 4312e0103..97c43072e 100644
--- a/tests/server/auth/providers/test_keycloak.py
+++ b/tests/server/auth/providers/test_keycloak.py
@@ -3,7 +3,7 @@
import pytest
from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+from fastmcp.server.plugins.auth.keycloak.provider import KeycloakAuthProvider
TEST_REALM_URL = "https://keycloak.example.com/realms/test"
TEST_BASE_URL = "https://example.com:8000"
diff --git a/tests/server/auth/providers/test_propelauth.py b/tests/server/auth/providers/test_propelauth.py
index f06efe685..a614aa6ba 100644
--- a/tests/server/auth/providers/test_propelauth.py
+++ b/tests/server/auth/providers/test_propelauth.py
@@ -10,7 +10,7 @@ from pydantic import SecretStr
from fastmcp import Client, FastMCP
from fastmcp.server.auth import AccessToken
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
-from fastmcp.server.auth.providers.propelauth import (
+from fastmcp.server.plugins.auth.propelauth.provider import (
PropelAuthProvider,
PropelAuthTokenIntrospectionOverrides,
)
@@ -318,7 +318,7 @@ class TestPropelAuthProviderIntegration:
real_httpx_client = httpx.AsyncClient
monkeypatch.setattr(
- "fastmcp.server.auth.providers.propelauth.httpx.AsyncClient",
+ "fastmcp.server.plugins.auth.propelauth.provider.httpx.AsyncClient",
DummyAsyncClient,
)
diff --git a/tests/server/auth/providers/test_scalekit.py b/tests/server/auth/providers/test_scalekit.py
index a47840682..47e4dd597 100644
--- a/tests/server/auth/providers/test_scalekit.py
+++ b/tests/server/auth/providers/test_scalekit.py
@@ -6,7 +6,7 @@ import pytest
from fastmcp import Client, FastMCP
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.server.auth.providers.scalekit import ScalekitProvider
+from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
from fastmcp.utilities.tests import HeadlessOAuth, run_server_async
@@ -202,7 +202,7 @@ class TestScalekitProviderIntegration:
real_httpx_client = httpx.AsyncClient
monkeypatch.setattr(
- "fastmcp.server.auth.providers.scalekit.httpx.AsyncClient",
+ "fastmcp.server.plugins.auth.scalekit.provider.httpx.AsyncClient",
DummyAsyncClient,
)
diff --git a/tests/server/auth/providers/test_supabase.py b/tests/server/auth/providers/test_supabase.py
index 1537bff04..8a1a490e5 100644
--- a/tests/server/auth/providers/test_supabase.py
+++ b/tests/server/auth/providers/test_supabase.py
@@ -8,7 +8,7 @@ import pytest
from fastmcp import Client, FastMCP
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.server.auth.providers.supabase import SupabaseProvider
+from fastmcp.server.plugins.auth.supabase.provider import SupabaseProvider
from fastmcp.utilities.tests import HeadlessOAuth, run_server_in_process
diff --git a/tests/server/auth/providers/test_workos.py b/tests/server/auth/providers/test_workos.py
index 2e87956c1..06c027b08 100644
--- a/tests/server/auth/providers/test_workos.py
+++ b/tests/server/auth/providers/test_workos.py
@@ -10,8 +10,8 @@ from pytest_httpx import HTTPXMock
from fastmcp import Client, FastMCP
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.server.auth.providers.jwt import JWTVerifier
-from fastmcp.server.auth.providers.workos import (
- AuthKitProvider,
+from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
+from fastmcp.server.plugins.auth.workos.provider import (
WorkOSProvider,
WorkOSTokenVerifier,
)
diff --git a/tests/server/plugins/test_auth_plugins.py b/tests/server/plugins/test_auth_plugins.py
index d208421fa..752febd75 100644
--- a/tests/server/plugins/test_auth_plugins.py
+++ b/tests/server/plugins/test_auth_plugins.py
@@ -10,53 +10,37 @@ from pydantic import ValidationError
from fastmcp import FastMCP
from fastmcp.server.auth.oidc_proxy import OIDCConfiguration
-from fastmcp.server.auth.providers.auth0 import Auth0Provider
-from fastmcp.server.auth.providers.aws import AWSCognitoProvider
-from fastmcp.server.auth.providers.azure import AzureProvider
-from fastmcp.server.auth.providers.clerk import ClerkProvider
-from fastmcp.server.auth.providers.descope import DescopeProvider
-from fastmcp.server.auth.providers.discord import DiscordProvider
-from fastmcp.server.auth.providers.github import GitHubProvider
-from fastmcp.server.auth.providers.google import GoogleProvider
from fastmcp.server.auth.providers.jwt import StaticTokenVerifier
-from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
-from fastmcp.server.auth.providers.oci import OCIProvider
-from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
-from fastmcp.server.auth.providers.scalekit import ScalekitProvider
-from fastmcp.server.auth.providers.supabase import SupabaseProvider
-from fastmcp.server.auth.providers.workos import AuthKitProvider, WorkOSProvider
-from fastmcp.server.plugins.auth import (
- Auth0Auth,
- Auth0AuthConfig,
- AuthKitAuth,
- AuthKitAuthConfig,
- AWSCognitoAuth,
- AWSCognitoAuthConfig,
- AzureAuth,
- AzureAuthConfig,
- ClerkAuth,
- ClerkAuthConfig,
- DescopeAuth,
- DescopeAuthConfig,
- DiscordAuth,
- DiscordAuthConfig,
- GitHubAuth,
- GitHubAuthConfig,
- GoogleAuth,
- GoogleAuthConfig,
- KeycloakAuth,
- KeycloakAuthConfig,
- OCIAuth,
- OCIAuthConfig,
- PropelAuth,
- PropelAuthConfig,
- ScalekitAuth,
- ScalekitAuthConfig,
- SupabaseAuth,
- SupabaseAuthConfig,
- WorkOSAuth,
- WorkOSAuthConfig,
-)
+from fastmcp.server.plugins.auth.auth0 import Auth0Auth
+from fastmcp.server.plugins.auth.auth0.provider import Auth0Provider
+from fastmcp.server.plugins.auth.authkit import AuthKitAuth
+from fastmcp.server.plugins.auth.authkit.provider import AuthKitProvider
+from fastmcp.server.plugins.auth.aws import AWSCognitoAuth
+from fastmcp.server.plugins.auth.aws.provider import AWSCognitoProvider
+from fastmcp.server.plugins.auth.azure import AzureAuth
+from fastmcp.server.plugins.auth.azure.provider import AzureProvider
+from fastmcp.server.plugins.auth.clerk import ClerkAuth
+from fastmcp.server.plugins.auth.clerk.provider import ClerkProvider
+from fastmcp.server.plugins.auth.descope import DescopeAuth
+from fastmcp.server.plugins.auth.descope.provider import DescopeProvider
+from fastmcp.server.plugins.auth.discord import DiscordAuth
+from fastmcp.server.plugins.auth.discord.provider import DiscordProvider
+from fastmcp.server.plugins.auth.github import GitHubAuth
+from fastmcp.server.plugins.auth.github.provider import GitHubProvider
+from fastmcp.server.plugins.auth.google import GoogleAuth
+from fastmcp.server.plugins.auth.google.provider import GoogleProvider
+from fastmcp.server.plugins.auth.keycloak import KeycloakAuth
+from fastmcp.server.plugins.auth.keycloak.provider import KeycloakAuthProvider
+from fastmcp.server.plugins.auth.oci import OCIAuth
+from fastmcp.server.plugins.auth.oci.provider import OCIProvider
+from fastmcp.server.plugins.auth.propelauth import PropelAuth
+from fastmcp.server.plugins.auth.propelauth.provider import PropelAuthProvider
+from fastmcp.server.plugins.auth.scalekit import ScalekitAuth
+from fastmcp.server.plugins.auth.scalekit.provider import ScalekitProvider
+from fastmcp.server.plugins.auth.supabase import SupabaseAuth
+from fastmcp.server.plugins.auth.supabase.provider import SupabaseProvider
+from fastmcp.server.plugins.auth.workos import WorkOSAuth
+from fastmcp.server.plugins.auth.workos.provider import WorkOSProvider
def _verifier() -> StaticTokenVerifier:
@@ -89,7 +73,7 @@ def _mock_oidc_discovery():
PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
(
Auth0Auth,
- Auth0AuthConfig,
+ Auth0Auth.Config,
{
"config_url": "https://idp.example.com/.well-known/openid-configuration",
"client_id": "client",
@@ -101,7 +85,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
AuthKitAuth,
- AuthKitAuthConfig,
+ AuthKitAuth.Config,
{
"authkit_domain": "https://example.authkit.app",
"base_url": "https://mcp.example.com",
@@ -110,7 +94,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
AWSCognitoAuth,
- AWSCognitoAuthConfig,
+ AWSCognitoAuth.Config,
{
"user_pool_id": "us-east-1_abc",
"client_id": "client",
@@ -122,7 +106,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
AzureAuth,
- AzureAuthConfig,
+ AzureAuth.Config,
{
"client_id": "client",
"client_secret": "secret",
@@ -134,7 +118,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
ClerkAuth,
- ClerkAuthConfig,
+ ClerkAuth.Config,
{
"domain": "example.clerk.accounts.dev",
"client_id": "client",
@@ -145,7 +129,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
DescopeAuth,
- DescopeAuthConfig,
+ DescopeAuth.Config,
{
"config_url": "https://api.descope.com/v1/apps/agentic/P123/M456/.well-known/openid-configuration",
"base_url": "https://mcp.example.com",
@@ -154,7 +138,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
DiscordAuth,
- DiscordAuthConfig,
+ DiscordAuth.Config,
{
"client_id": "client",
"client_secret": "secret",
@@ -164,7 +148,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
GitHubAuth,
- GitHubAuthConfig,
+ GitHubAuth.Config,
{
"client_id": "client",
"client_secret": "secret",
@@ -174,7 +158,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
GoogleAuth,
- GoogleAuthConfig,
+ GoogleAuth.Config,
{
"client_id": "client",
"client_secret": "secret",
@@ -184,7 +168,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
KeycloakAuth,
- KeycloakAuthConfig,
+ KeycloakAuth.Config,
{
"realm_url": "https://keycloak.example.com/realms/main",
"base_url": "https://mcp.example.com",
@@ -193,7 +177,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
OCIAuth,
- OCIAuthConfig,
+ OCIAuth.Config,
{
"config_url": "https://idp.example.com/.well-known/openid-configuration",
"client_id": "client",
@@ -204,7 +188,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
PropelAuth,
- PropelAuthConfig,
+ PropelAuth.Config,
{
"auth_url": "https://auth.example.com",
"introspection_client_id": "client",
@@ -215,7 +199,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
ScalekitAuth,
- ScalekitAuthConfig,
+ ScalekitAuth.Config,
{
"environment_url": "https://env.scalekit.com",
"resource_id": "res_123",
@@ -225,7 +209,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
SupabaseAuth,
- SupabaseAuthConfig,
+ SupabaseAuth.Config,
{
"project_url": "https://abc123.supabase.co",
"base_url": "https://mcp.example.com",
@@ -234,7 +218,7 @@ PROVIDER_CASES: list[tuple[type, type, dict[str, Any], type]] = [
),
(
WorkOSAuth,
- WorkOSAuthConfig,
+ WorkOSAuth.Config,
{
"client_id": "client",
"client_secret": "secret",
@@ -264,6 +248,7 @@ class TestAuthProviderPlugins:
)
def test_config_generic_binding(self, plugin_cls, config_cls, config, provider_cls):
assert plugin_cls._config_cls is config_cls
+ assert plugin_cls.Config is config_cls
@pytest.mark.parametrize(
("plugin_cls", "config_cls", "config", "provider_cls"), PROVIDER_CASES
@@ -318,7 +303,7 @@ class TestAuthProviderPlugins:
def test_supabase_passthroughs_config_and_python_verifier(self):
verifier = _verifier()
plugin = SupabaseAuth(
- SupabaseAuthConfig(
+ SupabaseAuth.Config(
project_url="https://abc123.supabase.co",
base_url="https://mcp.example.com",
required_scopes=["read"],
diff --git a/tests/server/plugins/test_code_mode_plugin.py b/tests/server/plugins/test_code_mode_plugin.py
index b06ba869f..5790a676d 100644
--- a/tests/server/plugins/test_code_mode_plugin.py
+++ b/tests/server/plugins/test_code_mode_plugin.py
@@ -34,6 +34,7 @@ class TestCodeModeConfig:
def test_config_generic_binding(self):
"""`Plugin[CodeModeConfig]` binds CodeModeConfig as the validated config type."""
assert CodeMode._config_cls is CodeModeConfig
+ assert CodeMode.Config is CodeModeConfig
def test_dict_config_accepted(self):
"""Dict config works for loading from JSON/YAML."""
@@ -42,11 +43,11 @@ class TestCodeModeConfig:
def test_unknown_sandbox_rejected(self):
with pytest.raises((ValidationError, Exception), match="sandbox"):
- CodeModeConfig(sandbox="docker") # ty: ignore[invalid-argument-type]
+ CodeMode.Config(sandbox="docker") # ty: ignore[invalid-argument-type]
def test_unknown_config_key_rejected(self):
with pytest.raises((ValidationError, Exception), match="forbid|extra"):
- CodeModeConfig(not_a_real_option=True) # ty: ignore[unknown-argument]
+ CodeMode.Config(not_a_real_option=True) # ty: ignore[unknown-argument]
def test_default_meta(self):
"""CodeMode uses Plugin's auto-derived meta: kebab-cased class
diff --git a/tests/server/plugins/test_openapi_plugin.py b/tests/server/plugins/test_openapi_plugin.py
index c07c36e3b..c2ab6ff3c 100644
--- a/tests/server/plugins/test_openapi_plugin.py
+++ b/tests/server/plugins/test_openapi_plugin.py
@@ -56,16 +56,17 @@ PETSTORE_SPEC: dict = {
class TestOpenAPIConfig:
def test_config_generic_binding(self):
assert OpenAPI._config_cls is OpenAPIConfig
+ assert OpenAPI.Config is OpenAPIConfig
def test_default_config_instantiable(self):
"""Defaults must pass the plugin framework's instantiate-with-no-args
contract. The spec/spec_path check fires at providers() time, not
at Config construction."""
- assert OpenAPIConfig() # must not raise
+ assert OpenAPI.Config() # must not raise
def test_unknown_config_key_rejected(self):
with pytest.raises((ValidationError, Exception), match="forbid|extra"):
- OpenAPIConfig(not_a_real_option=True) # ty: ignore[unknown-argument]
+ OpenAPI.Config(not_a_real_option=True) # ty: ignore[unknown-argument]
def test_meta_name_is_single_word(self):
"""'openapi' is one technical term — explicit meta override
@@ -76,7 +77,7 @@ class TestOpenAPIConfig:
class TestSpecLoading:
async def test_inline_spec_builds_provider(self):
- plugin = OpenAPI(OpenAPIConfig(spec=PETSTORE_SPEC))
+ plugin = OpenAPI(OpenAPI.Config(spec=PETSTORE_SPEC))
mcp = FastMCP("petstore", plugins=[plugin])
async with Client(mcp) as c:
@@ -89,7 +90,7 @@ class TestSpecLoading:
spec_file = tmp_path / "petstore.json"
spec_file.write_text(json.dumps(PETSTORE_SPEC))
- plugin = OpenAPI(OpenAPIConfig(spec_path=str(spec_file)))
+ plugin = OpenAPI(OpenAPI.Config(spec_path=str(spec_file)))
mcp = FastMCP("petstore", plugins=[plugin])
async with Client(mcp) as c:
@@ -113,12 +114,12 @@ class TestSpecLoading:
encoding="utf-8",
)
- plugin = OpenAPI(OpenAPIConfig(spec_path=str(spec_file)))
+ plugin = OpenAPI(OpenAPI.Config(spec_path=str(spec_file)))
providers = plugin.providers()
assert isinstance(providers[0], OpenAPIProvider)
def test_missing_spec_fails_at_build_time(self):
- plugin = OpenAPI(OpenAPIConfig())
+ plugin = OpenAPI(OpenAPI.Config())
with pytest.raises(ValueError, match="spec.*spec_path"):
plugin.providers()
@@ -126,7 +127,7 @@ class TestSpecLoading:
spec_file = tmp_path / "spec.json"
spec_file.write_text(json.dumps(PETSTORE_SPEC))
- plugin = OpenAPI(OpenAPIConfig(spec=PETSTORE_SPEC, spec_path=str(spec_file)))
+ plugin = OpenAPI(OpenAPI.Config(spec=PETSTORE_SPEC, spec_path=str(spec_file)))
with pytest.raises(ValueError, match="exactly one"):
plugin.providers()
@@ -134,7 +135,7 @@ class TestSpecLoading:
class TestRouteMapping:
def test_route_maps_dict_form_converts_to_typed(self):
plugin = OpenAPI(
- OpenAPIConfig(
+ OpenAPI.Config(
spec=PETSTORE_SPEC,
route_maps=[
RouteMapDict(
@@ -149,7 +150,7 @@ class TestRouteMapping:
async def test_list_pets_maps_to_resource_via_config(self):
plugin = OpenAPI(
- OpenAPIConfig(
+ OpenAPI.Config(
spec=PETSTORE_SPEC,
route_maps=[
RouteMapDict(
@@ -172,7 +173,7 @@ class TestRouteMapping:
the dict form in Config — advanced users shouldn't be shadowed
by an empty default."""
plugin = OpenAPI(
- OpenAPIConfig(spec=PETSTORE_SPEC),
+ OpenAPI.Config(spec=PETSTORE_SPEC),
route_maps=[RouteMap(mcp_type=MCPType.EXCLUDE, pattern=r".*")],
)
providers = plugin.providers()
@@ -186,7 +187,7 @@ class TestDefaultClient:
"""When the plugin builds its own httpx client (user didn't pass
`client=`), the provider's lifespan must still close it on
shutdown. A leaked client was bug noted on PR #4015."""
- plugin = OpenAPI(OpenAPIConfig(spec=PETSTORE_SPEC))
+ plugin = OpenAPI(OpenAPI.Config(spec=PETSTORE_SPEC))
provider = plugin.providers()[0]
assert isinstance(provider, OpenAPIProvider)
client = provider._client
@@ -210,7 +211,7 @@ class TestDefaultClient:
}
],
}
- plugin = OpenAPI(OpenAPIConfig(spec=templated_spec))
+ plugin = OpenAPI(OpenAPI.Config(spec=templated_spec))
provider = plugin.providers()[0]
assert isinstance(provider, OpenAPIProvider)
assert str(provider._client.base_url) == "https://us-east.api.example.com"
@@ -220,7 +221,7 @@ class TestEscapeHatches:
async def test_custom_client_is_used(self):
"""Passing `client=` bypasses the auto-derived httpx client."""
client = httpx.AsyncClient(base_url="https://override.example.com")
- plugin = OpenAPI(OpenAPIConfig(spec=PETSTORE_SPEC), client=client)
+ plugin = OpenAPI(OpenAPI.Config(spec=PETSTORE_SPEC), client=client)
providers = plugin.providers()
assert isinstance(providers[0], OpenAPIProvider)
# Access the provider's client through the known private attr.
diff --git a/tests/server/plugins/test_skills_plugin.py b/tests/server/plugins/test_skills_plugin.py
index c3a82b4dd..20d35e8be 100644
--- a/tests/server/plugins/test_skills_plugin.py
+++ b/tests/server/plugins/test_skills_plugin.py
@@ -24,15 +24,16 @@ from fastmcp.server.plugins.skills.skill_provider import SkillProvider
class TestSkillsConfig:
def test_config_generic_binding(self):
assert Skills._config_cls is SkillsConfig
+ assert Skills.Config is SkillsConfig
def test_default_config_instantiable(self):
"""Defaults must pass the plugin framework's instantiate-with-no-args
contract; the source check fires at providers() time."""
- assert SkillsConfig() # must not raise
+ assert Skills.Config() # must not raise
def test_unknown_config_key_rejected(self):
with pytest.raises((ValidationError, Exception), match="forbid|extra"):
- SkillsConfig(not_a_real_option=True) # ty: ignore[unknown-argument]
+ Skills.Config(not_a_real_option=True) # ty: ignore[unknown-argument]
def test_default_meta(self):
assert Skills.meta.name == "skills"
@@ -45,12 +46,12 @@ class TestSourceResolution:
skill.mkdir()
(skill / "SKILL.md").write_text("# My Skill")
- plugin = Skills(SkillsConfig(path=str(skill)))
+ plugin = Skills(Skills.Config(path=str(skill)))
providers = plugin.providers()
assert isinstance(providers[0], SkillProvider)
def test_directory_source_builds_directory_provider(self, tmp_path: Path):
- plugin = Skills(SkillsConfig(directory=str(tmp_path)))
+ plugin = Skills(Skills.Config(directory=str(tmp_path)))
providers = plugin.providers()
assert isinstance(providers[0], SkillsDirectoryProvider)
@@ -58,7 +59,7 @@ class TestSourceResolution:
a, b = tmp_path / "a", tmp_path / "b"
a.mkdir()
b.mkdir()
- plugin = Skills(SkillsConfig(directory=[str(a), str(b)]))
+ plugin = Skills(Skills.Config(directory=[str(a), str(b)]))
providers = plugin.providers()
assert isinstance(providers[0], SkillsDirectoryProvider)
@@ -66,17 +67,17 @@ class TestSourceResolution:
def test_vendor_presets_resolve_to_known_paths(self, vendor: str):
"""Every vendor string must produce a directory provider rooted
at the paths the old vendor subclass used to hardcode."""
- plugin = Skills(SkillsConfig(vendor=cast(Vendor, vendor)))
+ plugin = Skills(Skills.Config(vendor=cast(Vendor, vendor)))
providers = plugin.providers()
assert isinstance(providers[0], SkillsDirectoryProvider)
def test_no_source_fails_at_build_time(self):
- plugin = Skills(SkillsConfig())
+ plugin = Skills(Skills.Config())
with pytest.raises(ValueError, match="path.*directory.*vendor"):
plugin.providers()
def test_multiple_sources_rejected(self, tmp_path: Path):
- plugin = Skills(SkillsConfig(directory=str(tmp_path), vendor="claude"))
+ plugin = Skills(Skills.Config(directory=str(tmp_path), vendor="claude"))
with pytest.raises(ValueError, match="exactly one"):
plugin.providers()
diff --git a/tests/server/plugins/test_tool_search.py b/tests/server/plugins/test_tool_search.py
index f2a2990fa..193f7e59e 100644
--- a/tests/server/plugins/test_tool_search.py
+++ b/tests/server/plugins/test_tool_search.py
@@ -52,20 +52,20 @@ class TestSearchPluginRegistration:
assert names == {"search_tools", "call_tool"}
async def test_regex_strategy_dispatches_regex_transform(self):
- plugin = ToolSearch(ToolSearchConfig(strategy="regex"))
+ plugin = ToolSearch(ToolSearch.Config(strategy="regex"))
transforms = plugin.transforms()
assert len(transforms) == 1
assert isinstance(transforms[0], RegexSearchTransform)
async def test_bm25_strategy_dispatches_bm25_transform(self):
- plugin = ToolSearch(ToolSearchConfig(strategy="bm25"))
+ plugin = ToolSearch(ToolSearch.Config(strategy="bm25"))
transforms = plugin.transforms()
assert len(transforms) == 1
assert isinstance(transforms[0], BM25SearchTransform)
async def test_always_visible_pins_tools_alongside_search_call(self):
mcp = _make_server_with_tools(
- [ToolSearch(ToolSearchConfig(always_visible=["add"]))]
+ [ToolSearch(ToolSearch.Config(always_visible=["add"]))]
)
async with Client(mcp) as c:
@@ -78,7 +78,7 @@ class TestSearchPluginRegistration:
mcp = _make_server_with_tools(
[
ToolSearch(
- ToolSearchConfig(search_tool_name="find", call_tool_name="invoke")
+ ToolSearch.Config(search_tool_name="find", call_tool_name="invoke")
)
]
)
@@ -100,6 +100,7 @@ class TestSearchPluginRegistration:
async def test_search_binds_searchconfig_via_generic_parameter(self):
"""`Plugin[ToolSearchConfig]` makes ToolSearchConfig the validated config type."""
assert ToolSearch._config_cls is ToolSearchConfig
+ assert ToolSearch.Config is ToolSearchConfig
async def test_dict_config_still_accepted(self):
"""Dict config path (inherited from Plugin base) constructs cleanly —
@@ -119,11 +120,11 @@ class TestSearchPluginRegistration:
class TestSearchPluginConfigValidation:
def test_unknown_strategy_rejected(self):
with pytest.raises((ValidationError, Exception), match="strategy"):
- ToolSearchConfig(strategy="fuzzy") # ty: ignore[invalid-argument-type]
+ ToolSearch.Config(strategy="fuzzy") # ty: ignore[invalid-argument-type]
def test_unknown_config_key_rejected(self):
with pytest.raises((ValidationError, Exception), match="forbid|extra"):
- ToolSearchConfig(not_a_real_option=True) # ty: ignore[unknown-argument]
+ ToolSearch.Config(not_a_real_option=True) # ty: ignore[unknown-argument]
def test_default_meta_name_and_version(self):
"""ToolSearch relies on Plugin's auto-derived meta: kebab-cased
diff --git a/tests/server/test_plugins.py b/tests/server/test_plugins.py
index 7ffdb17a9..f53ef4d82 100644
--- a/tests/server/test_plugins.py
+++ b/tests/server/test_plugins.py
@@ -344,6 +344,7 @@ class TestPluginConstruction:
meta = PluginMeta(name="p", version="0.1.0")
assert P._config_cls is PConfig
+ assert P.Config is PConfig
def test_unparameterized_plugin_uses_empty_default_config(self):
"""A Plugin without a generic parameter gets an empty default that