diff --git a/package.json b/package.json index 5ba0f68..1bef2a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mcp-remote", - "version": "0.0.9", + "version": "0.0.10-1", "type": "module", "bin": { "mcp-remote": "dist/cli/proxy.js" diff --git a/src/cli/shared.ts b/src/cli/shared.ts index a4101aa..b146abd 100644 --- a/src/cli/shared.ts +++ b/src/cli/shared.ts @@ -44,7 +44,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { } get redirectUrl(): string { - return `http://localhost:${this.options.callbackPort}${this.callbackPath}` + return `http://127.0.0.1:${this.options.callbackPort}${this.callbackPath}` } get clientMetadata() { @@ -229,7 +229,7 @@ export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) { }) const server = app.listen(options.port, () => { - console.error(`OAuth callback server running at http://localhost:${options.port}`) + console.error(`OAuth callback server running at http://127.0.0.1:${options.port}`) }) /** @@ -299,7 +299,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, } const url = new URL(serverUrl) - const isLocalhost = url.hostname === 'localhost' && url.protocol === 'http:' + const isLocalhost = (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && url.protocol === 'http:' if (!(url.protocol == 'https:' || isLocalhost)) { console.error(usage) diff --git a/src/react/index.ts b/src/react/index.ts index 40948c2..2d95e6c 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -70,6 +70,10 @@ export type UseMcpResult = { * @returns Auth URL that can be used to manually open a new window */ authenticate: () => Promise + /** + * Clear all localStorage items for this server + */ + clearStorage: () => void } type StoredState = { @@ -120,6 +124,44 @@ class BrowserOAuthClientProvider { } } + /** + * Clears all storage items related to this server + * @returns The number of items cleared + */ + clearStorage(): number { + const prefix = `${this.storageKeyPrefix}_${this.serverUrlHash}`; + const keysToRemove = []; + + // Find all keys that match the prefix + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(prefix)) { + keysToRemove.push(key); + } + } + + // Also check for any state keys + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(`${this.storageKeyPrefix}:state_`)) { + // Load state to check if it's for this server + try { + const state = JSON.parse(localStorage.getItem(key) || '{}'); + if (state.serverUrlHash === this.serverUrlHash) { + keysToRemove.push(key); + } + } catch (e) { + // Ignore JSON parse errors + } + } + } + + // Remove all matching keys + keysToRemove.forEach(key => localStorage.removeItem(key)); + + return keysToRemove.length; + } + private hashString(str: string): string { // Simple hash function for browser environments let hash = 0 @@ -722,6 +764,25 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { } }, [disconnect]) + // Clear all localStorage items for this server + const clearStorage = useCallback(() => { + if (!authProviderRef.current) { + addLog('warn', 'Cannot clear storage: auth provider not initialized'); + return; + } + + // Use the provider's method to clear storage + const clearedCount = authProviderRef.current.clearStorage(); + + // Clear auth-related state in the hook + authUrlRef.current = undefined; + setAuthUrl(undefined); + metadataRef.current = undefined; + codeVerifierRef.current = undefined; + + addLog('info', `Cleared ${clearedCount} storage items for server`); + }, [addLog]); + return { state, tools, @@ -732,6 +793,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { retry, disconnect, authenticate, + clearStorage, } }