Skip to content

Commit aba186c

Browse files
authored
Merge pull request #616 from erichoracek/dev/eh/tool-meta-support
feat: Add _meta field support to tool UI
2 parents fd0b962 + 1465797 commit aba186c

File tree

3 files changed

+128
-0
lines changed

3 files changed

+128
-0
lines changed

client/src/components/ToolResults.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ const ToolResults = ({
154154
</div>
155155
</div>
156156
)}
157+
{structuredResult._meta && (
158+
<div className="mb-4">
159+
<h5 className="font-semibold mb-2 text-sm">Meta:</h5>
160+
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
161+
<JsonView data={structuredResult._meta} />
162+
</div>
163+
</div>
164+
)}
157165
{!structuredResult.structuredContent &&
158166
validationResult &&
159167
!validationResult.isValid && (

client/src/components/ToolsTab.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import ListPane from "./ListPane";
1919
import JsonView from "./JsonView";
2020
import ToolResults from "./ToolResults";
2121

22+
// Type guard to safely detect the optional _meta field without using `any`
23+
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
24+
typeof (tool as { _meta?: unknown })._meta !== "undefined";
25+
2226
const ToolsTab = ({
2327
tools,
2428
listTools,
@@ -46,6 +50,7 @@ const ToolsTab = ({
4650
const [params, setParams] = useState<Record<string, unknown>>({});
4751
const [isToolRunning, setIsToolRunning] = useState(false);
4852
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
53+
const [isMetaExpanded, setIsMetaExpanded] = useState(false);
4954

5055
useEffect(() => {
5156
const params = Object.entries(
@@ -245,6 +250,40 @@ const ToolsTab = ({
245250
</div>
246251
</div>
247252
)}
253+
{selectedTool &&
254+
hasMeta(selectedTool) &&
255+
selectedTool._meta && (
256+
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
257+
<div className="flex items-center justify-between mb-2">
258+
<h4 className="text-sm font-semibold">Meta:</h4>
259+
<Button
260+
size="sm"
261+
variant="ghost"
262+
onClick={() => setIsMetaExpanded(!isMetaExpanded)}
263+
className="h-6 px-2"
264+
>
265+
{isMetaExpanded ? (
266+
<>
267+
<ChevronUp className="h-3 w-3 mr-1" />
268+
Collapse
269+
</>
270+
) : (
271+
<>
272+
<ChevronDown className="h-3 w-3 mr-1" />
273+
Expand
274+
</>
275+
)}
276+
</Button>
277+
</div>
278+
<div
279+
className={`transition-all ${
280+
isMetaExpanded ? "" : "max-h-[8rem] overflow-y-auto"
281+
}`}
282+
>
283+
<JsonView data={selectedTool._meta} />
284+
</div>
285+
</div>
286+
)}
248287
<Button
249288
onClick={async () => {
250289
try {

client/src/components/__tests__/ToolsTab.test.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ToolsTab from "../ToolsTab";
55
import { Tool } from "@modelcontextprotocol/sdk/types.js";
66
import { Tabs } from "@/components/ui/tabs";
77
import { cacheToolOutputSchemas } from "@/utils/schemaUtils";
8+
import { within } from "@testing-library/react";
89

910
describe("ToolsTab", () => {
1011
beforeEach(() => {
@@ -556,4 +557,84 @@ describe("ToolsTab", () => {
556557
expect(mockOnReadResource).toHaveBeenCalledTimes(1);
557558
});
558559
});
560+
561+
describe("Meta Display", () => {
562+
const toolWithMeta = {
563+
name: "metaTool",
564+
description: "Tool with meta",
565+
inputSchema: {
566+
type: "object" as const,
567+
properties: {
568+
foo: { type: "string" as const },
569+
},
570+
},
571+
_meta: {
572+
author: "tester",
573+
version: 1,
574+
},
575+
} as unknown as Tool;
576+
577+
it("should display meta section when tool has _meta", () => {
578+
renderToolsTab({
579+
tools: [toolWithMeta],
580+
selectedTool: toolWithMeta,
581+
});
582+
583+
expect(screen.getByText("Meta:")).toBeInTheDocument();
584+
expect(
585+
screen.getByRole("button", { name: /expand/i }),
586+
).toBeInTheDocument();
587+
});
588+
589+
it("should toggle meta expansion", () => {
590+
renderToolsTab({
591+
tools: [toolWithMeta],
592+
selectedTool: toolWithMeta,
593+
});
594+
595+
// There might be multiple Expand buttons (Output Schema, Meta). We need the one within Meta section
596+
const metaHeading = screen.getByText("Meta:");
597+
const metaContainer = metaHeading.closest("div");
598+
expect(metaContainer).toBeTruthy();
599+
const toggleButton = within(metaContainer as HTMLElement).getByRole(
600+
"button",
601+
{ name: /expand/i },
602+
);
603+
604+
// Expand Meta
605+
fireEvent.click(toggleButton);
606+
expect(
607+
within(metaContainer as HTMLElement).getByRole("button", {
608+
name: /collapse/i,
609+
}),
610+
).toBeInTheDocument();
611+
612+
// Collapse Meta
613+
fireEvent.click(toggleButton);
614+
expect(
615+
within(metaContainer as HTMLElement).getByRole("button", {
616+
name: /expand/i,
617+
}),
618+
).toBeInTheDocument();
619+
});
620+
});
621+
622+
describe("ToolResults Meta", () => {
623+
it("should display meta information when present in toolResult", () => {
624+
const resultWithMeta = {
625+
content: [],
626+
_meta: { info: "details", version: 2 },
627+
};
628+
629+
renderToolsTab({
630+
selectedTool: mockTools[0],
631+
toolResult: resultWithMeta,
632+
});
633+
634+
// Only ToolResults meta should be present since selectedTool has no _meta
635+
expect(screen.getAllByText("Meta:")).toHaveLength(1);
636+
expect(screen.getByText(/info/i)).toBeInTheDocument();
637+
expect(screen.getByText(/version/i)).toBeInTheDocument();
638+
});
639+
});
559640
});

0 commit comments

Comments
 (0)