Skip to content

Commit 11798c2

Browse files
committed
feedback
1 parent 4b1df52 commit 11798c2

9 files changed

Lines changed: 391 additions & 202 deletions

File tree

src/i18n/locales/en/mcp.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,17 @@
2424
"refreshing_all": "Refreshing all MCP servers...",
2525
"all_refreshed": "All MCP servers have been refreshed.",
2626
"project_config_deleted": "Project MCP configuration file deleted. All project MCP servers have been disconnected."
27+
},
28+
"oauth": {
29+
"callback_title": "OAuth Callback - Roo Code",
30+
"failed": "Failed",
31+
"success": "Success!",
32+
"auth_failed": "Authentication failed: {{error}}. Please check the MCP server logs.",
33+
"auth_success": "MCP server authenticated successfully. You can now close this browser tab.",
34+
"connection_complete": "The server connection is complete.",
35+
"close_countdown": "This tab will attempt to close in <span id=\"count\">{{seconds}}</span>s...",
36+
"manual_close": "If the tab did not close, you can safely close it manually.",
37+
"invalid_state": "Error: Invalid state parameter",
38+
"auth_failed_title": "OAuth Authentication Failed"
2739
}
2840
}

src/services/mcp/McpHub.ts

Lines changed: 79 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -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

3132
import { 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

Comments
 (0)