{ "name": "mcp-resource-list", "type": "registry:ui", "title": "MCP Resource List", "description": "The resources a server exposes, addressed by URI. The URI is always shown in full and is what search matches first — “SSH key” and file:///Users/me/.ssh/id_rsa are the same resource, and only one of them makes anyone look twice.", "category": "MCP", "dependencies": [ "lucide-react@^1.39.0" ], "registryDependencies": [ "badge", "empty", "input", "lib-motion", "lib-styles", "lib-utils" ], "files": [ { "path": "components/ui/mcp-resource-list.tsx", "type": "registry:ui", "content": "'use client'\n\nimport { useMemo, useState, type ComponentProps, type ReactNode } from 'react'\nimport { File, FileJson, FileText, Image as ImageIcon, Search } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Empty } from '@/components/ui/empty'\nimport { Input } from '@/components/ui/input'\nimport { enterFade } from '@/lib/motion'\nimport { focusRing, radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * The resources an MCP server exposes, addressed by URI.\n *\n * Resources are the half of MCP that is easy to under-render. A tool has a name\n * and an obvious shape; a resource is a URI, and a list that shows only the\n * human title hides which server it came from and what scheme it speaks —\n * exactly the two things that decide whether it should be in the context.\n *\n * So the URI is always shown, in full, in mono, and it is what search matches\n * first. `file:///Users/me/.ssh/id_rsa` and \"SSH key\" are the same resource;\n * only one of them makes anyone look twice.\n *\n * Selection is optional and controlled. Attaching resources to a prompt is the\n * common case, and the caller owns that set because it is usually part of a\n * larger composer.\n */\nexport type McpResource = {\n uri: string\n /** Human title. The URI is shown regardless. */\n name?: string\n description?: ReactNode\n mimeType?: string\n /** Which server offered it — matters when several are connected. */\n server?: string\n /** Bytes, when the server reports it. */\n size?: number\n}\n\nfunction iconFor(mimeType: string | undefined) {\n if (!mimeType) return File\n if (mimeType.startsWith('image/')) return ImageIcon\n if (mimeType.includes('json')) return FileJson\n if (mimeType.startsWith('text/')) return FileText\n return File\n}\n\ntype McpResourceListProps = Omit, 'onSelect'> & {\n resources: McpResource[]\n /** Selected URIs. Omit both to render a read-only catalogue. */\n value?: string[]\n onValueChange?: (next: string[]) => void\n searchable?: boolean\n searchPlaceholder?: string\n searchLabel?: string\n emptyLabel?: string\n emptyHint?: string\n /** Show the offering server on each row. */\n showServer?: boolean\n formatSize?: (bytes: number) => string\n}\n\nfunction defaultSize(bytes: number) {\n if (bytes < 1024) return `${bytes} B`\n const units = ['KB', 'MB', 'GB']\n let value = bytes / 1024\n let unit = 0\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024\n unit++\n }\n return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`\n}\n\nfunction McpResourceList({\n resources,\n value,\n onValueChange,\n searchable = true,\n searchPlaceholder = 'Filter by URI or name…',\n searchLabel = 'Filter resources',\n emptyLabel = 'No resources match',\n emptyHint = 'Try a different word, or clear the filter.',\n showServer = true,\n formatSize = defaultSize,\n className,\n ...props\n}: McpResourceListProps) {\n const [query, setQuery] = useState('')\n const selectable = value !== undefined && onValueChange !== undefined\n const selected = new Set(value ?? [])\n\n const matched = useMemo(() => {\n const needle = query.trim().toLowerCase()\n if (!needle) return resources\n return resources.filter((resource) =>\n // URI first: it is the identity, and the part worth searching.\n `${resource.uri} ${resource.name ?? ''} ${resource.mimeType ?? ''}`\n .toLowerCase()\n .includes(needle),\n )\n }, [query, resources])\n\n function toggle(uri: string) {\n if (!selectable) return\n onValueChange!(\n selected.has(uri) ? (value ?? []).filter((item) => item !== uri) : [...(value ?? []), uri],\n )\n }\n\n return (\n
\n {searchable && (\n }\n clearable\n onChange={(event) => setQuery(event.target.value)}\n />\n )}\n\n {matched.length === 0 ? (\n \n ) : (\n
    \n {matched.map((resource) => {\n const Icon = iconFor(resource.mimeType)\n const on = selected.has(resource.uri)\n const Row = selectable ? 'button' : 'div'\n\n return (\n
  • \n toggle(resource.uri),\n 'aria-pressed': on,\n }\n : {})}\n className={cn(\n 'flex w-full items-start gap-3 px-4 py-3 text-start',\n selectable && 'hover:bg-accent/50 cursor-pointer',\n selectable && focusRing,\n on && 'bg-accent/60',\n )}\n >\n \n\n
    \n {resource.name &&

    {resource.name}

    }\n \n {resource.uri}\n

    \n {resource.description && (\n

    \n {resource.description}\n

    \n )}\n
    \n\n
    \n {resource.mimeType && (\n \n {resource.mimeType}\n \n )}\n {resource.size !== undefined && (\n \n {formatSize(resource.size)}\n \n )}\n {showServer && resource.server && (\n \n {resource.server}\n \n )}\n
    \n \n
  • \n )\n })}\n
\n )}\n
\n )\n}\n\nexport { McpResourceList }\nexport type { McpResourceListProps }\n" } ] }