@@ -27,6 +27,7 @@ import type {
2727 McpTool ,
2828 McpToolCallResponse ,
2929} from "@roo-code/types"
30+ import { TOKEN_EXPIRY_BUFFER_MS } from "./utils/constants"
3031
3132import { t } from "../../i18n"
3233
@@ -454,7 +455,7 @@ export class McpHub {
454455 const projectConnections = this . connections . filter ( ( conn ) => conn . server . source === "project" )
455456
456457 for ( const conn of projectConnections ) {
457- await this . deleteConnection ( conn . server . name , "project" )
458+ await this . deleteConnection ( conn . server . name , conn . server . source )
458459 }
459460
460461 // Clear project servers from the connections list
@@ -797,13 +798,8 @@ export class McpHub {
797798
798799 // Create an OAuth provider for this server.
799800 //
800- // McpOAuthClientProvider.create() performs OAuth discovery (RFC 9728 +
801- // RFC 8414) once and starts the local callback server so the redirect
802- // URI port is stable before any connect attempt.
803- //
804- // If the server already has a stored token the SDK will use it
805- // transparently; the browser is only opened when a 401 forces a new
806- // authorization flow.
801+ // McpOAuthClientProvider.create() returns a provider instance.
802+ // Discovery and callback-server startup are deferred until actually needed.
807803 const authProvider = await McpOAuthClientProvider . create ( configInjected . url , this . secretStorage , name )
808804
809805 // Pre-register the OAuth client so the SDK can skip its own
@@ -841,10 +837,10 @@ export class McpHub {
841837 if ( ! reauthPromise ) {
842838 reauthPromise = this . _completeOAuthFlow (
843839 authProvider ,
844- transport as StreamableHTTPClientTransport ,
845840 connection as ConnectedMcpConnection ,
846841 name ,
847842 source ,
843+ false , // mid-session re-auth needs toast
848844 )
849845 . catch ( ( err ) => {
850846 console . error ( `OAuth flow failed for "${ name } ":` , err )
@@ -965,58 +961,61 @@ export class McpHub {
965961 connection . server . status = "connecting"
966962
967963 void ( async ( ) => {
968- const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000
969-
970- // Check if another window already saved valid tokens
971- const existing = await this . secretStorage ! . getOAuthData ( serverUrl )
972- if ( existing && Date . now ( ) < existing . expires_at - TOKEN_EXPIRY_BUFFER_MS ) {
973- await streamableHttpAuthProvider . close ( )
974- await this . deleteConnection ( name , source )
975- await this . connectToServer ( name , config , source )
976- await this . notifyWebviewOfServerChanges ( )
977- return
978- }
979-
980- // Show a confirmation toast so the user can decide whether to authenticate.
981- // This resolves immediately when the user responds — but connectToServer
982- // has already returned so the panel is not blocked.
983- const choice = await vscode . window . showInformationMessage (
984- `MCP server "${ name } " requires authentication.` ,
985- "Authenticate" ,
986- )
987-
988- if ( choice === "Authenticate" ) {
989- // Check tokens again — another window may have authed while toast was showing
990- const tokens = await this . secretStorage ! . getOAuthData ( serverUrl )
991- if ( tokens && Date . now ( ) < tokens . expires_at - TOKEN_EXPIRY_BUFFER_MS ) {
964+ try {
965+ // Check if another window already saved valid tokens
966+ const existing = await this . secretStorage ! . getOAuthData ( serverUrl )
967+ if ( existing && Date . now ( ) < existing . expires_at - TOKEN_EXPIRY_BUFFER_MS ) {
992968 await streamableHttpAuthProvider . close ( )
993969 await this . deleteConnection ( name , source )
994970 await this . connectToServer ( name , config , source )
995971 await this . notifyWebviewOfServerChanges ( )
996972 return
997973 }
998- void this . _completeOAuthFlow (
999- streamableHttpAuthProvider ,
1000- transport as StreamableHTTPClientTransport ,
1001- connection ,
1002- name ,
1003- source ,
974+
975+ // Show a confirmation toast so the user can decide whether to authenticate.
976+ // This resolves immediately when the user responds — but connectToServer
977+ // has already returned so the panel is not blocked.
978+ const choice = await vscode . window . showInformationMessage (
979+ `MCP server "${ name } " requires authentication.` ,
980+ "Authenticate" ,
1004981 )
1005- } else {
1006- // Toast was dismissed or auto-timed-out.
1007- // First do an immediate check — another window may have already
1008- // completed auth while the toast was showing.
1009- const tokens = await this . secretStorage ! . getOAuthData ( serverUrl )
1010- if ( tokens && Date . now ( ) < tokens . expires_at - TOKEN_EXPIRY_BUFFER_MS ) {
982+
983+ if ( choice === "Authenticate" ) {
984+ // Check tokens again — another window may have authed while toast was showing
985+ const tokens = await this . secretStorage ! . getOAuthData ( serverUrl )
986+ if ( tokens && Date . now ( ) < tokens . expires_at - TOKEN_EXPIRY_BUFFER_MS ) {
987+ await streamableHttpAuthProvider . close ( )
988+ await this . deleteConnection ( name , source )
989+ await this . connectToServer ( name , config , source )
990+ await this . notifyWebviewOfServerChanges ( )
991+ return
992+ }
993+ void this . _completeOAuthFlow (
994+ streamableHttpAuthProvider ,
995+ connection ,
996+ name ,
997+ source ,
998+ true , // initial connect already showed toast
999+ )
1000+ } else {
1001+ // Toast was dismissed or auto-timed-out.
1002+ // First do an immediate check — another window may have already
1003+ // completed auth while the toast was showing.
1004+ const tokens = await this . secretStorage ! . getOAuthData ( serverUrl )
1005+ if ( tokens && Date . now ( ) < tokens . expires_at - TOKEN_EXPIRY_BUFFER_MS ) {
1006+ await streamableHttpAuthProvider . close ( )
1007+ await this . deleteConnection ( name , source )
1008+ await this . connectToServer ( name , config , source )
1009+ await this . notifyWebviewOfServerChanges ( )
1010+ return
1011+ }
1012+ // Tokens not available yet — start watching for them
10111013 await streamableHttpAuthProvider . close ( )
1012- await this . deleteConnection ( name , source )
1013- await this . connectToServer ( name , config , source )
1014- await this . notifyWebviewOfServerChanges ( )
1015- return
1014+ this . _watchForOAuthTokens ( name , source , serverUrl , config )
10161015 }
1017- // Tokens not available yet — start watching for them
1016+ } catch ( error ) {
1017+ console . error ( `[McpHub] Initial OAuth flow failed for "${ name } ":` , error )
10181018 await streamableHttpAuthProvider . close ( )
1019- this . _watchForOAuthTokens ( name , source , serverUrl , config )
10201019 }
10211020 } ) ( )
10221021
@@ -1067,13 +1066,40 @@ export class McpHub {
10671066 */
10681067 private async _completeOAuthFlow (
10691068 authProvider : McpOAuthClientProvider ,
1070- transport : StreamableHTTPClientTransport ,
10711069 connection : ConnectedMcpConnection ,
10721070 name : string ,
10731071 source : "global" | "project" ,
1072+ skipToast : boolean = false ,
10741073 ) : Promise < void > {
10751074 const config = JSON . parse ( connection . server . config )
1075+ const serverUrl = config . url
1076+
10761077 try {
1078+ if ( ! skipToast ) {
1079+ // Mid-session re-auth: check if tokens were already refreshed by another window
1080+ const tokens = await this . secretStorage ?. getOAuthData ( serverUrl )
1081+ if ( tokens && Date . now ( ) < tokens . expires_at - TOKEN_EXPIRY_BUFFER_MS ) {
1082+ // Tokens are already valid, just reconnect
1083+ const validatedConfig = this . validateServerConfig ( config , name )
1084+ await this . deleteConnection ( name , source )
1085+ await this . connectToServer ( name , validatedConfig , source )
1086+ await this . notifyWebviewOfServerChanges ( )
1087+ return
1088+ }
1089+
1090+ const choice = await vscode . window . showInformationMessage (
1091+ `MCP server "${ name } " requires re-authentication.` ,
1092+ "Authenticate" ,
1093+ )
1094+
1095+ if ( choice !== "Authenticate" ) {
1096+ // User cancelled or dismissed toast.
1097+ // We don't start a watcher here because _completeOAuthFlow is already
1098+ // running as a detached task. We just let it exit.
1099+ return
1100+ }
1101+ }
1102+
10771103 // Open the browser now that the user has confirmed the toast.
10781104 // redirectToAuthorization() was already called by the SDK (which stored
10791105 // the URL in _pendingAuthorizationUrl), but deliberately did not open it.
@@ -1130,8 +1156,6 @@ export class McpHub {
11301156 this . _oauthWatchers . delete ( watcherKey )
11311157 }
11321158
1133- const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000
1134-
11351159 // Called when SecretStorage fires onDidChange for this server's key.
11361160 // Runs in all VS Code windows the instant tokens are saved — no polling delay.
11371161 const onTokensChanged = async ( ) => {
@@ -1581,11 +1605,6 @@ export class McpHub {
15811605 // Validate the config
15821606 const validatedConfig = this . validateServerConfig ( parsedConfig , serverName )
15831607
1584- // Clear OAuth tokens for streamable-http servers on restart
1585- if ( validatedConfig . type === "streamable-http" && this . secretStorage ) {
1586- await this . secretStorage . deleteOAuthData ( validatedConfig . url )
1587- }
1588-
15891608 // Try to connect again using validated config
15901609 await this . connectToServer ( serverName , validatedConfig , connection . server . source || "global" )
15911610 vscode . window . showInformationMessage ( t ( "mcp:info.server_connected" , { serverName } ) )
@@ -2086,10 +2105,10 @@ export class McpHub {
20862105 if ( ! reauthPromise ) {
20872106 reauthPromise = this . _completeOAuthFlow (
20882107 connection . authProvider ,
2089- connection . transport as StreamableHTTPClientTransport ,
20902108 connection ,
20912109 serverName ,
20922110 source || connection . server . source || "global" ,
2111+ false , // mid-session re-auth needs toast
20932112 ) . finally ( ( ) => {
20942113 this . reauthPromises . delete ( reauthKey )
20952114 } )
0 commit comments