diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 23e21b6a4..797501127 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -102,16 +102,40 @@ 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 const clearScopeFromSessionStorage = (serverUrl: string) => { + const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl); + 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"; diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts index 85cc0b364..e7edc025e 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/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index a9ba6825b..b4d6705b8 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -112,6 +112,8 @@ jest.mock("../../auth", () => ({ })), clearClientInformationFromSessionStorage: jest.fn(), saveClientInformationToSessionStorage: jest.fn(), + saveScopeToSessionStorage: jest.fn(), + clearScopeFromSessionStorage: jest.fn(), discoverScopes: jest.fn(), })); diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 8639feebc..bd15e080a 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -45,6 +45,8 @@ import { clearClientInformationFromSessionStorage, InspectorOAuthClientProvider, saveClientInformationToSessionStorage, + saveScopeToSessionStorage, + clearScopeFromSessionStorage, discoverScopes, } from "../auth"; import { @@ -142,6 +144,15 @@ export function useConnection({ }); }, [oauthClientId, oauthClientSecret, sseUrl]); + useEffect(() => { + if (!oauthScope) { + clearScopeFromSessionStorage(sseUrl); + return; + } + + saveScopeToSessionStorage(sseUrl, oauthScope); + }, [oauthScope, sseUrl]); + const pushHistory = (request: object, response?: object) => { setRequestHistory((prev) => [ ...prev, @@ -346,10 +357,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,