diff --git a/clients/web/src/components/groups/ToolControls/ToolControls.stories.tsx b/clients/web/src/components/groups/ToolControls/ToolControls.stories.tsx index 9a5cc96a..87dd8719 100644 --- a/clients/web/src/components/groups/ToolControls/ToolControls.stories.tsx +++ b/clients/web/src/components/groups/ToolControls/ToolControls.stories.tsx @@ -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 = { export default meta; type Story = StoryObj; -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", }, }; diff --git a/clients/web/src/components/groups/ToolControls/ToolControls.tsx b/clients/web/src/components/groups/ToolControls/ToolControls.tsx index 33db7738..8779e617 100644 --- a/clients/web/src/components/groups/ToolControls/ToolControls.tsx +++ b/clients/web/src/components/groups/ToolControls/ToolControls.tsx @@ -1,22 +1,23 @@ 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; } -function listMaxHeight(): string { - return "calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)"; -} +const LIST_MAX_HEIGHT = + "calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)"; export function ToolControls({ tools, + selectedName, listChanged, onRefreshList, onSelectTool, @@ -42,13 +43,16 @@ export function ToolControls({ value={searchText} onChange={(e) => setSearchText(e.currentTarget.value)} /> - + {filteredTools.map((tool) => ( onSelectTool(tool.name)} + tool={tool} + selected={tool.name === selectedName} + onClick={() => { + if (tool.name !== selectedName) onSelectTool(tool.name); + }} /> ))} diff --git a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.stories.tsx b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.stories.tsx index 559a0a22..9dbee5da 100644 --- a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.stories.tsx +++ b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.stories.tsx @@ -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,101 @@ const meta: Meta = { export default meta; type Story = StoryObj; +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 = { 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 }, }, }; diff --git a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx index 2673d960..3cd88f7c 100644 --- a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx +++ b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx @@ -1,26 +1,23 @@ 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 type ToolProgress = Pick< + ProgressNotification["params"], + "progress" | "total" | "message" +>; export interface ToolDetailPanelProps { - name: string; - title?: string; - description?: string; - annotations?: ToolAnnotations; - schema: JsonSchema; + tool: Tool; formValues: Record; isExecuting: boolean; - progress?: { percent: number; description?: string }; + progress?: ToolProgress; onFormChange: (values: Record) => void; onExecute: () => void; onCancel: () => void; @@ -32,19 +29,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 +46,15 @@ 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.readOnlyHint || + annotations.destructiveHint || + annotations.idempotentHint || + annotations.openWorldHint) ); } export function ToolDetailPanel({ - name, - title, - description, - annotations, - schema, + tool, formValues, isExecuting, progress, @@ -76,29 +62,25 @@ export function ToolDetailPanel({ onExecute, onCancel, }: ToolDetailPanelProps) { + const { name, title, description, annotations, inputSchema } = tool; + return ( {resolveTitle(name, title)} {hasAnyAnnotation(annotations) && annotations && ( - {annotations.audience && ( - - )} - {annotations.readOnly && ( + {annotations.readOnlyHint && ( )} - {annotations.destructive && ( + {annotations.destructiveHint && ( )} - {annotations.longRunning && ( - + {annotations.idempotentHint && ( + + )} + {annotations.openWorldHint && ( + )} - {annotations.hints && {annotations.hints}} )} @@ -107,20 +89,13 @@ export function ToolDetailPanel({ - {progress && ( - - )} + {progress && } {isExecuting && Cancel} diff --git a/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx b/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx index cc3c03b5..86936a46 100644 --- a/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx +++ b/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx @@ -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 = { export default meta; type Story = StoryObj; +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, }, }; diff --git a/clients/web/src/components/groups/ToolListItem/ToolListItem.tsx b/clients/web/src/components/groups/ToolListItem/ToolListItem.tsx index bbe8c427..fa4c86f9 100644 --- a/clients/web/src/components/groups/ToolListItem/ToolListItem.tsx +++ b/clients/web/src/components/groups/ToolListItem/ToolListItem.tsx @@ -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 ( = { export default meta; type Story = StoryObj; +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, + }, +}; + +export const ErrorResult: Story = { + args: { + result: { + isError: true, + content: [ + { + type: "text", + text: "ENOENT: no such file or directory, open '/data/missing.txt'", + }, + ], + }, }, }; diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx index 86956c37..f7a0f520 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx @@ -1,16 +1,9 @@ -import { Button, Group, Stack, Text, Title } from "@mantine/core"; -import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js"; +import { Alert, Button, Group, Stack, Text, Title } from "@mantine/core"; +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,31 @@ 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 ( Results Clear - {content.length === 0 ? ( + {result.isError ? ( + + {result.content + .filter((b) => b.type === "text") + .map((b) => b.text) + .join("\n")} + + ) : result.content.length === 0 ? ( No results yet ) : ( <> - {content.map((item, index) => { - const block = toContentBlock(item); - if (!block) return null; - return ( - - ); - })} + {result.content.map((block, index) => ( + + ))} )} diff --git a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.stories.tsx b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.stories.tsx index a1725a02..768961b0 100644 --- a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.stories.tsx +++ b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.stories.tsx @@ -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 = { title: "Screens/ToolsScreen", @@ -13,85 +12,65 @@ const meta: Meta = { listChanged: false, onRefreshList: fn(), onSelectTool: fn(), + onCallTool: fn(), + onCancelCall: fn(), + onClearResult: fn(), }, }; export default meta; type Story = StoryObj; -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,42 +98,52 @@ 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, + selectedToolName: + "organization_internal_database_complex_multi_table_join_query_with_aggregation_and_filtering", + }, +}; + +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(), }, }, }; diff --git a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx index e6636cfe..e4458eee 100644 --- a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx +++ b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx @@ -1,18 +1,30 @@ +import { useState } from "react"; import { Card, Flex, Stack, Text } from "@mantine/core"; +import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js"; 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 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?: ToolProgress; +} 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) => void; + onCancelCall?: () => void; + onClearResult?: () => void; } const ScreenLayout = Flex.withProps({ @@ -46,36 +58,60 @@ const EmptyState = Text.withProps({ export function ToolsScreen({ tools, - selectedTool, - result, + selectedToolName, + callState, listChanged, onRefreshList, onSelectTool, + onCallTool, + onCancelCall, + onClearResult, }: ToolsScreenProps) { + const [formValues, setFormValues] = useState>({}); + const selectedTool = selectedToolName + ? tools.find((t) => t.name === selectedToolName) + : undefined; + const isExecuting = callState?.status === "pending"; + return ( { + setFormValues({}); + onSelectTool(name); + }} /> {selectedTool ? ( - + onCallTool(selectedTool.name, formValues)} + onCancel={() => onCancelCall?.()} + /> ) : ( Select a tool to view details )} - {result ? ( - + {callState?.result ? ( + onClearResult?.()} + /> ) : ( Results will appear here )} diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx index d5f6dcde..fd8baa01 100644 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx @@ -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()} /> ), },