@@ -82,6 +82,43 @@ function setupCallbackServerMock(code = "test-auth-code", state?: string) {
8282describe ( "McpOAuthClientProvider" , ( ) => {
8383 beforeEach ( ( ) => {
8484 vi . clearAllMocks ( )
85+ McpOAuthClientProvider . clearNonOAuthCache ( )
86+ } )
87+
88+ describe ( "static negative cache" , ( ) => {
89+ it ( "isKnownNonOAuth returns false for unknown servers" , ( ) => {
90+ expect ( McpOAuthClientProvider . isKnownNonOAuth ( "https://unknown.com/mcp" ) ) . toBe ( false )
91+ } )
92+
93+ it ( "markNonOAuth makes isKnownNonOAuth return true" , ( ) => {
94+ McpOAuthClientProvider . markNonOAuth ( "https://example.com/mcp" )
95+ expect ( McpOAuthClientProvider . isKnownNonOAuth ( "https://example.com/mcp" ) ) . toBe ( true )
96+ } )
97+
98+ it ( "clearNonOAuthCache(url) clears a specific entry" , ( ) => {
99+ McpOAuthClientProvider . markNonOAuth ( "https://a.com/mcp" )
100+ McpOAuthClientProvider . markNonOAuth ( "https://b.com/mcp" )
101+ McpOAuthClientProvider . clearNonOAuthCache ( "https://a.com/mcp" )
102+ expect ( McpOAuthClientProvider . isKnownNonOAuth ( "https://a.com/mcp" ) ) . toBe ( false )
103+ expect ( McpOAuthClientProvider . isKnownNonOAuth ( "https://b.com/mcp" ) ) . toBe ( true )
104+ } )
105+
106+ it ( "clearNonOAuthCache() with no arg clears all entries" , ( ) => {
107+ McpOAuthClientProvider . markNonOAuth ( "https://a.com/mcp" )
108+ McpOAuthClientProvider . markNonOAuth ( "https://b.com/mcp" )
109+ McpOAuthClientProvider . clearNonOAuthCache ( )
110+ expect ( McpOAuthClientProvider . isKnownNonOAuth ( "https://a.com/mcp" ) ) . toBe ( false )
111+ expect ( McpOAuthClientProvider . isKnownNonOAuth ( "https://b.com/mcp" ) ) . toBe ( false )
112+ } )
113+
114+ it ( "entries expire after the TTL" , ( ) => {
115+ const realNow = Date . now ( )
116+ McpOAuthClientProvider . markNonOAuth ( "https://example.com/mcp" )
117+ // Advance time past the 30-minute TTL
118+ const spy = vi . spyOn ( Date , "now" ) . mockReturnValue ( realNow + 31 * 60 * 1000 )
119+ expect ( McpOAuthClientProvider . isKnownNonOAuth ( "https://example.com/mcp" ) ) . toBe ( false )
120+ spy . mockRestore ( )
121+ } )
85122 } )
86123
87124 describe ( "create" , ( ) => {
@@ -95,6 +132,36 @@ describe("McpOAuthClientProvider", () => {
95132 expect ( provider . redirectUrl ) . toBe ( "http://localhost:0/callback" )
96133 await provider . close ( )
97134 } )
135+
136+ it ( "should skip discovery when skipDiscovery option is true" , async ( ) => {
137+ const secretStorage = createMockSecretStorage ( )
138+ const provider = await McpOAuthClientProvider . create ( "https://example.com/mcp" , secretStorage , undefined , {
139+ skipDiscovery : true ,
140+ } )
141+
142+ expect ( discoverOAuthProtectedResourceMetadata ) . not . toHaveBeenCalled ( )
143+ expect ( provider . hasMetadata ) . toBe ( false )
144+ } )
145+
146+ it ( "should have hasMetadata true when discovery succeeds" , async ( ) => {
147+ const secretStorage = createMockSecretStorage ( )
148+ const provider = await McpOAuthClientProvider . create ( "https://example.com/mcp" , secretStorage )
149+
150+ expect ( discoverOAuthProtectedResourceMetadata ) . toHaveBeenCalled ( )
151+ expect ( provider . hasMetadata ) . toBe ( true )
152+ } )
153+
154+ it ( "should cache server as non-OAuth when discovery fails" , async ( ) => {
155+ // Use mockImplementationOnce to override just this call
156+ ; ( discoverOAuthProtectedResourceMetadata as any ) . mockImplementationOnce ( ( ) => {
157+ throw new Error ( "not found" )
158+ } )
159+ const secretStorage = createMockSecretStorage ( )
160+ const provider = await McpOAuthClientProvider . create ( "https://no-oauth.example.com/mcp" , secretStorage )
161+
162+ expect ( provider . hasMetadata ) . toBe ( false )
163+ expect ( McpOAuthClientProvider . isKnownNonOAuth ( "https://no-oauth.example.com/mcp" ) ) . toBe ( true )
164+ } )
98165 } )
99166
100167 describe ( "redirectUrl (pre-server-start)" , ( ) => {
0 commit comments