Skip to content

fix(client,core): tighten OAuth PRM resource validation per RFC 8707 §2#2092

Open
bishnubista wants to merge 1 commit into
modelcontextprotocol:mainfrom
bishnubista:fix/oauth-resource-identifier-validation
Open

fix(client,core): tighten OAuth PRM resource validation per RFC 8707 §2#2092
bishnubista wants to merge 1 commit into
modelcontextprotocol:mainfrom
bishnubista:fix/oauth-resource-identifier-validation

Conversation

@bishnubista
Copy link
Copy Markdown

Problem

OAuthProtectedResourceMetadataSchema.resource is currently z.string().url(). RFC 8707 §2 requires that resource indicators MUST NOT include a fragment component, but z.string().url() accepts both https://example.com/mcp#section (non-empty fragment) and https://example.com/mcp# (empty fragment). PRM publishers can emit non-conformant identifiers and they parse silently.

Separately, selectResourceURL rejects PRM resource values that don't share the MCP server's origin/path — a defensible default per RFC 9728 §3.3 / §7.3 (metadata impersonation defense) — but the error message is opaque. Two downstream Inspector reports (modelcontextprotocol/inspector#1304, modelcontextprotocol/inspector#855) show users hit this wall with legitimate Entra ID api:// and URN identifiers and don't realise the existing OAuthClientProvider.validateResourceURL hook is the supported escape valve.

Fix

Three small changes:

  1. packages/core/src/shared/auth.ts — add .refine(value => !value.includes('#')) to OAuthProtectedResourceMetadataSchema.resource. The value.includes('#') check catches both empty and non-empty fragments per RFC 3986 §3.5; percent-encoded %23 inside the path/query is correctly preserved.

  2. packages/client/src/client/auth.ts — in selectResourceURL, append actionable guidance to the existing mismatch error pointing at OAuthClientProvider.validateResourceURL as the documented opt-in for non-URL RFC 8707 indicators (urn:, api://) or cross-origin identifiers. Same throw point; only the message changes.

  3. packages/client/src/client/auth.ts — tighten the validateResourceURL JSDoc to describe what the default validator actually enforces, distinguish it from RFC 8707 §2 (looser) and RFC 9728 §3.3 (stricter), and document the override use cases. Add the RFC 9728 §3.3 / §7.3 security rationale for the strict default.

Tests

  • packages/core/test/shared/auth.test.ts — new describe('OAuthProtectedResourceMetadataSchema') block covering: no-fragment accept, non-empty-fragment reject, empty-fragment reject, percent-encoded %23 accept.
  • packages/client/test/client/auth.test.ts — two new direct discoverOAuthProtectedResourceMetadata cases for non-empty and empty fragment rejection (added next to the existing schema-validation test, since auth() swallows PRM errors and would mask the assertion). One new auth() integration test asserting the actionable error message includes validateResourceURL when the default rejection path runs.

Standards

  • RFC 8707 §2 — resource indicator MUST be an absolute URI per RFC 3986 §4.3, MUST NOT include a fragment.
  • RFC 9728 §3.3 and §7.3 — PRM resource mismatch defense against metadata impersonation, which is why the strict default in selectResourceURL is correct as policy even though it surprises Entra/URN users.

Out of scope (deliberate)

This PR does not relax selectResourceURL's origin check. Whether to introduce an opt-in compatibility policy for accepting non-https PRM identifiers (e.g. an OAuthClientProvider flag, or a constructor option) is a public-API design decision. Per CONTRIBUTING.md that needs maintainer alignment first. Happy to open a follow-up issue framing the design tradeoffs (status-quo + override hook vs. opt-in policy) once direction here is clear.

Related

Both are downstream of this SDK's selectResourceURL behavior. This PR makes the existing escape valve discoverable from the error surface; the upstream fix (if desired) is the follow-up discussed above.

Validation

  • pnpm --filter @modelcontextprotocol/core test — 556/556 ✅
  • pnpm --filter @modelcontextprotocol/client test — 367/367 ✅
  • pnpm typecheck:all — ✅
  • pnpm lint:all — ✅

Changeset: .changeset/oauth-prm-resource-fragment-validation.md (patch on core + client).

- Reject Protected Resource Metadata `resource` values that include a
  fragment component in `OAuthProtectedResourceMetadataSchema`. RFC 8707
  §2 hard-requires absolute URIs without a fragment; the prior
  `z.string().url()` accepted both empty and non-empty fragments.

- When `selectResourceURL` rejects a PRM `resource` because the origin
  or path does not match the MCP server URL, point integrators at the
  documented escape hatch: `OAuthClientProvider.validateResourceURL`.

- Tighten the `validateResourceURL` JSDoc to describe what the default
  validator actually enforces (URL `resource` values with no fragment
  that share origin with, and are equal to or a parent path of, the
  MCP server URL), and to document the override use cases for non-URL
  RFC 8707 indicators (e.g. `urn:`, `api://`) and cross-origin
  identifiers. Reference RFC 9728 §3.3 / §7.3 for the strict-default
  security rationale.

Motivated by user reports against the Inspector consumer of this SDK:
  - modelcontextprotocol/inspector#1304 (URN identifier rejected)
  - modelcontextprotocol/inspector#855 (Entra ID `api://` identifier rejected)

This PR does not relax the origin check; that is a separate design
decision left for maintainer input. The intent here is to (a) close a
real RFC 8707 §2 gap (fragment validation), and (b) make the existing
`validateResourceURL` override discoverable from the error surface so
Entra-ID and URN-based deployments can opt in without confusion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@bishnubista bishnubista requested a review from a team as a code owner May 15, 2026 03:48
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 15, 2026

🦋 Changeset detected

Latest commit: de1800a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@modelcontextprotocol/core Patch
@modelcontextprotocol/client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 15, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2092

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2092

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2092

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2092

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2092

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2092

commit: de1800a

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant