Skip to content

fix: Always put the well-known endpoints at the server root - #1288

Closed
seanhoughton wants to merge 1 commit into
modelcontextprotocol:mainfrom
seanhoughton:fix/protected-resource-url
Closed

fix: Always put the well-known endpoints at the server root#1288
seanhoughton wants to merge 1 commit into
modelcontextprotocol:mainfrom
seanhoughton:fix/protected-resource-url

Conversation

@seanhoughton

Copy link
Copy Markdown

Bug Report: Incorrect .well-known/oauth-protected-resource endpoint path when resource_server_url ends with /sse

This is reported in 1264

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:
frommcp.server.fastmcpimportFastMCPfrommcp.server.auth.settingsimportAuthSettingsfrompydanticimportAnyHttpUrl# Configure auth settings with /sse endpointauth_settings=AuthSettings(
issuer_url=AnyHttpUrl("https://login.microsoftonline.com/tenant-id/v2.0"),
resource_server_url=AnyHttpUrl("http://localhost:8099/sse"), # Note: ends with /sserequired_scopes=["https://example.com/scope"]
)
mcp=FastMCP(
"Test Server",
token_verifier=your_token_verifier,
auth=auth_settings,
)
app=mcp.sse_app()
  1. Start the server: uvicorn server:app --port 8099

  2. Test the endpoints:

# 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:

# BUGGY CODE - Line ~790-797 in sse_app() methodresource_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:

# CORRECT CODE - This always creates /.well-known/oauth-protected-resource at rootreturn [
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:

# PROPOSED FIXifself.settings.authandself.settings.auth.resource_server_url:
frompydanticimportAnyHttpUrlfromurllib.parseimporturlparse# Extract base URL from resource_server_urlparsed=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:

asyncdefcustom_well_known_endpoint(request):
returnJSONResponse({
"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 endpointapp.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

# 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)
@seanhoughton
seanhoughton requested review from a team and ihrprAugust 20, 2025 14:50
@shulkx

Copy link
Copy Markdown
Contributor

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. (

resource_server_url=settings.server_url,
)

 # Create FastMCP server as a Resource Server
app = FastMCP(
name="MCP Resource Server",
instructions="Resource Server that validates tokens via Authorization Server introspection",
host=settings.host,
port=settings.port,
debug=True,
# Auth configuration for RS mode
token_verifier=token_verifier,
auth=AuthSettings(
issuer_url=settings.auth_server_url,
required_scopes=[settings.mcp_scope],
resource_server_url=AnyHttpUrl(f"{settings.server_url}mcp"), # <------ add "mcp"
),
)

@whitewg77

Copy link
Copy Markdown

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.

@soyacz

Copy link
Copy Markdown

Where did you get 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? I looked at https://www.ietf.org/rfc/rfc9728.pdf and there's no such statement. I find thou Different applications utilizing OAuth protected resources in application-specific ways define and register different well-known URI path suffixes for publishing protected resource metadata used by those applications

Actually I have opposite problem:
in VSCode when setting "url": "https://<host>/mcp", it tries to get oauth resources details from /mcp suffix: "GET /mcp/.well-known/oauth-protected-resource HTTP/1.1" 404 (actual nginx log)
This is because when setting server url to "https://example.com" FastMCP adds /mcp suffix for resource. This way resource is served at /mcp and .well-known/... from root.

@seanhoughton

Copy link
Copy Markdown
Author

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?

@pcarleton

Copy link
Copy Markdown
Member

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:
modelcontextprotocol/modelcontextprotocol#971

With ^ the way to handle a deeply nested metadata document is to indicate it in the WWW-Authenticate header as the metadata path.

@pcarletonpcarleton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(see above, please add a test)

@felixweinbergerfelixweinberger added needs more work Not ready to be merged yet, needs additional follow-up from the author(s). needs sync labels Sep 23, 2025

@felixweinbergerfelixweinberger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes to implement a test + there seem to be some CI failures still.

@shulkx

shulkx commented Sep 27, 2025

Copy link
Copy Markdown
Contributor

Hi @pcarleton, @felixweinberger - After reviewing the PR and the linked issue, I don’t think the proposed fix fully resolves the problem.

Root cause:
When resource_server_url includes a path (e.g. https://example.com/mcp), the implementation builds the WWW-Authenticate header as following which appends the .well-known/oauth-protected-resource directly after the resource_server_url.

https://example.com/mcp/.well-known/oauth-protected-resource

But, the server route is registered at following URL all the tme:

https://example.com/.well-known/oauth-protected-resource

These don’t align.

Besides, the RFC 9728 spec expects the .well-known suffix inserted between the host and the path, not appended after the full URL.

I have created a pull request to implement such fixes. Please see:

@felixweinbergerfelixweinberger added the auth Issues and PRs related to Authentication / OAuth label Sep 30, 2025
@pcarleton

Copy link
Copy Markdown
Member

👍 let's address in #1407 thanks @shulkx

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

authIssues and PRs related to Authentication / OAuthneeds more workNot ready to be merged yet, needs additional follow-up from the author(s).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@seanhoughton@shulkx@whitewg77@soyacz@pcarleton@felixweinberger