11// browser-provider.ts
2- import { OAuthClientInformation , OAuthMetadata , OAuthTokens , OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js' ;
3- import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js' ;
2+ import { OAuthClientInformation , OAuthMetadata , OAuthTokens , OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'
3+ import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
44// Assuming StoredState is defined in ./types.js and includes fields for provider options
5- import { StoredState } from './types.js' ; // Adjust path if necessary
5+ import { StoredState } from './types.js' // Adjust path if necessary
66
77/**
88 * Browser-compatible OAuth client provider for MCP using localStorage.
99 */
1010export class BrowserOAuthClientProvider implements OAuthClientProvider {
11- readonly serverUrl : string ;
12- readonly storageKeyPrefix : string ;
13- readonly serverUrlHash : string ;
14- readonly clientName : string ;
15- readonly clientUri : string ;
16- readonly callbackUrl : string ;
11+ readonly serverUrl : string
12+ readonly storageKeyPrefix : string
13+ readonly serverUrlHash : string
14+ readonly clientName : string
15+ readonly clientUri : string
16+ readonly callbackUrl : string
1717
1818 constructor (
1919 serverUrl : string ,
2020 options : {
21- storageKeyPrefix ?: string ;
22- clientName ?: string ;
23- clientUri ?: string ;
24- callbackUrl ?: string ;
21+ storageKeyPrefix ?: string
22+ clientName ?: string
23+ clientUri ?: string
24+ callbackUrl ?: string
2525 } = { } ,
2626 ) {
27- this . serverUrl = serverUrl ;
28- this . storageKeyPrefix = options . storageKeyPrefix || 'mcp:auth' ;
29- this . serverUrlHash = this . hashString ( serverUrl ) ;
30- this . clientName = options . clientName || 'MCP Browser Client' ;
31- this . clientUri = options . clientUri || ( typeof window !== 'undefined' ? window . location . origin : '' ) ;
32- this . callbackUrl = options . callbackUrl || ( typeof window !== 'undefined' ? new URL ( '/oauth/callback' , window . location . origin ) . toString ( ) : '/oauth/callback' ) ;
27+ this . serverUrl = serverUrl
28+ this . storageKeyPrefix = options . storageKeyPrefix || 'mcp:auth'
29+ this . serverUrlHash = this . hashString ( serverUrl )
30+ this . clientName = options . clientName || 'MCP Browser Client'
31+ this . clientUri = options . clientUri || ( typeof window !== 'undefined' ? window . location . origin : '' )
32+ this . callbackUrl =
33+ options . callbackUrl ||
34+ ( typeof window !== 'undefined' ? new URL ( '/oauth/callback' , window . location . origin ) . toString ( ) : '/oauth/callback' )
3335 }
3436
3537 // --- SDK Interface Methods ---
3638
3739 get redirectUrl ( ) : string {
38- return this . callbackUrl ;
40+ return this . callbackUrl
3941 }
4042
4143 get clientMetadata ( ) : OAuthClientMetadata {
@@ -47,68 +49,69 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
4749 client_name : this . clientName ,
4850 client_uri : this . clientUri ,
4951 // scope: 'openid profile email mcp', // Example scopes, adjust as needed
50- } ;
52+ }
5153 }
5254
5355 async clientInformation ( ) : Promise < OAuthClientInformation | undefined > {
54- const key = this . getKey ( 'client_info' ) ;
55- const data = localStorage . getItem ( key ) ;
56- if ( ! data ) return undefined ;
56+ const key = this . getKey ( 'client_info' )
57+ const data = localStorage . getItem ( key )
58+ if ( ! data ) return undefined
5759 try {
5860 // TODO: Add validation using a schema
59- return JSON . parse ( data ) as OAuthClientInformation ;
61+ return JSON . parse ( data ) as OAuthClientInformation
6062 } catch ( e ) {
61- console . warn ( `[${ this . storageKeyPrefix } ] Failed to parse client information:` , e ) ;
62- localStorage . removeItem ( key ) ;
63- return undefined ;
63+ console . warn ( `[${ this . storageKeyPrefix } ] Failed to parse client information:` , e )
64+ localStorage . removeItem ( key )
65+ return undefined
6466 }
6567 }
6668
6769 // NOTE: The SDK's auth() function uses this if dynamic registration is needed.
6870 // Ensure your OAuthClientInformationFull matches the expected structure if DCR is used.
6971 async saveClientInformation ( clientInformation : OAuthClientInformation /* | OAuthClientInformationFull */ ) : Promise < void > {
70- const key = this . getKey ( 'client_info' ) ;
72+ const key = this . getKey ( 'client_info' )
7173 // Cast needed if handling OAuthClientInformationFull specifically
72- localStorage . setItem ( key , JSON . stringify ( clientInformation ) ) ;
74+ localStorage . setItem ( key , JSON . stringify ( clientInformation ) )
7375 }
7476
75-
7677 async tokens ( ) : Promise < OAuthTokens | undefined > {
77- const key = this . getKey ( 'tokens' ) ;
78- const data = localStorage . getItem ( key ) ;
79- if ( ! data ) return undefined ;
78+ const key = this . getKey ( 'tokens' )
79+ const data = localStorage . getItem ( key )
80+ if ( ! data ) return undefined
8081 try {
8182 // TODO: Add validation
82- return JSON . parse ( data ) as OAuthTokens ;
83+ return JSON . parse ( data ) as OAuthTokens
8384 } catch ( e ) {
84- console . warn ( `[${ this . storageKeyPrefix } ] Failed to parse tokens:` , e ) ;
85- localStorage . removeItem ( key ) ;
86- return undefined ;
85+ console . warn ( `[${ this . storageKeyPrefix } ] Failed to parse tokens:` , e )
86+ localStorage . removeItem ( key )
87+ return undefined
8788 }
8889 }
8990
9091 async saveTokens ( tokens : OAuthTokens ) : Promise < void > {
91- const key = this . getKey ( 'tokens' ) ;
92- localStorage . setItem ( key , JSON . stringify ( tokens ) ) ;
92+ const key = this . getKey ( 'tokens' )
93+ localStorage . setItem ( key , JSON . stringify ( tokens ) )
9394 // Clean up code verifier and last auth URL after successful token save
94- localStorage . removeItem ( this . getKey ( 'code_verifier' ) ) ;
95- localStorage . removeItem ( this . getKey ( 'last_auth_url' ) ) ;
95+ localStorage . removeItem ( this . getKey ( 'code_verifier' ) )
96+ localStorage . removeItem ( this . getKey ( 'last_auth_url' ) )
9697 }
9798
9899 async saveCodeVerifier ( codeVerifier : string ) : Promise < void > {
99- const key = this . getKey ( 'code_verifier' ) ;
100- localStorage . setItem ( key , codeVerifier ) ;
100+ const key = this . getKey ( 'code_verifier' )
101+ localStorage . setItem ( key , codeVerifier )
101102 }
102103
103104 async codeVerifier ( ) : Promise < string > {
104- const key = this . getKey ( 'code_verifier' ) ;
105- const verifier = localStorage . getItem ( key ) ;
105+ const key = this . getKey ( 'code_verifier' )
106+ const verifier = localStorage . getItem ( key )
106107 if ( ! verifier ) {
107- throw new Error ( `[${ this . storageKeyPrefix } ] Code verifier not found in storage for key ${ key } . Auth flow likely corrupted or timed out.` ) ;
108+ throw new Error (
109+ `[${ this . storageKeyPrefix } ] Code verifier not found in storage for key ${ key } . Auth flow likely corrupted or timed out.` ,
110+ )
108111 }
109112 // SDK's auth() retrieves this BEFORE exchanging code. Don't remove it here.
110113 // It will be removed in saveTokens on success.
111- return verifier ;
114+ return verifier
112115 }
113116
114117 /**
@@ -118,8 +121,8 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
118121 */
119122 async redirectToAuthorization ( authorizationUrl : URL ) : Promise < void > {
120123 // Generate a unique state parameter for this authorization request
121- const state = crypto . randomUUID ( ) ;
122- const stateKey = `${ this . storageKeyPrefix } :state_${ state } ` ;
124+ const state = crypto . randomUUID ( )
125+ const stateKey = `${ this . storageKeyPrefix } :state_${ state } `
123126
124127 // Store context needed by the callback handler, associated with the state param
125128 const stateData : StoredState = {
@@ -132,32 +135,34 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
132135 clientName : this . clientName ,
133136 clientUri : this . clientUri ,
134137 callbackUrl : this . callbackUrl ,
135- }
136- } ;
137- localStorage . setItem ( stateKey , JSON . stringify ( stateData ) ) ;
138+ } ,
139+ }
140+ localStorage . setItem ( stateKey , JSON . stringify ( stateData ) )
138141
139142 // Add the state parameter to the URL
140- authorizationUrl . searchParams . set ( 'state' , state ) ;
141- const authUrlString = authorizationUrl . toString ( ) ;
143+ authorizationUrl . searchParams . set ( 'state' , state )
144+ const authUrlString = authorizationUrl . toString ( )
142145
143146 // Persist the exact auth URL in case the popup fails and manual navigation is needed
144- localStorage . setItem ( this . getKey ( 'last_auth_url' ) , authUrlString ) ;
147+ localStorage . setItem ( this . getKey ( 'last_auth_url' ) , authUrlString )
145148
146149 // Attempt to open the popup
147- const popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes' ; // Make configurable if needed
150+ const popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes' // Make configurable if needed
148151 try {
149- const popup = window . open ( authUrlString , `mcp_auth_${ this . serverUrlHash } ` , popupFeatures ) ;
152+ const popup = window . open ( authUrlString , `mcp_auth_${ this . serverUrlHash } ` , popupFeatures )
150153
151154 if ( ! popup || popup . closed || typeof popup . closed === 'undefined' ) {
152- console . warn ( `[${ this . storageKeyPrefix } ] Popup likely blocked by browser. Manual navigation might be required using the stored URL.` ) ;
155+ console . warn (
156+ `[${ this . storageKeyPrefix } ] Popup likely blocked by browser. Manual navigation might be required using the stored URL.` ,
157+ )
153158 // Cannot signal failure back via SDK auth() directly.
154159 // useMcp will need to rely on timeout or manual trigger if stuck.
155160 } else {
156- popup . focus ( ) ;
157- console . info ( `[${ this . storageKeyPrefix } ] Redirecting to authorization URL in popup.` ) ;
161+ popup . focus ( )
162+ console . info ( `[${ this . storageKeyPrefix } ] Redirecting to authorization URL in popup.` )
158163 }
159164 } catch ( e ) {
160- console . error ( `[${ this . storageKeyPrefix } ] Error opening popup window:` , e ) ;
165+ console . error ( `[${ this . storageKeyPrefix } ] Error opening popup window:` , e )
161166 // Cannot signal failure back via SDK auth() directly.
162167 }
163168 // Regardless of popup success, the interface expects this method to initiate the redirect.
@@ -170,60 +175,59 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
170175 * Retrieves the last URL passed to `redirectToAuthorization`. Useful for manual fallback.
171176 */
172177 getLastAttemptedAuthUrl ( ) : string | null {
173- return localStorage . getItem ( this . getKey ( 'last_auth_url' ) ) ;
178+ return localStorage . getItem ( this . getKey ( 'last_auth_url' ) )
174179 }
175180
176-
177181 clearStorage ( ) : number {
178- const prefixPattern = `${ this . storageKeyPrefix } _${ this . serverUrlHash } _` ;
179- const statePattern = `${ this . storageKeyPrefix } :state_` ;
180- const keysToRemove : string [ ] = [ ] ;
181- let count = 0 ;
182+ const prefixPattern = `${ this . storageKeyPrefix } _${ this . serverUrlHash } _`
183+ const statePattern = `${ this . storageKeyPrefix } :state_`
184+ const keysToRemove : string [ ] = [ ]
185+ let count = 0
182186
183187 for ( let i = 0 ; i < localStorage . length ; i ++ ) {
184- const key = localStorage . key ( i ) ;
185- if ( ! key ) continue ;
188+ const key = localStorage . key ( i )
189+ if ( ! key ) continue
186190
187191 if ( key . startsWith ( prefixPattern ) ) {
188- keysToRemove . push ( key ) ;
192+ keysToRemove . push ( key )
189193 } else if ( key . startsWith ( statePattern ) ) {
190194 try {
191- const item = localStorage . getItem ( key ) ;
195+ const item = localStorage . getItem ( key )
192196 if ( item ) {
193197 // Check if state belongs to this provider instance based on serverUrlHash
194198 // We need to parse cautiously as the structure isn't guaranteed.
195- const state = JSON . parse ( item ) as Partial < StoredState > ;
199+ const state = JSON . parse ( item ) as Partial < StoredState >
196200 if ( state . serverUrlHash === this . serverUrlHash ) {
197- keysToRemove . push ( key ) ;
201+ keysToRemove . push ( key )
198202 }
199203 }
200204 } catch ( e ) {
201- console . warn ( `[${ this . storageKeyPrefix } ] Error parsing state key ${ key } during clearStorage:` , e ) ;
205+ console . warn ( `[${ this . storageKeyPrefix } ] Error parsing state key ${ key } during clearStorage:` , e )
202206 // Optionally remove malformed keys
203207 // keysToRemove.push(key);
204208 }
205209 }
206210 }
207211
208- const uniqueKeysToRemove = [ ...new Set ( keysToRemove ) ] ;
209- uniqueKeysToRemove . forEach ( key => {
210- localStorage . removeItem ( key ) ;
211- count ++ ;
212- } ) ;
213- return count ;
212+ const uniqueKeysToRemove = [ ...new Set ( keysToRemove ) ]
213+ uniqueKeysToRemove . forEach ( ( key ) => {
214+ localStorage . removeItem ( key )
215+ count ++
216+ } )
217+ return count
214218 }
215219
216220 private hashString ( str : string ) : string {
217- let hash = 0 ;
221+ let hash = 0
218222 for ( let i = 0 ; i < str . length ; i ++ ) {
219- const char = str . charCodeAt ( i ) ;
220- hash = ( hash << 5 ) - hash + char ;
221- hash = hash & hash ;
223+ const char = str . charCodeAt ( i )
224+ hash = ( hash << 5 ) - hash + char
225+ hash = hash & hash
222226 }
223- return Math . abs ( hash ) . toString ( 16 ) ;
227+ return Math . abs ( hash ) . toString ( 16 )
224228 }
225229
226230 getKey ( keySuffix : string ) : string {
227- return `${ this . storageKeyPrefix } _${ this . serverUrlHash } _${ keySuffix } ` ;
231+ return `${ this . storageKeyPrefix } _${ this . serverUrlHash } _${ keySuffix } `
228232 }
229- }
233+ }
0 commit comments