vercel/ai · error

Protected resource ${resourceMetadata.resource} does not mat

Error message

Protected resource ${resourceMetadata.resource} does not match expected ${defaultResource} (or origin)

What it means

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).

Source

Thrown at packages/mcp/src/tool/oauth.ts:1218

  if (provider.validateResourceURL) {
    return await provider.validateResourceURL(
      defaultResource,
      resourceMetadata?.resource,
    );
  }

  if (!resourceMetadata) {
    return undefined;
  }

  if (
    !checkResourceAllowed({
      requestedResource: defaultResource,
      configuredResource: resourceMetadata.resource,
    })
  ) {
    throw new Error(
      `Protected resource ${resourceMetadata.resource} does not match expected ${defaultResource} (or origin)`,
    );
  }
  return new URL(resourceMetadata.resource);
}

async function authInternal(
  provider: OAuthClientProvider,
  {
    serverUrl,
    authorizationCode,
    callbackState,
    callbackIssuer,
    scope,
    resourceMetadataUrl,
    fetchFn,
  }: {
    serverUrl: string | URL;

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. 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).
  2. Check reverse-proxy/gateway config so the advertised resource matches the externally visible origin.
  3. If intentional, implement provider.validateResourceURL on your OAuthClientProvider to approve the mismatched resource explicitly.
  4. Compare the PRM resource value with your serverUrl (curl the PRM endpoint) to spot the exact mismatch (http vs https, port, path).

Example fix

// before: PRM served by server at https://mcp.example.com advertises
// { "resource": "https://internal-mcp.corp:8443" }
// after: correct the PRM resource to the public server URL
// { "resource": "https://mcp.example.com/" }
Defensive patterns

Strategy: validation

Validate before calling

const prm = await fetch(resourceMetadataUrl).then(r => r.json());
const expected = new URL(serverUrl).origin;
if (!prm.resource || new URL(prm.resource).origin !== expected) {
  throw new Error(`PRM resource ${prm.resource} does not match server origin ${expected}; fix server metadata`);
}

Type guard

function resourceMatchesServer(serverUrl: URL, resource: string): boolean {
  try { return new URL(resource).origin === serverUrl.origin; } catch { return false; }
}

Try / catch

try {
  await auth(provider, { serverUrl });
} catch (error) {
  if (String(error.message).includes('does not match expected')) {
    console.error('Protected resource metadata does not match the MCP server URL; fix the server RFC 9728 document.');
  }
}

Prevention

When it happens

Trigger: Running auth()/selectResourceURL where the discovered protected resource metadata's resource field does not match the serverUrl-derived default resource or its origin.

Common situations: 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).

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/6d0656f7289543b9. Report an issue: GitHub.