Repository: ggml-org/llama.cpp
Pull request: WebUI: Persist the on/off state of the MCP servers for new conversations

*Make sure to read the [contributing guidelines](https://github.com/ggml-org/llama.cpp/blob/master/CONTRIBUTING.md) before submitting a PR*

IndexedDB already stores the MCP server on/off state for existing
conversations, but new conversations always start with everything off.

This PR adds localStorage persistence so new conversations inherit the
previously set on/off state. It also simplifies the enabled checks by
removing dead code that was unreachable in practice.

Close #20736

Changed files:
filename: tools/server/webui/src/lib/constants/localstorage-keys.ts
patch: @@ -1,3 +1,4 @@
 export const CONFIG_LOCALSTORAGE_KEY = 'LlamaCppWebui.config';
 export const USER_OVERRIDES_LOCALSTORAGE_KEY = 'LlamaCppWebui.userOverrides';
 export const FAVOURITE_MODELS_LOCALSTORAGE_KEY = 'LlamaCppWebui.favouriteModels';
+export const MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY = 'LlamaCppWebui.mcpDefaultEnabled';

filename: tools/server/webui/src/lib/stores/conversations.svelte.ts
patch: @@ -36,7 +36,8 @@ import {
 	ISO_TIME_SEPARATOR,
 	ISO_TIME_SEPARATOR_REPLACEMENT,
 	NON_ALPHANUMERIC_REGEX,
-	MULTIPLE_UNDERSCORE_REGEX
+	MULTIPLE_UNDERSCORE_REGEX,
+	MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY
 } from '$lib/constants';
 
 class ConversationsStore {
@@ -61,7 +62,37 @@ class ConversationsStore {
 	isInitialized = $state(false);
 
 	/** Pending MCP server overrides for new conversations (before first message) */
-	pendingMcpServerOverrides = $state<McpServerOverride[]>([]);
+	pendingMcpServerOverrides = $state<McpServerOverride[]>(ConversationsStore.loadMcpDefaults());
+
+	/** Load MCP default overrides from localStorage */
+	private static loadMcpDefaults(): McpServerOverride[] {
+		if (typeof globalThis.localStorage === 'undefined') return [];
+		try {
+			const raw = localStorage.getItem(MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY);
+			if (!raw) return [];
+			const parsed = JSON.parse(raw);
+			if (!Array.isArray(parsed)) return [];
+			return parsed.filter(
+				(o: unknown) => typeof o === 'object' && o !== null && 'serverId' in o && 'enabled' in o
+			) as McpServerOverride[];
+		} catch {
+			return [];
+		}
+	}
+
+	/** Persist MCP default overrides to localStorage */
+	private saveMcpDefaults(): void {
+		if (typeof globalThis.localStorage === 'undefined') return;
+		const plain = this.pendingMcpServerOverrides.map((o) => ({
+			serverId: o.serverId,
+			enabled: o.enabled
+		}));
+		if (plain.length > 0) {
+			localStorage.setItem(MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY, JSON.stringify(plain));
+		} else {
+			localStorage.removeItem(MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY);
+		}
+	}
 
 	/** Callback for title update confirmation dialog */
 	titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
@@ -261,6 +292,8 @@ class ConversationsStore {
 	clearActiveConversation(): void {
 		this.activeConversation = null;
 		this.activeMessages = [];
+		// reload MCP defaults so new chats inherit persisted state
+		this.pendingMcpServerOverrides = ConversationsStore.loadMcpDefaults();
 	}
 
 	/**
@@ -597,6 +630,7 @@ class ConversationsStore {
 				this.pendingMcpServerOverrides = [...this.pendingMcpServerOverrides, { serverId, enabled }];
 			}
 		}
+		this.saveMcpDefaults();
 	}
 
 	/**
@@ -621,6 +655,7 @@ class ConversationsStore {
 	 */
 	clearPendingMcpServerOverrides(): void {
 		this.pendingMcpServerOverrides = [];
+		this.saveMcpDefaults();
 	}
 
 	/**

filename: tools/server/webui/src/lib/stores/mcp.svelte.ts
patch: @@ -208,23 +208,16 @@ class MCPStore {
 	}
 
 	/**
-	 * Checks if a server is enabled, considering per-chat overrides.
+	 * Checks if a server is enabled for a given chat.
+	 * Only per-chat overrides (persisted in localStorage for new chats,
+	 * or in IndexedDB for existing conversations) control enabled state.
 	 */
 	#checkServerEnabled(
 		server: MCPServerSettingsEntry,
 		perChatOverrides?: McpServerOverride[]
 	): boolean {
-		if (!server.enabled) {
-			return false;
-		}
-
-		if (perChatOverrides) {
-			const override = perChatOverrides.find((o) => o.serverId === server.id);
-
-			return override?.enabled ?? false;
-		}
-
-		return false;
+		const override = perChatOverrides?.find((o) => o.serverId === server.id);
+		return override?.enabled ?? false;
 	}
 
 	/**
@@ -570,18 +563,8 @@ class MCPStore {
 	getEnabledServersForConversation(
 		perChatOverrides?: McpServerOverride[]
 	): MCPServerSettingsEntry[] {
-		if (!perChatOverrides?.length) {
-			return [];
-		}
-
 		return this.getServers().filter((server) => {
-			if (!server.enabled) {
-				return false;
-			}
-
-			const override = perChatOverrides.find((o) => o.serverId === server.id);
-
-			return override?.enabled ?? false;
+			return this.#checkServerEnabled(server, perChatOverrides);
 		});
 	}