Skip to content

Commit 5dd6615

Browse files
committed
feat: Add _meta field support to tool UI
Shows [_meta](https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta) fields if present when listing and calling tools
1 parent 56ef795 commit 5dd6615

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const ToolsTab = ({
4646
const [params, setParams] = useState<Record<string, unknown>>({});
4747
const [isToolRunning, setIsToolRunning] = useState(false);
4848
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
49+
const [isMetaExpanded, setIsMetaExpanded] = useState(false);
4950

5051
useEffect(() => {
5152
const params = Object.entries(
@@ -245,6 +246,38 @@ const ToolsTab = ({
245246
</div>
246247
</div>
247248
)}
249+
{selectedTool && (selectedTool as any)._meta && (
250+
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
251+
<div className="flex items-center justify-between mb-2">
252+
<h4 className="text-sm font-semibold">Meta:</h4>
253+
<Button
254+
size="sm"
255+
variant="ghost"
256+
onClick={() => setIsMetaExpanded(!isMetaExpanded)}
257+
className="h-6 px-2"
258+
>
259+
{isMetaExpanded ? (
260+
<>
261+
<ChevronUp className="h-3 w-3 mr-1" />
262+
Collapse
263+
</>
264+
) : (
265+
<>
266+
<ChevronDown className="h-3 w-3 mr-1" />
267+
Expand
268+
</>
269+
)}
270+
</Button>
271+
</div>
272+
<div
273+
className={`transition-all ${
274+
isMetaExpanded ? "" : "max-h-[8rem] overflow-y-auto"
275+
}`}
276+
>
277+
<JsonView data={(selectedTool as any)._meta} />
278+
</div>
279+
</div>
280+
)}
248281
<Button
249282
onClick={async () => {
250283
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)