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:
parent
61230d533c
commit
d817c46911
10 changed files with 465 additions and 616 deletions
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -15,16 +15,41 @@ type Story = StoryObj<typeof ResourceListItem>;
|
|||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
<Text fw={500}>{name}</Text>
|
||||
<Text fw={500}>{resource.title ?? resource.name}</Text>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -15,13 +15,23 @@ type Story = StoryObj<typeof ResourceSubscribedItem>;
|
|||
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ItemRow>
|
||||
<Stack gap={2}>
|
||||
<NameText>{name}</NameText>
|
||||
{lastUpdated && <TimestampText>{lastUpdated}</TimestampText>}
|
||||
<NameText>{resource.title ?? resource.name}</NameText>
|
||||
{lastUpdated && (
|
||||
<TimestampText>{formatLastUpdated(lastUpdated)}</TimestampText>
|
||||
)}
|
||||
</Stack>
|
||||
<SubtleButton onClick={onUnsubscribe}>Unsubscribe</SubtleButton>
|
||||
</ItemRow>
|
||||
|
|
|
|||
|
|
@ -15,34 +15,42 @@ type Story = StoryObj<typeof ResourceTemplatePanel>;
|
|||
|
||||
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}",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<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} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue