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,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 },
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
Loading…
Reference in a new issue