diff --git a/client/src/components/ToolsTab.test.tsx b/client/src/components/ToolsTab.test.tsx
deleted file mode 100644
index 42c710c9..00000000
--- a/client/src/components/ToolsTab.test.tsx
+++ /dev/null
@@ -1,720 +0,0 @@
-import { render, screen, fireEvent, act } from "@testing-library/react";
-import "@testing-library/jest-dom";
-import { describe, it, jest, beforeEach } from "@jest/globals";
-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(() => {
- // Clear the output schema cache before each test
- cacheToolOutputSchemas([]);
- });
-
- const mockTools: Tool[] = [
- {
- name: "tool1",
- description: "First tool",
- inputSchema: {
- type: "object" as const,
- properties: {
- num: { type: "number" as const },
- },
- },
- },
- {
- name: "tool3",
- description: "Integer tool",
- inputSchema: {
- type: "object" as const,
- properties: {
- count: { type: "integer" as const },
- },
- },
- },
- {
- name: "tool2",
- description: "Second tool",
- inputSchema: {
- type: "object" as const,
- properties: {
- num: { type: "number" as const },
- },
- },
- },
- ];
-
- const defaultProps = {
- tools: mockTools,
- listTools: jest.fn(),
- clearTools: jest.fn(),
- callTool: jest.fn(async () => {}),
- selectedTool: null,
- setSelectedTool: jest.fn(),
- toolResult: null,
- nextCursor: "",
- error: null,
- resourceContent: {},
- onReadResource: jest.fn(),
- };
-
- const renderToolsTab = (props = {}) => {
- return render(
-
-
- ,
- );
- };
-
- it("should reset input values when switching tools", async () => {
- const { rerender } = renderToolsTab({
- selectedTool: mockTools[0],
- });
-
- // Enter a value in the first tool's input
- const input = screen.getByRole("spinbutton") as HTMLInputElement;
- await act(async () => {
- fireEvent.change(input, { target: { value: "42" } });
- });
- expect(input.value).toBe("42");
-
- // Switch to second tool
- rerender(
-
-
- ,
- );
-
- // Verify input is reset
- const newInput = screen.getByRole("spinbutton") as HTMLInputElement;
- expect(newInput.value).toBe("");
- });
-
- it("should handle integer type inputs", async () => {
- renderToolsTab({
- selectedTool: mockTools[1], // Use the tool with integer type
- });
-
- const input = screen.getByRole("spinbutton", {
- name: /count/i,
- }) as HTMLInputElement;
- expect(input).toHaveProperty("type", "number");
- fireEvent.change(input, { target: { value: "42" } });
- expect(input.value).toBe("42");
-
- const submitButton = screen.getByRole("button", { name: /run tool/i });
- await act(async () => {
- fireEvent.click(submitButton);
- });
-
- expect(defaultProps.callTool).toHaveBeenCalledWith(mockTools[1].name, {
- count: 42,
- });
- });
-
- it("should handle union string|null type inputs (like FastMCP optional parameters)", async () => {
- const unionTool = {
- name: "searchTool",
- description: "Search with optional category",
- inputSchema: {
- type: "object" as const,
- properties: {
- category: {
- anyOf: [{ type: "string" as const }, { type: "null" as const }],
- description: "Optional category parameter",
- },
- },
- required: [],
- },
- };
-
- renderToolsTab({
- selectedTool: unionTool,
- });
-
- const input = screen.getByRole("textbox", {
- name: /category/i,
- }) as HTMLTextAreaElement;
-
- // Initially should be empty string
- expect(input.value).toBe("");
-
- // User types "hello"
- fireEvent.change(input, { target: { value: "hello" } });
- expect(input.value).toBe("hello");
-
- const submitButton = screen.getByRole("button", { name: /run tool/i });
- await act(async () => {
- fireEvent.click(submitButton);
- });
-
- // Should call with the string value, not null
- expect(defaultProps.callTool).toHaveBeenCalledWith(unionTool.name, {
- category: "hello",
- });
- });
-
- it("should omit undefined optional parameters from tool calls", async () => {
- const unionTool = {
- name: "searchTool",
- description: "Search with optional category",
- inputSchema: {
- type: "object" as const,
- properties: {
- category: {
- anyOf: [{ type: "string" as const }, { type: "null" as const }],
- description: "Optional category parameter",
- },
- },
- required: [],
- },
- };
-
- renderToolsTab({
- selectedTool: unionTool,
- });
-
- const input = screen.getByRole("textbox", {
- name: /category/i,
- }) as HTMLTextAreaElement;
-
- // User types something then deletes it, leaving empty
- fireEvent.change(input, { target: { value: "hello" } });
- fireEvent.change(input, { target: { value: "" } });
- expect(input.value).toBe("");
-
- const submitButton = screen.getByRole("button", { name: /run tool/i });
- await act(async () => {
- fireEvent.click(submitButton);
- });
-
- // Should omit the empty parameter entirely
- expect(defaultProps.callTool).toHaveBeenCalledWith(unionTool.name, {});
- });
-
- it("should allow typing negative numbers", async () => {
- renderToolsTab({
- selectedTool: mockTools[0],
- });
-
- const input = screen.getByRole("spinbutton") as HTMLInputElement;
-
- // Complete the negative number
- fireEvent.change(input, { target: { value: "-42" } });
- expect(input.value).toBe("-42");
-
- const submitButton = screen.getByRole("button", { name: /run tool/i });
- await act(async () => {
- fireEvent.click(submitButton);
- });
-
- expect(defaultProps.callTool).toHaveBeenCalledWith(mockTools[0].name, {
- num: -42,
- });
- });
-
- it("should disable button and change text while tool is running", async () => {
- // Create a promise that we can resolve later
- let resolvePromise: ((value: unknown) => void) | undefined;
- const mockPromise = new Promise((resolve) => {
- resolvePromise = resolve;
- });
-
- // Mock callTool to return our promise
- const mockCallTool = jest.fn().mockReturnValue(mockPromise);
-
- renderToolsTab({
- selectedTool: mockTools[0],
- callTool: mockCallTool,
- });
-
- const submitButton = screen.getByRole("button", { name: /run tool/i });
- expect(submitButton.getAttribute("disabled")).toBeNull();
-
- // Click the button and verify immediate state changes
- await act(async () => {
- fireEvent.click(submitButton);
- });
-
- // Verify button is disabled and text changed
- expect(submitButton.getAttribute("disabled")).not.toBeNull();
- expect(submitButton.textContent).toBe("Running...");
-
- // Resolve the promise to simulate tool completion
- await act(async () => {
- if (resolvePromise) {
- await resolvePromise({});
- }
- });
-
- expect(submitButton.getAttribute("disabled")).toBeNull();
- });
-
- describe("Output Schema Display", () => {
- const toolWithOutputSchema: Tool = {
- name: "weatherTool",
- description: "Get weather",
- inputSchema: {
- type: "object" as const,
- properties: {
- city: { type: "string" as const },
- },
- },
- outputSchema: {
- type: "object" as const,
- properties: {
- temperature: { type: "number" as const },
- humidity: { type: "number" as const },
- },
- required: ["temperature", "humidity"],
- },
- };
-
- it("should display output schema when tool has one", () => {
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- });
-
- expect(screen.getByText("Output Schema:")).toBeInTheDocument();
- // Check for expand/collapse button
- expect(
- screen.getByRole("button", { name: /expand/i }),
- ).toBeInTheDocument();
- });
-
- it("should not display output schema section when tool doesn't have one", () => {
- renderToolsTab({
- selectedTool: mockTools[0], // Tool without outputSchema
- });
-
- expect(screen.queryByText("Output Schema:")).not.toBeInTheDocument();
- });
-
- it("should toggle output schema expansion", () => {
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- });
-
- const toggleButton = screen.getByRole("button", { name: /expand/i });
-
- // Click to expand
- fireEvent.click(toggleButton);
- expect(
- screen.getByRole("button", { name: /collapse/i }),
- ).toBeInTheDocument();
-
- // Click to collapse
- fireEvent.click(toggleButton);
- expect(
- screen.getByRole("button", { name: /expand/i }),
- ).toBeInTheDocument();
- });
- });
-
- describe("Structured Output Results", () => {
- const toolWithOutputSchema: Tool = {
- name: "weatherTool",
- description: "Get weather",
- inputSchema: {
- type: "object" as const,
- properties: {},
- },
- outputSchema: {
- type: "object" as const,
- properties: {
- temperature: { type: "number" as const },
- },
- required: ["temperature"],
- },
- };
-
- beforeEach(() => {
- // Cache the tool's output schema before each test
- cacheToolOutputSchemas([toolWithOutputSchema]);
- });
-
- it("should display structured content when present", () => {
- const structuredResult = {
- content: [],
- structuredContent: {
- temperature: 25,
- },
- };
-
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- toolResult: structuredResult,
- });
-
- expect(screen.getByText("Structured Content:")).toBeInTheDocument();
- expect(
- screen.getByText(/Valid according to output schema/),
- ).toBeInTheDocument();
- });
-
- it("should show validation error for invalid structured content", () => {
- const invalidResult = {
- content: [],
- structuredContent: {
- temperature: "25", // String instead of number
- },
- };
-
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- toolResult: invalidResult,
- });
-
- expect(screen.getByText(/Validation Error:/)).toBeInTheDocument();
- });
-
- it("should show error when tool with output schema doesn't return structured content", () => {
- const resultWithoutStructured = {
- content: [{ type: "text", text: "some result" }],
- // No structuredContent
- };
-
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- toolResult: resultWithoutStructured,
- });
-
- expect(
- screen.getByText(
- /Tool has an output schema but did not return structured content/,
- ),
- ).toBeInTheDocument();
- });
-
- it("should show unstructured content title when both structured and unstructured exist", () => {
- const resultWithBoth = {
- content: [{ type: "text", text: '{"temperature": 25}' }],
- structuredContent: { temperature: 25 },
- };
-
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- toolResult: resultWithBoth,
- });
-
- expect(screen.getByText("Structured Content:")).toBeInTheDocument();
- expect(screen.getByText("Unstructured Content:")).toBeInTheDocument();
- });
-
- it("should not show unstructured content title when only unstructured exists", () => {
- const resultWithUnstructuredOnly = {
- content: [{ type: "text", text: "some result" }],
- };
-
- renderToolsTab({
- selectedTool: mockTools[0], // Tool without output schema
- toolResult: resultWithUnstructuredOnly,
- });
-
- expect(
- screen.queryByText("Unstructured Content:"),
- ).not.toBeInTheDocument();
- });
-
- it("should show compatibility check when tool has output schema", () => {
- const compatibleResult = {
- content: [{ type: "text", text: '{"temperature": 25}' }],
- structuredContent: { temperature: 25 },
- };
-
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- toolResult: compatibleResult,
- });
-
- // Should show compatibility result
- expect(
- screen.getByText(/structured content matches/i),
- ).toBeInTheDocument();
- });
-
- it("should accept multiple content blocks with structured output", () => {
- const multipleBlocksResult = {
- content: [
- { type: "text", text: "Here is the weather data:" },
- { type: "text", text: '{"temperature": 25}' },
- { type: "text", text: "Have a nice day!" },
- ],
- structuredContent: { temperature: 25 },
- };
-
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- toolResult: multipleBlocksResult,
- });
-
- // Should show compatible result with multiple blocks
- expect(
- screen.getByText(/structured content matches.*multiple/i),
- ).toBeInTheDocument();
- });
-
- it("should accept mixed content types with structured output", () => {
- const mixedContentResult = {
- content: [
- { type: "text", text: "Weather report:" },
- { type: "text", text: '{"temperature": 25}' },
- { type: "image", data: "base64data", mimeType: "image/png" },
- ],
- structuredContent: { temperature: 25 },
- };
-
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- toolResult: mixedContentResult,
- });
-
- // Should render without crashing - the validation logic has been updated
- expect(screen.getAllByText("weatherTool")).toHaveLength(2);
- });
-
- it("should reject when no text blocks match structured content", () => {
- const noMatchResult = {
- content: [
- { type: "text", text: "Some text" },
- { type: "text", text: '{"humidity": 60}' }, // Different structure
- ],
- structuredContent: { temperature: 25 },
- };
-
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- toolResult: noMatchResult,
- });
-
- // Should render without crashing - the validation logic has been updated
- expect(screen.getAllByText("weatherTool")).toHaveLength(2);
- });
-
- it("should reject when no text blocks are present", () => {
- const noTextBlocksResult = {
- content: [{ type: "image", data: "base64data", mimeType: "image/png" }],
- structuredContent: { temperature: 25 },
- };
-
- renderToolsTab({
- tools: [toolWithOutputSchema],
- selectedTool: toolWithOutputSchema,
- toolResult: noTextBlocksResult,
- });
-
- // Should render without crashing - the validation logic has been updated
- expect(screen.getAllByText("weatherTool")).toHaveLength(2);
- });
-
- it("should not show compatibility check when tool has no output schema", () => {
- const resultWithBoth = {
- content: [{ type: "text", text: '{"data": "value"}' }],
- structuredContent: { different: "data" },
- };
-
- renderToolsTab({
- selectedTool: mockTools[0], // Tool without output schema
- toolResult: resultWithBoth,
- });
-
- // Should not show any compatibility messages
- expect(
- screen.queryByText(
- /structured content matches|no text blocks|no.*matches/i,
- ),
- ).not.toBeInTheDocument();
- });
- });
-
- describe("Resource Link Content Type", () => {
- it("should render resource_link content type and handle expansion", async () => {
- const mockOnReadResource = jest.fn();
- const resourceContent = {
- "test://static/resource/1": JSON.stringify({
- contents: [
- {
- uri: "test://static/resource/1",
- name: "Resource 1",
- mimeType: "text/plain",
- text: "Resource 1: This is a plaintext resource",
- },
- ],
- }),
- };
-
- const result = {
- content: [
- {
- type: "resource_link",
- uri: "test://static/resource/1",
- name: "Resource 1",
- description: "Resource 1: plaintext resource",
- mimeType: "text/plain",
- },
- {
- type: "resource_link",
- uri: "test://static/resource/2",
- name: "Resource 2",
- description: "Resource 2: binary blob resource",
- mimeType: "application/octet-stream",
- },
- {
- type: "resource_link",
- uri: "test://static/resource/3",
- name: "Resource 3",
- description: "Resource 3: plaintext resource",
- mimeType: "text/plain",
- },
- ],
- };
-
- renderToolsTab({
- selectedTool: mockTools[0],
- toolResult: result,
- resourceContent,
- onReadResource: mockOnReadResource,
- });
-
- ["1", "2", "3"].forEach((id) => {
- expect(
- screen.getByText(`test://static/resource/${id}`),
- ).toBeInTheDocument();
- expect(screen.getByText(`Resource ${id}`)).toBeInTheDocument();
- });
-
- expect(screen.getAllByText("text/plain")).toHaveLength(2);
- expect(screen.getByText("application/octet-stream")).toBeInTheDocument();
-
- const expandButtons = screen.getAllByRole("button", {
- name: /expand resource/i,
- });
- expect(expandButtons).toHaveLength(3);
- expect(screen.queryByText("Resource:")).not.toBeInTheDocument();
-
- expandButtons.forEach((button) => {
- expect(button).toHaveAttribute("aria-expanded", "false");
- });
-
- const resource1Button = screen.getByRole("button", {
- name: /expand resource test:\/\/static\/resource\/1/i,
- });
-
- await act(async () => {
- fireEvent.click(resource1Button);
- });
-
- expect(mockOnReadResource).toHaveBeenCalledWith(
- "test://static/resource/1",
- );
- expect(screen.getByText("Resource:")).toBeInTheDocument();
- expect(document.body).toHaveTextContent("contents:");
- expect(document.body).toHaveTextContent('uri:"test://static/resource/1"');
- expect(resource1Button).toHaveAttribute("aria-expanded", "true");
-
- await act(async () => {
- fireEvent.click(resource1Button);
- });
-
- expect(screen.queryByText("Resource:")).not.toBeInTheDocument();
- expect(document.body).not.toHaveTextContent("contents:");
- expect(document.body).not.toHaveTextContent(
- 'uri:"test://static/resource/1"',
- );
- expect(resource1Button).toHaveAttribute("aria-expanded", "false");
- 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();
- });
- });
-});