From 3682b37861f6f13e9df05c94c001b12ad0ab84df Mon Sep 17 00:00:00 2001 From: Sean Houghton Date: Tue, 12 Aug 2025 09:20:24 -0700 Subject: [PATCH] fix: Always put the well-known endpoints at the server root # Bug Report: Incorrect `.well-known/oauth-protected-resource` endpoint path when `resource_server_url` ends with `/sse` ## Summary When configuring FastMCP with OAuth2 authentication and setting the `resource_server_url` to end with `/sse` (as required by VSCode MCP clients), FastMCP incorrectly serves the `.well-known/oauth-protected-resource` endpoint at `/sse/.well-known/oauth-protected-resource` instead of the expected root path `/.well-known/oauth-protected-resource`. ## Environment - **FastMCP version**: Part of `mcp` Python library (check with `pip show mcp`) - **Python version**: 3.13 - **Operating System**: macOS - **MCP Client**: VSCode with MCP extension - **Affected Files**: - `mcp/server/fastmcp/server.py` (lines ~790-797) - `mcp/server/auth/routes.py` (lines ~215-224) ## Expected Behavior 1. The `.well-known/oauth-protected-resource` endpoint should always be served at the root path (`/.well-known/oauth-protected-resource`) regardless of the `resource_server_url` configuration 2. The `resource` field in the `.well-known` response should point to the actual protected resource (e.g., `/sse`) 3. OAuth2 discovery should work correctly with MCP clients like VSCode ## Actual Behavior When `resource_server_url` is set to `http://localhost:8099/sse`, FastMCP: 1. Serves `.well-known/oauth-protected-resource` at `/sse/.well-known/oauth-protected-resource` 2. The SSE endpoint `/sse` returns a `www-authenticate` header with `resource_metadata="http://localhost:8099/sse/.well-known/oauth-protected-resource"` 3. OAuth2 discovery fails because clients expect the `.well-known` endpoint at the root ## Steps to Reproduce 1. Create a FastMCP server with OAuth2 configuration: ```python from mcp.server.fastmcp import FastMCP from mcp.server.auth.settings import AuthSettings from pydantic import AnyHttpUrl # Configure auth settings with /sse endpoint auth_settings = AuthSettings( issuer_url=AnyHttpUrl("https://login.microsoftonline.com/tenant-id/v2.0"), resource_server_url=AnyHttpUrl("http://localhost:8099/sse"), # Note: ends with /sse required_scopes=["https://example.com/scope"] ) mcp = FastMCP( "Test Server", token_verifier=your_token_verifier, auth=auth_settings, ) app = mcp.sse_app() ``` 2. Start the server: `uvicorn server:app --port 8099` 3. Test the endpoints: ```bash # This should work but returns 404 curl http://localhost:8099/.well-known/oauth-protected-resource # This works but shouldn't be the location curl http://localhost:8099/sse/.well-known/oauth-protected-resource # SSE endpoint references wrong .well-known location curl -I http://localhost:8099/sse # Returns: resource_metadata="http://localhost:8099/sse/.well-known/oauth-protected-resource" ``` ## Root Cause Analysis **Exact Location of Bug**: - **File**: `mcp/server/fastmcp/server.py` - **Lines**: ~790-797 in the `sse_app()` method - **Function**: `FastMCP.sse_app()` **The Issue**: When setting up OAuth2 authentication, FastMCP constructs the `resource_metadata_url` incorrectly: ```python # BUGGY CODE - Line ~790-797 in sse_app() method resource_metadata_url = AnyHttpUrl( str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource" ) ``` When `resource_server_url` is `http://localhost:8099/sse`, this creates `http://localhost:8099/sse/.well-known/oauth-protected-resource`. However, the actual `.well-known` endpoint is created by `create_protected_resource_routes()` (in `mcp/server/auth/routes.py` lines ~215-224), which always creates it at the root path: ```python # CORRECT CODE - This always creates /.well-known/oauth-protected-resource at root return [ Route( "/.well-known/oauth-protected-resource", endpoint=cors_middleware(handler.handle, ["GET", "OPTIONS"]), methods=["GET", "OPTIONS"], ) ] ``` **The Fix**: The `resource_metadata_url` should be constructed from the base URL, not the `resource_server_url`: ```python # PROPOSED FIX if self.settings.auth and self.settings.auth.resource_server_url: from pydantic import AnyHttpUrl from urllib.parse import urlparse # Extract base URL from resource_server_url parsed = urlparse(str(self.settings.auth.resource_server_url)) base_url = f"{parsed.scheme}://{parsed.netloc}" resource_metadata_url = AnyHttpUrl( base_url + "/.well-known/oauth-protected-resource" ) ``` ## Impact - **High**: Breaks OAuth2 discovery for MCP clients like VSCode - MCP servers cannot be properly authenticated when using the recommended `/sse` resource URL pattern - Workarounds require custom endpoint overrides, defeating the purpose of built-in auth support ## Proposed Solution The `.well-known/oauth-protected-resource` endpoint should always be served at the root path (`/.well-known/oauth-protected-resource`), regardless of the `resource_server_url` configuration. The `resource_server_url` should only affect: 1. The `resource` field value in the `.well-known` response 2. The `resource_metadata` reference in `www-authenticate` headers ## Current Workaround Override the built-in `.well-known` endpoint with a custom implementation: ```python async def custom_well_known_endpoint(request): return JSONResponse({ "resource": f"{config.EXTERNAL_ADDRESS}/sse", "authorization_servers": ["https://login.microsoftonline.com/tenant/v2.0"], "scopes_supported": ["https://example.com/scope"], "bearer_methods_supported": ["header"] }) # Override the built-in endpoint app.router.routes.insert(0, Route("/.well-known/oauth-protected-resource", custom_well_known_endpoint, methods=["GET"])) ``` ## Additional Context - This issue specifically affects integration with VSCode MCP clients, which require the resource URL to end with `/sse` - The OAuth2 specification (RFC 8414) defines `.well-known` endpoints should be at predictable root paths - Other OAuth2 implementations (e.g., Auth0, Okta) serve `.well-known` endpoints at root regardless of resource configuration ## Related Documentation - [RFC 8414 - OAuth 2.0 Authorization Server Metadata](https://tools.ietf.org/html/rfc8414) - [MCP OAuth2 Authentication Documentation](https://modelcontextprotocol.io/docs/concepts/authentication) --- src/mcp/server/fastmcp/server.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 924baaa9b..6ec8ea30a 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -792,11 +792,15 @@ async def handle_sse(scope: Scope, receive: Receive, send: Send): resource_metadata_url = None if self.settings.auth and self.settings.auth.resource_server_url: from pydantic import AnyHttpUrl - + from urllib.parse import urlparse + + # Extract base URL from resource_server_url + parsed = urlparse(str(self.settings.auth.resource_server_url)) + base_url = f"{parsed.scheme}://{parsed.netloc}" + resource_metadata_url = AnyHttpUrl( - str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource" + base_url + "/.well-known/oauth-protected-resource" ) - # Auth is enabled, wrap the endpoints with RequireAuthMiddleware routes.append( Route(