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 @@
{
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);
+}