From 28362ea50bf54ed1211e3a039707212c0aea26f4 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Tue, 9 Jun 2026 21:34:09 +0100 Subject: [PATCH 1/5] feat(client,core): application_type client metadata with native/web inference (SEP-837) - Add optional application_type to OAuthClientMetadataSchema (and thus OAuthClientInformationFullSchema) so user-supplied values survive DCR request bodies and response parsing. - registerClient() infers application_type when absent: 'native' if every redirect URI is loopback (localhost, 127.0.0.1, [::1]) or a custom non-http(s) scheme, otherwise 'web'. Explicit values are never overridden. - New inferApplicationType(redirectUris) export. - JSDoc on OAuthClientProvider.clientMetadata + docs/client.md note. Closes #2198 --- .changeset/sep-837-application-type.md | 12 +++ packages/client/src/client/auth.ts | 44 ++++++++ packages/client/src/index.ts | 1 + packages/client/test/client/auth.test.ts | 116 ++++++++++++++++++++++ packages/core-internal/src/shared/auth.ts | 19 ++++ 5 files changed, 192 insertions(+) create mode 100644 .changeset/sep-837-application-type.md diff --git a/.changeset/sep-837-application-type.md b/.changeset/sep-837-application-type.md new file mode 100644 index 0000000000..f8e766992f --- /dev/null +++ b/.changeset/sep-837-application-type.md @@ -0,0 +1,12 @@ +--- +'@modelcontextprotocol/client': minor +'@modelcontextprotocol/core': patch +--- + +Support `application_type` client metadata with native/web inference for dynamic client registration (SEP-837) + +Per the MCP authorization specification, clients MUST specify an appropriate `application_type` when registering dynamically: OIDC-based authorization servers default the field to `'web'`, which conflicts with native/loopback redirect URIs and can cause registration to fail. + +- `OAuthClientMetadataSchema` (and therefore `OAuthClientInformationFullSchema`) now includes an optional `application_type` field, so user-supplied values are no longer stripped from registration requests or responses. +- `registerClient()` infers `application_type` when it is absent from the provided metadata: `'native'` if every redirect URI is loopback (`localhost`, `127.0.0.1`, `[::1]`) or uses a custom non-http(s) scheme, otherwise `'web'`. An explicitly provided value is never overridden. +- New `inferApplicationType(redirectUris)` export implements the inference rule. diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index a4d5b14c62..1ee67e3900 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -244,6 +244,13 @@ export interface OAuthClientProvider { /** * Metadata about this OAuth client. + * + * Per the MCP authorization specification (SEP-837), clients MUST specify an + * appropriate `application_type` when registering dynamically: `'native'` for + * desktop/CLI apps using loopback or custom-scheme redirect URIs, `'web'` for + * remote browser-based apps. If `application_type` is omitted, + * {@linkcode registerClient} infers it from `redirect_uris` (see + * {@linkcode inferApplicationType}). */ get clientMetadata(): OAuthClientMetadata; @@ -2282,6 +2289,43 @@ export async function fetchToken( }); } +/** + * Infers the OIDC `application_type` for dynamic client registration from a + * client's redirect URIs (SEP-837). + * + * Returns `'native'` when every redirect URI is either a loopback address + * (`localhost`, `127.0.0.1`, or `[::1]`) or uses a custom non-http(s) scheme + * (e.g. `myapp://callback`); otherwise returns `'web'`. + * + * OIDC-based authorization servers default `application_type` to `'web'`, which + * rejects loopback/custom-scheme redirect URIs — so native apps must declare + * themselves explicitly. Invalid or empty inputs conservatively yield `'web'`, + * matching the OIDC default. + */ +export function inferApplicationType(redirectUris: string[]): 'web' | 'native' { + if (redirectUris.length === 0) { + return 'web'; + } + + return redirectUris.every(uri => isNativeRedirectUri(uri)) ? 'native' : 'web'; +} + +function isNativeRedirectUri(uri: string): boolean { + let url: URL; + try { + url = new URL(uri); + } catch { + return false; + } + + if (url.protocol === 'http:' || url.protocol === 'https:') { + return url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]'; + } + + // Custom (non-http/https) schemes are used by native apps. + return true; +} + /** * Performs OAuth 2.0 Dynamic Client Registration according to * {@link https://datatracker.ietf.org/doc/html/rfc7591 | RFC 7591}. diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index c9088fc499..f4ef2f9487 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -30,6 +30,7 @@ export { extractResourceMetadataUrl, extractWWWAuthenticateParams, fetchToken, + inferApplicationType, isHttpsUrl, isStrictScopeSuperset, parseErrorResponse, diff --git a/packages/client/test/client/auth.test.ts b/packages/client/test/client/auth.test.ts index 62c6faed9a..2d6cbfe77f 100644 --- a/packages/client/test/client/auth.test.ts +++ b/packages/client/test/client/auth.test.ts @@ -2577,6 +2577,122 @@ describe('OAuth Authorization', () => { const tokens = await refreshAuthorization('https://auth.example.com', { clientInformation, refreshToken: 'rt-old' }); expect(tokens.refresh_token).toBe('rt-new'); }); + + describe('application_type (SEP-837)', () => { + const mockRegistrationResponse = (clientInfo: Record) => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => clientInfo + }); + }; + + const lastRegistrationBody = (): Record => { + const [, init] = mockFetch.mock.calls.at(-1)!; + return JSON.parse(init.body as string); + }; + + it('infers native for loopback-only redirect URIs', async () => { + mockRegistrationResponse(validClientInfo); + + await registerClient('https://auth.example.com', { + clientMetadata: { + redirect_uris: ['http://localhost:3000/callback', 'http://127.0.0.1:8080/cb', 'http://[::1]:9090/cb'] + } + }); + + expect(lastRegistrationBody().application_type).toBe('native'); + }); + + it('infers native for custom-scheme redirect URIs', async () => { + mockRegistrationResponse(validClientInfo); + + await registerClient('https://auth.example.com', { + clientMetadata: { + redirect_uris: ['myapp://oauth/callback'] + } + }); + + expect(lastRegistrationBody().application_type).toBe('native'); + }); + + it('infers web for https redirect URIs', async () => { + mockRegistrationResponse(validClientInfo); + + await registerClient('https://auth.example.com', { + clientMetadata: { + redirect_uris: ['https://app.example.com/callback'] + } + }); + + expect(lastRegistrationBody().application_type).toBe('web'); + }); + + it('infers web for mixed loopback and remote redirect URIs', async () => { + mockRegistrationResponse(validClientInfo); + + await registerClient('https://auth.example.com', { + clientMetadata: { + redirect_uris: ['http://localhost:3000/callback', 'https://app.example.com/callback'] + } + }); + + expect(lastRegistrationBody().application_type).toBe('web'); + }); + + it('never overrides an explicitly provided application_type', async () => { + mockRegistrationResponse(validClientInfo); + + await registerClient('https://auth.example.com', { + clientMetadata: { + // Loopback-only redirect URIs would infer 'native', but the + // explicit value must win. + redirect_uris: ['http://localhost:3000/callback'], + application_type: 'web' + } + }); + + expect(lastRegistrationBody().application_type).toBe('web'); + }); + + it('retains application_type from the registration response', async () => { + mockRegistrationResponse({ ...validClientInfo, application_type: 'native' }); + + const clientInfo = await registerClient('https://auth.example.com', { + clientMetadata: validClientMetadata + }); + + expect(clientInfo.application_type).toBe('native'); + }); + }); + }); + + describe('inferApplicationType', () => { + it('returns native when every redirect URI is loopback', () => { + expect(inferApplicationType(['http://localhost/callback', 'http://127.0.0.1:1234/cb', 'http://[::1]/cb'])).toBe('native'); + }); + + it('returns native for custom non-http(s) schemes', () => { + expect(inferApplicationType(['myapp://oauth/callback', 'com.example.app:/redirect'])).toBe('native'); + }); + + it('returns web for remote http(s) redirect URIs', () => { + expect(inferApplicationType(['https://app.example.com/callback'])).toBe('web'); + expect(inferApplicationType(['http://app.example.com/callback'])).toBe('web'); + }); + + it('returns web when any redirect URI is remote', () => { + expect(inferApplicationType(['http://localhost:3000/callback', 'https://app.example.com/callback'])).toBe('web'); + }); + + it('does not treat localhost subdomains as loopback', () => { + expect(inferApplicationType(['https://localhost.example.com/callback'])).toBe('web'); + }); + + it('returns web for empty or unparseable input', () => { + expect(inferApplicationType([])).toBe('web'); + expect(inferApplicationType(['not a url'])).toBe('web'); + }); }); describe('auth function', () => { diff --git a/packages/core-internal/src/shared/auth.ts b/packages/core-internal/src/shared/auth.ts index e21076d817..87c7aac446 100644 --- a/packages/core-internal/src/shared/auth.ts +++ b/packages/core-internal/src/shared/auth.ts @@ -183,6 +183,25 @@ export const OptionalSafeUrlSchema = SafeUrlSchema.optional().or(z.literal('').t export const OAuthClientMetadataSchema = z .object({ redirect_uris: z.array(SafeUrlSchema), + /** + * OpenID Connect Dynamic Client Registration `application_type`. + * + * The standard values are `'web'` and `'native'`. OIDC-based authorization + * servers default this to `'web'` when omitted, which conflicts with + * native/loopback redirect URIs (e.g. `http://localhost`, `http://127.0.0.1`, + * or custom URI schemes) and can cause registration to fail. Per the MCP + * authorization specification (SEP-837), clients MUST specify an appropriate + * `application_type` when registering: native apps (desktop, CLI, anything + * using loopback or custom-scheme redirects) SHOULD use `'native'`, while + * remote browser-based apps SHOULD use `'web'`. Authorization servers that + * do not implement OIDC registration ignore this field. + * + * Typed as a plain string because OIDC permits extension values beyond + * `'web'` and `'native'`. + * + * @see https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata + */ + application_type: z.string().optional(), token_endpoint_auth_method: z.string().optional(), grant_types: z.array(z.string()).optional(), response_types: z.array(z.string()).optional(), From 2047e85b297127104ba7a63adb8f2d5e7e6856c8 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 25 Jun 2026 11:27:28 +0200 Subject: [PATCH 2/5] fix(client): tighten application type inference --- .changeset/sep-837-application-type.md | 3 +- packages/client/src/client/auth.ts | 15 +++++---- packages/client/src/index.ts | 1 - packages/client/test/client/auth.test.ts | 40 +++++++----------------- 4 files changed, 22 insertions(+), 37 deletions(-) diff --git a/.changeset/sep-837-application-type.md b/.changeset/sep-837-application-type.md index f8e766992f..3ff926eafe 100644 --- a/.changeset/sep-837-application-type.md +++ b/.changeset/sep-837-application-type.md @@ -8,5 +8,4 @@ Support `application_type` client metadata with native/web inference for dynamic Per the MCP authorization specification, clients MUST specify an appropriate `application_type` when registering dynamically: OIDC-based authorization servers default the field to `'web'`, which conflicts with native/loopback redirect URIs and can cause registration to fail. - `OAuthClientMetadataSchema` (and therefore `OAuthClientInformationFullSchema`) now includes an optional `application_type` field, so user-supplied values are no longer stripped from registration requests or responses. -- `registerClient()` infers `application_type` when it is absent from the provided metadata: `'native'` if every redirect URI is loopback (`localhost`, `127.0.0.1`, `[::1]`) or uses a custom non-http(s) scheme, otherwise `'web'`. An explicitly provided value is never overridden. -- New `inferApplicationType(redirectUris)` export implements the inference rule. +- `registerClient()` infers `application_type` when it is absent from the provided metadata: `'native'` if every redirect URI is an HTTP loopback URI (`localhost`, `127.0.0.1`, `[::1]`) or uses a custom non-http(s) scheme, otherwise `'web'`. An explicitly provided value is never overridden. diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index 1ee67e3900..acf87da521 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -248,9 +248,8 @@ export interface OAuthClientProvider { * Per the MCP authorization specification (SEP-837), clients MUST specify an * appropriate `application_type` when registering dynamically: `'native'` for * desktop/CLI apps using loopback or custom-scheme redirect URIs, `'web'` for - * remote browser-based apps. If `application_type` is omitted, - * {@linkcode registerClient} infers it from `redirect_uris` (see - * {@linkcode inferApplicationType}). + * remote browser-based apps. If `application_type` is omitted, {@linkcode registerClient} + * infers it from `redirect_uris`. */ get clientMetadata(): OAuthClientMetadata; @@ -2293,7 +2292,7 @@ export async function fetchToken( * Infers the OIDC `application_type` for dynamic client registration from a * client's redirect URIs (SEP-837). * - * Returns `'native'` when every redirect URI is either a loopback address + * Returns `'native'` when every redirect URI is either an HTTP loopback address * (`localhost`, `127.0.0.1`, or `[::1]`) or uses a custom non-http(s) scheme * (e.g. `myapp://callback`); otherwise returns `'web'`. * @@ -2302,7 +2301,7 @@ export async function fetchToken( * themselves explicitly. Invalid or empty inputs conservatively yield `'web'`, * matching the OIDC default. */ -export function inferApplicationType(redirectUris: string[]): 'web' | 'native' { +function inferApplicationType(redirectUris: string[]): 'web' | 'native' { if (redirectUris.length === 0) { return 'web'; } @@ -2318,10 +2317,14 @@ function isNativeRedirectUri(uri: string): boolean { return false; } - if (url.protocol === 'http:' || url.protocol === 'https:') { + if (url.protocol === 'http:') { return url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]'; } + if (url.protocol === 'https:') { + return false; + } + // Custom (non-http/https) schemes are used by native apps. return true; } diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index f4ef2f9487..c9088fc499 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -30,7 +30,6 @@ export { extractResourceMetadataUrl, extractWWWAuthenticateParams, fetchToken, - inferApplicationType, isHttpsUrl, isStrictScopeSuperset, parseErrorResponse, diff --git a/packages/client/test/client/auth.test.ts b/packages/client/test/client/auth.test.ts index 2d6cbfe77f..1d1eb2b0ff 100644 --- a/packages/client/test/client/auth.test.ts +++ b/packages/client/test/client/auth.test.ts @@ -2628,6 +2628,18 @@ describe('OAuth Authorization', () => { expect(lastRegistrationBody().application_type).toBe('web'); }); + it('infers web for https loopback redirect URIs', async () => { + mockRegistrationResponse(validClientInfo); + + await registerClient('https://auth.example.com', { + clientMetadata: { + redirect_uris: ['https://localhost:8443/callback', 'https://127.0.0.1:8443/callback', 'https://[::1]:8443/callback'] + } + }); + + expect(lastRegistrationBody().application_type).toBe('web'); + }); + it('infers web for mixed loopback and remote redirect URIs', async () => { mockRegistrationResponse(validClientInfo); @@ -2667,34 +2679,6 @@ describe('OAuth Authorization', () => { }); }); - describe('inferApplicationType', () => { - it('returns native when every redirect URI is loopback', () => { - expect(inferApplicationType(['http://localhost/callback', 'http://127.0.0.1:1234/cb', 'http://[::1]/cb'])).toBe('native'); - }); - - it('returns native for custom non-http(s) schemes', () => { - expect(inferApplicationType(['myapp://oauth/callback', 'com.example.app:/redirect'])).toBe('native'); - }); - - it('returns web for remote http(s) redirect URIs', () => { - expect(inferApplicationType(['https://app.example.com/callback'])).toBe('web'); - expect(inferApplicationType(['http://app.example.com/callback'])).toBe('web'); - }); - - it('returns web when any redirect URI is remote', () => { - expect(inferApplicationType(['http://localhost:3000/callback', 'https://app.example.com/callback'])).toBe('web'); - }); - - it('does not treat localhost subdomains as loopback', () => { - expect(inferApplicationType(['https://localhost.example.com/callback'])).toBe('web'); - }); - - it('returns web for empty or unparseable input', () => { - expect(inferApplicationType([])).toBe('web'); - expect(inferApplicationType(['not a url'])).toBe('web'); - }); - }); - describe('auth function', () => { const mockProvider: OAuthClientProvider = { get redirectUrl() { From 5623ba78f2962cd1174002e35d342d928a606a2d Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Wed, 1 Jul 2026 10:00:32 +0100 Subject: [PATCH 3/5] fix(client): repair sep-837 rebase --- packages/client/src/client/auth.ts | 41 --------- packages/client/test/client/auth.test.ts | 100 ---------------------- packages/core-internal/src/shared/auth.ts | 9 -- 3 files changed, 150 deletions(-) diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index acf87da521..0774efb642 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -2288,47 +2288,6 @@ export async function fetchToken( }); } -/** - * Infers the OIDC `application_type` for dynamic client registration from a - * client's redirect URIs (SEP-837). - * - * Returns `'native'` when every redirect URI is either an HTTP loopback address - * (`localhost`, `127.0.0.1`, or `[::1]`) or uses a custom non-http(s) scheme - * (e.g. `myapp://callback`); otherwise returns `'web'`. - * - * OIDC-based authorization servers default `application_type` to `'web'`, which - * rejects loopback/custom-scheme redirect URIs — so native apps must declare - * themselves explicitly. Invalid or empty inputs conservatively yield `'web'`, - * matching the OIDC default. - */ -function inferApplicationType(redirectUris: string[]): 'web' | 'native' { - if (redirectUris.length === 0) { - return 'web'; - } - - return redirectUris.every(uri => isNativeRedirectUri(uri)) ? 'native' : 'web'; -} - -function isNativeRedirectUri(uri: string): boolean { - let url: URL; - try { - url = new URL(uri); - } catch { - return false; - } - - if (url.protocol === 'http:') { - return url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]'; - } - - if (url.protocol === 'https:') { - return false; - } - - // Custom (non-http/https) schemes are used by native apps. - return true; -} - /** * Performs OAuth 2.0 Dynamic Client Registration according to * {@link https://datatracker.ietf.org/doc/html/rfc7591 | RFC 7591}. diff --git a/packages/client/test/client/auth.test.ts b/packages/client/test/client/auth.test.ts index 1d1eb2b0ff..62c6faed9a 100644 --- a/packages/client/test/client/auth.test.ts +++ b/packages/client/test/client/auth.test.ts @@ -2577,106 +2577,6 @@ describe('OAuth Authorization', () => { const tokens = await refreshAuthorization('https://auth.example.com', { clientInformation, refreshToken: 'rt-old' }); expect(tokens.refresh_token).toBe('rt-new'); }); - - describe('application_type (SEP-837)', () => { - const mockRegistrationResponse = (clientInfo: Record) => { - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => clientInfo - }); - }; - - const lastRegistrationBody = (): Record => { - const [, init] = mockFetch.mock.calls.at(-1)!; - return JSON.parse(init.body as string); - }; - - it('infers native for loopback-only redirect URIs', async () => { - mockRegistrationResponse(validClientInfo); - - await registerClient('https://auth.example.com', { - clientMetadata: { - redirect_uris: ['http://localhost:3000/callback', 'http://127.0.0.1:8080/cb', 'http://[::1]:9090/cb'] - } - }); - - expect(lastRegistrationBody().application_type).toBe('native'); - }); - - it('infers native for custom-scheme redirect URIs', async () => { - mockRegistrationResponse(validClientInfo); - - await registerClient('https://auth.example.com', { - clientMetadata: { - redirect_uris: ['myapp://oauth/callback'] - } - }); - - expect(lastRegistrationBody().application_type).toBe('native'); - }); - - it('infers web for https redirect URIs', async () => { - mockRegistrationResponse(validClientInfo); - - await registerClient('https://auth.example.com', { - clientMetadata: { - redirect_uris: ['https://app.example.com/callback'] - } - }); - - expect(lastRegistrationBody().application_type).toBe('web'); - }); - - it('infers web for https loopback redirect URIs', async () => { - mockRegistrationResponse(validClientInfo); - - await registerClient('https://auth.example.com', { - clientMetadata: { - redirect_uris: ['https://localhost:8443/callback', 'https://127.0.0.1:8443/callback', 'https://[::1]:8443/callback'] - } - }); - - expect(lastRegistrationBody().application_type).toBe('web'); - }); - - it('infers web for mixed loopback and remote redirect URIs', async () => { - mockRegistrationResponse(validClientInfo); - - await registerClient('https://auth.example.com', { - clientMetadata: { - redirect_uris: ['http://localhost:3000/callback', 'https://app.example.com/callback'] - } - }); - - expect(lastRegistrationBody().application_type).toBe('web'); - }); - - it('never overrides an explicitly provided application_type', async () => { - mockRegistrationResponse(validClientInfo); - - await registerClient('https://auth.example.com', { - clientMetadata: { - // Loopback-only redirect URIs would infer 'native', but the - // explicit value must win. - redirect_uris: ['http://localhost:3000/callback'], - application_type: 'web' - } - }); - - expect(lastRegistrationBody().application_type).toBe('web'); - }); - - it('retains application_type from the registration response', async () => { - mockRegistrationResponse({ ...validClientInfo, application_type: 'native' }); - - const clientInfo = await registerClient('https://auth.example.com', { - clientMetadata: validClientMetadata - }); - - expect(clientInfo.application_type).toBe('native'); - }); - }); }); describe('auth function', () => { diff --git a/packages/core-internal/src/shared/auth.ts b/packages/core-internal/src/shared/auth.ts index 87c7aac446..6367909c3b 100644 --- a/packages/core-internal/src/shared/auth.ts +++ b/packages/core-internal/src/shared/auth.ts @@ -205,15 +205,6 @@ export const OAuthClientMetadataSchema = z token_endpoint_auth_method: z.string().optional(), grant_types: z.array(z.string()).optional(), response_types: z.array(z.string()).optional(), - /** - * OIDC Dynamic Client Registration `application_type`. MCP clients MUST set - * this to `'native'` or `'web'` when registering (SEP-837); the SDK defaults - * it from `redirect_uris` when omitted. Typed as `string` (not an enum) so - * that parsing an authorization server's registration response — which under - * RFC 7591 may echo extension values — never rejects the document on this - * field alone. - */ - application_type: z.string().optional(), client_name: z.string().optional(), client_uri: SafeUrlSchema.optional(), logo_uri: OptionalSafeUrlSchema, From 33cc2eb8cd808e4a2a6f47b9bcc3d331546b99d0 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Wed, 1 Jul 2026 10:43:17 +0100 Subject: [PATCH 4/5] fix(client): clarify application type inference --- .changeset/sep-837-application-type.md | 2 +- packages/client/src/client/auth.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.changeset/sep-837-application-type.md b/.changeset/sep-837-application-type.md index 3ff926eafe..719d6bfbc7 100644 --- a/.changeset/sep-837-application-type.md +++ b/.changeset/sep-837-application-type.md @@ -8,4 +8,4 @@ Support `application_type` client metadata with native/web inference for dynamic Per the MCP authorization specification, clients MUST specify an appropriate `application_type` when registering dynamically: OIDC-based authorization servers default the field to `'web'`, which conflicts with native/loopback redirect URIs and can cause registration to fail. - `OAuthClientMetadataSchema` (and therefore `OAuthClientInformationFullSchema`) now includes an optional `application_type` field, so user-supplied values are no longer stripped from registration requests or responses. -- `registerClient()` infers `application_type` when it is absent from the provided metadata: `'native'` if every redirect URI is an HTTP loopback URI (`localhost`, `127.0.0.1`, `[::1]`) or uses a custom non-http(s) scheme, otherwise `'web'`. An explicitly provided value is never overridden. +- `registerClient()` infers `application_type` when it is absent from the provided metadata: `'native'` if any redirect URI uses a loopback host (`localhost`, `127.0.0.1`, `[::1]`) or a custom non-http(s) scheme, otherwise `'web'`. An explicitly provided value is never overridden. diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index 0774efb642..eac23d828c 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -249,7 +249,8 @@ export interface OAuthClientProvider { * appropriate `application_type` when registering dynamically: `'native'` for * desktop/CLI apps using loopback or custom-scheme redirect URIs, `'web'` for * remote browser-based apps. If `application_type` is omitted, {@linkcode registerClient} - * infers it from `redirect_uris`. + * infers `'native'` when any `redirect_uris` entry uses a loopback host or custom + * URI scheme; otherwise it infers `'web'`. */ get clientMetadata(): OAuthClientMetadata; @@ -824,9 +825,9 @@ export function assertSecureTokenEndpoint(tokenEndpoint: string | URL): URL { /** * Derives an OIDC `application_type` from a client's registered redirect URIs - * when the consumer has not set one explicitly (SEP-837). Loopback hosts and - * non-`http(s)` custom URI schemes indicate a native application (RFC 8252); - * everything else is treated as a web application. The result is a heuristic + * when the consumer has not set one explicitly (SEP-837). Any loopback host or + * non-`http(s)` custom URI scheme indicates a native application (RFC 8252); + * otherwise the client is treated as a web application. The result is a heuristic * default — callers that know better should set `clientMetadata.application_type` * themselves, which {@linkcode resolveClientMetadata} never overwrites. * @@ -861,8 +862,8 @@ function deriveApplicationType(redirectUris: readonly string[] | undefined): 'na * `redirectUrl`) get no `grant_types` default. This default applies to the * Dynamic Client Registration body only — it does **not** drive * {@linkcode determineScope}'s `offline_access` augmentation. - * - `application_type` defaults from `redirect_uris`: loopback redirect hosts - * and custom URI schemes → `'native'`, otherwise `'web'` (SEP-837 / RFC 8252). + * - `application_type` defaults from `redirect_uris`: any loopback redirect host + * or custom URI scheme → `'native'`, otherwise `'web'` (SEP-837 / RFC 8252). * * A field the consumer set explicitly is **never** overwritten. {@linkcode auth} * calls this once at the top of the flow; direct callers of From 035f19f96290a2350b93111e5c58e6783f0ae672 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Wed, 1 Jul 2026 11:07:48 +0100 Subject: [PATCH 5/5] fix(changeset): drop stale sep-837 release note --- .changeset/sep-837-application-type.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .changeset/sep-837-application-type.md diff --git a/.changeset/sep-837-application-type.md b/.changeset/sep-837-application-type.md deleted file mode 100644 index 719d6bfbc7..0000000000 --- a/.changeset/sep-837-application-type.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@modelcontextprotocol/client': minor -'@modelcontextprotocol/core': patch ---- - -Support `application_type` client metadata with native/web inference for dynamic client registration (SEP-837) - -Per the MCP authorization specification, clients MUST specify an appropriate `application_type` when registering dynamically: OIDC-based authorization servers default the field to `'web'`, which conflicts with native/loopback redirect URIs and can cause registration to fail. - -- `OAuthClientMetadataSchema` (and therefore `OAuthClientInformationFullSchema`) now includes an optional `application_type` field, so user-supplied values are no longer stripped from registration requests or responses. -- `registerClient()` infers `application_type` when it is absent from the provided metadata: `'native'` if any redirect URI uses a loopback host (`localhost`, `127.0.0.1`, `[::1]`) or a custom non-http(s) scheme, otherwise `'web'`. An explicitly provided value is never overridden.