@@ -36,7 +36,7 @@ const mockOAuthClientInfo = {
36
36
// Mock MCP SDK functions - must be before imports
37
37
jest . mock ( "@modelcontextprotocol/sdk/client/auth.js" , ( ) => ( {
38
38
auth : jest . fn ( ) ,
39
- discoverOAuthMetadata : jest . fn ( ) ,
39
+ discoverAuthorizationServerMetadata : jest . fn ( ) ,
40
40
registerClient : jest . fn ( ) ,
41
41
startAuthorization : jest . fn ( ) ,
42
42
exchangeAuthorization : jest . fn ( ) ,
@@ -46,7 +46,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
46
46
47
47
// Import the functions to get their types
48
48
import {
49
- discoverOAuthMetadata ,
49
+ discoverAuthorizationServerMetadata ,
50
50
registerClient ,
51
51
startAuthorization ,
52
52
exchangeAuthorization ,
@@ -57,9 +57,10 @@ import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
57
57
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types" ;
58
58
59
59
// Type the mocked functions properly
60
- const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest . MockedFunction <
61
- typeof discoverOAuthMetadata
62
- > ;
60
+ const mockDiscoverAuthorizationServerMetadata =
61
+ discoverAuthorizationServerMetadata as jest . MockedFunction <
62
+ typeof discoverAuthorizationServerMetadata
63
+ > ;
63
64
const mockRegisterClient = registerClient as jest . MockedFunction <
64
65
typeof registerClient
65
66
> ;
@@ -102,7 +103,9 @@ describe("AuthDebugger", () => {
102
103
// Suppress console errors in tests to avoid JSDOM navigation noise
103
104
jest . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
104
105
105
- mockDiscoverOAuthMetadata . mockResolvedValue ( mockOAuthMetadata ) ;
106
+ mockDiscoverAuthorizationServerMetadata . mockResolvedValue (
107
+ mockOAuthMetadata ,
108
+ ) ;
106
109
mockRegisterClient . mockResolvedValue ( mockOAuthClientInfo ) ;
107
110
mockDiscoverOAuthProtectedResourceMetadata . mockRejectedValue (
108
111
new Error ( "No protected resource metadata found" ) ,
@@ -203,7 +206,7 @@ describe("AuthDebugger", () => {
203
206
} ) ;
204
207
205
208
// Should first discover and save OAuth metadata
206
- expect ( mockDiscoverOAuthMetadata ) . toHaveBeenCalledWith (
209
+ expect ( mockDiscoverAuthorizationServerMetadata ) . toHaveBeenCalledWith (
207
210
new URL ( "https://example.com/" ) ,
208
211
) ;
209
212
@@ -216,7 +219,7 @@ describe("AuthDebugger", () => {
216
219
} ) ;
217
220
218
221
it ( "should show error when quick OAuth flow fails to discover metadata" , async ( ) => {
219
- mockDiscoverOAuthMetadata . mockRejectedValue (
222
+ mockDiscoverAuthorizationServerMetadata . mockRejectedValue (
220
223
new Error ( "Metadata discovery failed" ) ,
221
224
) ;
222
225
@@ -362,7 +365,7 @@ describe("AuthDebugger", () => {
362
365
fireEvent . click ( screen . getByText ( "Continue" ) ) ;
363
366
} ) ;
364
367
365
- expect ( mockDiscoverOAuthMetadata ) . toHaveBeenCalledWith (
368
+ expect ( mockDiscoverAuthorizationServerMetadata ) . toHaveBeenCalledWith (
366
369
new URL ( "https://example.com/" ) ,
367
370
) ;
368
371
} ) ;
@@ -439,6 +442,103 @@ describe("AuthDebugger", () => {
439
442
} ) ;
440
443
} ) ;
441
444
445
+ describe ( "Client Registration behavior" , ( ) => {
446
+ it ( "uses preregistered (static) client information without calling DCR" , async ( ) => {
447
+ const preregClientInfo = {
448
+ client_id : "static_client_id" ,
449
+ client_secret : "static_client_secret" ,
450
+ redirect_uris : [ "http://localhost:3000/oauth/callback/debug" ] ,
451
+ } ;
452
+
453
+ // Return preregistered client info for the server-specific key
454
+ sessionStorageMock . getItem . mockImplementation ( ( key ) => {
455
+ if (
456
+ key ===
457
+ `[${ defaultProps . serverUrl } ] ${ SESSION_KEYS . PREREGISTERED_CLIENT_INFORMATION } `
458
+ ) {
459
+ return JSON . stringify ( preregClientInfo ) ;
460
+ }
461
+ return null ;
462
+ } ) ;
463
+
464
+ const updateAuthState = jest . fn ( ) ;
465
+
466
+ await act ( async ( ) => {
467
+ renderAuthDebugger ( {
468
+ updateAuthState,
469
+ authState : {
470
+ ...defaultAuthState ,
471
+ isInitiatingAuth : false ,
472
+ oauthStep : "client_registration" ,
473
+ oauthMetadata : mockOAuthMetadata as unknown as OAuthMetadata ,
474
+ } ,
475
+ } ) ;
476
+ } ) ;
477
+
478
+ // Proceed from client_registration → authorization_redirect
479
+ await act ( async ( ) => {
480
+ fireEvent . click ( screen . getByText ( "Continue" ) ) ;
481
+ } ) ;
482
+
483
+ // Should NOT attempt dynamic client registration
484
+ expect ( mockRegisterClient ) . not . toHaveBeenCalled ( ) ;
485
+
486
+ // Should advance with the preregistered client info
487
+ expect ( updateAuthState ) . toHaveBeenCalledWith (
488
+ expect . objectContaining ( {
489
+ oauthClientInfo : expect . objectContaining ( {
490
+ client_id : "static_client_id" ,
491
+ } ) ,
492
+ oauthStep : "authorization_redirect" ,
493
+ } ) ,
494
+ ) ;
495
+ } ) ;
496
+
497
+ it ( "falls back to DCR when no static client information is available" , async ( ) => {
498
+ // No preregistered or dynamic client info present in session storage
499
+ sessionStorageMock . getItem . mockImplementation ( ( ) => null ) ;
500
+
501
+ // DCR returns a new client
502
+ mockRegisterClient . mockResolvedValueOnce ( mockOAuthClientInfo ) ;
503
+
504
+ const updateAuthState = jest . fn ( ) ;
505
+
506
+ await act ( async ( ) => {
507
+ renderAuthDebugger ( {
508
+ updateAuthState,
509
+ authState : {
510
+ ...defaultAuthState ,
511
+ isInitiatingAuth : false ,
512
+ oauthStep : "client_registration" ,
513
+ oauthMetadata : mockOAuthMetadata as unknown as OAuthMetadata ,
514
+ } ,
515
+ } ) ;
516
+ } ) ;
517
+
518
+ await act ( async ( ) => {
519
+ fireEvent . click ( screen . getByText ( "Continue" ) ) ;
520
+ } ) ;
521
+
522
+ expect ( mockRegisterClient ) . toHaveBeenCalledTimes ( 1 ) ;
523
+
524
+ // Should save and advance with the DCR client info
525
+ expect ( updateAuthState ) . toHaveBeenCalledWith (
526
+ expect . objectContaining ( {
527
+ oauthClientInfo : expect . objectContaining ( {
528
+ client_id : "test_client_id" ,
529
+ } ) ,
530
+ oauthStep : "authorization_redirect" ,
531
+ } ) ,
532
+ ) ;
533
+
534
+ // Verify the dynamically registered client info was persisted
535
+ expect ( sessionStorage . setItem ) . toHaveBeenCalledWith (
536
+ `[${ defaultProps . serverUrl } ] ${ SESSION_KEYS . CLIENT_INFORMATION } ` ,
537
+ expect . any ( String ) ,
538
+ ) ;
539
+ } ) ;
540
+ } ) ;
541
+
442
542
describe ( "OAuth State Persistence" , ( ) => {
443
543
it ( "should store auth state to sessionStorage before redirect in Quick OAuth Flow" , async ( ) => {
444
544
const updateAuthState = jest . fn ( ) ;
@@ -509,7 +609,9 @@ describe("AuthDebugger", () => {
509
609
mockDiscoverOAuthProtectedResourceMetadata . mockResolvedValue (
510
610
mockResourceMetadata ,
511
611
) ;
512
- mockDiscoverOAuthMetadata . mockResolvedValue ( mockOAuthMetadata ) ;
612
+ mockDiscoverAuthorizationServerMetadata . mockResolvedValue (
613
+ mockOAuthMetadata ,
614
+ ) ;
513
615
514
616
await act ( async ( ) => {
515
617
renderAuthDebugger ( {
@@ -563,7 +665,9 @@ describe("AuthDebugger", () => {
563
665
// Mock failed metadata discovery
564
666
mockDiscoverOAuthProtectedResourceMetadata . mockRejectedValue ( mockError ) ;
565
667
// But OAuth metadata should still work with the original URL
566
- mockDiscoverOAuthMetadata . mockResolvedValue ( mockOAuthMetadata ) ;
668
+ mockDiscoverAuthorizationServerMetadata . mockResolvedValue (
669
+ mockOAuthMetadata ,
670
+ ) ;
567
671
568
672
await act ( async ( ) => {
569
673
renderAuthDebugger ( {
@@ -603,7 +707,7 @@ describe("AuthDebugger", () => {
603
707
} ) ;
604
708
605
709
// Verify that regular OAuth metadata discovery was still called
606
- expect ( mockDiscoverOAuthMetadata ) . toHaveBeenCalledWith (
710
+ expect ( mockDiscoverAuthorizationServerMetadata ) . toHaveBeenCalledWith (
607
711
new URL ( "https://example.com/" ) ,
608
712
) ;
609
713
} ) ;
0 commit comments