diff --git a/client/src/hooks/MCP/useMCPSelect.ts b/client/src/hooks/MCP/useMCPSelect.ts index ec9dfe0bbb..e0118b8be1 100644 --- a/client/src/hooks/MCP/useMCPSelect.ts +++ b/client/src/hooks/MCP/useMCPSelect.ts @@ -28,7 +28,7 @@ export function useMCPSelect({ const mcps = ephemeralAgent?.mcp ?? []; if (mcps.length === 1 && mcps[0] === Constants.mcp_clear) { setMCPValuesRaw([]); - } else if (mcps.length > 0) { + } else if (mcps.length > 0 && configuredServers.size > 0) { // Strip out servers that are not available in the startup config const activeMcps = mcps.filter((mcp) => configuredServers.has(mcp)); setMCPValuesRaw(activeMcps); diff --git a/client/src/store/favorites.ts b/client/src/store/favorites.ts index b3744f52b0..9065f1ca4e 100644 --- a/client/src/store/favorites.ts +++ b/client/src/store/favorites.ts @@ -1,4 +1,4 @@ -import { createStorageAtom } from './jotai-utils'; +import { createTabIsolatedAtom } from './jotai-utils'; export type Favorite = { agentId?: string; @@ -16,4 +16,4 @@ export type FavoritesState = Favorite[]; /** * This atom stores the user's favorite models/agents */ -export const favoritesAtom = createStorageAtom('favorites', []); +export const favoritesAtom = createTabIsolatedAtom('favorites', []); diff --git a/client/src/store/jotai-utils.ts b/client/src/store/jotai-utils.ts index d3ca9d817c..5d2769d7e9 100644 --- a/client/src/store/jotai-utils.ts +++ b/client/src/store/jotai-utils.ts @@ -1,5 +1,6 @@ import { atom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; +import type { SyncStorage } from 'jotai/vanilla/utils/atomWithStorage'; /** * Create a simple atom with localStorage persistence @@ -42,6 +43,68 @@ export function createStorageAtomWithEffect( ); } +/** + * Create a SyncStorage adapter that reads/writes to localStorage but does NOT + * subscribe to browser `storage` events. This prevents cross-tab synchronization + * for atoms where each tab should maintain independent state. + * + * Use this for atoms that represent per-tab working state (e.g., favorites toggle, + * MCP server selections) rather than user preferences. + */ +export function createTabIsolatedStorage(): SyncStorage { + return { + getItem(key: string, initialValue: Value): Value { + if (typeof window === 'undefined') { + return initialValue; + } + try { + const stored = localStorage.getItem(key); + if (stored === null) { + return initialValue; + } + return JSON.parse(stored) as Value; + } catch { + return initialValue; + } + }, + setItem(key: string, newValue: Value): void { + if (typeof window === 'undefined') { + return; + } + try { + localStorage.setItem(key, JSON.stringify(newValue)); + } catch { + // quota exceeded or other write error — silently ignore + } + }, + removeItem(key: string): void { + if (typeof window === 'undefined') { + return; + } + try { + localStorage.removeItem(key); + } catch { + // silently ignore + } + }, + // subscribe intentionally omitted — prevents cross-tab sync via storage events + }; +} + +/** + * Create an atom with localStorage persistence that does NOT sync across tabs. + * Parallels `createStorageAtom` but uses tab-isolated storage. + * + * @param key - localStorage key + * @param defaultValue - default value if no saved value exists + * @returns Jotai atom with localStorage persistence, isolated per tab + */ +export function createTabIsolatedAtom(key: string, defaultValue: T) { + return atomWithStorage(key, defaultValue, createTabIsolatedStorage(), { + getOnInit: true, + }); +} + /** * Initialize a value from localStorage and optionally apply it * Useful for applying saved values on app startup (e.g., theme, fontSize) diff --git a/client/src/store/mcp.ts b/client/src/store/mcp.ts index e540b167e4..793e1cebd0 100644 --- a/client/src/store/mcp.ts +++ b/client/src/store/mcp.ts @@ -1,6 +1,14 @@ import { atom } from 'jotai'; import { atomFamily, atomWithStorage } from 'jotai/utils'; import { Constants, LocalStorageKeys } from 'librechat-data-provider'; +import { createTabIsolatedStorage } from './jotai-utils'; + +/** + * Tab-isolated storage for MCP values — prevents cross-tab sync so that + * each tab's MCP server selections are independent (especially for new chats + * which all share the same `LAST_MCP_new` localStorage key). + */ +const mcpTabIsolatedStorage = createTabIsolatedStorage(); /** * Creates a storage atom for MCP values per conversation @@ -10,7 +18,7 @@ export const mcpValuesAtomFamily = atomFamily((conversationId: string | null) => const key = conversationId ?? Constants.NEW_CONVO; const storageKey = `${LocalStorageKeys.LAST_MCP_}${key}`; - return atomWithStorage(storageKey, [], undefined, { getOnInit: true }); + return atomWithStorage(storageKey, [], mcpTabIsolatedStorage, { getOnInit: true }); }); /**