@@ -36,13 +36,14 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
3636 // when the redirect URI port changes between sessions.
3737 private _clientInfo ?: OAuthClientInformationFull
3838 private _closed = false
39+ private _refreshPromise : Promise < OAuthTokens > | null = null
3940
4041 private constructor (
4142 private readonly _serverUrl : string ,
4243 private readonly _secretStorage : SecretStorageService ,
43- private readonly _server : http . Server ,
44- private readonly _port : number ,
45- private readonly _authCodePromise : Promise < string > ,
44+ private _server : http . Server | null ,
45+ private _port : number ,
46+ private _authCodePromise : Promise < string > | null ,
4647 private readonly _tokenEndpointAuthMethod : string ,
4748 private readonly _grantTypes : string [ ] ,
4849 private readonly _scopes : string [ ] ,
@@ -120,6 +121,20 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
120121 return `http://localhost:${ this . _port } /callback`
121122 }
122123
124+ private async _ensureCallbackServer ( ) : Promise < void > {
125+ if ( this . _server && ! this . _closed ) return
126+
127+ this . _closed = false
128+ const { server, port, result } = await startCallbackServer ( this . _port , this . _state )
129+ this . _server = server
130+ this . _port = port
131+ this . _authCodePromise = result . then ( ( r ) => {
132+ if ( r . error ) throw new Error ( `OAuth authorization failed: ${ r . error } ` )
133+ if ( ! r . code ) throw new Error ( "No authorization code received in callback" )
134+ return r . code
135+ } )
136+ }
137+
123138 state ( ) : string {
124139 return this . _state
125140 }
@@ -192,10 +207,33 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
192207 async tokens ( ) : Promise < OAuthTokens | undefined > {
193208 const data = await this . _secretStorage . getOAuthData ( this . _serverUrl )
194209 if ( ! data ) return undefined
195- // Return undefined 5 minutes before expiry so the SDK triggers re-auth
196- // before the server actually rejects requests.
197- if ( Date . now ( ) >= data . expires_at - 5 * 60 * 1000 ) return undefined
198- return data . tokens
210+
211+ // If the access token is still valid (with 5m buffer), return it.
212+ if ( Date . now ( ) < data . expires_at - 5 * 60 * 1000 ) {
213+ return data . tokens
214+ }
215+
216+ // Access token is expired or near expiry. Try to refresh if we have a refresh token.
217+ if ( data . tokens . refresh_token ) {
218+ if ( this . _refreshPromise ) {
219+ return this . _refreshPromise
220+ }
221+
222+ this . _refreshPromise = this . refreshAccessToken ( data . tokens . refresh_token ) . finally ( ( ) => {
223+ this . _refreshPromise = null
224+ } )
225+
226+ try {
227+ return await this . _refreshPromise
228+ } catch ( error ) {
229+ console . error ( `Failed to refresh MCP OAuth token for ${ this . _serverUrl } :` , error )
230+ // Clear stale tokens on refresh failure so we don't keep retrying a dead refresh token
231+ await this . _secretStorage . deleteOAuthData ( this . _serverUrl )
232+ // Fall through to return undefined, which triggers full re-auth
233+ }
234+ }
235+
236+ return undefined
199237 }
200238
201239 async saveTokens ( tokens : OAuthTokens ) : Promise < void > {
@@ -209,6 +247,10 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
209247 }
210248
211249 async redirectToAuthorization ( authorizationUrl : URL ) : Promise < void > {
250+ // Ensure the callback server is running before opening the browser.
251+ // This handles mid-session re-auth where the initial server was closed.
252+ await this . _ensureCallbackServer ( )
253+
212254 // Workaround for SDK metadata discovery bug (see utils/oauth.ts for issue links).
213255 // The SDK's discoverOAuthMetadata() builds a wrong well-known URL for issuers
214256 // with path components, causing it to fall back to a default "/authorize" path.
@@ -267,8 +309,11 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
267309 * browser flow and the local callback server receives the redirect.
268310 * Rejects on error or 5-minute timeout.
269311 */
270- waitForAuthCode ( ) : Promise < string > {
271- return this . _authCodePromise
312+ async waitForAuthCode ( ) : Promise < string > {
313+ if ( ! this . _authCodePromise ) {
314+ await this . _ensureCallbackServer ( )
315+ }
316+ return this . _authCodePromise !
272317 }
273318
274319 /**
@@ -327,11 +372,54 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
327372 await this . saveTokens ( tokens )
328373 }
329374
375+ /**
376+ * Refreshes the access token using a refresh token.
377+ * @param refreshToken The refresh token to use.
378+ * @returns The new tokens.
379+ */
380+ async refreshAccessToken ( refreshToken : string ) : Promise < OAuthTokens > {
381+ if ( ! this . _authServerMeta ?. token_endpoint ) {
382+ throw new Error ( "No token_endpoint in auth server metadata — cannot refresh token" )
383+ }
384+ if ( ! this . _clientInfo ) {
385+ throw new Error ( "No client information — registerClientIfNeeded() must be called first" )
386+ }
387+
388+ const params : Record < string , string > = {
389+ grant_type : "refresh_token" ,
390+ refresh_token : refreshToken ,
391+ client_id : this . _clientInfo . client_id ,
392+ }
393+
394+ if ( this . _tokenEndpointAuthMethod === "client_secret_post" && this . _clientInfo . client_secret ) {
395+ params . client_secret = this . _clientInfo . client_secret
396+ }
397+
398+ const response = await fetch ( this . _authServerMeta . token_endpoint as string , {
399+ method : "POST" ,
400+ headers : {
401+ "Content-Type" : "application/x-www-form-urlencoded" ,
402+ Accept : "application/json" ,
403+ } ,
404+ body : new URLSearchParams ( params ) . toString ( ) ,
405+ } )
406+
407+ if ( ! response . ok ) {
408+ throw new Error ( `Token refresh failed: HTTP ${ response . status } ` )
409+ }
410+
411+ const tokens = ( await response . json ( ) ) as OAuthTokens
412+ await this . saveTokens ( tokens )
413+ return tokens
414+ }
415+
330416 /** Close the local callback server. Always call this when done. */
331417 async close ( ) : Promise < void > {
332- if ( ! this . _closed ) {
418+ if ( ! this . _closed && this . _server ) {
333419 this . _closed = true
334420 await stopCallbackServer ( this . _server ) . catch ( ( ) => { } )
421+ this . _server = null
422+ this . _authCodePromise = null
335423 }
336424 }
337425}
0 commit comments