Skip to content

Conversation

seanhoughton
Copy link

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:
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()
  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() 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:

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

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

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

# 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 ihrpr August 20, 2025 14:50
@shulkx
Copy link
Contributor

shulkx commented Sep 2, 2025

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

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

soyacz commented Sep 11, 2025

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

Copy link
Member

@pcarleton pcarleton left a comment

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)

@felixweinberger felixweinberger added needs more work Not ready to be merged yet, needs additional changes. needs sync Needs sync with latest main branch to ensure CI passes labels Sep 23, 2025
Copy link
Contributor

@felixweinberger felixweinberger left a comment

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
Copy link
Contributor

shulkx commented Sep 27, 2025

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:

@felixweinberger felixweinberger added the auth Issues and PRs related to Authentication / OAuth label Sep 30, 2025
@pcarleton
Copy link
Member

👍 let's address in #1407 thanks @shulkx

@pcarleton pcarleton closed this Oct 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth Issues and PRs related to Authentication / OAuth needs more work Not ready to be merged yet, needs additional changes. needs sync Needs sync with latest main branch to ensure CI passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants