GEMINI TO THE RESCUE

This commit is contained in:
Glen Maddern 2025-05-06 14:59:38 +10:00
commit d0372fd8d8
3 changed files with 573 additions and 757 deletions

View file

@ -1,25 +1,24 @@
import { OAuthClientInformation, OAuthMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
// browser-provider.ts
import { OAuthClientInformation, OAuthMetadata, OAuthTokens, OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js';
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
import { StoredState } from './types.js';
// Assuming StoredState is defined in ./types.js and includes fields for provider options
import { StoredState } from './types.js'; // Adjust path if necessary
/**
* Browser-compatible OAuth client provider for MCP using localStorage.
* Handles storing client information, tokens, and managing the popup/redirect flow.
*/
export class BrowserOAuthClientProvider implements OAuthClientProvider {
private storageKeyPrefix: string;
private serverUrlHash: string;
private clientName: string;
private clientUri: string;
private callbackUrl: string;
readonly serverUrl: string;
readonly storageKeyPrefix: string;
readonly serverUrlHash: string;
readonly clientName: string;
readonly clientUri: string;
readonly callbackUrl: string;
/**
* Creates an instance of BrowserOAuthClientProvider.
* @param serverUrl The base URL of the MCP server requiring authentication.
* @param options Configuration options for the provider.
*/
constructor(
readonly serverUrl: string,
serverUrl: string,
options: {
storageKeyPrefix?: string;
clientName?: string;
@ -27,26 +26,21 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
callbackUrl?: string;
} = {},
) {
this.serverUrl = serverUrl;
this.storageKeyPrefix = options.storageKeyPrefix || 'mcp:auth';
// Hash the server URL to create unique storage keys per server
this.serverUrlHash = this.hashString(serverUrl);
this.clientName = options.clientName || 'MCP Browser Client';
this.clientUri = options.clientUri || window.location.origin;
// Default callback URL is /oauth/callback on the current origin
this.callbackUrl = options.callbackUrl || new URL('/oauth/callback', window.location.origin).toString();
this.clientUri = options.clientUri || (typeof window !== 'undefined' ? window.location.origin : '');
this.callbackUrl = options.callbackUrl || (typeof window !== 'undefined' ? new URL('/oauth/callback', window.location.origin).toString() : '/oauth/callback');
}
/**
* The redirect URL used for the OAuth flow.
*/
// --- SDK Interface Methods ---
get redirectUrl(): string {
return this.callbackUrl;
}
/**
* Metadata about this client, sent during dynamic client registration (if supported by the server).
*/
get clientMetadata() {
get clientMetadata(): OAuthClientMetadata {
return {
redirect_uris: [this.redirectUrl],
token_endpoint_auth_method: 'none', // Public client
@ -58,222 +52,180 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
};
}
/**
* Clears all localStorage items associated with this specific server URL.
* @returns The number of items removed from storage.
*/
clearStorage(): number {
const prefix = `${this.storageKeyPrefix}_${this.serverUrlHash}`;
const keysToRemove: string[] = [];
// Find keys directly related to this server instance
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(prefix)) {
keysToRemove.push(key);
}
}
// Also check any persisted OAuth state keys
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(`${this.storageKeyPrefix}:state_`)) {
try {
const item = localStorage.getItem(key);
if (item) {
const state = JSON.parse(item) as Partial<StoredState>;
// If state belongs to this server, mark for removal
if (state.serverUrlHash === this.serverUrlHash) {
keysToRemove.push(key);
}
}
} catch (e) {
console.warn(`[${this.storageKeyPrefix}] Error parsing state key ${key}:`, e);
// Optionally remove malformed keys
// keysToRemove.push(key);
}
}
}
// Remove all identified keys
const uniqueKeysToRemove = [...new Set(keysToRemove)]; // Ensure uniqueness
uniqueKeysToRemove.forEach(key => localStorage.removeItem(key));
return uniqueKeysToRemove.length;
}
private hashString(str: string): string {
// Simple, non-cryptographic hash function suitable for creating unique keys
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer
}
// Use absolute value and convert to hex for a cleaner key
return Math.abs(hash).toString(16);
}
private getKey(key: string): string {
return `${this.storageKeyPrefix}_${this.serverUrlHash}_${key}`;
}
/**
* Retrieves client information (like client_id) from storage.
* @returns The stored client information, or undefined if not found/invalid.
*/
async clientInformation(): Promise<OAuthClientInformation | undefined> {
const key = this.getKey('client_info');
const data = localStorage.getItem(key);
if (!data) return undefined;
try {
// TODO: Add validation using a schema (e.g., Zod)
// TODO: Add validation using a schema
return JSON.parse(data) as OAuthClientInformation;
} catch (e) {
console.warn(`[${this.storageKeyPrefix}] Failed to parse client information from storage:`, e);
localStorage.removeItem(key); // Clean up invalid data
console.warn(`[${this.storageKeyPrefix}] Failed to parse client information:`, e);
localStorage.removeItem(key);
return undefined;
}
}
/**
* Saves client information to storage.
* @param clientInformation The client information to save.
*/
async saveClientInformation(clientInformation: OAuthClientInformation): Promise<void> {
// NOTE: The SDK's auth() function uses this if dynamic registration is needed.
// Ensure your OAuthClientInformationFull matches the expected structure if DCR is used.
async saveClientInformation(clientInformation: OAuthClientInformation /* | OAuthClientInformationFull */): Promise<void> {
const key = this.getKey('client_info');
// Cast needed if handling OAuthClientInformationFull specifically
localStorage.setItem(key, JSON.stringify(clientInformation));
}
/**
* Retrieves OAuth tokens (access token, refresh token) from storage.
* @returns The stored tokens, or undefined if not found/invalid.
*/
async tokens(): Promise<OAuthTokens | undefined> {
const key = this.getKey('tokens');
const data = localStorage.getItem(key);
if (!data) return undefined;
try {
// TODO: Add validation using a schema (e.g., Zod)
const tokens = JSON.parse(data) as OAuthTokens;
// Optional: Check token expiry here if 'expires_at' is stored
return tokens;
// TODO: Add validation
return JSON.parse(data) as OAuthTokens;
} catch (e) {
console.warn(`[${this.storageKeyPrefix}] Failed to parse tokens from storage:`, e);
localStorage.removeItem(key); // Clean up invalid data
console.warn(`[${this.storageKeyPrefix}] Failed to parse tokens:`, e);
localStorage.removeItem(key);
return undefined;
}
}
/**
* Saves OAuth tokens to storage.
* @param tokens The tokens to save.
*/
async saveTokens(tokens: OAuthTokens): Promise<void> {
const key = this.getKey('tokens');
// Optional: Calculate and store expiry timestamp:
// const storedTokens = { ...tokens, expires_at: Date.now() + (tokens.expires_in || 0) * 1000 };
localStorage.setItem(key, JSON.stringify(tokens));
// Clean up code verifier and last auth URL after successful token save
localStorage.removeItem(this.getKey('code_verifier'));
localStorage.removeItem(this.getKey('last_auth_url'));
}
/**
* Initiates the OAuth authorization flow by opening a popup window.
* Stores necessary state in localStorage for the callback handler.
* @param authorizationUrl The fully constructed authorization URL (including client_id, redirect_uri, scope, code_challenge, etc.).
* @param metadata The discovered OAuth metadata of the server.
* @param options Configuration for the popup window.
* @returns An object indicating success, whether the popup was potentially blocked, and the URL used.
*/
// @ts-ignore
async redirectToAuthorization(
authorizationUrl: URL,
metadata: OAuthMetadata,
options?: {
popupFeatures?: string;
},
): Promise<{ success: boolean; popupBlocked?: boolean; url: string }> {
// Generate a unique state parameter for this authorization request
const state = crypto.randomUUID(); // Use crypto.randomUUID for better randomness
const stateKey = `${this.storageKeyPrefix}:state_${state}`;
// Store context needed by the callback handler, associated with the state param
localStorage.setItem(
stateKey,
JSON.stringify({
authorizationUrl: authorizationUrl.origin, // Store origin for exchangeAuthorization
metadata,
serverUrlHash: this.serverUrlHash, // Link state back to this server instance
expiry: Date.now() + 1000 * 60 * 10, // State expires in 10 minutes
} as StoredState),
);
authorizationUrl.searchParams.set('state', state);
const authUrlString = authorizationUrl.toString();
const popupFeatures = options?.popupFeatures || 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes';
// Persist the exact auth URL in case the popup fails and manual navigation is needed
localStorage.setItem(this.getKey('auth_url'), authUrlString);
try {
// Attempt to open the authorization URL in a new window
const popup = window.open(authUrlString, `mcp_auth_${this.serverUrlHash}`, popupFeatures);
// Check if the popup window handle was obtained and if it's not immediately closed
if (!popup || popup.closed || typeof popup.closed === 'undefined') {
// This is the most common indicator of a popup blocker
console.warn(`[${this.storageKeyPrefix}] Popup likely blocked by browser.`);
return { success: false, popupBlocked: true, url: authUrlString };
}
// Additional check: try accessing a property that might throw if blocked cross-origin
// Note: This might not always work due to browser security policies evolution
try {
// Accessing location.href will likely throw a cross-origin error immediately
// if the popup is loading the auth server's domain, which is expected.
// What we *really* want to detect is if the popup *failed to open at all*.
// The initial check (!popup || popup.closed) is usually sufficient.
// Focusing the popup can be a sign it opened successfully.
popup.focus();
} catch (e) {
// If accessing the popup threw an error immediately, it might indicate a deeper issue,
// but often it's just a standard cross-origin security restriction.
// The initial check is more reliable for detecting simple blocking.
console.debug(`[${this.storageKeyPrefix}] Accessing popup properties caused potential cross-origin error (normal):`, e);
}
// If we reached here, the popup handle was obtained and wasn't immediately closed.
return { success: true, url: authUrlString };
} catch (e) {
// Catch any unexpected errors during window.open
console.error(`[${this.storageKeyPrefix}] Error opening popup window:`, e);
return { success: false, popupBlocked: true, url: authUrlString }; // Assume blocked on error
}
}
/**
* Saves the PKCE code verifier to storage.
* @param codeVerifier The code verifier string.
*/
async saveCodeVerifier(codeVerifier: string): Promise<void> {
const key = this.getKey('code_verifier');
localStorage.setItem(key, codeVerifier);
}
/**
* Retrieves the PKCE code verifier from storage.
* @returns The stored code verifier.
* @throws If the code verifier is not found in storage.
*/
async codeVerifier(): Promise<string> {
const key = this.getKey('code_verifier');
const verifier = localStorage.getItem(key);
if (!verifier) {
throw new Error(`[${this.storageKeyPrefix}] Code verifier not found in storage for key ${key}. Auth flow likely corrupted or timed out.`);
}
// Optionally remove the verifier after retrieving it, as it's single-use
// localStorage.removeItem(key);
// SDK's auth() retrieves this BEFORE exchanging code. Don't remove it here.
// It will be removed in saveTokens on success.
return verifier;
}
}
/**
* Redirects the user agent to the authorization URL, storing necessary state.
* This now adheres to the SDK's void return type expectation for the interface.
* @param authorizationUrl The fully constructed authorization URL from the SDK.
*/
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
// Generate a unique state parameter for this authorization request
const state = crypto.randomUUID();
const stateKey = `${this.storageKeyPrefix}:state_${state}`;
// Store context needed by the callback handler, associated with the state param
const stateData: StoredState = {
serverUrlHash: this.serverUrlHash,
expiry: Date.now() + 1000 * 60 * 10, // State expires in 10 minutes
// Store provider options needed to reconstruct on callback
providerOptions: {
serverUrl: this.serverUrl,
storageKeyPrefix: this.storageKeyPrefix,
clientName: this.clientName,
clientUri: this.clientUri,
callbackUrl: this.callbackUrl,
}
};
localStorage.setItem(stateKey, JSON.stringify(stateData));
// Add the state parameter to the URL
authorizationUrl.searchParams.set('state', state);
const authUrlString = authorizationUrl.toString();
// Persist the exact auth URL in case the popup fails and manual navigation is needed
localStorage.setItem(this.getKey('last_auth_url'), authUrlString);
// Attempt to open the popup
const popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes'; // Make configurable if needed
try {
const popup = window.open(authUrlString, `mcp_auth_${this.serverUrlHash}`, popupFeatures);
if (!popup || popup.closed || typeof popup.closed === 'undefined') {
console.warn(`[${this.storageKeyPrefix}] Popup likely blocked by browser. Manual navigation might be required using the stored URL.`);
// Cannot signal failure back via SDK auth() directly.
// useMcp will need to rely on timeout or manual trigger if stuck.
} else {
popup.focus();
console.info(`[${this.storageKeyPrefix}] Redirecting to authorization URL in popup.`);
}
} catch (e) {
console.error(`[${this.storageKeyPrefix}] Error opening popup window:`, e);
// Cannot signal failure back via SDK auth() directly.
}
// Regardless of popup success, the interface expects this method to initiate the redirect.
// If the popup failed, the user journey stops here until manual action or timeout.
}
// --- Helper Methods ---
/**
* Retrieves the last URL passed to `redirectToAuthorization`. Useful for manual fallback.
*/
getLastAttemptedAuthUrl(): string | null {
return localStorage.getItem(this.getKey('last_auth_url'));
}
clearStorage(): number {
const prefixPattern = `${this.storageKeyPrefix}_${this.serverUrlHash}_`;
const statePattern = `${this.storageKeyPrefix}:state_`;
const keysToRemove: string[] = [];
let count = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key) continue;
if (key.startsWith(prefixPattern)) {
keysToRemove.push(key);
} else if (key.startsWith(statePattern)) {
try {
const item = localStorage.getItem(key);
if (item) {
// Check if state belongs to this provider instance based on serverUrlHash
// We need to parse cautiously as the structure isn't guaranteed.
const state = JSON.parse(item) as Partial<StoredState>;
if (state.serverUrlHash === this.serverUrlHash) {
keysToRemove.push(key);
}
}
} catch (e) {
console.warn(`[${this.storageKeyPrefix}] Error parsing state key ${key} during clearStorage:`, e);
// Optionally remove malformed keys
// keysToRemove.push(key);
}
}
}
const uniqueKeysToRemove = [...new Set(keysToRemove)];
uniqueKeysToRemove.forEach(key => {
localStorage.removeItem(key);
count++;
});
return count;
}
private hashString(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16);
}
getKey(keySuffix: string): string {
return `${this.storageKeyPrefix}_${this.serverUrlHash}_${keySuffix}`;
}
}

View file

@ -1,180 +1,137 @@
import { exchangeAuthorization } from '@modelcontextprotocol/sdk/client/auth.js';
import { OAuthClientInformation, OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth.js';
import { StoredState } from './types.js';
// callback.ts
import { auth } from '@modelcontextprotocol/sdk/client/auth.js'
import { BrowserOAuthClientProvider } from './browser-provider.js' // Adjust path
import { StoredState } from './types.js' // Adjust path, ensure definition includes providerOptions
/**
* Handles the OAuth callback. This function should be invoked on your callback page
* (e.g., /oauth/callback) with the URL query parameters.
*
* It validates the state parameter, retrieves stored context from localStorage,
* exchanges the authorization code for tokens using the stored code verifier,
* saves the tokens, and notifies the opener window.
*
* @param query The URL query parameters (e.g., from `window.location.search`).
* @param options Configuration options.
* @param options.storageKeyPrefix The prefix used for localStorage keys (must match the one used by `BrowserOAuthClientProvider`). Defaults to "mcp:auth".
* @returns An object indicating success or failure with an error message.
* Handles the OAuth callback using the SDK's auth() function.
* Assumes it's running on the page specified as the callbackUrl.
*/
export async function onMcpAuthorization(
query: Record<string, string>,
{
storageKeyPrefix = 'mcp:auth',
}: {
storageKeyPrefix?: string;
} = {},
): Promise<{ success: boolean; error?: string }> {
const logPrefix = `[${storageKeyPrefix}-callback]`;
console.log(`${logPrefix} Handling OAuth callback with query:`, query);
export async function onMcpAuthorization() {
const queryParams = new URLSearchParams(window.location.search);
const code = queryParams.get('code');
const state = queryParams.get('state');
const error = queryParams.get('error');
const errorDescription = queryParams.get('error_description');
const logPrefix = '[mcp-callback]'; // Generic prefix, or derive from stored state later
console.log(`${logPrefix} Handling callback...`, { code, state, error, errorDescription });
let provider: BrowserOAuthClientProvider | null = null;
let storedStateData: StoredState | null = null;
const stateKey = state ? `mcp:auth:state_${state}` : null; // Reconstruct state key prefix assumption
try {
const code = query.code;
const state = query.state;
const error = query.error;
const errorDescription = query.error_description;
// --- Basic Error Handling ---
if (error) {
throw new Error(`OAuth error: ${error} - ${errorDescription || 'No description provided.'}`);
}
if (!code) {
throw new Error('Authorization code not found in callback query parameters.');
}
if (!state) {
throw new Error('State parameter not found in callback query parameters.');
if (!state || !stateKey) {
throw new Error('State parameter not found or invalid in callback query parameters.');
}
// Retrieve the stored state using the state parameter from the query
const stateKey = `${storageKeyPrefix}:state_${state}`;
// --- Retrieve Stored State & Provider Options ---
const storedStateJSON = localStorage.getItem(stateKey);
if (!storedStateJSON) {
throw new Error(`Invalid or expired state parameter "${state}". No matching state found in storage.`);
}
let storedState: StoredState;
try {
storedState = JSON.parse(storedStateJSON);
storedStateData = JSON.parse(storedStateJSON) as StoredState;
} catch (e) {
throw new Error('Failed to parse stored OAuth state.');
}
// Validate expiry
if (storedState.expiry < Date.now()) {
if (!storedStateData.expiry || storedStateData.expiry < Date.now()) {
localStorage.removeItem(stateKey); // Clean up expired state
throw new Error('OAuth state has expired. Please try initiating authentication again.');
}
const { authorizationUrl, serverUrlHash, metadata } = storedState;
console.log(`${logPrefix} Found valid state for server hash: ${serverUrlHash}`);
// Construct keys to retrieve client info and code verifier for this server instance
const clientInfoKey = `${storageKeyPrefix}_${serverUrlHash}_client_info`;
const codeVerifierKey = `${storageKeyPrefix}_${serverUrlHash}_code_verifier`;
const tokensKey = `${storageKeyPrefix}_${serverUrlHash}_tokens`;
const authUrlKey = `${storageKeyPrefix}_${serverUrlHash}_auth_url`; // Key for the potentially stored manual auth URL
const clientInfoStr = localStorage.getItem(clientInfoKey);
const codeVerifier = localStorage.getItem(codeVerifierKey);
if (!clientInfoStr) {
throw new Error(`Client information not found in storage (key: ${clientInfoKey}).`);
}
if (!codeVerifier) {
throw new Error(`Code verifier not found in storage (key: ${codeVerifierKey}). Auth flow may be incomplete or timed out.`);
// Ensure provider options are present
if (!storedStateData.providerOptions) {
throw new Error('Stored state is missing required provider options.');
}
const { serverUrl, ...providerOptions } = storedStateData.providerOptions;
let clientInfo: OAuthClientInformation;
try {
clientInfo = JSON.parse(clientInfoStr);
} catch (e) {
throw new Error('Failed to parse stored client information.');
}
console.log(`${logPrefix} Exchanging authorization code for token...`);
// --- Instantiate Provider ---
console.log(`${logPrefix} Re-instantiating provider for server: ${serverUrl}`);
provider = new BrowserOAuthClientProvider(serverUrl, providerOptions);
// Exchange the code for tokens
const tokens = await exchangeAuthorization(new URL(metadata.token_endpoint), {
// Use token_endpoint from metadata
metadata,
clientInformation: clientInfo,
authorizationCode: code,
codeVerifier,
// redirectUrl is typically required by the token endpoint for verification
// @ts-ignore
redirectUrl: new URL('/oauth/callback', window.location.origin).toString(), // TODO: Make this configurable or get from provider instance?
});
// --- Call SDK Auth Function ---
console.log(`${logPrefix} Calling SDK auth() to exchange code...`);
// The SDK auth() function will internally:
// 1. Use provider.clientInformation()
// 2. Use provider.codeVerifier()
// 3. Call exchangeAuthorization()
// 4. Use provider.saveTokens() on success
const authResult = await auth(provider, { serverUrl, authorizationCode: code });
console.log(`${logPrefix} Token exchange successful. Saving tokens...`);
if (authResult === 'AUTHORIZED') {
console.log(`${logPrefix} Authorization successful via SDK auth(). Notifying opener...`);
// --- Notify Opener and Close (Success) ---
if (window.opener && !window.opener.closed) {
window.opener.postMessage(
{ type: 'mcp_auth_callback', success: true },
window.location.origin,
);
window.close();
} else {
console.warn(`${logPrefix} No opener window detected. Redirecting to root.`);
window.location.href = '/'; // Or a configured post-auth destination
}
// Clean up state ONLY on success and after notifying opener
localStorage.removeItem(stateKey);
// Save the obtained tokens
localStorage.setItem(tokensKey, JSON.stringify(tokens));
// Clean up the persisted manual auth URL if it exists
localStorage.removeItem(authUrlKey);
// Remove the state key immediately after retrieval (it's single-use)
localStorage.removeItem(stateKey);
// Remove the code verifier after retrieving it
localStorage.removeItem(codeVerifierKey);
console.log(`${logPrefix} Tokens saved. Notifying opener window.`);
// Notify the original window (opener) that authentication succeeded
if (window.opener && !window.opener.closed) {
window.opener.postMessage(
{
type: 'mcp_auth_callback',
success: true,
// Optionally pass serverUrlHash if multiple hooks might be listening
serverUrlHash: serverUrlHash,
},
window.location.origin, // Target origin must match the opener's origin
);
// Close the popup window
window.close();
} else {
// If there's no opener, this might be a full page redirect flow.
// Redirect back to the app's main page or a specific post-auth page.
console.warn(`${logPrefix} No opener window detected. Assuming redirect flow. Redirecting to root.`);
window.location.href = '/'; // Adjust as needed
// This case shouldn't happen if `authorizationCode` is provided to `auth()`
console.warn(`${logPrefix} SDK auth() returned unexpected status: ${authResult}`);
throw new Error(`Unexpected result from authentication library: ${authResult}`);
}
return { success: true };
} catch (err) {
console.error(`${logPrefix} Error during OAuth callback handling:`, err);
const errorMessage = err instanceof Error ? err.message : String(err);
// Attempt to notify the opener window about the failure
// --- Notify Opener and Display Error (Failure) ---
if (window.opener && !window.opener.closed) {
window.opener.postMessage(
{
type: 'mcp_auth_callback',
success: false,
error: errorMessage,
// Optionally pass serverUrlHash
// serverUrlHash: storedState?.serverUrlHash // Might not be available if state parsing failed
},
{ type: 'mcp_auth_callback', success: false, error: errorMessage },
window.location.origin,
);
// Optionally close even on error, depending on UX preference
// window.close();
}
// Display error in the callback window itself for better debugging
// Display error in the callback window
try {
document.body.innerHTML = `
<div style="font-family: sans-serif; padding: 20px;">
<h1>Authentication Error</h1>
<p style="color: red; background-color: #ffebeb; border: 1px solid red; padding: 10px; border-radius: 4px;">
${errorMessage}
</p>
<p>You can close this window.</p>
<pre style="font-size: 0.8em; color: #555; margin-top: 20px; white-space: pre-wrap;">${
<div style="font-family: sans-serif; padding: 20px;">
<h1>Authentication Error</h1>
<p style="color: red; background-color: #ffebeb; border: 1px solid red; padding: 10px; border-radius: 4px;">
${errorMessage}
</p>
<p>You can close this window or <a href="#" onclick="window.close(); return false;">click here to close</a>.</p>
<pre style="font-size: 0.8em; color: #555; margin-top: 20px; white-space: pre-wrap;">${
err instanceof Error ? err.stack : ''
}</pre>
</div>
`;
</div>
`;
} catch (displayError) {
// Fallback if document.body is not available or writable
console.error(`${logPrefix} Could not display error in callback window:`, displayError);
}
return { success: false, error: errorMessage };
// Clean up potentially invalid state on error
if (stateKey) {
localStorage.removeItem(stateKey);
}
// Clean up potentially dangling verifier or last_auth_url if auth failed badly
// Note: saveTokens should clean these on success
if (provider) {
localStorage.removeItem(provider.getKey('code_verifier'));
localStorage.removeItem(provider.getKey('last_auth_url'));
}
}
}
}

View file

@ -4,9 +4,16 @@ import { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth.js';
* Internal type for storing OAuth state in localStorage during the popup flow.
* @internal
*/
export type StoredState = {
authorizationUrl: string;
metadata: OAuthMetadata;
serverUrlHash: string; // To associate state with a specific server URL
expiry: number; // Timestamp when the state expires
};
export interface StoredState {
expiry: number;
metadata?: OAuthMetadata; // Optional: might not be needed if auth() rediscovers
serverUrlHash: string;
// Add provider options needed on callback:
providerOptions: {
serverUrl: string;
storageKeyPrefix: string;
clientName: string;
clientUri: string;
callbackUrl: string;
};
}