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) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-18 18:34:32 -04:00
commit d817c46911
2 changed files with 465 additions and 616 deletions

View file

@ -17,57 +17,96 @@ type Story = StoryObj<typeof ResourcePreviewPanel>;
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"),
},
};

View file

@ -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 (
<Stack gap="md">
<HeaderRow>
@ -93,7 +104,9 @@ export function ResourcePreviewPanel({
<CopyButton value={uri} />
</UriGroup>
</HeaderRow>
<ContentViewer block={toContentBlock(content, mimeType)} copyable />
{contents.map((item, index) => (
<ContentViewer key={index} block={toContentBlock(item)} copyable />
))}
<MetaRow>
{lastUpdated ? (
<TimestampText>{formatLastUpdated(lastUpdated)}</TimestampText>
@ -105,12 +118,7 @@ export function ResourcePreviewPanel({
<FooterRow>
<AnnotationGroup>
{annotations?.audience && (
<AnnotationBadge
facet="audience"
value={
annotations.audience.split(", ") as ("user" | "assistant")[]
}
/>
<AnnotationBadge facet="audience" value={annotations.audience} />
)}
{annotations?.priority !== undefined && (
<AnnotationBadge facet="priority" value={annotations.priority} />