{"record":{"id":"6d0656f7289543b9","repo":"vercel/ai","slug":"protected-resource-resourcemetadata-resource-do","errorCode":null,"errorMessage":"Protected resource ${resourceMetadata.resource} does not match expected ${defaultResource} (or origin)","messagePattern":"Protected resource (.+?) does not match expected (.+?) \\(or origin\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/mcp/src/tool/oauth.ts","lineNumber":1218,"sourceCode":"\n  if (provider.validateResourceURL) {\n    return await provider.validateResourceURL(\n      defaultResource,\n      resourceMetadata?.resource,\n    );\n  }\n\n  if (!resourceMetadata) {\n    return undefined;\n  }\n\n  if (\n    !checkResourceAllowed({\n      requestedResource: defaultResource,\n      configuredResource: resourceMetadata.resource,\n    })\n  ) {\n    throw new Error(\n      `Protected resource ${resourceMetadata.resource} does not match expected ${defaultResource} (or origin)`,\n    );\n  }\n  return new URL(resourceMetadata.resource);\n}\n\nasync function authInternal(\n  provider: OAuthClientProvider,\n  {\n    serverUrl,\n    authorizationCode,\n    callbackState,\n    callbackIssuer,\n    scope,\n    resourceMetadataUrl,\n    fetchFn,\n  }: {\n    serverUrl: string | URL;","sourceCodeStart":1200,"sourceCodeEnd":1236,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/mcp/src/tool/oauth.ts#L1200-L1236","documentation":"selectResourceURL validates the resource value from OAuth protected resource metadata (RFC 9728) against the MCP server URL the client is connecting to. checkResourceAllowed requires the configured resource to match the expected resource derived from the server URL (or at least share its origin); otherwise this security check throws to prevent sending tokens minted for a different resource (audience confusion / token substitution).","triggerScenarios":"Running auth()/selectResourceURL where the discovered protected resource metadata's resource field does not match the serverUrl-derived default resource or its origin.","commonSituations":"MCP server deployments where the PRM is served with a resource URL on a different host/port/scheme than the actual server URL (reverse proxy misconfiguration, trailing-slash or scheme mismatch, staging URL serving production metadata).","solutions":["Fix the MCP server's protected resource metadata so the resource field matches the URL clients use to reach the server (scheme, host, port, path).","Check reverse-proxy/gateway config so the advertised resource matches the externally visible origin.","If intentional, implement provider.validateResourceURL on your OAuthClientProvider to approve the mismatched resource explicitly.","Compare the PRM resource value with your serverUrl (curl the PRM endpoint) to spot the exact mismatch (http vs https, port, path)."],"exampleFix":"// before: PRM served by server at https://mcp.example.com advertises\n// { \"resource\": \"https://internal-mcp.corp:8443\" }\n// after: correct the PRM resource to the public server URL\n// { \"resource\": \"https://mcp.example.com/\" }","handlingStrategy":"validation","validationCode":"const prm = await fetch(resourceMetadataUrl).then(r => r.json());\nconst expected = new URL(serverUrl).origin;\nif (!prm.resource || new URL(prm.resource).origin !== expected) {\n  throw new Error(`PRM resource ${prm.resource} does not match server origin ${expected}; fix server metadata`);\n}","typeGuard":"function resourceMatchesServer(serverUrl: URL, resource: string): boolean {\n  try { return new URL(resource).origin === serverUrl.origin; } catch { return false; }\n}","tryCatchPattern":"try {\n  await auth(provider, { serverUrl });\n} catch (error) {\n  if (String(error.message).includes('does not match expected')) {\n    console.error('Protected resource metadata does not match the MCP server URL; fix the server RFC 9728 document.');\n  }\n}","preventionTips":["Keep the PRM resource field identical to the public server URL clients use.","Audit reverse-proxy deployments so advertised resource matches the external origin.","Optionally implement provider.validateResourceURL to codify any intentional origin-level tolerance.","Diff PRM resource against serverUrl in deployment smoke tests."],"tags":["oauth","resource-indicator","rfc9728","security","mcp"],"backgroundTag":"protected-resource-mismatch","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}