import { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; import { OAuthClientInformationSchema, OAuthClientInformation, OAuthTokens, OAuthTokensSchema, OAuthClientMetadata, OAuthMetadata, OAuthProtectedResourceMetadata, } from "@modelcontextprotocol/sdk/shared/auth.js"; import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js"; import { SESSION_KEYS, getServerSpecificKey } from "./constants"; import { generateOAuthState } from "@/utils/oauthUtils"; /** * Discovers OAuth scopes from server metadata, with preference for resource metadata scopes * @param serverUrl - The MCP server URL * @param resourceMetadata - Optional resource metadata containing preferred scopes * @returns Promise resolving to space-separated scope string or undefined */ export const discoverScopes = async ( serverUrl: string, resourceMetadata?: OAuthProtectedResourceMetadata, ): Promise => { try { const metadata = await discoverAuthorizationServerMetadata( new URL("/", serverUrl), ); // Prefer resource metadata scopes, but fall back to OAuth metadata if empty const resourceScopes = resourceMetadata?.scopes_supported; const oauthScopes = metadata?.scopes_supported; const scopesSupported = resourceScopes && resourceScopes.length > 0 ? resourceScopes : oauthScopes; return scopesSupported && scopesSupported.length > 0 ? scopesSupported.join(" ") : undefined; } catch (error) { console.debug("OAuth scope discovery failed:", error); return undefined; } }; export const getClientInformationFromSessionStorage = async ({ serverUrl, isPreregistered, }: { serverUrl: string; isPreregistered?: boolean; }) => { const key = getServerSpecificKey( isPreregistered ? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION : SESSION_KEYS.CLIENT_INFORMATION, serverUrl, ); const value = sessionStorage.getItem(key); if (!value) { return undefined; } return await OAuthClientInformationSchema.parseAsync(JSON.parse(value)); }; export const saveClientInformationToSessionStorage = ({ serverUrl, clientInformation, isPreregistered, }: { serverUrl: string; clientInformation: OAuthClientInformation; isPreregistered?: boolean; }) => { const key = getServerSpecificKey( isPreregistered ? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION : SESSION_KEYS.CLIENT_INFORMATION, serverUrl, ); sessionStorage.setItem(key, JSON.stringify(clientInformation)); }; export const clearClientInformationFromSessionStorage = ({ serverUrl, isPreregistered, }: { serverUrl: string; isPreregistered?: boolean; }) => { const key = getServerSpecificKey( isPreregistered ? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION : SESSION_KEYS.CLIENT_INFORMATION, serverUrl, ); sessionStorage.removeItem(key); }; export class InspectorOAuthClientProvider implements OAuthClientProvider { constructor( protected serverUrl: string, scope?: string, ) { this.scope = scope; // Save the server URL to session storage sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl); } scope: string | undefined; get redirectUrl() { return window.location.origin + "/oauth/callback"; } get clientMetadata(): OAuthClientMetadata { return { redirect_uris: [this.redirectUrl], token_endpoint_auth_method: "none", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], client_name: "MCP Inspector", client_uri: "https://github.com/modelcontextprotocol/inspector", scope: this.scope ?? "", }; } state(): string | Promise { return generateOAuthState(); } async clientInformation() { // Try to get the preregistered client information from session storage first const preregisteredClientInformation = await getClientInformationFromSessionStorage({ serverUrl: this.serverUrl, isPreregistered: true, }); // If no preregistered client information is found, get the dynamically registered client information return ( preregisteredClientInformation ?? (await getClientInformationFromSessionStorage({ serverUrl: this.serverUrl, isPreregistered: false, })) ); } saveClientInformation(clientInformation: OAuthClientInformation) { // Remove client_secret before storing (not needed after initial OAuth flow) const safeInfo = Object.fromEntries( Object.entries(clientInformation).filter( ([key]) => key !== "client_secret", ), ) as OAuthClientInformation; // Save the dynamically registered client information to session storage saveClientInformationToSessionStorage({ serverUrl: this.serverUrl, clientInformation: safeInfo, isPreregistered: false, }); } async tokens() { const key = getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl); const tokens = sessionStorage.getItem(key); if (!tokens) { return undefined; } return await OAuthTokensSchema.parseAsync(JSON.parse(tokens)); } saveTokens(tokens: OAuthTokens) { const key = getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl); sessionStorage.setItem(key, JSON.stringify(tokens)); } redirectToAuthorization(authorizationUrl: URL) { if ( authorizationUrl.protocol !== "http:" && authorizationUrl.protocol !== "https:" ) { throw new Error("Authorization URL must be HTTP or HTTPS"); } window.location.href = authorizationUrl.href; } saveCodeVerifier(codeVerifier: string) { const key = getServerSpecificKey( SESSION_KEYS.CODE_VERIFIER, this.serverUrl, ); sessionStorage.setItem(key, codeVerifier); } codeVerifier() { const key = getServerSpecificKey( SESSION_KEYS.CODE_VERIFIER, this.serverUrl, ); const verifier = sessionStorage.getItem(key); if (!verifier) { throw new Error("No code verifier saved for session"); } return verifier; } clear() { clearClientInformationFromSessionStorage({ serverUrl: this.serverUrl, isPreregistered: false, }); sessionStorage.removeItem( getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl), ); sessionStorage.removeItem( getServerSpecificKey(SESSION_KEYS.CODE_VERIFIER, this.serverUrl), ); } } // Overrides debug URL and allows saving server OAuth metadata to // display in debug UI. export class DebugInspectorOAuthClientProvider extends InspectorOAuthClientProvider { get redirectUrl(): string { return `${window.location.origin}/oauth/callback/debug`; } saveServerMetadata(metadata: OAuthMetadata) { const key = getServerSpecificKey( SESSION_KEYS.SERVER_METADATA, this.serverUrl, ); sessionStorage.setItem(key, JSON.stringify(metadata)); } getServerMetadata(): OAuthMetadata | null { const key = getServerSpecificKey( SESSION_KEYS.SERVER_METADATA, this.serverUrl, ); const metadata = sessionStorage.getItem(key); if (!metadata) { return null; } return JSON.parse(metadata); } clear() { super.clear(); sessionStorage.removeItem( getServerSpecificKey(SESSION_KEYS.SERVER_METADATA, this.serverUrl), ); } }