Skip to content

Send application_type during Dynamic Client Registration (SEP-837)#2930

Merged
Kludex merged 1 commit into
mainfrom
sep-837-application-type
Jun 20, 2026
Merged

Send application_type during Dynamic Client Registration (SEP-837)#2930
Kludex merged 1 commit into
mainfrom
sep-837-application-type

Conversation

@Kludex

@Kludex Kludex commented Jun 20, 2026

Copy link
Copy Markdown
Member

Implements SEP-837 (the third client-side item of #2902): send application_type during Dynamic Client Registration.

What changed

  • Added application_type: Literal["web", "native"] = "native" to OAuthClientMetadata. It serializes into the DCR /register body via the existing model_dump.
  • Default is "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_type defaults to "web" under OIDC, which an authorization server can reject for the localhost redirect URIs native clients use; non-OIDC servers ignore the parameter.

Conformance

Unblocks and removes from the expected-failures baselines:

  • Default leg (expected-failures.yml): the 6 auth/iss-* and auth/offline-access-not-supported (their iss/scope checks already passed under Validate the iss authorization-response parameter (RFC 9207 / SEP-2468) #2921; they only failed sep-837-application-type-present at DCR).
  • 2026-07-28 leg (expected-failures.2026-07-28.yml): the above plus auth/metadata-default, auth/metadata-var{1,2,3}, auth/scope-from-*, auth/scope-omitted-when-undefined, and auth/token-endpoint-auth-{basic,post,none}.

The 2026-07-28 leg passes cleanly with no stale or unexpected entries.

Note - pre-existing default-leg failure, not from this PR: the default leg still reports auth/scope-step-up as an unexpected failure. This fails identically on clean main (verified): #2927 removed it from expected-failures.yml for the alpha.5 harness (which gates the SEP-2350 scope-union check to 2026-07-28+), but CI is still pinned to alpha.4, where the check fires on the default leg. It resolves when the harness pin moves to alpha.5 (already a TODO in conformance.yml) or when SEP-2350 lands. Out of scope here.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

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.
Comment thread src/mcp/shared/auth.py
@Kludex Kludex merged commit cf41441 into main Jun 20, 2026
34 checks passed
@Kludex Kludex deleted the sep-837-application-type branch June 20, 2026 16:19
Comment thread src/mcp/shared/auth.py
Comment on lines +70 to +72
# 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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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-121redirect_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

  1. A third-party client POSTs to /register with {"redirect_uris": ["https://app.example.com/callback"], "application_type": "web", ...}.
  2. RegistrationHandler.handle() validates the body via OAuthClientMetadata.model_validate_jsonclient_metadata.application_type == "web" (the value parses fine; this PR's Literal accepts it).
  3. The handler builds OAuthClientInformationFull(...) at register.py:100-121, listing every passthrough field by hand. application_type is omitted, so the inherited default kicks in: client_info.application_type == "native".
  4. client_info is stored via provider.register_client(client_info) — the persisted client record now says "native".
  5. The 201 response is rendered with PydanticJSONResponse(content=client_info), whose render uses model_dump_json(exclude_none=True). "native" is not None, 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.

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