diff --git a/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentMcpResource.svelte b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentMcpResource.svelte new file mode 100644 index 0000000000..6814391321 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentMcpResource.svelte @@ -0,0 +1,124 @@ + + + + + + {/if} + + + + +
+ {#if favicon} + { + (e.currentTarget as HTMLImageElement).style.display = 'none'; + }} + /> + {/if} + + + {serverName} + +
+
+
diff --git a/tools/server/webui/src/lib/components/app/chat/index.ts b/tools/server/webui/src/lib/components/app/chat/index.ts index e01ae9942f..c5f1b74420 100644 --- a/tools/server/webui/src/lib/components/app/chat/index.ts +++ b/tools/server/webui/src/lib/components/app/chat/index.ts @@ -58,6 +58,11 @@ export { default as ChatAttachmentsList } from './ChatAttachments/ChatAttachment */ export { default as ChatAttachmentMcpPrompt } from './ChatAttachments/ChatAttachmentMcpPrompt.svelte'; +/** + * Todo - add description + */ +export { default as ChatAttachmentMcpResource } from './ChatAttachments/ChatAttachmentMcpResource.svelte'; + /** * Full-size attachment preview component for dialog display. Handles different file types: * images (full-size display), text files (syntax highlighted), PDFs (text extraction or image preview), diff --git a/tools/server/webui/src/lib/components/app/dialogs/DialogMcpResources.svelte b/tools/server/webui/src/lib/components/app/dialogs/DialogMcpResources.svelte new file mode 100644 index 0000000000..791d33c887 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/dialogs/DialogMcpResources.svelte @@ -0,0 +1,190 @@ + + + + + + + + MCP Resources + {#if totalCount > 0} + ({totalCount}) + {/if} + + + Browse and attach resources from connected MCP servers to your chat context. + + + +
+
+ +
+ +
+ {#if selectedResources.size === 1} + {@const allResources = getAllResourcesFlat()} + {@const selectedResource = allResources.find((r) => selectedResources.has(r.uri))} + + {:else if selectedResources.size > 1} +
+ {selectedResources.size} resources selected +
+ {:else} +
+ Select a resource to preview +
+ {/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 c9b664326a..262d374f5b 100644 --- a/tools/server/webui/src/lib/components/app/dialogs/index.ts +++ b/tools/server/webui/src/lib/components/app/dialogs/index.ts @@ -443,3 +443,33 @@ export { default as DialogConversationSelection } from './DialogConversationSele * ``` */ export { default as DialogModelInformation } from './DialogModelInformation.svelte'; + +/** + * **DialogMcpResources** - MCP resources browser dialog + * + * Dialog for browsing and attaching MCP resources to chat context. + * Displays resources from connected MCP servers in a tree structure + * with preview panel and multi-select support. + * + * **Architecture:** + * - Uses ShadCN Dialog with two-panel layout + * - Left panel: McpResourceBrowser with tree navigation + * - Right panel: McpResourcePreview for selected resource + * - Integrates with mcpStore for resource fetching and attachment + * + * **Features:** + * - Tree-based resource navigation by server and path + * - Single and multi-select with shift+click + * - Resource preview with content display + * - Quick attach button per resource + * - Batch attach for multiple selections + * + * @example + * ```svelte + * + * ``` + */ +export { default as DialogMcpResources } from './DialogMcpResources.svelte'; diff --git a/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser.svelte b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser.svelte new file mode 100644 index 0000000000..ced60ba9a4 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser.svelte @@ -0,0 +1,402 @@ + + +{#snippet renderTreeNode( + node: ResourceTreeNode, + serverName: string, + depth: number, + parentPath: string +)} + {@const isFolder = !node.resource && node.children.size > 0} + {@const folderId = `${serverName}:${parentPath}/${node.name}`} + {@const isFolderExpanded = expandedFolders.has(folderId)} + + {#if isFolder} + {@const folderCount = countTreeResources(node)} + toggleFolder(folderId)}> + + {#if isFolderExpanded} + + {:else} + + {/if} + + {node.name} + ({folderCount}) + + +
+ {#each [...node.children.values()].sort((a, b) => { + // Folders first, then files + const aIsFolder = !a.resource && a.children.size > 0; + const bIsFolder = !b.resource && b.children.size > 0; + if (aIsFolder && !bIsFolder) return -1; + if (!aIsFolder && bIsFolder) return 1; + return a.name.localeCompare(b.name); + }) as child} + {@render renderTreeNode(child, serverName, depth + 1, `${parentPath}/${node.name}`)} + {/each} +
+
+
+ {:else if node.resource} + {@const resource = node.resource} + {@const ResourceIcon = getResourceIcon(resource)} + {@const isSelected = isResourceSelected(resource)} + {@const displayName = resource.title || getDisplayName(node.name)} +
+ {#if onToggle} + handleCheckboxChange(resource, checked === true)} + class="h-4 w-4" + /> + {/if} + + {/if} + +
+ {/if} +{/snippet} + +
+
+

Available resources

+ + +
+ +
+ {#if resources.size === 0} +
+ {#if isLoading} + Loading resources... + {:else} + No resources available + {/if} +
+ {:else} + {#each [...resources.entries()] as [serverName, serverRes]} + {@const isExpanded = expandedServers.has(serverName)} + {@const hasResources = serverRes.resources.length > 0} + {@const displayName = getServerDisplayName(serverName)} + {@const favicon = getServerFavicon(serverName)} + {@const resourceTree = buildResourceTree(serverRes.resources, serverName)} + toggleServer(serverName)}> + + {#if isExpanded} + + {:else} + + {/if} + {#if favicon} + { + (e.currentTarget as HTMLImageElement).style.display = 'none'; + }} + /> + {/if} + {displayName} + + ({serverRes.resources.length}) + + {#if serverRes.loading} + + {/if} + + + +
+ {#if serverRes.error} +
+ Error: {serverRes.error} +
+ {:else if !hasResources} +
No resources
+ {:else} + {#each [...resourceTree.children.values()].sort((a, b) => { + const aIsFolder = !a.resource && a.children.size > 0; + const bIsFolder = !b.resource && b.children.size > 0; + + if (aIsFolder && !bIsFolder) return -1; + if (!aIsFolder && bIsFolder) return 1; + + return a.name.localeCompare(b.name); + }) as child} + {@render renderTreeNode(child, serverName, 1, '')} + {/each} + {/if} +
+
+
+ {/each} + {/if} +
+
diff --git a/tools/server/webui/src/lib/components/app/mcp/McpResourcePicker.svelte b/tools/server/webui/src/lib/components/app/mcp/McpResourcePicker.svelte new file mode 100644 index 0000000000..a822f18396 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/mcp/McpResourcePicker.svelte @@ -0,0 +1,21 @@ + + + diff --git a/tools/server/webui/src/lib/components/app/mcp/McpResourcePreview.svelte b/tools/server/webui/src/lib/components/app/mcp/McpResourcePreview.svelte new file mode 100644 index 0000000000..ba899147e5 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/mcp/McpResourcePreview.svelte @@ -0,0 +1,194 @@ + + +
+ {#if !resource} +
+ + Select a resource to preview +
+ {:else} + +
+
+

{resource.title || resource.name}

+

{resource.uri}

+ {#if resource.description} +

{resource.description}

+ {/if} +
+
+ + +
+
+ + +
+ {#if isLoading} +
+ +
+ {:else if error} +
+ + {error} +
+ {:else if content} + {@const textContent = getTextContent()} + {@const blobContent = getBlobContent()} + + {#if textContent} +
{textContent}
+ {/if} + + {#each blobContent as blob} + {#if isImageMimeType(blob.mimeType)} + Resource content + {:else} +
+ + Binary content ({blob.mimeType || 'unknown type'}) +
+ {/if} + {/each} + + {#if !textContent && blobContent.length === 0} +
No content available
+ {/if} + {/if} +
+ + + {#if resource.mimeType || resource.annotations} +
+ {#if resource.mimeType} + {resource.mimeType} + {/if} + {#if resource.annotations?.priority !== undefined} + + Priority: {resource.annotations.priority} + + {/if} + + Server: {resource.serverName} + +
+ {/if} + {/if} +
diff --git a/tools/server/webui/src/lib/components/app/mcp/index.ts b/tools/server/webui/src/lib/components/app/mcp/index.ts index 44e0ec69ce..ea353ca5c0 100644 --- a/tools/server/webui/src/lib/components/app/mcp/index.ts +++ b/tools/server/webui/src/lib/components/app/mcp/index.ts @@ -211,3 +211,50 @@ export { default as McpServerCardDeleteDialog } from './McpServerCard/McpServerC * Displays guidance text from the MCP server for users. */ export { default as McpServerInfo } from './McpServerInfo.svelte'; + +/** + * **McpResourceBrowser** - MCP resources tree browser + * + * Tree view component showing resources grouped by server. + * Supports resource selection and quick attach actions. + * + * **Features:** + * - Collapsible server sections + * - Resource icons based on MIME type + * - Resource selection highlighting + * - Quick attach button per resource + * - Refresh all resources action + * - Loading states per server + */ +export { default as McpResourceBrowser } from './McpResourceBrowser.svelte'; + +/** + * **McpResourcePreview** - MCP resource content preview + * + * Preview panel showing resource content with metadata. + * Supports text and binary content display. + * + * **Features:** + * - Text content display with monospace formatting + * - Image preview for image MIME types + * - Copy to clipboard action + * - Download content action + * - Resource metadata display (MIME type, priority, server) + * - Loading and error states + */ +export { default as McpResourcePreview } from './McpResourcePreview.svelte'; + +/** + * **McpResourcePicker** - MCP resource selection dialog + * + * Full dialog for browsing and attaching MCP resources. + * Combines browser and preview panels. + * + * **Features:** + * - Split panel layout (browser + preview) + * - Resource selection with preview + * - Quick attach from browser + * - Attach selected resource action + * - Auto-fetch resources on open + */ +export { default as McpResourcePicker } from './McpResourcePicker.svelte';