@@ -2174,6 +2174,135 @@ describe('OAuth Authorization', () => {
21742174 expect ( body . get ( 'refresh_token' ) ) . toBe ( 'refresh123' ) ;
21752175 } ) ;
21762176
2177+ it ( 'uses scopes_supported from PRM when scope is not provided' , async ( ) => {
2178+ // Mock PRM with scopes_supported
2179+ mockFetch . mockImplementation ( url => {
2180+ const urlString = url . toString ( ) ;
2181+
2182+ if ( urlString . includes ( '/.well-known/oauth-protected-resource' ) ) {
2183+ return Promise . resolve ( {
2184+ ok : true ,
2185+ status : 200 ,
2186+ json : async ( ) => ( {
2187+ resource : 'https://api.example.com/' ,
2188+ authorization_servers : [ 'https://auth.example.com' ] ,
2189+ scopes_supported : [ 'mcp:read' , 'mcp:write' , 'mcp:admin' ]
2190+ } )
2191+ } ) ;
2192+ } else if ( urlString . includes ( '/.well-known/oauth-authorization-server' ) ) {
2193+ return Promise . resolve ( {
2194+ ok : true ,
2195+ status : 200 ,
2196+ json : async ( ) => ( {
2197+ issuer : 'https://auth.example.com' ,
2198+ authorization_endpoint : 'https://auth.example.com/authorize' ,
2199+ token_endpoint : 'https://auth.example.com/token' ,
2200+ registration_endpoint : 'https://auth.example.com/register' ,
2201+ response_types_supported : [ 'code' ] ,
2202+ code_challenge_methods_supported : [ 'S256' ]
2203+ } )
2204+ } ) ;
2205+ } else if ( urlString . includes ( '/register' ) ) {
2206+ return Promise . resolve ( {
2207+ ok : true ,
2208+ status : 200 ,
2209+ json : async ( ) => ( {
2210+ client_id : 'test-client-id' ,
2211+ client_secret : 'test-client-secret' ,
2212+ redirect_uris : [ 'http://localhost:3000/callback' ] ,
2213+ client_name : 'Test Client'
2214+ } )
2215+ } ) ;
2216+ }
2217+
2218+ return Promise . resolve ( { ok : false , status : 404 } ) ;
2219+ } ) ;
2220+
2221+ // Mock provider methods - no scope in clientMetadata
2222+ ( mockProvider . clientInformation as Mock ) . mockResolvedValue ( undefined ) ;
2223+ ( mockProvider . tokens as Mock ) . mockResolvedValue ( undefined ) ;
2224+ mockProvider . saveClientInformation = vi . fn ( ) ;
2225+ ( mockProvider . saveCodeVerifier as Mock ) . mockResolvedValue ( undefined ) ;
2226+ ( mockProvider . redirectToAuthorization as Mock ) . mockResolvedValue ( undefined ) ;
2227+
2228+ // Call auth without scope parameter
2229+ const result = await auth ( mockProvider , {
2230+ serverUrl : 'https://api.example.com/'
2231+ } ) ;
2232+
2233+ expect ( result ) . toBe ( 'REDIRECT' ) ;
2234+
2235+ // Verify the authorization URL includes the scopes from PRM
2236+ const redirectCall = ( mockProvider . redirectToAuthorization as Mock ) . mock . calls [ 0 ] ;
2237+ const authUrl : URL = redirectCall [ 0 ] ;
2238+ expect ( authUrl . searchParams . get ( 'scope' ) ) . toBe ( 'mcp:read mcp:write mcp:admin' ) ;
2239+ } ) ;
2240+
2241+ it ( 'prefers explicit scope parameter over scopes_supported from PRM' , async ( ) => {
2242+ // Mock PRM with scopes_supported
2243+ mockFetch . mockImplementation ( url => {
2244+ const urlString = url . toString ( ) ;
2245+
2246+ if ( urlString . includes ( '/.well-known/oauth-protected-resource' ) ) {
2247+ return Promise . resolve ( {
2248+ ok : true ,
2249+ status : 200 ,
2250+ json : async ( ) => ( {
2251+ resource : 'https://api.example.com/' ,
2252+ authorization_servers : [ 'https://auth.example.com' ] ,
2253+ scopes_supported : [ 'mcp:read' , 'mcp:write' , 'mcp:admin' ]
2254+ } )
2255+ } ) ;
2256+ } else if ( urlString . includes ( '/.well-known/oauth-authorization-server' ) ) {
2257+ return Promise . resolve ( {
2258+ ok : true ,
2259+ status : 200 ,
2260+ json : async ( ) => ( {
2261+ issuer : 'https://auth.example.com' ,
2262+ authorization_endpoint : 'https://auth.example.com/authorize' ,
2263+ token_endpoint : 'https://auth.example.com/token' ,
2264+ registration_endpoint : 'https://auth.example.com/register' ,
2265+ response_types_supported : [ 'code' ] ,
2266+ code_challenge_methods_supported : [ 'S256' ]
2267+ } )
2268+ } ) ;
2269+ } else if ( urlString . includes ( '/register' ) ) {
2270+ return Promise . resolve ( {
2271+ ok : true ,
2272+ status : 200 ,
2273+ json : async ( ) => ( {
2274+ client_id : 'test-client-id' ,
2275+ client_secret : 'test-client-secret' ,
2276+ redirect_uris : [ 'http://localhost:3000/callback' ] ,
2277+ client_name : 'Test Client'
2278+ } )
2279+ } ) ;
2280+ }
2281+
2282+ return Promise . resolve ( { ok : false , status : 404 } ) ;
2283+ } ) ;
2284+
2285+ // Mock provider methods
2286+ ( mockProvider . clientInformation as Mock ) . mockResolvedValue ( undefined ) ;
2287+ ( mockProvider . tokens as Mock ) . mockResolvedValue ( undefined ) ;
2288+ mockProvider . saveClientInformation = vi . fn ( ) ;
2289+ ( mockProvider . saveCodeVerifier as Mock ) . mockResolvedValue ( undefined ) ;
2290+ ( mockProvider . redirectToAuthorization as Mock ) . mockResolvedValue ( undefined ) ;
2291+
2292+ // Call auth with explicit scope parameter
2293+ const result = await auth ( mockProvider , {
2294+ serverUrl : 'https://api.example.com/' ,
2295+ scope : 'mcp:read'
2296+ } ) ;
2297+
2298+ expect ( result ) . toBe ( 'REDIRECT' ) ;
2299+
2300+ // Verify the authorization URL uses the explicit scope, not scopes_supported
2301+ const redirectCall = ( mockProvider . redirectToAuthorization as Mock ) . mock . calls [ 0 ] ;
2302+ const authUrl : URL = redirectCall [ 0 ] ;
2303+ expect ( authUrl . searchParams . get ( 'scope' ) ) . toBe ( 'mcp:read' ) ;
2304+ } ) ;
2305+
21772306 it ( 'fetches AS metadata with path from serverUrl when PRM returns external AS' , async ( ) => {
21782307 // Mock PRM discovery that returns an external AS
21792308 mockFetch . mockImplementation ( url => {
0 commit comments