Merge pull request #1212 from modelcontextprotocol/v2/phase-2-tools
Phase 2: Refactor Tools cluster to MCP SDK types
This commit is contained in:
commit
12c41cbed7
11 changed files with 430 additions and 415 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
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 } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
import { ToolControls } from "./ToolControls";
|
import { ToolControls } from "./ToolControls";
|
||||||
|
|
||||||
|
|
@ -15,22 +16,20 @@ const meta: Meta<typeof ToolControls> = {
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof ToolControls>;
|
type Story = StoryObj<typeof ToolControls>;
|
||||||
|
|
||||||
const sampleTools = [
|
const sampleTools: Tool[] = [
|
||||||
{
|
{
|
||||||
name: "send_message",
|
name: "send_message",
|
||||||
title: "Send Message",
|
title: "Send Message",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "create_record",
|
name: "create_record",
|
||||||
title: "Create Record",
|
title: "Create Record",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{ name: "delete_records", selected: false, onClick: fn() },
|
{ name: "delete_records", inputSchema: { type: "object" } },
|
||||||
{ name: "list_users", selected: false, onClick: fn() },
|
{ name: "list_users", inputSchema: { type: "object" } },
|
||||||
{ name: "batch_process", selected: false, onClick: fn() },
|
{ name: "batch_process", inputSchema: { type: "object" } },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
|
|
@ -41,9 +40,8 @@ export const Default: Story = {
|
||||||
|
|
||||||
export const WithSelection: Story = {
|
export const WithSelection: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools.map((t) =>
|
tools: sampleTools,
|
||||||
t.name === "create_record" ? { ...t, selected: true } : t,
|
selectedName: "create_record",
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Group, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
|
import { Group, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
|
||||||
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
||||||
import { ToolListItem } from "../ToolListItem/ToolListItem";
|
import { ToolListItem } from "../ToolListItem/ToolListItem";
|
||||||
import type { ToolListItemProps } from "../ToolListItem/ToolListItem";
|
|
||||||
|
|
||||||
export interface ToolControlsProps {
|
export interface ToolControlsProps {
|
||||||
tools: ToolListItemProps[];
|
tools: Tool[];
|
||||||
|
selectedName?: string;
|
||||||
listChanged: boolean;
|
listChanged: boolean;
|
||||||
onRefreshList: () => void;
|
onRefreshList: () => void;
|
||||||
onSelectTool: (name: string) => void;
|
onSelectTool: (name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function listMaxHeight(): string {
|
const LIST_MAX_HEIGHT =
|
||||||
return "calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)";
|
"calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)";
|
||||||
}
|
|
||||||
|
|
||||||
export function ToolControls({
|
export function ToolControls({
|
||||||
tools,
|
tools,
|
||||||
|
selectedName,
|
||||||
listChanged,
|
listChanged,
|
||||||
onRefreshList,
|
onRefreshList,
|
||||||
onSelectTool,
|
onSelectTool,
|
||||||
|
|
@ -42,13 +43,16 @@ export function ToolControls({
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => setSearchText(e.currentTarget.value)}
|
onChange={(e) => setSearchText(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<ScrollArea.Autosize mah={listMaxHeight()}>
|
<ScrollArea.Autosize mah={LIST_MAX_HEIGHT}>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{filteredTools.map((tool) => (
|
{filteredTools.map((tool) => (
|
||||||
<ToolListItem
|
<ToolListItem
|
||||||
key={tool.name}
|
key={tool.name}
|
||||||
{...tool}
|
tool={tool}
|
||||||
onClick={() => onSelectTool(tool.name)}
|
selected={tool.name === selectedName}
|
||||||
|
onClick={() => {
|
||||||
|
if (tool.name !== selectedName) onSelectTool(tool.name);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
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 } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
import { ToolDetailPanel } from "./ToolDetailPanel";
|
import { ToolDetailPanel } from "./ToolDetailPanel";
|
||||||
|
|
||||||
|
|
@ -17,83 +18,101 @@ const meta: Meta<typeof ToolDetailPanel> = {
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof ToolDetailPanel>;
|
type Story = StoryObj<typeof ToolDetailPanel>;
|
||||||
|
|
||||||
|
const sendMessageTool: Tool = {
|
||||||
|
name: "send_message",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
message: { type: "string", description: "The message to send" },
|
||||||
|
},
|
||||||
|
required: ["message"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecordTool: Tool = {
|
||||||
|
name: "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"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteRecordsTool: Tool = {
|
||||||
|
name: "delete_records",
|
||||||
|
description: "Deletes records matching the given criteria",
|
||||||
|
annotations: {
|
||||||
|
destructiveHint: true,
|
||||||
|
},
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
pattern: { type: "string", description: "Pattern to match records" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const longQueryTool: Tool = {
|
||||||
|
name: "long_query",
|
||||||
|
description: "Runs a long database query",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: { type: "string", description: "SQL query to execute" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const batchProcessTool: Tool = {
|
||||||
|
name: "batch_process",
|
||||||
|
description: "Processes items in batch",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
batchSize: { type: "number", description: "Number of items per batch" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const SimpleStringParam: Story = {
|
export const SimpleStringParam: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "send_message",
|
tool: sendMessageTool,
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
message: { type: "string", description: "The message to send" },
|
|
||||||
},
|
|
||||||
required: ["message"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultipleParams: Story = {
|
export const MultipleParams: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "create_record",
|
tool: createRecordTool,
|
||||||
description: "Creates a new record with the given parameters",
|
|
||||||
schema: {
|
|
||||||
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"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithAnnotations: Story = {
|
export const WithAnnotations: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "delete_records",
|
tool: deleteRecordsTool,
|
||||||
description: "Deletes records matching the given criteria",
|
|
||||||
annotations: {
|
|
||||||
audience: "admin",
|
|
||||||
readOnly: true,
|
|
||||||
destructive: true,
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
pattern: { type: "string", description: "Pattern to match records" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Executing: Story = {
|
export const Executing: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "long_query",
|
tool: longQueryTool,
|
||||||
description: "Runs a long database query",
|
|
||||||
isExecuting: true,
|
isExecuting: true,
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
query: { type: "string", description: "SQL query to execute" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
formValues: { query: "SELECT * FROM users" },
|
formValues: { query: "SELECT * FROM users" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithProgress: Story = {
|
export const WithProgress: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "batch_process",
|
tool: batchProcessTool,
|
||||||
description: "Processes items in batch",
|
|
||||||
isExecuting: true,
|
isExecuting: true,
|
||||||
progress: { percent: 60, description: "Processing step 3 of 5" },
|
progress: { progress: 3, total: 5, message: "Processing step 3 of 5" },
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
batchSize: { type: "number", description: "Number of items per batch" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
formValues: { batchSize: 100 },
|
formValues: { batchSize: 100 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,23 @@
|
||||||
import { Button, Divider, Group, Stack, Text } from "@mantine/core";
|
import { Button, Divider, Group, Stack, Text } from "@mantine/core";
|
||||||
|
import type {
|
||||||
|
ProgressNotification,
|
||||||
|
Tool,
|
||||||
|
ToolAnnotations,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge";
|
import { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge";
|
||||||
import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay";
|
import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay";
|
||||||
import { SchemaForm } from "../SchemaForm/SchemaForm";
|
import { SchemaForm } from "../SchemaForm/SchemaForm";
|
||||||
import type { JsonSchema } from "../SchemaForm/SchemaForm";
|
|
||||||
|
|
||||||
export interface ToolAnnotations {
|
export type ToolProgress = Pick<
|
||||||
audience?: string;
|
ProgressNotification["params"],
|
||||||
readOnly?: boolean;
|
"progress" | "total" | "message"
|
||||||
destructive?: boolean;
|
>;
|
||||||
longRunning?: boolean;
|
|
||||||
hints?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolDetailPanelProps {
|
export interface ToolDetailPanelProps {
|
||||||
name: string;
|
tool: Tool;
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
annotations?: ToolAnnotations;
|
|
||||||
schema: JsonSchema;
|
|
||||||
formValues: Record<string, unknown>;
|
formValues: Record<string, unknown>;
|
||||||
isExecuting: boolean;
|
isExecuting: boolean;
|
||||||
progress?: { percent: number; description?: string };
|
progress?: ToolProgress;
|
||||||
onFormChange: (values: Record<string, unknown>) => void;
|
onFormChange: (values: Record<string, unknown>) => void;
|
||||||
onExecute: () => void;
|
onExecute: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
|
@ -32,19 +29,13 @@ const ToolTitle = Text.withProps({
|
||||||
truncate: "end",
|
truncate: "end",
|
||||||
});
|
});
|
||||||
|
|
||||||
const HintsText = Text.withProps({
|
|
||||||
size: "xs",
|
|
||||||
c: "dimmed",
|
|
||||||
fs: "italic",
|
|
||||||
});
|
|
||||||
|
|
||||||
const DescriptionText = Text.withProps({
|
const DescriptionText = Text.withProps({
|
||||||
size: "sm",
|
size: "sm",
|
||||||
c: "dimmed",
|
c: "dimmed",
|
||||||
});
|
});
|
||||||
|
|
||||||
const CancelButton = Button.withProps({
|
const CancelButton = Button.withProps({
|
||||||
variant: "light",
|
variant: "subtle",
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -55,20 +46,15 @@ function resolveTitle(name: string, title?: string): string {
|
||||||
function hasAnyAnnotation(annotations?: ToolAnnotations): boolean {
|
function hasAnyAnnotation(annotations?: ToolAnnotations): boolean {
|
||||||
return !!(
|
return !!(
|
||||||
annotations &&
|
annotations &&
|
||||||
(annotations.audience ||
|
(annotations.readOnlyHint ||
|
||||||
annotations.readOnly ||
|
annotations.destructiveHint ||
|
||||||
annotations.destructive ||
|
annotations.idempotentHint ||
|
||||||
annotations.longRunning ||
|
annotations.openWorldHint)
|
||||||
annotations.hints)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolDetailPanel({
|
export function ToolDetailPanel({
|
||||||
name,
|
tool,
|
||||||
title,
|
|
||||||
description,
|
|
||||||
annotations,
|
|
||||||
schema,
|
|
||||||
formValues,
|
formValues,
|
||||||
isExecuting,
|
isExecuting,
|
||||||
progress,
|
progress,
|
||||||
|
|
@ -76,29 +62,25 @@ export function ToolDetailPanel({
|
||||||
onExecute,
|
onExecute,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: ToolDetailPanelProps) {
|
}: ToolDetailPanelProps) {
|
||||||
|
const { name, title, description, annotations, inputSchema } = tool;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md" miw={0}>
|
<Stack gap="md" miw={0}>
|
||||||
<ToolTitle>{resolveTitle(name, title)}</ToolTitle>
|
<ToolTitle>{resolveTitle(name, title)}</ToolTitle>
|
||||||
{hasAnyAnnotation(annotations) && annotations && (
|
{hasAnyAnnotation(annotations) && annotations && (
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
{annotations.audience && (
|
{annotations.readOnlyHint && (
|
||||||
<AnnotationBadge
|
|
||||||
facet="audience"
|
|
||||||
value={
|
|
||||||
annotations.audience.split(", ") as ("user" | "assistant")[]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{annotations.readOnly && (
|
|
||||||
<AnnotationBadge facet="readOnlyHint" value={true} />
|
<AnnotationBadge facet="readOnlyHint" value={true} />
|
||||||
)}
|
)}
|
||||||
{annotations.destructive && (
|
{annotations.destructiveHint && (
|
||||||
<AnnotationBadge facet="destructiveHint" value={true} />
|
<AnnotationBadge facet="destructiveHint" value={true} />
|
||||||
)}
|
)}
|
||||||
{annotations.longRunning && (
|
{annotations.idempotentHint && (
|
||||||
<AnnotationBadge facet="longRunHint" value={true} />
|
<AnnotationBadge facet="idempotentHint" value={true} />
|
||||||
|
)}
|
||||||
|
{annotations.openWorldHint && (
|
||||||
|
<AnnotationBadge facet="openWorldHint" value={true} />
|
||||||
)}
|
)}
|
||||||
{annotations.hints && <HintsText>{annotations.hints}</HintsText>}
|
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -107,20 +89,13 @@ export function ToolDetailPanel({
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<SchemaForm
|
<SchemaForm
|
||||||
schema={schema}
|
schema={inputSchema}
|
||||||
values={formValues}
|
values={formValues}
|
||||||
onChange={onFormChange}
|
onChange={onFormChange}
|
||||||
disabled={isExecuting}
|
disabled={isExecuting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{progress && (
|
{progress && <ProgressDisplay params={progress} />}
|
||||||
<ProgressDisplay
|
|
||||||
params={{
|
|
||||||
progress: progress.percent,
|
|
||||||
message: progress.description,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
{isExecuting && <CancelButton onClick={onCancel}>Cancel</CancelButton>}
|
{isExecuting && <CancelButton onClick={onCancel}>Cancel</CancelButton>}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
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 } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
import { ToolListItem } from "./ToolListItem";
|
import { ToolListItem } from "./ToolListItem";
|
||||||
|
|
||||||
|
|
@ -13,31 +14,44 @@ const meta: Meta<typeof ToolListItem> = {
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof ToolListItem>;
|
type Story = StoryObj<typeof ToolListItem>;
|
||||||
|
|
||||||
|
const weatherTool: Tool = {
|
||||||
|
name: "get_weather",
|
||||||
|
inputSchema: { type: "object" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const weatherToolWithTitle: Tool = {
|
||||||
|
name: "get_weather",
|
||||||
|
title: "Get Weather",
|
||||||
|
inputSchema: { type: "object" },
|
||||||
|
};
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "get_weather",
|
tool: weatherTool,
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Selected: Story = {
|
export const Selected: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "get_weather",
|
tool: weatherTool,
|
||||||
selected: true,
|
selected: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithTitle: Story = {
|
export const WithTitle: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "get_weather",
|
tool: weatherToolWithTitle,
|
||||||
title: "Get Weather",
|
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LongName: Story = {
|
export const LongName: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "this_is_a_very_long_tool_name_that_might_need_to_wrap_across_multiple_lines_in_the_ui",
|
tool: {
|
||||||
|
name: "this_is_a_very_long_tool_name_that_might_need_to_wrap_across_multiple_lines_in_the_ui",
|
||||||
|
inputSchema: { type: "object" },
|
||||||
|
},
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Stack, Text, UnstyledButton } from "@mantine/core";
|
import { Stack, Text, UnstyledButton } from "@mantine/core";
|
||||||
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
export interface ToolListItemProps {
|
export interface ToolListItemProps {
|
||||||
name: string;
|
tool: Tool;
|
||||||
title?: string;
|
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -22,12 +22,8 @@ function resolveLabel(name: string, title?: string): string {
|
||||||
return title ?? name;
|
return title ?? name;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolListItem({
|
export function ToolListItem({ tool, selected, onClick }: ToolListItemProps) {
|
||||||
name,
|
const { name, title } = tool;
|
||||||
title,
|
|
||||||
selected,
|
|
||||||
onClick,
|
|
||||||
}: ToolListItemProps) {
|
|
||||||
return (
|
return (
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
w="100%"
|
w="100%"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { fn } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
import { ToolResultPanel } from "./ToolResultPanel";
|
import { ToolResultPanel } from "./ToolResultPanel";
|
||||||
|
|
||||||
|
|
@ -13,58 +14,92 @@ const meta: Meta<typeof ToolResultPanel> = {
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof ToolResultPanel>;
|
type Story = StoryObj<typeof ToolResultPanel>;
|
||||||
|
|
||||||
|
const emptyResult: CallToolResult = {
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const textResult: CallToolResult = {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "The current weather in San Francisco is 65°F and sunny.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonResult: CallToolResult = {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: '{"temperature":65,"unit":"fahrenheit","condition":"sunny","city":"San Francisco"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageResult: CallToolResult = {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
||||||
|
mimeType: "image/png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mixedResult: CallToolResult = {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Here is the generated image based on your description:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
||||||
|
mimeType: "image/png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export const Empty: Story = {
|
export const Empty: Story = {
|
||||||
args: {
|
args: {
|
||||||
content: [],
|
result: emptyResult,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextResult: Story = {
|
export const TextResult: Story = {
|
||||||
args: {
|
args: {
|
||||||
content: [
|
result: textResult,
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: "The current weather in San Francisco is 65°F and sunny.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const JsonResult: Story = {
|
export const JsonResult: Story = {
|
||||||
args: {
|
args: {
|
||||||
content: [
|
result: jsonResult,
|
||||||
{
|
|
||||||
type: "json",
|
|
||||||
text: '{"temperature":65,"unit":"fahrenheit","condition":"sunny","city":"San Francisco"}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageResult: Story = {
|
export const ImageResult: Story = {
|
||||||
args: {
|
args: {
|
||||||
content: [
|
result: imageResult,
|
||||||
{
|
|
||||||
type: "image",
|
|
||||||
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
|
||||||
mimeType: "image/png",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MixedContent: Story = {
|
export const MixedContent: Story = {
|
||||||
args: {
|
args: {
|
||||||
content: [
|
result: mixedResult,
|
||||||
{
|
},
|
||||||
type: "text",
|
};
|
||||||
text: "Here is the generated image based on your description:",
|
|
||||||
},
|
export const ErrorResult: Story = {
|
||||||
{
|
args: {
|
||||||
type: "image",
|
result: {
|
||||||
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
isError: true,
|
||||||
mimeType: "image/png",
|
content: [
|
||||||
},
|
{
|
||||||
],
|
type: "text",
|
||||||
|
text: "ENOENT: no such file or directory, open '/data/missing.txt'",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,9 @@
|
||||||
import { Button, Group, Stack, Text, Title } from "@mantine/core";
|
import { Alert, Button, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js";
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
|
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
|
||||||
|
|
||||||
export interface ResultContentItem {
|
|
||||||
type: string;
|
|
||||||
text?: string;
|
|
||||||
data?: string;
|
|
||||||
mimeType?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolResultPanelProps {
|
export interface ToolResultPanelProps {
|
||||||
content: ResultContentItem[];
|
result: CallToolResult;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,41 +12,31 @@ const ClearButton = Button.withProps({
|
||||||
size: "sm",
|
size: "sm",
|
||||||
});
|
});
|
||||||
|
|
||||||
function toContentBlock(item: ResultContentItem): ContentBlock | null {
|
export function ToolResultPanel({ result, onClear }: ToolResultPanelProps) {
|
||||||
if (item.text !== undefined) {
|
|
||||||
return { type: "text", text: item.text };
|
|
||||||
}
|
|
||||||
if (item.data !== undefined && item.mimeType?.startsWith("image")) {
|
|
||||||
return { type: "image", data: item.data, mimeType: item.mimeType };
|
|
||||||
}
|
|
||||||
if (item.data !== undefined && item.mimeType) {
|
|
||||||
return { type: "audio", data: item.data, mimeType: item.mimeType };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ToolResultPanel({ content, onClear }: ToolResultPanelProps) {
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={4}>Results</Title>
|
<Title order={4}>Results</Title>
|
||||||
<ClearButton onClick={onClear}>Clear</ClearButton>
|
<ClearButton onClick={onClear}>Clear</ClearButton>
|
||||||
</Group>
|
</Group>
|
||||||
{content.length === 0 ? (
|
{result.isError ? (
|
||||||
|
<Alert color="red" variant="light" title="Tool Error">
|
||||||
|
{result.content
|
||||||
|
.filter((b) => b.type === "text")
|
||||||
|
.map((b) => b.text)
|
||||||
|
.join("\n")}
|
||||||
|
</Alert>
|
||||||
|
) : result.content.length === 0 ? (
|
||||||
<Text c="dimmed">No results yet</Text>
|
<Text c="dimmed">No results yet</Text>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{content.map((item, index) => {
|
{result.content.map((block, index) => (
|
||||||
const block = toContentBlock(item);
|
<ContentViewer
|
||||||
if (!block) return null;
|
key={index}
|
||||||
return (
|
block={block}
|
||||||
<ContentViewer
|
copyable={block.type === "text"}
|
||||||
key={index}
|
/>
|
||||||
block={block}
|
))}
|
||||||
copyable={block.type === "text"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
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 } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
import { ToolsScreen } from "./ToolsScreen";
|
import { ToolsScreen } from "./ToolsScreen";
|
||||||
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
|
import type { ToolCallState } from "./ToolsScreen";
|
||||||
import type { ToolDetailPanelProps } from "../../groups/ToolDetailPanel/ToolDetailPanel";
|
|
||||||
import type { ToolResultPanelProps } from "../../groups/ToolResultPanel/ToolResultPanel";
|
|
||||||
|
|
||||||
const meta: Meta<typeof ToolsScreen> = {
|
const meta: Meta<typeof ToolsScreen> = {
|
||||||
title: "Screens/ToolsScreen",
|
title: "Screens/ToolsScreen",
|
||||||
|
|
@ -13,85 +12,65 @@ const meta: Meta<typeof ToolsScreen> = {
|
||||||
listChanged: false,
|
listChanged: false,
|
||||||
onRefreshList: fn(),
|
onRefreshList: fn(),
|
||||||
onSelectTool: fn(),
|
onSelectTool: fn(),
|
||||||
|
onCallTool: fn(),
|
||||||
|
onCancelCall: fn(),
|
||||||
|
onClearResult: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof ToolsScreen>;
|
type Story = StoryObj<typeof ToolsScreen>;
|
||||||
|
|
||||||
const sampleTools: ToolListItemProps[] = [
|
const sampleTools: Tool[] = [
|
||||||
{
|
{
|
||||||
name: "send_message",
|
name: "send_message",
|
||||||
title: "Send Message",
|
title: "Send Message",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "create_record",
|
name: "create_record",
|
||||||
title: "Create Record",
|
title: "Create Record",
|
||||||
selected: false,
|
description: "Creates a new record with the given parameters",
|
||||||
onClick: fn(),
|
inputSchema: {
|
||||||
},
|
type: "object",
|
||||||
{
|
properties: {
|
||||||
name: "delete_records",
|
title: { type: "string", description: "Record title" },
|
||||||
selected: false,
|
count: { type: "number", description: "Number of items" },
|
||||||
onClick: fn(),
|
enabled: {
|
||||||
},
|
type: "boolean",
|
||||||
{ name: "list_users", selected: false, onClick: fn() },
|
description: "Whether the record is active",
|
||||||
{
|
},
|
||||||
name: "batch_process",
|
},
|
||||||
selected: false,
|
required: ["title"],
|
||||||
onClick: fn(),
|
},
|
||||||
},
|
},
|
||||||
|
{ name: "delete_records", inputSchema: { type: "object" } },
|
||||||
|
{ name: "list_users", inputSchema: { type: "object" } },
|
||||||
|
{ name: "batch_process", inputSchema: { type: "object" } },
|
||||||
];
|
];
|
||||||
|
|
||||||
const selectedToolData: ToolDetailPanelProps = {
|
const resultState: ToolCallState = {
|
||||||
name: "create_record",
|
status: "ok",
|
||||||
title: "Create Record",
|
result: {
|
||||||
description: "Creates a new record with the given parameters",
|
content: [
|
||||||
schema: {
|
{
|
||||||
type: "object",
|
type: "text",
|
||||||
properties: {
|
text: JSON.stringify(
|
||||||
title: { type: "string", description: "Record title" },
|
{
|
||||||
count: { type: "number", description: "Number of items" },
|
id: 42,
|
||||||
enabled: { type: "boolean", description: "Whether the record is active" },
|
title: "New Record",
|
||||||
},
|
count: 5,
|
||||||
required: ["title"],
|
enabled: true,
|
||||||
|
createdAt: "2026-03-17T12:00:00Z",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
formValues: {},
|
|
||||||
isExecuting: false,
|
|
||||||
onFormChange: fn(),
|
|
||||||
onExecute: fn(),
|
|
||||||
onCancel: fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resultData: ToolResultPanelProps = {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(
|
|
||||||
{
|
|
||||||
id: 42,
|
|
||||||
title: "New Record",
|
|
||||||
count: 5,
|
|
||||||
enabled: true,
|
|
||||||
createdAt: "2026-03-17T12:00:00Z",
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onClear: fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
function toolsWithSelected(selectedName: string): ToolListItemProps[] {
|
|
||||||
return sampleTools.map((tool) => ({
|
|
||||||
...tool,
|
|
||||||
selected: tool.name === selectedName,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NoSelection: Story = {
|
export const NoSelection: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: sampleTools,
|
tools: sampleTools,
|
||||||
|
|
@ -100,16 +79,16 @@ export const NoSelection: Story = {
|
||||||
|
|
||||||
export const ToolSelected: Story = {
|
export const ToolSelected: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: toolsWithSelected("create_record"),
|
tools: sampleTools,
|
||||||
selectedTool: selectedToolData,
|
selectedToolName: "create_record",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithResult: Story = {
|
export const WithResult: Story = {
|
||||||
args: {
|
args: {
|
||||||
tools: toolsWithSelected("create_record"),
|
tools: sampleTools,
|
||||||
selectedTool: selectedToolData,
|
selectedToolName: "create_record",
|
||||||
result: resultData,
|
callState: resultState,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -119,42 +98,52 @@ export const LongToolName: Story = {
|
||||||
...sampleTools,
|
...sampleTools,
|
||||||
{
|
{
|
||||||
name: "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
|
name: "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
|
||||||
selected: true,
|
description:
|
||||||
onClick: fn(),
|
"Executes a complex multi-table join query across the organization's internal database with support for aggregation functions, nested filtering, and pagination of large result sets.",
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
},
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
primary_table_name: {
|
||||||
|
type: "string",
|
||||||
|
description: "The main table to query from",
|
||||||
|
},
|
||||||
|
join_configuration: {
|
||||||
|
type: "string",
|
||||||
|
description: "JSON configuration for table joins",
|
||||||
|
},
|
||||||
|
aggregation_functions: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"Comma-separated list of aggregation functions to apply",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["primary_table_name"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
selectedTool: {
|
selectedToolName:
|
||||||
name: "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
|
"organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
|
||||||
annotations: {
|
},
|
||||||
readOnly: true,
|
};
|
||||||
longRunning: true,
|
|
||||||
|
export const WithError: Story = {
|
||||||
|
args: {
|
||||||
|
tools: sampleTools,
|
||||||
|
selectedToolName: "create_record",
|
||||||
|
callState: {
|
||||||
|
status: "error",
|
||||||
|
result: {
|
||||||
|
isError: true,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: 'Error executing tool "create_record": ECONNREFUSED 127.0.0.1:5432 — could not connect to the database server. The server may not be running or may be unreachable at the configured host and port. Please verify that PostgreSQL is started, the connection string is correct, and any firewall rules allow traffic on port 5432. If this is a transient issue, retrying after a short delay may resolve it.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
description:
|
|
||||||
"Executes a complex multi-table join query across the organization's internal database with support for aggregation functions, nested filtering, and pagination of large result sets.",
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
primary_table_name: {
|
|
||||||
type: "string",
|
|
||||||
description: "The main table to query from",
|
|
||||||
},
|
|
||||||
join_configuration: {
|
|
||||||
type: "string",
|
|
||||||
description: "JSON configuration for table joins",
|
|
||||||
},
|
|
||||||
aggregation_functions: {
|
|
||||||
type: "string",
|
|
||||||
description:
|
|
||||||
"Comma-separated list of aggregation functions to apply",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["primary_table_name"],
|
|
||||||
},
|
|
||||||
formValues: {},
|
|
||||||
isExecuting: false,
|
|
||||||
onFormChange: fn(),
|
|
||||||
onExecute: fn(),
|
|
||||||
onCancel: fn(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,30 @@
|
||||||
|
import { useState } from "react";
|
||||||
import { Card, Flex, Stack, Text } from "@mantine/core";
|
import { Card, Flex, Stack, Text } from "@mantine/core";
|
||||||
|
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { ToolControls } from "../../groups/ToolControls/ToolControls";
|
import { ToolControls } from "../../groups/ToolControls/ToolControls";
|
||||||
import { ToolDetailPanel } from "../../groups/ToolDetailPanel/ToolDetailPanel";
|
import {
|
||||||
|
ToolDetailPanel,
|
||||||
|
type ToolProgress,
|
||||||
|
} from "../../groups/ToolDetailPanel/ToolDetailPanel";
|
||||||
import { ToolResultPanel } from "../../groups/ToolResultPanel/ToolResultPanel";
|
import { ToolResultPanel } from "../../groups/ToolResultPanel/ToolResultPanel";
|
||||||
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
|
|
||||||
import type { ToolDetailPanelProps } from "../../groups/ToolDetailPanel/ToolDetailPanel";
|
export interface ToolCallState {
|
||||||
import type { ToolResultPanelProps } from "../../groups/ToolResultPanel/ToolResultPanel";
|
status: "idle" | "pending" | "ok" | "error";
|
||||||
|
result?: CallToolResult;
|
||||||
|
error?: string;
|
||||||
|
progress?: ToolProgress;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ToolsScreenProps {
|
export interface ToolsScreenProps {
|
||||||
tools: ToolListItemProps[];
|
tools: Tool[];
|
||||||
selectedTool?: ToolDetailPanelProps;
|
selectedToolName?: string;
|
||||||
result?: ToolResultPanelProps;
|
callState?: ToolCallState;
|
||||||
listChanged: boolean;
|
listChanged: boolean;
|
||||||
onRefreshList: () => void;
|
onRefreshList: () => void;
|
||||||
onSelectTool: (name: string) => void;
|
onSelectTool: (name: string) => void;
|
||||||
|
onCallTool: (name: string, args: Record<string, unknown>) => void;
|
||||||
|
onCancelCall?: () => void;
|
||||||
|
onClearResult?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScreenLayout = Flex.withProps({
|
const ScreenLayout = Flex.withProps({
|
||||||
|
|
@ -46,36 +58,60 @@ const EmptyState = Text.withProps({
|
||||||
|
|
||||||
export function ToolsScreen({
|
export function ToolsScreen({
|
||||||
tools,
|
tools,
|
||||||
selectedTool,
|
selectedToolName,
|
||||||
result,
|
callState,
|
||||||
listChanged,
|
listChanged,
|
||||||
onRefreshList,
|
onRefreshList,
|
||||||
onSelectTool,
|
onSelectTool,
|
||||||
|
onCallTool,
|
||||||
|
onCancelCall,
|
||||||
|
onClearResult,
|
||||||
}: ToolsScreenProps) {
|
}: ToolsScreenProps) {
|
||||||
|
const [formValues, setFormValues] = useState<Record<string, unknown>>({});
|
||||||
|
const selectedTool = selectedToolName
|
||||||
|
? tools.find((t) => t.name === selectedToolName)
|
||||||
|
: undefined;
|
||||||
|
const isExecuting = callState?.status === "pending";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenLayout>
|
<ScreenLayout>
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<SidebarCard>
|
<SidebarCard>
|
||||||
<ToolControls
|
<ToolControls
|
||||||
tools={tools}
|
tools={tools}
|
||||||
|
selectedName={selectedToolName}
|
||||||
listChanged={listChanged}
|
listChanged={listChanged}
|
||||||
onRefreshList={onRefreshList}
|
onRefreshList={onRefreshList}
|
||||||
onSelectTool={onSelectTool}
|
onSelectTool={(name) => {
|
||||||
|
setFormValues({});
|
||||||
|
onSelectTool(name);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</SidebarCard>
|
</SidebarCard>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
||||||
<ContentCard flex={1}>
|
<ContentCard flex={1}>
|
||||||
{selectedTool ? (
|
{selectedTool ? (
|
||||||
<ToolDetailPanel {...selectedTool} />
|
<ToolDetailPanel
|
||||||
|
tool={selectedTool}
|
||||||
|
formValues={formValues}
|
||||||
|
isExecuting={isExecuting}
|
||||||
|
progress={callState?.progress}
|
||||||
|
onFormChange={setFormValues}
|
||||||
|
onExecute={() => onCallTool(selectedTool.name, formValues)}
|
||||||
|
onCancel={() => onCancelCall?.()}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState>Select a tool to view details</EmptyState>
|
<EmptyState>Select a tool to view details</EmptyState>
|
||||||
)}
|
)}
|
||||||
</ContentCard>
|
</ContentCard>
|
||||||
|
|
||||||
<ContentCard flex={1}>
|
<ContentCard flex={1}>
|
||||||
{result ? (
|
{callState?.result ? (
|
||||||
<ToolResultPanel {...result} />
|
<ToolResultPanel
|
||||||
|
result={callState.result}
|
||||||
|
onClear={() => onClearResult?.()}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState>Results will appear here</EmptyState>
|
<EmptyState>Results will appear here</EmptyState>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -173,228 +173,194 @@ export const ToolsActive: Story = {
|
||||||
{
|
{
|
||||||
name: "send_message",
|
name: "send_message",
|
||||||
title: "Send Message",
|
title: "Send Message",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "create_record",
|
name: "create_record",
|
||||||
title: "Create Record",
|
title: "Create Record",
|
||||||
selected: true,
|
description: "Creates a new record with the given parameters",
|
||||||
onClick: fn(),
|
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",
|
name: "delete_records",
|
||||||
title: "Delete Records",
|
title: "Delete Records",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "list_users",
|
name: "list_users",
|
||||||
title: "List Users",
|
title: "List Users",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "batch_process",
|
name: "batch_process",
|
||||||
title: "Batch Process",
|
title: "Batch Process",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "search_files",
|
name: "search_files",
|
||||||
title: "Search Files",
|
title: "Search Files",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "read_file",
|
name: "read_file",
|
||||||
title: "Read File",
|
title: "Read File",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "write_file",
|
name: "write_file",
|
||||||
title: "Write File",
|
title: "Write File",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "get_schema",
|
name: "get_schema",
|
||||||
title: "Get Schema",
|
title: "Get Schema",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "run_query",
|
name: "run_query",
|
||||||
title: "Run Query",
|
title: "Run Query",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "create_migration",
|
name: "create_migration",
|
||||||
title: "Create Migration",
|
title: "Create Migration",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "validate_config",
|
name: "validate_config",
|
||||||
title: "Validate Config",
|
title: "Validate Config",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "deploy_service",
|
name: "deploy_service",
|
||||||
title: "Deploy Service",
|
title: "Deploy Service",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "rollback_deploy",
|
name: "rollback_deploy",
|
||||||
title: "Rollback Deploy",
|
title: "Rollback Deploy",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "check_health",
|
name: "check_health",
|
||||||
title: "Check Health",
|
title: "Check Health",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "generate_report",
|
name: "generate_report",
|
||||||
title: "Generate Report",
|
title: "Generate Report",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "export_csv",
|
name: "export_csv",
|
||||||
title: "Export CSV",
|
title: "Export CSV",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "import_data",
|
name: "import_data",
|
||||||
title: "Import Data",
|
title: "Import Data",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "send_notification",
|
name: "send_notification",
|
||||||
title: "Send Notification",
|
title: "Send Notification",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "schedule_task",
|
name: "schedule_task",
|
||||||
title: "Schedule Task",
|
title: "Schedule Task",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "cancel_task",
|
name: "cancel_task",
|
||||||
title: "Cancel Task",
|
title: "Cancel Task",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "list_connections",
|
name: "list_connections",
|
||||||
title: "List Connections",
|
title: "List Connections",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "refresh_cache",
|
name: "refresh_cache",
|
||||||
title: "Refresh Cache",
|
title: "Refresh Cache",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "purge_logs",
|
name: "purge_logs",
|
||||||
title: "Purge Logs",
|
title: "Purge Logs",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "encrypt_secret",
|
name: "encrypt_secret",
|
||||||
title: "Encrypt Secret",
|
title: "Encrypt Secret",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "rotate_keys",
|
name: "rotate_keys",
|
||||||
title: "Rotate Keys",
|
title: "Rotate Keys",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "sync_resources",
|
name: "sync_resources",
|
||||||
title: "Sync Resources",
|
title: "Sync Resources",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "compile_template",
|
name: "compile_template",
|
||||||
title: "Compile Template",
|
title: "Compile Template",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "analyze_performance",
|
name: "analyze_performance",
|
||||||
title: "Analyze Performance",
|
title: "Analyze Performance",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "resize_image",
|
name: "resize_image",
|
||||||
title: "Resize Image",
|
title: "Resize Image",
|
||||||
selected: false,
|
inputSchema: { type: "object" },
|
||||||
onClick: fn(),
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
selectedTool={{
|
selectedToolName="create_record"
|
||||||
name: "create_record",
|
callState={{
|
||||||
title: "Create Record",
|
status: "ok",
|
||||||
description: "Creates a new record with the given parameters",
|
result: {
|
||||||
schema: {
|
content: [
|
||||||
type: "object",
|
{
|
||||||
properties: {
|
type: "text",
|
||||||
title: { type: "string", description: "Record title" },
|
text: JSON.stringify(
|
||||||
count: { type: "number", description: "Number of items" },
|
{
|
||||||
enabled: {
|
id: 42,
|
||||||
type: "boolean",
|
title: "New Record",
|
||||||
description: "Whether the record is active",
|
count: 5,
|
||||||
|
enabled: true,
|
||||||
|
createdAt: "2026-03-17T12:00:00Z",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
required: ["title"],
|
|
||||||
},
|
},
|
||||||
formValues: {},
|
|
||||||
isExecuting: false,
|
|
||||||
onFormChange: fn(),
|
|
||||||
onExecute: fn(),
|
|
||||||
onCancel: fn(),
|
|
||||||
}}
|
|
||||||
result={{
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(
|
|
||||||
{
|
|
||||||
id: 42,
|
|
||||||
title: "New Record",
|
|
||||||
count: 5,
|
|
||||||
enabled: true,
|
|
||||||
createdAt: "2026-03-17T12:00:00Z",
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onClear: fn(),
|
|
||||||
}}
|
}}
|
||||||
listChanged={true}
|
listChanged={true}
|
||||||
onRefreshList={fn()}
|
onRefreshList={fn()}
|
||||||
onSelectTool={fn()}
|
onSelectTool={fn()}
|
||||||
|
onCallTool={fn()}
|
||||||
|
onCancelCall={fn()}
|
||||||
|
onClearResult={fn()}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue