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}
-{/snippet}
-
-
-
-
Available resources
-
-
- {#if isLoading}
-
- {:else}
-
- {/if}
-
-
-
-
- {#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
+
+
+ {#if isLoading}
+
+ {:else}
+
+ {/if}
+
+
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}
+ handleResourceClick(resource, e)}
+ title={resourceDisplayName}
+ >
+
+
+ {resourceDisplayName}
+
+ {#if onAttach}
+ handleAttachClick(e, resource)}
+ >
+ Attach
+
+ {/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