fix: Always put the well-known endpoints at the server root#1288
fix: Always put the well-known endpoints at the server root#1288seanhoughton wants to merge 1 commit intomodelcontextprotocol:mainfrom
Conversation
# 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)
|
Can we speed up reviewing current PR. I ran into the same issue with vscode and solved by using your branch. Besides, the sample code will also need to be modified. ( ) |
|
Question... The listed Expected Behavior in this pull request states: "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". That seems the exact opposite of what would be needed in use cases where the MCP server must run at a custom path. When attached to a GKE gateway + HttpRoute (or api management, such as Apigee or APIM), serving the well-known endpoints from the root path is always going to return 404 responses. |
|
Where did you get Actually I have opposite problem: |
|
I agree that if someone is hosting an mcp endpoint somewhere deep into a path structure that this PR will not work. I suggest being able to explicitly configure the protected resource path. Currently this library is not compatible with VSCode's protected resource resolution. You must supply an "/sse" endpoint in the mcp configuration and then this library will host the well known endpoint at "/sse/.well-known" which is not what VSCode requests. If you feel this is a bug in VSCode then we can close the PR and go on our way and people cause use the workaround I provided. Is there anything in the standard that clarifies these paths for auth? Is VSCode supposed to require you to include the "/sse" portion of the URL? |
|
This change moves things closer to the RFC, so generally supportive. If you could add a test, then we can merge this. There's a draft PR to the spec to clarify the preference order here that should get merged today: With ^ the way to handle a deeply nested metadata document is to indicate it in the |
pcarleton
left a comment
There was a problem hiding this comment.
(see above, please add a test)
felixweinberger
left a comment
There was a problem hiding this comment.
Requesting changes to implement a test + there seem to be some CI failures still.
|
Hi @pcarleton, @felixweinberger - After reviewing the PR and the linked issue, I don’t think the proposed fix fully resolves the problem. Root cause: But, the server route is registered at following URL all the tme: These don’t align. Besides, the RFC 9728 spec expects the I have created a pull request to implement such fixes. Please see: |
Bug Report: Incorrect
.well-known/oauth-protected-resourceendpoint path whenresource_server_urlends with/sseThis is reported in 1264
Summary
When configuring FastMCP with OAuth2 authentication and setting the
resource_server_urlto end with/sse(as required by VSCode MCP clients), FastMCP incorrectly serves the.well-known/oauth-protected-resourceendpoint at/sse/.well-known/oauth-protected-resourceinstead of the expected root path/.well-known/oauth-protected-resource.Environment
mcpPython library (check withpip show mcp)mcp/server/fastmcp/server.py(lines ~790-797)mcp/server/auth/routes.py(lines ~215-224)Expected Behavior
.well-known/oauth-protected-resourceendpoint should always be served at the root path (/.well-known/oauth-protected-resource) regardless of theresource_server_urlconfigurationresourcefield in the.well-knownresponse should point to the actual protected resource (e.g.,/sse)Actual Behavior
When
resource_server_urlis set tohttp://localhost:8099/sse, FastMCP:.well-known/oauth-protected-resourceat/sse/.well-known/oauth-protected-resource/ssereturns awww-authenticateheader withresource_metadata="http://localhost:8099/sse/.well-known/oauth-protected-resource".well-knownendpoint at the rootSteps to Reproduce
Start the server:
uvicorn server:app --port 8099Test the endpoints:
Root Cause Analysis
Exact Location of Bug:
mcp/server/fastmcp/server.pysse_app()methodFastMCP.sse_app()The Issue: When setting up OAuth2 authentication, FastMCP constructs the
resource_metadata_urlincorrectly:When
resource_server_urlishttp://localhost:8099/sse, this createshttp://localhost:8099/sse/.well-known/oauth-protected-resource.However, the actual
.well-knownendpoint is created bycreate_protected_resource_routes()(inmcp/server/auth/routes.pylines ~215-224), which always creates it at the root path:The Fix: The
resource_metadata_urlshould be constructed from the base URL, not theresource_server_url:Impact
/sseresource URL patternProposed Solution
The
.well-known/oauth-protected-resourceendpoint should always be served at the root path (/.well-known/oauth-protected-resource), regardless of theresource_server_urlconfiguration. Theresource_server_urlshould only affect:resourcefield value in the.well-knownresponseresource_metadatareference inwww-authenticateheadersCurrent Workaround
Override the built-in
.well-knownendpoint with a custom implementation:Additional Context
/sse.well-knownendpoints should be at predictable root paths.well-knownendpoints at root regardless of resource configurationRelated Documentation