@@ -924,7 +924,7 @@ describe("OAuth Authorization", () => {
924
924
metadata : undefined ,
925
925
clientInformation : validClientInfo ,
926
926
redirectUrl : "http://localhost:3000/callback" ,
927
- resource : new URL ( "https://api.example.com/mcp-server" ) ,
927
+ resource : "https://api.example.com/mcp-server" ,
928
928
}
929
929
) ;
930
930
@@ -1088,7 +1088,7 @@ describe("OAuth Authorization", () => {
1088
1088
authorizationCode : "code123" ,
1089
1089
codeVerifier : "verifier123" ,
1090
1090
redirectUri : "http://localhost:3000/callback" ,
1091
- resource : new URL ( "https://api.example.com/mcp-server" ) ,
1091
+ resource : "https://api.example.com/mcp-server" ,
1092
1092
} ) ;
1093
1093
1094
1094
expect ( tokens ) . toEqual ( validTokens ) ;
@@ -1210,7 +1210,7 @@ describe("OAuth Authorization", () => {
1210
1210
authorizationCode : "code123" ,
1211
1211
codeVerifier : "verifier123" ,
1212
1212
redirectUri : "http://localhost:3000/callback" ,
1213
- resource : new URL ( "https://api.example.com/mcp-server" ) ,
1213
+ resource : "https://api.example.com/mcp-server" ,
1214
1214
fetchFn : customFetch ,
1215
1215
} ) ;
1216
1216
@@ -1274,7 +1274,7 @@ describe("OAuth Authorization", () => {
1274
1274
const tokens = await refreshAuthorization ( "https://auth.example.com" , {
1275
1275
clientInformation : validClientInfo ,
1276
1276
refreshToken : "refresh123" ,
1277
- resource : new URL ( "https://api.example.com/mcp-server" ) ,
1277
+ resource : "https://api.example.com/mcp-server" ,
1278
1278
} ) ;
1279
1279
1280
1280
expect ( tokens ) . toEqual ( validTokensWithNewRefreshToken ) ;
@@ -1497,6 +1497,183 @@ describe("OAuth Authorization", () => {
1497
1497
codeVerifier : jest . fn ( ) ,
1498
1498
} ;
1499
1499
1500
+ describe ( "resource URL handling (trailing slash preservation)" , ( ) => {
1501
+ beforeEach ( ( ) => {
1502
+ jest . clearAllMocks ( ) ;
1503
+ } ) ;
1504
+
1505
+ it ( "preserves server URLs without trailing slash in resource parameter" , async ( ) => {
1506
+ // Mock successful metadata discovery
1507
+ mockFetch . mockImplementation ( ( url ) => {
1508
+ const urlString = url . toString ( ) ;
1509
+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1510
+ return Promise . resolve ( {
1511
+ ok : true ,
1512
+ status : 200 ,
1513
+ json : async ( ) => ( {
1514
+ resource : "https://api.example.com/mcp-server" , // No trailing slash
1515
+ authorization_servers : [ "https://auth.example.com" ] ,
1516
+ } ) ,
1517
+ } ) ;
1518
+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1519
+ return Promise . resolve ( {
1520
+ ok : true ,
1521
+ status : 200 ,
1522
+ json : async ( ) => ( {
1523
+ issuer : "https://auth.example.com" ,
1524
+ authorization_endpoint : "https://auth.example.com/authorize" ,
1525
+ token_endpoint : "https://auth.example.com/token" ,
1526
+ response_types_supported : [ "code" ] ,
1527
+ code_challenge_methods_supported : [ "S256" ] ,
1528
+ } ) ,
1529
+ } ) ;
1530
+ }
1531
+ return Promise . resolve ( { ok : false , status : 404 } ) ;
1532
+ } ) ;
1533
+
1534
+ // Mock provider methods for authorization flow
1535
+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
1536
+ client_id : "test-client" ,
1537
+ client_secret : "test-secret" ,
1538
+ } ) ;
1539
+ ( mockProvider . tokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
1540
+ ( mockProvider . saveCodeVerifier as jest . Mock ) . mockResolvedValue ( undefined ) ;
1541
+ ( mockProvider . redirectToAuthorization as jest . Mock ) . mockResolvedValue ( undefined ) ;
1542
+
1543
+ // Call auth with URL that has no trailing slash
1544
+ const result = await auth ( mockProvider , {
1545
+ serverUrl : "https://api.example.com/mcp-server" , // No trailing slash
1546
+ } ) ;
1547
+
1548
+ expect ( result ) . toBe ( "REDIRECT" ) ;
1549
+
1550
+ // Verify the authorization URL includes the resource parameter WITHOUT trailing slash
1551
+ const redirectCall = ( mockProvider . redirectToAuthorization as jest . Mock ) . mock . calls [ 0 ] ;
1552
+ const authUrl : URL = redirectCall [ 0 ] ;
1553
+ expect ( authUrl . searchParams . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ; // No trailing slash
1554
+ } ) ;
1555
+
1556
+ it ( "preserves server URLs with trailing slash in resource parameter" , async ( ) => {
1557
+ // Mock successful metadata discovery
1558
+ mockFetch . mockImplementation ( ( url ) => {
1559
+ const urlString = url . toString ( ) ;
1560
+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1561
+ return Promise . resolve ( {
1562
+ ok : true ,
1563
+ status : 200 ,
1564
+ json : async ( ) => ( {
1565
+ resource : "https://api.example.com/mcp-server/" , // With trailing slash
1566
+ authorization_servers : [ "https://auth.example.com" ] ,
1567
+ } ) ,
1568
+ } ) ;
1569
+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1570
+ return Promise . resolve ( {
1571
+ ok : true ,
1572
+ status : 200 ,
1573
+ json : async ( ) => ( {
1574
+ issuer : "https://auth.example.com" ,
1575
+ authorization_endpoint : "https://auth.example.com/authorize" ,
1576
+ token_endpoint : "https://auth.example.com/token" ,
1577
+ response_types_supported : [ "code" ] ,
1578
+ code_challenge_methods_supported : [ "S256" ] ,
1579
+ } ) ,
1580
+ } ) ;
1581
+ }
1582
+ return Promise . resolve ( { ok : false , status : 404 } ) ;
1583
+ } ) ;
1584
+
1585
+ // Mock provider methods
1586
+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
1587
+ client_id : "test-client" ,
1588
+ client_secret : "test-secret" ,
1589
+ } ) ;
1590
+ ( mockProvider . tokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
1591
+ ( mockProvider . saveCodeVerifier as jest . Mock ) . mockResolvedValue ( undefined ) ;
1592
+ ( mockProvider . redirectToAuthorization as jest . Mock ) . mockResolvedValue ( undefined ) ;
1593
+
1594
+ // Call auth with URL that has trailing slash
1595
+ const result = await auth ( mockProvider , {
1596
+ serverUrl : "https://api.example.com/mcp-server/" , // With trailing slash
1597
+ } ) ;
1598
+
1599
+ expect ( result ) . toBe ( "REDIRECT" ) ;
1600
+
1601
+ // Verify the authorization URL includes the resource parameter WITH trailing slash
1602
+ const redirectCall = ( mockProvider . redirectToAuthorization as jest . Mock ) . mock . calls [ 0 ] ;
1603
+ const authUrl : URL = redirectCall [ 0 ] ;
1604
+ expect ( authUrl . searchParams . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server/" ) ; // With trailing slash
1605
+ } ) ;
1606
+
1607
+ it ( "handles token exchange with preserved resource URL format" , async ( ) => {
1608
+ // Mock successful metadata discovery and token exchange
1609
+ mockFetch . mockImplementation ( ( url ) => {
1610
+ const urlString = url . toString ( ) ;
1611
+
1612
+ if ( urlString . includes ( "/.well-known/oauth-protected-resource" ) ) {
1613
+ return Promise . resolve ( {
1614
+ ok : true ,
1615
+ status : 200 ,
1616
+ json : async ( ) => ( {
1617
+ resource : "https://api.example.com/mcp-server" , // No trailing slash
1618
+ authorization_servers : [ "https://auth.example.com" ] ,
1619
+ } ) ,
1620
+ } ) ;
1621
+ } else if ( urlString . includes ( "/.well-known/oauth-authorization-server" ) ) {
1622
+ return Promise . resolve ( {
1623
+ ok : true ,
1624
+ status : 200 ,
1625
+ json : async ( ) => ( {
1626
+ issuer : "https://auth.example.com" ,
1627
+ authorization_endpoint : "https://auth.example.com/authorize" ,
1628
+ token_endpoint : "https://auth.example.com/token" ,
1629
+ response_types_supported : [ "code" ] ,
1630
+ code_challenge_methods_supported : [ "S256" ] ,
1631
+ } ) ,
1632
+ } ) ;
1633
+ } else if ( urlString . includes ( "/token" ) ) {
1634
+ return Promise . resolve ( {
1635
+ ok : true ,
1636
+ status : 200 ,
1637
+ json : async ( ) => ( {
1638
+ access_token : "access123" ,
1639
+ token_type : "Bearer" ,
1640
+ expires_in : 3600 ,
1641
+ refresh_token : "refresh123" ,
1642
+ } ) ,
1643
+ } ) ;
1644
+ }
1645
+
1646
+ return Promise . resolve ( { ok : false , status : 404 } ) ;
1647
+ } ) ;
1648
+
1649
+ // Mock provider methods for token exchange
1650
+ ( mockProvider . clientInformation as jest . Mock ) . mockResolvedValue ( {
1651
+ client_id : "test-client" ,
1652
+ client_secret : "test-secret" ,
1653
+ } ) ;
1654
+ ( mockProvider . codeVerifier as jest . Mock ) . mockResolvedValue ( "test-verifier" ) ;
1655
+ ( mockProvider . saveTokens as jest . Mock ) . mockResolvedValue ( undefined ) ;
1656
+
1657
+ // Call auth with authorization code and URL without trailing slash
1658
+ const result = await auth ( mockProvider , {
1659
+ serverUrl : "https://api.example.com/mcp-server" , // No trailing slash
1660
+ authorizationCode : "auth-code-123" ,
1661
+ } ) ;
1662
+
1663
+ expect ( result ) . toBe ( "AUTHORIZED" ) ;
1664
+
1665
+ // Find the token exchange call and verify resource parameter format
1666
+ const tokenCall = mockFetch . mock . calls . find ( call =>
1667
+ call [ 0 ] . toString ( ) . includes ( "/token" )
1668
+ ) ;
1669
+ expect ( tokenCall ) . toBeDefined ( ) ;
1670
+
1671
+ const body = tokenCall ! [ 1 ] . body as URLSearchParams ;
1672
+ expect ( body . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ; // No trailing slash added
1673
+ expect ( body . get ( "code" ) ) . toBe ( "auth-code-123" ) ;
1674
+ } ) ;
1675
+ } ) ;
1676
+
1500
1677
beforeEach ( ( ) => {
1501
1678
jest . clearAllMocks ( ) ;
1502
1679
} ) ;
@@ -1829,7 +2006,7 @@ describe("OAuth Authorization", () => {
1829
2006
1830
2007
// Verify custom validation method was called
1831
2008
expect ( mockValidateResourceURL ) . toHaveBeenCalledWith (
1832
- new URL ( "https://api.example.com/mcp-server" ) ,
2009
+ "https://api.example.com/mcp-server" ,
1833
2010
"https://different-resource.example.com/mcp-server"
1834
2011
) ;
1835
2012
} ) ;
0 commit comments