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