From c1df1421c606b9597a5174939e8f7108f18dd77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Karaaslan?= Date: Thu, 15 Oct 2026 16:32:21 +0300 Subject: [PATCH 1/7] test(auth): cover OAuth client-credentials metadata without authorization URL --- .../core-internal/test/shared/auth.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/core-internal/test/shared/auth.test.ts b/packages/core-internal/test/shared/auth.test.ts index 4561d8fb11..73cfe569df 100644 --- a/packages/core-internal/test/shared/auth.test.ts +++ b/packages/core-internal/test/shared/auth.test.ts @@ -50,6 +50,17 @@ describe('OAuthMetadataSchema', () => { expect(() => OAuthMetadataSchema.parse(metadata)).not.toThrow(); }); + it('accepts client credentials metadata without an authorization endpoint', () => { + const metadata = { + issuer: 'https://auth.example.com', + token_endpoint: 'https://auth.example.com/oauth/token', + grant_types_supported: ['client_credentials'], + response_types_supported: [] + }; + + expect(OAuthMetadataSchema.parse(metadata)).toMatchObject(metadata); + }); + it('rejects metadata with javascript: URLs', () => { const metadata = { issuer: 'https://auth.example.com', @@ -85,6 +96,19 @@ describe('OpenIdProviderMetadataSchema', () => { expect(() => OpenIdProviderMetadataSchema.parse(metadata)).not.toThrow(); }); + it('continues to require an authorization endpoint for OpenID providers', () => { + const metadata = { + issuer: 'https://auth.example.com', + token_endpoint: 'https://auth.example.com/oauth/token', + jwks_uri: 'https://auth.example.com/.well-known/jwks.json', + response_types_supported: ['code'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'] + }; + + expect(() => OpenIdProviderMetadataSchema.parse(metadata)).toThrow(); + }); + it('rejects metadata with javascript: in jwks_uri', () => { const metadata = { issuer: 'https://auth.example.com', From a4b086d4a80a4ffe4d5393370d77715796242a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Karaaslan?= Date: Thu, 15 Oct 2026 16:32:42 +0300 Subject: [PATCH 2/7] test(client): cover endpoint-free OAuth discovery and code-flow guard --- packages/client/test/client/auth.test.ts | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/client/test/client/auth.test.ts b/packages/client/test/client/auth.test.ts index a87cbd7d92..e822ba4ed8 100644 --- a/packages/client/test/client/auth.test.ts +++ b/packages/client/test/client/auth.test.ts @@ -597,6 +597,22 @@ describe('OAuth Authorization', () => { code_challenge_methods_supported: ['S256'] }; + it('accepts client-credentials-only OAuth metadata without an authorization endpoint', async () => { + const metadata = { + issuer: 'https://auth.example.com', + token_endpoint: 'https://auth.example.com/token', + grant_types_supported: ['client_credentials'], + response_types_supported: [] + }; + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => metadata + }); + + await expect(discoverOAuthMetadata('https://auth.example.com')).resolves.toEqual(metadata); + }); + it('returns metadata when discovery succeeds', async () => { mockFetch.mockResolvedValueOnce({ ok: true, @@ -1781,6 +1797,16 @@ describe('OAuth Authorization', () => { client_name: 'Test Client' }; + it('rejects authorization-code login when authorization_endpoint is absent', async () => { + await expect( + startAuthorization('https://auth.example.com', { + metadata: { ...validMetadata, authorization_endpoint: undefined }, + clientInformation: validClientInfo, + redirectUrl: 'http://localhost:3000/callback' + }) + ).rejects.toThrow('Incompatible auth server: missing authorization_endpoint'); + }); + it('generates authorization URL with PKCE challenge', async () => { const { authorizationUrl, codeVerifier } = await startAuthorization('https://auth.example.com', { metadata: undefined, From c5fd1a14a265c14db7ffd05092b7a4e060cb0011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Karaaslan?= Date: Thu, 15 Oct 2026 16:33:01 +0300 Subject: [PATCH 3/7] fix(core): accept RFC 8414 metadata without authorization endpoint --- packages/core/src/auth.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts index d7f7f8858e..e93965104b 100644 --- a/packages/core/src/auth.ts +++ b/packages/core/src/auth.ts @@ -49,7 +49,8 @@ export const OAuthProtectedResourceMetadataSchema = z.looseObject({ */ export const OAuthMetadataSchema = z.looseObject({ issuer: z.string(), - authorization_endpoint: SafeUrlSchema, + // RFC 8414 ยง2: client_credentials-only servers need no authorization endpoint. + authorization_endpoint: SafeUrlSchema.optional(), token_endpoint: SafeUrlSchema, registration_endpoint: SafeUrlSchema.optional(), scopes_supported: z.array(z.string()).optional(), From 9e8fe27e77702c74dc6ed360050fc6394897f65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Karaaslan?= Date: Thu, 15 Oct 2026 16:33:15 +0300 Subject: [PATCH 4/7] fix(client): guard missing authorization endpoint only for code flow --- packages/client/src/client/auth.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index a95aca0a03..cf63bddddd 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -2095,11 +2095,13 @@ export async function startAuthorization( ): Promise<{ authorizationUrl: URL; codeVerifier: string }> { let authorizationUrl: URL; if (metadata) { - authorizationUrl = new URL(metadata.authorization_endpoint); - if (!metadata.response_types_supported.includes(AUTHORIZATION_CODE_RESPONSE_TYPE)) { throw new Error(`Incompatible auth server: does not support response type ${AUTHORIZATION_CODE_RESPONSE_TYPE}`); } + if (!metadata.authorization_endpoint) { + throw new Error('Incompatible auth server: missing authorization_endpoint'); + } + authorizationUrl = new URL(metadata.authorization_endpoint); if ( metadata.code_challenge_methods_supported && From 929f4837fcad40cd5cf10828a6e4f47cadc6c57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Karaaslan?= Date: Thu, 15 Oct 2026 16:33:27 +0300 Subject: [PATCH 5/7] test(examples): exercise client credentials metadata without fake authorize URL --- examples/shared/src/clientCredentialsAuthServer.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/shared/src/clientCredentialsAuthServer.ts b/examples/shared/src/clientCredentialsAuthServer.ts index b4d87b0462..1925c5e74e 100644 --- a/examples/shared/src/clientCredentialsAuthServer.ts +++ b/examples/shared/src/clientCredentialsAuthServer.ts @@ -60,8 +60,6 @@ export function createClientCredentialsAuthServer(options: ClientCredentialsAuth const metadata: OAuthMetadata = { issuer, token_endpoint: `${issuer}/token`, - // Required by the RFC 8414 schema even though this AS doesn't implement the endpoint. - authorization_endpoint: `${issuer}/authorize`, response_types_supported: [], grant_types_supported: ['client_credentials'], token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'], From bbd63f4b0563e412e44fbdf9b87607c567c3a9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Karaaslan?= Date: Thu, 15 Oct 2026 16:33:45 +0300 Subject: [PATCH 6/7] chore: changeset for endpoint-free client credentials metadata --- .changeset/oauth-client-credentials-endpoint.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/oauth-client-credentials-endpoint.md diff --git a/.changeset/oauth-client-credentials-endpoint.md b/.changeset/oauth-client-credentials-endpoint.md new file mode 100644 index 0000000000..2dd323e69e --- /dev/null +++ b/.changeset/oauth-client-credentials-endpoint.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/core': patch +'@modelcontextprotocol/client': patch +--- + +Accept RFC 8414 authorization-server metadata without an authorization endpoint for client-credentials-only servers, while producing a clear error if an authorization-code flow is attempted without an endpoint. From 5df51220a30269c894a22b53a99b5670517c663c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Karaaslan?= Date: Thu, 15 Oct 2026 16:35:59 +0300 Subject: [PATCH 7/7] fix(server-legacy): retain generated auth URL guarantee after metadata schema change --- packages/server-legacy/src/auth/router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server-legacy/src/auth/router.ts b/packages/server-legacy/src/auth/router.ts index 866615540b..78d95ff01f 100644 --- a/packages/server-legacy/src/auth/router.ts +++ b/packages/server-legacy/src/auth/router.ts @@ -86,7 +86,7 @@ export const createOAuthMetadata = (options: { baseUrl?: URL; serviceDocumentationUrl?: URL; scopesSupported?: string[]; -}): OAuthMetadata => { +}): OAuthMetadata & { authorization_endpoint: string } => { const issuer = options.issuerUrl; const baseUrl = options.baseUrl; @@ -97,7 +97,7 @@ export const createOAuthMetadata = (options: { const registration_endpoint = options.provider.clientsStore.registerClient ? '/register' : undefined; const revocation_endpoint = options.provider.revokeToken ? '/revoke' : undefined; - const metadata: OAuthMetadata = { + const metadata: OAuthMetadata & { authorization_endpoint: string } = { issuer: issuer.href, service_documentation: options.serviceDocumentationUrl?.href,