Merge pull request #1214 from modelcontextprotocol/v2/phase-2-resources
Phase 2: Refactor Resources cluster to MCP SDK types
This commit is contained in:
commit
91e5a1257a
13 changed files with 485 additions and 619 deletions
|
|
@ -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>;
|
||||||
|
|
||||||
export const Default: Story = {
|
const sampleResources: Resource[] = [
|
||||||
args: {
|
|
||||||
resources: [
|
|
||||||
{
|
{
|
||||||
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",
|
name: "README.md",
|
||||||
uri: "file:///README.md",
|
uri: "file:///README.md",
|
||||||
selected: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "schema.sql",
|
name: "schema.sql",
|
||||||
uri: "file:///schema.sql",
|
uri: "file:///schema.sql",
|
||||||
selected: false,
|
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
templates: [
|
|
||||||
|
const sampleTemplates: ResourceTemplate[] = [
|
||||||
{
|
{
|
||||||
name: "User Profile",
|
name: "User Profile",
|
||||||
uriTemplate: "file:///users/{userId}/profile",
|
uriTemplate: "file:///users/{userId}/profile",
|
||||||
selected: false,
|
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
subscriptions: [
|
|
||||||
|
const sampleSubscriptions: InspectorResourceSubscription[] = [
|
||||||
{
|
{
|
||||||
|
resource: {
|
||||||
name: "config.json",
|
name: "config.json",
|
||||||
uri: "file:///config.json",
|
uri: "file:///config.json",
|
||||||
lastUpdated: "2026-03-17T10:30:00Z",
|
|
||||||
},
|
},
|
||||||
],
|
lastUpdated: new Date("2026-03-17T10:30:00Z"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
resources: sampleResources,
|
||||||
|
templates: sampleTemplates,
|
||||||
|
subscriptions: sampleSubscriptions,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,41 @@ type Story = StoryObj<typeof ResourceListItem>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
resource: {
|
||||||
name: "config.json",
|
name: "config.json",
|
||||||
uri: "file:///config.json",
|
uri: "file:///config.json",
|
||||||
|
},
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Selected: Story = {
|
export const Selected: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
resource: {
|
||||||
name: "config.json",
|
name: "config.json",
|
||||||
uri: "file:///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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,57 +17,96 @@ type Story = StoryObj<typeof ResourcePreviewPanel>;
|
||||||
|
|
||||||
export const JsonResource: Story = {
|
export const JsonResource: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
resource: {
|
||||||
|
name: "config.json",
|
||||||
|
uri: "file:///config.json",
|
||||||
|
},
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
uri: "file:///config.json",
|
uri: "file:///config.json",
|
||||||
mimeType: "application/json",
|
mimeType: "application/json",
|
||||||
content: JSON.stringify(
|
text: JSON.stringify(
|
||||||
{ name: "mcp-inspector", version: "2.0.0", debug: true },
|
{ name: "mcp-inspector", version: "2.0.0", debug: true },
|
||||||
null,
|
null,
|
||||||
2,
|
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: {
|
||||||
|
resource: {
|
||||||
|
name: "readme.txt",
|
||||||
|
uri: "file:///readme.txt",
|
||||||
|
},
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
uri: "file:///readme.txt",
|
uri: "file:///readme.txt",
|
||||||
mimeType: "text/plain",
|
mimeType: "text/plain",
|
||||||
content:
|
text: "This is a plain text resource with some example content.\nLine two of the content.",
|
||||||
"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: {
|
||||||
|
resource: {
|
||||||
|
name: "data.json",
|
||||||
|
uri: "file:///data.json",
|
||||||
|
},
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
uri: "file:///data.json",
|
uri: "file:///data.json",
|
||||||
mimeType: "application/json",
|
mimeType: "application/json",
|
||||||
content: JSON.stringify({ status: "active" }, null, 2),
|
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: {
|
||||||
|
resource: {
|
||||||
|
name: "data.json",
|
||||||
|
uri: "file:///data.json",
|
||||||
|
},
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
uri: "file:///data.json",
|
uri: "file:///data.json",
|
||||||
mimeType: "application/json",
|
mimeType: "application/json",
|
||||||
content: JSON.stringify({ status: "active" }, null, 2),
|
text: JSON.stringify({ status: "active" }, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
isSubscribed: false,
|
isSubscribed: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithAnnotations: Story = {
|
export const WithAnnotations: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
resource: {
|
||||||
|
name: "settings.json",
|
||||||
uri: "file:///settings.json",
|
uri: "file:///settings.json",
|
||||||
mimeType: "application/json",
|
|
||||||
content: JSON.stringify({ theme: "dark", lang: "en" }, null, 2),
|
|
||||||
isSubscribed: false,
|
|
||||||
annotations: {
|
annotations: {
|
||||||
audience: "application",
|
audience: ["user"],
|
||||||
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"),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,23 @@ type Story = StoryObj<typeof ResourceSubscribedItem>;
|
||||||
|
|
||||||
export const WithTimestamp: Story = {
|
export const WithTimestamp: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
subscription: {
|
||||||
|
resource: {
|
||||||
name: "config.json",
|
name: "config.json",
|
||||||
lastUpdated: "2026-03-17T10:30:00Z",
|
uri: "file:///config.json",
|
||||||
|
},
|
||||||
|
lastUpdated: new Date("2026-03-17T10:30:00Z"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithoutTimestamp: Story = {
|
export const WithoutTimestamp: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
subscription: {
|
||||||
|
resource: {
|
||||||
name: "schema.sql",
|
name: "schema.sql",
|
||||||
|
uri: "file:///schema.sql",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -15,34 +15,42 @@ type Story = StoryObj<typeof ResourceTemplatePanel>;
|
||||||
|
|
||||||
export const SingleVariable: Story = {
|
export const SingleVariable: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
template: {
|
||||||
name: "User Profile",
|
name: "User Profile",
|
||||||
uriTemplate: "file:///users/{userId}/profile",
|
uriTemplate: "file:///users/{userId}/profile",
|
||||||
description: "Fetch a user profile by their unique identifier.",
|
description: "Fetch a user profile by their unique identifier.",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultipleVariables: Story = {
|
export const MultipleVariables: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
template: {
|
||||||
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}",
|
||||||
description: "Access a specific row in a database table.",
|
description: "Access a specific row in a database table.",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithAnnotations: Story = {
|
export const WithAnnotations: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
template: {
|
||||||
name: "Dynamic Text Resource",
|
name: "Dynamic Text Resource",
|
||||||
uriTemplate: "resource://dynamic/{resourceId}",
|
uriTemplate: "resource://dynamic/{resourceId}",
|
||||||
description:
|
description:
|
||||||
"Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.",
|
"Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.",
|
||||||
annotations: { audience: "developer", priority: 0.8 },
|
annotations: { audience: ["user"], priority: 0.8 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoDescription: Story = {
|
export const NoDescription: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
template: {
|
||||||
name: "Simple Template",
|
name: "Simple Template",
|
||||||
uriTemplate: "file:///data/{filename}",
|
uriTemplate: "file:///data/{filename}",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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,209 +28,68 @@ 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" },
|
||||||
|
lastUpdated: new Date("2026-03-17T10:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resource: { name: "schema.sql", uri: "file:///schema.sql" },
|
||||||
|
lastUpdated: new Date("2026-03-17T10:28:00Z"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const readConfigState: ReadResourceState = {
|
||||||
|
status: "ok",
|
||||||
uri: "file:///config.json",
|
uri: "file:///config.json",
|
||||||
lastUpdated: "2026-03-17T10:30:00Z",
|
result: {
|
||||||
},
|
contents: [
|
||||||
{
|
{
|
||||||
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const selectedResourceData: SelectedResource = {
|
|
||||||
uri: "file:///config.json",
|
uri: "file:///config.json",
|
||||||
mimeType: "application/json",
|
mimeType: "application/json",
|
||||||
annotations: { audience: "developer", priority: 0.8 },
|
text: JSON.stringify(
|
||||||
content: JSON.stringify(
|
|
||||||
{
|
{
|
||||||
name: "my-project",
|
name: "my-project",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
|
|
@ -239,16 +98,13 @@ const selectedResourceData: SelectedResource = {
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
lastUpdated: "2026-03-17T10:30:00Z",
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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",
|
||||||
),
|
uri: "file:///users/42/profile",
|
||||||
selectedTemplate: selectedTemplateData,
|
result: {
|
||||||
selectedResource: {
|
contents: [
|
||||||
|
{
|
||||||
uri: "file:///users/42/profile",
|
uri: "file:///users/42/profile",
|
||||||
mimeType: "application/json",
|
mimeType: "application/json",
|
||||||
annotations: { audience: "developer", priority: 0.8 },
|
text: JSON.stringify(
|
||||||
content: JSON.stringify(
|
|
||||||
{ id: 42, name: "Alice", email: "alice@example.com" },
|
{ id: 42, name: "Alice", email: "alice@example.com" },
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
lastUpdated: "2026-03-17T11:15:00Z",
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
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",
|
||||||
|
result: {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
uri: "file:///users/42/profile",
|
uri: "file:///users/42/profile",
|
||||||
mimeType: "application/json",
|
mimeType: "application/json",
|
||||||
annotations: { audience: "developer", priority: 0.8 },
|
text: JSON.stringify(
|
||||||
content: JSON.stringify(
|
|
||||||
{ id: 42, name: "Alice", email: "alice@example.com" },
|
{ id: 42, name: "Alice", email: "alice@example.com" },
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
lastUpdated: "2026-03-17T11:15:00Z",
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
isSubscribed: false,
|
isSubscribed: false,
|
||||||
},
|
},
|
||||||
|
selectedResourceUri: "file:///users/42/profile",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
<DetailCard>
|
||||||
<ResourcePreviewPanel
|
<EmptyState>Click to read this resource</EmptyState>
|
||||||
{...selectedResource}
|
|
||||||
onRefresh={() => onReadResource(selectedResource.uri)}
|
|
||||||
onSubscribe={() => onSubscribeResource(selectedResource.uri)}
|
|
||||||
onUnsubscribe={() => onUnsubscribeResource(selectedResource.uri)}
|
|
||||||
/>
|
|
||||||
</DetailCard>
|
</DetailCard>
|
||||||
|
)}
|
||||||
</ScrollArea.Autosize>
|
</ScrollArea.Autosize>
|
||||||
) : (
|
) : (
|
||||||
<DetailCard flex={1}>
|
<DetailCard flex={1}>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
}}
|
uri: "file:///users/42/profile",
|
||||||
selectedResource={{
|
result: {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
uri: "file:///users/42/profile",
|
uri: "file:///users/42/profile",
|
||||||
mimeType: "application/json",
|
mimeType: "application/json",
|
||||||
annotations: { audience: "developer", priority: 0.8 },
|
text: JSON.stringify(
|
||||||
content: JSON.stringify(
|
|
||||||
{ id: 42, name: "Alice", email: "alice@example.com" },
|
{ id: 42, name: "Alice", email: "alice@example.com" },
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
lastUpdated: "2026-03-17T11:15:00Z",
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
lastUpdated: new Date("2026-03-17T11:15:00Z"),
|
||||||
isSubscribed: false,
|
isSubscribed: false,
|
||||||
}}
|
}}
|
||||||
listChanged={true}
|
listChanged={true}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue