Skip to content

fix: prefix auth routes with issuer_url base path for gateway deployments#2401

Open
enjoykumawat wants to merge 1 commit into
modelcontextprotocol:mainfrom
enjoykumawat:fix/auth-routes-custom-base-path
Open

fix: prefix auth routes with issuer_url base path for gateway deployments#2401
enjoykumawat wants to merge 1 commit into
modelcontextprotocol:mainfrom
enjoykumawat:fix/auth-routes-custom-base-path

Conversation

@enjoykumawat
Copy link
Copy Markdown

Summary

Fixes #1335 — When an MCP server is deployed behind a gateway with a custom base path (e.g., https://gateway/custom/path/mcp), the OAuth auth routes (.well-known, /authorize, /token, /register, /revoke) are hardcoded at root, making them unreachable through the gateway.

Root cause: create_auth_routes() registers routes at fixed root paths (/.well-known/oauth-authorization-server, /authorize, etc.) regardless of the issuer_url path. Meanwhile, build_metadata() correctly builds metadata URLs using issuer_url + path, creating a mismatch.

Fix: Extract the path component from issuer_url and prefix it to all auth route registrations. This aligns the actual route paths with the metadata URLs already built by build_metadata().

# issuer_url = "https://example.com/custom/path"
# Before: routes at /authorize, /token, etc. (unreachable behind gateway)
# After:  routes at /custom/path/authorize, /custom/path/token, etc.

Backward compatible: when issuer_url has no path (or just /), issuer_path is empty and routes stay at root.

Changes

  • src/mcp/server/auth/routes.py: Extract issuer_path from issuer_url and prefix all route paths
  • tests/server/auth/test_routes.py: Add 3 tests for default paths, custom base path, and trailing slash handling

Test plan

  • All 12 test_routes.py tests pass (9 existing + 3 new)
  • All 4 test_error_handling.py tests pass (no regression)
  • All 42 test_auth_integration.py tests pass (no regression)
  • Ruff format + lint clean

When an MCP server is deployed behind a gateway with a custom base
path (e.g., /custom/path), the OAuth auth routes (.well-known,
/authorize, /token, /register, /revoke) were hardcoded at root,
making them unreachable through the gateway.

Extract the path component from issuer_url and prefix it to all
auth route registrations. This matches the metadata URLs already
built by build_metadata(), which correctly use issuer_url + path.
Backward compatible: when issuer_url has no path, routes stay at root.

Github-Issue: modelcontextprotocol#1335
Reported-by: whitewg77
@enjoykumawat
Copy link
Copy Markdown
Author

Friendly ping — happy to address any feedback or make changes if needed. Let me know if there's anything blocking review.

@aadnehovda
Copy link
Copy Markdown

.well-known is only compliant at domain root, see https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#protected-resource-metadata-discovery-requirements and https://datatracker.ietf.org/doc/html/rfc8615#section-3 :

Well-known URIs are rooted in the top of the path's hierarchy; they are not well-known by definition in other parts of the path. For example, "/.well-known/example" is a well-known URI, whereas "/foo/.well-known/example" is not.

This prefixing also competes with Mounting the app under a prefix, like in https://gofastmcp.com/deployment/http#mounting-strategy ?

Which is why FastMCP has get_well_known_routes to extract those routes before Mounting, while patching up .well-known at https://github.com/PrefectHQ/fastmcp/blob/53b20168c892b05b86ea8bf576a33b95936dda26/fastmcp_slim/fastmcp/server/auth/auth.py#L831-L872.

Prefixing the rest of the endpoints makes sense, but afaict FastMCP has hardcoded references to /authorize, /token, etc which may break.

For trusted reverse proxying we could get the scheme and host via X-Forwarded-*, and even the dynamic base path via X-Forwarded-Prefix.

It would be nice if someone took a long hard look at all the various pieces in play here, like proxy forwarding headers and path stripping, FastMCP path (which annoyingly cannot be "", which means you can't avoid the last / with Mount), uvicorn --root-path, Starlette Mount, etc. and avoid overrides and hardcoding. I get the feeling that a lot of this belongs in middleware and not in route construction. E.g. if the routes were named, they could be referenced and resolved by handlers during runtime when they need to be injected into the response with request.url_for(...), which would make them respect namespacing and rewriting middleware.

Just my 2c

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Using /.well-known/ OAuth endpoints behind custom path on GKE

2 participants