@@ -1365,4 +1365,196 @@ public async Task AuthorizationFlow_DoesNotDuplicateOfflineAccess_WhenAlreadyPre
13651365 var scopeTokens = requestedScope ! . Split ( ' ' ) ;
13661366 Assert . Single ( scopeTokens , t => t == "offline_access" ) ;
13671367 }
1368+
1369+ [ Fact ]
1370+ public async Task AuthorizationFlow_ScopeSelector_CanFilterServerProposedScopes ( )
1371+ {
1372+ Builder . Services . Configure < McpAuthenticationOptions > ( McpAuthenticationDefaults . AuthenticationScheme , options =>
1373+ {
1374+ options . ResourceMetadata ! . ScopesSupported = [ "mcp:tools" , "files:read" ] ;
1375+ } ) ;
1376+
1377+ await using var app = await StartMcpServerAsync ( ) ;
1378+
1379+ string ? requestedScope = null ;
1380+
1381+ await using var transport = new HttpClientTransport ( new ( )
1382+ {
1383+ Endpoint = new ( McpServerUrl ) ,
1384+ OAuth = new ( )
1385+ {
1386+ ClientId = "demo-client" ,
1387+ ClientSecret = "demo-secret" ,
1388+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
1389+ AuthorizationRedirectDelegate = ( uri , redirect , ct ) =>
1390+ {
1391+ var query = QueryHelpers . ParseQuery ( uri . Query ) ;
1392+ requestedScope = query [ "scope" ] . ToString ( ) ;
1393+ return HandleAuthorizationUrlAsync ( uri , redirect , ct ) ;
1394+ } ,
1395+ ScopeSelector = scopes => scopes ? . Where ( s => s == "mcp:tools" ) ,
1396+ } ,
1397+ } , HttpClient , LoggerFactory ) ;
1398+
1399+ await using var client = await McpClient . CreateAsync (
1400+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
1401+
1402+ Assert . Equal ( "mcp:tools" , requestedScope ) ;
1403+ }
1404+
1405+ [ Fact ]
1406+ public async Task AuthorizationFlow_ScopeSelector_CanAddCustomScope ( )
1407+ {
1408+ await using var app = await StartMcpServerAsync ( ) ;
1409+
1410+ string ? requestedScope = null ;
1411+
1412+ await using var transport = new HttpClientTransport ( new ( )
1413+ {
1414+ Endpoint = new ( McpServerUrl ) ,
1415+ OAuth = new ( )
1416+ {
1417+ ClientId = "demo-client" ,
1418+ ClientSecret = "demo-secret" ,
1419+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
1420+ AuthorizationRedirectDelegate = ( uri , redirect , ct ) =>
1421+ {
1422+ var query = QueryHelpers . ParseQuery ( uri . Query ) ;
1423+ requestedScope = query [ "scope" ] . ToString ( ) ;
1424+ return HandleAuthorizationUrlAsync ( uri , redirect , ct ) ;
1425+ } ,
1426+ ScopeSelector = scopes => scopes ? . Append ( "custom:scope" ) ?? [ "custom:scope" ] ,
1427+ } ,
1428+ } , HttpClient , LoggerFactory ) ;
1429+
1430+ await using var client = await McpClient . CreateAsync (
1431+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
1432+
1433+ Assert . NotNull ( requestedScope ) ;
1434+ Assert . Contains ( "custom:scope" , requestedScope ! . Split ( ' ' ) ) ;
1435+ }
1436+
1437+ [ Fact ]
1438+ public async Task AuthorizationFlow_ScopeSelector_ReceivesNull_WhenServerProvidesNoScopes ( )
1439+ {
1440+ // No ScopesSupported on PRM, no Scopes fallback on client, no offline_access on AS (default).
1441+ Builder . Services . Configure < McpAuthenticationOptions > ( McpAuthenticationDefaults . AuthenticationScheme , options =>
1442+ {
1443+ options . ResourceMetadata ! . ScopesSupported = [ ] ;
1444+ } ) ;
1445+
1446+ await using var app = await StartMcpServerAsync ( ) ;
1447+
1448+ IEnumerable < string > ? capturedInput = [ "sentinel" ] ;
1449+
1450+ await using var transport = new HttpClientTransport ( new ( )
1451+ {
1452+ Endpoint = new ( McpServerUrl ) ,
1453+ OAuth = new ( )
1454+ {
1455+ ClientId = "demo-client" ,
1456+ ClientSecret = "demo-secret" ,
1457+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
1458+ AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync ,
1459+ ScopeSelector = scopes =>
1460+ {
1461+ capturedInput = scopes ;
1462+ return scopes ;
1463+ } ,
1464+ } ,
1465+ } , HttpClient , LoggerFactory ) ;
1466+
1467+ await using var client = await McpClient . CreateAsync (
1468+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
1469+
1470+ Assert . Null ( capturedInput ) ;
1471+ }
1472+
1473+ [ Fact ]
1474+ public async Task AuthorizationFlow_ScopeSelector_ReturningNull_OmitsScopeParameter ( )
1475+ {
1476+ await using var app = await StartMcpServerAsync ( ) ;
1477+
1478+ bool ? scopePresent = null ;
1479+
1480+ await using var transport = new HttpClientTransport ( new ( )
1481+ {
1482+ Endpoint = new ( McpServerUrl ) ,
1483+ OAuth = new ( )
1484+ {
1485+ ClientId = "demo-client" ,
1486+ ClientSecret = "demo-secret" ,
1487+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
1488+ AuthorizationRedirectDelegate = ( uri , redirect , ct ) =>
1489+ {
1490+ scopePresent = QueryHelpers . ParseQuery ( uri . Query ) . ContainsKey ( "scope" ) ;
1491+ return HandleAuthorizationUrlAsync ( uri , redirect , ct ) ;
1492+ } ,
1493+ ScopeSelector = _ => null ,
1494+ } ,
1495+ } , HttpClient , LoggerFactory ) ;
1496+
1497+ await using var client = await McpClient . CreateAsync (
1498+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
1499+
1500+ Assert . False ( scopePresent ) ;
1501+ }
1502+
1503+ [ Fact ]
1504+ public async Task AuthorizationFlow_ScopeSelector_ReturningEmpty_OmitsScopeParameter ( )
1505+ {
1506+ await using var app = await StartMcpServerAsync ( ) ;
1507+
1508+ bool ? scopePresent = null ;
1509+
1510+ await using var transport = new HttpClientTransport ( new ( )
1511+ {
1512+ Endpoint = new ( McpServerUrl ) ,
1513+ OAuth = new ( )
1514+ {
1515+ ClientId = "demo-client" ,
1516+ ClientSecret = "demo-secret" ,
1517+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
1518+ AuthorizationRedirectDelegate = ( uri , redirect , ct ) =>
1519+ {
1520+ scopePresent = QueryHelpers . ParseQuery ( uri . Query ) . ContainsKey ( "scope" ) ;
1521+ return HandleAuthorizationUrlAsync ( uri , redirect , ct ) ;
1522+ } ,
1523+ ScopeSelector = _ => [ ] ,
1524+ } ,
1525+ } , HttpClient , LoggerFactory ) ;
1526+
1527+ await using var client = await McpClient . CreateAsync (
1528+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
1529+
1530+ Assert . False ( scopePresent ) ;
1531+ }
1532+
1533+ [ Fact ]
1534+ public async Task DynamicClientRegistration_ScopeSelector_AppliesToDcrScope ( )
1535+ {
1536+ Builder . Services . Configure < McpAuthenticationOptions > ( McpAuthenticationDefaults . AuthenticationScheme , options =>
1537+ {
1538+ options . ResourceMetadata ! . ScopesSupported = [ "mcp:tools" , "files:read" ] ;
1539+ } ) ;
1540+
1541+ await using var app = await StartMcpServerAsync ( ) ;
1542+
1543+ await using var transport = new HttpClientTransport ( new ( )
1544+ {
1545+ Endpoint = new ( McpServerUrl ) ,
1546+ OAuth = new ClientOAuthOptions ( )
1547+ {
1548+ RedirectUri = new Uri ( "http://localhost:1179/callback" ) ,
1549+ AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync ,
1550+ DynamicClientRegistration = new ( ) { ClientName = "Test MCP Client" } ,
1551+ ScopeSelector = scopes => scopes ? . Where ( s => s == "mcp:tools" ) ,
1552+ } ,
1553+ } , HttpClient , LoggerFactory ) ;
1554+
1555+ await using var client = await McpClient . CreateAsync (
1556+ transport , loggerFactory : LoggerFactory , cancellationToken : TestContext . Current . CancellationToken ) ;
1557+
1558+ Assert . Equal ( "mcp:tools" , TestOAuthServer . LastRegistrationScope ) ;
1559+ }
13681560}
0 commit comments