Phase 2: Refactor Tools cluster to use MCP SDK types

Replace ad hoc local types with MCP SDK schema types across the Tools
group components and ToolsScreen, per the v2 UX interfaces plan.

- ToolListItem: accept `tool: Tool` instead of flat `name`/`title`
- ToolControls: accept `Tool[]` + `selectedName` instead of `ToolListItemProps[]`
- ToolDetailPanel: accept `tool: Tool` instead of flat props; use SDK
  `ToolAnnotations` (readOnlyHint/destructiveHint/idempotentHint/openWorldHint);
  use `ProgressNotification['params']` for progress
- ToolResultPanel: accept `CallToolResult` instead of `ResultContentItem[]`
- ToolsScreen: accept `Tool[]` + `selectedToolName` + `ToolCallState`;
  derive selected tool via `tools.find()`; own `formValues` state locally
- Delete local `ToolAnnotations`, `ResultContentItem` types
- Update all stories with real SDK `Tool` fixtures
- Update ConnectedView ToolsActive story

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-18 16:57:10 -04:00
commit 92ecfcbd6c
11 changed files with 388 additions and 411 deletions

View file

@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test";
import { ToolControls } from "./ToolControls";
@ -15,22 +16,20 @@ const meta: Meta<typeof ToolControls> = {
export default meta;
type Story = StoryObj<typeof ToolControls>;
const sampleTools = [
const sampleTools: Tool[] = [
{
name: "send_message",
title: "Send Message",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "create_record",
title: "Create Record",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{ name: "delete_records", selected: false, onClick: fn() },
{ name: "list_users", selected: false, onClick: fn() },
{ name: "batch_process", selected: false, onClick: fn() },
{ name: "delete_records", inputSchema: { type: "object" } },
{ name: "list_users", inputSchema: { type: "object" } },
{ name: "batch_process", inputSchema: { type: "object" } },
];
export const Default: Story = {
@ -41,9 +40,8 @@ export const Default: Story = {
export const WithSelection: Story = {
args: {
tools: sampleTools.map((t) =>
t.name === "create_record" ? { ...t, selected: true } : t,
),
tools: sampleTools,
selectedName: "create_record",
},
};

View file

@ -1,11 +1,12 @@
import { useState } from "react";
import { Group, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { ToolListItem } from "../ToolListItem/ToolListItem";
import type { ToolListItemProps } from "../ToolListItem/ToolListItem";
export interface ToolControlsProps {
tools: ToolListItemProps[];
tools: Tool[];
selectedName?: string;
listChanged: boolean;
onRefreshList: () => void;
onSelectTool: (name: string) => void;
@ -17,6 +18,7 @@ function listMaxHeight(): string {
export function ToolControls({
tools,
selectedName,
listChanged,
onRefreshList,
onSelectTool,
@ -47,8 +49,11 @@ export function ToolControls({
{filteredTools.map((tool) => (
<ToolListItem
key={tool.name}
{...tool}
onClick={() => onSelectTool(tool.name)}
tool={tool}
selected={tool.name === selectedName}
onClick={() => {
if (tool.name !== selectedName) onSelectTool(tool.name);
}}
/>
))}
</Stack>

View file

@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test";
import { ToolDetailPanel } from "./ToolDetailPanel";
@ -17,83 +18,102 @@ const meta: Meta<typeof ToolDetailPanel> = {
export default meta;
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: {
readOnlyHint: true,
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 = {
args: {
name: "send_message",
schema: {
type: "object",
properties: {
message: { type: "string", description: "The message to send" },
},
required: ["message"],
},
tool: sendMessageTool,
},
};
export const MultipleParams: Story = {
args: {
name: "create_record",
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"],
},
tool: createRecordTool,
},
};
export const WithAnnotations: Story = {
args: {
name: "delete_records",
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" },
},
},
tool: deleteRecordsTool,
},
};
export const Executing: Story = {
args: {
name: "long_query",
description: "Runs a long database query",
tool: longQueryTool,
isExecuting: true,
schema: {
type: "object",
properties: {
query: { type: "string", description: "SQL query to execute" },
},
},
formValues: { query: "SELECT * FROM users" },
},
};
export const WithProgress: Story = {
args: {
name: "batch_process",
description: "Processes items in batch",
tool: batchProcessTool,
isExecuting: true,
progress: { percent: 60, description: "Processing step 3 of 5" },
schema: {
type: "object",
properties: {
batchSize: { type: "number", description: "Number of items per batch" },
},
},
progress: { progress: 3, total: 5, message: "Processing step 3 of 5" },
formValues: { batchSize: 100 },
},
};

View file

@ -1,26 +1,21 @@
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 { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay";
import { SchemaForm } from "../SchemaForm/SchemaForm";
import type { JsonSchema } from "../SchemaForm/SchemaForm";
export interface ToolAnnotations {
audience?: string;
readOnly?: boolean;
destructive?: boolean;
longRunning?: boolean;
hints?: string;
}
export interface ToolDetailPanelProps {
name: string;
title?: string;
description?: string;
annotations?: ToolAnnotations;
schema: JsonSchema;
tool: Tool;
formValues: Record<string, unknown>;
isExecuting: boolean;
progress?: { percent: number; description?: string };
progress?: Pick<
ProgressNotification["params"],
"progress" | "total" | "message"
>;
onFormChange: (values: Record<string, unknown>) => void;
onExecute: () => void;
onCancel: () => void;
@ -32,19 +27,13 @@ const ToolTitle = Text.withProps({
truncate: "end",
});
const HintsText = Text.withProps({
size: "xs",
c: "dimmed",
fs: "italic",
});
const DescriptionText = Text.withProps({
size: "sm",
c: "dimmed",
});
const CancelButton = Button.withProps({
variant: "light",
variant: "subtle",
color: "red",
});
@ -55,20 +44,16 @@ function resolveTitle(name: string, title?: string): string {
function hasAnyAnnotation(annotations?: ToolAnnotations): boolean {
return !!(
annotations &&
(annotations.audience ||
annotations.readOnly ||
annotations.destructive ||
annotations.longRunning ||
annotations.hints)
(annotations.title ||
annotations.readOnlyHint ||
annotations.destructiveHint ||
annotations.idempotentHint ||
annotations.openWorldHint)
);
}
export function ToolDetailPanel({
name,
title,
description,
annotations,
schema,
tool,
formValues,
isExecuting,
progress,
@ -76,29 +61,25 @@ export function ToolDetailPanel({
onExecute,
onCancel,
}: ToolDetailPanelProps) {
const { name, title, description, annotations, inputSchema } = tool;
return (
<Stack gap="md" miw={0}>
<ToolTitle>{resolveTitle(name, title)}</ToolTitle>
{hasAnyAnnotation(annotations) && annotations && (
<Group gap="xs">
{annotations.audience && (
<AnnotationBadge
facet="audience"
value={
annotations.audience.split(", ") as ("user" | "assistant")[]
}
/>
)}
{annotations.readOnly && (
{annotations.readOnlyHint && (
<AnnotationBadge facet="readOnlyHint" value={true} />
)}
{annotations.destructive && (
{annotations.destructiveHint && (
<AnnotationBadge facet="destructiveHint" value={true} />
)}
{annotations.longRunning && (
<AnnotationBadge facet="longRunHint" value={true} />
{annotations.idempotentHint && (
<AnnotationBadge facet="idempotentHint" value={true} />
)}
{annotations.openWorldHint && (
<AnnotationBadge facet="openWorldHint" value={true} />
)}
{annotations.hints && <HintsText>{annotations.hints}</HintsText>}
</Group>
)}
@ -107,20 +88,13 @@ export function ToolDetailPanel({
<Divider />
<SchemaForm
schema={schema}
schema={inputSchema}
values={formValues}
onChange={onFormChange}
disabled={isExecuting}
/>
{progress && (
<ProgressDisplay
params={{
progress: progress.percent,
message: progress.description,
}}
/>
)}
{progress && <ProgressDisplay params={progress} />}
<Group justify="flex-end">
{isExecuting && <CancelButton onClick={onCancel}>Cancel</CancelButton>}

View file

@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test";
import { ToolListItem } from "./ToolListItem";
@ -13,31 +14,44 @@ const meta: Meta<typeof ToolListItem> = {
export default meta;
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 = {
args: {
name: "get_weather",
tool: weatherTool,
selected: false,
},
};
export const Selected: Story = {
args: {
name: "get_weather",
tool: weatherTool,
selected: true,
},
};
export const WithTitle: Story = {
args: {
name: "get_weather",
title: "Get Weather",
tool: weatherToolWithTitle,
selected: false,
},
};
export const LongName: Story = {
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,
},
};

View file

@ -1,8 +1,8 @@
import { Stack, Text, UnstyledButton } from "@mantine/core";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
export interface ToolListItemProps {
name: string;
title?: string;
tool: Tool;
selected: boolean;
onClick: () => void;
}
@ -22,12 +22,8 @@ function resolveLabel(name: string, title?: string): string {
return title ?? name;
}
export function ToolListItem({
name,
title,
selected,
onClick,
}: ToolListItemProps) {
export function ToolListItem({ tool, selected, onClick }: ToolListItemProps) {
const { name, title } = tool;
return (
<UnstyledButton
w="100%"

View file

@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test";
import { ToolResultPanel } from "./ToolResultPanel";
@ -13,58 +14,78 @@ const meta: Meta<typeof ToolResultPanel> = {
export default meta;
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 = {
args: {
content: [],
result: emptyResult,
},
};
export const TextResult: Story = {
args: {
content: [
{
type: "text",
text: "The current weather in San Francisco is 65°F and sunny.",
},
],
result: textResult,
},
};
export const JsonResult: Story = {
args: {
content: [
{
type: "json",
text: '{"temperature":65,"unit":"fahrenheit","condition":"sunny","city":"San Francisco"}',
},
],
result: jsonResult,
},
};
export const ImageResult: Story = {
args: {
content: [
{
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
mimeType: "image/png",
},
],
result: imageResult,
},
};
export const MixedContent: Story = {
args: {
content: [
{
type: "text",
text: "Here is the generated image based on your description:",
},
{
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
mimeType: "image/png",
},
],
result: mixedResult,
},
};

View file

@ -1,16 +1,9 @@
import { 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";
export interface ResultContentItem {
type: string;
text?: string;
data?: string;
mimeType?: string;
}
export interface ToolResultPanelProps {
content: ResultContentItem[];
result: CallToolResult;
onClear: () => void;
}
@ -19,41 +12,24 @@ const ClearButton = Button.withProps({
size: "sm",
});
function toContentBlock(item: ResultContentItem): ContentBlock | null {
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) {
export function ToolResultPanel({ result, onClear }: ToolResultPanelProps) {
return (
<Stack>
<Group justify="space-between">
<Title order={4}>Results</Title>
<ClearButton onClick={onClear}>Clear</ClearButton>
</Group>
{content.length === 0 ? (
{result.content.length === 0 ? (
<Text c="dimmed">No results yet</Text>
) : (
<>
{content.map((item, index) => {
const block = toContentBlock(item);
if (!block) return null;
return (
<ContentViewer
key={index}
block={block}
copyable={block.type === "text"}
/>
);
})}
{result.content.map((block, index) => (
<ContentViewer
key={index}
block={block}
copyable={block.type === "text"}
/>
))}
</>
)}
</Stack>

View file

@ -1,9 +1,8 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test";
import { ToolsScreen } from "./ToolsScreen";
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
import type { ToolDetailPanelProps } from "../../groups/ToolDetailPanel/ToolDetailPanel";
import type { ToolResultPanelProps } from "../../groups/ToolResultPanel/ToolResultPanel";
import type { ToolCallState } from "./ToolsScreen";
const meta: Meta<typeof ToolsScreen> = {
title: "Screens/ToolsScreen",
@ -13,85 +12,65 @@ const meta: Meta<typeof ToolsScreen> = {
listChanged: false,
onRefreshList: fn(),
onSelectTool: fn(),
onCallTool: fn(),
onCancelCall: fn(),
onClearResult: fn(),
},
};
export default meta;
type Story = StoryObj<typeof ToolsScreen>;
const sampleTools: ToolListItemProps[] = [
const sampleTools: Tool[] = [
{
name: "send_message",
title: "Send Message",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "create_record",
title: "Create Record",
selected: false,
onClick: fn(),
},
{
name: "delete_records",
selected: false,
onClick: fn(),
},
{ name: "list_users", selected: false, onClick: fn() },
{
name: "batch_process",
selected: false,
onClick: fn(),
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" } },
{ name: "batch_process", inputSchema: { type: "object" } },
];
const selectedToolData: ToolDetailPanelProps = {
name: "create_record",
title: "Create Record",
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"],
const resultState: ToolCallState = {
status: "ok",
result: {
content: [
{
type: "text",
text: JSON.stringify(
{
id: 42,
title: "New Record",
count: 5,
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 = {
args: {
tools: sampleTools,
@ -100,16 +79,16 @@ export const NoSelection: Story = {
export const ToolSelected: Story = {
args: {
tools: toolsWithSelected("create_record"),
selectedTool: selectedToolData,
tools: sampleTools,
selectedToolName: "create_record",
},
};
export const WithResult: Story = {
args: {
tools: toolsWithSelected("create_record"),
selectedTool: selectedToolData,
result: resultData,
tools: sampleTools,
selectedToolName: "create_record",
callState: resultState,
},
};
@ -119,43 +98,34 @@ export const LongToolName: Story = {
...sampleTools,
{
name: "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
selected: true,
onClick: fn(),
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.",
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: {
name: "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
annotations: {
readOnly: true,
longRunning: true,
},
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(),
},
selectedToolName:
"organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering",
},
};

View file

@ -1,18 +1,34 @@
import { useState } from "react";
import { Card, Flex, Stack, Text } from "@mantine/core";
import type {
CallToolResult,
ProgressNotification,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { ToolControls } from "../../groups/ToolControls/ToolControls";
import { ToolDetailPanel } from "../../groups/ToolDetailPanel/ToolDetailPanel";
import { ToolResultPanel } from "../../groups/ToolResultPanel/ToolResultPanel";
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
import type { ToolDetailPanelProps } from "../../groups/ToolDetailPanel/ToolDetailPanel";
import type { ToolResultPanelProps } from "../../groups/ToolResultPanel/ToolResultPanel";
export interface ToolCallState {
status: "idle" | "pending" | "ok" | "error";
result?: CallToolResult;
error?: string;
progress?: Pick<
ProgressNotification["params"],
"progress" | "total" | "message"
>;
}
export interface ToolsScreenProps {
tools: ToolListItemProps[];
selectedTool?: ToolDetailPanelProps;
result?: ToolResultPanelProps;
tools: Tool[];
selectedToolName?: string;
callState?: ToolCallState;
listChanged: boolean;
onRefreshList: () => void;
onSelectTool: (name: string) => void;
onCallTool: (name: string, args: Record<string, unknown>) => void;
onCancelCall?: () => void;
onClearResult?: () => void;
}
const ScreenLayout = Flex.withProps({
@ -46,18 +62,28 @@ const EmptyState = Text.withProps({
export function ToolsScreen({
tools,
selectedTool,
result,
selectedToolName,
callState,
listChanged,
onRefreshList,
onSelectTool,
onCallTool,
onCancelCall,
onClearResult,
}: ToolsScreenProps) {
const [formValues, setFormValues] = useState<Record<string, unknown>>({});
const selectedTool = selectedToolName
? tools.find((t) => t.name === selectedToolName)
: undefined;
const isExecuting = callState?.status === "pending";
return (
<ScreenLayout>
<Sidebar>
<SidebarCard>
<ToolControls
tools={tools}
selectedName={selectedToolName}
listChanged={listChanged}
onRefreshList={onRefreshList}
onSelectTool={onSelectTool}
@ -67,15 +93,26 @@ export function ToolsScreen({
<ContentCard flex={1}>
{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>
)}
</ContentCard>
<ContentCard flex={1}>
{result ? (
<ToolResultPanel {...result} />
{callState?.result ? (
<ToolResultPanel
result={callState.result}
onClear={() => onClearResult?.()}
/>
) : (
<EmptyState>Results will appear here</EmptyState>
)}

View file

@ -173,228 +173,194 @@ export const ToolsActive: Story = {
{
name: "send_message",
title: "Send Message",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "create_record",
title: "Create Record",
selected: true,
onClick: fn(),
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",
title: "Delete Records",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "list_users",
title: "List Users",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "batch_process",
title: "Batch Process",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "search_files",
title: "Search Files",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "read_file",
title: "Read File",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "write_file",
title: "Write File",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "get_schema",
title: "Get Schema",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "run_query",
title: "Run Query",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "create_migration",
title: "Create Migration",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "validate_config",
title: "Validate Config",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "deploy_service",
title: "Deploy Service",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "rollback_deploy",
title: "Rollback Deploy",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "check_health",
title: "Check Health",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "generate_report",
title: "Generate Report",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "export_csv",
title: "Export CSV",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "import_data",
title: "Import Data",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "send_notification",
title: "Send Notification",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "schedule_task",
title: "Schedule Task",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "cancel_task",
title: "Cancel Task",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "list_connections",
title: "List Connections",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "refresh_cache",
title: "Refresh Cache",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "purge_logs",
title: "Purge Logs",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "encrypt_secret",
title: "Encrypt Secret",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "rotate_keys",
title: "Rotate Keys",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "sync_resources",
title: "Sync Resources",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "compile_template",
title: "Compile Template",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "analyze_performance",
title: "Analyze Performance",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
{
name: "resize_image",
title: "Resize Image",
selected: false,
onClick: fn(),
inputSchema: { type: "object" },
},
]}
selectedTool={{
name: "create_record",
title: "Create Record",
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",
selectedToolName="create_record"
callState={{
status: "ok",
result: {
content: [
{
type: "text",
text: JSON.stringify(
{
id: 42,
title: "New Record",
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}
onRefreshList={fn()}
onSelectTool={fn()}
onCallTool={fn()}
onCancelCall={fn()}
onClearResult={fn()}
/>
),
},