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 @@