@@ -25,6 +25,7 @@ const mockOAuthMetadata = {
2525 token_endpoint : "https://oauth.example.com/token" ,
2626 response_types_supported : [ "code" ] ,
2727 grant_types_supported : [ "authorization_code" ] ,
28+ scopes_supported : [ "read" , "write" ] ,
2829} ;
2930
3031const mockOAuthClientInfo = {
@@ -56,6 +57,57 @@ import {
5657import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js" ;
5758import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types" ;
5859
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+
59111// Type the mocked functions properly
60112const mockDiscoverAuthorizationServerMetadata =
61113 discoverAuthorizationServerMetadata as jest . MockedFunction <
@@ -75,6 +127,9 @@ const mockDiscoverOAuthProtectedResourceMetadata =
75127 discoverOAuthProtectedResourceMetadata as jest . MockedFunction <
76128 typeof discoverOAuthProtectedResourceMetadata
77129 > ;
130+ const mockDiscoverScopes = discoverScopes as jest . MockedFunction <
131+ typeof discoverScopes
132+ > ;
78133
79134const sessionStorageMock = {
80135 getItem : jest . fn ( ) ,
@@ -103,9 +158,15 @@ describe("AuthDebugger", () => {
103158 // Suppress console errors in tests to avoid JSDOM navigation noise
104159 jest . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
105160
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+ } ) ;
109170 mockRegisterClient . mockResolvedValue ( mockOAuthClientInfo ) ;
110171 mockDiscoverOAuthProtectedResourceMetadata . mockRejectedValue (
111172 new Error ( "No protected resource metadata found" ) ,
@@ -427,7 +488,24 @@ describe("AuthDebugger", () => {
427488 } ) ;
428489 } ) ;
429490
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+
431509 const updateAuthState =
432510 await setupAuthorizationUrlTest ( mockOAuthMetadata ) ;
433511
0 commit comments