fix: MCP Resources Browser selection & attaching

This commit is contained in:
Aleksander Grygier 2026-02-09 01:44:00 +01:00
parent 5508ad0bee
commit 1d28b6b1be
3 changed files with 44 additions and 43 deletions

View File

@ -58,7 +58,8 @@
function handleResourceSelect(resource: MCPResourceInfo, shiftKey: boolean = false) {
if (shiftKey && lastSelectedUri) {
const allResources = getAllResourcesFlat();
// Get all resources in tree order (matching the display order in McpResourceBrowser)
const allResources = getAllResourcesFlatInTreeOrder();
const lastIndex = allResources.findIndex((r) => r.uri === lastSelectedUri);
const currentIndex = allResources.findIndex((r) => r.uri === resource.uri);
@ -87,7 +88,20 @@
lastSelectedUri = resource.uri;
}
function getAllResourcesFlat(): MCPResourceInfo[] {
function getResourceDisplayName(resource: MCPResourceInfo): string {
// Extract the display name from the resource URI (last path segment)
try {
const uriWithoutProtocol = resource.uri.replace(/^[a-z]+:\/\//, '');
const parts = uriWithoutProtocol.split('/');
return parts[parts.length - 1] || resource.name || resource.uri;
} catch {
return resource.name || resource.uri;
}
}
function getAllResourcesFlatInTreeOrder(): MCPResourceInfo[] {
// Get resources in the same order as displayed in McpResourceBrowser tree
// Sort by: folders first (if applicable), then alphabetically by display name
const allResources: MCPResourceInfo[] = [];
const resourcesMap = mcpResources();
@ -97,7 +111,17 @@
}
}
return allResources;
// Sort to match tree display order: folders first, then alphabetically
return allResources.sort((a, b) => {
const aName = getResourceDisplayName(a);
const bName = getResourceDisplayName(b);
return aName.localeCompare(bName);
});
}
function getAllResourcesFlat(): MCPResourceInfo[] {
// Fallback for other uses (like attaching)
return getAllResourcesFlatInTreeOrder();
}
async function handleAttach() {
@ -129,27 +153,11 @@
isAttaching = false;
}
}
async function handleQuickAttach(resource: MCPResourceInfo) {
isAttaching = true;
try {
await mcpStore.attachResource(resource.uri);
onAttach?.(resource);
toast.success(`Resource attached: ${resource.name}`);
} catch (error) {
console.error('Failed to attach resource:', error);
} finally {
isAttaching = false;
}
}
</script>
<Dialog.Root {open} onOpenChange={handleOpenChange}>
<Dialog.Content class="max-h-[80vh] !max-w-4xl overflow-hidden p-0">
<Dialog.Header class="border-b px-6 py-4">
<Dialog.Header class="border-b border-border/30 px-6 py-4">
<Dialog.Title class="flex items-center gap-2">
<FolderOpen class="h-5 w-5" />
@ -165,18 +173,17 @@
</Dialog.Description>
</Dialog.Header>
<div class="flex h-[500px]">
<div class="w-72 shrink-0 overflow-y-auto border-r p-4">
<div class="flex h-[500px] min-w-0">
<div class="w-72 shrink-0 overflow-y-auto border-r border-border/30 p-4">
<McpResourceBrowser
onSelect={handleResourceSelect}
onToggle={handleResourceToggle}
onAttach={handleQuickAttach}
selectedUris={selectedResources}
expandToUri={preSelectedUri}
/>
</div>
<div class="flex-1 overflow-y-auto p-4">
<div class="min-w-0 flex-1 overflow-auto p-4">
{#if selectedResources.size === 1}
{@const allResources = getAllResourcesFlat()}
{@const selectedResource = allResources.find((r) => selectedResources.has(r.uri))}
@ -194,7 +201,7 @@
</div>
</div>
<Dialog.Footer class="border-t px-6 py-4">
<Dialog.Footer class="border-t border-border/30 px-6 py-4">
<Button variant="outline" onclick={() => handleOpenChange(false)}>Cancel</Button>
<Button onclick={handleAttach} disabled={selectedResources.size === 0 || isAttaching}>

View File

@ -21,7 +21,6 @@
let {
onSelect,
onToggle,
onAttach,
selectedUris = new Set(),
expandToUri,
class: className

View File

@ -2,7 +2,6 @@
import { FolderOpen, ChevronDown, ChevronRight, Loader2 } from '@lucide/svelte';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as Collapsible from '$lib/components/ui/collapsible';
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/components/ui/utils';
import { mcpStore } from '$lib/stores/mcp.svelte';
import type { MCPResourceInfo, MCPServerResources } from '$lib/types';
@ -26,7 +25,7 @@
onToggleFolder: (folderId: string) => void;
onSelect?: (resource: MCPResourceInfo, shiftKey?: boolean) => void;
onToggle?: (resource: MCPResourceInfo, checked: boolean) => void;
onAttach?: (resource: MCPResourceInfo) => void;
searchQuery?: string;
}
let {
@ -55,11 +54,6 @@
onToggle?.(resource, checked);
}
function handleAttachClick(e: Event, resource: MCPResourceInfo) {
e.stopPropagation();
onAttach?.(resource);
}
function isResourceSelected(resource: MCPResourceInfo): boolean {
return selectedUris.has(resource.uri);
}
@ -81,10 +75,14 @@
{:else}
<ChevronRight class="h-3 w-3" />
{/if}
<FolderOpen class="h-3.5 w-3.5 text-muted-foreground" />
<span class="font-medium">{node.name}</span>
<span class="text-xs text-muted-foreground">({folderCount})</span>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="ml-4 flex flex-col gap-0.5 border-l border-border/50 pl-2">
{#each sortTreeChildren( [...node.children.values()] ) as child (child.resource?.uri || `${serverName}:${parentPath}/${node.name}/${child.name}`)}
@ -98,6 +96,7 @@
{@const ResourceIcon = getResourceIcon(resource)}
{@const isSelected = isResourceSelected(resource)}
{@const resourceDisplayName = resource.title || getDisplayName(node.name)}
<div class="group flex w-full items-center gap-2">
{#if onToggle}
<Checkbox
@ -107,6 +106,7 @@
class="h-4 w-4"
/>
{/if}
<button
class={cn(
'flex flex-1 items-center gap-2 rounded px-2 py-1 text-left text-sm transition-colors',
@ -117,19 +117,10 @@
title={resourceDisplayName}
>
<ResourceIcon class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span class="min-w-0 flex-1 truncate text-left">
{resourceDisplayName}
</span>
{#if onAttach}
<Button
variant="ghost"
size="sm"
class="h-5 px-1.5 text-xs opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100"
onclick={(e: MouseEvent) => handleAttachClick(e, resource)}
>
Attach
</Button>
{/if}
</button>
</div>
{/if}
@ -144,6 +135,7 @@
{:else}
<ChevronRight class="h-3.5 w-3.5" />
{/if}
{#if favicon}
<img
src={favicon}
@ -154,10 +146,13 @@
}}
/>
{/if}
<span class="font-medium">{displayName}</span>
<span class="text-xs text-muted-foreground">
({serverRes.resources.length})
</span>
{#if serverRes.loading}
<Loader2 class="ml-auto h-3 w-3 animate-spin text-muted-foreground" />
{/if}