Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions client/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
1 change: 1 addition & 0 deletions client/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions client/src/lib/hooks/__tests__/useConnection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ jest.mock("../../auth", () => ({
})),
clearClientInformationFromSessionStorage: jest.fn(),
saveClientInformationToSessionStorage: jest.fn(),
saveScopeToSessionStorage: jest.fn(),
clearScopeFromSessionStorage: jest.fn(),
discoverScopes: jest.fn(),
}));

Expand Down
18 changes: 14 additions & 4 deletions client/src/lib/hooks/useConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import {
clearClientInformationFromSessionStorage,
InspectorOAuthClientProvider,
saveClientInformationToSessionStorage,
saveScopeToSessionStorage,
clearScopeFromSessionStorage,
discoverScopes,
} from "../auth";
import {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
29 changes: 18 additions & 11 deletions client/src/lib/oauth-state-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,16 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
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
Expand All @@ -113,10 +116,14 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
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,
Expand Down