From c2dbff738e582941d6b1af04c4b9f41c28305487 Mon Sep 17 00:00:00 2001 From: Stanislav Deviatykh Date: Thu, 16 Oct 2025 00:50:42 +0300 Subject: [PATCH 1/4] fix: set user specified scope in DCR --- client/src/lib/auth.ts | 34 ++++++++++++++++++++++----- client/src/lib/constants.ts | 1 + client/src/lib/hooks/useConnection.ts | 12 ++++++---- client/src/lib/oauth-state-machine.ts | 29 ++++++++++++++--------- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 23e21b6a4..edd3ef585 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -102,16 +102,35 @@ export const clearClientInformationFromSessionStorage = ({ sessionStorage.removeItem(key); }; +export const getScopeFromSessionStorage = ( + serverUrl: string, +): string | undefined => { + const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl); + const value = sessionStorage.getItem(key); + return value || undefined; +}; + +export const saveScopeToSessionStorage = ( + serverUrl: string, + scope: string | undefined, +) => { + const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl); + if (scope) { + sessionStorage.setItem(key, scope); + } else { + sessionStorage.removeItem(key); + } +}; + export class InspectorOAuthClientProvider implements OAuthClientProvider { - constructor( - protected serverUrl: string, - scope?: string, - ) { - this.scope = scope; + constructor(protected serverUrl: string) { // Save the server URL to session storage sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl); } - scope: string | undefined; + + get scope(): string | undefined { + return getScopeFromSessionStorage(this.serverUrl); + } get redirectUrl() { return window.location.origin + "/oauth/callback"; @@ -224,6 +243,9 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { sessionStorage.removeItem( getServerSpecificKey(SESSION_KEYS.CODE_VERIFIER, this.serverUrl), ); + sessionStorage.removeItem( + getServerSpecificKey(SESSION_KEYS.SCOPE, this.serverUrl), + ); } } diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts index 8e4ff2b56..7e4e0697d 100644 --- a/client/src/lib/constants.ts +++ b/client/src/lib/constants.ts @@ -17,6 +17,7 @@ export const SESSION_KEYS = { PREREGISTERED_CLIENT_INFORMATION: "mcp_preregistered_client_information", SERVER_METADATA: "mcp_server_metadata", AUTH_DEBUGGER_STATE: "mcp_auth_debugger_state", + SCOPE: "mcp_scope", } as const; // Generate server-specific session storage keys diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 8639feebc..f6d42380c 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -45,6 +45,7 @@ import { clearClientInformationFromSessionStorage, InspectorOAuthClientProvider, saveClientInformationToSessionStorage, + saveScopeToSessionStorage, discoverScopes, } from "../auth"; import { @@ -142,6 +143,10 @@ export function useConnection({ }); }, [oauthClientId, oauthClientSecret, sseUrl]); + useEffect(() => { + saveScopeToSessionStorage(sseUrl, oauthScope); + }, [oauthScope, sseUrl]); + const pushHistory = (request: object, response?: object) => { setRequestHistory((prev) => [ ...prev, @@ -346,10 +351,9 @@ export function useConnection({ } scope = await discoverScopes(sseUrl, resourceMetadata); } - const serverAuthProvider = new InspectorOAuthClientProvider( - sseUrl, - scope, - ); + + saveScopeToSessionStorage(sseUrl, scope); + const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl); const result = await auth(serverAuthProvider, { serverUrl: sseUrl, diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index c505f698e..8dc9da8f9 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -80,13 +80,16 @@ export const oauthTransitions: Record = { const metadata = context.state.oauthMetadata!; const clientMetadata = context.provider.clientMetadata; - // Prefer scopes from resource metadata if available - const scopesSupported = - context.state.resourceMetadata?.scopes_supported || - metadata.scopes_supported; - // Add all supported scopes to client registration - if (scopesSupported) { - clientMetadata.scope = scopesSupported.join(" "); + // Priority: user-provided scope > discovered scopes + if (!context.provider.scope || context.provider.scope.trim() === "") { + // Prefer scopes from resource metadata if available + const scopesSupported = + context.state.resourceMetadata?.scopes_supported || + metadata.scopes_supported; + // Add all supported scopes to client registration + if (scopesSupported) { + clientMetadata.scope = scopesSupported.join(" "); + } } // Try Static client first, with DCR as fallback @@ -113,10 +116,14 @@ export const oauthTransitions: Record = { const metadata = context.state.oauthMetadata!; const clientInformation = context.state.oauthClientInfo!; - const scope = await discoverScopes( - context.serverUrl, - context.state.resourceMetadata ?? undefined, - ); + // Priority: user-provided scope > discovered scopes + let scope = context.provider.scope; + if (!scope || scope.trim() === "") { + scope = await discoverScopes( + context.serverUrl, + context.state.resourceMetadata ?? undefined, + ); + } const { authorizationUrl, codeVerifier } = await startAuthorization( context.serverUrl, From 40fa6cb27d65fec026bec36f54e7f8d059817e2f Mon Sep 17 00:00:00 2001 From: Stanislav Deviatykh Date: Thu, 16 Oct 2025 00:57:06 +0300 Subject: [PATCH 2/4] refactor test --- client/src/lib/hooks/__tests__/useConnection.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index a9ba6825b..2d09b8b2a 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -112,6 +112,7 @@ jest.mock("../../auth", () => ({ })), clearClientInformationFromSessionStorage: jest.fn(), saveClientInformationToSessionStorage: jest.fn(), + saveScopeToSessionStorage: jest.fn(), discoverScopes: jest.fn(), })); From eb76696d9748b07c79b07736ac5d26d922107725 Mon Sep 17 00:00:00 2001 From: Stanislav Deviatykh Date: Wed, 22 Oct 2025 00:09:57 +0300 Subject: [PATCH 3/4] correctly clear scope --- client/src/lib/auth.ts | 8 +++++--- client/src/lib/hooks/useConnection.ts | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index edd3ef585..797501127 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -122,6 +122,11 @@ export const saveScopeToSessionStorage = ( } }; +export const clearScopeFromSessionStorage = (serverUrl: string) => { + const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl); + sessionStorage.removeItem(key); +}; + export class InspectorOAuthClientProvider implements OAuthClientProvider { constructor(protected serverUrl: string) { // Save the server URL to session storage @@ -243,9 +248,6 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { sessionStorage.removeItem( getServerSpecificKey(SESSION_KEYS.CODE_VERIFIER, this.serverUrl), ); - sessionStorage.removeItem( - getServerSpecificKey(SESSION_KEYS.SCOPE, this.serverUrl), - ); } } diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index f6d42380c..bd15e080a 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -46,6 +46,7 @@ import { InspectorOAuthClientProvider, saveClientInformationToSessionStorage, saveScopeToSessionStorage, + clearScopeFromSessionStorage, discoverScopes, } from "../auth"; import { @@ -144,6 +145,11 @@ export function useConnection({ }, [oauthClientId, oauthClientSecret, sseUrl]); useEffect(() => { + if (!oauthScope) { + clearScopeFromSessionStorage(sseUrl); + return; + } + saveScopeToSessionStorage(sseUrl, oauthScope); }, [oauthScope, sseUrl]); From 3b247ea21f18511f33c0de91d47e78c53bc2a092 Mon Sep 17 00:00:00 2001 From: Stanislav Deviatykh Date: Wed, 22 Oct 2025 00:11:58 +0300 Subject: [PATCH 4/4] mock clearScopeFromSessionStorage --- client/src/lib/hooks/__tests__/useConnection.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index 2d09b8b2a..b4d6705b8 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -113,6 +113,7 @@ jest.mock("../../auth", () => ({ clearClientInformationFromSessionStorage: jest.fn(), saveClientInformationToSessionStorage: jest.fn(), saveScopeToSessionStorage: jest.fn(), + clearScopeFromSessionStorage: jest.fn(), discoverScopes: jest.fn(), }));