Merge pull request #1229 from modelcontextprotocol/v2/phase-4-screen-selection-state
Phase 4: Move screen selection state into the screens
This commit is contained in:
commit
56abced72b
11 changed files with 527 additions and 137 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
import { fn, userEvent, within } from "storybook/test";
|
||||||
import { fn } from "storybook/test";
|
|
||||||
import { ToolControls } from "./ToolControls";
|
import { ToolControls } from "./ToolControls";
|
||||||
|
import { longToolList } from "../../screens/ToolsScreen/ToolsScreen.fixtures";
|
||||||
|
|
||||||
const meta: Meta<typeof ToolControls> = {
|
const meta: Meta<typeof ToolControls> = {
|
||||||
title: "Groups/ToolControls",
|
title: "Groups/ToolControls",
|
||||||
|
|
@ -16,44 +16,35 @@ const meta: Meta<typeof ToolControls> = {
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof ToolControls>;
|
type Story = StoryObj<typeof ToolControls>;
|
||||||
|
|
||||||
const sampleTools: Tool[] = [
|
|
||||||
{
|
|
||||||
name: "send_message",
|
|
||||||
title: "Send Message",
|
|
||||||
inputSchema: { type: "object" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "create_record",
|
|
||||||
title: "Create Record",
|
|
||||||
inputSchema: { type: "object" },
|
|
||||||
},
|
|
||||||
{ name: "delete_records", inputSchema: { type: "object" } },
|
|
||||||
{ name: "list_users", inputSchema: { type: "object" } },
|
|
||||||
{ name: "batch_process", inputSchema: { type: "object" } },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: longToolList,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithSelection: Story = {
|
export const WithSelection: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: longToolList,
|
||||||
selectedName: "create_record",
|
selectedName: "query_database",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithSearch: Story = {
|
export const WithSearch: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: longToolList,
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.type(
|
||||||
|
await canvas.findByPlaceholderText("Search tools..."),
|
||||||
|
"git",
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ListChanged: Story = {
|
export const ListChanged: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: longToolList,
|
||||||
listChanged: true,
|
listChanged: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
|
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { fn } from "storybook/test";
|
import { fn, userEvent, within } from "storybook/test";
|
||||||
import { PromptsScreen } from "./PromptsScreen";
|
import { PromptsScreen } from "./PromptsScreen";
|
||||||
import type { GetPromptState } from "./PromptsScreen";
|
import type { GetPromptState } from "./PromptsScreen";
|
||||||
|
|
||||||
|
|
@ -10,7 +10,6 @@ const meta: Meta<typeof PromptsScreen> = {
|
||||||
parameters: { layout: "fullscreen" },
|
parameters: { layout: "fullscreen" },
|
||||||
args: {
|
args: {
|
||||||
onRefreshList: fn(),
|
onRefreshList: fn(),
|
||||||
onSelectPrompt: fn(),
|
|
||||||
onGetPrompt: fn(),
|
onGetPrompt: fn(),
|
||||||
onCopyMessages: fn(),
|
onCopyMessages: fn(),
|
||||||
listChanged: false,
|
listChanged: false,
|
||||||
|
|
@ -70,6 +69,11 @@ const translateResult: GetPromptState = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function selectPromptByName(canvasElement: HTMLElement, name: string) {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.click(await canvas.findByText(name));
|
||||||
|
}
|
||||||
|
|
||||||
export const NoSelection: Story = {
|
export const NoSelection: Story = {
|
||||||
args: {
|
args: {
|
||||||
prompts: samplePrompts,
|
prompts: samplePrompts,
|
||||||
|
|
@ -79,36 +83,44 @@ export const NoSelection: Story = {
|
||||||
export const PromptSelected: Story = {
|
export const PromptSelected: Story = {
|
||||||
args: {
|
args: {
|
||||||
prompts: samplePrompts,
|
prompts: samplePrompts,
|
||||||
selectedPromptName: "translate",
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectPromptByName(canvasElement, "translate");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithResult: Story = {
|
export const WithResult: Story = {
|
||||||
args: {
|
args: {
|
||||||
prompts: samplePrompts,
|
prompts: samplePrompts,
|
||||||
selectedPromptName: "translate",
|
|
||||||
getPromptState: translateResult,
|
getPromptState: translateResult,
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectPromptByName(canvasElement, "translate");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Loading: Story = {
|
export const Loading: Story = {
|
||||||
args: {
|
args: {
|
||||||
prompts: samplePrompts,
|
prompts: samplePrompts,
|
||||||
selectedPromptName: "translate",
|
|
||||||
getPromptState: { status: "pending" },
|
getPromptState: { status: "pending" },
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectPromptByName(canvasElement, "translate");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithError: Story = {
|
export const WithError: Story = {
|
||||||
args: {
|
args: {
|
||||||
prompts: samplePrompts,
|
prompts: samplePrompts,
|
||||||
selectedPromptName: "translate",
|
|
||||||
getPromptState: {
|
getPromptState: {
|
||||||
status: "error",
|
status: "error",
|
||||||
error:
|
error:
|
||||||
'Prompt "translate" requires argument "text" but none was provided. Please fill in all required arguments before submitting.',
|
'Prompt "translate" requires argument "text" but none was provided. Please fill in all required arguments before submitting.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectPromptByName(canvasElement, "translate");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ListChanged: Story = {
|
export const ListChanged: Story = {
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,9 @@ export interface GetPromptState {
|
||||||
|
|
||||||
export interface PromptsScreenProps {
|
export interface PromptsScreenProps {
|
||||||
prompts: Prompt[];
|
prompts: Prompt[];
|
||||||
selectedPromptName?: string;
|
|
||||||
getPromptState?: GetPromptState;
|
getPromptState?: GetPromptState;
|
||||||
listChanged: boolean;
|
listChanged: boolean;
|
||||||
onRefreshList: () => void;
|
onRefreshList: () => void;
|
||||||
onSelectPrompt: (name: string) => void;
|
|
||||||
onGetPrompt: (name: string, args: Record<string, string>) => void;
|
onGetPrompt: (name: string, args: Record<string, string>) => void;
|
||||||
onCopyMessages?: () => void;
|
onCopyMessages?: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -65,14 +63,15 @@ const EmptyState = Text.withProps({
|
||||||
|
|
||||||
export function PromptsScreen({
|
export function PromptsScreen({
|
||||||
prompts,
|
prompts,
|
||||||
selectedPromptName,
|
|
||||||
getPromptState,
|
getPromptState,
|
||||||
listChanged,
|
listChanged,
|
||||||
onRefreshList,
|
onRefreshList,
|
||||||
onSelectPrompt,
|
|
||||||
onGetPrompt,
|
onGetPrompt,
|
||||||
onCopyMessages,
|
onCopyMessages,
|
||||||
}: PromptsScreenProps) {
|
}: PromptsScreenProps) {
|
||||||
|
const [selectedPromptName, setSelectedPromptName] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
const [argumentValues, setArgumentValues] = useState<Record<string, string>>(
|
const [argumentValues, setArgumentValues] = useState<Record<string, string>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
@ -91,7 +90,7 @@ export function PromptsScreen({
|
||||||
onRefreshList={onRefreshList}
|
onRefreshList={onRefreshList}
|
||||||
onSelectPrompt={(name) => {
|
onSelectPrompt={(name) => {
|
||||||
setArgumentValues({});
|
setArgumentValues({});
|
||||||
onSelectPrompt(name);
|
setSelectedPromptName(name);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SidebarCard>
|
</SidebarCard>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type {
|
||||||
ResourceTemplate,
|
ResourceTemplate,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js";
|
import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js";
|
||||||
import { fn } from "storybook/test";
|
import { fn, userEvent, within } from "storybook/test";
|
||||||
import { ResourcesScreen } from "./ResourcesScreen";
|
import { ResourcesScreen } from "./ResourcesScreen";
|
||||||
import type { ReadResourceState } from "./ResourcesScreen";
|
import type { ReadResourceState } from "./ResourcesScreen";
|
||||||
|
|
||||||
|
|
@ -14,8 +14,6 @@ const meta: Meta<typeof ResourcesScreen> = {
|
||||||
parameters: { layout: "fullscreen" },
|
parameters: { layout: "fullscreen" },
|
||||||
args: {
|
args: {
|
||||||
onRefreshList: fn(),
|
onRefreshList: fn(),
|
||||||
onSelectUri: fn(),
|
|
||||||
onSelectTemplate: fn(),
|
|
||||||
onReadResource: fn(),
|
onReadResource: fn(),
|
||||||
onSubscribeResource: fn(),
|
onSubscribeResource: fn(),
|
||||||
onUnsubscribeResource: fn(),
|
onUnsubscribeResource: fn(),
|
||||||
|
|
@ -105,6 +103,43 @@ const readConfigState: ReadResourceState = {
|
||||||
isSubscribed: true,
|
isSubscribed: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readUserProfileState: ReadResourceState = {
|
||||||
|
status: "ok",
|
||||||
|
uri: "file:///users/42/profile",
|
||||||
|
result: {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "file:///users/42/profile",
|
||||||
|
mimeType: "application/json",
|
||||||
|
text: JSON.stringify(
|
||||||
|
{ id: 42, name: "Alice", email: "alice@example.com" },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
isSubscribed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function clickByText(canvasElement: HTMLElement, label: string) {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.click(await canvas.findByText(label));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The userId value typed below must match the `{userId}` segment in
|
||||||
|
// `readUserProfileState.uri` so the synthetic-resource fallback in
|
||||||
|
// ResourcesScreen matches and the preview panel renders.
|
||||||
|
async function expandUserProfileTemplate(canvasElement: HTMLElement) {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.click(await canvas.findByText("User Profile"));
|
||||||
|
const userIdInput = await canvas.findByLabelText("userId");
|
||||||
|
await userEvent.type(userIdInput, "42");
|
||||||
|
await userEvent.click(
|
||||||
|
await canvas.findByRole("button", { name: "Read Resource" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const WithResources: Story = {
|
export const WithResources: Story = {
|
||||||
args: {
|
args: {
|
||||||
resources: sampleResources,
|
resources: sampleResources,
|
||||||
|
|
@ -115,9 +150,11 @@ export const ResourceSelected: Story = {
|
||||||
args: {
|
args: {
|
||||||
resources: sampleResources,
|
resources: sampleResources,
|
||||||
subscriptions: sampleSubscriptions,
|
subscriptions: sampleSubscriptions,
|
||||||
selectedResourceUri: "file:///config.json",
|
|
||||||
readState: readConfigState,
|
readState: readConfigState,
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await clickByText(canvasElement, "config.json");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithTemplates: Story = {
|
export const WithTemplates: Story = {
|
||||||
|
|
@ -131,7 +168,9 @@ export const TemplateSelected: Story = {
|
||||||
args: {
|
args: {
|
||||||
resources: sampleResources,
|
resources: sampleResources,
|
||||||
templates: sampleTemplates,
|
templates: sampleTemplates,
|
||||||
selectedTemplateUri: "file:///users/{userId}/profile",
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await clickByText(canvasElement, "User Profile");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -139,26 +178,10 @@ export const TemplateWithResource: Story = {
|
||||||
args: {
|
args: {
|
||||||
resources: sampleResources,
|
resources: sampleResources,
|
||||||
templates: sampleTemplates,
|
templates: sampleTemplates,
|
||||||
selectedTemplateUri: "file:///users/{userId}/profile",
|
readState: readUserProfileState,
|
||||||
readState: {
|
},
|
||||||
status: "ok",
|
play: async ({ canvasElement }) => {
|
||||||
uri: "file:///users/42/profile",
|
await expandUserProfileTemplate(canvasElement);
|
||||||
result: {
|
|
||||||
contents: [
|
|
||||||
{
|
|
||||||
uri: "file:///users/42/profile",
|
|
||||||
mimeType: "application/json",
|
|
||||||
text: JSON.stringify(
|
|
||||||
{ id: 42, name: "Alice", email: "alice@example.com" },
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
isSubscribed: false,
|
|
||||||
},
|
|
||||||
selectedResourceUri: "file:///users/42/profile",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -167,26 +190,10 @@ export const AllSections: Story = {
|
||||||
resources: sampleResources,
|
resources: sampleResources,
|
||||||
templates: sampleTemplates,
|
templates: sampleTemplates,
|
||||||
subscriptions: sampleSubscriptions,
|
subscriptions: sampleSubscriptions,
|
||||||
selectedTemplateUri: "file:///users/{userId}/profile",
|
readState: readUserProfileState,
|
||||||
readState: {
|
},
|
||||||
status: "ok",
|
play: async ({ canvasElement }) => {
|
||||||
uri: "file:///users/42/profile",
|
await expandUserProfileTemplate(canvasElement);
|
||||||
result: {
|
|
||||||
contents: [
|
|
||||||
{
|
|
||||||
uri: "file:///users/42/profile",
|
|
||||||
mimeType: "application/json",
|
|
||||||
text: JSON.stringify(
|
|
||||||
{ id: 42, name: "Alice", email: "alice@example.com" },
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
isSubscribed: false,
|
|
||||||
},
|
|
||||||
selectedResourceUri: "file:///users/42/profile",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -31,13 +32,9 @@ export interface ResourcesScreenProps {
|
||||||
resources: Resource[];
|
resources: Resource[];
|
||||||
templates: ResourceTemplate[];
|
templates: ResourceTemplate[];
|
||||||
subscriptions: InspectorResourceSubscription[];
|
subscriptions: InspectorResourceSubscription[];
|
||||||
selectedResourceUri?: string;
|
|
||||||
selectedTemplateUri?: string;
|
|
||||||
readState?: ReadResourceState;
|
readState?: ReadResourceState;
|
||||||
listChanged: boolean;
|
listChanged: boolean;
|
||||||
onRefreshList: () => void;
|
onRefreshList: () => void;
|
||||||
onSelectUri: (uri: string) => void;
|
|
||||||
onSelectTemplate: (uriTemplate: string) => void;
|
|
||||||
onReadResource: (uri: string) => void;
|
onReadResource: (uri: string) => void;
|
||||||
onSubscribeResource: (uri: string) => void;
|
onSubscribeResource: (uri: string) => void;
|
||||||
onUnsubscribeResource: (uri: string) => void;
|
onUnsubscribeResource: (uri: string) => void;
|
||||||
|
|
@ -78,17 +75,20 @@ export function ResourcesScreen({
|
||||||
resources,
|
resources,
|
||||||
templates,
|
templates,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
selectedResourceUri,
|
|
||||||
selectedTemplateUri,
|
|
||||||
readState,
|
readState,
|
||||||
listChanged,
|
listChanged,
|
||||||
onRefreshList,
|
onRefreshList,
|
||||||
onSelectUri,
|
|
||||||
onSelectTemplate,
|
|
||||||
onReadResource,
|
onReadResource,
|
||||||
onSubscribeResource,
|
onSubscribeResource,
|
||||||
onUnsubscribeResource,
|
onUnsubscribeResource,
|
||||||
}: ResourcesScreenProps) {
|
}: ResourcesScreenProps) {
|
||||||
|
const [selectedResourceUri, setSelectedResourceUri] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [selectedTemplateUri, setSelectedTemplateUri] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const selectedResource = selectedResourceUri
|
const selectedResource = selectedResourceUri
|
||||||
? resources.find((r) => r.uri === selectedResourceUri)
|
? resources.find((r) => r.uri === selectedResourceUri)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
@ -104,6 +104,21 @@ export function ResourcesScreen({
|
||||||
? { name: readState.uri, uri: readState.uri }
|
? { name: readState.uri, uri: readState.uri }
|
||||||
: undefined);
|
: undefined);
|
||||||
|
|
||||||
|
function handleSelectResource(uri: string) {
|
||||||
|
setSelectedTemplateUri(undefined);
|
||||||
|
setSelectedResourceUri(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectTemplate(uriTemplate: string) {
|
||||||
|
setSelectedResourceUri(undefined);
|
||||||
|
setSelectedTemplateUri(uriTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReadResource(uri: string) {
|
||||||
|
setSelectedResourceUri(uri);
|
||||||
|
onReadResource(uri);
|
||||||
|
}
|
||||||
|
|
||||||
function renderReadState() {
|
function renderReadState() {
|
||||||
if (!readState) return null;
|
if (!readState) return null;
|
||||||
|
|
||||||
|
|
@ -136,7 +151,7 @@ export function ResourcesScreen({
|
||||||
contents={readState.result.contents}
|
contents={readState.result.contents}
|
||||||
lastUpdated={readState.lastUpdated}
|
lastUpdated={readState.lastUpdated}
|
||||||
isSubscribed={readState.isSubscribed ?? false}
|
isSubscribed={readState.isSubscribed ?? false}
|
||||||
onRefresh={() => onReadResource(readResource.uri)}
|
onRefresh={() => handleReadResource(readResource.uri)}
|
||||||
onSubscribe={() => onSubscribeResource(readResource.uri)}
|
onSubscribe={() => onSubscribeResource(readResource.uri)}
|
||||||
onUnsubscribe={() => onUnsubscribeResource(readResource.uri)}
|
onUnsubscribe={() => onUnsubscribeResource(readResource.uri)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -159,8 +174,8 @@ export function ResourcesScreen({
|
||||||
selectedTemplateUri={selectedTemplateUri}
|
selectedTemplateUri={selectedTemplateUri}
|
||||||
listChanged={listChanged}
|
listChanged={listChanged}
|
||||||
onRefreshList={onRefreshList}
|
onRefreshList={onRefreshList}
|
||||||
onSelectUri={onSelectUri}
|
onSelectUri={handleSelectResource}
|
||||||
onSelectTemplate={onSelectTemplate}
|
onSelectTemplate={handleSelectTemplate}
|
||||||
onUnsubscribeResource={onUnsubscribeResource}
|
onUnsubscribeResource={onUnsubscribeResource}
|
||||||
/>
|
/>
|
||||||
</SidebarCard>
|
</SidebarCard>
|
||||||
|
|
@ -172,7 +187,7 @@ export function ResourcesScreen({
|
||||||
<DetailCard>
|
<DetailCard>
|
||||||
<ResourceTemplatePanel
|
<ResourceTemplatePanel
|
||||||
template={selectedTemplate}
|
template={selectedTemplate}
|
||||||
onReadResource={onReadResource}
|
onReadResource={handleReadResource}
|
||||||
/>
|
/>
|
||||||
</DetailCard>
|
</DetailCard>
|
||||||
</ScrollArea.Autosize>
|
</ScrollArea.Autosize>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,328 @@
|
||||||
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
export const longToolList: Tool[] = [
|
||||||
|
{
|
||||||
|
name: "read_file",
|
||||||
|
title: "Read File",
|
||||||
|
description:
|
||||||
|
"Read the contents of a text file from the local filesystem. Returns the file's contents as a UTF-8 string along with metadata.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
path: { type: "string", description: "Absolute or relative file path" },
|
||||||
|
encoding: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["utf-8", "ascii", "latin1"],
|
||||||
|
description: "Text encoding to use",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["path"],
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
contents: { type: "string" },
|
||||||
|
bytes: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["contents", "bytes"],
|
||||||
|
},
|
||||||
|
annotations: { readOnlyHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "write_file",
|
||||||
|
title: "Write File",
|
||||||
|
description:
|
||||||
|
"Write the given contents to a file at the specified path, creating it if it does not exist.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
path: { type: "string", description: "Destination file path" },
|
||||||
|
contents: { type: "string", description: "Text contents to write" },
|
||||||
|
overwrite: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Allow overwriting an existing file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["path", "contents"],
|
||||||
|
},
|
||||||
|
annotations: { destructiveHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list_directory",
|
||||||
|
title: "List Directory",
|
||||||
|
description: "List the entries in a directory, optionally recursing.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
path: { type: "string", description: "Directory to list" },
|
||||||
|
recursive: { type: "boolean", description: "Recurse into subdirs" },
|
||||||
|
},
|
||||||
|
required: ["path"],
|
||||||
|
},
|
||||||
|
annotations: { readOnlyHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete_file",
|
||||||
|
title: "Delete File",
|
||||||
|
description:
|
||||||
|
"Delete a file from the filesystem. This action cannot be undone.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
path: { type: "string", description: "File to delete" },
|
||||||
|
},
|
||||||
|
required: ["path"],
|
||||||
|
},
|
||||||
|
annotations: { destructiveHint: true, idempotentHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search_files",
|
||||||
|
title: "Search Files",
|
||||||
|
description:
|
||||||
|
"Search for files matching a glob pattern under the given root.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
root: { type: "string" },
|
||||||
|
pattern: { type: "string", description: "e.g. **/*.ts" },
|
||||||
|
maxResults: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["root", "pattern"],
|
||||||
|
},
|
||||||
|
annotations: { readOnlyHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query_database",
|
||||||
|
title: "Query Database",
|
||||||
|
description:
|
||||||
|
"Execute a read-only SQL query against the configured database connection.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
sql: { type: "string", description: "SELECT statement" },
|
||||||
|
params: {
|
||||||
|
type: "array",
|
||||||
|
description: "Positional bind parameters",
|
||||||
|
items: { type: "string" },
|
||||||
|
},
|
||||||
|
limit: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["sql"],
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
rows: { type: "array", items: { type: "object" } },
|
||||||
|
rowCount: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["rows", "rowCount"],
|
||||||
|
},
|
||||||
|
annotations: { readOnlyHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "insert_record",
|
||||||
|
title: "Insert Record",
|
||||||
|
description: "Insert a new record into the named table.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
table: { type: "string" },
|
||||||
|
values: { type: "object", description: "Column → value map" },
|
||||||
|
},
|
||||||
|
required: ["table", "values"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_record",
|
||||||
|
title: "Update Record",
|
||||||
|
description: "Update an existing record by primary key.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
table: { type: "string" },
|
||||||
|
id: { type: "string" },
|
||||||
|
values: { type: "object" },
|
||||||
|
},
|
||||||
|
required: ["table", "id", "values"],
|
||||||
|
},
|
||||||
|
annotations: { idempotentHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete_record",
|
||||||
|
title: "Delete Record",
|
||||||
|
description: "Delete a record from the named table by primary key.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
table: { type: "string" },
|
||||||
|
id: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["table", "id"],
|
||||||
|
},
|
||||||
|
annotations: { destructiveHint: true, idempotentHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_schema",
|
||||||
|
title: "Get Schema",
|
||||||
|
description: "Return the schema definition for the named table or view.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
table: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["table"],
|
||||||
|
},
|
||||||
|
annotations: { readOnlyHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "git_status",
|
||||||
|
title: "Git Status",
|
||||||
|
description: "Show the working tree status of the given repository.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
repoPath: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["repoPath"],
|
||||||
|
},
|
||||||
|
annotations: { readOnlyHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "git_commit",
|
||||||
|
title: "Git Commit",
|
||||||
|
description:
|
||||||
|
"Create a new commit on the current branch with the given message.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
repoPath: { type: "string" },
|
||||||
|
message: { type: "string" },
|
||||||
|
files: { type: "array", items: { type: "string" } },
|
||||||
|
},
|
||||||
|
required: ["repoPath", "message"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "git_push",
|
||||||
|
title: "Git Push",
|
||||||
|
description:
|
||||||
|
"Push commits to the configured remote. Requires network access.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
repoPath: { type: "string" },
|
||||||
|
remote: { type: "string" },
|
||||||
|
branch: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["repoPath"],
|
||||||
|
},
|
||||||
|
annotations: { openWorldHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "git_log",
|
||||||
|
title: "Git Log",
|
||||||
|
description: "List recent commits on the current branch.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
repoPath: { type: "string" },
|
||||||
|
limit: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["repoPath"],
|
||||||
|
},
|
||||||
|
annotations: { readOnlyHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "git_diff",
|
||||||
|
title: "Git Diff",
|
||||||
|
description: "Show changes between commits, branches, or the working tree.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
repoPath: { type: "string" },
|
||||||
|
ref: { type: "string", description: "Optional revision range" },
|
||||||
|
},
|
||||||
|
required: ["repoPath"],
|
||||||
|
},
|
||||||
|
annotations: { readOnlyHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fetch_url",
|
||||||
|
title: "Fetch URL",
|
||||||
|
description:
|
||||||
|
"Issue an HTTP GET request and return the response body and headers.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
url: { type: "string", description: "URL to fetch" },
|
||||||
|
headers: { type: "object" },
|
||||||
|
},
|
||||||
|
required: ["url"],
|
||||||
|
},
|
||||||
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "post_request",
|
||||||
|
title: "Post Request",
|
||||||
|
description: "Send a JSON HTTP POST to the given URL.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
url: { type: "string" },
|
||||||
|
body: { type: "object", description: "JSON request body" },
|
||||||
|
headers: { type: "object" },
|
||||||
|
},
|
||||||
|
required: ["url", "body"],
|
||||||
|
},
|
||||||
|
annotations: { openWorldHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parse_html",
|
||||||
|
title: "Parse HTML",
|
||||||
|
description:
|
||||||
|
"Parse an HTML document and return a structured representation.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
html: { type: "string" },
|
||||||
|
selector: {
|
||||||
|
type: "string",
|
||||||
|
description: "Optional CSS selector to scope the parse",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["html"],
|
||||||
|
},
|
||||||
|
annotations: { readOnlyHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "send_email",
|
||||||
|
title: "Send Email",
|
||||||
|
description: "Send an email through the configured SMTP relay.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
to: { type: "array", items: { type: "string" } },
|
||||||
|
subject: { type: "string" },
|
||||||
|
body: { type: "string" },
|
||||||
|
attachments: { type: "array", items: { type: "string" } },
|
||||||
|
},
|
||||||
|
required: ["to", "subject", "body"],
|
||||||
|
},
|
||||||
|
annotations: { openWorldHint: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "schedule_task",
|
||||||
|
title: "Schedule Task",
|
||||||
|
description:
|
||||||
|
"Schedule a background task to run at the specified time or interval.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string" },
|
||||||
|
cron: { type: "string", description: "Cron expression" },
|
||||||
|
payload: { type: "object" },
|
||||||
|
},
|
||||||
|
required: ["name", "cron"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import type { ElicitRequest, Tool } from "@modelcontextprotocol/sdk/types.js";
|
import type { ElicitRequest, Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { Modal } from "@mantine/core";
|
import { Modal } from "@mantine/core";
|
||||||
import { fn } from "storybook/test";
|
import { fn, userEvent, within } from "storybook/test";
|
||||||
import { ToolsScreen } from "./ToolsScreen";
|
import { ToolsScreen } from "./ToolsScreen";
|
||||||
import type { ToolCallState } from "./ToolsScreen";
|
import type { ToolCallState } from "./ToolsScreen";
|
||||||
import { SamplingRequestPanel } from "../../groups/SamplingRequestPanel/SamplingRequestPanel";
|
import { SamplingRequestPanel } from "../../groups/SamplingRequestPanel/SamplingRequestPanel";
|
||||||
|
|
@ -17,7 +17,6 @@ const meta: Meta<typeof ToolsScreen> = {
|
||||||
args: {
|
args: {
|
||||||
listChanged: false,
|
listChanged: false,
|
||||||
onRefreshList: fn(),
|
onRefreshList: fn(),
|
||||||
onSelectTool: fn(),
|
|
||||||
onCallTool: fn(),
|
onCallTool: fn(),
|
||||||
onCancelCall: fn(),
|
onCancelCall: fn(),
|
||||||
onClearResult: fn(),
|
onClearResult: fn(),
|
||||||
|
|
@ -77,6 +76,11 @@ const resultState: ToolCallState = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function selectToolByLabel(canvasElement: HTMLElement, label: string) {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.click(await canvas.findByText(label));
|
||||||
|
}
|
||||||
|
|
||||||
export const NoSelection: Story = {
|
export const NoSelection: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: sampleTools,
|
||||||
|
|
@ -86,16 +90,20 @@ export const NoSelection: Story = {
|
||||||
export const ToolSelected: Story = {
|
export const ToolSelected: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: sampleTools,
|
||||||
selectedToolName: "create_record",
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectToolByLabel(canvasElement, "Create Record");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithResult: Story = {
|
export const WithResult: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: sampleTools,
|
||||||
selectedToolName: "create_record",
|
|
||||||
callState: resultState,
|
callState: resultState,
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectToolByLabel(canvasElement, "Create Record");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LongToolName: Story = {
|
export const LongToolName: Story = {
|
||||||
|
|
@ -130,15 +138,18 @@ export const LongToolName: Story = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selectedToolName:
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectToolByLabel(
|
||||||
|
canvasElement,
|
||||||
"organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
|
"organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithError: Story = {
|
export const WithError: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: sampleTools,
|
||||||
selectedToolName: "create_record",
|
|
||||||
callState: {
|
callState: {
|
||||||
status: "error",
|
status: "error",
|
||||||
result: {
|
result: {
|
||||||
|
|
@ -152,6 +163,9 @@ export const WithError: Story = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectToolByLabel(canvasElement, "Create Record");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithListChanged: Story = {
|
export const WithListChanged: Story = {
|
||||||
|
|
@ -176,9 +190,11 @@ const elicitFormRequest = {
|
||||||
export const WithSamplingModal: Story = {
|
export const WithSamplingModal: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: sampleTools,
|
||||||
selectedToolName: "create_record",
|
|
||||||
callState: { status: "pending" },
|
callState: { status: "pending" },
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectToolByLabel(canvasElement, "Create Record");
|
||||||
|
},
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<>
|
<>
|
||||||
<ToolsScreen {...args} />
|
<ToolsScreen {...args} />
|
||||||
|
|
@ -217,9 +233,11 @@ export const WithSamplingModal: Story = {
|
||||||
export const WithElicitationModal: Story = {
|
export const WithElicitationModal: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: sampleTools,
|
||||||
selectedToolName: "create_record",
|
|
||||||
callState: { status: "pending" },
|
callState: { status: "pending" },
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectToolByLabel(canvasElement, "Create Record");
|
||||||
|
},
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<>
|
<>
|
||||||
<ToolsScreen {...args} />
|
<ToolsScreen {...args} />
|
||||||
|
|
@ -240,9 +258,11 @@ export const WithElicitationModal: Story = {
|
||||||
export const WithPendingRequests: Story = {
|
export const WithPendingRequests: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: sampleTools,
|
||||||
selectedToolName: "create_record",
|
|
||||||
callState: { status: "pending" },
|
callState: { status: "pending" },
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
await selectToolByLabel(canvasElement, "Create Record");
|
||||||
|
},
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<>
|
<>
|
||||||
<ToolsScreen {...args} />
|
<ToolsScreen {...args} />
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,9 @@ export interface ToolCallState {
|
||||||
|
|
||||||
export interface ToolsScreenProps {
|
export interface ToolsScreenProps {
|
||||||
tools: Tool[];
|
tools: Tool[];
|
||||||
selectedToolName?: string;
|
|
||||||
callState?: ToolCallState;
|
callState?: ToolCallState;
|
||||||
listChanged: boolean;
|
listChanged: boolean;
|
||||||
onRefreshList: () => void;
|
onRefreshList: () => void;
|
||||||
onSelectTool: (name: string) => void;
|
|
||||||
onCallTool: (name: string, args: Record<string, unknown>) => void;
|
onCallTool: (name: string, args: Record<string, unknown>) => void;
|
||||||
onCancelCall?: () => void;
|
onCancelCall?: () => void;
|
||||||
onClearResult?: () => void;
|
onClearResult?: () => void;
|
||||||
|
|
@ -58,15 +56,16 @@ const EmptyState = Text.withProps({
|
||||||
|
|
||||||
export function ToolsScreen({
|
export function ToolsScreen({
|
||||||
tools,
|
tools,
|
||||||
selectedToolName,
|
|
||||||
callState,
|
callState,
|
||||||
listChanged,
|
listChanged,
|
||||||
onRefreshList,
|
onRefreshList,
|
||||||
onSelectTool,
|
|
||||||
onCallTool,
|
onCallTool,
|
||||||
onCancelCall,
|
onCancelCall,
|
||||||
onClearResult,
|
onClearResult,
|
||||||
}: ToolsScreenProps) {
|
}: ToolsScreenProps) {
|
||||||
|
const [selectedToolName, setSelectedToolName] = useState<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
const [formValues, setFormValues] = useState<Record<string, unknown>>({});
|
const [formValues, setFormValues] = useState<Record<string, unknown>>({});
|
||||||
const selectedTool = selectedToolName
|
const selectedTool = selectedToolName
|
||||||
? tools.find((t) => t.name === selectedToolName)
|
? tools.find((t) => t.name === selectedToolName)
|
||||||
|
|
@ -84,7 +83,7 @@ export function ToolsScreen({
|
||||||
onRefreshList={onRefreshList}
|
onRefreshList={onRefreshList}
|
||||||
onSelectTool={(name) => {
|
onSelectTool={(name) => {
|
||||||
setFormValues({});
|
setFormValues({});
|
||||||
onSelectTool(name);
|
setSelectedToolName(name);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SidebarCard>
|
</SidebarCard>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import type {
|
||||||
Resource,
|
Resource,
|
||||||
ResourceTemplate,
|
ResourceTemplate,
|
||||||
Task,
|
Task,
|
||||||
Tool,
|
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import type {
|
import type {
|
||||||
InspectorResourceSubscription,
|
InspectorResourceSubscription,
|
||||||
|
|
@ -14,6 +13,7 @@ import { fn } from "storybook/test";
|
||||||
import { InspectorView } from "./InspectorView";
|
import { InspectorView } from "./InspectorView";
|
||||||
import type { ServerEntry } from "../../screens/ServerListScreen/ServerListScreen";
|
import type { ServerEntry } from "../../screens/ServerListScreen/ServerListScreen";
|
||||||
import { mixedEntries as demoLogs } from "../../screens/LoggingScreen/LoggingScreen.fixtures";
|
import { mixedEntries as demoLogs } from "../../screens/LoggingScreen/LoggingScreen.fixtures";
|
||||||
|
import { longToolList as demoTools } from "../../screens/ToolsScreen/ToolsScreen.fixtures";
|
||||||
import type { TaskProgress } from "../../groups/TaskCard/TaskCard";
|
import type { TaskProgress } from "../../groups/TaskCard/TaskCard";
|
||||||
|
|
||||||
const demoServers: ServerEntry[] = [
|
const demoServers: ServerEntry[] = [
|
||||||
|
|
@ -43,33 +43,6 @@ const demoServers: ServerEntry[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const demoTools: Tool[] = [
|
|
||||||
{
|
|
||||||
name: "send_message",
|
|
||||||
title: "Send Message",
|
|
||||||
inputSchema: { type: "object" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "create_record",
|
|
||||||
title: "Create Record",
|
|
||||||
description: "Creates a new record with the given parameters",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
title: { type: "string", description: "Record title" },
|
|
||||||
count: { type: "number", description: "Number of items" },
|
|
||||||
enabled: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether the record is active",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["title"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ name: "delete_records", inputSchema: { type: "object" } },
|
|
||||||
{ name: "list_users", inputSchema: { type: "object" } },
|
|
||||||
];
|
|
||||||
|
|
||||||
const demoPrompts: Prompt[] = [
|
const demoPrompts: Prompt[] = [
|
||||||
{
|
{
|
||||||
name: "summarize",
|
name: "summarize",
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,6 @@ export function InspectorView({
|
||||||
tools={tools}
|
tools={tools}
|
||||||
listChanged={false}
|
listChanged={false}
|
||||||
onRefreshList={noop}
|
onRefreshList={noop}
|
||||||
onSelectTool={noop}
|
|
||||||
onCallTool={noop}
|
onCallTool={noop}
|
||||||
/>
|
/>
|
||||||
</ScreenStage>
|
</ScreenStage>
|
||||||
|
|
@ -298,7 +297,6 @@ export function InspectorView({
|
||||||
prompts={prompts}
|
prompts={prompts}
|
||||||
listChanged={false}
|
listChanged={false}
|
||||||
onRefreshList={noop}
|
onRefreshList={noop}
|
||||||
onSelectPrompt={noop}
|
|
||||||
onGetPrompt={noop}
|
onGetPrompt={noop}
|
||||||
/>
|
/>
|
||||||
</ScreenStage>
|
</ScreenStage>
|
||||||
|
|
@ -309,8 +307,6 @@ export function InspectorView({
|
||||||
subscriptions={subscriptions}
|
subscriptions={subscriptions}
|
||||||
listChanged={false}
|
listChanged={false}
|
||||||
onRefreshList={noop}
|
onRefreshList={noop}
|
||||||
onSelectUri={noop}
|
|
||||||
onSelectTemplate={noop}
|
|
||||||
onReadResource={noop}
|
onReadResource={noop}
|
||||||
onSubscribeResource={noop}
|
onSubscribeResource={noop}
|
||||||
onUnsubscribeResource={noop}
|
onUnsubscribeResource={noop}
|
||||||
|
|
|
||||||
|
|
@ -532,6 +532,56 @@ existing stories) rather than a screen refactor.
|
||||||
- No prop changes needed.
|
- No prop changes needed.
|
||||||
- Optional: add `connectionStatus` if the embedded header needs it.
|
- Optional: add `connectionStatus` if the embedded header needs it.
|
||||||
|
|
||||||
|
### 4.3 Move selection state into the screens
|
||||||
|
|
||||||
|
The Phase 2 cluster work threaded `selectedXxxName` / `onSelectXxx` props
|
||||||
|
through every list-and-detail screen, leaving selection as caller-owned
|
||||||
|
state. That choice contradicts Phase 3's "selected name stays in `useState`
|
||||||
|
inside the screen" rule and creates a real wiring gap: nothing above the
|
||||||
|
screen has any reason to know which tool/prompt/resource the user clicked
|
||||||
|
on, so under `InspectorView` clicks would have to be round-tripped through
|
||||||
|
`App.tsx` purely to get the detail panel to render.
|
||||||
|
|
||||||
|
Selection is screen-local UI state. MCP-derived data (`callState` / fetched
|
||||||
|
prompt messages / resource read results) keeps flowing in from
|
||||||
|
`InspectorView` as props. Refactor each screen accordingly.
|
||||||
|
|
||||||
|
Screens to fix:
|
||||||
|
|
||||||
|
- **`ToolsScreen`** — drop `selectedToolName` and `onSelectTool` from the
|
||||||
|
prop interface; track the selected tool name in `useState` inside the
|
||||||
|
screen. `ToolControls` continues to receive `selectedName` and
|
||||||
|
`onSelectTool` as props from the screen. `callState` (and any future
|
||||||
|
result/progress payload) keeps coming in from `InspectorView`.
|
||||||
|
`InspectorView` only needs to know the selected tool name when it
|
||||||
|
triggers `onCallTool` — which the screen already owns, so it can pass
|
||||||
|
the name up at call time rather than maintaining it in the parent.
|
||||||
|
- **`PromptsScreen`** — same pattern for `selectedPromptName`. Fetched
|
||||||
|
`GetPromptResult` continues to arrive as a prop from `InspectorView`.
|
||||||
|
- **`ResourcesScreen`** — same pattern for the selected resource URI and
|
||||||
|
selected resource template URI. `ReadResourceResult` continues to
|
||||||
|
arrive as a prop.
|
||||||
|
|
||||||
|
For each screen:
|
||||||
|
|
||||||
|
1. Remove the `selectedXxxName` / `onSelectXxx` props from the screen's
|
||||||
|
prop interface.
|
||||||
|
2. Add a `useState` for the selection inside the screen and pass it down
|
||||||
|
to the controls group.
|
||||||
|
3. When the user triggers a request (`onCallTool` / `onGetPrompt` /
|
||||||
|
`onReadResource`), pass the currently selected name/uri up alongside
|
||||||
|
any other arguments — the parent does not need to track selection
|
||||||
|
between calls.
|
||||||
|
4. Update each screen's stories to remove the now-deleted props. The
|
||||||
|
stories will start working without `useArgs` selection wiring, because
|
||||||
|
selection is now genuinely internal.
|
||||||
|
5. Audit the controls groups (`ToolControls`, `PromptControls`,
|
||||||
|
`ResourceControls`) — their `selectedName` / `onSelectXxx` props are
|
||||||
|
correct (they ARE caller-owned, because the screen owns them) and
|
||||||
|
should not change.
|
||||||
|
6. Update `InspectorView` and any wiring fixtures to drop the now-removed
|
||||||
|
selection props they were passing to each screen.
|
||||||
|
|
||||||
## Phase 5 — Cleanup pass
|
## Phase 5 — Cleanup pass
|
||||||
|
|
||||||
After all four phases, sweep:
|
After all four phases, sweep:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue