Send application_type during Dynamic Client Registration (SEP-837)#2930
Conversation
Add application_type to OAuthClientMetadata, defaulting to "native" since MCP clients typically register loopback redirect URIs; remote browser-based clients set "web". The field serializes into the DCR /register body via the existing model_dump, and non-OIDC servers ignore it. This unblocks the auth/iss-* and offline-access-not-supported conformance scenarios (their iss/scope checks already passed; they only failed the sep-837-application-type-present check at DCR), plus the metadata/scope/ token-endpoint-auth scenarios on the 2026-07-28 leg. Burns those down from both expected-failures baselines.
| # SEP-837: OIDC application_type. Defaults to "native" since MCP clients typically use | ||
| # loopback redirect URIs; set "web" for remote browser-based clients on a non-local host. | ||
| application_type: Literal["web", "native"] = "native" |
There was a problem hiding this comment.
🔴 The new application_type field is shared with the server-side DCR path, but RegistrationHandler.handle() (src/mcp/server/auth/handlers/register.py:100-121) builds OAuthClientInformationFull from an explicit field allowlist that does not include it, so a client registering with application_type="web" is silently rewritten to the new default "native" in both the stored client record and the RFC 7591 registration response (and clients omitting the field also get "native" echoed, contrary to the OIDC default of "web"). Forward application_type=client_metadata.application_type in the handler's passthrough block.
Extended reasoning...
What the bug is
This PR adds application_type: Literal["web", "native"] = "native" to OAuthClientMetadata (src/mcp/shared/auth.py:70-72). That model is shared with the server-side Dynamic Client Registration path: RegistrationHandler.handle() parses the inbound /register body into OAuthClientMetadata and then constructs the stored/returned OAuthClientInformationFull (which subclasses OAuthClientMetadata) by explicitly enumerating the passthrough fields at src/mcp/server/auth/handlers/register.py:100-121 — redirect_uris, token_endpoint_auth_method, grant_types, ..., software_version. application_type is not in that list, so the constructed client_info always falls back to the model default "native", regardless of what the registering client sent.
Code path / step-by-step proof
- A third-party client POSTs to
/registerwith{"redirect_uris": ["https://app.example.com/callback"], "application_type": "web", ...}. RegistrationHandler.handle()validates the body viaOAuthClientMetadata.model_validate_json—client_metadata.application_type == "web"(the value parses fine; this PR'sLiteralaccepts it).- The handler builds
OAuthClientInformationFull(...)at register.py:100-121, listing every passthrough field by hand.application_typeis omitted, so the inherited default kicks in:client_info.application_type == "native". client_infois stored viaprovider.register_client(client_info)— the persisted client record now says"native".- The 201 response is rendered with
PydanticJSONResponse(content=client_info), whoserenderusesmodel_dump_json(exclude_none=True)."native"is notNone, so the wire response actively carries"application_type": "native"— misreporting the registered metadata, which RFC 7591 §3.2.1 expects to reflect the registered values. The same happens for clients that omit the field, which under the OIDC default should mean"web".
Why nothing prevents it and what the impact is
Before this PR the key simply wasn't modeled: an inbound application_type was an ignored extra field and the server neither stored nor echoed anything — which is fine. After this PR the server affirmatively rewrites an explicitly supplied value and echoes the wrong one. Nothing else in src/ consumes application_type, so there is no authorization/enforcement consequence inside the SDK today; the harm is an incorrect stored record handed to provider implementations and a misleading registration response that the registering client may persist (the SDK client itself stores the response into TokenStorage). RFC 7591 technically permits an AS to substitute requested metadata, but here the rewrite is an accidental drop introduced by adding the field to the shared request model without updating the server passthrough, not a policy decision.
How to fix
Add one line to the passthrough block in RegistrationHandler.handle():
application_type=client_metadata.application_type,so the stored and echoed OAuthClientInformationFull reflects what the client actually registered.
Implements SEP-837 (the third client-side item of #2902): send
application_typeduring Dynamic Client Registration.What changed
application_type: Literal["web", "native"] = "native"toOAuthClientMetadata. It serializes into the DCR/registerbody via the existingmodel_dump."native", matching the Rust SDK and the common MCP case (CLI/desktop clients with loopback redirect URIs - all our examples). Remote browser-based clients override to"web".Per the spec: omitting
application_typedefaults to"web"under OIDC, which an authorization server can reject for thelocalhostredirect URIs native clients use; non-OIDC servers ignore the parameter.Conformance
Unblocks and removes from the expected-failures baselines:
expected-failures.yml): the 6auth/iss-*andauth/offline-access-not-supported(their iss/scope checks already passed under Validate theissauthorization-response parameter (RFC 9207 / SEP-2468) #2921; they only failedsep-837-application-type-presentat DCR).expected-failures.2026-07-28.yml): the above plusauth/metadata-default,auth/metadata-var{1,2,3},auth/scope-from-*,auth/scope-omitted-when-undefined, andauth/token-endpoint-auth-{basic,post,none}.The 2026-07-28 leg passes cleanly with no stale or unexpected entries.
AI Disclaimer
This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.