refactor: Cleanup
This commit is contained in:
parent
fc4c392dce
commit
d938994395
Binary file not shown.
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<string, string> | undefined;
|
||||
if (entry.headers) {
|
||||
try {
|
||||
const parsed = JSON.parse(entry.headers);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
headers = parsed as Record<string, string>;
|
||||
}
|
||||
} 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<string, MCPServerConfig> = {};
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import type { DatabaseMessageExtraMcpPrompt, MCPServerSettingsEntry } from '$lib/types';
|
||||
import { getFaviconUrl, getMcpServerLabel } from '$lib/utils/mcp';
|
||||
import { getFaviconUrl } from '$lib/utils';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
|
|
@ -51,8 +51,7 @@
|
|||
const server = serverSettingsMap.get(prompt.serverName);
|
||||
if (!server) return prompt.serverName;
|
||||
|
||||
const healthState = mcpStore.getHealthCheckState(server.id);
|
||||
return getMcpServerLabel(server, healthState);
|
||||
return mcpStore.getServerLabel(server);
|
||||
}
|
||||
|
||||
let contentParts = $derived.by((): ContentPart[] => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { cn } from '$lib/components/ui/utils';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { getFaviconUrl } from '$lib/utils/mcp';
|
||||
import { getFaviconUrl } from '$lib/utils';
|
||||
import { HealthCheckStatus } from '$lib/enums';
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { SearchableDropdownMenu } from '$lib/components/app';
|
||||
import McpLogo from '$lib/components/app/misc/McpLogo.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { getMcpServerLabel, getFaviconUrl } from '$lib/utils/mcp';
|
||||
import { getFaviconUrl } from '$lib/utils';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types';
|
||||
import { HealthCheckStatus } from '$lib/enums';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
);
|
||||
|
||||
function getServerLabel(server: MCPServerSettingsEntry): string {
|
||||
return getMcpServerLabel(server, mcpStore.getHealthCheckState(server.id));
|
||||
return mcpStore.getServerLabel(server);
|
||||
}
|
||||
|
||||
function handleDropdownOpen(open: boolean) {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import type { MCPConnectionLog } from '$lib/types';
|
||||
import { formatTime } from '$lib/utils/formatters';
|
||||
import { getMcpLogLevelIcon, getMcpLogLevelClass } from '$lib/utils/mcp';
|
||||
import { formatTime, getMcpLogLevelIcon, getMcpLogLevelClass } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
logs: MCPConnectionLog[];
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import * as Card from '$lib/components/ui/card';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
import type { MCPServerSettingsEntry, HealthCheckState } from '$lib/types';
|
||||
import { getMcpServerLabel } from '$lib/utils/mcp';
|
||||
import { HealthCheckStatus } from '$lib/enums';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { mcpClient } from '$lib/clients/mcp.client';
|
||||
|
|
@ -27,7 +26,7 @@
|
|||
let { server, faviconUrl, enabled, onToggle, onUpdate, onDelete }: Props = $props();
|
||||
|
||||
let healthState = $derived<HealthCheckState>(mcpStore.getHealthCheckState(server.id));
|
||||
let displayName = $derived(getMcpServerLabel(server, healthState));
|
||||
let displayName = $derived(mcpStore.getServerLabel(server));
|
||||
let isIdle = $derived(healthState.status === HealthCheckStatus.Idle);
|
||||
let isHealthChecking = $derived(healthState.status === HealthCheckStatus.Connecting);
|
||||
let isConnected = $derived(healthState.status === HealthCheckStatus.Success);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { Input } from '$lib/components/ui/input';
|
||||
import { KeyValuePairs } from '$lib/components/app';
|
||||
import type { KeyValuePair } from '$lib/types';
|
||||
import { parseHeadersToArray, serializeHeaders } from '$lib/utils/mcp';
|
||||
import { parseHeadersToArray, serializeHeaders } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { Plus, X } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { getFaviconUrl } from '$lib/utils/mcp';
|
||||
import { getFaviconUrl } from '$lib/utils';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { McpServerCard } from '$lib/components/app/mcp/McpServerCard';
|
||||
|
|
|
|||
|
|
@ -16,14 +16,13 @@
|
|||
import { rehypeEnhanceCodeBlocks } from '$lib/markdown/enhance-code-blocks';
|
||||
import { rehypeResolveAttachmentImages } from '$lib/markdown/resolve-attachment-images';
|
||||
import { remarkLiteralHtml } from '$lib/markdown/literal-html';
|
||||
import { copyCodeToClipboard, preprocessLaTeX } from '$lib/utils';
|
||||
import { copyCodeToClipboard, preprocessLaTeX, getImageErrorFallbackHtml } from '$lib/utils';
|
||||
import '$styles/katex-custom.scss';
|
||||
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
|
||||
import githubLightCss from 'highlight.js/styles/github.css?inline';
|
||||
import { mode } from 'mode-watcher';
|
||||
import CodePreviewDialog from './CodePreviewDialog.svelte';
|
||||
import type { DatabaseMessage } from '$lib/types/database';
|
||||
import { getImageErrorFallbackHtml } from '$lib/utils/image-error-fallback';
|
||||
|
||||
interface Props {
|
||||
message?: DatabaseMessage;
|
||||
|
|
|
|||
|
|
@ -7,3 +7,5 @@ export const DEFAULT_MCP_CONFIG = {
|
|||
requestTimeoutSeconds: 300, // 5 minutes for long-running tools
|
||||
connectionTimeoutMs: 10_000 // 10 seconds for connection establishment
|
||||
} as const;
|
||||
|
||||
export const MCP_SERVER_ID_PREFIX = 'LlamaCpp-WebUI-MCP-Server-';
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
import { browser } from '$app/environment';
|
||||
import type { AgenticFlowParams, AgenticFlowResult } from '$lib/clients';
|
||||
import type { AgenticSession } from '$lib/types/agentic';
|
||||
import { agenticClient } from '$lib/clients/agentic.client';
|
||||
|
||||
export type {
|
||||
AgenticFlowCallbacks,
|
||||
|
|
@ -56,8 +57,8 @@ class AgenticStore {
|
|||
*/
|
||||
private _sessions = $state<Map<string, AgenticSession>>(new Map());
|
||||
|
||||
/** Reference to the client (lazy loaded to avoid circular dependency) */
|
||||
private _client: typeof import('$lib/clients/agentic.client').agenticClient | null = null;
|
||||
/** Reference to the client */
|
||||
private _client = agenticClient;
|
||||
|
||||
private get client() {
|
||||
return this._client;
|
||||
|
|
@ -65,19 +66,18 @@ class AgenticStore {
|
|||
|
||||
/** Check if store is ready (client initialized) */
|
||||
get isReady(): boolean {
|
||||
return this._client !== null;
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
private _initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the store by wiring up to the client.
|
||||
* Must be called once after app startup.
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
init(): void {
|
||||
if (!browser) return;
|
||||
if (this._client) return; // Already initialized
|
||||
|
||||
const { agenticClient } = await import('$lib/clients/agentic.client');
|
||||
this._client = agenticClient;
|
||||
if (this._initialized) return; // Already initialized
|
||||
|
||||
agenticClient.setStoreCallbacks({
|
||||
setRunning: (convId, running) => this.updateSession(convId, { isRunning: running }),
|
||||
|
|
@ -87,6 +87,8 @@ class AgenticStore {
|
|||
setStreamingToolCall: (convId, tc) => this.updateSession(convId, { streamingToolCall: tc }),
|
||||
clearStreamingToolCall: (convId) => this.updateSession(convId, { streamingToolCall: null })
|
||||
});
|
||||
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,45 +19,19 @@
|
|||
* @see MCPService in services/mcp.ts for protocol operations
|
||||
*/
|
||||
|
||||
import { mcpClient } from '$lib/clients/mcp.client';
|
||||
import { mcpClient, buildMcpClientConfig } from '$lib/clients/mcp.client';
|
||||
import type {
|
||||
HealthCheckState,
|
||||
MCPServerSettingsEntry,
|
||||
McpServerUsageStats,
|
||||
MCPPromptInfo,
|
||||
GetPromptResult
|
||||
} from '$lib/types';
|
||||
import type { McpServerOverride } from '$lib/types/database';
|
||||
import { buildMcpClientConfig, parseMcpServerSettings, getMcpServerLabel } from '$lib/utils/mcp';
|
||||
import { parseMcpServerSettings } from '$lib/utils';
|
||||
import { HealthCheckStatus } from '$lib/enums';
|
||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
||||
|
||||
/**
|
||||
* Parses MCP server usage stats from settings.
|
||||
* @param rawStats - The raw stats to parse
|
||||
* @returns MCP server usage stats or empty object if invalid
|
||||
*/
|
||||
function parseMcpServerUsageStats(rawStats: unknown): McpServerUsageStats {
|
||||
if (!rawStats) return {};
|
||||
|
||||
if (typeof rawStats === 'string') {
|
||||
const trimmed = rawStats.trim();
|
||||
if (!trimmed) return {};
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
return parsed as McpServerUsageStats;
|
||||
}
|
||||
} catch {
|
||||
console.warn('[MCP] Failed to parse mcpServerUsageStats JSON, ignoring value');
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
class MCPStore {
|
||||
private _isInitializing = $state(false);
|
||||
private _error = $state<string | null>(null);
|
||||
|
|
@ -178,6 +152,21 @@ class MCPStore {
|
|||
return parseMcpServerSettings(config().mcpServers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a display label for an MCP server.
|
||||
* Automatically fetches health state from store.
|
||||
*/
|
||||
getServerLabel(server: MCPServerSettingsEntry): string {
|
||||
const healthState = this.getHealthCheckState(server.id);
|
||||
if (healthState?.status === HealthCheckStatus.Success) {
|
||||
return (
|
||||
healthState.serverInfo?.title || healthState.serverInfo?.name || server.name || server.url
|
||||
);
|
||||
}
|
||||
|
||||
return server.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any server is still loading (idle or connecting).
|
||||
*/
|
||||
|
|
@ -205,8 +194,8 @@ class MCPStore {
|
|||
|
||||
// Sort alphabetically by display label once all health checks are done
|
||||
return [...servers].sort((a, b) => {
|
||||
const labelA = getMcpServerLabel(a, this.getHealthCheckState(a.id));
|
||||
const labelB = getMcpServerLabel(b, this.getHealthCheckState(b.id));
|
||||
const labelA = this.getServerLabel(a);
|
||||
const labelB = this.getServerLabel(b);
|
||||
return labelA.localeCompare(labelB);
|
||||
});
|
||||
}
|
||||
|
|
@ -266,33 +255,24 @@ class MCPStore {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Server Usage Stats
|
||||
*
|
||||
* Gets enabled MCP servers for a conversation based on per-chat overrides.
|
||||
* Returns servers that are both globally enabled AND enabled for this chat.
|
||||
*/
|
||||
getEnabledServersForConversation(
|
||||
perChatOverrides?: McpServerOverride[]
|
||||
): MCPServerSettingsEntry[] {
|
||||
if (!perChatOverrides?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed usage stats for all servers
|
||||
*/
|
||||
getUsageStats(): McpServerUsageStats {
|
||||
return parseMcpServerUsageStats(config().mcpServerUsageStats);
|
||||
}
|
||||
const allServers = this.getServers();
|
||||
|
||||
/**
|
||||
* Get usage count for a specific server
|
||||
*/
|
||||
getServerUsageCount(serverId: string): number {
|
||||
const stats = this.getUsageStats();
|
||||
return stats[serverId] || 0;
|
||||
}
|
||||
return allServers.filter((server) => {
|
||||
if (!server.enabled) return false;
|
||||
const override = perChatOverrides.find((o) => o.serverId === server.id);
|
||||
|
||||
/**
|
||||
* Increment usage count for a server
|
||||
*/
|
||||
incrementServerUsage(serverId: string): void {
|
||||
const stats = this.getUsageStats();
|
||||
stats[serverId] = (stats[serverId] || 0) + 1;
|
||||
settingsStore.updateConfig('mcpServerUsageStats', JSON.stringify(stats));
|
||||
return override?.enabled ?? false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ export type {
|
|||
MCPServerConfig,
|
||||
MCPClientConfig,
|
||||
MCPServerSettingsEntry,
|
||||
McpServerUsageStats,
|
||||
MCPToolCall,
|
||||
OpenAIToolDefinition,
|
||||
ServerStatus,
|
||||
|
|
|
|||
|
|
@ -232,8 +232,6 @@ export interface ServerStatus {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
export type McpServerUsageStats = Record<string, number>;
|
||||
|
||||
export interface MCPServerConnectionConfig {
|
||||
name: string;
|
||||
server: MCPServerConfig;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Favicon utility functions for extracting favicons from URLs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets a favicon URL for a given URL using Google's favicon service.
|
||||
* Returns null if the URL is invalid.
|
||||
*
|
||||
* @param urlString - The URL to get the favicon for
|
||||
* @returns The favicon URL or null if invalid
|
||||
*/
|
||||
export function getFaviconUrl(urlString: string): string | null {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
const hostnameParts = url.hostname.split('.');
|
||||
const rootDomain = hostnameParts.length >= 2 ? hostnameParts.slice(-2).join('.') : url.hostname;
|
||||
|
||||
return `https://www.google.com/s2/favicons?domain=${rootDomain}&sz=32`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Header utilities for parsing and serializing HTTP headers.
|
||||
* Generic utilities not specific to MCP.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parses a JSON string of headers into an array of key-value pairs.
|
||||
* Returns empty array if the JSON is invalid or empty.
|
||||
*/
|
||||
export function parseHeadersToArray(headersJson: string): { key: string; value: string }[] {
|
||||
if (!headersJson?.trim()) return [];
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(headersJson);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
return Object.entries(parsed).map(([key, value]) => ({
|
||||
key,
|
||||
value: String(value)
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an array of header key-value pairs to a JSON string.
|
||||
* Filters out pairs with empty keys and returns empty string if no valid pairs.
|
||||
*/
|
||||
export function serializeHeaders(pairs: { key: string; value: string }[]): string {
|
||||
const validPairs = pairs.filter((p) => p.key.trim());
|
||||
|
||||
if (validPairs.length === 0) return '';
|
||||
|
||||
const obj: Record<string, string> = {};
|
||||
|
||||
for (const pair of validPairs) {
|
||||
obj[pair.key.trim()] = pair.value;
|
||||
}
|
||||
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
|
@ -63,7 +63,14 @@ export {
|
|||
} from './file-type';
|
||||
|
||||
// Formatting utilities
|
||||
export { formatFileSize, formatParameters, formatNumber } from './formatters';
|
||||
export {
|
||||
formatFileSize,
|
||||
formatParameters,
|
||||
formatNumber,
|
||||
formatJsonPretty,
|
||||
formatTime,
|
||||
formatPerformanceTime
|
||||
} from './formatters';
|
||||
|
||||
// IME utilities
|
||||
export { isIMEComposing } from './is-ime-composing';
|
||||
|
|
@ -95,7 +102,30 @@ export { getLanguageFromFilename } from './syntax-highlight-language';
|
|||
export { isTextFileByName, readFileAsText, isLikelyTextFile } from './text-files';
|
||||
|
||||
// Agentic utilities
|
||||
export { toAgenticMessages } from './agentic';
|
||||
export { toAgenticMessages, getAgenticConfig } from './agentic';
|
||||
|
||||
// Base64 utilities
|
||||
export { decodeBase64 } from './base64';
|
||||
|
||||
// Chat stream utilities
|
||||
export { mergeToolCallDeltas, extractModelName } from './chat-stream';
|
||||
|
||||
// Debounce utilities
|
||||
export { debounce } from './debounce';
|
||||
|
||||
// Image error fallback utilities
|
||||
export { getImageErrorFallbackHtml } from './image-error-fallback';
|
||||
|
||||
// MCP utilities
|
||||
export {
|
||||
detectMcpTransportFromUrl,
|
||||
parseMcpServerSettings,
|
||||
getMcpLogLevelIcon,
|
||||
getMcpLogLevelClass
|
||||
} from './mcp';
|
||||
|
||||
// Header utilities
|
||||
export { parseHeadersToArray, serializeHeaders } from './headers';
|
||||
|
||||
// Favicon utilities
|
||||
export { getFaviconUrl } from './favicon';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
import type {
|
||||
HealthCheckState,
|
||||
MCPClientConfig,
|
||||
MCPServerConfig,
|
||||
MCPServerSettingsEntry
|
||||
} from '$lib/types';
|
||||
import type { SettingsConfigType } from '$lib/types/settings';
|
||||
import type { McpServerOverride } from '$lib/types/database';
|
||||
import { MCPTransportType, MCPLogLevel, HealthCheckStatus } from '$lib/enums';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types';
|
||||
import { MCPTransportType, MCPLogLevel } from '$lib/enums';
|
||||
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
||||
import { Info, AlertTriangle, XCircle } from '@lucide/svelte';
|
||||
import type { Component } from 'svelte';
|
||||
|
|
@ -23,90 +16,6 @@ export function detectMcpTransportFromUrl(url: string): MCPTransportType {
|
|||
: MCPTransportType.StreamableHttp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a valid MCP server ID from user input.
|
||||
* Returns the trimmed ID if valid, otherwise generates 'server-{index+1}'.
|
||||
*/
|
||||
export function generateMcpServerId(id: unknown, index: number): string {
|
||||
if (typeof id === 'string' && id.trim()) {
|
||||
return id.trim();
|
||||
}
|
||||
|
||||
return `server-${index + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a display label for an MCP server based on health state.
|
||||
*/
|
||||
export function getMcpServerLabel(
|
||||
server: MCPServerSettingsEntry,
|
||||
healthState?: HealthCheckState
|
||||
): string {
|
||||
if (healthState?.status === HealthCheckStatus.Success) {
|
||||
return (
|
||||
healthState.serverInfo?.title || healthState.serverInfo?.name || server.name || server.url
|
||||
);
|
||||
}
|
||||
|
||||
return server.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a favicon URL for an MCP server using Google's favicon service.
|
||||
* Returns null if the URL is invalid.
|
||||
*/
|
||||
export function getFaviconUrl(serverUrl: string): string | null {
|
||||
try {
|
||||
const parsedUrl = new URL(serverUrl);
|
||||
const hostnameParts = parsedUrl.hostname.split('.');
|
||||
const rootDomain =
|
||||
hostnameParts.length >= 2 ? hostnameParts.slice(-2).join('.') : parsedUrl.hostname;
|
||||
return `https://www.google.com/s2/favicons?domain=${rootDomain}&sz=32`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON string of headers into an array of key-value pairs.
|
||||
* Returns empty array if the JSON is invalid or empty.
|
||||
*/
|
||||
export function parseHeadersToArray(headersJson: string): { key: string; value: string }[] {
|
||||
if (!headersJson?.trim()) return [];
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(headersJson);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
return Object.entries(parsed).map(([key, value]) => ({
|
||||
key,
|
||||
value: String(value)
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an array of header key-value pairs to a JSON string.
|
||||
* Filters out pairs with empty keys and returns empty string if no valid pairs.
|
||||
*/
|
||||
export function serializeHeaders(pairs: { key: string; value: string }[]): string {
|
||||
const validPairs = pairs.filter((p) => p.key.trim());
|
||||
|
||||
if (validPairs.length === 0) return '';
|
||||
|
||||
const obj: Record<string, string> = {};
|
||||
|
||||
for (const pair of validPairs) {
|
||||
obj[pair.key.trim()] = pair.value;
|
||||
}
|
||||
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
@ -136,9 +45,13 @@ export function parseMcpServerSettings(rawServers: unknown): MCPServerSettingsEn
|
|||
return parsed.map((entry, index) => {
|
||||
const url = typeof entry?.url === 'string' ? entry.url.trim() : '';
|
||||
const headers = typeof entry?.headers === 'string' ? entry.headers.trim() : undefined;
|
||||
const id =
|
||||
typeof (entry as { id?: unknown })?.id === 'string' && (entry as { id?: string }).id?.trim()
|
||||
? (entry as { id: string }).id.trim()
|
||||
: `server-${index + 1}`;
|
||||
|
||||
return {
|
||||
id: generateMcpServerId((entry as { id?: unknown })?.id, index),
|
||||
id,
|
||||
enabled: Boolean((entry as { enabled?: unknown })?.enabled),
|
||||
url,
|
||||
requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
|
||||
|
|
@ -147,122 +60,6 @@ export function parseMcpServerSettings(rawServers: unknown): MCPServerSettingsEn
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, string> | undefined;
|
||||
if (entry.headers) {
|
||||
try {
|
||||
const parsed = JSON.parse(entry.headers);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
headers = parsed as Record<string, string>;
|
||||
}
|
||||
} 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.
|
||||
*/
|
||||
export function checkServerEnabled(
|
||||
server: MCPServerSettingsEntry,
|
||||
perChatOverrides?: McpServerOverride[]
|
||||
): boolean {
|
||||
// Server must be available in settings first
|
||||
if (!server.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Then check if it's enabled for this chat via override
|
||||
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<string, MCPServerConfig> = {};
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets enabled MCP servers for a conversation based on per-chat overrides.
|
||||
* Returns servers that are both globally enabled AND enabled for this chat.
|
||||
* @param config - Global settings configuration
|
||||
* @param perChatOverrides - Per-chat server overrides
|
||||
* @returns Array of enabled server settings entries
|
||||
*/
|
||||
export function getEnabledServersForConversation(
|
||||
config: SettingsConfigType,
|
||||
perChatOverrides?: McpServerOverride[]
|
||||
): MCPServerSettingsEntry[] {
|
||||
if (!perChatOverrides?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allServers = parseMcpServerSettings(config.mcpServers);
|
||||
return allServers.filter((server) => checkServerEnabled(server, perChatOverrides));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate icon component for a log level
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in New Issue