Skip to content

feat: Add _meta field support to tool UI #616

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions client/src/components/ToolResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ const ToolResults = ({
</div>
</div>
)}
{structuredResult._meta && (
<div className="mb-4">
<h5 className="font-semibold mb-2 text-sm">Meta:</h5>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<JsonView data={structuredResult._meta} />
</div>
</div>
)}
{!structuredResult.structuredContent &&
validationResult &&
!validationResult.isValid && (
Expand Down
39 changes: 39 additions & 0 deletions client/src/components/ToolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import ListPane from "./ListPane";
import JsonView from "./JsonView";
import ToolResults from "./ToolResults";

// Type guard to safely detect the optional _meta field without using `any`
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
typeof (tool as { _meta?: unknown })._meta !== "undefined";

const ToolsTab = ({
tools,
listTools,
Expand Down Expand Up @@ -46,6 +50,7 @@ const ToolsTab = ({
const [params, setParams] = useState<Record<string, unknown>>({});
const [isToolRunning, setIsToolRunning] = useState(false);
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
const [isMetaExpanded, setIsMetaExpanded] = useState(false);

useEffect(() => {
const params = Object.entries(
Expand Down Expand Up @@ -245,6 +250,40 @@ const ToolsTab = ({
</div>
</div>
)}
{selectedTool &&
hasMeta(selectedTool) &&
selectedTool._meta && (
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-semibold">Meta:</h4>
<Button
size="sm"
variant="ghost"
onClick={() => setIsMetaExpanded(!isMetaExpanded)}
className="h-6 px-2"
>
{isMetaExpanded ? (
<>
<ChevronUp className="h-3 w-3 mr-1" />
Collapse
</>
) : (
<>
<ChevronDown className="h-3 w-3 mr-1" />
Expand
</>
)}
</Button>
</div>
<div
className={`transition-all ${
isMetaExpanded ? "" : "max-h-[8rem] overflow-y-auto"
}`}
>
<JsonView data={selectedTool._meta} />
</div>
</div>
)}
<Button
onClick={async () => {
try {
Expand Down
81 changes: 81 additions & 0 deletions client/src/components/__tests__/ToolsTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ToolsTab from "../ToolsTab";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { Tabs } from "@/components/ui/tabs";
import { cacheToolOutputSchemas } from "@/utils/schemaUtils";
import { within } from "@testing-library/react";

describe("ToolsTab", () => {
beforeEach(() => {
Expand Down Expand Up @@ -556,4 +557,84 @@ describe("ToolsTab", () => {
expect(mockOnReadResource).toHaveBeenCalledTimes(1);
});
});

describe("Meta Display", () => {
const toolWithMeta = {
name: "metaTool",
description: "Tool with meta",
inputSchema: {
type: "object" as const,
properties: {
foo: { type: "string" as const },
},
},
_meta: {
author: "tester",
version: 1,
},
} as unknown as Tool;

it("should display meta section when tool has _meta", () => {
renderToolsTab({
tools: [toolWithMeta],
selectedTool: toolWithMeta,
});

expect(screen.getByText("Meta:")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /expand/i }),
).toBeInTheDocument();
});

it("should toggle meta expansion", () => {
renderToolsTab({
tools: [toolWithMeta],
selectedTool: toolWithMeta,
});

// There might be multiple Expand buttons (Output Schema, Meta). We need the one within Meta section
const metaHeading = screen.getByText("Meta:");
const metaContainer = metaHeading.closest("div");
expect(metaContainer).toBeTruthy();
const toggleButton = within(metaContainer as HTMLElement).getByRole(
"button",
{ name: /expand/i },
);

// Expand Meta
fireEvent.click(toggleButton);
expect(
within(metaContainer as HTMLElement).getByRole("button", {
name: /collapse/i,
}),
).toBeInTheDocument();

// Collapse Meta
fireEvent.click(toggleButton);
expect(
within(metaContainer as HTMLElement).getByRole("button", {
name: /expand/i,
}),
).toBeInTheDocument();
});
});

describe("ToolResults Meta", () => {
it("should display meta information when present in toolResult", () => {
const resultWithMeta = {
content: [],
_meta: { info: "details", version: 2 },
};

renderToolsTab({
selectedTool: mockTools[0],
toolResult: resultWithMeta,
});

// Only ToolResults meta should be present since selectedTool has no _meta
expect(screen.getAllByText("Meta:")).toHaveLength(1);
expect(screen.getByText(/info/i)).toBeInTheDocument();
expect(screen.getByText(/version/i)).toBeInTheDocument();
});
});
});