Skip to content

plain-oauthserver: accept Client ID Metadata Documents (SEP-991) - #108

Draft
davegaeddert wants to merge 1 commit into
masterfrom
claude/mcp-cimd-futures-58qc2v
Draft

plain-oauthserver: accept Client ID Metadata Documents (SEP-991)#108
davegaeddert wants to merge 1 commit into
masterfrom
claude/mcp-cimd-futures-58qc2v

Conversation

@davegaeddert

Copy link
Copy Markdown
Member

Why

Claude's Add custom connector dialog now marks Client ID Metadata Documents (CIMD, MCP SEP-991) as the recommended OAuth-client option, and Claude Code uses CIMD since v2.1.81. MCP 2026-07-28 deprecates DCR. A DCR-only plain.oauthserver forces people to pick the non-default option in Claude's dialog.

Claude selects CIMD only when the AS metadata advertises both client_id_metadata_document_supported: true and "none" in token_endpoint_auth_methods_supported; otherwise it silently falls back to DCR.

What

  • cimd.py (new): URL validation per draft-ietf-oauth-client-id-metadata-document-02; an SSRF-guarded fetch (every DNS answer must be globally routable, incl. seeing through IPv4-mapped / 6to4 / NAT64; the connection is pinned to the checked address with the hostname as SNI and Host, so DNS rebinding can't swap it; no redirects; 5 KB cap; 5 s deadline; JSON content type required); document validation (public clients only — private_key_jwt and any client_secret* rejected); and a resolver that stores the document as an OAuthApplication row keyed by the URL.
  • Caching: TTL from Cache-Control (s-maxage/max-age) clamped to [5 min, 24 h], 1 h default. A failed refetch serves the stored copy for 7 days (claude.ai's metadata endpoints have had outages that took down live-fetch-only servers).
  • Views: /authorize resolves URL client_ids. Token and revocation endpoints never fetch — the row exists from authorize, the client is proven by PKCE / the refresh token, and redirect_uri is bound to the code. All fetches happen outside transaction.atomic().
  • Metadata: advertises client_id_metadata_document_supported: true (gated by the new setting). DCR stays on by default.
  • Consent screen: shows the metadata host (the document's client_name is self-asserted), the redirect host, and a warning for loopback-only clients — per the MCP security-considerations page.
  • Settings: OAUTH_SERVER_ALLOW_CLIENT_ID_METADATA_DOCUMENTS (default True), OAUTH_SERVER_CLIENT_ID_METADATA_ALLOWED_HOSTS (default None = any public host).
  • Migration 0002: nullable metadata_fetched_at / metadata_expires_at on OAuthApplication.
  • _is_allowed_redirect_uri moved from views.py to models.py so cimd.py can share it. New dependency: httpx.
  • Docs: README section + FAQs (including the shared-client-identity trap: every Claude user presents the same client_id, so revocation must be scoped by (user, application)), plain-mcp README handshake wording, conformance README, and doctor.py now probes the CIMD path.

Testing

  • ./scripts/test plain-oauthserver: 165 passed. New tests/internal/test_cimd_internals.py (URL / address / TTL / fetch-via-httpx.MockTransport / document vectors, using Claude's and ChatGPT's frozen live documents) and tests/public/test_cimd.py (end-to-end through the endpoints: Claude full flow with no /register, Claude Code's port-less loopback with an ephemeral port, refetch-on-expiry, stale-if-error, grace expiry, rejections, allowlist, setting off, registered clients unaffected).
  • ./scripts/type-check plain-oauthserver clean; ruff clean.
  • Manual interop still to do before release: npx @modelcontextprotocol/conformance authorization --url <app> --client-id https://claude.ai/oauth/claude-code-client-metadata -p 3000 (works with no hosted document because our loopback match is port-agnostic), then Claude's connector dialog on the recommended option.

Plan and research: plain/oauthserver/cimd.md in the futures repo.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C1WfjEHGYHdbH5Yd9diDBg


Generated by Claude Code

A client_id may now be an HTTPS URL to a hosted metadata document, which
is what Claude's custom connector uses by default (its "recommended"
option) and what Claude Code uses since v2.1.81. A DCR-only server made
people pick the non-default option in Claude's dialog.

- New `cimd.py`: URL validation per draft-ietf-oauth-client-id-metadata-
  document-02, an SSRF-guarded fetch (public-address-only DNS, connection
  pinned to the checked address with the hostname as SNI, no redirects,
  5 KB cap, 5 s deadline), document validation (public clients only), and
  a resolver that stores the document as an `OAuthApplication` row keyed
  by the URL with a Cache-Control-derived TTL clamped to [5m, 24h] and a
  7-day stale-if-error grace.
- `/authorize` resolves URL client_ids; token and revocation endpoints
  never fetch. Metadata advertises `client_id_metadata_document_supported`.
- Consent screen shows the metadata host, the redirect host, and a warning
  for loopback-only clients, per the MCP security guidance.
- Settings: `OAUTH_SERVER_ALLOW_CLIENT_ID_METADATA_DOCUMENTS` (default on)
  and `OAUTH_SERVER_CLIENT_ID_METADATA_ALLOWED_HOSTS` (default any public
  host).
- Migration adds `metadata_fetched_at` / `metadata_expires_at`.
- `_is_allowed_redirect_uri` moved from views to models so cimd.py can
  share it. New dependency on httpx.
- Tests: internal vectors for the URL, address, TTL, fetch, and document
  rules using Claude's frozen documents; public end-to-end flows through
  the endpoints. Conformance doctor probes the CIMD path and the README
  documents the MCP conformance CLI invocation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1WfjEHGYHdbH5Yd9diDBg
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.

2 participants