A tool declaring an `outputSchema` returns its real payload in `structuredContent`; the `content[]` blocks usually only summarize it. v1 rendered that payload as its own inspectable JSON section — v2's ToolResultPanel only ever walked `result.content`, so the payload was dropped with no label, section, or hint that it existed. Adds a `StructuredOutputPanel` group: a collapsible "Structured Output" box (bordered like the existing "Resource Links" box) rendering the payload as pretty-printed, syntax-highlighted, copyable JSON, bounded so a large payload scrolls within itself. ToolResultPanel renders it below the content blocks — and also when `content` is empty (where "No results yet" was simply wrong) and below the alert on an error result, so it is never silently dropped. Adds a `list_items` test-server preset (nested payload, the issue's repro shape) and a `structured-output-http.json` showcase config, documented in the README. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt
102 lines
3.3 KiB
TypeScript
102 lines
3.3 KiB
TypeScript
import { useState } from "react";
|
|
import {
|
|
Collapse,
|
|
Group,
|
|
Paper,
|
|
ScrollArea,
|
|
Stack,
|
|
Title,
|
|
} from "@mantine/core";
|
|
import type { CallToolResult } from "@modelcontextprotocol/client";
|
|
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
|
|
import { ExpandToggle } from "../../elements/ExpandToggle/ExpandToggle";
|
|
|
|
export interface StructuredOutputPanelProps {
|
|
/** The result's `structuredContent` — the tool's schema-validated payload. */
|
|
structuredContent: NonNullable<CallToolResult["structuredContent"]>;
|
|
/** Whether the section starts expanded. Defaults to `true`. */
|
|
defaultExpanded?: boolean;
|
|
}
|
|
|
|
// Bordered box matching the "Resource Links" group in the result panel, so the
|
|
// two supplementary sections of a tool result read as siblings.
|
|
const StructuredBox = Paper.withProps({
|
|
withBorder: true,
|
|
radius: "md",
|
|
p: "md",
|
|
variant: "panel",
|
|
});
|
|
|
|
const StructuredInner = Stack.withProps({
|
|
gap: "sm",
|
|
});
|
|
|
|
const HeaderRow = Group.withProps({
|
|
justify: "space-between",
|
|
wrap: "nowrap",
|
|
});
|
|
|
|
// h4 (size h5) for the same reason as the "Resource Links" heading: the panel's
|
|
// "Results" title is h3, so a sub-box heading is h4 and the heading order never
|
|
// skips a level (axe `heading-order`).
|
|
const StructuredHeader = Title.withProps({
|
|
order: 4,
|
|
size: "h5",
|
|
});
|
|
|
|
// Caps the payload so a large structured result scrolls within the box instead
|
|
// of pushing the content blocks out of view. `Autosize` sizes to the content up
|
|
// to `mah`, so a small object still takes only what it needs.
|
|
const StructuredScroll = ScrollArea.Autosize.withProps({
|
|
mah: 400,
|
|
type: "auto",
|
|
scrollbars: "y",
|
|
offsetScrollbars: true,
|
|
});
|
|
|
|
/**
|
|
* Collapsible "Structured Output" section for a tool result's
|
|
* `structuredContent` (#1908). A tool declaring an `outputSchema` returns its
|
|
* real payload here — the `content[]` blocks usually only summarize it — so v1
|
|
* rendered it as its own inspectable JSON section. Without this, the payload is
|
|
* dropped from the Tools screen entirely, with no hint it was ever returned.
|
|
*
|
|
* The JSON is pretty-printed and syntax-highlighted through {@link ContentViewer}
|
|
* (an `application/json` text block), so it is copyable and scannable field by
|
|
* field.
|
|
*/
|
|
export function StructuredOutputPanel({
|
|
structuredContent,
|
|
defaultExpanded = true,
|
|
}: StructuredOutputPanelProps) {
|
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
|
|
return (
|
|
<StructuredBox>
|
|
<StructuredInner>
|
|
<HeaderRow>
|
|
<StructuredHeader>Structured Output</StructuredHeader>
|
|
<ExpandToggle
|
|
expanded={expanded}
|
|
onToggle={() => setExpanded((value) => !value)}
|
|
ariaLabel={`${expanded ? "Collapse" : "Expand"} structured output`}
|
|
/>
|
|
</HeaderRow>
|
|
{/* Content stays mounted across a collapse (Mantine `Collapse`), so the
|
|
highlighted JSON isn't re-rendered from scratch on every toggle. */}
|
|
<Collapse in={expanded}>
|
|
<StructuredScroll>
|
|
<ContentViewer
|
|
block={{
|
|
type: "text",
|
|
text: JSON.stringify(structuredContent, null, 2),
|
|
}}
|
|
mimeType="application/json"
|
|
copyable
|
|
/>
|
|
</StructuredScroll>
|
|
</Collapse>
|
|
</StructuredInner>
|
|
</StructuredBox>
|
|
);
|
|
}
|