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

@ -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<typeof ResourceControls> = {
export default meta;
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 = {
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,
},
};

View file

@ -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) => (
<ResourceListItem
key={resource.uri}
name={resource.name}
uri={resource.uri}
annotations={resource.annotations}
selected={resource.selected}
onClick={() => onSelectUri(resource.uri)}
resource={resource}
selected={resource.uri === selectedUri}
onClick={() => {
if (resource.uri !== selectedUri)
onSelectUri(resource.uri);
}}
/>
))}
</Stack>
@ -137,10 +136,12 @@ export function ResourceControls({
{filteredTemplates.map((template) => (
<ResourceListItem
key={template.uriTemplate}
name={templateDisplayName(template)}
uri={template.uriTemplate}
selected={template.selected}
onClick={() => onSelectTemplate(template.uriTemplate)}
resource={template}
selected={template.uriTemplate === selectedTemplate}
onClick={() => {
if (template.uriTemplate !== selectedTemplate)
onSelectTemplate(template.uriTemplate);
}}
/>
))}
</Stack>
@ -157,10 +158,11 @@ export function ResourceControls({
<Stack gap="xs">
{filteredSubscriptions.map((sub) => (
<ResourceSubscribedItem
key={sub.name}
name={sub.name}
lastUpdated={sub.lastUpdated}
onUnsubscribe={() => onUnsubscribeResource(sub.uri)}
key={sub.resource.uri}
subscription={sub}
onUnsubscribe={() =>
onUnsubscribeResource(sub.resource.uri)
}
/>
))}
</Stack>