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:
parent
9082e02e65
commit
92ecfcbd6c
2 changed files with 388 additions and 411 deletions
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue