diff --git a/.github/actions/conformance/expected-failures.2026-07-28.yml b/.github/actions/conformance/expected-failures.2026-07-28.yml index 129af7bd2b..f4450b87f1 100644 --- a/.github/actions/conformance/expected-failures.2026-07-28.yml +++ b/.github/actions/conformance/expected-failures.2026-07-28.yml @@ -28,32 +28,6 @@ client: # branch takes the per-request _meta path. - 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 --- # The auth fixture flow drives the 2025 stateful lifecycle; the 2026-mode # mock rejects the MCP POST before the scope-escalation behaviour these diff --git a/.github/actions/conformance/expected-failures.yml b/.github/actions/conformance/expected-failures.yml index 0dedf1c4cc..9ed50602ce 100644 --- a/.github/actions/conformance/expected-failures.yml +++ b/.github/actions/conformance/expected-failures.yml @@ -26,18 +26,6 @@ client: # SEP-2352 (authorization server migration): client does not re-register when # PRM authorization_servers changes. - 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 --- # SEP-990 (enterprise-managed authorization extension): no fixture handler / diff --git a/docs/migration.md b/docs/migration.md index c8e2ecdd0e..895a68963d 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1295,6 +1295,21 @@ If you relied on extra fields round-tripping through MCP types, move that data i ## 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 `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. diff --git a/src/mcp/shared/auth.py b/src/mcp/shared/auth.py index 03b5f3f7fd..f86a4d923c 100644 --- a/src/mcp/shared/auth.py +++ b/src/mcp/shared/auth.py @@ -67,6 +67,9 @@ class OAuthClientMetadata(BaseModel): # servers may also return additional types they support response_types: list[str] = ["code"] 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 # future use diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 2b7978b2e8..d33482a01b 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -1,6 +1,7 @@ """Tests for refactored OAuth client authentication implementation.""" import base64 +import json import time from unittest import mock from urllib.parse import parse_qs, quote, unquote, urlparse @@ -1049,6 +1050,21 @@ def test_falls_back_when_metadata_has_no_registration_endpoint(self): 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: """Test the auth flow in httpx.""" diff --git a/tests/interaction/auth/test_flow.py b/tests/interaction/auth/test_flow.py index 968fc5f980..ab96185796 100644 --- a/tests/interaction/auth/test_flow.py +++ b/tests/interaction/auth/test_flow.py @@ -203,6 +203,7 @@ async def test_the_dcr_request_carries_the_client_metadata() -> None: "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "scope": "mcp", + "application_type": "native", "client_name": "interaction-suite", "software_id": "interaction-test-suite", }