diff --git a/packages/client/src/client/authExtensions.ts b/packages/client/src/client/authExtensions.ts index c366947be..219bc210d 100644 --- a/packages/client/src/client/authExtensions.ts +++ b/packages/client/src/client/authExtensions.ts @@ -112,6 +112,11 @@ export interface ClientCredentialsProviderOptions { * Optional client name for metadata. */ clientName?: string; + + /** + * Optional scope to request during token exchange. + */ + scope?: string; } /** @@ -148,6 +153,9 @@ export class ClientCredentialsProvider implements OAuthClientProvider { grant_types: ['client_credentials'], token_endpoint_auth_method: 'client_secret_basic' }; + if (options.scope !== undefined) { + this._clientMetadata.scope = options.scope; + } } get redirectUrl(): undefined { @@ -222,6 +230,11 @@ export interface PrivateKeyJwtProviderOptions { * Optional JWT lifetime in seconds (default: 300). */ jwtLifetimeSeconds?: number; + + /** + * Optional scope to request during token exchange. + */ + scope?: string; } /** @@ -260,6 +273,9 @@ export class PrivateKeyJwtProvider implements OAuthClientProvider { grant_types: ['client_credentials'], token_endpoint_auth_method: 'private_key_jwt' }; + if (options.scope !== undefined) { + this._clientMetadata.scope = options.scope; + } this.addClientAuthentication = createPrivateKeyJwtAuth({ issuer: options.clientId, subject: options.clientId, @@ -333,6 +349,11 @@ export interface StaticPrivateKeyJwtProviderOptions { * Optional client name for metadata. */ clientName?: string; + + /** + * Optional scope to request during token exchange. + */ + scope?: string; } /** @@ -358,6 +379,9 @@ export class StaticPrivateKeyJwtProvider implements OAuthClientProvider { grant_types: ['client_credentials'], token_endpoint_auth_method: 'private_key_jwt' }; + if (options.scope !== undefined) { + this._clientMetadata.scope = options.scope; + } const assertion = options.jwtBearerAssertion; this.addClientAuthentication = async (_headers, params) => { diff --git a/packages/client/test/client/authExtensions.test.ts b/packages/client/test/client/authExtensions.test.ts index f2601c823..fb0426ae3 100644 --- a/packages/client/test/client/authExtensions.test.ts +++ b/packages/client/test/client/authExtensions.test.ts @@ -150,6 +150,85 @@ describe('auth-extensions providers (end-to-end with auth())', () => { expect(tokens).toBeTruthy(); expect(tokens?.access_token).toBe('test-access-token'); }); + + it('uses configured scope for ClientCredentialsProvider token requests', async () => { + const provider = new ClientCredentialsProvider({ + clientId: 'my-client', + clientSecret: 'my-secret', + scope: 'mcp:read mcp:write' + }); + + expect(provider.clientMetadata.scope).toBe('mcp:read mcp:write'); + + const fetchMock = createMockOAuthFetch({ + resourceServerUrl: RESOURCE_SERVER_URL, + authServerUrl: AUTH_SERVER_URL, + onTokenRequest: async (_url, init) => { + const params = init?.body as URLSearchParams; + expect(params.get('scope')).toBe('mcp:read mcp:write'); + } + }); + + const result = await auth(provider, { + serverUrl: RESOURCE_SERVER_URL, + fetchFn: fetchMock + }); + + expect(result).toBe('AUTHORIZED'); + }); + + it('uses configured scope for PrivateKeyJwtProvider token requests', async () => { + const provider = new PrivateKeyJwtProvider({ + clientId: 'client-id', + privateKey: 'a-string-secret-at-least-256-bits-long', + algorithm: 'HS256', + scope: 'mcp:read mcp:write' + }); + + expect(provider.clientMetadata.scope).toBe('mcp:read mcp:write'); + + const fetchMock = createMockOAuthFetch({ + resourceServerUrl: RESOURCE_SERVER_URL, + authServerUrl: AUTH_SERVER_URL, + onTokenRequest: async (_url, init) => { + const params = init?.body as URLSearchParams; + expect(params.get('scope')).toBe('mcp:read mcp:write'); + } + }); + + const result = await auth(provider, { + serverUrl: RESOURCE_SERVER_URL, + fetchFn: fetchMock + }); + + expect(result).toBe('AUTHORIZED'); + }); + + it('uses configured scope for StaticPrivateKeyJwtProvider token requests', async () => { + const provider = new StaticPrivateKeyJwtProvider({ + clientId: 'static-client', + jwtBearerAssertion: 'header.payload.signature', + scope: 'mcp:read mcp:write' + }); + + expect(provider.clientMetadata.scope).toBe('mcp:read mcp:write'); + + const fetchMock = createMockOAuthFetch({ + resourceServerUrl: RESOURCE_SERVER_URL, + authServerUrl: AUTH_SERVER_URL, + onTokenRequest: async (_url, init) => { + const params = init?.body as URLSearchParams; + expect(params.get('scope')).toBe('mcp:read mcp:write'); + } + }); + + const result = await auth(provider, { + serverUrl: RESOURCE_SERVER_URL, + fetchFn: fetchMock + }); + + expect(result).toBe('AUTHORIZED'); + }); }); describe('createPrivateKeyJwtAuth', () => {