From 1c406eafbd0a377c0094257810829bff1a343429 Mon Sep 17 00:00:00 2001 From: "lorenzo.neumann" <36760115+ln-12@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:52:31 +0200 Subject: [PATCH 01/15] Added UI to provide additional _meta values --- client/src/App.tsx | 15 ++- client/src/components/ToolResults.tsx | 2 +- client/src/components/ToolsTab.tsx | 120 +++++++++++++++++- .../components/__tests__/ToolsTab.test.tsx | 70 ++++++++-- 4 files changed, 188 insertions(+), 19 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index fecd98399..3bf3b8874 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -698,7 +698,11 @@ const App = () => { cacheToolOutputSchemas(response.tools); }; - const callTool = async (name: string, params: Record) => { + const callTool = async ( + name: string, + params: Record, + meta?: Record, + ) => { lastToolCallOriginTabRef.current = currentTabRef.current; try { @@ -710,6 +714,7 @@ const App = () => { arguments: params, _meta: { progressToken: progressTokenRef.current++, + ...(meta ?? {}), }, }, }, @@ -1008,10 +1013,14 @@ const App = () => { setNextToolCursor(undefined); cacheToolOutputSchemas([]); }} - callTool={async (name, params) => { + callTool={async ( + name: string, + params: Record, + meta?: Record, + ) => { clearError("tools"); setToolResult(null); - await callTool(name, params); + await callTool(name, params, meta); }} selectedTool={selectedTool} setSelectedTool={(tool) => { diff --git a/client/src/components/ToolResults.tsx b/client/src/components/ToolResults.tsx index 6479b5fbb..64798cd9d 100644 --- a/client/src/components/ToolResults.tsx +++ b/client/src/components/ToolResults.tsx @@ -156,7 +156,7 @@ const ToolResults = ({ )} {structuredResult._meta && (
-
Meta:
+
Meta Schema:
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 2654feed9..798467c1f 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -42,7 +42,11 @@ const ToolsTab = ({ tools: Tool[]; listTools: () => void; clearTools: () => void; - callTool: (name: string, params: Record) => Promise; + callTool: ( + name: string, + params: Record, + meta?: Record, + ) => Promise; selectedTool: Tool | null; setSelectedTool: (tool: Tool | null) => void; toolResult: CompatibilityCallToolResult | null; @@ -55,6 +59,9 @@ const ToolsTab = ({ const [isToolRunning, setIsToolRunning] = useState(false); const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false); const [isMetaExpanded, setIsMetaExpanded] = useState(false); + const [metaEntries, setMetaEntries] = useState< + { id: string; key: string; value: string }[] + >([]); useEffect(() => { const params = Object.entries( @@ -221,6 +228,102 @@ const ToolsTab = ({ ); }, )} +
+
+

Meta:

+ +
+ {metaEntries.length === 0 ? ( +

+ No meta pairs. +

+ ) : ( +
+ {metaEntries.map((entry, index) => ( +
+ + { + const value = e.target.value; + setMetaEntries((prev) => + prev.map((m, i) => + i === index ? { ...m, key: value } : m, + ), + ); + }} + className="h-8 flex-1" + /> + + { + const value = e.target.value; + setMetaEntries((prev) => + prev.map((m, i) => + i === index ? { ...m, value } : m, + ), + ); + }} + className="h-8 flex-1" + /> + +
+ ))} +
+ )} +
{selectedTool.outputSchema && (
@@ -262,7 +365,7 @@ const ToolsTab = ({ selectedTool._meta && (
-

Meta:

+

Meta Schema:

- +
+ +
+ {entries.map((entry, index) => ( +
+
+ + updateEntry(index, "key", e.target.value)} + /> +
+
+ + updateEntry(index, "value", e.target.value)} + /> +
+ +
+ ))} +
+ + {entries.length === 0 && ( +
+

+ No meta data entries. Click "Add Entry" to add key-value pairs. +

+
+ )} +
+ + ); +}; + +export default MetaDataTab; diff --git a/client/src/components/__tests__/MetaDataTab.test.tsx b/client/src/components/__tests__/MetaDataTab.test.tsx new file mode 100644 index 000000000..e26d0b653 --- /dev/null +++ b/client/src/components/__tests__/MetaDataTab.test.tsx @@ -0,0 +1,558 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import MetaDataTab from "../MetaDataTab"; +import { Tabs } from "@/components/ui/tabs"; + +describe("MetaDataTab", () => { + const defaultProps = { + metaData: {}, + onMetaDataChange: jest.fn(), + }; + + const renderMetaDataTab = (props = {}) => { + return render( + + + , + ); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Initial Rendering", () => { + it("should render the metadata tab with title and description", () => { + renderMetaDataTab(); + + expect(screen.getByText("Meta Data")).toBeInTheDocument(); + expect( + screen.getByText( + "Key-value pairs that will be included in all MCP requests", + ), + ).toBeInTheDocument(); + }); + + it("should render Add Entry button", () => { + renderMetaDataTab(); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + expect(addButton).toBeInTheDocument(); + }); + + it("should show empty state message when no entries exist", () => { + renderMetaDataTab(); + + expect( + screen.getByText( + 'No meta data entries. Click "Add Entry" to add key-value pairs.', + ), + ).toBeInTheDocument(); + }); + + it("should not show empty state message when entries exist", () => { + renderMetaDataTab({ + metaData: { key1: "value1" }, + }); + + expect( + screen.queryByText( + 'No meta data entries. Click "Add Entry" to add key-value pairs.', + ), + ).not.toBeInTheDocument(); + }); + }); + + describe("Initial Data Handling", () => { + it("should initialize with existing metadata", () => { + const initialMetaData = { + API_KEY: "test-key", + VERSION: "1.0.0", + }; + + renderMetaDataTab({ metaData: initialMetaData }); + + expect(screen.getByDisplayValue("API_KEY")).toBeInTheDocument(); + expect(screen.getByDisplayValue("test-key")).toBeInTheDocument(); + expect(screen.getByDisplayValue("VERSION")).toBeInTheDocument(); + expect(screen.getByDisplayValue("1.0.0")).toBeInTheDocument(); + }); + + it("should render multiple entries in correct order", () => { + const initialMetaData = { + FIRST: "first-value", + SECOND: "second-value", + THIRD: "third-value", + }; + + renderMetaDataTab({ metaData: initialMetaData }); + + const keyInputs = screen.getAllByPlaceholderText("Key"); + const valueInputs = screen.getAllByPlaceholderText("Value"); + + expect(keyInputs).toHaveLength(3); + expect(valueInputs).toHaveLength(3); + + // Check that entries are rendered in the order they appear in the object + const entries = Object.entries(initialMetaData); + entries.forEach(([key, value], index) => { + expect(keyInputs[index]).toHaveValue(key); + expect(valueInputs[index]).toHaveValue(value); + }); + }); + }); + + describe("Adding Entries", () => { + it("should add a new empty entry when Add Entry button is clicked", () => { + renderMetaDataTab(); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + + const keyInputs = screen.getAllByPlaceholderText("Key"); + const valueInputs = screen.getAllByPlaceholderText("Value"); + + expect(keyInputs).toHaveLength(1); + expect(valueInputs).toHaveLength(1); + expect(keyInputs[0]).toHaveValue(""); + expect(valueInputs[0]).toHaveValue(""); + }); + + it("should add multiple entries when Add Entry button is clicked multiple times", () => { + renderMetaDataTab(); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + + fireEvent.click(addButton); + fireEvent.click(addButton); + fireEvent.click(addButton); + + const keyInputs = screen.getAllByPlaceholderText("Key"); + const valueInputs = screen.getAllByPlaceholderText("Value"); + + expect(keyInputs).toHaveLength(3); + expect(valueInputs).toHaveLength(3); + }); + + it("should hide empty state message after adding first entry", () => { + renderMetaDataTab(); + + expect( + screen.getByText( + 'No meta data entries. Click "Add Entry" to add key-value pairs.', + ), + ).toBeInTheDocument(); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + + expect( + screen.queryByText( + 'No meta data entries. Click "Add Entry" to add key-value pairs.', + ), + ).not.toBeInTheDocument(); + }); + }); + + describe("Removing Entries", () => { + it("should render remove button for each entry", () => { + renderMetaDataTab({ + metaData: { key1: "value1", key2: "value2" }, + }); + + const removeButtons = screen.getAllByRole("button", { name: "" }); // Trash icon buttons have no text + expect(removeButtons).toHaveLength(2); + }); + + it("should remove entry when remove button is clicked", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ + metaData: { key1: "value1", key2: "value2" }, + onMetaDataChange, + }); + + const removeButtons = screen.getAllByRole("button", { name: "" }); + fireEvent.click(removeButtons[0]); + + expect(onMetaDataChange).toHaveBeenCalledWith({ key2: "value2" }); + }); + + it("should remove correct entry when multiple entries exist", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ + metaData: { + FIRST: "first-value", + SECOND: "second-value", + THIRD: "third-value", + }, + onMetaDataChange, + }); + + const removeButtons = screen.getAllByRole("button", { name: "" }); + fireEvent.click(removeButtons[1]); // Remove second entry + + expect(onMetaDataChange).toHaveBeenCalledWith({ + FIRST: "first-value", + THIRD: "third-value", + }); + }); + + it("should show empty state message after removing all entries", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ + metaData: { key1: "value1" }, + onMetaDataChange, + }); + + const removeButton = screen.getByRole("button", { name: "" }); + fireEvent.click(removeButton); + + expect(onMetaDataChange).toHaveBeenCalledWith({}); + }); + }); + + describe("Editing Entries", () => { + it("should update key when key input is changed", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ + metaData: { oldKey: "value1" }, + onMetaDataChange, + }); + + const keyInput = screen.getByDisplayValue("oldKey"); + fireEvent.change(keyInput, { target: { value: "newKey" } }); + + expect(onMetaDataChange).toHaveBeenCalledWith({ newKey: "value1" }); + }); + + it("should update value when value input is changed", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ + metaData: { key1: "oldValue" }, + onMetaDataChange, + }); + + const valueInput = screen.getByDisplayValue("oldValue"); + fireEvent.change(valueInput, { target: { value: "newValue" } }); + + expect(onMetaDataChange).toHaveBeenCalledWith({ key1: "newValue" }); + }); + + it("should handle editing multiple entries independently", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ + metaData: { + key1: "value1", + key2: "value2", + }, + onMetaDataChange, + }); + + const keyInputs = screen.getAllByPlaceholderText("Key"); + const valueInputs = screen.getAllByPlaceholderText("Value"); + + // Edit first entry key + fireEvent.change(keyInputs[0], { target: { value: "newKey1" } }); + expect(onMetaDataChange).toHaveBeenCalledWith({ + newKey1: "value1", + key2: "value2", + }); + + // Clear mock to test second edit independently + onMetaDataChange.mockClear(); + + // Edit second entry value + fireEvent.change(valueInputs[1], { target: { value: "newValue2" } }); + expect(onMetaDataChange).toHaveBeenCalledWith({ + newKey1: "value1", + key2: "newValue2", + }); + }); + }); + + describe("Data Validation and Trimming", () => { + it("should trim whitespace from keys and values", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ onMetaDataChange }); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + + const keyInput = screen.getByPlaceholderText("Key"); + const valueInput = screen.getByPlaceholderText("Value"); + + fireEvent.change(keyInput, { target: { value: " trimmedKey " } }); + fireEvent.change(valueInput, { target: { value: " trimmedValue " } }); + + expect(onMetaDataChange).toHaveBeenCalledWith({ + trimmedKey: "trimmedValue", + }); + }); + + it("should exclude entries with empty keys or values after trimming", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ onMetaDataChange }); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + fireEvent.click(addButton); + + const keyInputs = screen.getAllByPlaceholderText("Key"); + const valueInputs = screen.getAllByPlaceholderText("Value"); + + // First entry: valid key and value + fireEvent.change(keyInputs[0], { target: { value: "validKey" } }); + fireEvent.change(valueInputs[0], { target: { value: "validValue" } }); + + // Second entry: empty key (should be excluded) + fireEvent.change(keyInputs[1], { target: { value: "" } }); + fireEvent.change(valueInputs[1], { target: { value: "someValue" } }); + + expect(onMetaDataChange).toHaveBeenCalledWith({ + validKey: "validValue", + }); + }); + + it("should exclude entries with whitespace-only keys or values", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ onMetaDataChange }); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + fireEvent.click(addButton); + + const keyInputs = screen.getAllByPlaceholderText("Key"); + const valueInputs = screen.getAllByPlaceholderText("Value"); + + // First entry: valid key and value + fireEvent.change(keyInputs[0], { target: { value: "validKey" } }); + fireEvent.change(valueInputs[0], { target: { value: "validValue" } }); + + // Second entry: whitespace-only key (should be excluded) + fireEvent.change(keyInputs[1], { target: { value: " " } }); + fireEvent.change(valueInputs[1], { target: { value: "someValue" } }); + + expect(onMetaDataChange).toHaveBeenCalledWith({ + validKey: "validValue", + }); + }); + + it("should handle mixed valid and invalid entries", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ onMetaDataChange }); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + fireEvent.click(addButton); + fireEvent.click(addButton); + + const keyInputs = screen.getAllByPlaceholderText("Key"); + const valueInputs = screen.getAllByPlaceholderText("Value"); + + // First entry: valid + fireEvent.change(keyInputs[0], { target: { value: "key1" } }); + fireEvent.change(valueInputs[0], { target: { value: "value1" } }); + + // Second entry: empty key (invalid) + fireEvent.change(keyInputs[1], { target: { value: "" } }); + fireEvent.change(valueInputs[1], { target: { value: "value2" } }); + + // Third entry: valid + fireEvent.change(keyInputs[2], { target: { value: "key3" } }); + fireEvent.change(valueInputs[2], { target: { value: "value3" } }); + + expect(onMetaDataChange).toHaveBeenCalledWith({ + key1: "value1", + key3: "value3", + }); + }); + }); + + describe("Input Accessibility", () => { + it("should have proper labels for screen readers", () => { + renderMetaDataTab({ + metaData: { key1: "value1" }, + }); + + const keyLabel = screen.getByLabelText("Key", { selector: "input" }); + const valueLabel = screen.getByLabelText("Value", { selector: "input" }); + + expect(keyLabel).toBeInTheDocument(); + expect(valueLabel).toBeInTheDocument(); + }); + + it("should have unique IDs for each input pair", () => { + renderMetaDataTab({ + metaData: { + key1: "value1", + key2: "value2", + }, + }); + + const keyInputs = screen.getAllByPlaceholderText("Key"); + const valueInputs = screen.getAllByPlaceholderText("Value"); + + expect(keyInputs[0]).toHaveAttribute("id", "key-0"); + expect(keyInputs[1]).toHaveAttribute("id", "key-1"); + expect(valueInputs[0]).toHaveAttribute("id", "value-0"); + expect(valueInputs[1]).toHaveAttribute("id", "value-1"); + }); + + it("should have proper placeholder text", () => { + renderMetaDataTab(); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + + const keyInput = screen.getByPlaceholderText("Key"); + const valueInput = screen.getByPlaceholderText("Value"); + + expect(keyInput).toHaveAttribute("placeholder", "Key"); + expect(valueInput).toHaveAttribute("placeholder", "Value"); + }); + }); + + describe("Edge Cases", () => { + it("should handle special characters in keys and values", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ onMetaDataChange }); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + + const keyInput = screen.getByPlaceholderText("Key"); + const valueInput = screen.getByPlaceholderText("Value"); + + fireEvent.change(keyInput, { + target: { value: "key-with-special@chars!" }, + }); + fireEvent.change(valueInput, { + target: { value: "value with spaces & symbols $%^" }, + }); + + expect(onMetaDataChange).toHaveBeenCalledWith({ + "key-with-special@chars!": "value with spaces & symbols $%^", + }); + }); + + it("should handle unicode characters", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ onMetaDataChange }); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + + const keyInput = screen.getByPlaceholderText("Key"); + const valueInput = screen.getByPlaceholderText("Value"); + + fireEvent.change(keyInput, { target: { value: "🔑_key" } }); + fireEvent.change(valueInput, { target: { value: "值_value_🎯" } }); + + expect(onMetaDataChange).toHaveBeenCalledWith({ + "🔑_key": "值_value_🎯", + }); + }); + + it("should handle very long keys and values", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ onMetaDataChange }); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + + const keyInput = screen.getByPlaceholderText("Key"); + const valueInput = screen.getByPlaceholderText("Value"); + + const longKey = "A".repeat(100); + const longValue = "B".repeat(500); + + fireEvent.change(keyInput, { target: { value: longKey } }); + fireEvent.change(valueInput, { target: { value: longValue } }); + + expect(onMetaDataChange).toHaveBeenCalledWith({ + [longKey]: longValue, + }); + }); + + it("should handle duplicate keys by keeping the last one", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ onMetaDataChange }); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + fireEvent.click(addButton); + + const keyInputs = screen.getAllByPlaceholderText("Key"); + const valueInputs = screen.getAllByPlaceholderText("Value"); + + // Set same key for both entries + fireEvent.change(keyInputs[0], { target: { value: "duplicateKey" } }); + fireEvent.change(valueInputs[0], { target: { value: "firstValue" } }); + + fireEvent.change(keyInputs[1], { target: { value: "duplicateKey" } }); + fireEvent.change(valueInputs[1], { target: { value: "secondValue" } }); + + // The second value should overwrite the first + expect(onMetaDataChange).toHaveBeenCalledWith({ + duplicateKey: "secondValue", + }); + }); + }); + + describe("Integration with Parent Component", () => { + it("should not call onMetaDataChange when component mounts with existing data", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ + metaData: { key1: "value1" }, + onMetaDataChange, + }); + + expect(onMetaDataChange).not.toHaveBeenCalled(); + }); + + it("should call onMetaDataChange only when user makes changes", () => { + const onMetaDataChange = jest.fn(); + renderMetaDataTab({ + metaData: { key1: "value1" }, + onMetaDataChange, + }); + + // Should not be called on mount + expect(onMetaDataChange).not.toHaveBeenCalled(); + + // Should be called when user changes value + const valueInput = screen.getByDisplayValue("value1"); + fireEvent.change(valueInput, { target: { value: "newValue" } }); + + expect(onMetaDataChange).toHaveBeenCalledTimes(1); + expect(onMetaDataChange).toHaveBeenCalledWith({ key1: "newValue" }); + }); + + it("should maintain internal state when props change (component doesn't sync with prop changes)", () => { + const { rerender } = renderMetaDataTab({ + metaData: { key1: "value1" }, + }); + + expect(screen.getByDisplayValue("key1")).toBeInTheDocument(); + expect(screen.getByDisplayValue("value1")).toBeInTheDocument(); + + // Rerender with different props - component should maintain its internal state + // This is the intended behavior since useState initializer only runs once + rerender( + + + , + ); + + // The component should still show the original values since it maintains internal state + expect(screen.getByDisplayValue("key1")).toBeInTheDocument(); + expect(screen.getByDisplayValue("value1")).toBeInTheDocument(); + // The new prop values should not be displayed + expect(screen.queryByDisplayValue("key2")).not.toBeInTheDocument(); + expect(screen.queryByDisplayValue("value2")).not.toBeInTheDocument(); + }); + }); +}); diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 37073e9b7..7bb3d01c9 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -79,6 +79,7 @@ interface UseConnectionOptions { // eslint-disable-next-line @typescript-eslint/no-explicit-any getRoots?: () => any[]; defaultLoggingLevel?: LoggingLevel; + metaData?: Record; } export function useConnection({ @@ -97,6 +98,7 @@ export function useConnection({ onElicitationRequest, getRoots, defaultLoggingLevel, + metaData = {}, }: UseConnectionOptions) { const [connectionStatus, setConnectionStatus] = useState("disconnected"); @@ -153,6 +155,20 @@ export function useConnection({ try { const abortController = new AbortController(); + // Add metadata to the request if available, but skip for tool calls + // as they handle metadata merging separately + const shouldAddGeneralMeta = + request.method !== "tools/call" && Object.keys(metaData).length > 0; + const requestWithMeta = shouldAddGeneralMeta + ? { + ...request, + params: { + ...request.params, + _meta: metaData, + }, + } + : request; + // prepare MCP Client request options const mcpRequestOptions: RequestOptions = { signal: options?.signal ?? abortController.signal, @@ -181,13 +197,17 @@ export function useConnection({ let response; try { - response = await mcpClient.request(request, schema, mcpRequestOptions); + response = await mcpClient.request( + requestWithMeta, + schema, + mcpRequestOptions, + ); - pushHistory(request, response); + pushHistory(requestWithMeta, response); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - pushHistory(request, { error: errorMessage }); + pushHistory(requestWithMeta, { error: errorMessage }); throw error; } From 3d3925d02ad4513da71ee95f7b4a454e6e01d649 Mon Sep 17 00:00:00 2001 From: "lorenzo.neumann" <36760115+ln-12@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:01:30 +0200 Subject: [PATCH 05/15] Fixed metadata naming --- client/src/App.tsx | 10 +-- client/src/components/MetaDataTab.tsx | 10 +-- .../components/__tests__/MetaDataTab.test.tsx | 80 +++++++++---------- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 7f5314711..0f8997bdc 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -81,7 +81,7 @@ import { CustomHeaders, migrateFromLegacyAuth, } from "./lib/types/customHeaders"; -import MetaDataTab from "./components/MetaDataTab"; +import MetadataTab from "./components/MetadataTab"; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; @@ -197,14 +197,14 @@ const App = () => { const [authState, setAuthState] = useState(EMPTY_DEBUGGER_STATE); - // Meta data state - persisted in localStorage + // Metadata state - persisted in localStorage const [metaData, setMetaData] = useState>(() => { const savedMetaData = localStorage.getItem("lastMetaData"); if (savedMetaData) { try { return JSON.parse(savedMetaData); } catch (error) { - console.warn("Failed to parse saved meta data:", error); + console.warn("Failed to parse saved metadata:", error); } } return {}; @@ -1022,7 +1022,7 @@ const App = () => { - Meta Data + Metadata @@ -1184,7 +1184,7 @@ const App = () => { onRootsChange={handleRootsChange} /> - diff --git a/client/src/components/MetaDataTab.tsx b/client/src/components/MetaDataTab.tsx index c4cc6d214..eefedba49 100644 --- a/client/src/components/MetaDataTab.tsx +++ b/client/src/components/MetaDataTab.tsx @@ -10,12 +10,12 @@ interface MetaDataEntry { value: string; } -interface MetaDataTabProps { +interface MetadataTabProps { metaData: Record; onMetaDataChange: (metaData: Record) => void; } -const MetaDataTab: React.FC = ({ +const MetadataTab: React.FC = ({ metaData, onMetaDataChange, }) => { @@ -59,7 +59,7 @@ const MetaDataTab: React.FC = ({
-

Meta Data

+

Metadata

Key-value pairs that will be included in all MCP requests

@@ -109,7 +109,7 @@ const MetaDataTab: React.FC = ({ {entries.length === 0 && (

- No meta data entries. Click "Add Entry" to add key-value pairs. + No metadata entries. Click "Add Entry" to add key-value pairs.

)} @@ -118,4 +118,4 @@ const MetaDataTab: React.FC = ({ ); }; -export default MetaDataTab; +export default MetadataTab; diff --git a/client/src/components/__tests__/MetaDataTab.test.tsx b/client/src/components/__tests__/MetaDataTab.test.tsx index e26d0b653..339e70158 100644 --- a/client/src/components/__tests__/MetaDataTab.test.tsx +++ b/client/src/components/__tests__/MetaDataTab.test.tsx @@ -1,18 +1,18 @@ import { render, screen, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom"; -import MetaDataTab from "../MetaDataTab"; +import MetadataTab from "../MetadataTab"; import { Tabs } from "@/components/ui/tabs"; -describe("MetaDataTab", () => { +describe("MetadataTab", () => { const defaultProps = { metaData: {}, onMetaDataChange: jest.fn(), }; - const renderMetaDataTab = (props = {}) => { + const renderMetadataTab = (props = {}) => { return render( - + , ); }; @@ -23,9 +23,9 @@ describe("MetaDataTab", () => { describe("Initial Rendering", () => { it("should render the metadata tab with title and description", () => { - renderMetaDataTab(); + renderMetadataTab(); - expect(screen.getByText("Meta Data")).toBeInTheDocument(); + expect(screen.getByText("Metadata")).toBeInTheDocument(); expect( screen.getByText( "Key-value pairs that will be included in all MCP requests", @@ -34,30 +34,30 @@ describe("MetaDataTab", () => { }); it("should render Add Entry button", () => { - renderMetaDataTab(); + renderMetadataTab(); const addButton = screen.getByRole("button", { name: /add entry/i }); expect(addButton).toBeInTheDocument(); }); it("should show empty state message when no entries exist", () => { - renderMetaDataTab(); + renderMetadataTab(); expect( screen.getByText( - 'No meta data entries. Click "Add Entry" to add key-value pairs.', + 'No metadata entries. Click "Add Entry" to add key-value pairs.', ), ).toBeInTheDocument(); }); it("should not show empty state message when entries exist", () => { - renderMetaDataTab({ + renderMetadataTab({ metaData: { key1: "value1" }, }); expect( screen.queryByText( - 'No meta data entries. Click "Add Entry" to add key-value pairs.', + 'No metadata entries. Click "Add Entry" to add key-value pairs.', ), ).not.toBeInTheDocument(); }); @@ -70,7 +70,7 @@ describe("MetaDataTab", () => { VERSION: "1.0.0", }; - renderMetaDataTab({ metaData: initialMetaData }); + renderMetadataTab({ metaData: initialMetaData }); expect(screen.getByDisplayValue("API_KEY")).toBeInTheDocument(); expect(screen.getByDisplayValue("test-key")).toBeInTheDocument(); @@ -85,7 +85,7 @@ describe("MetaDataTab", () => { THIRD: "third-value", }; - renderMetaDataTab({ metaData: initialMetaData }); + renderMetadataTab({ metaData: initialMetaData }); const keyInputs = screen.getAllByPlaceholderText("Key"); const valueInputs = screen.getAllByPlaceholderText("Value"); @@ -104,7 +104,7 @@ describe("MetaDataTab", () => { describe("Adding Entries", () => { it("should add a new empty entry when Add Entry button is clicked", () => { - renderMetaDataTab(); + renderMetadataTab(); const addButton = screen.getByRole("button", { name: /add entry/i }); fireEvent.click(addButton); @@ -119,7 +119,7 @@ describe("MetaDataTab", () => { }); it("should add multiple entries when Add Entry button is clicked multiple times", () => { - renderMetaDataTab(); + renderMetadataTab(); const addButton = screen.getByRole("button", { name: /add entry/i }); @@ -135,11 +135,11 @@ describe("MetaDataTab", () => { }); it("should hide empty state message after adding first entry", () => { - renderMetaDataTab(); + renderMetadataTab(); expect( screen.getByText( - 'No meta data entries. Click "Add Entry" to add key-value pairs.', + 'No metadata entries. Click "Add Entry" to add key-value pairs.', ), ).toBeInTheDocument(); @@ -148,7 +148,7 @@ describe("MetaDataTab", () => { expect( screen.queryByText( - 'No meta data entries. Click "Add Entry" to add key-value pairs.', + 'No metadata entries. Click "Add Entry" to add key-value pairs.', ), ).not.toBeInTheDocument(); }); @@ -156,7 +156,7 @@ describe("MetaDataTab", () => { describe("Removing Entries", () => { it("should render remove button for each entry", () => { - renderMetaDataTab({ + renderMetadataTab({ metaData: { key1: "value1", key2: "value2" }, }); @@ -166,7 +166,7 @@ describe("MetaDataTab", () => { it("should remove entry when remove button is clicked", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ + renderMetadataTab({ metaData: { key1: "value1", key2: "value2" }, onMetaDataChange, }); @@ -179,7 +179,7 @@ describe("MetaDataTab", () => { it("should remove correct entry when multiple entries exist", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ + renderMetadataTab({ metaData: { FIRST: "first-value", SECOND: "second-value", @@ -199,7 +199,7 @@ describe("MetaDataTab", () => { it("should show empty state message after removing all entries", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ + renderMetadataTab({ metaData: { key1: "value1" }, onMetaDataChange, }); @@ -214,7 +214,7 @@ describe("MetaDataTab", () => { describe("Editing Entries", () => { it("should update key when key input is changed", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ + renderMetadataTab({ metaData: { oldKey: "value1" }, onMetaDataChange, }); @@ -227,7 +227,7 @@ describe("MetaDataTab", () => { it("should update value when value input is changed", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ + renderMetadataTab({ metaData: { key1: "oldValue" }, onMetaDataChange, }); @@ -240,7 +240,7 @@ describe("MetaDataTab", () => { it("should handle editing multiple entries independently", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ + renderMetadataTab({ metaData: { key1: "value1", key2: "value2", @@ -273,7 +273,7 @@ describe("MetaDataTab", () => { describe("Data Validation and Trimming", () => { it("should trim whitespace from keys and values", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ onMetaDataChange }); + renderMetadataTab({ onMetaDataChange }); const addButton = screen.getByRole("button", { name: /add entry/i }); fireEvent.click(addButton); @@ -291,7 +291,7 @@ describe("MetaDataTab", () => { it("should exclude entries with empty keys or values after trimming", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ onMetaDataChange }); + renderMetadataTab({ onMetaDataChange }); const addButton = screen.getByRole("button", { name: /add entry/i }); fireEvent.click(addButton); @@ -315,7 +315,7 @@ describe("MetaDataTab", () => { it("should exclude entries with whitespace-only keys or values", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ onMetaDataChange }); + renderMetadataTab({ onMetaDataChange }); const addButton = screen.getByRole("button", { name: /add entry/i }); fireEvent.click(addButton); @@ -339,7 +339,7 @@ describe("MetaDataTab", () => { it("should handle mixed valid and invalid entries", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ onMetaDataChange }); + renderMetadataTab({ onMetaDataChange }); const addButton = screen.getByRole("button", { name: /add entry/i }); fireEvent.click(addButton); @@ -370,7 +370,7 @@ describe("MetaDataTab", () => { describe("Input Accessibility", () => { it("should have proper labels for screen readers", () => { - renderMetaDataTab({ + renderMetadataTab({ metaData: { key1: "value1" }, }); @@ -382,7 +382,7 @@ describe("MetaDataTab", () => { }); it("should have unique IDs for each input pair", () => { - renderMetaDataTab({ + renderMetadataTab({ metaData: { key1: "value1", key2: "value2", @@ -399,7 +399,7 @@ describe("MetaDataTab", () => { }); it("should have proper placeholder text", () => { - renderMetaDataTab(); + renderMetadataTab(); const addButton = screen.getByRole("button", { name: /add entry/i }); fireEvent.click(addButton); @@ -415,7 +415,7 @@ describe("MetaDataTab", () => { describe("Edge Cases", () => { it("should handle special characters in keys and values", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ onMetaDataChange }); + renderMetadataTab({ onMetaDataChange }); const addButton = screen.getByRole("button", { name: /add entry/i }); fireEvent.click(addButton); @@ -437,7 +437,7 @@ describe("MetaDataTab", () => { it("should handle unicode characters", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ onMetaDataChange }); + renderMetadataTab({ onMetaDataChange }); const addButton = screen.getByRole("button", { name: /add entry/i }); fireEvent.click(addButton); @@ -455,7 +455,7 @@ describe("MetaDataTab", () => { it("should handle very long keys and values", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ onMetaDataChange }); + renderMetadataTab({ onMetaDataChange }); const addButton = screen.getByRole("button", { name: /add entry/i }); fireEvent.click(addButton); @@ -476,7 +476,7 @@ describe("MetaDataTab", () => { it("should handle duplicate keys by keeping the last one", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ onMetaDataChange }); + renderMetadataTab({ onMetaDataChange }); const addButton = screen.getByRole("button", { name: /add entry/i }); fireEvent.click(addButton); @@ -502,7 +502,7 @@ describe("MetaDataTab", () => { describe("Integration with Parent Component", () => { it("should not call onMetaDataChange when component mounts with existing data", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ + renderMetadataTab({ metaData: { key1: "value1" }, onMetaDataChange, }); @@ -512,7 +512,7 @@ describe("MetaDataTab", () => { it("should call onMetaDataChange only when user makes changes", () => { const onMetaDataChange = jest.fn(); - renderMetaDataTab({ + renderMetadataTab({ metaData: { key1: "value1" }, onMetaDataChange, }); @@ -529,7 +529,7 @@ describe("MetaDataTab", () => { }); it("should maintain internal state when props change (component doesn't sync with prop changes)", () => { - const { rerender } = renderMetaDataTab({ + const { rerender } = renderMetadataTab({ metaData: { key1: "value1" }, }); @@ -540,7 +540,7 @@ describe("MetaDataTab", () => { // This is the intended behavior since useState initializer only runs once rerender( - From b9963014a0f94cb654268f9c67eef20bdd6d545e Mon Sep 17 00:00:00 2001 From: "lorenzo.neumann" <36760115+ln-12@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:02:41 +0200 Subject: [PATCH 06/15] Fixed naming --- client/src/components/ToolsTab.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 04d298e13..780fa2085 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -306,7 +306,9 @@ const ToolsTab = ({ )}
-

Meta:

+

+ Tool-specific Metadata: +

- {metaEntries.length === 0 ? ( + {metadataEntries.length === 0 ? (

- No meta pairs. + No metadata pairs.

) : (
- {metaEntries.map((entry, index) => ( + {metadataEntries.map((entry, index) => (
{ const value = e.target.value; - setMetaEntries((prev) => + setMetadataEntries((prev) => prev.map((m, i) => i === index ? { ...m, key: value } : m, ), @@ -451,18 +451,18 @@ const ToolsTab = ({ className="h-8 flex-1" /> { const value = e.target.value; - setMetaEntries((prev) => + setMetadataEntries((prev) => prev.map((m, i) => i === index ? { ...m, value } : m, ), @@ -475,7 +475,7 @@ const ToolsTab = ({ variant="ghost" className="h-8 w-8 p-0 ml-auto shrink-0" onClick={() => - setMetaEntries((prev) => + setMetadataEntries((prev) => prev.filter((_, i) => i !== index), ) } @@ -533,10 +533,12 @@ const ToolsTab = ({
@@ -565,17 +569,16 @@ const ToolsTab = ({ try { setIsToolRunning(true); - const meta = metaEntries.reduce>( - (acc, { key, value }) => { - if (key.trim() !== "") acc[key] = value; - return acc; - }, - {}, - ); + const metadata = metadataEntries.reduce< + Record + >((acc, { key, value }) => { + if (key.trim() !== "") acc[key] = value; + return acc; + }, {}); await callTool( selectedTool.name, params, - Object.keys(meta).length ? meta : undefined, + Object.keys(metadata).length ? metadata : undefined, ); } finally { setIsToolRunning(false); diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 0c20e7c81..3e07d017f 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -716,10 +716,10 @@ describe("ToolsTab", () => { }); }); - describe("Meta Display", () => { - const toolWithMeta = { + describe("Metadata Display", () => { + const toolWithMetadata = { name: "metaTool", - description: "Tool with meta", + description: "Tool with metadata", inputSchema: { type: "object" as const, properties: { @@ -732,10 +732,10 @@ describe("ToolsTab", () => { }, } as unknown as Tool; - it("should display meta section when tool has _meta", () => { + it("should display metadata section when tool has _meta", () => { renderToolsTab({ - tools: [toolWithMeta], - selectedTool: toolWithMeta, + tools: [toolWithMetadata], + selectedTool: toolWithMetadata, }); expect(screen.getByText("Meta:")).toBeInTheDocument(); @@ -744,10 +744,10 @@ describe("ToolsTab", () => { ).toBeInTheDocument(); }); - it("should toggle meta schema expansion", () => { + it("should toggle metadata schema expansion", () => { renderToolsTab({ - tools: [toolWithMeta], - selectedTool: toolWithMeta, + tools: [toolWithMetadata], + selectedTool: toolWithMetadata, }); // There might be multiple Expand buttons (Output Schema, Meta). We need the one within Meta section @@ -777,13 +777,13 @@ describe("ToolsTab", () => { }); }); - describe("Meta submission", () => { - it("should send meta values when provided", async () => { + describe("Metadata submission", () => { + it("should send metadata values when provided", async () => { const callToolMock = jest.fn(async () => {}); renderToolsTab({ selectedTool: mockTools[0], callTool: callToolMock }); - // Add a meta key/value pair + // Add a metadata key/value pair const addPairButton = screen.getByRole("button", { name: /add pair/i }); await act(async () => { fireEvent.click(addPairButton); @@ -815,19 +815,19 @@ describe("ToolsTab", () => { }); }); - describe("ToolResults Meta", () => { - it("should display meta information when present in toolResult", () => { - const resultWithMeta = { + describe("ToolResults Metadata", () => { + it("should display metadata information when present in toolResult", () => { + const resultWithMetadata = { content: [], _meta: { info: "details", version: 2 }, }; renderToolsTab({ selectedTool: mockTools[0], - toolResult: resultWithMeta, + toolResult: resultWithMetadata, }); - // Only ToolResults meta should be present since selectedTool has no _meta + // Only ToolResults metadata 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(); diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 4f639c08e..4584ff0a2 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -167,9 +167,9 @@ export function useConnection({ // Add metadata to the request if available, but skip for tool calls // as they handle metadata merging separately - const shouldAddGeneralMeta = + const shouldAddGeneralMetadata = request.method !== "tools/call" && Object.keys(metadata).length > 0; - const requestWithMeta = shouldAddGeneralMeta + const requestWithMetadata = shouldAddGeneralMetadata ? { ...request, params: { @@ -208,16 +208,16 @@ export function useConnection({ let response; try { response = await mcpClient.request( - requestWithMeta, + requestWithMetadata, schema, mcpRequestOptions, ); - pushHistory(requestWithMeta, response); + pushHistory(requestWithMetadata, response); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - pushHistory(requestWithMeta, { error: errorMessage }); + pushHistory(requestWithMetadata, { error: errorMessage }); throw error; } From 5b9e69f36a191f24d3f33387f9cb7f8200752a25 Mon Sep 17 00:00:00 2001 From: "lorenzo.neumann" <36760115+ln-12@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:24:47 +0100 Subject: [PATCH 14/15] Aligned naming --- cli/src/client/tools.ts | 8 ++++---- client/src/App.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/src/client/tools.ts b/cli/src/client/tools.ts index 909f17af3..516814115 100644 --- a/cli/src/client/tools.ts +++ b/cli/src/client/tools.ts @@ -115,9 +115,9 @@ export async function callTool( // Merge general metadata with tool-specific metadata // Tool-specific metadata takes precedence over general metadata - let mergedMeta: Record | undefined; + let mergedMetadata: Record | undefined; if (generalMetadata || toolSpecificMetadata) { - mergedMeta = { + mergedMetadata = { ...(generalMetadata || {}), ...(toolSpecificMetadata || {}), }; @@ -127,8 +127,8 @@ export async function callTool( name: name, arguments: convertedArgs, _meta: - mergedMeta && Object.keys(mergedMeta).length > 0 - ? mergedMeta + mergedMetadata && Object.keys(mergedMetadata).length > 0 + ? mergedMetadata : undefined, }); return response; diff --git a/client/src/App.tsx b/client/src/App.tsx index e17c24ede..d58cd8fe4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -821,7 +821,7 @@ const App = () => { // Merge general metadata with tool-specific metadata // Tool-specific metadata takes precedence over general metadata - const mergedMeta = { + const mergedMetadata = { ...metadata, // General metadata first progressToken: progressTokenRef.current++, ...(metadata ?? {}), // Tool-specific metadata overrides @@ -833,7 +833,7 @@ const App = () => { params: { name, arguments: cleanedParams, - _meta: mergedMeta, + _meta: mergedMetadata, }, }, CompatibilityCallToolResultSchema, From 1c1afc30cc6e61a8a9216c3878c295c27191b975 Mon Sep 17 00:00:00 2001 From: "lorenzo.neumann" <36760115+ln-12@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:54:23 +0100 Subject: [PATCH 15/15] Fix global metadata not being applied --- client/src/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index d58cd8fe4..c53294d52 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -808,7 +808,7 @@ const App = () => { const callTool = async ( name: string, params: Record, - metadata?: Record, + toolMetadata?: Record, ) => { lastToolCallOriginTabRef.current = currentTabRef.current; @@ -822,9 +822,9 @@ const App = () => { // Merge general metadata with tool-specific metadata // Tool-specific metadata takes precedence over general metadata const mergedMetadata = { - ...metadata, // General metadata first + ...metadata, // General metadata progressToken: progressTokenRef.current++, - ...(metadata ?? {}), // Tool-specific metadata overrides + ...(toolMetadata ?? {}), // Tool-specific metadata }; const response = await sendMCPRequest(