From 703499d12b47f08bbf901de34b5e05a82e481bb8 Mon Sep 17 00:00:00 2001 From: Nicolas Ayral Seydoux Date: Mon, 2 Feb 2026 16:02:10 +0100 Subject: [PATCH 1/5] Support scopes for client creds providers --- packages/client/src/client/authExtensions.ts | 24 +++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/client/src/client/authExtensions.ts b/packages/client/src/client/authExtensions.ts index d5f63bd66..18460f010 100644 --- a/packages/client/src/client/authExtensions.ts +++ b/packages/client/src/client/authExtensions.ts @@ -105,6 +105,11 @@ export interface ClientCredentialsProviderOptions { * Optional client name for metadata. */ clientName?: string; + + /** + * Space-separated scopes values requested by the client. + */ + scope?: string; } /** @@ -137,7 +142,8 @@ export class ClientCredentialsProvider implements OAuthClientProvider { client_name: options.clientName ?? 'client-credentials-client', redirect_uris: [], grant_types: ['client_credentials'], - token_endpoint_auth_method: 'client_secret_basic' + token_endpoint_auth_method: 'client_secret_basic', + scope: options.scope }; } @@ -213,6 +219,11 @@ export interface PrivateKeyJwtProviderOptions { * Optional JWT lifetime in seconds (default: 300). */ jwtLifetimeSeconds?: number; + + /** + * Space-separated scopes values requested by the client. + */ + scope?: string; } /** @@ -246,7 +257,8 @@ export class PrivateKeyJwtProvider implements OAuthClientProvider { client_name: options.clientName ?? 'private-key-jwt-client', redirect_uris: [], grant_types: ['client_credentials'], - token_endpoint_auth_method: 'private_key_jwt' + token_endpoint_auth_method: 'private_key_jwt', + scope: options.scope, }; this.addClientAuthentication = createPrivateKeyJwtAuth({ issuer: options.clientId, @@ -321,6 +333,11 @@ export interface StaticPrivateKeyJwtProviderOptions { * Optional client name for metadata. */ clientName?: string; + + /** + * Space-separated scopes values requested by the client. + */ + scope?: string; } /** @@ -344,7 +361,8 @@ export class StaticPrivateKeyJwtProvider implements OAuthClientProvider { client_name: options.clientName ?? 'static-private-key-jwt-client', redirect_uris: [], grant_types: ['client_credentials'], - token_endpoint_auth_method: 'private_key_jwt' + token_endpoint_auth_method: 'private_key_jwt', + scope: options.scope, }; const assertion = options.jwtBearerAssertion; From a94769782b63a7fbc0f3ec40ccf8a7e502149790 Mon Sep 17 00:00:00 2001 From: Nicolas Ayral Seydoux Date: Mon, 2 Feb 2026 16:34:28 +0100 Subject: [PATCH 2/5] Add tests --- .../client/test/client/authExtensions.test.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/packages/client/test/client/authExtensions.test.ts b/packages/client/test/client/authExtensions.test.ts index 36b797ca1..7e55102fe 100644 --- a/packages/client/test/client/authExtensions.test.ts +++ b/packages/client/test/client/authExtensions.test.ts @@ -50,6 +50,35 @@ describe('auth-extensions providers (end-to-end with auth())', () => { expect(tokens?.access_token).toBe('test-access-token'); }); + it('sends scope in token request when ClientCredentialsProvider is configured with scope', async () => { + const provider = new ClientCredentialsProvider({ + clientId: 'my-client', + clientSecret: 'my-secret', + clientName: 'test-client', + scope: 'read write' + }); + + expect(provider.clientMetadata.scope).toBe('read write'); + + const fetchMock = createMockOAuthFetch({ + resourceServerUrl: RESOURCE_SERVER_URL, + authServerUrl: AUTH_SERVER_URL, + onTokenRequest: async (_url, init) => { + const params = init?.body as URLSearchParams; + expect(params).toBeInstanceOf(URLSearchParams); + expect(params.get('grant_type')).toBe('client_credentials'); + expect(params.get('scope')).toBe('read write'); + } + }); + + const result = await auth(provider, { + serverUrl: RESOURCE_SERVER_URL, + fetchFn: fetchMock + }); + + expect(result).toBe('AUTHORIZED'); + }); + it('authenticates using PrivateKeyJwtProvider with private_key_jwt', async () => { const provider = new PrivateKeyJwtProvider({ clientId: 'client-id', @@ -93,6 +122,38 @@ describe('auth-extensions providers (end-to-end with auth())', () => { expect(assertionFromRequest).toBeTruthy(); }); + it('sends scope in token request when PrivateKeyJwtProvider is configured with scope', async () => { + const provider = new PrivateKeyJwtProvider({ + clientId: 'client-id', + privateKey: 'a-string-secret-at-least-256-bits-long', + algorithm: 'HS256', + clientName: 'private-key-jwt-client', + scope: 'openid profile' + }); + + expect(provider.clientMetadata.scope).toBe('openid profile'); + + const fetchMock = createMockOAuthFetch({ + resourceServerUrl: RESOURCE_SERVER_URL, + authServerUrl: AUTH_SERVER_URL, + onTokenRequest: async (_url, init) => { + const params = init?.body as URLSearchParams; + expect(params).toBeInstanceOf(URLSearchParams); + expect(params.get('grant_type')).toBe('client_credentials'); + expect(params.get('scope')).toBe('openid profile'); + expect(params.get('client_assertion')).toBeTruthy(); + expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer'); + } + }); + + const result = await auth(provider, { + serverUrl: RESOURCE_SERVER_URL, + fetchFn: fetchMock + }); + + expect(result).toBe('AUTHORIZED'); + }); + it('fails when PrivateKeyJwtProvider is configured with an unsupported algorithm', async () => { const provider = new PrivateKeyJwtProvider({ clientId: 'client-id', @@ -150,6 +211,39 @@ describe('auth-extensions providers (end-to-end with auth())', () => { expect(tokens).toBeTruthy(); expect(tokens?.access_token).toBe('test-access-token'); }); + + it('sends scope in token request when StaticPrivateKeyJwtProvider is configured with scope', async () => { + const staticAssertion = 'header.payload.signature'; + + const provider = new StaticPrivateKeyJwtProvider({ + clientId: 'static-client', + jwtBearerAssertion: staticAssertion, + clientName: 'static-private-key-jwt-client', + scope: 'api:read api:write' + }); + + expect(provider.clientMetadata.scope).toBe('api:read api:write'); + + const fetchMock = createMockOAuthFetch({ + resourceServerUrl: RESOURCE_SERVER_URL, + authServerUrl: AUTH_SERVER_URL, + onTokenRequest: async (_url, init) => { + const params = init?.body as URLSearchParams; + expect(params).toBeInstanceOf(URLSearchParams); + expect(params.get('grant_type')).toBe('client_credentials'); + expect(params.get('scope')).toBe('api:read api:write'); + expect(params.get('client_assertion')).toBe(staticAssertion); + expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer'); + } + }); + + const result = await auth(provider, { + serverUrl: RESOURCE_SERVER_URL, + fetchFn: fetchMock + }); + + expect(result).toBe('AUTHORIZED'); + }); }); describe('createPrivateKeyJwtAuth', () => { From 44e241ed1534c09560feb67f3f960137e00ae5b2 Mon Sep 17 00:00:00 2001 From: Nicolas Ayral Seydoux Date: Mon, 2 Feb 2026 16:50:42 +0100 Subject: [PATCH 3/5] Lint --- packages/client/src/client/authExtensions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/client/authExtensions.ts b/packages/client/src/client/authExtensions.ts index 18460f010..e2486f5f7 100644 --- a/packages/client/src/client/authExtensions.ts +++ b/packages/client/src/client/authExtensions.ts @@ -258,7 +258,7 @@ export class PrivateKeyJwtProvider implements OAuthClientProvider { redirect_uris: [], grant_types: ['client_credentials'], token_endpoint_auth_method: 'private_key_jwt', - scope: options.scope, + scope: options.scope }; this.addClientAuthentication = createPrivateKeyJwtAuth({ issuer: options.clientId, @@ -362,7 +362,7 @@ export class StaticPrivateKeyJwtProvider implements OAuthClientProvider { redirect_uris: [], grant_types: ['client_credentials'], token_endpoint_auth_method: 'private_key_jwt', - scope: options.scope, + scope: options.scope }; const assertion = options.jwtBearerAssertion; From 598dff885dd186480694b49b40c3e6a0e4ac4adf Mon Sep 17 00:00:00 2001 From: Nicolas Ayral Seydoux Date: Tue, 3 Feb 2026 16:00:55 +0100 Subject: [PATCH 4/5] Add changeset --- .changeset/spotty-cats-tickle.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spotty-cats-tickle.md diff --git a/.changeset/spotty-cats-tickle.md b/.changeset/spotty-cats-tickle.md new file mode 100644 index 000000000..848a1cc41 --- /dev/null +++ b/.changeset/spotty-cats-tickle.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/client': patch +--- + +The client credentials providers now support scopes being added to the token request. From 0564ec0899e93c6ac1ad21b3bd245fcc8fff3ee7 Mon Sep 17 00:00:00 2001 From: Zwifi Date: Tue, 3 Feb 2026 17:31:57 +0100 Subject: [PATCH 5/5] Update .changeset/spotty-cats-tickle.md --- .changeset/spotty-cats-tickle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/spotty-cats-tickle.md b/.changeset/spotty-cats-tickle.md index 848a1cc41..502130dc5 100644 --- a/.changeset/spotty-cats-tickle.md +++ b/.changeset/spotty-cats-tickle.md @@ -1,5 +1,5 @@ --- -'@modelcontextprotocol/client': patch +'@modelcontextprotocol/client': minor --- The client credentials providers now support scopes being added to the token request.