From fcae4b3a1b5b375cce619af5748133125d3cc640 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 3 Feb 2026 16:35:21 +0100 Subject: [PATCH] refactor: Componentize MCP Resource Browser --- .../ChatMessageMcpPromptContent.svelte | 2 +- .../app/mcp/McpResourceBrowser.svelte | 386 ------------------ .../McpResourceBrowser.svelte | 108 +++++ .../McpResourceBrowserEmptyState.svelte | 15 + .../McpResourceBrowserHeader.svelte | 30 ++ .../McpResourceBrowserServerItem.svelte | 181 ++++++++ .../mcp-resource-browser.ts | 97 +++++ .../webui/src/lib/components/app/mcp/index.ts | 2 +- 8 files changed, 433 insertions(+), 388 deletions(-) delete mode 100644 tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser.svelte create mode 100644 tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowser.svelte create mode 100644 tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserEmptyState.svelte create mode 100644 tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserHeader.svelte create mode 100644 tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserServerItem.svelte create mode 100644 tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/mcp-resource-browser.ts diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageMcpPromptContent.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageMcpPromptContent.svelte index e647c0065d..381d73159d 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageMcpPromptContent.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageMcpPromptContent.svelte @@ -1,6 +1,6 @@ - -{#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 (child.resource?.uri || `${serverName}:${parentPath}/${node.name}/${child.name}`)} - {@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] (serverName)} - {@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 (child.resource?.uri || `${serverName}:${child.name}`)} - {@render renderTreeNode(child, serverName, 1, '')} - {/each} - {/if} -
-
-
- {/each} - {/if} -
-
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 new file mode 100644 index 0000000000..fa38078ffb --- /dev/null +++ b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowser.svelte @@ -0,0 +1,108 @@ + + +
+ + +
+ {#if resources.size === 0} + + {:else} + {#each [...resources.entries()] as [serverName, serverRes] (serverName)} + toggleServer(serverName)} + onToggleFolder={toggleFolder} + {onSelect} + {onToggle} + {onAttach} + /> + {/each} + {/if} +
+
diff --git a/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserEmptyState.svelte b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserEmptyState.svelte new file mode 100644 index 0000000000..4fb0c1e24f --- /dev/null +++ b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserEmptyState.svelte @@ -0,0 +1,15 @@ + + +
+ {#if isLoading} + Loading resources... + {:else} + No resources available + {/if} +
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 new file mode 100644 index 0000000000..3c02061444 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserHeader.svelte @@ -0,0 +1,30 @@ + + +
+

Available resources

+ + +
diff --git a/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserServerItem.svelte b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserServerItem.svelte new file mode 100644 index 0000000000..8b45c19431 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/McpResourceBrowserServerItem.svelte @@ -0,0 +1,181 @@ + + +{#snippet renderTreeNode(node: ResourceTreeNode, 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)} + onToggleFolder(folderId)}> + + {#if isFolderExpanded} + + {:else} + + {/if} + + {node.name} + ({folderCount}) + + +
+ {#each sortTreeChildren( [...node.children.values()] ) as child (child.resource?.uri || `${serverName}:${parentPath}/${node.name}/${child.name}`)} + {@render renderTreeNode(child, depth + 1, `${parentPath}/${node.name}`)} + {/each} +
+
+
+ {:else if node.resource} + {@const resource = node.resource} + {@const ResourceIcon = getResourceIcon(resource)} + {@const isSelected = isResourceSelected(resource)} + {@const resourceDisplayName = resource.title || getDisplayName(node.name)} +
+ {#if onToggle} + + handleCheckboxChange(resource, checked === true)} + class="h-4 w-4" + /> + {/if} + + {/if} + +
+ {/if} +{/snippet} + + + + {#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 sortTreeChildren( [...resourceTree.children.values()] ) as child (child.resource?.uri || `${serverName}:${child.name}`)} + {@render renderTreeNode(child, 1, '')} + {/each} + {/if} +
+
+
diff --git a/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/mcp-resource-browser.ts b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/mcp-resource-browser.ts new file mode 100644 index 0000000000..2bfecd914a --- /dev/null +++ b/tools/server/webui/src/lib/components/app/mcp/McpResourceBrowser/mcp-resource-browser.ts @@ -0,0 +1,97 @@ +import { Database, File, FileText, Image, Code } from '@lucide/svelte'; +import type { MCPResource, MCPResourceInfo } from '$lib/types'; + +export interface ResourceTreeNode { + name: string; + resource?: MCPResourceInfo; + children: Map; +} + +export function parseResourcePath(uri: string): string[] { + try { + const withoutProtocol = uri.replace(/^[a-z]+:\/\//, ''); + return withoutProtocol.split('/').filter((p) => p.length > 0); + } catch { + return [uri]; + } +} + +export function getDisplayName(pathPart: string): string { + const withoutExt = pathPart.replace(/\.[^.]+$/, ''); + return withoutExt + .split(/[-_]/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +export function buildResourceTree( + resourceList: MCPResource[], + serverName: string +): ResourceTreeNode { + const root: ResourceTreeNode = { name: 'root', children: new Map() }; + + for (const resource of resourceList) { + const pathParts = parseResourcePath(resource.uri); + let current = root; + + for (let i = 0; i < pathParts.length - 1; i++) { + const part = pathParts[i]; + if (!current.children.has(part)) { + current.children.set(part, { name: part, children: new Map() }); + } + current = current.children.get(part)!; + } + + const fileName = pathParts[pathParts.length - 1] || resource.name; + current.children.set(resource.uri, { + name: fileName, + resource: { ...resource, serverName }, + children: new Map() + }); + } + + return root; +} + +export function countTreeResources(node: ResourceTreeNode): number { + if (node.resource) return 1; + let count = 0; + for (const child of node.children.values()) { + count += countTreeResources(child); + } + return count; +} + +export function getResourceIcon(resource: MCPResourceInfo) { + const mimeType = resource.mimeType?.toLowerCase() || ''; + const uri = resource.uri.toLowerCase(); + + if (mimeType.startsWith('image/') || /\.(png|jpg|jpeg|gif|svg|webp)$/.test(uri)) { + return Image; + } + if ( + mimeType.includes('json') || + mimeType.includes('javascript') || + mimeType.includes('typescript') || + /\.(js|ts|json|yaml|yml|xml|html|css)$/.test(uri) + ) { + return Code; + } + if (mimeType.includes('text') || /\.(txt|md|log)$/.test(uri)) { + return FileText; + } + if (uri.includes('database') || uri.includes('db://')) { + return Database; + } + return File; +} + +export function sortTreeChildren(children: ResourceTreeNode[]): ResourceTreeNode[] { + return children.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); + }); +} 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 4f2d69d69f..594c1aeeb0 100644 --- a/tools/server/webui/src/lib/components/app/mcp/index.ts +++ b/tools/server/webui/src/lib/components/app/mcp/index.ts @@ -229,7 +229,7 @@ export { default as McpServerInfo } from './McpServerInfo.svelte'; * - Refresh all resources action * - Loading states per server */ -export { default as McpResourceBrowser } from './McpResourceBrowser.svelte'; +export { default as McpResourceBrowser } from './McpResourceBrowser/McpResourceBrowser.svelte'; /** * **McpResourcePreview** - MCP resource content preview