diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz
index 0d0d81277a..8e07d0117e 100644
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
diff --git a/tools/server/webui/src/lib/clients/agentic.client.ts b/tools/server/webui/src/lib/clients/agentic.client.ts
index 198169fab0..439fae7f2d 100644
--- a/tools/server/webui/src/lib/clients/agentic.client.ts
+++ b/tools/server/webui/src/lib/clients/agentic.client.ts
@@ -26,8 +26,7 @@
import { mcpClient } from '$lib/clients';
import { ChatService } from '$lib/services';
import { config } from '$lib/stores/settings.svelte';
-import { getAgenticConfig } from '$lib/utils/agentic';
-import { toAgenticMessages } from '$lib/utils';
+import { getAgenticConfig, toAgenticMessages } from '$lib/utils';
import type { AgenticMessage, AgenticToolCallList } from '$lib/types/agentic';
import type {
ApiChatCompletionToolCall,
diff --git a/tools/server/webui/src/lib/clients/chat.client.ts b/tools/server/webui/src/lib/clients/chat.client.ts
index 5689a8a438..12e11bea10 100644
--- a/tools/server/webui/src/lib/clients/chat.client.ts
+++ b/tools/server/webui/src/lib/clients/chat.client.ts
@@ -12,10 +12,10 @@ import {
normalizeModelName,
filterByLeafNodeId,
findDescendantMessages,
- findLeafNode
+ findLeafNode,
+ getAgenticConfig
} from '$lib/utils';
import { DEFAULT_CONTEXT } from '$lib/constants/default-context';
-import { getAgenticConfig } from '$lib/utils/agentic';
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants/ui';
import { REASONING_TAGS } from '$lib/constants/agentic';
import type { ChatMessageTimings, ChatMessagePromptProgress } from '$lib/types/chat';
diff --git a/tools/server/webui/src/lib/clients/conversations.client.ts b/tools/server/webui/src/lib/clients/conversations.client.ts
index 2f0466160b..b28cab36ee 100644
--- a/tools/server/webui/src/lib/clients/conversations.client.ts
+++ b/tools/server/webui/src/lib/clients/conversations.client.ts
@@ -25,8 +25,8 @@ import { toast } from 'svelte-sonner';
import { DatabaseService } from '$lib/services/database.service';
import { config } from '$lib/stores/settings.svelte';
import { filterByLeafNodeId, findLeafNode } from '$lib/utils';
-import { getEnabledServersForConversation } from '$lib/utils/mcp';
import { mcpClient } from '$lib/clients/mcp.client';
+import { mcpStore } from '$lib/stores/mcp.svelte';
import type { McpServerOverride } from '$lib/types/database';
import { MessageRole } from '$lib/enums';
@@ -182,7 +182,7 @@ export class ConversationsClient {
return;
}
- const enabledServers = getEnabledServersForConversation(config(), mcpServerOverrides);
+ const enabledServers = mcpStore.getEnabledServersForConversation(mcpServerOverrides);
if (enabledServers.length === 0) {
return;
diff --git a/tools/server/webui/src/lib/clients/mcp.client.ts b/tools/server/webui/src/lib/clients/mcp.client.ts
index 6d685485fd..e2e50d57ee 100644
--- a/tools/server/webui/src/lib/clients/mcp.client.ts
+++ b/tools/server/webui/src/lib/clients/mcp.client.ts
@@ -49,9 +49,158 @@ import type { ListChangedHandlers } from '@modelcontextprotocol/sdk/types.js';
import { MCPConnectionPhase, MCPLogLevel, HealthCheckStatus } from '$lib/enums';
import type { McpServerOverride } from '$lib/types/database';
import { MCPError } from '$lib/errors';
-import { buildMcpClientConfig, detectMcpTransportFromUrl } from '$lib/utils/mcp';
+import { detectMcpTransportFromUrl } from '$lib/utils';
import { config } from '$lib/stores/settings.svelte';
-import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
+import { DEFAULT_MCP_CONFIG, MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp';
+import type { MCPServerConfig, MCPServerSettingsEntry } from '$lib/types';
+import type { SettingsConfigType } from '$lib/types/settings';
+
+/**
+ * Generates a valid MCP server ID from user input.
+ * Returns the trimmed ID if valid, otherwise generates 'server-{index+1}'.
+ */
+function generateMcpServerId(id: unknown, index: number): string {
+ if (typeof id === 'string' && id.trim()) {
+ return id.trim();
+ }
+
+ return `${MCP_SERVER_ID_PREFIX}${index + 1}`;
+}
+
+/**
+ * Parses MCP server settings from a JSON string or array.
+ * requestTimeoutSeconds is not user-configurable in the UI, so we always use the default value.
+ * @param rawServers - The raw servers to parse
+ * @returns An empty array if the input is invalid.
+ */
+function parseMcpServerSettings(rawServers: unknown): MCPServerSettingsEntry[] {
+ if (!rawServers) return [];
+
+ let parsed: unknown;
+ if (typeof rawServers === 'string') {
+ const trimmed = rawServers.trim();
+ if (!trimmed) return [];
+
+ try {
+ parsed = JSON.parse(trimmed);
+ } catch (error) {
+ console.warn('[MCP] Failed to parse mcpServers JSON, ignoring value:', error);
+ return [];
+ }
+ } else {
+ parsed = rawServers;
+ }
+
+ if (!Array.isArray(parsed)) return [];
+
+ return parsed.map((entry, index) => {
+ const url = typeof entry?.url === 'string' ? entry.url.trim() : '';
+ const headers = typeof entry?.headers === 'string' ? entry.headers.trim() : undefined;
+
+ return {
+ id: generateMcpServerId((entry as { id?: unknown })?.id, index),
+ enabled: Boolean((entry as { enabled?: unknown })?.enabled),
+ url,
+ requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
+ headers: headers || undefined
+ } satisfies MCPServerSettingsEntry;
+ });
+}
+
+/**
+ * Builds an MCP server configuration from a server settings entry.
+ * @param entry - The server settings entry to build the configuration from
+ * @param connectionTimeoutMs - The connection timeout in milliseconds
+ * @returns The built server configuration, or undefined if the entry is invalid
+ */
+function buildServerConfig(
+ entry: MCPServerSettingsEntry,
+ connectionTimeoutMs = DEFAULT_MCP_CONFIG.connectionTimeoutMs
+): MCPServerConfig | undefined {
+ if (!entry?.url) {
+ return undefined;
+ }
+
+ let headers: Record | undefined;
+ if (entry.headers) {
+ try {
+ const parsed = JSON.parse(entry.headers);
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
+ headers = parsed as Record;
+ }
+ } catch {
+ console.warn('[MCP] Failed to parse custom headers JSON, ignoring:', entry.headers);
+ }
+ }
+
+ return {
+ url: entry.url,
+ transport: detectMcpTransportFromUrl(entry.url),
+ handshakeTimeoutMs: connectionTimeoutMs,
+ requestTimeoutMs: Math.round(entry.requestTimeoutSeconds * 1000),
+ headers
+ };
+}
+
+/**
+ * Checks if a server is enabled for the current chat.
+ * Server must be available (server.enabled) AND have a per-chat override enabling it.
+ * Pure helper function - no side effects.
+ */
+function 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;
+}
+
+/**
+ * Builds MCP client configuration from settings.
+ * Returns undefined if no valid servers are configured.
+ * @param config - Global settings configuration
+ * @param perChatOverrides - Optional per-chat server overrides
+ */
+export function buildMcpClientConfig(
+ config: SettingsConfigType,
+ perChatOverrides?: McpServerOverride[]
+): MCPClientConfig | undefined {
+ const rawServers = parseMcpServerSettings(config.mcpServers);
+
+ if (!rawServers.length) {
+ return undefined;
+ }
+
+ const servers: Record = {};
+ for (const [index, entry] of rawServers.entries()) {
+ if (!checkServerEnabled(entry, perChatOverrides)) continue;
+
+ const normalized = buildServerConfig(entry);
+ if (normalized) {
+ servers[generateMcpServerId(entry.id, index)] = normalized;
+ }
+ }
+
+ if (Object.keys(servers).length === 0) {
+ return undefined;
+ }
+
+ return {
+ protocolVersion: DEFAULT_MCP_CONFIG.protocolVersion,
+ capabilities: DEFAULT_MCP_CONFIG.capabilities,
+ clientInfo: DEFAULT_MCP_CONFIG.clientInfo,
+ requestTimeoutMs: Math.round(DEFAULT_MCP_CONFIG.requestTimeoutSeconds * 1000),
+ servers
+ };
+}
/**
* Build capabilities info from server and client capabilities
@@ -613,8 +762,6 @@ export class MCPClient {
throw new MCPError(`Server "${serverName}" is not connected`, -32000);
}
- mcpStore.incrementServerUsage(serverName);
-
const args = this.parseToolArguments(toolCall.function.arguments);
return MCPService.callTool(connection, { name: toolName, arguments: args }, signal);
diff --git a/tools/server/webui/src/lib/clients/openai-sse.ts b/tools/server/webui/src/lib/clients/openai-sse.ts
index 57f33931d1..4ec5bd9bd5 100644
--- a/tools/server/webui/src/lib/clients/openai-sse.ts
+++ b/tools/server/webui/src/lib/clients/openai-sse.ts
@@ -4,7 +4,7 @@ import type {
ApiChatCompletionStreamChunk
} from '$lib/types/api';
import type { ChatMessagePromptProgress, ChatMessageTimings } from '$lib/types/chat';
-import { mergeToolCallDeltas, extractModelName } from '$lib/utils/chat-stream';
+import { mergeToolCallDeltas, extractModelName } from '$lib/utils';
import type { AgenticChatCompletionRequest } from '$lib/types/agentic';
export type OpenAISseCallbacks = {
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormPromptPicker.svelte b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormPromptPicker.svelte
index 172634bab5..f178038c80 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormPromptPicker.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormPromptPicker.svelte
@@ -2,11 +2,10 @@
import { mcpClient } from '$lib/clients/mcp.client';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
- import { getFaviconUrl, getMcpServerLabel } from '$lib/utils/mcp';
+ import { getFaviconUrl, debounce } from '$lib/utils';
import type { MCPPromptInfo, GetPromptResult, MCPServerSettingsEntry } from '$lib/types';
import { fly } from 'svelte/transition';
import { SvelteMap } from 'svelte/reactivity';
- import { debounce } from '$lib/utils/debounce';
import { SearchInput } from '$lib/components/app';
interface Props {
@@ -65,11 +64,9 @@
function getServerLabel(serverId: string): string {
const server = serverSettingsMap.get(serverId);
-
if (!server) return serverId;
- const healthState = mcpStore.getHealthCheckState(serverId);
- return getMcpServerLabel(server, healthState);
+ return mcpStore.getServerLabel(server);
}
$effect(() => {
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte
index fdd4d0c6c2..5f7882d9bd 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte
@@ -15,7 +15,7 @@
import { Wrench, Loader2, AlertTriangle, Brain } from '@lucide/svelte';
import { AgenticSectionType } from '$lib/enums';
import { AGENTIC_TAGS, AGENTIC_REGEX, REASONING_TAGS } from '$lib/constants/agentic';
- import { formatJsonPretty } from '$lib/utils/formatters';
+ import { formatJsonPretty } from '$lib/utils';
import type { DatabaseMessage } from '$lib/types/database';
interface Props {
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte
index db44bedbe4..2f0b3a1b01 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte
@@ -4,6 +4,7 @@
import { Switch } from '$lib/components/ui/switch';
import { ChatFormInputArea, DialogConfirmation } from '$lib/components/app';
import { chatStore } from '$lib/stores/chat.svelte';
+ import { processFilesToChatUploaded } from '$lib/utils/browser-only';
interface Props {
editedContent: string;
@@ -104,7 +105,6 @@
async function handleFilesAdd(files: File[]) {
if (!onEditedUploadedFilesChange) return;
- const { processFilesToChatUploaded } = await import('$lib/utils/browser-only');
const processed = await processFilesToChatUploaded(files);
onEditedUploadedFilesChange([...editedUploadedFiles, processed].flat());
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte
index b4b1042257..b31a5ed641 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte
@@ -4,7 +4,7 @@
import * as Tooltip from '$lib/components/ui/tooltip';
import { ChatMessageStatsView } from '$lib/enums';
import type { ChatMessageAgenticTimings } from '$lib/types/chat';
- import { formatPerformanceTime } from '$lib/utils/formatters';
+ import { formatPerformanceTime } from '$lib/utils';
interface Props {
predictedTokens?: number;
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte b/tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte
index aa0c27f6d3..970394baa4 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte
@@ -9,7 +9,7 @@
import Input from '$lib/components/ui/input/input.svelte';
import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
- import { getPreviewText } from '$lib/utils/text';
+ import { getPreviewText } from '$lib/utils';
import ChatSidebarActions from './ChatSidebarActions.svelte';
const sidebar = Sidebar.useSidebar();
diff --git a/tools/server/webui/src/lib/components/app/chat/McpPromptContent.svelte b/tools/server/webui/src/lib/components/app/chat/McpPromptContent.svelte
index 6c51c610dd..79bdf5a043 100644
--- a/tools/server/webui/src/lib/components/app/chat/McpPromptContent.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/McpPromptContent.svelte
@@ -1,7 +1,7 @@