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

This commit is contained in:
Marcelo Trylesinski 2026-06-20 17:19:12 +01:00 • committed by GitHub
commit cf41441e44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 35 additions and 38 deletions

View file

@ -28,32 +28,6 @@ client:
# branch takes the per-request _meta path. # branch takes the per-request _meta path.
- tools_call - tools_call
# --- SEP-837 (application_type during DCR) ---
# The sep-837-application-type-present check only fires on 2026-version
# runs; the client omits application_type during Dynamic Client
# Registration, so every auth scenario that reaches DCR fails it on this
# leg (the same scenarios pass at their default version in the 2025 legs).
- auth/metadata-default
- auth/metadata-var1
- auth/metadata-var2
- auth/metadata-var3
- auth/scope-from-www-authenticate
- auth/scope-from-scopes-supported
- auth/scope-omitted-when-undefined
- auth/token-endpoint-auth-basic
- auth/token-endpoint-auth-post
- auth/token-endpoint-auth-none
- auth/offline-access-not-supported
# SEP-2468 (authorization response iss parameter) is implemented, but these
# 2026-introduced scenarios reach DCR and so still fail the application_type
# check above; they unblock with SEP-837, not SEP-2468.
- auth/iss-supported
- auth/iss-not-advertised
- auth/iss-supported-missing
- auth/iss-wrong-issuer
- auth/iss-unexpected
- auth/iss-normalized
# --- Auth scenarios cut short by the 2026 connection lifecycle --- # --- Auth scenarios cut short by the 2026 connection lifecycle ---
# The auth fixture flow drives the 2025 stateful lifecycle; the 2026-mode # The auth fixture flow drives the 2025 stateful lifecycle; the 2026-mode
# mock rejects the MCP POST before the scope-escalation behaviour these # mock rejects the MCP POST before the scope-escalation behaviour these

View file

@ -26,18 +26,6 @@ client:
# SEP-2352 (authorization server migration): client does not re-register when # SEP-2352 (authorization server migration): client does not re-register when
# PRM authorization_servers changes. # PRM authorization_servers changes.
- auth/authorization-server-migration - auth/authorization-server-migration
# SEP-837 (application_type during DCR): the check fires on every non-legacy
# spec version (the default LATEST is 2026-07-28). The client omits
# application_type during Dynamic Client Registration, so every scenario that
# reaches DCR fails it. SEP-2468 iss validation is implemented, so these now
# fail only on the application_type check, not on iss.
- auth/offline-access-not-supported
- auth/iss-supported
- auth/iss-not-advertised
- auth/iss-supported-missing
- auth/iss-wrong-issuer
- auth/iss-unexpected
- auth/iss-normalized
# --- Pre-existing scenarios that fail on checks added after conformance 0.1.15 --- # --- Pre-existing scenarios that fail on checks added after conformance 0.1.15 ---
# SEP-990 (enterprise-managed authorization extension): no fixture handler / # SEP-990 (enterprise-managed authorization extension): no fixture handler /

View file

@ -1303,6 +1303,21 @@ If you relied on extra fields round-tripping through MCP types, move that data i
## New Features ## New Features
### OAuth Dynamic Client Registration sends `application_type` (SEP-837)
`OAuthClientMetadata` now carries an `application_type` field that is sent during Dynamic Client Registration. It defaults to `"native"`, which suits MCP clients that use loopback redirect URIs (CLI and desktop apps); browser-based clients served from a non-local host should set it to `"web"`:
```python
from mcp.shared.auth import OAuthClientMetadata
client_metadata = OAuthClientMetadata(
redirect_uris=["https://app.example.com/callback"],
application_type="web",
)
```
Under OIDC, omitting `application_type` defaults to `"web"`, which an authorization server may reject for the `localhost` redirect URIs native clients use; sending `"native"` avoids that. Non-OIDC servers ignore the parameter.
### 2025-11-25 and 2026-07-28 protocol fields modeled ### 2025-11-25 and 2026-07-28 protocol fields modeled
`mcp.types` models the 2025-11-25 and 2026-07-28 protocol fields (e.g. `resultType`, `ttlMs`/`cacheScope` on cacheable results, `inputResponses`/`requestState` on retried requests), so inbound payloads carrying these keys parse into typed fields and round-trip. `ttlMs`/`cacheScope` default to `0`/`"private"` (immediately stale, not shared-cacheable); `resultType` defaults to `"complete"` on concrete results (`None` on `EmptyResult`); the server strips all of them from the wire at pre-2026 versions. `mcp.types` models the 2025-11-25 and 2026-07-28 protocol fields (e.g. `resultType`, `ttlMs`/`cacheScope` on cacheable results, `inputResponses`/`requestState` on retried requests), so inbound payloads carrying these keys parse into typed fields and round-trip. `ttlMs`/`cacheScope` default to `0`/`"private"` (immediately stale, not shared-cacheable); `resultType` defaults to `"complete"` on concrete results (`None` on `EmptyResult`); the server strips all of them from the wire at pre-2026 versions.

View file

@ -67,6 +67,9 @@ class OAuthClientMetadata(BaseModel):
# servers may also return additional types they support # servers may also return additional types they support
response_types: list[str] = ["code"] response_types: list[str] = ["code"]
scope: str | None = None scope: str | None = None
# 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"
# these fields are currently unused, but we support & store them for potential # these fields are currently unused, but we support & store them for potential
# future use # future use

View file

@ -1,6 +1,7 @@
"""Tests for refactored OAuth client authentication implementation.""" """Tests for refactored OAuth client authentication implementation."""
import base64 import base64
import json
import time import time
from unittest import mock from unittest import mock
from urllib.parse import parse_qs, quote, unquote, urlparse from urllib.parse import parse_qs, quote, unquote, urlparse
@ -1049,6 +1050,21 @@ class TestCreateClientRegistrationRequest:
assert request.method == "POST" assert request.method == "POST"
def test_registration_request_sends_application_type():
"""SEP-837: the DCR body carries application_type, defaulting to native and overridable."""
redirect_uris: list[AnyUrl] = [AnyUrl("http://localhost:3000/callback")]
default_request = create_client_registration_request(
None, OAuthClientMetadata(redirect_uris=redirect_uris), "https://auth.example.com"
)
assert json.loads(default_request.content)["application_type"] == "native"
web_request = create_client_registration_request(
None, OAuthClientMetadata(redirect_uris=redirect_uris, application_type="web"), "https://auth.example.com"
)
assert json.loads(web_request.content)["application_type"] == "web"
class TestAuthFlow: class TestAuthFlow:
"""Test the auth flow in httpx.""" """Test the auth flow in httpx."""

View file

@ -203,6 +203,7 @@ async def test_the_dcr_request_carries_the_client_metadata() -> None:
"grant_types": ["authorization_code", "refresh_token"], "grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"], "response_types": ["code"],
"scope": "mcp", "scope": "mcp",
"application_type": "native",
"client_name": "interaction-suite", "client_name": "interaction-suite",
"software_id": "interaction-test-suite", "software_id": "interaction-test-suite",
} }