{"record":{"id":"34a4aa027e444c2e","repo":"vercel/ai","slug":"oauth-protected-resource-metadata-url-resourceme","errorCode":null,"errorMessage":"OAuth protected resource metadata URL ${resourceMetadataUrl.href} must have the same origin as the MCP server URL ${expectedOrigin}","messagePattern":"OAuth protected resource metadata URL (.+?) must have the same origin as the MCP server URL (.+?)","errorType":"exception","errorClass":"MCPClientOAuthError","httpStatus":null,"severity":"error","filePath":"packages/mcp/src/tool/oauth.ts","lineNumber":311,"sourceCode":"      ),\n    );\n    return true;\n  }\n\n  return false;\n}\n\nfunction assertResourceMetadataUrlSameOrigin(\n  serverUrl: string | URL,\n  resourceMetadataUrl?: URL,\n): void {\n  if (!resourceMetadataUrl) {\n    return;\n  }\n\n  const expectedOrigin = new URL(serverUrl).origin;\n  if (resourceMetadataUrl.origin !== expectedOrigin) {\n    throw new MCPClientOAuthError({\n      message: `OAuth protected resource metadata URL ${resourceMetadataUrl.href} must have the same origin as the MCP server URL ${expectedOrigin}`,\n    });\n  }\n}\n\nfunction assertAuthorizationServerInformationMatches({\n  storedAuthorizationServerInformation,\n  currentAuthorizationServerInformation,\n}: {\n  storedAuthorizationServerInformation: OAuthAuthorizationServerInformation;\n  currentAuthorizationServerInformation: OAuthAuthorizationServerInformation;\n}): void {\n  if (\n    (storedAuthorizationServerInformation.issuer != null &&\n      currentAuthorizationServerInformation.issuer != null &&\n      storedAuthorizationServerInformation.issuer !==\n        currentAuthorizationServerInformation.issuer) ||\n    storedAuthorizationServerInformation.authorizationServerUrl !==","sourceCodeStart":293,"sourceCodeEnd":329,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/mcp/src/tool/oauth.ts#L293-L329","documentation":"assertResourceMetadataUrlSameOrigin enforces RFC 9728 hardening: the `resource_metadata` URL returned in a WWW-Authenticate challenge must be same-origin with the MCP server URL, otherwise a malicious server could point the client at attacker-controlled metadata. When the origins differ, MCPClientOAuthError is thrown listing both the metadata URL and the expected origin. It runs in authInternal when the MCP server responds 401 with a resource-metadata URL.","triggerScenarios":"An MCP server returns a 401 challenge whose `resource_metadata` header/field points at a different origin than the server URL (CDN host, different subdomain, another port); a reverse proxy rewrites the metadata URL to an external host; the server advertises `http://` metadata while the client uses `https://` (different origin due to scheme); server configured with the wrong absolute URL for its own metadata document.","commonSituations":"MCP gateways where auth metadata lives on a separate domain (e.g. server at api.example.com, metadata at auth.example.com); localhost dev with mismatched ports (localhost:3000 server advertising localhost:8080 metadata — different origin); misconfigured proxy setting absolute URLs to a public domain; self-hosted servers with hand-written WWW-Authenticate headers.","solutions":["Fix the MCP server (or its reverse proxy) so the resource_metadata URL in the 401 challenge is served from the same origin as the MCP server URL itself.","If metadata legitimately lives elsewhere, host a same-origin metadata document (or route /​.well-known/oauth-protected-resource on the server origin) per RFC 9728.","Check scheme and port match: https server must advertise https metadata on the same port; localhost dev should use the same port for server and metadata.","Inspect the two URLs in the error message — align the metadata URL host/scheme/port with the serverUrl origin you passed to the client.","If a proxy injects the wrong absolute URL, configure it to emit relative or same-origin absolute resource_metadata URLs."],"exampleFix":"// before: challenge from https://api.example.com points metadata elsewhere\nWWW-Authenticate: Bearer resource_metadata=\"https://auth.example.com/.well-known/oauth-protected-resource\"\n// after: same-origin metadata URL\nWWW-Authenticate: Bearer resource_metadata=\"https://api.example.com/.well-known/oauth-protected-resource\"","handlingStrategy":"validation","validationCode":"export function isSameOriginResourceMetadata(serverUrl: string, resourceMetadataUrl?: string): boolean {\n  if (!resourceMetadataUrl) return true;\n  try {\n    return new URL(resourceMetadataUrl).origin === new URL(serverUrl).origin;\n  } catch {\n    return false;\n  }\n}\n// run after parsing the 401 WWW-Authenticate challenge, before continuing the auth flow","typeGuard":null,"tryCatchPattern":"import { MCPClientOAuthError } from './oauth';\ntry {\n  await client.auth();\n} catch (error) {\n  if (MCPClientOAuthError.isInstance(error) && error.message.includes('must have the same origin')) {\n    // fix the server's resource_metadata URL (or host metadata same-origin) and retry\n  } else {\n    throw error;\n  }\n}","preventionTips":["Serve .well-known/oauth-protected-resource from the MCP server's own origin.","Match scheme and port exactly between server URL and metadata URL (http/https and ports create different origins).","Configure reverse proxies to emit relative or same-origin absolute metadata URLs.","Test the 401 challenge with curl to inspect the resource_metadata value before wiring up the client."],"tags":["oauth","security","same-origin","mcp","rfc9728"],"backgroundTag":"cross-origin-resource-metadata","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}