From 718b226177e580abc074e7917ad993700ec41f83 Mon Sep 17 00:00:00 2001 From: Alberto de la Cruz Date: Mon, 29 Jun 2026 11:34:36 +0200 Subject: [PATCH] fix: discover MCP Protected Resource Metadata when server does not return 401 (#25980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_protected_resource_metadata() only attempted RFC 9728 discovery when the anonymous `initialize` probe returned 401 with a WWW-Authenticate header. Some remote MCP servers — notably Google's Gmail/Drive/Calendar MCPs (gmailmcp.googleapis.com, etc.) — answer 200 to an anonymous initialize, so OAuth scope and authorization-server discovery silently failed and connections to them could not be established. Run the discovery regardless of the probe's HTTP status: prefer the resource_metadata URL from WWW-Authenticate when present, and otherwise fall back to the RFC 9728 §4.2 well-known URIs. The trade-off is a couple of extra well-known GETs during MCP connection setup for servers that expose no PRM document; behavior for 401-responding servers is unchanged. --- backend/open_webui/utils/oauth.py | 86 ++++++++++++++++--------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index cceb3408a7..f5d861af0c 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -372,53 +372,57 @@ async def get_protected_resource_metadata(server_url: str) -> ProtectedResourceM headers={'Content-Type': 'application/json'}, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: - if response.status == 401: - resource_metadata_urls = [] - match = re.search( - r'resource_metadata=(?:"([^"]+)"|([^\s,]+))', - response.headers.get('WWW-Authenticate', ''), - ) - if match: - resource_metadata_urls = [match.group(1) or match.group(2)] - log.debug(f'Found resource_metadata URL: {resource_metadata_urls[0]}') - else: - # Fall back to well-known resource metadata URIs (RFC 9728 §4.2) - parsed, base_url = get_parsed_and_base_url(server_url) - if parsed.path and parsed.path != '/': - path = parsed.path.rstrip('/') - resource_metadata_urls.append( - urllib.parse.urljoin(base_url, f'/.well-known/oauth-protected-resource{path}') - ) + # Discover Protected Resource Metadata regardless of HTTP status. + # A 401 carries a WWW-Authenticate header pointing at the PRM, but + # some MCP servers (e.g. Google's gmail/drive/calendar remote MCPs) + # answer 200 to an anonymous `initialize`, so we must still fall + # back to the RFC 9728 well-known URIs when there is no 401/header. + resource_metadata_urls = [] + match = re.search( + r'resource_metadata=(?:"([^"]+)"|([^\s,]+))', + response.headers.get('WWW-Authenticate', ''), + ) + if match: + resource_metadata_urls = [match.group(1) or match.group(2)] + log.debug(f'Found resource_metadata URL: {resource_metadata_urls[0]}') + else: + # Fall back to well-known resource metadata URIs (RFC 9728 §4.2) + parsed, base_url = get_parsed_and_base_url(server_url) + if parsed.path and parsed.path != '/': + path = parsed.path.rstrip('/') resource_metadata_urls.append( - urllib.parse.urljoin(base_url, '/.well-known/oauth-protected-resource') + urllib.parse.urljoin(base_url, f'/.well-known/oauth-protected-resource{path}') ) - log.debug(f'No resource_metadata in header, trying well-known URIs: {resource_metadata_urls}') + resource_metadata_urls.append( + urllib.parse.urljoin(base_url, '/.well-known/oauth-protected-resource') + ) + log.debug(f'No resource_metadata in header, trying well-known URIs: {resource_metadata_urls}') - # Fetch Protected Resource metadata from candidate URLs - for resource_metadata_url in resource_metadata_urls: - try: - async with session.get( - resource_metadata_url, ssl=AIOHTTP_CLIENT_SESSION_SSL - ) as resource_response: - if resource_response.status == 200: - resource_metadata = await resource_response.json() + # Fetch Protected Resource metadata from candidate URLs + for resource_metadata_url in resource_metadata_urls: + try: + async with session.get( + resource_metadata_url, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resource_response: + if resource_response.status == 200: + resource_metadata = await resource_response.json() - resource = resource_metadata.get('resource') or None - if resource: - log.debug(f'Discovered resource indicator: {resource}') + resource = resource_metadata.get('resource') or None + if resource: + log.debug(f'Discovered resource indicator: {resource}') - servers = resource_metadata.get('authorization_servers', []) - scopes = resource_metadata.get('scopes_supported', []) - if scopes: - log.debug(f'Discovered resource scopes: {scopes}') + servers = resource_metadata.get('authorization_servers', []) + scopes = resource_metadata.get('scopes_supported', []) + if scopes: + log.debug(f'Discovered resource scopes: {scopes}') - if servers: - authorization_servers = servers - log.debug(f'Discovered authorization servers: {servers}') - break - except Exception as e: - log.debug(f'Failed to fetch resource metadata from {resource_metadata_url}: {e}') - continue + if servers: + authorization_servers = servers + log.debug(f'Discovered authorization servers: {servers}') + break + except Exception as e: + log.debug(f'Failed to fetch resource metadata from {resource_metadata_url}: {e}') + continue except Exception as e: log.debug(f'MCP Protected Resource discovery failed: {e}')