Skip to content

[v1.x] Preserve the exact OAuth resource indicator (backport #2925) - #3462

Draft
pcarleton wants to merge 2 commits into
v1.xfrom
paulc/v1x-backport-2925
Draft

pcarleton wants to merge 2 commits into
v1.xfrom
paulc/v1x-backport-2925

Conversation

@pcarleton

@pcarleton pcarleton commented Sep 6, 2026 •

Copy link
Copy Markdown
Member

Backport of #2925 to the 1.x line, plus a small compat shim so the fix holds on pydantic < 2.12 (which 1.x still supports).

Motivation and Context

RFC 8707's resource parameter must be the protected resource's identifier byte-for-byte. On 1.x a path-less resource from the server's Protected Resource Metadata (http://host:port) is parsed into AnyHttpUrl and re-serialised as http://host:port/, so the client sends a different identifier in the authorization and token requests (#2578). main fixed this in #2925 via url_preserve_empty_path=True on the OAuth metadata models; that was never backported.

This now matters for conformance: modelcontextprotocol/conformance#488 adds a resource-parameter-matches-prm check (FAILURE, RFC 8707 MUST) to the scored auth/metadata-var2 client scenario, and the 1.x line fails it without this change while main, go, rust, csharp and the TS SDK pass.

What's in here

  1. Cherry-pick of Preserve empty URL paths on OAuth metadata models #2925 (url_preserve_empty_path=True on OAuthClientMetadata, OAuthMetadata, ProtectedResourceMetadata; the v2 migration-guide hunk is dropped since that file doesn't exist on 1.x).
  2. A compat commit for pydantic 2.11: the 1.x dependency range (and the lowest-direct CI leg) still resolves pydantic 2.11, where url_preserve_empty_path is silently ignored. ProtectedResourceMetadata now records the wire string (resource_str) and OAuthContext.get_resource_url() / _validate_resource_match use it, so the resource parameter is echoed verbatim on every supported pydantic version. The config is spelled as a cast dict so the 2.11 type stubs accept it, and the cherry-picked issuer assertion is made version-tolerant.

How Has This Been Tested?

  • uv run --frozen pytest tests/client/test_auth.py → 129 passed (new test test_get_resource_url_echoes_pathless_prm_resource_verbatim); ruff + pyright clean on the touched files; behaviour checked on pydantic 2.11.7 and 2.13.4.
  • Conformance (modelcontextprotocol/conformance main + feat(client/auth): check the resource parameter matches the PRM-published identifier conformance#488 ), client --scenario auth/metadata-var2 --spec-version 2025-11-25 with .github/actions/conformance/client.py:
    • before (v1.x @ 8c2fa6e): resource-parameter-matches-prm FAILURE → scenario FAILED
    • after (this branch): resource-parameter-matches-prm SUCCESS, 18/18 checks, scenario PASSED; auth/metadata-default PASSED before and after.

Breaking Changes

None intended. str(prm.resource) is unchanged on pydantic < 2.12 (still normalised); code that needs the exact identifier should use the new ProtectedResourceMetadata.resource_str.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

AI Disclaimer

Prepared with Claude Code; reviewed the diff and the test/conformance output.

Kludex and others added 2 commits September 6, 2026 19:22
The 1.x line still allows pydantic 2.11 (Python < 3.14), where the
url_preserve_empty_path config from #2925 is silently ignored and a
path-less PRM resource still renders with a trailing slash. Record the wire
string on ProtectedResourceMetadata (resource_str) and use it for the
resource parameter, so the client echoes the server's identifier verbatim on
every supported pydantic version. Also spell the config as a cast dict so the
2.11 type stubs accept it, and make the cherry-picked issuer assertion
version-tolerant.

Co-Authored-By: Claude <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

2 participants