Merge pull request #1214 from modelcontextprotocol/v2/phase-2-resources

Phase 2: Refactor Resources cluster to MCP SDK types
This commit is contained in:
Cliff Hall 2026-04-18 19:34:15 -04:00 • committed by GitHub
commit 91e5a1257a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 485 additions and 619 deletions

View file

@ -1,4 +1,9 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; 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 { fn } from "storybook/test";
import { ResourceControls } from "./ResourceControls"; import { ResourceControls } from "./ResourceControls";
@ -17,40 +22,44 @@ const meta: Meta<typeof ResourceControls> = {
export default meta; export default meta;
type Story = StoryObj<typeof ResourceControls>; type Story = StoryObj<typeof ResourceControls>;
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 = { export const Default: Story = {
args: { args: {
resources: [ resources: sampleResources,
{ templates: sampleTemplates,
name: "config.json", subscriptions: sampleSubscriptions,
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",
},
],
}, },
}; };

View file

@ -7,20 +7,22 @@ import {
TextInput, TextInput,
Title, Title,
} from "@mantine/core"; } 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 { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { ListToggle } from "../../elements/ListToggle/ListToggle"; import { ListToggle } from "../../elements/ListToggle/ListToggle";
import { ResourceListItem } from "../ResourceListItem/ResourceListItem"; import { ResourceListItem } from "../ResourceListItem/ResourceListItem";
import { ResourceSubscribedItem } from "../ResourceSubscribedItem/ResourceSubscribedItem"; import { ResourceSubscribedItem } from "../ResourceSubscribedItem/ResourceSubscribedItem";
import type {
ResourceItem,
TemplateListItem,
SubscriptionItem,
} from "../../screens/ResourcesScreen/ResourcesScreen";
export interface ResourceControlsProps { export interface ResourceControlsProps {
resources: ResourceItem[]; resources: Resource[];
templates: TemplateListItem[]; templates: ResourceTemplate[];
subscriptions: SubscriptionItem[]; subscriptions: InspectorResourceSubscription[];
selectedUri?: string;
selectedTemplateUri?: string;
listChanged: boolean; listChanged: boolean;
onRefreshList: () => void; onRefreshList: () => void;
onSelectUri: (uri: string) => void; onSelectUri: (uri: string) => void;
@ -28,10 +30,6 @@ export interface ResourceControlsProps {
onUnsubscribeResource: (uri: string) => void; 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 { function panelMaxHeight(openCount: number): string {
const n = Math.max(openCount, 1); const n = Math.max(openCount, 1);
const fixedChrome = 300; const fixedChrome = 300;
@ -43,14 +41,12 @@ function formatSectionCount(label: string, count: number): string {
return `${label} (${count})`; return `${label} (${count})`;
} }
function templateDisplayName(item: TemplateListItem): string {
return item.title ?? item.name;
}
export function ResourceControls({ export function ResourceControls({
resources, resources,
templates, templates,
subscriptions, subscriptions,
selectedUri,
selectedTemplateUri,
listChanged, listChanged,
onRefreshList, onRefreshList,
onSelectUri, onSelectUri,
@ -62,17 +58,20 @@ export function ResourceControls({
const filteredResources = resources.filter( const filteredResources = resources.filter(
(r) => (r) =>
r.name.toLowerCase().includes(query) || r.name.toLowerCase().includes(query) ||
(r.title?.toLowerCase().includes(query) ?? false) ||
r.uri.toLowerCase().includes(query), r.uri.toLowerCase().includes(query),
); );
const filteredTemplates = templates.filter( const filteredTemplates = templates.filter(
(t) => (t) =>
templateDisplayName(t).toLowerCase().includes(query) || t.name.toLowerCase().includes(query) ||
(t.title?.toLowerCase().includes(query) ?? false) ||
t.uriTemplate.toLowerCase().includes(query), t.uriTemplate.toLowerCase().includes(query),
); );
const filteredSubscriptions = subscriptions.filter( const filteredSubscriptions = subscriptions.filter(
(s) => (s) =>
s.name.toLowerCase().includes(query) || s.resource.name.toLowerCase().includes(query) ||
s.uri.toLowerCase().includes(query), (s.resource.title?.toLowerCase().includes(query) ?? false) ||
s.resource.uri.toLowerCase().includes(query),
); );
const defaultOpen = [ const defaultOpen = [
@ -115,11 +114,12 @@ export function ResourceControls({
{filteredResources.map((resource) => ( {filteredResources.map((resource) => (
<ResourceListItem <ResourceListItem
key={resource.uri} key={resource.uri}
name={resource.name} resource={resource}
uri={resource.uri} selected={resource.uri === selectedUri}
annotations={resource.annotations} onClick={() => {
selected={resource.selected} if (resource.uri !== selectedUri)
onClick={() => onSelectUri(resource.uri)} onSelectUri(resource.uri);
}}
/> />
))} ))}
</Stack> </Stack>
@ -137,10 +137,12 @@ export function ResourceControls({
{filteredTemplates.map((template) => ( {filteredTemplates.map((template) => (
<ResourceListItem <ResourceListItem
key={template.uriTemplate} key={template.uriTemplate}
name={templateDisplayName(template)} resource={template}
uri={template.uriTemplate} selected={template.uriTemplate === selectedTemplateUri}
selected={template.selected} onClick={() => {
onClick={() => onSelectTemplate(template.uriTemplate)} if (template.uriTemplate !== selectedTemplateUri)
onSelectTemplate(template.uriTemplate);
}}
/> />
))} ))}
</Stack> </Stack>
@ -157,10 +159,11 @@ export function ResourceControls({
<Stack gap="xs"> <Stack gap="xs">
{filteredSubscriptions.map((sub) => ( {filteredSubscriptions.map((sub) => (
<ResourceSubscribedItem <ResourceSubscribedItem
key={sub.name} key={sub.resource.uri}
name={sub.name} subscription={sub}
lastUpdated={sub.lastUpdated} onUnsubscribe={() =>
onUnsubscribe={() => onUnsubscribeResource(sub.uri)} onUnsubscribeResource(sub.resource.uri)
}
/> />
))} ))}
</Stack> </Stack>

View file

@ -15,16 +15,41 @@ type Story = StoryObj<typeof ResourceListItem>;
export const Default: Story = { export const Default: Story = {
args: { args: {
name: "config.json", resource: {
uri: "file:///config.json", name: "config.json",
uri: "file:///config.json",
},
selected: false, selected: false,
}, },
}; };
export const Selected: Story = { export const Selected: Story = {
args: { args: {
name: "config.json", resource: {
uri: "file:///config.json", name: "config.json",
uri: "file:///config.json",
},
selected: true, 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,
},
};

View file

@ -1,20 +1,17 @@
import { Text, UnstyledButton } from "@mantine/core"; import { Text, UnstyledButton } from "@mantine/core";
import type {
export interface ResourceAnnotations { Resource,
audience?: string; ResourceTemplate,
priority?: number; } from "@modelcontextprotocol/sdk/types.js";
}
export interface ResourceListItemProps { export interface ResourceListItemProps {
name: string; resource: Resource | ResourceTemplate;
uri: string;
annotations?: ResourceAnnotations;
selected: boolean; selected: boolean;
onClick: () => void; onClick: () => void;
} }
export function ResourceListItem({ export function ResourceListItem({
name, resource,
selected, selected,
onClick, onClick,
}: ResourceListItemProps) { }: ResourceListItemProps) {
@ -26,7 +23,7 @@ export function ResourceListItem({
bg={selected ? "var(--mantine-primary-color-light)" : undefined} bg={selected ? "var(--mantine-primary-color-light)" : undefined}
onClick={onClick} onClick={onClick}
> >
<Text fw={500}>{name}</Text> <Text fw={500}>{resource.title ?? resource.name}</Text>
</UnstyledButton> </UnstyledButton>
); );
} }

View file

@ -17,57 +17,96 @@ type Story = StoryObj<typeof ResourcePreviewPanel>;
export const JsonResource: Story = { export const JsonResource: Story = {
args: { args: {
uri: "file:///config.json", resource: {
mimeType: "application/json", name: "config.json",
content: JSON.stringify( uri: "file:///config.json",
{ name: "mcp-inspector", version: "2.0.0", debug: true }, },
null, contents: [
2, {
), uri: "file:///config.json",
mimeType: "application/json",
text: JSON.stringify(
{ name: "mcp-inspector", version: "2.0.0", debug: true },
null,
2,
),
},
],
isSubscribed: false, isSubscribed: false,
lastUpdated: "2026-03-17T10:30:00Z", lastUpdated: new Date("2026-03-17T10:30:00Z"),
}, },
}; };
export const TextResource: Story = { export const TextResource: Story = {
args: { args: {
uri: "file:///readme.txt", resource: {
mimeType: "text/plain", name: "readme.txt",
content: uri: "file:///readme.txt",
"This is a plain text resource with some example content.\nLine two of the content.", },
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, isSubscribed: false,
}, },
}; };
export const Subscribed: Story = { export const Subscribed: Story = {
args: { args: {
uri: "file:///data.json", resource: {
mimeType: "application/json", name: "data.json",
content: JSON.stringify({ status: "active" }, null, 2), uri: "file:///data.json",
},
contents: [
{
uri: "file:///data.json",
mimeType: "application/json",
text: JSON.stringify({ status: "active" }, null, 2),
},
],
isSubscribed: true, isSubscribed: true,
lastUpdated: "2026-03-17T12:00:00Z", lastUpdated: new Date("2026-03-17T12:00:00Z"),
}, },
}; };
export const NotSubscribed: Story = { export const NotSubscribed: Story = {
args: { args: {
uri: "file:///data.json", resource: {
mimeType: "application/json", name: "data.json",
content: JSON.stringify({ status: "active" }, null, 2), uri: "file:///data.json",
},
contents: [
{
uri: "file:///data.json",
mimeType: "application/json",
text: JSON.stringify({ status: "active" }, null, 2),
},
],
isSubscribed: false, isSubscribed: false,
}, },
}; };
export const WithAnnotations: Story = { export const WithAnnotations: Story = {
args: { args: {
uri: "file:///settings.json", resource: {
mimeType: "application/json", name: "settings.json",
content: JSON.stringify({ theme: "dark", lang: "en" }, null, 2), uri: "file:///settings.json",
isSubscribed: false, annotations: {
annotations: { audience: ["user"],
audience: "application", priority: 0.8,
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"),
}, },
}; };

View file

@ -1,31 +1,46 @@
import { Button, Flex, Group, Stack, Text, Title } from "@mantine/core"; 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 { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge";
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
import { CopyButton } from "../../elements/CopyButton/CopyButton"; import { CopyButton } from "../../elements/CopyButton/CopyButton";
import { SubscribeButton } from "../../elements/SubscribeButton/SubscribeButton"; import { SubscribeButton } from "../../elements/SubscribeButton/SubscribeButton";
export interface ResourcePreviewPanelProps { export interface ResourcePreviewPanelProps {
uri: string; resource: Resource;
mimeType: string; contents: (TextResourceContents | BlobResourceContents)[];
annotations?: { audience?: string; priority?: number }; lastUpdated?: Date;
content: string;
lastUpdated?: string;
isSubscribed: boolean; isSubscribed: boolean;
onRefresh: () => void; onRefresh: () => void;
onSubscribe: () => void; onSubscribe: () => void;
onUnsubscribe: () => void; onUnsubscribe: () => void;
} }
function toContentBlock(content: string, mimeType: string): ContentBlock { function toContentBlock(
if (mimeType.startsWith("image/")) { item: TextResourceContents | BlobResourceContents,
return { type: "image", data: content, mimeType }; ): 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 };
}
if (mimeType.startsWith("audio/")) {
return { type: "audio", data: item.blob, mimeType };
}
return {
type: "text",
text: `[Binary content (${mimeType}) — preview not supported]`,
};
} }
function formatLastUpdated(lastUpdated: string): string { function formatLastUpdated(date: Date): string {
return `Last updated: ${lastUpdated}`; return `Last updated: ${date.toLocaleString()}`;
} }
const HeaderRow = Group.withProps({ const HeaderRow = Group.withProps({
@ -74,16 +89,18 @@ const ActionGroup = Group.withProps({
const Spacer = Flex.withProps({}); const Spacer = Flex.withProps({});
export function ResourcePreviewPanel({ export function ResourcePreviewPanel({
uri, resource,
mimeType, contents,
annotations,
content,
lastUpdated, lastUpdated,
isSubscribed, isSubscribed,
onRefresh, onRefresh,
onSubscribe, onSubscribe,
onUnsubscribe, onUnsubscribe,
}: ResourcePreviewPanelProps) { }: ResourcePreviewPanelProps) {
const { uri, annotations } = resource;
const mimeType =
contents[0]?.mimeType ?? resource.mimeType ?? "application/octet-stream";
return ( return (
<Stack gap="md"> <Stack gap="md">
<HeaderRow> <HeaderRow>
@ -93,24 +110,21 @@ export function ResourcePreviewPanel({
<CopyButton value={uri} /> <CopyButton value={uri} />
</UriGroup> </UriGroup>
</HeaderRow> </HeaderRow>
<ContentViewer block={toContentBlock(content, mimeType)} copyable /> {contents.map((item, index) => (
<ContentViewer key={index} block={toContentBlock(item)} copyable />
))}
<MetaRow> <MetaRow>
{lastUpdated ? ( {lastUpdated ? (
<TimestampText>{formatLastUpdated(lastUpdated)}</TimestampText> <TimestampText>{formatLastUpdated(lastUpdated)}</TimestampText>
) : ( ) : (
<Spacer /> <Spacer />
)} )}
<MimeText>{mimeType}</MimeText> {contents.length <= 1 && <MimeText>{mimeType}</MimeText>}
</MetaRow> </MetaRow>
<FooterRow> <FooterRow>
<AnnotationGroup> <AnnotationGroup>
{annotations?.audience && ( {annotations?.audience && (
<AnnotationBadge <AnnotationBadge facet="audience" value={annotations.audience} />
facet="audience"
value={
annotations.audience.split(", ") as ("user" | "assistant")[]
}
/>
)} )}
{annotations?.priority !== undefined && ( {annotations?.priority !== undefined && (
<AnnotationBadge facet="priority" value={annotations.priority} /> <AnnotationBadge facet="priority" value={annotations.priority} />

View file

@ -15,13 +15,23 @@ type Story = StoryObj<typeof ResourceSubscribedItem>;
export const WithTimestamp: Story = { export const WithTimestamp: Story = {
args: { args: {
name: "config.json", subscription: {
lastUpdated: "2026-03-17T10:30:00Z", resource: {
name: "config.json",
uri: "file:///config.json",
},
lastUpdated: new Date("2026-03-17T10:30:00Z"),
},
}, },
}; };
export const WithoutTimestamp: Story = { export const WithoutTimestamp: Story = {
args: { args: {
name: "schema.sql", subscription: {
resource: {
name: "schema.sql",
uri: "file:///schema.sql",
},
},
}, },
}; };

View file

@ -1,8 +1,8 @@
import { Button, Group, Stack, Text } from "@mantine/core"; import { Button, Group, Stack, Text } from "@mantine/core";
import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js";
export interface ResourceSubscribedItemProps { export interface ResourceSubscribedItemProps {
name: string; subscription: InspectorResourceSubscription;
lastUpdated?: string;
onUnsubscribe: () => void; onUnsubscribe: () => void;
} }
@ -26,16 +26,22 @@ const ItemRow = Group.withProps({
wrap: "nowrap", wrap: "nowrap",
}); });
function formatLastUpdated(date: Date): string {
return date.toLocaleString();
}
export function ResourceSubscribedItem({ export function ResourceSubscribedItem({
name, subscription,
lastUpdated,
onUnsubscribe, onUnsubscribe,
}: ResourceSubscribedItemProps) { }: ResourceSubscribedItemProps) {
const { resource, lastUpdated } = subscription;
return ( return (
<ItemRow> <ItemRow>
<Stack gap={2}> <Stack gap={2}>
<NameText>{name}</NameText> <NameText>{resource.title ?? resource.name}</NameText>
{lastUpdated && <TimestampText>{lastUpdated}</TimestampText>} {lastUpdated && (
<TimestampText>{formatLastUpdated(lastUpdated)}</TimestampText>
)}
</Stack> </Stack>
<SubtleButton onClick={onUnsubscribe}>Unsubscribe</SubtleButton> <SubtleButton onClick={onUnsubscribe}>Unsubscribe</SubtleButton>
</ItemRow> </ItemRow>

View file

@ -15,34 +15,42 @@ type Story = StoryObj<typeof ResourceTemplatePanel>;
export const SingleVariable: Story = { export const SingleVariable: Story = {
args: { args: {
name: "User Profile", template: {
uriTemplate: "file:///users/{userId}/profile", name: "User Profile",
description: "Fetch a user profile by their unique identifier.", uriTemplate: "file:///users/{userId}/profile",
description: "Fetch a user profile by their unique identifier.",
},
}, },
}; };
export const MultipleVariables: Story = { export const MultipleVariables: Story = {
args: { args: {
name: "Table Row", template: {
title: "Database Table Row", name: "Table Row",
uriTemplate: "db://tables/{tableName}/rows/{rowId}", title: "Database Table Row",
description: "Access a specific row in a database table.", uriTemplate: "db://tables/{tableName}/rows/{rowId}",
description: "Access a specific row in a database table.",
},
}, },
}; };
export const WithAnnotations: Story = { export const WithAnnotations: Story = {
args: { args: {
name: "Dynamic Text Resource", template: {
uriTemplate: "resource://dynamic/{resourceId}", name: "Dynamic Text Resource",
description: uriTemplate: "resource://dynamic/{resourceId}",
"Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.", description:
annotations: { audience: "developer", priority: 0.8 }, "Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.",
annotations: { audience: ["user"], priority: 0.8 },
},
}, },
}; };
export const NoDescription: Story = { export const NoDescription: Story = {
args: { args: {
name: "Simple Template", template: {
uriTemplate: "file:///data/{filename}", name: "Simple Template",
uriTemplate: "file:///data/{filename}",
},
}, },
}; };

View file

@ -1,14 +1,11 @@
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { Button, Group, Stack, Text, TextInput, Title } from "@mantine/core"; 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 { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge";
import { CopyButton } from "../../elements/CopyButton/CopyButton"; import { CopyButton } from "../../elements/CopyButton/CopyButton";
export interface ResourceTemplatePanelProps { export interface ResourceTemplatePanelProps {
name: string; template: ResourceTemplate;
title?: string;
uriTemplate: string;
description?: string;
annotations?: { audience?: string; priority?: number };
onReadResource: (uri: string) => void; onReadResource: (uri: string) => void;
} }
@ -70,13 +67,11 @@ const AnnotationGroup = Group.withProps({
}); });
export function ResourceTemplatePanel({ export function ResourceTemplatePanel({
name, template,
title,
uriTemplate,
description,
annotations,
onReadResource, onReadResource,
}: ResourceTemplatePanelProps) { }: ResourceTemplatePanelProps) {
const { name, title, uriTemplate, description, annotations } = template;
const variableNames = useMemo( const variableNames = useMemo(
() => parseVariableNames(uriTemplate), () => parseVariableNames(uriTemplate),
[uriTemplate], [uriTemplate],
@ -96,13 +91,15 @@ export function ResourceTemplatePanel({
onReadResource(resolveUri(uriTemplate, variables)); onReadResource(resolveUri(uriTemplate, variables));
} }
const preview = previewUri(uriTemplate, variables);
return ( return (
<Stack gap="md"> <Stack gap="md">
<HeaderRow> <HeaderRow>
<Title order={4}>{title ?? name} Template</Title> <Title order={4}>{title ?? name} Template</Title>
<UriGroup> <UriGroup>
<UriText>{previewUri(uriTemplate, variables)}</UriText> <UriText>{preview}</UriText>
<CopyButton value={previewUri(uriTemplate, variables)} /> <CopyButton value={preview} />
</UriGroup> </UriGroup>
</HeaderRow> </HeaderRow>
{description && <DescriptionText>{description}</DescriptionText>} {description && <DescriptionText>{description}</DescriptionText>}
@ -122,12 +119,7 @@ export function ResourceTemplatePanel({
<FooterRow> <FooterRow>
<AnnotationGroup> <AnnotationGroup>
{annotations?.audience && ( {annotations?.audience && (
<AnnotationBadge <AnnotationBadge facet="audience" value={annotations.audience} />
facet="audience"
value={
annotations.audience.split(", ") as ("user" | "assistant")[]
}
/>
)} )}
{annotations?.priority !== undefined && ( {annotations?.priority !== undefined && (
<AnnotationBadge facet="priority" value={annotations.priority} /> <AnnotationBadge facet="priority" value={annotations.priority} />

View file

@ -1,12 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; 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 { fn } from "storybook/test";
import { ResourcesScreen } from "./ResourcesScreen"; import { ResourcesScreen } from "./ResourcesScreen";
import type { import type { ReadResourceState } from "./ResourcesScreen";
ResourceItem,
TemplateListItem,
SelectedResource,
SelectedTemplate,
} from "./ResourcesScreen";
const meta: Meta<typeof ResourcesScreen> = { const meta: Meta<typeof ResourcesScreen> = {
title: "Screens/ResourcesScreen", title: "Screens/ResourcesScreen",
@ -28,227 +28,83 @@ const meta: Meta<typeof ResourcesScreen> = {
export default meta; export default meta;
type Story = StoryObj<typeof ResourcesScreen>; type Story = StoryObj<typeof ResourcesScreen>;
const sampleResources: ResourceItem[] = [ const sampleResources: Resource[] = [
{ {
name: "config.json", name: "config.json",
uri: "file:///config.json", uri: "file:///config.json",
annotations: { audience: "developer", priority: 0.8 }, annotations: { audience: ["user"], priority: 0.8 },
selected: false,
},
{
name: "README.md",
uri: "file:///README.md",
selected: false,
}, },
{ name: "README.md", uri: "file:///README.md" },
{ {
name: "schema.sql", name: "schema.sql",
uri: "file:///schema.sql", uri: "file:///schema.sql",
annotations: { priority: 0.5 }, annotations: { priority: 0.5 },
selected: false,
}, },
{ {
name: "package.json", name: "package.json",
uri: "file:///package.json", uri: "file:///package.json",
annotations: { audience: "developer" }, annotations: { audience: ["user"] },
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: ".env.example", uri: "file:///.env.example" },
{ {
name: "docker-compose.yml", name: "docker-compose.yml",
uri: "file:///docker-compose.yml", uri: "file:///docker-compose.yml",
annotations: { priority: 0.6 }, 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", name: "User Profile",
uriTemplate: "file:///users/{userId}/profile", uriTemplate: "file:///users/{userId}/profile",
selected: false,
}, },
{ {
name: "Table Row", name: "Table Row",
title: "Database Table Row", title: "Database Table Row",
uriTemplate: "db://tables/{tableName}/rows/{rowId}", uriTemplate: "db://tables/{tableName}/rows/{rowId}",
selected: false,
}, },
{ {
name: "Log File", name: "Log File",
title: "Application Log", title: "Application Log",
uriTemplate: "file:///logs/{service}/{date}.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", resource: { name: "config.json", uri: "file:///config.json" },
uri: "file:///config.json", lastUpdated: new Date("2026-03-17T10:30:00Z"),
lastUpdated: "2026-03-17T10:30:00Z",
}, },
{ {
name: "schema.sql", resource: { name: "schema.sql", uri: "file:///schema.sql" },
uri: "file:///schema.sql", lastUpdated: new Date("2026-03-17T10:28:00Z"),
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",
}, },
]; ];
const selectedResourceData: SelectedResource = { const readConfigState: ReadResourceState = {
status: "ok",
uri: "file:///config.json", uri: "file:///config.json",
mimeType: "application/json", result: {
annotations: { audience: "developer", priority: 0.8 }, contents: [
content: JSON.stringify( {
{ uri: "file:///config.json",
name: "my-project", mimeType: "application/json",
version: "1.0.0", text: JSON.stringify(
settings: { debug: true, logLevel: "info" }, {
}, name: "my-project",
null, version: "1.0.0",
2, settings: { debug: true, logLevel: "info" },
), },
lastUpdated: "2026-03-17T10:30:00Z", null,
2,
),
},
],
},
lastUpdated: new Date("2026-03-17T10:30:00Z"),
isSubscribed: true, 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 = { export const WithResources: Story = {
args: { args: {
resources: sampleResources, resources: sampleResources,
@ -257,11 +113,10 @@ export const WithResources: Story = {
export const ResourceSelected: Story = { export const ResourceSelected: Story = {
args: { args: {
resources: sampleResources.map((r) => resources: sampleResources,
r.uri === "file:///config.json" ? { ...r, selected: true } : r,
),
selectedResource: selectedResourceData,
subscriptions: sampleSubscriptions, subscriptions: sampleSubscriptions,
selectedResourceUri: "file:///config.json",
readState: readConfigState,
}, },
}; };
@ -275,61 +130,63 @@ export const WithTemplates: Story = {
export const TemplateSelected: Story = { export const TemplateSelected: Story = {
args: { args: {
resources: sampleResources, resources: sampleResources,
templates: sampleTemplates.map((t) => templates: sampleTemplates,
t.uriTemplate === "file:///users/{userId}/profile" selectedTemplateUri: "file:///users/{userId}/profile",
? { ...t, selected: true }
: t,
),
selectedTemplate: selectedTemplateData,
}, },
}; };
export const TemplateWithResource: Story = { export const TemplateWithResource: Story = {
args: { args: {
resources: sampleResources, resources: sampleResources,
templates: sampleTemplates.map((t) => templates: sampleTemplates,
t.uriTemplate === "file:///users/{userId}/profile" selectedTemplateUri: "file:///users/{userId}/profile",
? { ...t, selected: true } readState: {
: t, status: "ok",
),
selectedTemplate: selectedTemplateData,
selectedResource: {
uri: "file:///users/42/profile", uri: "file:///users/42/profile",
mimeType: "application/json", result: {
annotations: { audience: "developer", priority: 0.8 }, contents: [
content: JSON.stringify( {
{ id: 42, name: "Alice", email: "alice@example.com" }, uri: "file:///users/42/profile",
null, mimeType: "application/json",
2, text: JSON.stringify(
), { id: 42, name: "Alice", email: "alice@example.com" },
lastUpdated: "2026-03-17T11:15:00Z", null,
2,
),
},
],
},
isSubscribed: false, isSubscribed: false,
}, },
selectedResourceUri: "file:///users/42/profile",
}, },
}; };
export const AllSections: Story = { export const AllSections: Story = {
args: { args: {
resources: sampleResources, resources: sampleResources,
templates: sampleTemplates.map((t) => templates: sampleTemplates,
t.uriTemplate === "file:///users/{userId}/profile"
? { ...t, selected: true }
: t,
),
subscriptions: sampleSubscriptions, subscriptions: sampleSubscriptions,
selectedTemplate: selectedTemplateData, selectedTemplateUri: "file:///users/{userId}/profile",
selectedResource: { readState: {
status: "ok",
uri: "file:///users/42/profile", uri: "file:///users/42/profile",
mimeType: "application/json", result: {
annotations: { audience: "developer", priority: 0.8 }, contents: [
content: JSON.stringify( {
{ id: 42, name: "Alice", email: "alice@example.com" }, uri: "file:///users/42/profile",
null, mimeType: "application/json",
2, text: JSON.stringify(
), { id: 42, name: "Alice", email: "alice@example.com" },
lastUpdated: "2026-03-17T11:15:00Z", null,
2,
),
},
],
},
isSubscribed: false, isSubscribed: false,
}, },
selectedResourceUri: "file:///users/42/profile",
}, },
}; };

View file

@ -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 { ResourceControls } from "../../groups/ResourceControls/ResourceControls";
import { ResourcePreviewPanel } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel"; import { ResourcePreviewPanel } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel";
import { ResourceTemplatePanel } from "../../groups/ResourceTemplatePanel/ResourceTemplatePanel"; import { ResourceTemplatePanel } from "../../groups/ResourceTemplatePanel/ResourceTemplatePanel";
export interface ResourceItem { export interface ReadResourceState {
name: string; status: "idle" | "pending" | "ok" | "error";
uri: string; uri?: string;
annotations?: { audience?: string; priority?: number }; result?: ReadResourceResult;
selected: boolean; error?: string;
} lastUpdated?: Date;
isSubscribed?: 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 ResourcesScreenProps { export interface ResourcesScreenProps {
resources: ResourceItem[]; resources: Resource[];
templates: TemplateListItem[]; templates: ResourceTemplate[];
subscriptions: SubscriptionItem[]; subscriptions: InspectorResourceSubscription[];
selectedResource?: SelectedResource; selectedResourceUri?: string;
selectedTemplate?: SelectedTemplate; selectedTemplateUri?: string;
readState?: ReadResourceState;
listChanged: boolean; listChanged: boolean;
onRefreshList: () => void; onRefreshList: () => void;
onSelectUri: (uri: string) => void; onSelectUri: (uri: string) => void;
@ -83,12 +71,16 @@ const EmptyState = Text.withProps({
py: "xl", py: "xl",
}); });
const SCROLL_MAX_HEIGHT =
"calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)";
export function ResourcesScreen({ export function ResourcesScreen({
resources, resources,
templates, templates,
subscriptions, subscriptions,
selectedResource, selectedResourceUri,
selectedTemplate, selectedTemplateUri,
readState,
listChanged, listChanged,
onRefreshList, onRefreshList,
onSelectUri, onSelectUri,
@ -97,6 +89,64 @@ export function ResourcesScreen({
onSubscribeResource, onSubscribeResource,
onUnsubscribeResource, onUnsubscribeResource,
}: ResourcesScreenProps) { }: ResourcesScreenProps) {
const selectedResource = selectedResourceUri
? resources.find((r) => r.uri === selectedResourceUri)
: undefined;
const selectedTemplate = selectedTemplateUri
? 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;
if (readState.status === "pending") {
return (
<DetailCard>
<Stack align="center" py="xl">
<Loader size="sm" />
<Text c="dimmed">Reading resource...</Text>
</Stack>
</DetailCard>
);
}
if (readState.status === "error") {
return (
<DetailCard>
<Alert color="red" variant="light" title="Read Error">
{readState.error ?? "Failed to read resource"}
</Alert>
</DetailCard>
);
}
if (readState.result && readResource) {
return (
<DetailCard>
<ResourcePreviewPanel
resource={readResource}
contents={readState.result.contents}
lastUpdated={readState.lastUpdated}
isSubscribed={readState.isSubscribed ?? false}
onRefresh={() => onReadResource(readResource.uri)}
onSubscribe={() => onSubscribeResource(readResource.uri)}
onUnsubscribe={() => onUnsubscribeResource(readResource.uri)}
/>
</DetailCard>
);
}
return null;
}
return ( return (
<ScreenLayout> <ScreenLayout>
<Sidebar> <Sidebar>
@ -105,6 +155,8 @@ export function ResourcesScreen({
resources={resources} resources={resources}
templates={templates} templates={templates}
subscriptions={subscriptions} subscriptions={subscriptions}
selectedUri={selectedResourceUri}
selectedTemplateUri={selectedTemplateUri}
listChanged={listChanged} listChanged={listChanged}
onRefreshList={onRefreshList} onRefreshList={onRefreshList}
onSelectUri={onSelectUri} onSelectUri={onSelectUri}
@ -116,33 +168,16 @@ export function ResourcesScreen({
{selectedTemplate ? ( {selectedTemplate ? (
<Group flex={1} gap="md" align="flex-start" wrap="nowrap"> <Group flex={1} gap="md" align="flex-start" wrap="nowrap">
<ScrollArea.Autosize <ScrollArea.Autosize flex={1} mah={SCROLL_MAX_HEIGHT}>
flex={1}
mah="calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)"
>
<DetailCard> <DetailCard>
<ResourceTemplatePanel <ResourceTemplatePanel
{...selectedTemplate} template={selectedTemplate}
onReadResource={onReadResource} onReadResource={onReadResource}
/> />
</DetailCard> </DetailCard>
</ScrollArea.Autosize> </ScrollArea.Autosize>
<ScrollArea.Autosize <ScrollArea.Autosize flex={1} mah={SCROLL_MAX_HEIGHT}>
flex={1} {renderReadState() ?? (
mah="calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)"
>
{selectedResource ? (
<DetailCard>
<ResourcePreviewPanel
{...selectedResource}
onRefresh={() => onReadResource(selectedResource.uri)}
onSubscribe={() => onSubscribeResource(selectedResource.uri)}
onUnsubscribe={() =>
onUnsubscribeResource(selectedResource.uri)
}
/>
</DetailCard>
) : (
<DetailCard> <DetailCard>
<EmptyState>Enter a URI and click Read to preview</EmptyState> <EmptyState>Enter a URI and click Read to preview</EmptyState>
</DetailCard> </DetailCard>
@ -150,18 +185,12 @@ export function ResourcesScreen({
</ScrollArea.Autosize> </ScrollArea.Autosize>
</Group> </Group>
) : selectedResource ? ( ) : selectedResource ? (
<ScrollArea.Autosize <ScrollArea.Autosize flex={1} mah={SCROLL_MAX_HEIGHT}>
flex={1} {renderReadState() ?? (
mah="calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)" <DetailCard>
> <EmptyState>Click to read this resource</EmptyState>
<DetailCard> </DetailCard>
<ResourcePreviewPanel )}
{...selectedResource}
onRefresh={() => onReadResource(selectedResource.uri)}
onSubscribe={() => onSubscribeResource(selectedResource.uri)}
onUnsubscribe={() => onUnsubscribeResource(selectedResource.uri)}
/>
</DetailCard>
</ScrollArea.Autosize> </ScrollArea.Autosize>
) : ( ) : (
<DetailCard flex={1}> <DetailCard flex={1}>

View file

@ -375,202 +375,79 @@ export const ResourcesActive: Story = {
{ {
name: "config.json", name: "config.json",
uri: "file:///config.json", uri: "file:///config.json",
annotations: { audience: "developer", priority: 0.8 }, annotations: { audience: ["user"], priority: 0.8 },
selected: false,
},
{
name: "README.md",
uri: "file:///README.md",
selected: false,
}, },
{ name: "README.md", uri: "file:///README.md" },
{ {
name: "schema.sql", name: "schema.sql",
uri: "file:///schema.sql", uri: "file:///schema.sql",
annotations: { priority: 0.5 }, 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", name: "docker-compose.yml",
uri: "file:///docker-compose.yml", uri: "file:///docker-compose.yml",
annotations: { priority: 0.6 }, annotations: { priority: 0.6 },
selected: false,
}, },
{ {
name: "migrations/001_init.sql", name: "migrations/001_init.sql",
uri: "file:///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", name: "logs/error.log",
uri: "file:///logs/error.log", uri: "file:///logs/error.log",
annotations: { audience: "application", priority: 0.7 }, annotations: { 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,
}, },
{ name: "api-spec.yaml", uri: "file:///api-spec.yaml" },
{ name: "Makefile", uri: "file:///Makefile" },
]} ]}
templates={[ templates={[
{ {
name: "User Profile", name: "User Profile",
uriTemplate: "file:///users/{userId}/profile", uriTemplate: "file:///users/{userId}/profile",
selected: true, description: "Fetch a user profile by their unique identifier.",
}, },
{ {
name: "Table Row", name: "Table Row",
title: "Database Table Row", title: "Database Table Row",
uriTemplate: "db://tables/{tableName}/rows/{rowId}", uriTemplate: "db://tables/{tableName}/rows/{rowId}",
selected: false,
}, },
{ {
name: "Log File", name: "Log File",
title: "Application Log", title: "Application Log",
uriTemplate: "file:///logs/{service}/{date}.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={[ subscriptions={[
{ {
name: "config.json", resource: { name: "config.json", uri: "file:///config.json" },
uri: "file:///config.json", lastUpdated: new Date("2026-03-17T10:30:00Z"),
lastUpdated: "2026-03-17T10:30:00Z",
}, },
{ {
name: "schema.sql", resource: { name: "schema.sql", uri: "file:///schema.sql" },
uri: "file:///schema.sql", lastUpdated: new Date("2026-03-17T10:28:00Z"),
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",
}, },
]} ]}
selectedTemplate={{ selectedTemplateUri="file:///users/{userId}/profile"
name: "User Profile", selectedResourceUri="file:///users/42/profile"
uriTemplate: "file:///users/{userId}/profile", readState={{
description: "Fetch a user profile by their unique identifier.", status: "ok",
}}
selectedResource={{
uri: "file:///users/42/profile", uri: "file:///users/42/profile",
mimeType: "application/json", result: {
annotations: { audience: "developer", priority: 0.8 }, contents: [
content: JSON.stringify( {
{ id: 42, name: "Alice", email: "alice@example.com" }, uri: "file:///users/42/profile",
null, mimeType: "application/json",
2, text: JSON.stringify(
), { id: 42, name: "Alice", email: "alice@example.com" },
lastUpdated: "2026-03-17T11:15:00Z", null,
2,
),
},
],
},
lastUpdated: new Date("2026-03-17T11:15:00Z"),
isSubscribed: false, isSubscribed: false,
}} }}
listChanged={true} listChanged={true}