fix: build MCP WWW-Authenticate metadata URL from host_name with port (#196)#201
Conversation
…#196) The MCP endpoint's 401 WWW-Authenticate header advertises the OAuth resource_metadata URL using frappe.oauth.get_server_url(). That helper reconstructs the host/port from the request URL or the Social Login Key base_url and does not honor a configured host_name's non-standard port, so the advertised URL dropped the port (e.g. :8000). Behind a port-restricted network where the public URL is only reachable on a non-standard port, the client then tries the implied :443 and the OAuth handshake fails with "couldn't reach the MCP server". The discovery endpoints already build URLs from _get_public_base_url(), which uses host_name verbatim (scheme + port) — only the WWW-Authenticate header was still on get_server_url(). - Expose get_public_base_url() in oauth_discovery as the shared accessor for the canonical public base URL. - fac_endpoint now builds all six resource_metadata URLs (bearer-not-found, token-validation-error, API-auth-error, API-other-error, no-auth, and the HEAD probe) from get_public_base_url() instead of get_server_url(), so the configured port is preserved. Add test_oauth_metadata_port: get_public_base_url returns a host_name with a non-standard port verbatim (and does not consult get_server_url), and the MCP 401 WWW-Authenticate resource_metadata URL keeps the port on both the unauthenticated and invalid-bearer paths. The header tests fail against the old get_server_url code and pass with the fix. Fixes #196
|
@buildswithpaul |
|
This iwll be part the release 2.4.4 which will happen in a couple of days as I'm testing other implementations too. |
Thank you Paul, You are doing great and important job in moving and connecting ERPNext to the AI Era |
|
@hameedacpa Thank you for the kind words. This PR has been merged to the main branch and the latest version of FAC is 2.5.0 |
|
Thank you Paul @paulpromantia |
Summary
Fixes #196 — the MCP
WWW-Authenticate401 header advertised an OAuthresource_metadataURL that dropped a configured non-standard port, breaking the OAuth handshake behind port-restricted networks.Root cause
fac_endpoint.pybuilt theresource_metadataURL from Frappe'sfrappe.oauth.get_server_url():This reconstructs host/port from the (internal, proxied) request URL or the Social Login Key
base_urland never consults the configuredhost_name. For a deployment whose public URL ishttps://mysite.example.net:8000(ISP blocks 80/443; non-standard port forwards to internal nginx), the advertised URL came back as…example.net/.well-known/oauth-protected-resource— no port. The client then tried the implied:443, which is blocked, and failed with "couldn't reach the MCP server".The discovery endpoints (
/.well-known/oauth-*,openid-configuration) already build URLs from_get_public_base_url(), which honorshost_nameverbatim (scheme + port) — that was the earlier fix for thehttp://issue (#156). Only theWWW-Authenticateheader was still onget_server_url(), which is why the reporter saw the discovery URLs render:8000correctly while the header dropped it.Changes
get_public_base_url()inoauth_discovery.pyas the shared accessor for the canonical public base URL.fac_endpoint.pybuilds all sixresource_metadataURLs (bearer-not-found, token-validation-error, API-auth-error, API-other-error, no-auth fallback, and the HEAD connectivity probe) fromget_public_base_url()instead ofget_server_url(). Port is now preserved.No behavior change when
host_nameis unset or has no scheme — the existingget_server_url()fallback inside_get_public_base_url()is unchanged.Tests
test_oauth_metadata_port.py:get_public_base_url()returns ahost_namewith a non-standard port verbatim, without consultingget_server_url(); and still falls back whenhost_namehas no scheme.WWW-Authenticateresource_metadataURL keeps the port on both the unauthenticated and invalid-bearer-token paths.The header tests fail against the old
get_server_url()code and pass with the fix. Also verified live: withhost_name = https://mysite.example.net:8000,get_public_base_url()→https://mysite.example.net:8000. Existingtest_oauth_cors(8) still passes.Checklist
develop