From e55ee82f07e561c76ace26b80783f65a7bde9b70 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 9 Feb 2026 13:49:13 +0100 Subject: [PATCH] refactor: Cleanup --- .../ChatAttachmentMcpResourceStored.svelte | 36 +--- .../ChatFormActions/ChatFormActions.svelte | 13 +- .../ChatFormPromptPicker.svelte | 8 +- .../ChatMessageAgenticContent.svelte | 4 +- .../dialogs/DialogMcpResourcePreview.svelte | 46 ++--- .../app/dialogs/DialogMcpResources.svelte | 24 +-- .../McpResourceBrowser.svelte | 2 +- .../McpResourceBrowserHeader.svelte | 2 +- .../McpResourceBrowserServerItem.svelte | 5 +- .../mcp-resource-browser.ts | 49 +---- .../app/mcp/McpResourcePreview.svelte | 52 ++--- .../McpServerCard/McpServerCardHeader.svelte | 22 +-- .../webui/src/lib/constants/agentic-ui.ts | 1 - .../server/webui/src/lib/constants/agentic.ts | 4 + .../webui/src/lib/constants/mcp-resource.ts | 2 +- tools/server/webui/src/lib/constants/mcp.ts | 17 ++ tools/server/webui/src/lib/enums/files.ts | 1 + .../webui/src/lib/stores/agentic.svelte.ts | 10 +- tools/server/webui/src/lib/types/common.d.ts | 2 + tools/server/webui/src/lib/types/mcp.d.ts | 17 +- tools/server/webui/src/lib/utils/index.ts | 11 +- tools/server/webui/src/lib/utils/mcp.ts | 181 +++++++++++++++++- 22 files changed, 285 insertions(+), 224 deletions(-) delete mode 100644 tools/server/webui/src/lib/constants/agentic-ui.ts diff --git a/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentMcpResourceStored.svelte b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentMcpResourceStored.svelte index 266c5ccd20..e8e28b8072 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentMcpResourceStored.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentMcpResourceStored.svelte @@ -1,14 +1,8 @@ @@ -113,7 +95,7 @@
- {#if isImage(extra.mimeType, extra.uri) && extra.content} + {#if isImageResource(extra.mimeType, extra.uri) && extra.content}
- {:else if isCode(extra.mimeType, extra.uri) && extra.content} + {:else if isCodeResource(extra.mimeType, extra.uri) && extra.content} {:else if extra.content}
 {
 			const aName = getResourceDisplayName(a);
 			const bName = getResourceDisplayName(b);
@@ -119,18 +106,13 @@
 		});
 	}
 
-	function getAllResourcesFlat(): MCPResourceInfo[] {
-		// Fallback for other uses (like attaching)
-		return getAllResourcesFlatInTreeOrder();
-	}
-
 	async function handleAttach() {
 		if (selectedResources.size === 0) return;
 
 		isAttaching = true;
 
 		try {
-			const allResources = getAllResourcesFlat();
+			const allResources = getAllResourcesFlatInTreeOrder();
 			const resourcesToAttach = allResources.filter((r) => selectedResources.has(r.uri));
 
 			for (const resource of resourcesToAttach) {
@@ -185,7 +167,7 @@
 
 			
{#if selectedResources.size === 1} - {@const allResources = getAllResourcesFlat()} + {@const allResources = getAllResourcesFlatInTreeOrder()} {@const selectedResource = allResources.find((r) => selectedResources.has(r.uri))} diff --git a/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowser.svelte b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowser.svelte index 44b43b4f9a..3e14ef8c60 100644 --- a/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowser.svelte +++ b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowser.svelte @@ -4,7 +4,7 @@ import { mcpResources, mcpResourcesLoading } from '$lib/stores/mcp-resources.svelte'; import type { MCPServerResources, MCPResourceInfo } from '$lib/types'; import { SvelteMap, SvelteSet } from 'svelte/reactivity'; - import { parseResourcePath } from './mcp-resource-browser'; + import { parseResourcePath } from '$lib/utils'; import McpResourceBrowserHeader from './McpResourceBrowserHeader.svelte'; import McpResourceBrowserEmptyState from './McpResourceBrowserEmptyState.svelte'; import McpResourceBrowserServerItem from './McpResourceBrowserServerItem.svelte'; diff --git a/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserHeader.svelte b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserHeader.svelte index 1a57d5c724..419654c13c 100644 --- a/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserHeader.svelte +++ b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserHeader.svelte @@ -1,7 +1,7 @@ @@ -98,8 +84,8 @@
@@ -108,7 +94,7 @@ size="sm" class="h-7 w-7 p-0" onclick={handleDownload} - disabled={isLoading || !getTextContent()} + disabled={isLoading || !getResourceTextContent(content)} title="Download content" > @@ -128,8 +114,8 @@ {error}
{:else if content} - {@const textContent = getTextContent()} - {@const blobContent = getBlobContent()} + {@const textContent = getResourceTextContent(content)} + {@const blobContent = getResourceBlobContent(content)} {#if textContent}
{textContent}
diff --git a/tools/server/webui/src/lib/components/app/mcp/McpServerCard/McpServerCardHeader.svelte b/tools/server/webui/src/lib/components/app/mcp/McpServerCard/McpServerCardHeader.svelte index 353e357ded..509bba5c45 100644 --- a/tools/server/webui/src/lib/components/app/mcp/McpServerCard/McpServerCardHeader.svelte +++ b/tools/server/webui/src/lib/components/app/mcp/McpServerCard/McpServerCardHeader.svelte @@ -1,8 +1,9 @@
@@ -87,13 +73,13 @@ {#if capabilities || transportType}
{#if transportType} - {@const TransportIcon = transportIcons[transportType]} + {@const TransportIcon = MCP_TRANSPORT_ICONS[transportType]} {#if TransportIcon} {/if} - {transportLabels[transportType] || transportType} + {MCP_TRANSPORT_LABELS[transportType] || transportType} {/if} diff --git a/tools/server/webui/src/lib/constants/agentic-ui.ts b/tools/server/webui/src/lib/constants/agentic-ui.ts deleted file mode 100644 index a4b9baa272..0000000000 --- a/tools/server/webui/src/lib/constants/agentic-ui.ts +++ /dev/null @@ -1 +0,0 @@ -export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/; diff --git a/tools/server/webui/src/lib/constants/agentic.ts b/tools/server/webui/src/lib/constants/agentic.ts index c0f412c9ab..9458ab0097 100644 --- a/tools/server/webui/src/lib/constants/agentic.ts +++ b/tools/server/webui/src/lib/constants/agentic.ts @@ -1,5 +1,9 @@ import type { AgenticConfig } from '$lib/types/agentic'; +export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/; + +export const NEWLINE_SEPARATOR = '\n'; + export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = { enabled: true, maxTurns: 100, diff --git a/tools/server/webui/src/lib/constants/mcp-resource.ts b/tools/server/webui/src/lib/constants/mcp-resource.ts index 8f82d1d2c5..9f3fc23eee 100644 --- a/tools/server/webui/src/lib/constants/mcp-resource.ts +++ b/tools/server/webui/src/lib/constants/mcp-resource.ts @@ -18,7 +18,7 @@ export const FILE_EXTENSION_REGEX = /\.[^.]+$/; */ export const IMAGE_MIME_TO_EXTENSION: Record = { [MimeTypeImage.JPEG]: 'jpg', - 'image/jpg': 'jpg', + [MimeTypeImage.JPG]: 'jpg', [MimeTypeImage.PNG]: 'png', [MimeTypeImage.GIF]: 'gif', [MimeTypeImage.WEBP]: 'webp' diff --git a/tools/server/webui/src/lib/constants/mcp.ts b/tools/server/webui/src/lib/constants/mcp.ts index c084b9f5cc..a11eb461e1 100644 --- a/tools/server/webui/src/lib/constants/mcp.ts +++ b/tools/server/webui/src/lib/constants/mcp.ts @@ -1,4 +1,7 @@ +import { Zap, Globe, Radio } from '@lucide/svelte'; +import { MCPTransportType } from '$lib/enums'; import type { ClientCapabilities, Implementation } from '$lib/types'; +import type { Component } from 'svelte'; export const DEFAULT_MCP_CONFIG = { protocolVersion: '2025-06-18', @@ -15,3 +18,17 @@ export const DEFAULT_IMAGE_MIME_TYPE = 'image/png'; export const MCP_RECONNECT_INITIAL_DELAY = 1000; export const MCP_RECONNECT_BACKOFF_MULTIPLIER = 2; export const MCP_RECONNECT_MAX_DELAY = 30000; + +/** Human-readable labels for MCP transport types */ +export const MCP_TRANSPORT_LABELS: Record = { + [MCPTransportType.WEBSOCKET]: 'WebSocket', + [MCPTransportType.STREAMABLE_HTTP]: 'HTTP', + [MCPTransportType.SSE]: 'SSE' +}; + +/** Icon components for MCP transport types */ +export const MCP_TRANSPORT_ICONS: Record = { + [MCPTransportType.WEBSOCKET]: Zap, + [MCPTransportType.STREAMABLE_HTTP]: Globe, + [MCPTransportType.SSE]: Radio +}; diff --git a/tools/server/webui/src/lib/enums/files.ts b/tools/server/webui/src/lib/enums/files.ts index 84f90b3ea6..7efe0c706b 100644 --- a/tools/server/webui/src/lib/enums/files.ts +++ b/tools/server/webui/src/lib/enums/files.ts @@ -178,6 +178,7 @@ export enum MimeTypeAudio { export enum MimeTypeImage { JPEG = 'image/jpeg', + JPG = 'image/jpg', PNG = 'image/png', GIF = 'image/gif', WEBP = 'image/webp', diff --git a/tools/server/webui/src/lib/stores/agentic.svelte.ts b/tools/server/webui/src/lib/stores/agentic.svelte.ts index 5eb6dd0262..3da57f7fd9 100644 --- a/tools/server/webui/src/lib/stores/agentic.svelte.ts +++ b/tools/server/webui/src/lib/stores/agentic.svelte.ts @@ -23,6 +23,7 @@ import { mcpStore } from '$lib/stores/mcp.svelte'; import { modelsStore } from '$lib/stores/models.svelte'; import { isAbortError } from '$lib/utils'; import { DEFAULT_AGENTIC_CONFIG, AGENTIC_TAGS } from '$lib/constants/agentic'; +import { IMAGE_MIME_TO_EXTENSION } from '$lib/constants/mcp-resource'; import { AttachmentType, ContentPartType, MessageRole } from '$lib/enums'; import type { AgenticFlowParams, @@ -658,14 +659,7 @@ class AgenticStore { } private buildAttachmentName(mimeType: string, index: number): string { - const extensionMap: Record = { - 'image/jpeg': 'jpg', - 'image/jpg': 'jpg', - 'image/png': 'png', - 'image/gif': 'gif', - 'image/webp': 'webp' - }; - const extension = extensionMap[mimeType] ?? 'img'; + const extension = IMAGE_MIME_TO_EXTENSION[mimeType] ?? 'img'; return `mcp-attachment-${Date.now()}-${index}.${extension}`; } } diff --git a/tools/server/webui/src/lib/types/common.d.ts b/tools/server/webui/src/lib/types/common.d.ts index 88db71e199..a9bd34722e 100644 --- a/tools/server/webui/src/lib/types/common.d.ts +++ b/tools/server/webui/src/lib/types/common.d.ts @@ -59,3 +59,5 @@ export interface ParsedClipboardContent { textAttachments: ClipboardTextAttachment[]; mcpPromptAttachments: ClipboardMcpPromptAttachment[]; } + +export type MimeTypeUnion = MimeTypeAudio | MimeTypeImage | MimeTypeApplication | MimeTypeText; diff --git a/tools/server/webui/src/lib/types/mcp.d.ts b/tools/server/webui/src/lib/types/mcp.d.ts index 9110aeba93..39d7d92439 100644 --- a/tools/server/webui/src/lib/types/mcp.d.ts +++ b/tools/server/webui/src/lib/types/mcp.d.ts @@ -11,6 +11,7 @@ import type { PromptMessage, Transport } from '@modelcontextprotocol/sdk'; +import type { MimeTypeUnion } from './common'; export type { Tool, CallToolResult, Prompt, GetPromptResult, PromptMessage }; export type ClientCapabilities = SDKClientCapabilities; @@ -37,7 +38,7 @@ export interface MCPServerInfo { title?: string; description?: string; websiteUrl?: string; - icons?: Array<{ src: string; mimeType?: string; sizes?: string[] }>; + icons?: Array<{ src: string; mimeType?: MimeTypeUnion; sizes?: string[] }>; } /** @@ -281,7 +282,7 @@ export interface MCPResourceAnnotations { */ export interface MCPResourceIcon { src: string; - mimeType?: string; + mimeType?: MimeTypeUnion; sizes?: string[]; theme?: 'light' | 'dark'; } @@ -294,7 +295,7 @@ export interface MCPResource { name: string; title?: string; description?: string; - mimeType?: string; + mimeType?: MimeTypeUnion; annotations?: MCPResourceAnnotations; icons?: MCPResourceIcon[]; _meta?: Record; @@ -308,7 +309,7 @@ export interface MCPResourceTemplate { name: string; title?: string; description?: string; - mimeType?: string; + mimeType?: MimeTypeUnion; annotations?: MCPResourceAnnotations; icons?: MCPResourceIcon[]; _meta?: Record; @@ -319,7 +320,7 @@ export interface MCPResourceTemplate { */ export interface MCPTextResourceContent { uri: string; - mimeType?: string; + mimeType?: MimeTypeUnion; text: string; } @@ -328,7 +329,7 @@ export interface MCPTextResourceContent { */ export interface MCPBlobResourceContent { uri: string; - mimeType?: string; + mimeType?: MimeTypeUnion; /** Base64-encoded binary data */ blob: string; } @@ -354,7 +355,7 @@ export interface MCPResourceInfo { name: string; title?: string; description?: string; - mimeType?: string; + mimeType?: MimeTypeUnion; serverName: string; annotations?: MCPResourceAnnotations; icons?: MCPResourceIcon[]; @@ -368,7 +369,7 @@ export interface MCPResourceTemplateInfo { name: string; title?: string; description?: string; - mimeType?: string; + mimeType?: MimeTypeUnion; serverName: string; annotations?: MCPResourceAnnotations; icons?: MCPResourceIcon[]; diff --git a/tools/server/webui/src/lib/utils/index.ts b/tools/server/webui/src/lib/utils/index.ts index 45bf5a9c68..19eddf3ee7 100644 --- a/tools/server/webui/src/lib/utils/index.ts +++ b/tools/server/webui/src/lib/utils/index.ts @@ -109,7 +109,16 @@ export { parseMcpServerSettings, getMcpLogLevelIcon, getMcpLogLevelClass, - isImageMimeType + isImageMimeType, + parseResourcePath, + getDisplayName, + getResourceDisplayName, + isCodeResource, + isImageResource, + getResourceIcon, + getResourceTextContent, + getResourceBlobContent, + downloadResourceContent } from './mcp'; // Data URL utilities diff --git a/tools/server/webui/src/lib/utils/mcp.ts b/tools/server/webui/src/lib/utils/mcp.ts index 1470d114fa..4b55b9145f 100644 --- a/tools/server/webui/src/lib/utils/mcp.ts +++ b/tools/server/webui/src/lib/utils/mcp.ts @@ -5,7 +5,8 @@ import { UrlPrefix, MimeTypePrefix, MimeTypeIncludes, - UriPattern + UriPattern, + MimeTypeText } from '$lib/enums'; import { DEFAULT_MCP_CONFIG, MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp'; import { @@ -15,8 +16,18 @@ import { PROTOCOL_PREFIX_REGEX, FILE_EXTENSION_REGEX } from '$lib/constants/mcp-resource'; -import { Database, File, FileText, Image, Code, Info, AlertTriangle, XCircle } from '@lucide/svelte'; +import { + Database, + File, + FileText, + Image, + Code, + Info, + AlertTriangle, + XCircle +} from '@lucide/svelte'; import type { Component } from 'svelte'; +import type { MimeTypeUnion } from '$lib/types/common'; /** * Detects the MCP transport type from a URL. @@ -119,6 +130,170 @@ export function getMcpLogLevelClass(level: MCPLogLevel): string { * @param mimeType - The MIME type to check * @returns True if the MIME type starts with 'image/' */ -export function isImageMimeType(mimeType?: string): boolean { +export function isImageMimeType(mimeType?: MimeTypeUnion): boolean { return mimeType?.startsWith(MimeTypePrefix.IMAGE) ?? false; } + +/** + * Parse a resource URI into path segments, stripping the protocol prefix. + * + * @param uri - The resource URI to parse + * @returns Array of non-empty path segments + */ +export function parseResourcePath(uri: string): string[] { + try { + const withoutProtocol = uri.replace(PROTOCOL_PREFIX_REGEX, ''); + return withoutProtocol.split('/').filter((p) => p.length > 0); + } catch { + return [uri]; + } +} + +/** + * Convert a path part into a human-readable display name. + * Strips file extensions and converts kebab-case/snake_case to Title Case. + * + * @param pathPart - The path segment to convert + * @returns Human-readable display name + */ +export function getDisplayName(pathPart: string): string { + const withoutExt = pathPart.replace(FILE_EXTENSION_REGEX, ''); + return withoutExt + .split(/[-_]/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +/** + * Get the display name from a resource, extracting the last path segment from the URI. + * + * @param resource - The MCP resource info + * @returns Display name string + */ +export function getResourceDisplayName(resource: MCPResourceInfo): string { + try { + const parts = parseResourcePath(resource.uri); + return parts[parts.length - 1] || resource.name || resource.uri; + } catch { + return resource.name || resource.uri; + } +} + +/** + * Determine if a MIME type and/or URI represents code content. + * + * @param mimeType - Optional MIME type string + * @param uri - Optional URI string + * @returns True if the content is code + */ +export function isCodeResource(mimeType?: MimeTypeUnion, uri?: string): boolean { + const mime = mimeType?.toLowerCase() || ''; + const u = uri?.toLowerCase() || ''; + return ( + mime.includes(MimeTypeIncludes.JSON) || + mime.includes(MimeTypeIncludes.JAVASCRIPT) || + mime.includes(MimeTypeIncludes.TYPESCRIPT) || + CODE_FILE_EXTENSION_REGEX.test(u) + ); +} + +/** + * Determine if a MIME type and/or URI represents image content. + * + * @param mimeType - Optional MIME type string + * @param uri - Optional URI string + * @returns True if the content is an image + */ +export function isImageResource(mimeType?: MimeTypeUnion, uri?: string): boolean { + const mime = mimeType?.toLowerCase() || ''; + const u = uri?.toLowerCase() || ''; + return mime.startsWith(MimeTypePrefix.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u); +} + +/** + * Get the appropriate Lucide icon component for an MCP resource based on its MIME type and URI. + * + * @param mimeType - Optional MIME type of the resource + * @param uri - Optional URI of the resource + * @returns Lucide icon component + */ +export function getResourceIcon(mimeType?: MimeTypeUnion, uri?: string): Component { + const mime = mimeType?.toLowerCase() || ''; + const u = uri?.toLowerCase() || ''; + + if (mime.startsWith(MimeTypePrefix.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u)) { + return Image; + } + + if ( + mime.includes(MimeTypeIncludes.JSON) || + mime.includes(MimeTypeIncludes.JAVASCRIPT) || + mime.includes(MimeTypeIncludes.TYPESCRIPT) || + CODE_FILE_EXTENSION_REGEX.test(u) + ) { + return Code; + } + + if (mime.includes(MimeTypePrefix.TEXT) || TEXT_FILE_EXTENSION_REGEX.test(u)) { + return FileText; + } + + if (u.includes(UriPattern.DATABASE_KEYWORD) || u.includes(UriPattern.DATABASE_SCHEME)) { + return Database; + } + + return File; +} + +/** + * Extract text content from MCP resource content array. + * + * @param content - Array of MCP resource content items + * @returns Joined text content string + */ +export function getResourceTextContent(content: MCPResourceContent[] | null | undefined): string { + if (!content) return ''; + return content + .filter((c): c is { uri: string; mimeType?: MimeTypeUnion; text: string } => 'text' in c) + .map((c) => c.text) + .join('\n\n'); +} + +/** + * Extract blob content from MCP resource content array. + * + * @param content - Array of MCP resource content items + * @returns Array of blob content items + */ +export function getResourceBlobContent( + content: MCPResourceContent[] | null | undefined +): Array<{ uri: string; mimeType?: MimeTypeUnion; blob: string }> { + if (!content) return []; + + return content.filter( + (c): c is { uri: string; mimeType?: MimeTypeUnion; blob: string } => 'blob' in c + ); +} + +/** + * Trigger a file download from text content. + * + * @param text - The text content to download + * @param mimeType - MIME type for the blob + * @param filename - Suggested filename + */ +export function downloadResourceContent( + text: string, + mimeType: MimeTypeUnion = MimeTypeText.PLAIN, + filename: string = 'resource.txt' +): void { + const blob = new Blob([text], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +}