Skip to content

Commit 7c1ef9a

Browse files
committed
fix(McpHub): not blocking on authprovider flow
1 parent 8efcc35 commit 7c1ef9a

5 files changed

Lines changed: 102 additions & 3 deletions

File tree

apps/vscode-e2e/src/suite/mcp-oauth.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,13 @@ suite("Roo Code MCP OAuth", function () {
128128

129129
// SDK constructs: new URL("/.well-known/oauth-authorization-server", "http://host/auth")
130130
// which resolves to http://host/.well-known/oauth-authorization-server (origin-relative)
131-
if (url === "/.well-known/oauth-authorization-server") {
131+
// Our custom fetchOAuthAuthServerMetadata constructs the RFC 8414 URL with issuer path:
132+
// /.well-known/oauth-authorization-server/auth (with issuer path)
133+
// Handle BOTH forms so our provider gets _authServerMeta.
134+
if (
135+
url === "/.well-known/oauth-authorization-server" ||
136+
url === "/.well-known/oauth-authorization-server/auth"
137+
) {
132138
endpointsHit.add("auth-metadata")
133139
res.writeHead(200, { "Content-Type": "application/json" })
134140
res.end(

src/services/mcp/McpHub.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ export class McpHub {
985985
// and hits the same broken URL for path-prefixed issuers (see
986986
// utils/oauth.ts for upstream issue links).
987987
await authProvider.exchangeCodeForTokens(code)
988-
await authProvider.close()
988+
authProvider.close().catch(console.error)
989989

990990
// Recover the validated server config stored on the connection so we
991991
// can pass it directly to connectToServer without re-reading the file.

src/services/mcp/McpOAuthClientProvider.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,21 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
156156
*/
157157
async registerClientIfNeeded(): Promise<void> {
158158
if (this._clientInfo) return // already registered
159+
160+
// Check if we have a cached client_id from previous registration
161+
const cachedData = await this._secretStorage.getOAuthData(this._serverUrl)
162+
if (cachedData?.client_id && cachedData.redirect_uri === this.redirectUrl) {
163+
this._clientInfo = {
164+
client_id: cachedData.client_id,
165+
redirect_uris: [this.redirectUrl],
166+
client_name: this._clientName,
167+
grant_types: this._grantTypes,
168+
response_types: ["code"],
169+
token_endpoint_auth_method: this._tokenEndpointAuthMethod,
170+
}
171+
return
172+
}
173+
159174
if (!this._authServerMeta?.registration_endpoint) return // DCR not supported
160175

161176
const response = await fetch(this._authServerMeta.registration_endpoint as string, {
@@ -185,7 +200,12 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
185200

186201
async saveTokens(tokens: OAuthTokens): Promise<void> {
187202
const expires_at = tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : Date.now() + 3600 * 1000 // default 1 hour when server omits expires_in
188-
await this._secretStorage.saveOAuthData(this._serverUrl, { tokens, expires_at })
203+
await this._secretStorage.saveOAuthData(this._serverUrl, {
204+
tokens,
205+
expires_at,
206+
client_id: this._clientInfo?.client_id,
207+
redirect_uri: this.redirectUrl,
208+
})
189209
}
190210

191211
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {

src/services/mcp/SecretStorageService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export interface StoredMcpOAuthData {
55
tokens: OAuthTokens
66
/** Unix ms timestamp after which the access token should be considered expired. */
77
expires_at: number
8+
/** The client_id used to obtain these tokens (for token reuse without re-registration). */
9+
client_id?: string
10+
/** The redirect_uri used during client registration (to detect port changes). */
11+
redirect_uri?: string
812
}
913

1014
/**

src/services/mcp/__tests__/McpOAuthClientProvider.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ mockFetch.mockResolvedValue({
3939
issuer: "https://auth.example.com",
4040
authorization_endpoint: "https://auth.example.com/authorize",
4141
token_endpoint: "https://auth.example.com/token",
42+
registration_endpoint: "https://auth.example.com/register",
4243
response_types_supported: ["code"],
4344
token_endpoint_auth_methods_supported: ["none"],
4445
grant_types_supported: ["authorization_code", "refresh_token"],
@@ -543,4 +544,72 @@ describe("McpOAuthClientProvider", () => {
543544
expect(stopCallbackServer).toHaveBeenCalledTimes(1)
544545
})
545546
})
547+
548+
describe("registerClientIfNeeded", () => {
549+
it("should reuse cached client_id when redirect_uri matches", async () => {
550+
setupCallbackServerMock()
551+
const secretStorage = createMockSecretStorage()
552+
553+
// Pre-populate storage with cached data
554+
await secretStorage.saveOAuthData("https://example.com/mcp", {
555+
tokens: { access_token: "cached-token", token_type: "Bearer" },
556+
expires_at: Date.now() + 3600000,
557+
client_id: "cached-client-id",
558+
redirect_uri: "http://localhost:12345/callback",
559+
})
560+
561+
const provider = await McpOAuthClientProvider.create("https://example.com/mcp", secretStorage)
562+
await provider.registerClientIfNeeded()
563+
564+
expect((await provider.clientInformation())?.client_id).toBe("cached-client-id")
565+
await provider.close()
566+
})
567+
568+
it("should not reuse cached client_id when redirect_uri does not match", async () => {
569+
setupCallbackServerMock()
570+
const secretStorage = createMockSecretStorage()
571+
572+
// Clear previous mocks and set up for this test
573+
mockFetch.mockClear()
574+
mockFetch.mockResolvedValueOnce({
575+
ok: true,
576+
json: () =>
577+
Promise.resolve({
578+
issuer: "https://auth.example.com",
579+
authorization_endpoint: "https://auth.example.com/authorize",
580+
token_endpoint: "https://auth.example.com/token",
581+
registration_endpoint: "https://auth.example.com/register",
582+
response_types_supported: ["code"],
583+
token_endpoint_auth_methods_supported: ["none"],
584+
grant_types_supported: ["authorization_code", "refresh_token"],
585+
}),
586+
})
587+
mockFetch.mockResolvedValueOnce({
588+
ok: true,
589+
json: () =>
590+
Promise.resolve({
591+
client_id: "new-client-id",
592+
redirect_uris: ["http://localhost:12345/callback"],
593+
client_name: "Roo Code",
594+
grant_types: ["authorization_code", "refresh_token"],
595+
response_types: ["code"],
596+
token_endpoint_auth_method: "none",
597+
}),
598+
})
599+
600+
// Pre-populate storage with cached data with different redirect_uri
601+
await secretStorage.saveOAuthData("https://example.com/mcp", {
602+
tokens: { access_token: "cached-token", token_type: "Bearer" },
603+
expires_at: Date.now() + 3600000,
604+
client_id: "cached-client-id",
605+
redirect_uri: "http://localhost:99999/callback", // different port
606+
})
607+
608+
const provider = await McpOAuthClientProvider.create("https://example.com/mcp", secretStorage)
609+
await provider.registerClientIfNeeded()
610+
611+
expect((await provider.clientInformation())?.client_id).toBe("new-client-id")
612+
await provider.close()
613+
})
614+
})
546615
})

0 commit comments

Comments
 (0)