diff --git a/docs/docs.json b/docs/docs.json
index 76488b529..e8af19b5e 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -359,6 +359,7 @@
"python-sdk/fastmcp-server-auth-providers-bearer",
"python-sdk/fastmcp-server-auth-providers-debug",
"python-sdk/fastmcp-server-auth-providers-descope",
+ "python-sdk/fastmcp-server-auth-providers-discord",
"python-sdk/fastmcp-server-auth-providers-github",
"python-sdk/fastmcp-server-auth-providers-google",
"python-sdk/fastmcp-server-auth-providers-in_memory",
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
index 96d81714d..3aa245abb 100644
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx
@@ -26,17 +26,23 @@ production use with enterprise identity providers.
## Functions
-### `create_consent_html`
+### `create_consent_html`
```python
-create_consent_html(client_id: str, redirect_uri: str, scopes: list[str], txn_id: str, csrf_token: str, client_name: str | None = None, title: str = 'Application Access Request', server_name: str | None = None, server_icon_url: str | None = None, server_website_url: str | None = None, client_website_url: str | None = None) -> str
+create_consent_html(client_id: str, redirect_uri: str, scopes: list[str], txn_id: str, csrf_token: str, client_name: str | None = None, title: str = 'Application Access Request', server_name: str | None = None, server_icon_url: str | None = None, server_website_url: str | None = None, client_website_url: str | None = None, csp_policy: str | None = None) -> str
```
Create a styled HTML consent page for OAuth authorization requests.
+**Args:**
+- `csp_policy`: Content Security Policy override.
+If None, uses the built-in CSP policy with appropriate directives.
+If empty string "", disables CSP entirely (no meta tag is rendered).
+If a non-empty string, uses that as the CSP policy value.
-### `create_error_html`
+
+### `create_error_html`
```python
create_error_html(error_title: str, error_message: str, error_details: dict[str, str] | None = None, server_name: str | None = None, server_icon_url: str | None = None) -> str
@@ -95,7 +101,16 @@ This allows stateless JWT validation while still being able to look up
the corresponding upstream token when tools need to access upstream APIs.
-### `ProxyDCRClient`
+### `RefreshTokenMetadata`
+
+
+Metadata for a refresh token, stored keyed by token hash.
+
+We store only metadata (not the token itself) for security - if storage
+is compromised, attackers get hashes they can't reverse into usable tokens.
+
+
+### `ProxyDCRClient`
Client for DCR proxy with configurable redirect URI validation.
@@ -125,7 +140,7 @@ arise from accepting arbitrary redirect URIs.
**Methods:**
-#### `validate_redirect_uri`
+#### `validate_redirect_uri`
```python
validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl
@@ -139,7 +154,7 @@ This is essential for cached token scenarios where the client may
reconnect with a different port.
-### `TokenHandler`
+### `TokenHandler`
TokenHandler that returns OAuth 2.1 compliant error responses.
@@ -162,7 +177,7 @@ Per MCP spec: "Invalid or expired tokens MUST receive a HTTP 401 response."
**Methods:**
-#### `response`
+#### `response`
```python
response(self, obj: TokenSuccessResponse | TokenErrorResponse)
@@ -171,7 +186,7 @@ response(self, obj: TokenSuccessResponse | TokenErrorResponse)
Override response method to provide OAuth 2.1 compliant error handling.
-### `OAuthProxy`
+### `OAuthProxy`
OAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.
@@ -251,14 +266,18 @@ OAuth Flow Implementation
State Management
---------------
-The proxy maintains minimal but crucial state:
+The proxy maintains minimal but crucial state via pluggable storage (client_storage):
- _oauth_transactions: Active authorization flows with client context
- _client_codes: Authorization codes with PKCE challenges and upstream tokens
-- _access_tokens, _refresh_tokens: Token storage for revocation
-- Token relationship mappings for cleanup and rotation
+- _jti_mapping_store: Maps FastMCP token JTIs to upstream token IDs
+- _refresh_token_store: Refresh token metadata (keyed by token hash)
+
+All state is stored in the configured client_storage backend (Redis, disk, etc.)
+enabling horizontal scaling across multiple instances.
Security Considerations
----------------------
+- Refresh tokens stored by hash only (defense in depth if storage compromised)
- PKCE enforced end-to-end (client to proxy, proxy to upstream)
- Authorization codes are single-use with short expiry
- Transaction IDs are cryptographically random
@@ -281,7 +300,7 @@ Handles provider-specific requirements:
**Methods:**
-#### `get_client`
+#### `get_client`
```python
get_client(self, client_id: str) -> OAuthClientInformationFull | None
@@ -293,7 +312,7 @@ provided to the DCR client during registration, not the upstream client ID.
For unregistered clients, returns None (which will raise an error in the SDK).
-#### `register_client`
+#### `register_client`
```python
register_client(self, client_info: OAuthClientInformationFull) -> None
@@ -307,7 +326,7 @@ redirect URI will likely be localhost or unknown to the proxied IDP. The
proxied IDP only knows about this server's fixed redirect URI.
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@@ -324,7 +343,7 @@ If consent is disabled (require_authorization_consent=False), skip the consent s
and redirect directly to the upstream IdP.
-#### `load_authorization_code`
+#### `load_authorization_code`
```python
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
@@ -336,7 +355,7 @@ Look up our client code and return authorization code object
with PKCE challenge for validation.
-#### `exchange_authorization_code`
+#### `exchange_authorization_code`
```python
exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
@@ -354,16 +373,19 @@ Implements the token factory pattern:
PKCE validation is handled by the MCP framework before this method is called.
-#### `load_refresh_token`
+#### `load_refresh_token`
```python
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
```
-Load refresh token from local storage.
+Load refresh token metadata from distributed storage.
+
+Looks up by token hash and reconstructs the RefreshToken object.
+Validates that the token belongs to the requesting client.
-#### `exchange_refresh_token`
+#### `exchange_refresh_token`
```python
exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
@@ -380,7 +402,7 @@ Implements two-tier refresh:
6. Keep same FastMCP refresh token (unless upstream rotates)
-#### `load_access_token`
+#### `load_access_token`
```python
load_access_token(self, token: str) -> AccessToken | None
@@ -399,7 +421,7 @@ The FastMCP JWT is a reference token - all authorization data comes
from validating the upstream token via the TokenVerifier.
-#### `revoke_token`
+#### `revoke_token`
```python
revoke_token(self, token: AccessToken | RefreshToken) -> None
@@ -407,11 +429,12 @@ revoke_token(self, token: AccessToken | RefreshToken) -> None
Revoke token locally and with upstream server if supported.
-Removes tokens from local storage and attempts to revoke them with
-the upstream server if a revocation endpoint is configured.
+For refresh tokens, removes from local storage by hash.
+For all tokens, attempts upstream revocation if endpoint is configured.
+Access token JTI mappings expire via TTL.
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
index 573aea7bb..184bc82af 100644
--- a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
@@ -52,7 +52,7 @@ that is OIDC compliant.
**Methods:**
-#### `get_oidc_configuration`
+#### `get_oidc_configuration`
```python
get_oidc_configuration(self, config_url: AnyHttpUrl, strict: bool | None, timeout_seconds: int | None) -> OIDCConfiguration
@@ -66,7 +66,7 @@ Gets the OIDC configuration for the specified configuration URL.
- `timeout_seconds`: HTTP request timeout in seconds
-#### `get_token_verifier`
+#### `get_token_verifier`
```python
get_token_verifier(self) -> TokenVerifier
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
index b0474eb58..5fb265b32 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
@@ -14,13 +14,13 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
## Classes
-### `AzureProviderSettings`
+### `AzureProviderSettings`
Settings for Azure OAuth provider.
-### `AzureProvider`
+### `AzureProvider`
Azure (Microsoft Entra) OAuth provider for FastMCP.
@@ -55,7 +55,7 @@ Setup:
**Methods:**
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-discord.mdx b/docs/python-sdk/fastmcp-server-auth-providers-discord.mdx
new file mode 100644
index 000000000..031adaae9
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-auth-providers-discord.mdx
@@ -0,0 +1,72 @@
+---
+title: discord
+sidebarTitle: discord
+---
+
+# `fastmcp.server.auth.providers.discord`
+
+
+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)
+ ```
+
+
+## Classes
+
+### `DiscordProviderSettings`
+
+
+Settings for Discord OAuth provider.
+
+
+### `DiscordTokenVerifier`
+
+
+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.
+
+
+**Methods:**
+
+#### `verify_token`
+
+```python
+verify_token(self, token: str) -> AccessToken | None
+```
+
+Verify Discord OAuth token by calling Discord's tokeninfo API.
+
+
+### `DiscordProvider`
+
+
+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
+
diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx
index 7db6f2f0a..9092633d9 100644
--- a/docs/python-sdk/fastmcp-server-dependencies.mdx
+++ b/docs/python-sdk/fastmcp-server-dependencies.mdx
@@ -7,19 +7,19 @@ sidebarTitle: dependencies
## Functions
-### `get_context`
+### `get_context`
```python
get_context() -> Context
```
-### `get_http_request`
+### `get_http_request`
```python
get_http_request() -> Request
```
-### `get_http_headers`
+### `get_http_headers`
```python
get_http_headers(include_all: bool = False) -> dict[str, str]
@@ -35,7 +35,7 @@ By default, strips problematic headers like `content-length` that cause issues i
If `include_all` is True, all headers are returned.
-### `get_access_token`
+### `get_access_token`
```python
get_access_token() -> AccessToken | None
@@ -44,6 +44,11 @@ get_access_token() -> AccessToken | None
Get the FastMCP access token from the current context.
+This function first tries to get the token from the current HTTP request's scope,
+which is more reliable for long-lived connections where the SDK's auth_context_var
+may become stale after token refresh. Falls back to the SDK's context var if no
+request is available.
+
**Returns:**
- The access token if an authenticated user is available, None otherwise.
diff --git a/docs/python-sdk/fastmcp-server-proxy.mdx b/docs/python-sdk/fastmcp-server-proxy.mdx
index a64acca64..445fc1a68 100644
--- a/docs/python-sdk/fastmcp-server-proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-proxy.mdx
@@ -7,7 +7,7 @@ sidebarTitle: proxy
## Functions
-### `default_proxy_roots_handler`
+### `default_proxy_roots_handler`
```python
default_proxy_roots_handler(context: RequestContext[ClientSession, LifespanContextT]) -> RootsList
@@ -165,7 +165,7 @@ from_mcp_tool(cls, client: Client, mcp_tool: mcp.types.Tool) -> ProxyTool
Factory method to create a ProxyTool from a raw MCP tool schema.
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResult
@@ -174,7 +174,7 @@ run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResu
Executes the tool by making a call through the client.
-### `ProxyResource`
+### `ProxyResource`
A Resource that represents and reads a resource from a remote server.
@@ -182,7 +182,7 @@ A Resource that represents and reads a resource from a remote server.
**Methods:**
-#### `from_mcp_resource`
+#### `from_mcp_resource`
```python
from_mcp_resource(cls, client: Client, mcp_resource: mcp.types.Resource) -> ProxyResource
@@ -191,7 +191,7 @@ from_mcp_resource(cls, client: Client, mcp_resource: mcp.types.Resource) -> Prox
Factory method to create a ProxyResource from a raw MCP resource schema.
-#### `read`
+#### `read`
```python
read(self) -> str | bytes
@@ -200,7 +200,7 @@ read(self) -> str | bytes
Read the resource content from the remote server.
-### `ProxyTemplate`
+### `ProxyTemplate`
A ResourceTemplate that represents and creates resources from a remote server template.
@@ -208,7 +208,7 @@ A ResourceTemplate that represents and creates resources from a remote server te
**Methods:**
-#### `from_mcp_template`
+#### `from_mcp_template`
```python
from_mcp_template(cls, client: Client, mcp_template: mcp.types.ResourceTemplate) -> ProxyTemplate
@@ -217,7 +217,7 @@ from_mcp_template(cls, client: Client, mcp_template: mcp.types.ResourceTemplate)
Factory method to create a ProxyTemplate from a raw MCP template schema.
-#### `create_resource`
+#### `create_resource`
```python
create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> ProxyResource
@@ -226,7 +226,7 @@ create_resource(self, uri: str, params: dict[str, Any], context: Context | None
Create a resource from the template by calling the remote server.
-### `ProxyPrompt`
+### `ProxyPrompt`
A Prompt that represents and renders a prompt from a remote server.
@@ -234,7 +234,7 @@ A Prompt that represents and renders a prompt from a remote server.
**Methods:**
-#### `from_mcp_prompt`
+#### `from_mcp_prompt`
```python
from_mcp_prompt(cls, client: Client, mcp_prompt: mcp.types.Prompt) -> ProxyPrompt
@@ -243,7 +243,7 @@ from_mcp_prompt(cls, client: Client, mcp_prompt: mcp.types.Prompt) -> ProxyPromp
Factory method to create a ProxyPrompt from a raw MCP prompt schema.
-#### `render`
+#### `render`
```python
render(self, arguments: dict[str, Any]) -> list[PromptMessage]
@@ -252,14 +252,14 @@ render(self, arguments: dict[str, Any]) -> list[PromptMessage]
Render the prompt by making a call through the client.
-### `FastMCPProxy`
+### `FastMCPProxy`
A FastMCP server that acts as a proxy to a remote MCP-compliant server.
It uses specialized managers that fulfill requests via a client factory.
-### `ProxyClient`
+### `ProxyClient`
A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.
@@ -268,7 +268,7 @@ Supports forwarding roots, sampling, elicitation, logging, and progress.
**Methods:**
-#### `default_sampling_handler`
+#### `default_sampling_handler`
```python
default_sampling_handler(cls, messages: list[mcp.types.SamplingMessage], params: mcp.types.CreateMessageRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> mcp.types.CreateMessageResult
@@ -277,7 +277,7 @@ default_sampling_handler(cls, messages: list[mcp.types.SamplingMessage], params:
A handler that forwards the sampling request from the remote server to the proxy's connected clients and relays the response back to the remote server.
-#### `default_elicitation_handler`
+#### `default_elicitation_handler`
```python
default_elicitation_handler(cls, message: str, response_type: type, params: mcp.types.ElicitRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> ElicitResult
@@ -286,7 +286,7 @@ default_elicitation_handler(cls, message: str, response_type: type, params: mcp.
A handler that forwards the elicitation request from the remote server to the proxy's connected clients and relays the response back to the remote server.
-#### `default_log_handler`
+#### `default_log_handler`
```python
default_log_handler(cls, message: LogMessage) -> None
@@ -295,7 +295,7 @@ default_log_handler(cls, message: LogMessage) -> None
A handler that forwards the log notification from the remote server to the proxy's connected clients.
-#### `default_progress_handler`
+#### `default_progress_handler`
```python
default_progress_handler(cls, progress: float, total: float | None, message: str | None) -> None
@@ -304,7 +304,7 @@ default_progress_handler(cls, progress: float, total: float | None, message: str
A handler that forwards the progress notification from the remote server to the proxy's connected clients.
-### `StatefulProxyClient`
+### `StatefulProxyClient`
A proxy client that provides a stateful client factory for the proxy server.
@@ -318,7 +318,7 @@ Note that it is essential to ensure that the proxy server itself is also statefu
**Methods:**
-#### `clear`
+#### `clear`
```python
clear(self)
@@ -327,7 +327,7 @@ clear(self)
Clear all cached clients and force disconnect them.
-#### `new_stateful`
+#### `new_stateful`
```python
new_stateful(self) -> Client[ClientTransportT]
diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
index c9c416543..bccf670d4 100644
--- a/docs/python-sdk/fastmcp-tools-tool_transform.mdx
+++ b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
@@ -7,7 +7,7 @@ sidebarTitle: tool_transform
## Functions
-### `forward`
+### `forward`
```python
forward(**kwargs: Any) -> ToolResult
@@ -36,7 +36,7 @@ tool has args `a` and `b`, and an `transform_args` was provided that maps `x` to
- `TypeError`: If provided arguments don't match the transformed schema.
-### `forward_raw`
+### `forward_raw`
```python
forward_raw(**kwargs: Any) -> ToolResult
@@ -62,7 +62,7 @@ y=2)` will call the parent tool with `x=1` and `y=2`.
- `RuntimeError`: If called outside a transformed tool context.
-### `apply_transformations_to_tools`
+### `apply_transformations_to_tools`
```python
apply_transformations_to_tools(tools: dict[str, Tool], transformations: dict[str, ToolTransformConfig]) -> dict[str, Tool]
@@ -75,7 +75,7 @@ are left unchanged.
## Classes
-### `ArgTransform`
+### `ArgTransform`
Configuration for transforming a parent tool's argument.
@@ -137,7 +137,7 @@ ArgTransform(name="new_name", description="New desc", default=None, type=int)
```
-### `ArgTransformConfig`
+### `ArgTransformConfig`
A model for requesting a single argument transform.
@@ -145,7 +145,7 @@ A model for requesting a single argument transform.
**Methods:**
-#### `to_arg_transform`
+#### `to_arg_transform`
```python
to_arg_transform(self) -> ArgTransform
@@ -154,7 +154,7 @@ to_arg_transform(self) -> ArgTransform
Convert the argument transform to a FastMCP argument transform.
-### `TransformedTool`
+### `TransformedTool`
A tool that is transformed from another tool.
@@ -171,7 +171,7 @@ inherited from the parent tool but can be overridden or disabled.
**Methods:**
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -190,7 +190,7 @@ functions.
- ToolResult object containing content and optional structured output.
-#### `from_tool`
+#### `from_tool`
```python
from_tool(cls, tool: Tool, name: str | None = None, title: str | NotSetT | None = NotSet, description: str | NotSetT | None = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | NotSetT | None = NotSet, output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, serializer: Callable[[Any], str] | NotSetT | None = NotSet, meta: dict[str, Any] | NotSetT | None = NotSet, enabled: bool | None = None) -> TransformedTool
@@ -272,7 +272,7 @@ async def custom_output(**kwargs) -> ToolResult:
```
-### `ToolTransformConfig`
+### `ToolTransformConfig`
Provides a way to transform a tool.
@@ -280,7 +280,7 @@ Provides a way to transform a tool.
**Methods:**
-#### `apply`
+#### `apply`
```python
apply(self, tool: Tool) -> TransformedTool
diff --git a/docs/python-sdk/fastmcp-utilities-ui.mdx b/docs/python-sdk/fastmcp-utilities-ui.mdx
index 09f5a425c..5066eab5e 100644
--- a/docs/python-sdk/fastmcp-utilities-ui.mdx
+++ b/docs/python-sdk/fastmcp-utilities-ui.mdx
@@ -28,13 +28,14 @@ Create a complete HTML page with FastMCP styling.
- `content`: HTML content to place inside the page
- `title`: Page title
- `additional_styles`: Extra CSS to include
-- `csp_policy`: Content Security Policy header value
+- `csp_policy`: Content Security Policy header value.
+If empty string "", the CSP meta tag is omitted entirely.
**Returns:**
- Complete HTML page as string
-### `create_logo`
+### `create_logo`
```python
create_logo(icon_url: str | None = None, alt_text: str = 'FastMCP') -> str
@@ -51,7 +52,7 @@ Create logo HTML.
- HTML for logo image tag.
-### `create_status_message`
+### `create_status_message`
```python
create_status_message(message: str, is_success: bool = True) -> str
@@ -68,7 +69,7 @@ Create a status message with icon.
- HTML for status message
-### `create_info_box`
+### `create_info_box`
```python
create_info_box(content: str, is_error: bool = False, centered: bool = False, monospace: bool = False) -> str
@@ -87,7 +88,7 @@ Create an info box.
- HTML for info box
-### `create_detail_box`
+### `create_detail_box`
```python
create_detail_box(rows: list[tuple[str, str]]) -> str
@@ -103,7 +104,7 @@ Create a detail box with key-value pairs.
- HTML for detail box
-### `create_button_group`
+### `create_button_group`
```python
create_button_group(buttons: list[tuple[str, str, str]]) -> str
@@ -119,7 +120,7 @@ Create a group of buttons.
- HTML for button group
-### `create_secure_html_response`
+### `create_secure_html_response`
```python
create_secure_html_response(html: str, status_code: int = 200) -> HTMLResponse