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 new file mode 100644 index 0000000000..40de04d895 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentMcpResourceStored.svelte @@ -0,0 +1,99 @@ + + + + + { + e.stopPropagation(); + onClick?.(e); + }} + disabled={!onClick} + > + + + + {extra.name} + + + {#if !readonly && onRemove} + onRemove?.()} /> + {/if} + + + + + + {#if favicon} + { + (e.currentTarget as HTMLImageElement).style.display = 'none'; + }} + /> + {/if} + + + {serverName} + + + + diff --git a/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList.svelte b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList.svelte index ea9ac97099..edeb988995 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList.svelte @@ -1,15 +1,17 @@ + + + + + {extra.name} + + + {extra.uri} + + {#if serverName} + + ยท + {#if favicon} + { + (e.currentTarget as HTMLImageElement).style.display = 'none'; + }} + /> + {/if} + {serverName} + + {/if} + + {#if extra.mimeType} + {extra.mimeType} + {/if} + + + + + + + + + + + + + + {#if isImage(extra.mimeType, extra.uri) && extra.content} + + + + {:else if isCode(extra.mimeType, extra.uri) && extra.content} + + {:else if extra.content} + {extra.content} + {:else} + No content available + {/if} + + + diff --git a/tools/server/webui/src/lib/components/app/dialogs/index.ts b/tools/server/webui/src/lib/components/app/dialogs/index.ts index 262d374f5b..77160e02aa 100644 --- a/tools/server/webui/src/lib/components/app/dialogs/index.ts +++ b/tools/server/webui/src/lib/components/app/dialogs/index.ts @@ -473,3 +473,27 @@ export { default as DialogModelInformation } from './DialogModelInformation.svel * ``` */ export { default as DialogMcpResources } from './DialogMcpResources.svelte'; + +/** + * **DialogMcpResourcePreview** - MCP resource content preview + * + * Dialog for previewing the content of a stored MCP resource attachment. + * Displays the resource content with syntax highlighting for code, + * image rendering for images, and plain text for other content. + * + * **Features:** + * - Syntax highlighted code preview + * - Image rendering for image resources + * - Copy to clipboard and download actions + * - Server name and favicon display + * - MIME type badge + * + * @example + * ```svelte + * + * ``` + */ +export { default as DialogMcpResourcePreview } from './DialogMcpResourcePreview.svelte'; diff --git a/tools/server/webui/src/lib/constants/attachment-labels.ts b/tools/server/webui/src/lib/constants/attachment-labels.ts index 2a32e4e18b..be9999c0f9 100644 --- a/tools/server/webui/src/lib/constants/attachment-labels.ts +++ b/tools/server/webui/src/lib/constants/attachment-labels.ts @@ -1,3 +1,4 @@ export const ATTACHMENT_LABEL_FILE = 'File'; export const ATTACHMENT_LABEL_PDF_FILE = 'PDF File'; export const ATTACHMENT_LABEL_MCP_PROMPT = 'MCP Prompt'; +export const ATTACHMENT_LABEL_MCP_RESOURCE = 'MCP Resource'; diff --git a/tools/server/webui/src/lib/enums/attachment.ts b/tools/server/webui/src/lib/enums/attachment.ts index 1a3ad5dbbb..28863d7efc 100644 --- a/tools/server/webui/src/lib/enums/attachment.ts +++ b/tools/server/webui/src/lib/enums/attachment.ts @@ -5,6 +5,7 @@ export enum AttachmentType { AUDIO = 'AUDIO', IMAGE = 'IMAGE', MCP_PROMPT = 'MCP_PROMPT', + MCP_RESOURCE = 'MCP_RESOURCE', PDF = 'PDF', TEXT = 'TEXT', LEGACY_CONTEXT = 'context' // Legacy attachment type for backward compatibility diff --git a/tools/server/webui/src/lib/services/chat.service.ts b/tools/server/webui/src/lib/services/chat.service.ts index 0003875ce8..4c329df73c 100644 --- a/tools/server/webui/src/lib/services/chat.service.ts +++ b/tools/server/webui/src/lib/services/chat.service.ts @@ -2,7 +2,8 @@ import { getJsonHeaders, formatAttachmentText, isAbortError } from '$lib/utils'; import { AGENTIC_REGEX } from '$lib/constants/agentic'; import { ATTACHMENT_LABEL_PDF_FILE, - ATTACHMENT_LABEL_MCP_PROMPT + ATTACHMENT_LABEL_MCP_PROMPT, + ATTACHMENT_LABEL_MCP_RESOURCE } from '$lib/constants/attachment-labels'; import { AttachmentType, @@ -12,7 +13,7 @@ import { UrlPrefix } from '$lib/enums'; import type { ApiChatMessageContentPart, ApiChatCompletionToolCall } from '$lib/types/api'; -import type { DatabaseMessageExtraMcpPrompt } from '$lib/types'; +import type { DatabaseMessageExtraMcpPrompt, DatabaseMessageExtraMcpResource } from '$lib/types'; import { modelsStore } from '$lib/stores/models.svelte'; /** @@ -796,6 +797,23 @@ export class ChatService { }); } + const mcpResources = message.extra.filter( + (extra: DatabaseMessageExtra): extra is DatabaseMessageExtraMcpResource => + extra.type === AttachmentType.MCP_RESOURCE + ); + + for (const mcpResource of mcpResources) { + contentParts.push({ + type: ContentPartType.TEXT, + text: formatAttachmentText( + ATTACHMENT_LABEL_MCP_RESOURCE, + mcpResource.name, + mcpResource.content, + mcpResource.serverName + ) + }); + } + const result: ApiChatMessageData = { role: message.role as MessageRole, content: contentParts diff --git a/tools/server/webui/src/lib/stores/chat.svelte.ts b/tools/server/webui/src/lib/stores/chat.svelte.ts index dc21d9444b..e213cc54bd 100644 --- a/tools/server/webui/src/lib/stores/chat.svelte.ts +++ b/tools/server/webui/src/lib/stores/chat.svelte.ts @@ -447,6 +447,11 @@ class ChatStore { if (!content.trim() && (!extras || extras.length === 0)) return; const activeConv = conversationsStore.activeConversation; if (activeConv && this.isChatLoadingInternal(activeConv.id)) return; + + // Consume MCP resource attachments - converts them to extras and clears the live store + const resourceExtras = mcpStore.consumeResourceAttachmentsAsExtras(); + const allExtras = resourceExtras.length > 0 ? [...(extras || []), ...resourceExtras] : extras; + let isNewConversation = false; if (!activeConv) { await conversationsStore.createConversation(); @@ -478,7 +483,7 @@ class ChatStore { content, MessageType.TEXT, parentIdForUserMessage ?? '-1', - extras + allExtras ); if (isNewConversation && content) await conversationsStore.updateConversationName(currentConv.id, content.trim()); @@ -686,6 +691,7 @@ class ChatStore { } }; const perChatOverrides = conversationsStore.activeConversation?.mcpServerOverrides; + const agenticConfig = agenticStore.getConfig(config(), perChatOverrides); if (agenticConfig.enabled) { const agenticResult = await agenticStore.runAgenticFlow({ @@ -698,29 +704,16 @@ class ChatStore { }); if (agenticResult.handled) return; } - const resourceContext = mcpStore.getResourceContextForChat(); - let messagesWithResources = allMessages; - if (resourceContext) { - messagesWithResources = allMessages.map((msg, idx) => { - if (idx === allMessages.length - 1 && msg.role === MessageRole.USER) { - return { - ...msg, - content: resourceContext + '\n\n' + msg.content - }; - } - return msg; - }); - mcpStore.clearResourceAttachments(); - } + const completionOptions = { + ...this.getApiOptions(), + ...(effectiveModel ? { model: effectiveModel } : {}), + ...streamCallbacks + }; await ChatService.sendMessage( - messagesWithResources, - { - ...this.getApiOptions(), - ...(effectiveModel ? { model: effectiveModel } : {}), - ...streamCallbacks - }, + allMessages, + completionOptions, assistantMessage.convId, abortController.signal ); diff --git a/tools/server/webui/src/lib/stores/mcp-resources.svelte.ts b/tools/server/webui/src/lib/stores/mcp-resources.svelte.ts index d83b7b2ce2..481c4491b5 100644 --- a/tools/server/webui/src/lib/stores/mcp-resources.svelte.ts +++ b/tools/server/webui/src/lib/stores/mcp-resources.svelte.ts @@ -11,6 +11,7 @@ */ import { SvelteMap } from 'svelte/reactivity'; +import { AttachmentType } from '$lib/enums'; import type { MCPResource, MCPResourceTemplate, @@ -524,6 +525,43 @@ class MCPResourceStore { return parts.join(''); } + + /** + * Convert current resource attachments to DatabaseMessageExtra[] for persisting with a message. + * Each attachment becomes a DatabaseMessageExtraMcpResource stored on the user message. + */ + toMessageExtras(): import('$lib/types').DatabaseMessageExtraMcpResource[] { + const extras: import('$lib/types').DatabaseMessageExtraMcpResource[] = []; + + for (const attachment of this._attachments) { + if (attachment.error) continue; + if (!attachment.content || attachment.content.length === 0) continue; + + const resourceName = attachment.resource.title || attachment.resource.name; + const contentParts: string[] = []; + + for (const content of attachment.content) { + if ('text' in content && content.text) { + contentParts.push(content.text); + } else if ('blob' in content && content.blob) { + contentParts.push(`[Binary content: ${content.mimeType || 'unknown type'}]`); + } + } + + if (contentParts.length > 0) { + extras.push({ + type: AttachmentType.MCP_RESOURCE, + name: resourceName, + uri: attachment.resource.uri, + serverName: attachment.resource.serverName, + content: contentParts.join('\n'), + mimeType: attachment.resource.mimeType + }); + } + } + + return extras; + } } export const mcpResourceStore = new MCPResourceStore(); diff --git a/tools/server/webui/src/lib/stores/mcp.svelte.ts b/tools/server/webui/src/lib/stores/mcp.svelte.ts index 4ed78bb646..45b27f63d3 100644 --- a/tools/server/webui/src/lib/stores/mcp.svelte.ts +++ b/tools/server/webui/src/lib/stores/mcp.svelte.ts @@ -23,7 +23,7 @@ import { browser } from '$app/environment'; import { MCPService } from '$lib/services/mcp.service'; import { config, settingsStore } from '$lib/stores/settings.svelte'; import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte'; -import { parseMcpServerSettings, detectMcpTransportFromUrl } from '$lib/utils'; +import { parseMcpServerSettings, detectMcpTransportFromUrl, getFaviconUrl } from '$lib/utils'; import { MCPConnectionPhase, MCPLogLevel, HealthCheckStatus, MCPRefType } from '$lib/enums'; import { DEFAULT_MCP_CONFIG, @@ -298,17 +298,13 @@ class MCPStore { /** * Get favicon URL for an MCP server by its ID. + * Uses Google's favicon service for consistent display. * Returns null if server is not found. */ getServerFavicon(serverId: string): string | null { const server = this.getServerById(serverId); if (!server) return null; - try { - const url = new URL(server.url); - return `${url.origin}/favicon.ico`; - } catch { - return null; - } + return getFaviconUrl(server.url); } isAnyServerLoading(): boolean { @@ -1425,6 +1421,18 @@ class MCPStore { getResourceContextForChat(): string { return mcpResourceStore.formatAttachmentsForContext(); } + + /** + * Convert current resource attachments to DatabaseMessageExtra[] and clear them. + * Called during message send to persist resources with the user message. + */ + consumeResourceAttachmentsAsExtras(): import('$lib/types').DatabaseMessageExtraMcpResource[] { + const extras = mcpResourceStore.toMessageExtras(); + if (extras.length > 0) { + mcpResourceStore.clearAttachments(); + } + return extras; + } } export const mcpStore = new MCPStore(); diff --git a/tools/server/webui/src/lib/types/chat.d.ts b/tools/server/webui/src/lib/types/chat.d.ts index 2e32b56d5b..86e98c8b6b 100644 --- a/tools/server/webui/src/lib/types/chat.d.ts +++ b/tools/server/webui/src/lib/types/chat.d.ts @@ -25,6 +25,7 @@ export interface ChatAttachmentDisplayItem { preview?: string; isImage: boolean; isMcpPrompt?: boolean; + isMcpResource?: boolean; isLoading?: boolean; loadError?: string; uploadedFile?: ChatUploadedFile; diff --git a/tools/server/webui/src/lib/types/database.d.ts b/tools/server/webui/src/lib/types/database.d.ts index e4e24a6099..50f51ecf5d 100644 --- a/tools/server/webui/src/lib/types/database.d.ts +++ b/tools/server/webui/src/lib/types/database.d.ts @@ -61,12 +61,22 @@ export interface DatabaseMessageExtraMcpPrompt { arguments?: Record; } +export interface DatabaseMessageExtraMcpResource { + type: AttachmentType.MCP_RESOURCE; + name: string; + uri: string; + serverName: string; + content: string; + mimeType?: string; +} + export type DatabaseMessageExtra = | DatabaseMessageExtraImageFile | DatabaseMessageExtraTextFile | DatabaseMessageExtraAudioFile | DatabaseMessageExtraPdfFile | DatabaseMessageExtraMcpPrompt + | DatabaseMessageExtraMcpResource | DatabaseMessageExtraLegacyContext; export interface DatabaseMessage { diff --git a/tools/server/webui/src/lib/types/index.ts b/tools/server/webui/src/lib/types/index.ts index 5df5cf039b..93a39f03da 100644 --- a/tools/server/webui/src/lib/types/index.ts +++ b/tools/server/webui/src/lib/types/index.ts @@ -59,6 +59,7 @@ export type { DatabaseMessageExtraImageFile, DatabaseMessageExtraLegacyContext, DatabaseMessageExtraMcpPrompt, + DatabaseMessageExtraMcpResource, DatabaseMessageExtraPdfFile, DatabaseMessageExtraTextFile, DatabaseMessageExtra, diff --git a/tools/server/webui/src/lib/utils/attachment-display.ts b/tools/server/webui/src/lib/utils/attachment-display.ts index c02c4ab5d3..396ed6671d 100644 --- a/tools/server/webui/src/lib/utils/attachment-display.ts +++ b/tools/server/webui/src/lib/utils/attachment-display.ts @@ -20,6 +20,13 @@ function isMcpPromptAttachment(attachment: DatabaseMessageExtra): boolean { return attachment.type === AttachmentType.MCP_PROMPT; } +/** + * Check if an attachment is an MCP resource + */ +function isMcpResourceAttachment(attachment: DatabaseMessageExtra): boolean { + return attachment.type === AttachmentType.MCP_RESOURCE; +} + /** * Gets the file type category from an uploaded file, checking both MIME type and extension */ @@ -63,6 +70,7 @@ export function getAttachmentDisplayItems( for (const [index, attachment] of attachments.entries()) { const isImage = isImageFile(attachment); const isMcpPrompt = isMcpPromptAttachment(attachment); + const isMcpResource = isMcpResourceAttachment(attachment); items.push({ id: `attachment-${index}`, @@ -70,6 +78,7 @@ export function getAttachmentDisplayItems( preview: isImage && 'base64Url' in attachment ? attachment.base64Url : undefined, isImage, isMcpPrompt, + isMcpResource, attachment, attachmentIndex: index, textContent: 'content' in attachment ? attachment.content : undefined diff --git a/tools/server/webui/src/lib/utils/clipboard.ts b/tools/server/webui/src/lib/utils/clipboard.ts index 7c3437cac5..8fcb554b1a 100644 --- a/tools/server/webui/src/lib/utils/clipboard.ts +++ b/tools/server/webui/src/lib/utils/clipboard.ts @@ -5,6 +5,7 @@ import type { DatabaseMessageExtraTextFile, DatabaseMessageExtraLegacyContext, DatabaseMessageExtraMcpPrompt, + DatabaseMessageExtraMcpResource, ClipboardTextAttachment, ClipboardMcpPromptAttachment, ClipboardAttachment, @@ -104,7 +105,7 @@ export function formatMessageForClipboard( extras?: DatabaseMessageExtra[], asPlainText: boolean = false ): string { - // Filter text-like attachments (TEXT, LEGACY_CONTEXT, and MCP_PROMPT types) + // Filter text-like attachments (TEXT, LEGACY_CONTEXT, MCP_PROMPT, and MCP_RESOURCE types) const textAttachments = extras?.filter( ( @@ -112,10 +113,12 @@ export function formatMessageForClipboard( ): extra is | DatabaseMessageExtraTextFile | DatabaseMessageExtraLegacyContext - | DatabaseMessageExtraMcpPrompt => + | DatabaseMessageExtraMcpPrompt + | DatabaseMessageExtraMcpResource => extra.type === AttachmentType.TEXT || extra.type === AttachmentType.LEGACY_CONTEXT || - extra.type === AttachmentType.MCP_PROMPT + extra.type === AttachmentType.MCP_PROMPT || + extra.type === AttachmentType.MCP_RESOURCE ) ?? []; if (textAttachments.length === 0) { diff --git a/tools/server/webui/src/lib/utils/mcp.ts b/tools/server/webui/src/lib/utils/mcp.ts index 78954e479f..41aa4a5b74 100644 --- a/tools/server/webui/src/lib/utils/mcp.ts +++ b/tools/server/webui/src/lib/utils/mcp.ts @@ -1,6 +1,6 @@ import type { MCPServerSettingsEntry } from '$lib/types'; import { MCPTransportType, MCPLogLevel, UrlPrefix, MimeTypePrefix } from '$lib/enums'; -import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp'; +import { DEFAULT_MCP_CONFIG, MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp'; import { Info, AlertTriangle, XCircle } from '@lucide/svelte'; import type { Component } from 'svelte'; @@ -51,12 +51,13 @@ export function parseMcpServerSettings(rawServers: unknown): MCPServerSettingsEn const id = typeof (entry as { id?: unknown })?.id === 'string' && (entry as { id?: string }).id?.trim() ? (entry as { id: string }).id.trim() - : `server-${index + 1}`; + : `${MCP_SERVER_ID_PREFIX}${index + 1}`; return { id, enabled: Boolean((entry as { enabled?: unknown })?.enabled), url, + name: (entry as { name?: string })?.name, requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds, headers: headers || undefined, useProxy: Boolean((entry as { useProxy?: unknown })?.useProxy)
{extra.content}