Skip to content

Conversation

shulkx
Copy link

@shulkx shulkx commented Sep 26, 2025

Summary

Fixes RFC 9728 compliance issues where the /.well-known/oauth-protected-resource endpoint was not correctly constructed for resource identifiers containing path components, causing client discovery failures. (#1400)

Motivation and Context

Problem: The current implementation has two critical RFC 9728 compliance issues:

  1. URL Construction Mismatch: When resource_server_url contains a path (e.g., http://localhost:8001/mcp), the WWW-Authenticate header points to http://localhost:8001/mcp/.well-known/oauth-protected-resource (appending after the path), but the actual route is served at http://localhost:8001/.well-known/oauth-protected-resource (root level only).

  2. Non-RFC Compliant Path Insertion: RFC 9728 §3.1 specifies that the well-known path should be inserted between the host and resource path, not appended after the full URL.

  3. Example Configuration Issue: The simple-auth example had an incorrect resource_server_url configuration that didn't include the /mcp path component, causing resource validation errors in VS Code MCP clients.

Impact:

  • Clients following RFC 9728 get 404 errors when trying to discover metadata
  • VS Code shows "Protected Resource Metadata resource does not match MCP server resolved resource" error
  • Breaks interoperability with standards-compliant OAuth clients
  • Prevents multi-tenant hosting scenarios

Root Cause: String concatenation approach doesn't properly handle URL path components according to RFC 9728 specification.

Changes Made

1. Created Reusable Utility Function

Added build_resource_metadata_url() in src/mcp/server/auth/routes.py to centralize RFC 9728 compliant URL construction:

def build_resource_metadata_url(resource_server_url: AnyHttpUrl) -> AnyHttpUrl:
    """Build RFC 9728 compliant protected resource metadata URL."""
    parsed = urlparse(str(resource_server_url))
    # Handle trailing slash: if path is just "/", treat as empty
    resource_path = parsed.path if parsed.path != "/" else ""
    return AnyHttpUrl(
        f"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource{resource_path}"
    )

2. Updated WWW-Authenticate Header Construction

Before (Non-compliant):

resource_metadata_url = AnyHttpUrl(
    str(resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource"
)

After (RFC 9728 Compliant):

resource_metadata_url = build_resource_metadata_url(resource_server_url)

3. Updated Route Registration

Before (Non-compliant):

Route("/.well-known/oauth-protected-resource", ...)  # Always at root

After (RFC 9728 Compliant):

# Dynamic path based on resource URL structure
metadata_url = build_resource_metadata_url(resource_url)
parsed = urlparse(str(metadata_url))
well_known_path = parsed.path
Route(well_known_path, ...)

4. Fixed Example Configuration

Updated examples/servers/simple-auth/mcp_simple_auth/server.py to include proper trailing slash handling:

# Before
resource_server_url=AnyHttpUrl(f"{settings.server_url}mcp"),

# After  
resource_server_url=AnyHttpUrl(f"{str(settings.server_url).rstrip('/')}/mcp"),

This ensures the resource identifier correctly matches the MCP server endpoint and prevents VS Code authentication errors.

5. Eliminated Code Duplication

  • Refactored 3 instances of identical URL construction logic to use the utility function
  • Replaced inline route creation in streamable_http_app() with create_protected_resource_routes()
  • Reduced code duplication by ~40 lines while improving maintainability

URL Construction Examples:

Resource Server URL WWW-Authenticate Header Route Path Status
http://localhost:8001 http://localhost:8001/.well-known/oauth-protected-resource /.well-known/oauth-protected-resource ✅ Match
http://localhost:8001/ http://localhost:8001/.well-known/oauth-protected-resource /.well-known/oauth-protected-resource ✅ Match
http://localhost:8001/mcp http://localhost:8001/.well-known/oauth-protected-resource/mcp /.well-known/oauth-protected-resource/mcp ✅ Match

How Has This Been Tested?

  • Comprehensive Test Suite: Added 11 new tests covering URL construction, route consistency, and edge cases
  • Edge Case Testing: Confirmed proper handling of trailing slashes, various URL formats, and localhost scenarios
  • Integration Testing: Verified consistency between WWW-Authenticate header and actual route registration
  • VS Code Testing: Confirmed the example fix resolves authentication errors with VS Code MCP client
  • Backwards Compatibility: Confirmed existing OAuth flows continue to work for root-level resources
  • All Tests Pass: 14/14 tests pass, including both new and existing functionality

Breaking Changes

None. This fix maintains backward compatibility for existing deployments where resource_server_url has no path component, while fixing the broken behavior for path-based resources.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Files Modified

  • src/mcp/server/fastmcp/server.py - Updated both SSE and StreamableHTTP apps to use RFC 9728 compliant URL construction
  • src/mcp/server/auth/routes.py - Added utility function and updated create_protected_resource_routes()
  • examples/servers/simple-auth/mcp_simple_auth/server.py - Fixed resource_server_url configuration with proper trailing slash handling
  • tests/server/auth/test_protected_resource.py - Added comprehensive tests for new functionality

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional Context

RFC 9728 References:

Section 3.1 - Protected Resource Metadata Request (RFC 9728 §3.1):

"If the resource identifier value contains a path or query component, any terminating slash (/) following the host component MUST be removed before inserting /.well-known/ and the well-known URI path suffix between the host component and the path and/or query components."

Section 3.3 - Protected Resource Metadata Validation (RFC 9728 §3.3):

"The resource value returned MUST be identical to the protected resource's resource identifier value into which the well-known URI path suffix was inserted to create the URL used to retrieve the metadata. If these values are not identical, the data contained in the response MUST NOT be used."

…per RFC 9728

- Replace string manipulation with proper URL parsing using urllib.parse.urlparse
- Handle edge case where resource server path is "/" by treating as empty string
- Ensure consistent URL construction for both WWW-Authenticate header and metadata endpoint
- Add RFC 9728 §3.1 compliance comments for better code documentation
@shulkx shulkx requested review from a team and felixweinberger September 26, 2025 15:48
@felixweinberger felixweinberger added needs more eyes Needs alignment among maintainers whether this is something we want to add auth Issues and PRs related to Authentication / OAuth labels Sep 26, 2025
- Moved import of AnyHttpUrl to the appropriate section for clarity
- Removed unnecessary duplicate import of urlparse
- Cleaned up whitespace for improved code readability
…ed function

- Introduced `build_resource_metadata_url` to create RFC 9728 compliant metadata URLs.
- Updated `FastMCP` to utilize the new function for constructing resource metadata URLs.
- Improved code readability by removing redundant URL parsing logic from `server.py`.
@shulkx shulkx requested review from a team and ochafik September 26, 2025 17:11
…rces

- Renamed `test_metadata_endpoint` to `test_metadata_endpoint_with_path` for clarity.
- Added a new test `test_metadata_endpoint_root_path_returns_404` to verify 404 response for root path.
- Introduced fixtures `root_resource_app` and `root_resource_client` for testing root-level resources.
- Added `test_metadata_endpoint_without_path` to validate metadata retrieval for root-level resources.
Append '/mcp' to resource_server_url and handle trailing slashes to ensure
proper OAuth Protected Resource Metadata validation. This prevents the error:
"Protected Resource Metadata resource does not match MCP server resolved resource"
which occurs when the PRM resource URL doesn't exactly match the server URL.

Fixes compatibility with VS Code + MCP authentication per RFC 9728.

References: microsoft/vscode#255255
…istency

- Introduced `TestMetadataUrlConstruction` to validate URL construction for various resource configurations.
- Added `TestRouteConsistency` to ensure consistency between generated metadata URLs and route paths.
- Enhanced existing tests for better clarity and coverage of edge cases in URL handling.
Copy link

@shrujanm shrujanm left a comment

Choose a reason for hiding this comment

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

@shulkx - I am following this PR and respective issue closely. Thanks for drafting this must have fix for the mcp python sdk which ensures that it closely follows the directed Oauth guidelines.

I am in process of testing the fix outlined here in the PR by starting the resource server with --oauth-strict flag as mentioned here - I came across another issue, which pertains to token introspection. While performing the token introspection by client - it encountered below error within the resource server.

INFO:httpx:HTTP Request: POST http://localhost:9000/introspect "HTTP/1.1 200 OK"
WARNING:mcp_simple_auth.token_verifier:Token resource validation failed. Expected: http://localhost:8001/
INFO:     127.0.0.1:65498 - "POST /mcp HTTP/1.1" 401 Unauthorized

While triaging this, I figured that Oauth server is sending aud (for audience) as http://localhost:8001/mcp but the token verifier is expecting it to be http://localhost:8001/ which is also highlighted within the error above.

In order to successfully perform this token introspection, I made a small change within the token_verifier configuration within examples/servers/simple-auth/mcp_simple_auth/server.py as noted below. If you can also verify the change suggested below and incorporate within your fix here, it would be great.

Before:

    # Create token verifier for introspection with RFC 8707 resource validation
    token_verifier = IntrospectionTokenVerifier(
        introspection_endpoint=settings.auth_server_introspection_endpoint,
        server_url=str(settings.server_url),
        validate_resource=settings.oauth_strict,  # Only validate when --oauth-strict is set
    )

After:

    # Create token verifier for introspection with RFC 8707 resource validation
    token_verifier = IntrospectionTokenVerifier(
        introspection_endpoint=settings.auth_server_introspection_endpoint,
        server_url=f"{str(settings.server_url).rstrip('/')}/mcp",  ## this is the change that resolved the introspection issue
        validate_resource=settings.oauth_strict,  # Only validate when --oauth-strict is set
    )

The OAuth server returns `aud` including the `/mcp` path, while the token verifier previously expected only the base URL. This mismatch caused introspection failures under `--oauth-strict`. Updated the verifier configuration to use the correct audience to ensure successful token introspection.
@shulkx
Copy link
Author

shulkx commented Sep 29, 2025

Thanks @shrujanm - I’ve made the change as suggested to align the token verifier’s audience with the OAuth server’s response. Since I am currently traveling and unable to verify the fix myself, I’ve committed the change based on your recommendation. It would be great if you could confirm the resolution and validate it further on your end.

Let me know if you run into any issues or if anything needs further adjustments during reviewing.

@shrujanm
Copy link

shrujanm commented Sep 29, 2025

Thanks @shulkx - I have just added couple loggers on my own resource server to show that above mentioned fix worked as expected.

This one is while the resource server starts and initialize the IntrospectionTokenVerifier. We can see that the resource_url is correctly mentioned as http://localhost:8001/mcp

IntrospectionTokenVerifier initialized with endpoint: http://localhost:9000/introspect, server_url: http://localhost:8001/mcp, resource_url: http://localhost:8001/mcp, validate_resource: True

This logger is on resource server side while performing the introspection operation - post response is received from Oauth server /introspect endpoint.

Before the fix:

INFO:httpx:HTTP Request: POST http://localhost:9000/introspect "HTTP/1.1 200 OK"
WARNING:mcp_simple_auth.token_verifier:Token resource validation failed. Expected: http://localhost:8001/
INFO:     127.0.0.1:65498 - "POST /mcp HTTP/1.1" 401 Unauthorized

After the fix:

INFO:httpx:HTTP Request: POST http://localhost:9000/introspect "HTTP/1.1 200 OK"
INFO:mcp_simple_auth.token_verifier:Received introspection response: status=200
INFO:mcp_simple_auth.token_verifier:Validating resource: requested=http://localhost:8001/mcp, configured=http://localhost:8001/mcp
INFO:     127.0.0.1:54839 - "POST /mcp HTTP/1.1" 200 OK

@shulkx shulkx changed the title refactor: improve OAuth protected resource metadata URL construction per RFC 9728 Improve OAuth protected resource metadata URL construction per RFC 9728 Oct 1, 2025
@shulkx
Copy link
Author

shulkx commented Oct 3, 2025

Hi @felixweinberger - It has been over a week since this PR was opened, and I wanted to follow up to ensure it gets reviewed. The changes address critical compliance issues with RFC 9728, which might cause client discovery failures.

Could the assigned reviewers or the relevant teams take a look at this when they get a chance? Your input would be greatly appreciated.

@felixweinberger
Copy link
Contributor

Hi @felixweinberger - It has been over a week since this PR was opened, and I wanted to follow up to ensure it gets reviewed. The changes address critical compliance issues with RFC 9728, which might cause client discovery failures.

Could the assigned reviewers or the relevant teams take a look at this when they get a chance? Your input would be greatly appreciated.

Hi @shulkx yes we'll get back to this next week - we had a hectic week here with an MCP event which led to less review capacity than we would've liked.

Will follow up with the auth codeowners to take another look Monday AM!

@felixweinberger felixweinberger added the needs maintainer action Potentially serious issue - needs proactive fix and maintainer attention label Oct 3, 2025
@felixweinberger felixweinberger requested review from pcarleton and removed request for a team October 3, 2025 19:34
@shulkx
Copy link
Author

shulkx commented Oct 4, 2025

Hi @felixweinberger - It has been over a week since this PR was opened, and I wanted to follow up to ensure it gets reviewed. The changes address critical compliance issues with RFC 9728, which might cause client discovery failures.

Could the assigned reviewers or the relevant teams take a look at this when they get a chance? Your input would be greatly appreciated.

Hi @shulkx yes we'll get back to this next week - we had a hectic week here with an MCP event which led to less review capacity than we would've liked.

Will follow up with the auth codeowners to take another look Monday AM!

Understood. Thanks for the update!

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 maintainer action Potentially serious issue - needs proactive fix and maintainer attention needs more eyes Needs alignment among maintainers whether this is something we want to add
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants