From d9fe3733585927054c053aa6e09e0ce3fc3a9779 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:12:59 -0400 Subject: [PATCH] Fix SSE endpoint accepting POST requests instead of returning 405 (#1953) --- src/fastmcp/server/auth/auth.py | 44 +++++-------------- src/fastmcp/server/auth/oauth_proxy.py | 5 +-- src/fastmcp/server/auth/providers/descope.py | 7 +-- src/fastmcp/server/auth/providers/scalekit.py | 7 +-- src/fastmcp/server/auth/providers/workos.py | 7 +-- src/fastmcp/server/http.py | 43 ++++++++++++------ 6 files changed, 48 insertions(+), 65 deletions(-) diff --git a/src/fastmcp/server/auth/auth.py b/src/fastmcp/server/auth/auth.py index e67928651..de545c7c2 100644 --- a/src/fastmcp/server/auth/auth.py +++ b/src/fastmcp/server/auth/auth.py @@ -3,10 +3,7 @@ from __future__ import annotations from typing import Any from mcp.server.auth.middleware.auth_context import AuthContextMiddleware -from mcp.server.auth.middleware.bearer_auth import ( - BearerAuthBackend, - RequireAuthMiddleware, -) +from mcp.server.auth.middleware.bearer_auth import BearerAuthBackend from mcp.server.auth.provider import ( AccessToken as _SDKAccessToken, ) @@ -81,7 +78,6 @@ class AuthProvider(TokenVerifierProtocol): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get the routes for this authentication provider. @@ -93,30 +89,13 @@ class AuthProvider(TokenVerifierProtocol): Args: mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp") - mcp_endpoint: The MCP endpoint handler to protect with auth + This is used to advertise the resource URL in metadata, but the + provider does not create the actual MCP endpoint route. Returns: - List of routes for this provider, including protected MCP endpoints if provided + List of routes for this provider (excluding the MCP endpoint itself) """ - - routes = [] - - # Add protected MCP endpoint if provided - if mcp_path and mcp_endpoint: - resource_metadata_url = self._get_resource_url( - "/.well-known/oauth-protected-resource" - ) - - routes.append( - Route( - mcp_path, - endpoint=RequireAuthMiddleware( - mcp_endpoint, self.required_scopes, resource_metadata_url - ), - ) - ) - - return routes + return [] def get_middleware(self) -> list: """Get HTTP application-level middleware for this auth provider. @@ -225,14 +204,13 @@ class RemoteAuthProvider(AuthProvider): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth routes for this provider. - Creates protected resource metadata routes and optionally wraps MCP endpoints with auth. + Creates protected resource metadata routes. """ - # Start with base routes (protected MCP endpoint) - routes = super().get_routes(mcp_path, mcp_endpoint) + # Start with base routes + routes = super().get_routes(mcp_path) # Get the resource URL based on the MCP path resource_url = self._get_resource_url(mcp_path) @@ -326,14 +304,12 @@ class OAuthProvider( def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth authorization server routes and optional protected resource routes. This method creates the full set of OAuth routes including: - Standard OAuth authorization server routes (/.well-known/oauth-authorization-server, /authorize, /token, etc.) - Optional protected resource routes - - Protected MCP endpoints if provided Returns: List of OAuth routes @@ -366,7 +342,7 @@ class OAuthProvider( ) oauth_routes.extend(protected_routes) - # Add protected MCP endpoint from base class - oauth_routes.extend(super().get_routes(mcp_path, mcp_endpoint)) + # Add base routes + oauth_routes.extend(super().get_routes(mcp_path)) return oauth_routes diff --git a/src/fastmcp/server/auth/oauth_proxy.py b/src/fastmcp/server/auth/oauth_proxy.py index 5d0dd217d..97f5e7526 100644 --- a/src/fastmcp/server/auth/oauth_proxy.py +++ b/src/fastmcp/server/auth/oauth_proxy.py @@ -873,7 +873,6 @@ class OAuthProxy(OAuthProvider): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth routes with custom proxy token handler. @@ -882,10 +881,10 @@ class OAuthProxy(OAuthProvider): Args: mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp") - mcp_endpoint: The MCP endpoint handler to protect with auth + This is used to advertise the resource URL in metadata. """ # Get standard OAuth routes from parent class - routes = super().get_routes(mcp_path, mcp_endpoint) + routes = super().get_routes(mcp_path) custom_routes = [] token_route_found = False diff --git a/src/fastmcp/server/auth/providers/descope.py b/src/fastmcp/server/auth/providers/descope.py index 1195cdfb9..03ae1d007 100644 --- a/src/fastmcp/server/auth/providers/descope.py +++ b/src/fastmcp/server/auth/providers/descope.py @@ -7,8 +7,6 @@ for seamless MCP client authentication. from __future__ import annotations -from typing import Any - import httpx from pydantic import AnyHttpUrl from pydantic_settings import BaseSettings, SettingsConfigDict @@ -127,7 +125,6 @@ class DescopeProvider(RemoteAuthProvider): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth routes including Descope authorization server metadata forwarding. @@ -136,10 +133,10 @@ class DescopeProvider(RemoteAuthProvider): Args: mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp") - mcp_endpoint: The MCP endpoint handler to protect with auth + This is used to advertise the resource URL in metadata. """ # Get the standard protected resource routes from RemoteAuthProvider - routes = super().get_routes(mcp_path, mcp_endpoint) + routes = super().get_routes(mcp_path) async def oauth_authorization_server_metadata(request): """Forward Descope OAuth authorization server metadata with FastMCP customizations.""" diff --git a/src/fastmcp/server/auth/providers/scalekit.py b/src/fastmcp/server/auth/providers/scalekit.py index 8c6a0a79d..a0c568515 100644 --- a/src/fastmcp/server/auth/providers/scalekit.py +++ b/src/fastmcp/server/auth/providers/scalekit.py @@ -7,8 +7,6 @@ authentication for seamless MCP client authentication. from __future__ import annotations -from typing import Any - import httpx from pydantic import AnyHttpUrl from pydantic_settings import BaseSettings, SettingsConfigDict @@ -135,7 +133,6 @@ class ScalekitProvider(RemoteAuthProvider): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth routes including Scalekit authorization server metadata forwarding. @@ -144,10 +141,10 @@ class ScalekitProvider(RemoteAuthProvider): Args: mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp") - mcp_endpoint: The MCP endpoint handler to protect with auth + This is used to advertise the resource URL in metadata. """ # Get the standard protected resource routes from RemoteAuthProvider - routes = super().get_routes(mcp_path, mcp_endpoint) + routes = super().get_routes(mcp_path) async def oauth_authorization_server_metadata(request): """Forward Scalekit OAuth authorization server metadata with FastMCP customizations.""" diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py index d68e16c71..b0c5f1817 100644 --- a/src/fastmcp/server/auth/providers/workos.py +++ b/src/fastmcp/server/auth/providers/workos.py @@ -10,8 +10,6 @@ Choose based on your WorkOS setup and authentication requirements. from __future__ import annotations -from typing import Any - import httpx from pydantic import AnyHttpUrl, SecretStr, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -364,7 +362,6 @@ class AuthKitProvider(RemoteAuthProvider): def get_routes( self, mcp_path: str | None = None, - mcp_endpoint: Any | None = None, ) -> list[Route]: """Get OAuth routes including AuthKit authorization server metadata forwarding. @@ -373,10 +370,10 @@ class AuthKitProvider(RemoteAuthProvider): Args: mcp_path: The path where the MCP endpoint is mounted (e.g., "/mcp") - mcp_endpoint: The MCP endpoint handler to protect with auth + This is used to advertise the resource URL in metadata. """ # Get the standard protected resource routes from RemoteAuthProvider - routes = super().get_routes(mcp_path, mcp_endpoint) + routes = super().get_routes(mcp_path) async def oauth_authorization_server_metadata(request): """Forward AuthKit OAuth authorization server metadata with FastMCP customizations.""" diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index 21271129a..a5e41daf6 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -167,16 +167,25 @@ def create_sse_app( # Get auth middleware from the provider auth_middleware = auth.get_middleware() - # Get auth routes including protected MCP endpoint - auth_routes = auth.get_routes( - mcp_path=sse_path, - mcp_endpoint=handle_sse, - ) - + # Get auth provider's own routes (OAuth endpoints, metadata, etc) + auth_routes = auth.get_routes(mcp_path=sse_path) server_routes.extend(auth_routes) server_middleware.extend(auth_middleware) - # Manually wrap the SSE message endpoint with RequireAuthMiddleware + # Create protected SSE endpoint route with GET method only + server_routes.append( + Route( + sse_path, + endpoint=RequireAuthMiddleware( + handle_sse, + auth.required_scopes, + auth._get_resource_url("/.well-known/oauth-protected-resource"), + ), + methods=["GET"], + ) + ) + + # Wrap the SSE message endpoint with RequireAuthMiddleware server_routes.append( Mount( message_path, @@ -274,14 +283,22 @@ def create_streamable_http_app( # Get auth middleware from the provider auth_middleware = auth.get_middleware() - # Get auth routes including protected MCP endpoint - auth_routes = auth.get_routes( - mcp_path=streamable_http_path, - mcp_endpoint=streamable_http_app, - ) - + # Get auth provider's own routes (OAuth endpoints, metadata, etc) + auth_routes = auth.get_routes(mcp_path=streamable_http_path) server_routes.extend(auth_routes) server_middleware.extend(auth_middleware) + + # Create protected HTTP endpoint route + server_routes.append( + Route( + streamable_http_path, + endpoint=RequireAuthMiddleware( + streamable_http_app, + auth.required_scopes, + auth._get_resource_url("/.well-known/oauth-protected-resource"), + ), + ) + ) else: # No auth required server_routes.append(