Skip to content

discoverOAuthProtectedResourceMetadata validation errors are silently swallowed, leaving no signal when PRM is malformed #2866

Description

@sonmaximum

Summary

When PRM parsing fails against OAuthProtectedResourceMetadataSchema (Zod validation error, e.g. missing the RFC 9728-required resource field, or any other schema violation), the SDK silently swallows the exception with no log, warning, or diagnostic. resourceMetadata stays undefined and the flow proceeds as if PRM discovery legitimately returned nothing, leaving downstream failures as the only user-visible signal.

Network failures were fixed to propagate; validation failures are still silent.

Location

packages/client/src/client/auth.ts (current main, ~L1163-1176), in the main auth() flow:

try {
    resourceMetadata = await discoverOAuthProtectedResourceMetadata(
        serverUrl,
        { resourceMetadataUrl: effectiveResourceMetadataUrl },
        fetchFn
    );
} catch (error) {
    // Network failures (DNS, connection refused) surface as TypeError — propagate
    // those rather than masking a transient reachability problem.
    if (error instanceof TypeError) {
        throw error;
    }
    // RFC 9728 not available — selectResourceURL will handle undefined
}

The TypeError propagation was added since earlier versions of this code (which had a bare catch {}), which is good; network problems now surface. But the "silently continue" branch is taken for all non-network errors, including Zod validation failures on a PRM that did fetch successfully.

The distinction matters:

  • PRM 404 / 500 / non-200: correctly treated as "RFC 9728 not available" → soft fallback is reasonable
  • PRM present, Zod-invalid: this is a spec/config mismatch on the server side. Silent fallback masks it.

Currently both are handled identically.

Reproduction

  1. Point a client using this SDK at an MCP server whose PRM document returns HTTP 200 with a body that is invalid, for example, that omits the RFC 9728-required resource field (only authorization_servers, scopes_supported, etc.).

    Example PRM body:

{
 "authorization_servers": ["https://login.microsoftonline.com/{tenant}/v2.0"],
 "scopes_supported": ["https://graph.microsoft.com/Mail.Read"],
 "bearer_methods_supported": ["header"]
}
  1. Call auth() with no authServerMetadataUrl override.
  2. Expected: an actionable error or log naming the Zod validation failure as the root cause.
  3. Actual: no log, no warning; resourceMetadata becomes undefined and the downstream flow surfaces whatever unrelated error the fallback path produces.

Why this matters

The failure mode is genuinely hard to diagnose. Nothing in the observable output points at PRM validation. I spent a significant amount of time attributing this to authorization-server-derivation logic (thinking the SDK was ignoring authorization_servers from PRM), when in fact the SDK reads that field correctly - a completely different code path (schema validation) was throwing and being swallowed.

Adjacent acknowledgment in-repo:

PR #2092 ("fix(client,core): tighten OAuth PRM resource validation per RFC 8707 §2") explicitly notes in its body:

"since auth() swallows PRM errors and would mask the assertion"

The PR author works around this in their tests but doesn't scope the PR to fixing it. This issue is that scope.

Suggested fixes

  1. Minimum: log the swallowed exception.
} catch (error) {
   if (error instanceof TypeError) throw error;
   // RFC 9728 not available — selectResourceURL will handle undefined
   console.warn(
       'MCP OAuth: protected resource metadata discovery failed. ' +
       'Downstream errors may reference the fallback path rather than PRM. Reason:',
       error
   );
}
  1. Better: differentiate schema-validation errors (Zod exception on a 200 response) from HTTP/network errors. Zod errors represent server-side spec violations and deserve a loud signal; HTTP failures are the "server doesn't implement RFC 9728" case where soft fallback is appropriate.

Related

Environment

  • @modelcontextprotocol/sdk - reproduced against current main (2536-line auth.ts)
  • Reproduced with clients: Cline (VS Code) and Claude Code (with authServerMetadataUrl unset)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v1Issues / PRs related to v1.xv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions