@@ -25,6 +25,7 @@ const mockOAuthMetadata = {
25
25
token_endpoint : "https://oauth.example.com/token" ,
26
26
response_types_supported : [ "code" ] ,
27
27
grant_types_supported : [ "authorization_code" ] ,
28
+ scopes_supported : [ "read" , "write" ] ,
28
29
} ;
29
30
30
31
const mockOAuthClientInfo = {
@@ -56,6 +57,57 @@ import {
56
57
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js" ;
57
58
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types" ;
58
59
60
+ // Mock local auth module
61
+ jest . mock ( "@/lib/auth" , ( ) => ( {
62
+ DebugInspectorOAuthClientProvider : jest . fn ( ) . mockImplementation ( ( ) => ( {
63
+ tokens : jest . fn ( ) . mockImplementation ( ( ) => Promise . resolve ( undefined ) ) ,
64
+ clear : jest . fn ( ) . mockImplementation ( ( ) => {
65
+ // Mock the real clear() behavior which removes items from sessionStorage
66
+ sessionStorage . removeItem ( "[https://example.com/mcp] mcp_tokens" ) ;
67
+ sessionStorage . removeItem ( "[https://example.com/mcp] mcp_client_info" ) ;
68
+ sessionStorage . removeItem (
69
+ "[https://example.com/mcp] mcp_server_metadata" ,
70
+ ) ;
71
+ } ) ,
72
+ redirectUrl : "http://localhost:3000/oauth/callback/debug" ,
73
+ clientMetadata : {
74
+ redirect_uris : [ "http://localhost:3000/oauth/callback/debug" ] ,
75
+ token_endpoint_auth_method : "none" ,
76
+ grant_types : [ "authorization_code" , "refresh_token" ] ,
77
+ response_types : [ "code" ] ,
78
+ client_name : "MCP Inspector" ,
79
+ } ,
80
+ clientInformation : jest . fn ( ) . mockImplementation ( async ( ) => {
81
+ const serverUrl = "https://example.com/mcp" ;
82
+ const preregisteredKey = `[${ serverUrl } ] ${ SESSION_KEYS . PREREGISTERED_CLIENT_INFORMATION } ` ;
83
+ const preregisteredData = sessionStorage . getItem ( preregisteredKey ) ;
84
+ if ( preregisteredData ) {
85
+ return JSON . parse ( preregisteredData ) ;
86
+ }
87
+ const dynamicKey = `[${ serverUrl } ] ${ SESSION_KEYS . CLIENT_INFORMATION } ` ;
88
+ const dynamicData = sessionStorage . getItem ( dynamicKey ) ;
89
+ if ( dynamicData ) {
90
+ return JSON . parse ( dynamicData ) ;
91
+ }
92
+ return undefined ;
93
+ } ) ,
94
+ saveClientInformation : jest . fn ( ) . mockImplementation ( ( clientInfo ) => {
95
+ const serverUrl = "https://example.com/mcp" ;
96
+ const key = `[${ serverUrl } ] ${ SESSION_KEYS . CLIENT_INFORMATION } ` ;
97
+ sessionStorage . setItem ( key , JSON . stringify ( clientInfo ) ) ;
98
+ } ) ,
99
+ saveTokens : jest . fn ( ) ,
100
+ redirectToAuthorization : jest . fn ( ) ,
101
+ saveCodeVerifier : jest . fn ( ) ,
102
+ codeVerifier : jest . fn ( ) ,
103
+ saveServerMetadata : jest . fn ( ) ,
104
+ getServerMetadata : jest . fn ( ) ,
105
+ } ) ) ,
106
+ discoverScopes : jest . fn ( ) . mockResolvedValue ( "read write" as never ) ,
107
+ } ) ) ;
108
+
109
+ import { discoverScopes } from "@/lib/auth" ;
110
+
59
111
// Type the mocked functions properly
60
112
const mockDiscoverAuthorizationServerMetadata =
61
113
discoverAuthorizationServerMetadata as jest . MockedFunction <
@@ -75,6 +127,9 @@ const mockDiscoverOAuthProtectedResourceMetadata =
75
127
discoverOAuthProtectedResourceMetadata as jest . MockedFunction <
76
128
typeof discoverOAuthProtectedResourceMetadata
77
129
> ;
130
+ const mockDiscoverScopes = discoverScopes as jest . MockedFunction <
131
+ typeof discoverScopes
132
+ > ;
78
133
79
134
const sessionStorageMock = {
80
135
getItem : jest . fn ( ) ,
@@ -103,9 +158,15 @@ describe("AuthDebugger", () => {
103
158
// Suppress console errors in tests to avoid JSDOM navigation noise
104
159
jest . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
105
160
106
- mockDiscoverAuthorizationServerMetadata . mockResolvedValue (
107
- mockOAuthMetadata ,
108
- ) ;
161
+ // Set default mock behaviors with complete OAuth metadata
162
+ mockDiscoverAuthorizationServerMetadata . mockResolvedValue ( {
163
+ issuer : "https://oauth.example.com" ,
164
+ authorization_endpoint : "https://oauth.example.com/authorize" ,
165
+ token_endpoint : "https://oauth.example.com/token" ,
166
+ response_types_supported : [ "code" ] ,
167
+ grant_types_supported : [ "authorization_code" ] ,
168
+ scopes_supported : [ "read" , "write" ] ,
169
+ } ) ;
109
170
mockRegisterClient . mockResolvedValue ( mockOAuthClientInfo ) ;
110
171
mockDiscoverOAuthProtectedResourceMetadata . mockRejectedValue (
111
172
new Error ( "No protected resource metadata found" ) ,
@@ -427,7 +488,24 @@ describe("AuthDebugger", () => {
427
488
} ) ;
428
489
} ) ;
429
490
430
- it ( "should not include scope in authorization URL when scopes_supported is not present" , async ( ) => {
491
+ it ( "should include scope in authorization URL when scopes_supported is not present" , async ( ) => {
492
+ const updateAuthState =
493
+ await setupAuthorizationUrlTest ( mockOAuthMetadata ) ;
494
+
495
+ // Wait for the updateAuthState to be called
496
+ await waitFor ( ( ) => {
497
+ expect ( updateAuthState ) . toHaveBeenCalledWith (
498
+ expect . objectContaining ( {
499
+ authorizationUrl : expect . stringContaining ( "scope=" ) ,
500
+ } ) ,
501
+ ) ;
502
+ } ) ;
503
+ } ) ;
504
+
505
+ it ( "should omit scope from authorization URL when discoverScopes returns undefined" , async ( ) => {
506
+ // Mock discoverScopes to return undefined (no scopes available)
507
+ mockDiscoverScopes . mockResolvedValueOnce ( undefined ) ;
508
+
431
509
const updateAuthState =
432
510
await setupAuthorizationUrlTest ( mockOAuthMetadata ) ;
433
511
0 commit comments