From d817c4691131e61a4f73c1173ca83264056a9a3d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 18 Apr 2026 18:34:32 -0400 Subject: [PATCH 1/3] Phase 2: Refactor Resources cluster to MCP SDK types Replace ad hoc local types with MCP SDK schema types across the Resources group components and ResourcesScreen, per the v2 UX interfaces plan. - ResourceListItem: accept `resource: Resource | ResourceTemplate` instead of flat `name`/`uri`; render `title ?? name`; delete local `ResourceAnnotations` type - ResourceControls: accept `Resource[]`, `ResourceTemplate[]`, `InspectorResourceSubscription[]` + `selectedUri`/`selectedTemplate` instead of `ResourceItem[]`/`TemplateListItem[]`/`SubscriptionItem[]`; no-op on re-selecting - ResourcePreviewPanel: accept `resource: Resource` + `contents: (TextResourceContents | BlobResourceContents)[]` instead of flat `content: string`; use SDK `Annotations` (audience as Role[]); `lastUpdated` as `Date` not `string` - ResourceTemplatePanel: accept `template: ResourceTemplate` instead of flat props; use SDK `Annotations` - ResourceSubscribedItem: accept `subscription: InspectorResourceSubscription` instead of flat `name`/`lastUpdated`; `lastUpdated` as `Date` - ResourcesScreen: accept `Resource[]`, `ResourceTemplate[]`, `InspectorResourceSubscription[]` + `selectedResourceUri`/ `selectedTemplateUri` + `ReadResourceState`; derive selections via `find()`; add loading/error states - Delete local `ResourceItem`, `TemplateListItem`, `SubscriptionItem`, `SelectedResource`, `SelectedTemplate`, `ResourceAnnotations` types - Update all stories with real SDK fixtures - Update ConnectedView ResourcesActive story Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ResourceControls.stories.tsx | 73 +++-- .../ResourceControls/ResourceControls.tsx | 66 ++-- .../ResourceListItem.stories.tsx | 33 +- .../ResourceListItem/ResourceListItem.tsx | 17 +- .../ResourcePreviewPanel.stories.tsx | 93 ++++-- .../ResourcePreviewPanel.tsx | 54 ++-- .../ResourceSubscribedItem.stories.tsx | 16 +- .../ResourceSubscribedItem.tsx | 18 +- .../ResourceTemplatePanel.stories.tsx | 36 ++- .../ResourceTemplatePanel.tsx | 22 +- .../ResourcesScreen.stories.tsx | 299 +++++------------- .../ResourcesScreen/ResourcesScreen.tsx | 173 +++++----- .../ConnectedView/ConnectedView.stories.tsx | 183 ++--------- 13 files changed, 466 insertions(+), 617 deletions(-) diff --git a/clients/web/src/components/groups/ResourceControls/ResourceControls.stories.tsx b/clients/web/src/components/groups/ResourceControls/ResourceControls.stories.tsx index 994eff69..42534799 100644 --- a/clients/web/src/components/groups/ResourceControls/ResourceControls.stories.tsx +++ b/clients/web/src/components/groups/ResourceControls/ResourceControls.stories.tsx @@ -1,4 +1,9 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { + Resource, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/types.js"; +import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js"; import { fn } from "storybook/test"; import { ResourceControls } from "./ResourceControls"; @@ -17,40 +22,44 @@ const meta: Meta = { export default meta; type Story = StoryObj; +const sampleResources: Resource[] = [ + { + name: "config.json", + uri: "file:///config.json", + annotations: { audience: ["user"], priority: 0.8 }, + }, + { + name: "README.md", + uri: "file:///README.md", + }, + { + name: "schema.sql", + uri: "file:///schema.sql", + }, +]; + +const sampleTemplates: ResourceTemplate[] = [ + { + name: "User Profile", + uriTemplate: "file:///users/{userId}/profile", + }, +]; + +const sampleSubscriptions: InspectorResourceSubscription[] = [ + { + resource: { + name: "config.json", + uri: "file:///config.json", + }, + lastUpdated: new Date("2026-03-17T10:30:00Z"), + }, +]; + export const Default: Story = { args: { - resources: [ - { - name: "config.json", - uri: "file:///config.json", - annotations: { audience: "developer", priority: 0.8 }, - selected: false, - }, - { - name: "README.md", - uri: "file:///README.md", - selected: false, - }, - { - name: "schema.sql", - uri: "file:///schema.sql", - selected: false, - }, - ], - templates: [ - { - name: "User Profile", - uriTemplate: "file:///users/{userId}/profile", - selected: false, - }, - ], - subscriptions: [ - { - name: "config.json", - uri: "file:///config.json", - lastUpdated: "2026-03-17T10:30:00Z", - }, - ], + resources: sampleResources, + templates: sampleTemplates, + subscriptions: sampleSubscriptions, }, }; diff --git a/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx b/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx index f6c121a2..06997fcd 100644 --- a/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx +++ b/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx @@ -7,20 +7,22 @@ import { TextInput, Title, } from "@mantine/core"; +import type { + Resource, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/types.js"; +import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js"; import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator"; import { ListToggle } from "../../elements/ListToggle/ListToggle"; import { ResourceListItem } from "../ResourceListItem/ResourceListItem"; import { ResourceSubscribedItem } from "../ResourceSubscribedItem/ResourceSubscribedItem"; -import type { - ResourceItem, - TemplateListItem, - SubscriptionItem, -} from "../../screens/ResourcesScreen/ResourcesScreen"; export interface ResourceControlsProps { - resources: ResourceItem[]; - templates: TemplateListItem[]; - subscriptions: SubscriptionItem[]; + resources: Resource[]; + templates: ResourceTemplate[]; + subscriptions: InspectorResourceSubscription[]; + selectedUri?: string; + selectedTemplate?: string; listChanged: boolean; onRefreshList: () => void; onSelectUri: (uri: string) => void; @@ -28,10 +30,6 @@ export interface ResourceControlsProps { onUnsubscribeResource: (uri: string) => void; } -// Available height for each accordion panel's scroll area. -// Subtracts: AppShell extra padding (md*2), screen padding (xl*2, already in calc), -// card padding (2*lg=40), title+search+gaps (~84), 3 accordion controls (~144), -// and per-panel accordion padding (~20px each). function panelMaxHeight(openCount: number): string { const n = Math.max(openCount, 1); const fixedChrome = 300; @@ -43,14 +41,12 @@ function formatSectionCount(label: string, count: number): string { return `${label} (${count})`; } -function templateDisplayName(item: TemplateListItem): string { - return item.title ?? item.name; -} - export function ResourceControls({ resources, templates, subscriptions, + selectedUri, + selectedTemplate, listChanged, onRefreshList, onSelectUri, @@ -62,17 +58,19 @@ export function ResourceControls({ const filteredResources = resources.filter( (r) => r.name.toLowerCase().includes(query) || + (r.title?.toLowerCase().includes(query) ?? false) || r.uri.toLowerCase().includes(query), ); const filteredTemplates = templates.filter( (t) => - templateDisplayName(t).toLowerCase().includes(query) || + t.name.toLowerCase().includes(query) || + (t.title?.toLowerCase().includes(query) ?? false) || t.uriTemplate.toLowerCase().includes(query), ); const filteredSubscriptions = subscriptions.filter( (s) => - s.name.toLowerCase().includes(query) || - s.uri.toLowerCase().includes(query), + s.resource.name.toLowerCase().includes(query) || + s.resource.uri.toLowerCase().includes(query), ); const defaultOpen = [ @@ -115,11 +113,12 @@ export function ResourceControls({ {filteredResources.map((resource) => ( onSelectUri(resource.uri)} + resource={resource} + selected={resource.uri === selectedUri} + onClick={() => { + if (resource.uri !== selectedUri) + onSelectUri(resource.uri); + }} /> ))} @@ -137,10 +136,12 @@ export function ResourceControls({ {filteredTemplates.map((template) => ( onSelectTemplate(template.uriTemplate)} + resource={template} + selected={template.uriTemplate === selectedTemplate} + onClick={() => { + if (template.uriTemplate !== selectedTemplate) + onSelectTemplate(template.uriTemplate); + }} /> ))} @@ -157,10 +158,11 @@ export function ResourceControls({ {filteredSubscriptions.map((sub) => ( onUnsubscribeResource(sub.uri)} + key={sub.resource.uri} + subscription={sub} + onUnsubscribe={() => + onUnsubscribeResource(sub.resource.uri) + } /> ))} diff --git a/clients/web/src/components/groups/ResourceListItem/ResourceListItem.stories.tsx b/clients/web/src/components/groups/ResourceListItem/ResourceListItem.stories.tsx index 3adf897c..f68dce0a 100644 --- a/clients/web/src/components/groups/ResourceListItem/ResourceListItem.stories.tsx +++ b/clients/web/src/components/groups/ResourceListItem/ResourceListItem.stories.tsx @@ -15,16 +15,41 @@ type Story = StoryObj; export const Default: Story = { args: { - name: "config.json", - uri: "file:///config.json", + resource: { + name: "config.json", + uri: "file:///config.json", + }, selected: false, }, }; export const Selected: Story = { args: { - name: "config.json", - uri: "file:///config.json", + resource: { + name: "config.json", + uri: "file:///config.json", + }, selected: true, }, }; + +export const WithTitle: Story = { + args: { + resource: { + name: "config.json", + title: "Configuration File", + uri: "file:///config.json", + }, + selected: false, + }, +}; + +export const Template: Story = { + args: { + resource: { + name: "User Profile", + uriTemplate: "file:///users/{userId}/profile", + }, + selected: false, + }, +}; diff --git a/clients/web/src/components/groups/ResourceListItem/ResourceListItem.tsx b/clients/web/src/components/groups/ResourceListItem/ResourceListItem.tsx index 9b4a93d1..3dbdf99b 100644 --- a/clients/web/src/components/groups/ResourceListItem/ResourceListItem.tsx +++ b/clients/web/src/components/groups/ResourceListItem/ResourceListItem.tsx @@ -1,20 +1,17 @@ import { Text, UnstyledButton } from "@mantine/core"; - -export interface ResourceAnnotations { - audience?: string; - priority?: number; -} +import type { + Resource, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/types.js"; export interface ResourceListItemProps { - name: string; - uri: string; - annotations?: ResourceAnnotations; + resource: Resource | ResourceTemplate; selected: boolean; onClick: () => void; } export function ResourceListItem({ - name, + resource, selected, onClick, }: ResourceListItemProps) { @@ -26,7 +23,7 @@ export function ResourceListItem({ bg={selected ? "var(--mantine-primary-color-light)" : undefined} onClick={onClick} > - {name} + {resource.title ?? resource.name} ); } diff --git a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.stories.tsx b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.stories.tsx index bc125901..b4ce17e8 100644 --- a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.stories.tsx +++ b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.stories.tsx @@ -17,57 +17,96 @@ type Story = StoryObj; export const JsonResource: Story = { args: { - uri: "file:///config.json", - mimeType: "application/json", - content: JSON.stringify( - { name: "mcp-inspector", version: "2.0.0", debug: true }, - null, - 2, - ), + resource: { + name: "config.json", + uri: "file:///config.json", + }, + contents: [ + { + uri: "file:///config.json", + mimeType: "application/json", + text: JSON.stringify( + { name: "mcp-inspector", version: "2.0.0", debug: true }, + null, + 2, + ), + }, + ], isSubscribed: false, - lastUpdated: "2026-03-17T10:30:00Z", + lastUpdated: new Date("2026-03-17T10:30:00Z"), }, }; export const TextResource: Story = { args: { - uri: "file:///readme.txt", - mimeType: "text/plain", - content: - "This is a plain text resource with some example content.\nLine two of the content.", + resource: { + name: "readme.txt", + uri: "file:///readme.txt", + }, + contents: [ + { + uri: "file:///readme.txt", + mimeType: "text/plain", + text: "This is a plain text resource with some example content.\nLine two of the content.", + }, + ], isSubscribed: false, }, }; export const Subscribed: Story = { args: { - uri: "file:///data.json", - mimeType: "application/json", - content: JSON.stringify({ status: "active" }, null, 2), + resource: { + name: "data.json", + uri: "file:///data.json", + }, + contents: [ + { + uri: "file:///data.json", + mimeType: "application/json", + text: JSON.stringify({ status: "active" }, null, 2), + }, + ], isSubscribed: true, - lastUpdated: "2026-03-17T12:00:00Z", + lastUpdated: new Date("2026-03-17T12:00:00Z"), }, }; export const NotSubscribed: Story = { args: { - uri: "file:///data.json", - mimeType: "application/json", - content: JSON.stringify({ status: "active" }, null, 2), + resource: { + name: "data.json", + uri: "file:///data.json", + }, + contents: [ + { + uri: "file:///data.json", + mimeType: "application/json", + text: JSON.stringify({ status: "active" }, null, 2), + }, + ], isSubscribed: false, }, }; export const WithAnnotations: Story = { args: { - uri: "file:///settings.json", - mimeType: "application/json", - content: JSON.stringify({ theme: "dark", lang: "en" }, null, 2), - isSubscribed: false, - annotations: { - audience: "application", - priority: 0.8, + resource: { + name: "settings.json", + uri: "file:///settings.json", + annotations: { + audience: ["user"], + priority: 0.8, + }, }, - lastUpdated: "2026-03-17T09:00:00Z", + contents: [ + { + uri: "file:///settings.json", + mimeType: "application/json", + text: JSON.stringify({ theme: "dark", lang: "en" }, null, 2), + }, + ], + isSubscribed: false, + lastUpdated: new Date("2026-03-17T09:00:00Z"), }, }; diff --git a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx index 7ee3c638..ab97194c 100644 --- a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx +++ b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx @@ -1,31 +1,40 @@ import { Button, Flex, Group, Stack, Text, Title } from "@mantine/core"; -import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js"; +import type { + BlobResourceContents, + ContentBlock, + Resource, + TextResourceContents, +} from "@modelcontextprotocol/sdk/types.js"; import { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; import { CopyButton } from "../../elements/CopyButton/CopyButton"; import { SubscribeButton } from "../../elements/SubscribeButton/SubscribeButton"; export interface ResourcePreviewPanelProps { - uri: string; - mimeType: string; - annotations?: { audience?: string; priority?: number }; - content: string; - lastUpdated?: string; + resource: Resource; + contents: (TextResourceContents | BlobResourceContents)[]; + lastUpdated?: Date; isSubscribed: boolean; onRefresh: () => void; onSubscribe: () => void; onUnsubscribe: () => void; } -function toContentBlock(content: string, mimeType: string): ContentBlock { - if (mimeType.startsWith("image/")) { - return { type: "image", data: content, mimeType }; +function toContentBlock( + item: TextResourceContents | BlobResourceContents, +): ContentBlock { + if ("text" in item) { + return { type: "text", text: item.text }; } - return { type: "text", text: content }; + const mimeType = item.mimeType ?? "application/octet-stream"; + if (mimeType.startsWith("image/")) { + return { type: "image", data: item.blob, mimeType }; + } + return { type: "text", text: item.blob }; } -function formatLastUpdated(lastUpdated: string): string { - return `Last updated: ${lastUpdated}`; +function formatLastUpdated(date: Date): string { + return `Last updated: ${date.toLocaleString()}`; } const HeaderRow = Group.withProps({ @@ -74,16 +83,18 @@ const ActionGroup = Group.withProps({ const Spacer = Flex.withProps({}); export function ResourcePreviewPanel({ - uri, - mimeType, - annotations, - content, + resource, + contents, lastUpdated, isSubscribed, onRefresh, onSubscribe, onUnsubscribe, }: ResourcePreviewPanelProps) { + const { uri, annotations } = resource; + const mimeType = + contents[0]?.mimeType ?? resource.mimeType ?? "application/octet-stream"; + return ( @@ -93,7 +104,9 @@ export function ResourcePreviewPanel({ - + {contents.map((item, index) => ( + + ))} {lastUpdated ? ( {formatLastUpdated(lastUpdated)} @@ -105,12 +118,7 @@ export function ResourcePreviewPanel({ {annotations?.audience && ( - + )} {annotations?.priority !== undefined && ( diff --git a/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.stories.tsx b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.stories.tsx index 684bfb36..b004a2d2 100644 --- a/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.stories.tsx +++ b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.stories.tsx @@ -15,13 +15,23 @@ type Story = StoryObj; export const WithTimestamp: Story = { args: { - name: "config.json", - lastUpdated: "2026-03-17T10:30:00Z", + subscription: { + resource: { + name: "config.json", + uri: "file:///config.json", + }, + lastUpdated: new Date("2026-03-17T10:30:00Z"), + }, }, }; export const WithoutTimestamp: Story = { args: { - name: "schema.sql", + subscription: { + resource: { + name: "schema.sql", + uri: "file:///schema.sql", + }, + }, }, }; diff --git a/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx index fafbc6d7..84b4d0c8 100644 --- a/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx +++ b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx @@ -1,8 +1,8 @@ import { Button, Group, Stack, Text } from "@mantine/core"; +import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js"; export interface ResourceSubscribedItemProps { - name: string; - lastUpdated?: string; + subscription: InspectorResourceSubscription; onUnsubscribe: () => void; } @@ -26,16 +26,22 @@ const ItemRow = Group.withProps({ wrap: "nowrap", }); +function formatLastUpdated(date: Date): string { + return date.toLocaleString(); +} + export function ResourceSubscribedItem({ - name, - lastUpdated, + subscription, onUnsubscribe, }: ResourceSubscribedItemProps) { + const { resource, lastUpdated } = subscription; return ( - {name} - {lastUpdated && {lastUpdated}} + {resource.title ?? resource.name} + {lastUpdated && ( + {formatLastUpdated(lastUpdated)} + )} Unsubscribe diff --git a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.stories.tsx b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.stories.tsx index 291935e1..8098dbf1 100644 --- a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.stories.tsx +++ b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.stories.tsx @@ -15,34 +15,42 @@ type Story = StoryObj; export const SingleVariable: Story = { args: { - name: "User Profile", - uriTemplate: "file:///users/{userId}/profile", - description: "Fetch a user profile by their unique identifier.", + template: { + name: "User Profile", + uriTemplate: "file:///users/{userId}/profile", + description: "Fetch a user profile by their unique identifier.", + }, }, }; export const MultipleVariables: Story = { args: { - name: "Table Row", - title: "Database Table Row", - uriTemplate: "db://tables/{tableName}/rows/{rowId}", - description: "Access a specific row in a database table.", + template: { + name: "Table Row", + title: "Database Table Row", + uriTemplate: "db://tables/{tableName}/rows/{rowId}", + description: "Access a specific row in a database table.", + }, }, }; export const WithAnnotations: Story = { args: { - name: "Dynamic Text Resource", - uriTemplate: "resource://dynamic/{resourceId}", - description: - "Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.", - annotations: { audience: "developer", priority: 0.8 }, + template: { + name: "Dynamic Text Resource", + uriTemplate: "resource://dynamic/{resourceId}", + description: + "Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.", + annotations: { audience: ["user"], priority: 0.8 }, + }, }, }; export const NoDescription: Story = { args: { - name: "Simple Template", - uriTemplate: "file:///data/{filename}", + template: { + name: "Simple Template", + uriTemplate: "file:///data/{filename}", + }, }, }; diff --git a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx index febbc60b..6e56fd11 100644 --- a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx +++ b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx @@ -1,14 +1,11 @@ import { useState, useMemo } from "react"; import { Button, Group, Stack, Text, TextInput, Title } from "@mantine/core"; +import type { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js"; import { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge"; import { CopyButton } from "../../elements/CopyButton/CopyButton"; export interface ResourceTemplatePanelProps { - name: string; - title?: string; - uriTemplate: string; - description?: string; - annotations?: { audience?: string; priority?: number }; + template: ResourceTemplate; onReadResource: (uri: string) => void; } @@ -70,13 +67,11 @@ const AnnotationGroup = Group.withProps({ }); export function ResourceTemplatePanel({ - name, - title, - uriTemplate, - description, - annotations, + template, onReadResource, }: ResourceTemplatePanelProps) { + const { name, title, uriTemplate, description, annotations } = template; + const variableNames = useMemo( () => parseVariableNames(uriTemplate), [uriTemplate], @@ -122,12 +117,7 @@ export function ResourceTemplatePanel({ {annotations?.audience && ( - + )} {annotations?.priority !== undefined && ( diff --git a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.stories.tsx b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.stories.tsx index dae741af..b6cb9c66 100644 --- a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.stories.tsx +++ b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.stories.tsx @@ -1,12 +1,12 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { + Resource, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/types.js"; +import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js"; import { fn } from "storybook/test"; import { ResourcesScreen } from "./ResourcesScreen"; -import type { - ResourceItem, - TemplateListItem, - SelectedResource, - SelectedTemplate, -} from "./ResourcesScreen"; +import type { ReadResourceState } from "./ResourcesScreen"; const meta: Meta = { title: "Screens/ResourcesScreen", @@ -28,227 +28,83 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const sampleResources: ResourceItem[] = [ +const sampleResources: Resource[] = [ { name: "config.json", uri: "file:///config.json", - annotations: { audience: "developer", priority: 0.8 }, - selected: false, - }, - { - name: "README.md", - uri: "file:///README.md", - selected: false, + annotations: { audience: ["user"], priority: 0.8 }, }, + { name: "README.md", uri: "file:///README.md" }, { name: "schema.sql", uri: "file:///schema.sql", annotations: { priority: 0.5 }, - selected: false, }, { name: "package.json", uri: "file:///package.json", - annotations: { audience: "developer" }, - selected: false, - }, - { - name: "tsconfig.json", - uri: "file:///tsconfig.json", - annotations: { audience: "developer", priority: 0.3 }, - selected: false, - }, - { - name: ".env.example", - uri: "file:///.env.example", - selected: false, + annotations: { audience: ["user"] }, }, + { name: ".env.example", uri: "file:///.env.example" }, { name: "docker-compose.yml", uri: "file:///docker-compose.yml", annotations: { priority: 0.6 }, - selected: false, - }, - { - name: "migrations/001_init.sql", - uri: "file:///migrations/001_init.sql", - annotations: { audience: "developer", priority: 0.4 }, - selected: false, - }, - { - name: "migrations/002_add_users.sql", - uri: "file:///migrations/002_add_users.sql", - annotations: { audience: "developer", priority: 0.4 }, - selected: false, - }, - { - name: "seeds/users.json", - uri: "file:///seeds/users.json", - selected: false, - }, - { - name: "seeds/products.json", - uri: "file:///seeds/products.json", - selected: false, - }, - { - name: "certs/server.pem", - uri: "file:///certs/server.pem", - annotations: { priority: 0.9 }, - selected: false, - }, - { - name: "logs/access.log", - uri: "file:///logs/access.log", - annotations: { audience: "application", priority: 0.2 }, - selected: false, - }, - { - name: "logs/error.log", - uri: "file:///logs/error.log", - annotations: { audience: "application", priority: 0.7 }, - selected: false, - }, - { - name: "api-spec.yaml", - uri: "file:///api-spec.yaml", - annotations: { audience: "developer" }, - selected: false, - }, - { - name: "CHANGELOG.md", - uri: "file:///CHANGELOG.md", - selected: false, - }, - { - name: "LICENSE", - uri: "file:///LICENSE", - selected: false, - }, - { - name: ".gitignore", - uri: "file:///.gitignore", - selected: false, - }, - { - name: "Makefile", - uri: "file:///Makefile", - annotations: { audience: "developer", priority: 0.3 }, - selected: false, - }, - { - name: "fixtures/test-data.json", - uri: "file:///fixtures/test-data.json", - selected: false, }, ]; -const sampleTemplates: TemplateListItem[] = [ +const sampleTemplates: ResourceTemplate[] = [ { name: "User Profile", uriTemplate: "file:///users/{userId}/profile", - selected: false, }, { name: "Table Row", title: "Database Table Row", uriTemplate: "db://tables/{tableName}/rows/{rowId}", - selected: false, }, { name: "Log File", title: "Application Log", uriTemplate: "file:///logs/{service}/{date}.log", - selected: false, - }, - { - name: "Migration", - uriTemplate: "file:///migrations/{version}_{name}.sql", - selected: false, - }, - { - name: "Config by Environment", - title: "Environment Config", - uriTemplate: "file:///config/{environment}.json", - selected: false, - }, - { - name: "API Endpoint", - uriTemplate: "https://api.example.com/{version}/{resource}", - selected: false, - }, - { - name: "Report", - title: "Generated Report", - uriTemplate: "reports://{reportType}/{year}/{month}", - selected: false, }, ]; -const sampleSubscriptions = [ +const sampleSubscriptions: InspectorResourceSubscription[] = [ { - name: "config.json", - uri: "file:///config.json", - lastUpdated: "2026-03-17T10:30:00Z", + resource: { name: "config.json", uri: "file:///config.json" }, + lastUpdated: new Date("2026-03-17T10:30:00Z"), }, { - name: "schema.sql", - uri: "file:///schema.sql", - lastUpdated: "2026-03-17T10:28:00Z", - }, - { - name: "docker-compose.yml", - uri: "file:///docker-compose.yml", - lastUpdated: "2026-03-17T09:45:00Z", - }, - { - name: "logs/error.log", - uri: "file:///logs/error.log", - lastUpdated: "2026-03-17T10:31:12Z", - }, - { - name: "certs/server.pem", - uri: "file:///certs/server.pem", - }, - { - name: "api-spec.yaml", - uri: "file:///api-spec.yaml", - lastUpdated: "2026-03-17T08:15:00Z", - }, - { - name: "package.json", - uri: "file:///package.json", - lastUpdated: "2026-03-17T10:22:00Z", - }, - { - name: "seeds/users.json", - uri: "file:///seeds/users.json", + resource: { name: "schema.sql", uri: "file:///schema.sql" }, + lastUpdated: new Date("2026-03-17T10:28:00Z"), }, ]; -const selectedResourceData: SelectedResource = { +const readConfigState: ReadResourceState = { + status: "ok", uri: "file:///config.json", - mimeType: "application/json", - annotations: { audience: "developer", priority: 0.8 }, - content: JSON.stringify( - { - name: "my-project", - version: "1.0.0", - settings: { debug: true, logLevel: "info" }, - }, - null, - 2, - ), - lastUpdated: "2026-03-17T10:30:00Z", + result: { + contents: [ + { + uri: "file:///config.json", + mimeType: "application/json", + text: JSON.stringify( + { + name: "my-project", + version: "1.0.0", + settings: { debug: true, logLevel: "info" }, + }, + null, + 2, + ), + }, + ], + }, + lastUpdated: new Date("2026-03-17T10:30:00Z"), isSubscribed: true, }; -const selectedTemplateData: SelectedTemplate = { - name: "User Profile", - uriTemplate: "file:///users/{userId}/profile", - description: "Fetch a user profile by their unique identifier.", -}; - export const WithResources: Story = { args: { resources: sampleResources, @@ -257,11 +113,10 @@ export const WithResources: Story = { export const ResourceSelected: Story = { args: { - resources: sampleResources.map((r) => - r.uri === "file:///config.json" ? { ...r, selected: true } : r, - ), - selectedResource: selectedResourceData, + resources: sampleResources, subscriptions: sampleSubscriptions, + selectedResourceUri: "file:///config.json", + readState: readConfigState, }, }; @@ -275,61 +130,63 @@ export const WithTemplates: Story = { export const TemplateSelected: Story = { args: { resources: sampleResources, - templates: sampleTemplates.map((t) => - t.uriTemplate === "file:///users/{userId}/profile" - ? { ...t, selected: true } - : t, - ), - selectedTemplate: selectedTemplateData, + templates: sampleTemplates, + selectedTemplateUri: "file:///users/{userId}/profile", }, }; export const TemplateWithResource: Story = { args: { resources: sampleResources, - templates: sampleTemplates.map((t) => - t.uriTemplate === "file:///users/{userId}/profile" - ? { ...t, selected: true } - : t, - ), - selectedTemplate: selectedTemplateData, - selectedResource: { + templates: sampleTemplates, + selectedTemplateUri: "file:///users/{userId}/profile", + readState: { + status: "ok", uri: "file:///users/42/profile", - mimeType: "application/json", - annotations: { audience: "developer", priority: 0.8 }, - content: JSON.stringify( - { id: 42, name: "Alice", email: "alice@example.com" }, - null, - 2, - ), - lastUpdated: "2026-03-17T11:15:00Z", + result: { + contents: [ + { + uri: "file:///users/42/profile", + mimeType: "application/json", + text: JSON.stringify( + { id: 42, name: "Alice", email: "alice@example.com" }, + null, + 2, + ), + }, + ], + }, isSubscribed: false, }, + selectedResourceUri: "file:///users/42/profile", }, }; export const AllSections: Story = { args: { resources: sampleResources, - templates: sampleTemplates.map((t) => - t.uriTemplate === "file:///users/{userId}/profile" - ? { ...t, selected: true } - : t, - ), + templates: sampleTemplates, subscriptions: sampleSubscriptions, - selectedTemplate: selectedTemplateData, - selectedResource: { + selectedTemplateUri: "file:///users/{userId}/profile", + readState: { + status: "ok", uri: "file:///users/42/profile", - mimeType: "application/json", - annotations: { audience: "developer", priority: 0.8 }, - content: JSON.stringify( - { id: 42, name: "Alice", email: "alice@example.com" }, - null, - 2, - ), - lastUpdated: "2026-03-17T11:15:00Z", + result: { + contents: [ + { + uri: "file:///users/42/profile", + mimeType: "application/json", + text: JSON.stringify( + { id: 42, name: "Alice", email: "alice@example.com" }, + null, + 2, + ), + }, + ], + }, isSubscribed: false, }, + selectedResourceUri: "file:///users/42/profile", }, }; diff --git a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx index 6cedd3d8..76039d12 100644 --- a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx +++ b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx @@ -1,51 +1,39 @@ -import { Card, Flex, Group, ScrollArea, Stack, Text } from "@mantine/core"; +import { + Alert, + Card, + Flex, + Group, + Loader, + ScrollArea, + Stack, + Text, +} from "@mantine/core"; +import type { + ReadResourceResult, + Resource, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/types.js"; +import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js"; import { ResourceControls } from "../../groups/ResourceControls/ResourceControls"; import { ResourcePreviewPanel } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel"; import { ResourceTemplatePanel } from "../../groups/ResourceTemplatePanel/ResourceTemplatePanel"; -export interface ResourceItem { - name: string; - uri: string; - annotations?: { audience?: string; priority?: number }; - selected: boolean; -} - -export interface TemplateListItem { - name: string; - title?: string; - uriTemplate: string; - selected: boolean; -} - -export interface SubscriptionItem { - name: string; - uri: string; - lastUpdated?: string; -} - -export interface SelectedResource { - uri: string; - mimeType: string; - annotations?: { audience?: string; priority?: number }; - content: string; - lastUpdated?: string; - isSubscribed: boolean; -} - -export interface SelectedTemplate { - name: string; - title?: string; - uriTemplate: string; - description?: string; - annotations?: { audience?: string; priority?: number }; +export interface ReadResourceState { + status: "idle" | "pending" | "ok" | "error"; + uri?: string; + result?: ReadResourceResult; + error?: string; + lastUpdated?: Date; + isSubscribed?: boolean; } export interface ResourcesScreenProps { - resources: ResourceItem[]; - templates: TemplateListItem[]; - subscriptions: SubscriptionItem[]; - selectedResource?: SelectedResource; - selectedTemplate?: SelectedTemplate; + resources: Resource[]; + templates: ResourceTemplate[]; + subscriptions: InspectorResourceSubscription[]; + selectedResourceUri?: string; + selectedTemplateUri?: string; + readState?: ReadResourceState; listChanged: boolean; onRefreshList: () => void; onSelectUri: (uri: string) => void; @@ -83,12 +71,16 @@ const EmptyState = Text.withProps({ py: "xl", }); +const SCROLL_MAX_HEIGHT = + "calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)"; + export function ResourcesScreen({ resources, templates, subscriptions, - selectedResource, - selectedTemplate, + selectedResourceUri, + selectedTemplateUri, + readState, listChanged, onRefreshList, onSelectUri, @@ -97,6 +89,56 @@ export function ResourcesScreen({ onSubscribeResource, onUnsubscribeResource, }: ResourcesScreenProps) { + const selectedResource = selectedResourceUri + ? resources.find((r) => r.uri === selectedResourceUri) + : undefined; + const selectedTemplate = selectedTemplateUri + ? templates.find((t) => t.uriTemplate === selectedTemplateUri) + : undefined; + + function renderReadState() { + if (!readState) return null; + + if (readState.status === "pending") { + return ( + + + + Reading resource... + + + ); + } + + if (readState.status === "error") { + return ( + + + {readState.error ?? "Failed to read resource"} + + + ); + } + + if (readState.result && selectedResource) { + return ( + + onReadResource(selectedResource.uri)} + onSubscribe={() => onSubscribeResource(selectedResource.uri)} + onUnsubscribe={() => onUnsubscribeResource(selectedResource.uri)} + /> + + ); + } + + return null; + } + return ( @@ -105,6 +147,8 @@ export function ResourcesScreen({ resources={resources} templates={templates} subscriptions={subscriptions} + selectedUri={selectedResourceUri} + selectedTemplate={selectedTemplateUri} listChanged={listChanged} onRefreshList={onRefreshList} onSelectUri={onSelectUri} @@ -116,33 +160,16 @@ export function ResourcesScreen({ {selectedTemplate ? ( - + - - {selectedResource ? ( - - onReadResource(selectedResource.uri)} - onSubscribe={() => onSubscribeResource(selectedResource.uri)} - onUnsubscribe={() => - onUnsubscribeResource(selectedResource.uri) - } - /> - - ) : ( + + {renderReadState() ?? ( Enter a URI and click Read to preview @@ -150,18 +177,12 @@ export function ResourcesScreen({ ) : selectedResource ? ( - - - onReadResource(selectedResource.uri)} - onSubscribe={() => onSubscribeResource(selectedResource.uri)} - onUnsubscribe={() => onUnsubscribeResource(selectedResource.uri)} - /> - + + {renderReadState() ?? ( + + Click to read this resource + + )} ) : ( diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx index ba8f85b9..5c70c104 100644 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx @@ -375,202 +375,79 @@ export const ResourcesActive: Story = { { name: "config.json", uri: "file:///config.json", - annotations: { audience: "developer", priority: 0.8 }, - selected: false, - }, - { - name: "README.md", - uri: "file:///README.md", - selected: false, + annotations: { audience: ["user"], priority: 0.8 }, }, + { name: "README.md", uri: "file:///README.md" }, { name: "schema.sql", uri: "file:///schema.sql", annotations: { priority: 0.5 }, - selected: false, - }, - { - name: "package.json", - uri: "file:///package.json", - annotations: { audience: "developer" }, - selected: false, - }, - { - name: "tsconfig.json", - uri: "file:///tsconfig.json", - annotations: { audience: "developer", priority: 0.3 }, - selected: false, - }, - { - name: ".env.example", - uri: "file:///.env.example", - selected: false, }, + { name: "package.json", uri: "file:///package.json" }, + { name: ".env.example", uri: "file:///.env.example" }, { name: "docker-compose.yml", uri: "file:///docker-compose.yml", annotations: { priority: 0.6 }, - selected: false, }, { name: "migrations/001_init.sql", uri: "file:///migrations/001_init.sql", - annotations: { audience: "developer", priority: 0.4 }, - selected: false, - }, - { - name: "migrations/002_add_users.sql", - uri: "file:///migrations/002_add_users.sql", - annotations: { audience: "developer", priority: 0.4 }, - selected: false, - }, - { - name: "seeds/users.json", - uri: "file:///seeds/users.json", - selected: false, - }, - { - name: "seeds/products.json", - uri: "file:///seeds/products.json", - selected: false, - }, - { - name: "certs/server.pem", - uri: "file:///certs/server.pem", - annotations: { priority: 0.9 }, - selected: false, - }, - { - name: "logs/access.log", - uri: "file:///logs/access.log", - annotations: { audience: "application", priority: 0.2 }, - selected: false, }, { name: "logs/error.log", uri: "file:///logs/error.log", - annotations: { audience: "application", priority: 0.7 }, - selected: false, - }, - { - name: "api-spec.yaml", - uri: "file:///api-spec.yaml", - annotations: { audience: "developer" }, - selected: false, - }, - { - name: "CHANGELOG.md", - uri: "file:///CHANGELOG.md", - selected: false, - }, - { - name: "LICENSE", - uri: "file:///LICENSE", - selected: false, - }, - { - name: "Makefile", - uri: "file:///Makefile", - annotations: { audience: "developer", priority: 0.3 }, - selected: false, + annotations: { priority: 0.7 }, }, + { name: "api-spec.yaml", uri: "file:///api-spec.yaml" }, + { name: "Makefile", uri: "file:///Makefile" }, ]} templates={[ { name: "User Profile", uriTemplate: "file:///users/{userId}/profile", - selected: true, + description: "Fetch a user profile by their unique identifier.", }, { name: "Table Row", title: "Database Table Row", uriTemplate: "db://tables/{tableName}/rows/{rowId}", - selected: false, }, { name: "Log File", title: "Application Log", uriTemplate: "file:///logs/{service}/{date}.log", - selected: false, - }, - { - name: "Migration", - uriTemplate: "file:///migrations/{version}_{name}.sql", - selected: false, - }, - { - name: "Config by Environment", - title: "Environment Config", - uriTemplate: "file:///config/{environment}.json", - selected: false, - }, - { - name: "API Endpoint", - uriTemplate: "https://api.example.com/{version}/{resource}", - selected: false, - }, - { - name: "Report", - title: "Generated Report", - uriTemplate: "reports://{reportType}/{year}/{month}", - selected: false, }, ]} subscriptions={[ { - name: "config.json", - uri: "file:///config.json", - lastUpdated: "2026-03-17T10:30:00Z", + resource: { name: "config.json", uri: "file:///config.json" }, + lastUpdated: new Date("2026-03-17T10:30:00Z"), }, { - name: "schema.sql", - uri: "file:///schema.sql", - lastUpdated: "2026-03-17T10:28:00Z", - }, - { - name: "docker-compose.yml", - uri: "file:///docker-compose.yml", - lastUpdated: "2026-03-17T09:45:00Z", - }, - { - name: "logs/error.log", - uri: "file:///logs/error.log", - lastUpdated: "2026-03-17T10:31:12Z", - }, - { - name: "certs/server.pem", - uri: "file:///certs/server.pem", - }, - { - name: "api-spec.yaml", - uri: "file:///api-spec.yaml", - lastUpdated: "2026-03-17T08:15:00Z", - }, - { - name: "package.json", - uri: "file:///package.json", - lastUpdated: "2026-03-17T10:22:00Z", - }, - { - name: "seeds/users.json", - uri: "file:///seeds/users.json", + resource: { name: "schema.sql", uri: "file:///schema.sql" }, + lastUpdated: new Date("2026-03-17T10:28:00Z"), }, ]} - selectedTemplate={{ - name: "User Profile", - uriTemplate: "file:///users/{userId}/profile", - description: "Fetch a user profile by their unique identifier.", - }} - selectedResource={{ + selectedTemplateUri="file:///users/{userId}/profile" + selectedResourceUri="file:///users/42/profile" + readState={{ + status: "ok", uri: "file:///users/42/profile", - mimeType: "application/json", - annotations: { audience: "developer", priority: 0.8 }, - content: JSON.stringify( - { id: 42, name: "Alice", email: "alice@example.com" }, - null, - 2, - ), - lastUpdated: "2026-03-17T11:15:00Z", + result: { + contents: [ + { + uri: "file:///users/42/profile", + mimeType: "application/json", + text: JSON.stringify( + { id: 42, name: "Alice", email: "alice@example.com" }, + null, + 2, + ), + }, + ], + }, + lastUpdated: new Date("2026-03-17T11:15:00Z"), isSubscribed: false, }} listChanged={true} From 8c9aac6df13ea6a55eba9a5ed5e29719fff09bcf Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 18 Apr 2026 18:54:48 -0400 Subject: [PATCH 2/3] Address PR #1214 code review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix template-expanded URIs not rendering: construct synthetic Resource from readState.uri when the URI is absent from the resources list - Fix non-image blob content rendering raw base64: show placeholder message; also handle audio blobs properly - Hide mimeType display when multiple content items to avoid confusion - Rename ResourceControls selectedTemplate → selectedTemplateUri for consistency with selectedUri and selectedResourceUri Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ResourceControls/ResourceControls.tsx | 8 ++++---- .../ResourcePreviewPanel.tsx | 10 ++++++++-- .../ResourcesScreen/ResourcesScreen.tsx | 20 +++++++++++++------ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx b/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx index 06997fcd..3ee2640a 100644 --- a/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx +++ b/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx @@ -22,7 +22,7 @@ export interface ResourceControlsProps { templates: ResourceTemplate[]; subscriptions: InspectorResourceSubscription[]; selectedUri?: string; - selectedTemplate?: string; + selectedTemplateUri?: string; listChanged: boolean; onRefreshList: () => void; onSelectUri: (uri: string) => void; @@ -46,7 +46,7 @@ export function ResourceControls({ templates, subscriptions, selectedUri, - selectedTemplate, + selectedTemplateUri, listChanged, onRefreshList, onSelectUri, @@ -137,9 +137,9 @@ export function ResourceControls({ { - if (template.uriTemplate !== selectedTemplate) + if (template.uriTemplate !== selectedTemplateUri) onSelectTemplate(template.uriTemplate); }} /> diff --git a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx index ab97194c..8a2a0b97 100644 --- a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx +++ b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx @@ -30,7 +30,13 @@ function toContentBlock( if (mimeType.startsWith("image/")) { return { type: "image", data: item.blob, mimeType }; } - return { type: "text", text: item.blob }; + if (mimeType.startsWith("audio/")) { + return { type: "audio", data: item.blob, mimeType }; + } + return { + type: "text", + text: `[Binary content (${mimeType}) — preview not supported]`, + }; } function formatLastUpdated(date: Date): string { @@ -113,7 +119,7 @@ export function ResourcePreviewPanel({ ) : ( )} - {mimeType} + {contents.length <= 1 && {mimeType}} diff --git a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx index 76039d12..85ce0816 100644 --- a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx +++ b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx @@ -96,6 +96,14 @@ export function ResourcesScreen({ ? templates.find((t) => t.uriTemplate === selectedTemplateUri) : undefined; + // For template-expanded URIs that don't appear in the resources list, + // construct a synthetic Resource so the preview panel can render. + const readResource: Resource | undefined = + selectedResource ?? + (readState?.uri && readState.uri === selectedResourceUri + ? { name: readState.uri, uri: readState.uri } + : undefined); + function renderReadState() { if (!readState) return null; @@ -120,17 +128,17 @@ export function ResourcesScreen({ ); } - if (readState.result && selectedResource) { + if (readState.result && readResource) { return ( onReadResource(selectedResource.uri)} - onSubscribe={() => onSubscribeResource(selectedResource.uri)} - onUnsubscribe={() => onUnsubscribeResource(selectedResource.uri)} + onRefresh={() => onReadResource(readResource.uri)} + onSubscribe={() => onSubscribeResource(readResource.uri)} + onUnsubscribe={() => onUnsubscribeResource(readResource.uri)} /> ); @@ -148,7 +156,7 @@ export function ResourcesScreen({ templates={templates} subscriptions={subscriptions} selectedUri={selectedResourceUri} - selectedTemplate={selectedTemplateUri} + selectedTemplateUri={selectedTemplateUri} listChanged={listChanged} onRefreshList={onRefreshList} onSelectUri={onSelectUri} From 995c06948cb6df9cdc89831ddbe0a4be545ce161 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 18 Apr 2026 19:13:22 -0400 Subject: [PATCH 3/3] Address PR #1214 review nits - Hoist previewUri() to a const in ResourceTemplatePanel to avoid duplicate computation - Include resource.title in subscriptions search filter for consistency with resources and templates filters Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/groups/ResourceControls/ResourceControls.tsx | 1 + .../groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx b/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx index 3ee2640a..7e612064 100644 --- a/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx +++ b/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx @@ -70,6 +70,7 @@ export function ResourceControls({ const filteredSubscriptions = subscriptions.filter( (s) => s.resource.name.toLowerCase().includes(query) || + (s.resource.title?.toLowerCase().includes(query) ?? false) || s.resource.uri.toLowerCase().includes(query), ); diff --git a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx index 6e56fd11..b7e76e30 100644 --- a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx +++ b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx @@ -91,13 +91,15 @@ export function ResourceTemplatePanel({ onReadResource(resolveUri(uriTemplate, variables)); } + const preview = previewUri(uriTemplate, variables); + return ( {title ?? name} Template - {previewUri(uriTemplate, variables)} - + {preview} + {description && {description}}