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/38] 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 fecd9839..3bf3b887 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 6479b5fb..64798cd9 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 2654feed..798467c1 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 00000000..e26d0b65 --- /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 37073e9b..7bb3d01c 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 c2dbff738e582941d6b1af04c4b9f41c28305487 Mon Sep 17 00:00:00 2001 From: Stanislav Deviatykh Date: Thu, 16 Oct 2025 00:50:42 +0300 Subject: [PATCH 05/38] fix: set user specified scope in DCR --- client/src/lib/auth.ts | 34 ++++++++++++++++++++++----- client/src/lib/constants.ts | 1 + client/src/lib/hooks/useConnection.ts | 12 ++++++---- client/src/lib/oauth-state-machine.ts | 29 ++++++++++++++--------- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 23e21b6a..edd3ef58 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -102,16 +102,35 @@ export const clearClientInformationFromSessionStorage = ({ sessionStorage.removeItem(key); }; +export const getScopeFromSessionStorage = ( + serverUrl: string, +): string | undefined => { + const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl); + const value = sessionStorage.getItem(key); + return value || undefined; +}; + +export const saveScopeToSessionStorage = ( + serverUrl: string, + scope: string | undefined, +) => { + const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl); + if (scope) { + sessionStorage.setItem(key, scope); + } else { + sessionStorage.removeItem(key); + } +}; + export class InspectorOAuthClientProvider implements OAuthClientProvider { - constructor( - protected serverUrl: string, - scope?: string, - ) { - this.scope = scope; + constructor(protected serverUrl: string) { // Save the server URL to session storage sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl); } - scope: string | undefined; + + get scope(): string | undefined { + return getScopeFromSessionStorage(this.serverUrl); + } get redirectUrl() { return window.location.origin + "/oauth/callback"; @@ -224,6 +243,9 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { sessionStorage.removeItem( getServerSpecificKey(SESSION_KEYS.CODE_VERIFIER, this.serverUrl), ); + sessionStorage.removeItem( + getServerSpecificKey(SESSION_KEYS.SCOPE, this.serverUrl), + ); } } diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts index 8e4ff2b5..7e4e0697 100644 --- a/client/src/lib/constants.ts +++ b/client/src/lib/constants.ts @@ -17,6 +17,7 @@ export const SESSION_KEYS = { PREREGISTERED_CLIENT_INFORMATION: "mcp_preregistered_client_information", SERVER_METADATA: "mcp_server_metadata", AUTH_DEBUGGER_STATE: "mcp_auth_debugger_state", + SCOPE: "mcp_scope", } as const; // Generate server-specific session storage keys diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 8639feeb..f6d42380 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -45,6 +45,7 @@ import { clearClientInformationFromSessionStorage, InspectorOAuthClientProvider, saveClientInformationToSessionStorage, + saveScopeToSessionStorage, discoverScopes, } from "../auth"; import { @@ -142,6 +143,10 @@ export function useConnection({ }); }, [oauthClientId, oauthClientSecret, sseUrl]); + useEffect(() => { + saveScopeToSessionStorage(sseUrl, oauthScope); + }, [oauthScope, sseUrl]); + const pushHistory = (request: object, response?: object) => { setRequestHistory((prev) => [ ...prev, @@ -346,10 +351,9 @@ export function useConnection({ } scope = await discoverScopes(sseUrl, resourceMetadata); } - const serverAuthProvider = new InspectorOAuthClientProvider( - sseUrl, - scope, - ); + + saveScopeToSessionStorage(sseUrl, scope); + const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl); const result = await auth(serverAuthProvider, { serverUrl: sseUrl, diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index c505f698..8dc9da8f 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -80,13 +80,16 @@ export const oauthTransitions: Record = { const metadata = context.state.oauthMetadata!; const clientMetadata = context.provider.clientMetadata; - // Prefer scopes from resource metadata if available - const scopesSupported = - context.state.resourceMetadata?.scopes_supported || - metadata.scopes_supported; - // Add all supported scopes to client registration - if (scopesSupported) { - clientMetadata.scope = scopesSupported.join(" "); + // Priority: user-provided scope > discovered scopes + if (!context.provider.scope || context.provider.scope.trim() === "") { + // Prefer scopes from resource metadata if available + const scopesSupported = + context.state.resourceMetadata?.scopes_supported || + metadata.scopes_supported; + // Add all supported scopes to client registration + if (scopesSupported) { + clientMetadata.scope = scopesSupported.join(" "); + } } // Try Static client first, with DCR as fallback @@ -113,10 +116,14 @@ export const oauthTransitions: Record = { const metadata = context.state.oauthMetadata!; const clientInformation = context.state.oauthClientInfo!; - const scope = await discoverScopes( - context.serverUrl, - context.state.resourceMetadata ?? undefined, - ); + // Priority: user-provided scope > discovered scopes + let scope = context.provider.scope; + if (!scope || scope.trim() === "") { + scope = await discoverScopes( + context.serverUrl, + context.state.resourceMetadata ?? undefined, + ); + } const { authorizationUrl, codeVerifier } = await startAuthorization( context.serverUrl, From 40fa6cb27d65fec026bec36f54e7f8d059817e2f Mon Sep 17 00:00:00 2001 From: Stanislav Deviatykh Date: Thu, 16 Oct 2025 00:57:06 +0300 Subject: [PATCH 06/38] refactor test --- client/src/lib/hooks/__tests__/useConnection.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index a9ba6825..2d09b8b2 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -112,6 +112,7 @@ jest.mock("../../auth", () => ({ })), clearClientInformationFromSessionStorage: jest.fn(), saveClientInformationToSessionStorage: jest.fn(), + saveScopeToSessionStorage: jest.fn(), discoverScopes: jest.fn(), })); From 742fd8e18dcf9600e2eafde05bf5c2518bba8ccd Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 21 Oct 2025 12:17:56 -0400 Subject: [PATCH 07/38] Bump TS SDK to latest + npm audit fix * In package.json under cli/, client/, server/ and / - bump @modelcontextprotocol/sdk to ^1.20.1 * In client/package.json - bump @vitejs/plugin-react to ^5.0.4 - bump vite to ^7.1.11 * In package-lock.json - bump playwright to 1.56.1 - cascading changes to rollup and other dependencies --- cli/package.json | 2 +- client/package.json | 6 +- package-lock.json | 1110 ++++++++++++++++++++++++------------------- package.json | 3 +- server/package.json | 2 +- 5 files changed, 637 insertions(+), 486 deletions(-) diff --git a/cli/package.json b/cli/package.json index 527b0035..e67c5fbe 100644 --- a/cli/package.json +++ b/cli/package.json @@ -24,7 +24,7 @@ }, "devDependencies": {}, "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "commander": "^13.1.0", "spawn-rx": "^5.1.2" } diff --git a/client/package.json b/client/package.json index a206816a..a657b797 100644 --- a/client/package.json +++ b/client/package.json @@ -25,7 +25,7 @@ "cleanup:e2e": "node e2e/global-teardown.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", @@ -61,7 +61,7 @@ "@types/react": "^18.3.23", "@types/react-dom": "^18.3.0", "@types/serve-handler": "^6.1.4", - "@vitejs/plugin-react": "^4.7.0", + "@vitejs/plugin-react": "^5.0.4", "autoprefixer": "^10.4.20", "co": "^4.6.0", "eslint": "^9.11.1", @@ -77,6 +77,6 @@ "ts-jest": "^29.4.0", "typescript": "^5.5.3", "typescript-eslint": "^8.38.0", - "vite": "^6.3.5" + "vite": "^7.1.11" } } diff --git a/package-lock.json b/package-lock.json index 373f0790..5c62e1f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,10 +17,11 @@ "@modelcontextprotocol/inspector-cli": "^0.17.1", "@modelcontextprotocol/inspector-client": "^0.17.1", "@modelcontextprotocol/inspector-server": "^0.17.1", - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "concurrently": "^9.2.0", "node-fetch": "^3.3.2", "open": "^10.2.0", + "playwright": "^1.56.1", "shell-quote": "^1.8.3", "spawn-rx": "^5.1.2", "ts-node": "^10.9.2", @@ -50,7 +51,7 @@ "version": "0.17.1", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "commander": "^13.1.0", "spawn-rx": "^5.1.2" }, @@ -73,7 +74,7 @@ "version": "0.17.1", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", @@ -112,7 +113,7 @@ "@types/react": "^18.3.23", "@types/react-dom": "^18.3.0", "@types/serve-handler": "^6.1.4", - "@vitejs/plugin-react": "^4.7.0", + "@vitejs/plugin-react": "^5.0.4", "autoprefixer": "^10.4.20", "co": "^4.6.0", "eslint": "^9.11.1", @@ -128,7 +129,454 @@ "ts-jest": "^29.4.0", "typescript": "^5.5.3", "typescript-eslint": "^8.38.0", - "vite": "^6.3.5" + "vite": "^7.1.11" + } + }, + "client/node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "client/node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "client/node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "client/node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "client/node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "client/node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "client/node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "client/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.38", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", + "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", + "dev": true, + "license": "MIT" + }, + "client/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "client/node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "client/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "client/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "client/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "client/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "client/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "client/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "client/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "client/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "client/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "client/node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "client/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "client/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "client/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "client/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "client/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "client/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "client/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "client/node_modules/@vitejs/plugin-react": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", + "integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.38", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "client/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "client/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "client/node_modules/pkce-challenge": { @@ -140,6 +588,140 @@ "node": ">=16.20.0" } }, + "client/node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "client/node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "client/node_modules/vite": { + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/@adobe/css-tools": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", @@ -1975,6 +2557,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2014,9 +2607,9 @@ "link": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.0.tgz", - "integrity": "sha512-JvKyB6YwS3quM+88JPR0axeRgvdDu3Pv6mdZUy+w4qVkCzGgumb9bXG/TmtDRQv+671yaofVfXSQmFLlWU5qPQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz", + "integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -2086,13 +2679,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", - "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.54.1" + "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -2920,157 +3513,10 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", "cpu": [ "loong64" ], @@ -3081,10 +3527,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", "cpu": [ "ppc64" ], @@ -3095,80 +3541,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", "cpu": [ "arm64" ], @@ -3176,27 +3552,13 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "openharmony" ] }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", "cpu": [ "x64" ], @@ -4002,27 +4364,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -9261,13 +9602,12 @@ } }, "node_modules/playwright": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", - "dev": true, + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.54.1" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -9280,10 +9620,9 @@ } }, "node_modules/playwright-core": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", - "dev": true, + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -9296,7 +9635,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -10038,46 +10376,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.1", - "@rollup/rollup-android-arm64": "4.40.1", - "@rollup/rollup-darwin-arm64": "4.40.1", - "@rollup/rollup-darwin-x64": "4.40.1", - "@rollup/rollup-freebsd-arm64": "4.40.1", - "@rollup/rollup-freebsd-x64": "4.40.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", - "@rollup/rollup-linux-arm64-gnu": "4.40.1", - "@rollup/rollup-linux-arm64-musl": "4.40.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-musl": "4.40.1", - "@rollup/rollup-linux-s390x-gnu": "4.40.1", - "@rollup/rollup-linux-x64-gnu": "4.40.1", - "@rollup/rollup-linux-x64-musl": "4.40.1", - "@rollup/rollup-win32-arm64-msvc": "4.40.1", - "@rollup/rollup-win32-ia32-msvc": "4.40.1", - "@rollup/rollup-win32-x64-msvc": "4.40.1", - "fsevents": "~2.3.2" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -10932,51 +11230,6 @@ "node": ">=0.8" } }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -11461,109 +11714,6 @@ "node": ">= 0.8" } }, - "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -11878,7 +12028,7 @@ "version": "0.17.1", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "cors": "^2.8.5", "express": "^5.1.0", "shell-quote": "^1.8.3", diff --git a/package.json b/package.json index 01f5cc58..8e0f4e01 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@modelcontextprotocol/inspector-cli": "^0.17.1", "@modelcontextprotocol/inspector-client": "^0.17.1", "@modelcontextprotocol/inspector-server": "^0.17.1", - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "concurrently": "^9.2.0", "node-fetch": "^3.3.2", "open": "^10.2.0", @@ -67,6 +67,7 @@ "husky": "^9.1.7", "jest-fixed-jsdom": "^0.0.9", "lint-staged": "^16.1.5", + "playwright": "^1.56.1", "prettier": "^3.6.2", "rimraf": "^6.0.1", "typescript": "^5.4.2" diff --git a/server/package.json b/server/package.json index 50711f2c..c8281216 100644 --- a/server/package.json +++ b/server/package.json @@ -28,7 +28,7 @@ "typescript": "^5.6.2" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "cors": "^2.8.5", "express": "^5.1.0", "shell-quote": "^1.8.3", From eeaaeb5707b0b83f55ded32ca2460962a21b3e87 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 21 Oct 2025 13:29:09 -0400 Subject: [PATCH 08/38] Bump TS SDK to latest + npm audit fix * In package.json under cli/, client/, server/ and / - bump @modelcontextprotocol/sdk to ^1.20.1 * In client/package.json - bump @vitejs/plugin-react to ^5.0.4 - bump vite to ^7.1.11 * In package-lock.json - bump playwright to 1.56.1 - cascading changes to rollup and other dependencies --- package-lock.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 5c62e1f5..118066b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "concurrently": "^9.2.0", "node-fetch": "^3.3.2", "open": "^10.2.0", - "playwright": "^1.56.1", "shell-quote": "^1.8.3", "spawn-rx": "^5.1.2", "ts-node": "^10.9.2", @@ -38,6 +37,7 @@ "husky": "^9.1.7", "jest-fixed-jsdom": "^0.0.9", "lint-staged": "^16.1.5", + "playwright": "^1.56.1", "prettier": "^3.6.2", "rimraf": "^6.0.1", "typescript": "^5.4.2" @@ -9605,6 +9605,7 @@ "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.56.1" @@ -9623,6 +9624,7 @@ "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -9635,6 +9637,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, From eb76696d9748b07c79b07736ac5d26d922107725 Mon Sep 17 00:00:00 2001 From: Stanislav Deviatykh Date: Wed, 22 Oct 2025 00:09:57 +0300 Subject: [PATCH 09/38] correctly clear scope --- client/src/lib/auth.ts | 8 +++++--- client/src/lib/hooks/useConnection.ts | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index edd3ef58..79750112 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -122,6 +122,11 @@ export const saveScopeToSessionStorage = ( } }; +export const clearScopeFromSessionStorage = (serverUrl: string) => { + const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl); + sessionStorage.removeItem(key); +}; + export class InspectorOAuthClientProvider implements OAuthClientProvider { constructor(protected serverUrl: string) { // Save the server URL to session storage @@ -243,9 +248,6 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { sessionStorage.removeItem( getServerSpecificKey(SESSION_KEYS.CODE_VERIFIER, this.serverUrl), ); - sessionStorage.removeItem( - getServerSpecificKey(SESSION_KEYS.SCOPE, this.serverUrl), - ); } } diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index f6d42380..bd15e080 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -46,6 +46,7 @@ import { InspectorOAuthClientProvider, saveClientInformationToSessionStorage, saveScopeToSessionStorage, + clearScopeFromSessionStorage, discoverScopes, } from "../auth"; import { @@ -144,6 +145,11 @@ export function useConnection({ }, [oauthClientId, oauthClientSecret, sseUrl]); useEffect(() => { + if (!oauthScope) { + clearScopeFromSessionStorage(sseUrl); + return; + } + saveScopeToSessionStorage(sseUrl, oauthScope); }, [oauthScope, sseUrl]); From 3b247ea21f18511f33c0de91d47e78c53bc2a092 Mon Sep 17 00:00:00 2001 From: Stanislav Deviatykh Date: Wed, 22 Oct 2025 00:11:58 +0300 Subject: [PATCH 10/38] mock clearScopeFromSessionStorage --- client/src/lib/hooks/__tests__/useConnection.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index 2d09b8b2..b4d6705b 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -113,6 +113,7 @@ jest.mock("../../auth", () => ({ clearClientInformationFromSessionStorage: jest.fn(), saveClientInformationToSessionStorage: jest.fn(), saveScopeToSessionStorage: jest.fn(), + clearScopeFromSessionStorage: jest.fn(), discoverScopes: jest.fn(), })); From aa17bb223f3fe92e04932a65f60b89d3b1a60c1d Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 22 Oct 2025 07:58:44 -0700 Subject: [PATCH 11/38] 0.17.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 118066b0..69a8f1a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.17.1", + "version": "0.17.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.17.1", + "version": "0.17.2", "license": "MIT", "workspaces": [ "client", diff --git a/package.json b/package.json index 8e0f4e01..e25c950e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.17.1", + "version": "0.17.2", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From d99a3576b19cb6675fadbdb6f2da00f28f2db80b Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 22 Oct 2025 08:23:24 -0700 Subject: [PATCH 12/38] chore: bump version to 0.17.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- cli/package.json | 2 +- client/package.json | 2 +- package-lock.json | 12 ++++++------ package.json | 6 +++--- server/package.json | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/package.json b/cli/package.json index e67c5fbe..85431073 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.17.1", + "version": "0.17.2", "description": "CLI for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/client/package.json b/client/package.json index a657b797..56317c40 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.17.1", + "version": "0.17.2", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/package-lock.json b/package-lock.json index 69a8f1a2..e82fa1c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,9 @@ "cli" ], "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.17.1", - "@modelcontextprotocol/inspector-client": "^0.17.1", - "@modelcontextprotocol/inspector-server": "^0.17.1", + "@modelcontextprotocol/inspector-cli": "^0.17.2", + "@modelcontextprotocol/inspector-client": "^0.17.2", + "@modelcontextprotocol/inspector-server": "^0.17.2", "@modelcontextprotocol/sdk": "^1.20.1", "concurrently": "^9.2.0", "node-fetch": "^3.3.2", @@ -48,7 +48,7 @@ }, "cli": { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.17.1", + "version": "0.17.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", @@ -71,7 +71,7 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.17.1", + "version": "0.17.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", @@ -12028,7 +12028,7 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.17.1", + "version": "0.17.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", diff --git a/package.json b/package.json index e25c950e..35b3f622 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,9 @@ "check-version": "node scripts/check-version-consistency.js" }, "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.17.1", - "@modelcontextprotocol/inspector-client": "^0.17.1", - "@modelcontextprotocol/inspector-server": "^0.17.1", + "@modelcontextprotocol/inspector-cli": "^0.17.2", + "@modelcontextprotocol/inspector-client": "^0.17.2", + "@modelcontextprotocol/inspector-server": "^0.17.2", "@modelcontextprotocol/sdk": "^1.20.1", "concurrently": "^9.2.0", "node-fetch": "^3.3.2", diff --git a/server/package.json b/server/package.json index c8281216..f0d34655 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.17.1", + "version": "0.17.2", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", 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 13/38] 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 7f531471..0f8997bd 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 c4cc6d21..eefedba4 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 e26d0b65..339e7015 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 14/38] 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 04d298e1..780fa208 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 0c20e7c8..3e07d017 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 4f639c08..4584ff0a 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 23/38] 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 909f17af..51681411 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 e17c24ed..d58cd8fe 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 24/38] 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 d58cd8fe..c53294d5 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( From 37acee9283642bbc826ea09d26565a4a01c61979 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 29 Oct 2025 14:28:49 -0400 Subject: [PATCH 25/38] Handle nullable arrays in tool inputSchemas * In DynamicJsonForm.tsx - In debouncedUpdateParent, formatJson, and validateJson, - when trimming the JSON value, use ?.trim() in case the field is null * In jsonUtils.ts - in tryParseJson - use ?.trim() in case the field is null * In schemaUtils.ts - in normalizeUnionType - add condition to handle the case where there is an anyOf with array or null as options * In ToolsTab.tsx - for nullable fields, in null checkbox, in onCheckedChange handler - when not checked, if prop type is array, set to undefined (empty input field that can be edited rather than the word "null" as the default could provide Fixes #846 --- client/src/components/DynamicJsonForm.tsx | 8 ++++---- client/src/components/ToolsTab.tsx | 22 ++++++++++++---------- client/src/utils/jsonUtils.ts | 3 ++- client/src/utils/schemaUtils.ts | 10 ++++++++++ 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 96b03b3a..685401ab 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -113,8 +113,8 @@ const DynamicJsonForm = forwardRef( setJsonError(errorMessage); // Reset to default for clearly invalid JSON (not just incomplete typing) - const trimmed = jsonString.trim(); - if (trimmed.length > 5 && !trimmed.match(/^[\s[{]/)) { + const trimmed = jsonString?.trim(); + if (trimmed && trimmed.length > 5 && !trimmed.match(/^[\s[{]/)) { onChange(generateDefaultValue(schema)); } } @@ -155,7 +155,7 @@ const DynamicJsonForm = forwardRef( const formatJson = () => { try { - const jsonStr = rawJsonValue.trim(); + const jsonStr = rawJsonValue?.trim(); if (!jsonStr) { return; } @@ -171,7 +171,7 @@ const DynamicJsonForm = forwardRef( const validateJson = () => { if (!isJsonMode) return { isValid: true, error: null }; try { - const jsonStr = rawJsonValue.trim(); + const jsonStr = rawJsonValue?.trim(); if (!jsonStr) return { isValid: true, error: null }; const parsed = JSON.parse(jsonStr); // Clear any pending debounced update and immediately update parent diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 740cc5ed..6b9c1c3e 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -181,16 +181,18 @@ const ToolsTab = ({ ...params, [key]: checked ? null - : prop.default !== null - ? prop.default - : prop.type === "boolean" - ? false - : prop.type === "string" - ? "" - : prop.type === "number" || - prop.type === "integer" - ? undefined - : undefined, + : prop.type === "array" + ? undefined + : prop.default !== null + ? prop.default + : prop.type === "boolean" + ? false + : prop.type === "string" + ? "" + : prop.type === "number" || + prop.type === "integer" + ? undefined + : undefined, }) } /> diff --git a/client/src/utils/jsonUtils.ts b/client/src/utils/jsonUtils.ts index 3a9336f1..c2f9af52 100644 --- a/client/src/utils/jsonUtils.ts +++ b/client/src/utils/jsonUtils.ts @@ -84,8 +84,9 @@ export function tryParseJson(str: string): { success: boolean; data: JsonValue; } { - const trimmed = str.trim(); + const trimmed = str?.trim(); if ( + trimmed && !(trimmed.startsWith("{") && trimmed.endsWith("}")) && !(trimmed.startsWith("[") && trimmed.endsWith("]")) ) { diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index 73137805..27f13eaf 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -191,6 +191,16 @@ export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType { return { ...schema, type: "integer", anyOf: undefined, nullable: true }; } + // Handle anyOf with exactly array and null (FastMCP pattern) + if ( + schema.anyOf && + schema.anyOf.length === 2 && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "array") && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") + ) { + return { ...schema, type: "array", anyOf: undefined, nullable: true }; + } + // Handle array type with exactly string and null if ( Array.isArray(schema.type) && From 002dad07bcf5e47c67933498f3a9a698490642e0 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Mon, 3 Nov 2025 17:01:50 +0900 Subject: [PATCH 26/38] Fix a typo --- .github/workflows/e2e_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 573672cc..378905b4 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -8,7 +8,7 @@ on: jobs: test: - # Installing Playright dependencies can take quite awhile, and also depends on GitHub CI load. + # Installing Playwright dependencies can take quite awhile, and also depends on GitHub CI load. timeout-minutes: 15 runs-on: ubuntu-latest From 2ab7d625e4d76b0afc477adb15eac1f5c3314142 Mon Sep 17 00:00:00 2001 From: Vincent <0426vincent@gmail.com> Date: Sat, 1 Nov 2025 21:03:02 -0700 Subject: [PATCH 27/38] Resolve ref before validation --- .../hooks/__tests__/useConnection.test.tsx | 58 ++++++++++++++++++- client/src/lib/hooks/useConnection.ts | 36 ++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index a9ba6825..5c47767f 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -1,7 +1,10 @@ import { renderHook, act } from "@testing-library/react"; import { useConnection } from "../useConnection"; import { z } from "zod"; -import { ClientRequest } from "@modelcontextprotocol/sdk/types.js"; +import { + ClientRequest, + JSONRPCMessage, +} from "@modelcontextprotocol/sdk/types.js"; import { DEFAULT_INSPECTOR_CONFIG, CLIENT_IDENTITY } from "../../constants"; import { SSEClientTransportOptions, @@ -42,10 +45,12 @@ const mockSSETransport: { start: jest.Mock; url: URL | undefined; options: SSEClientTransportOptions | undefined; + onmessage?: (message: JSONRPCMessage) => void; } = { start: jest.fn(), url: undefined, options: undefined, + onmessage: undefined, }; const mockStreamableHTTPTransport: { @@ -480,6 +485,57 @@ describe("useConnection", () => { }); }); + describe("Ref Resolution", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("resolves $ref references in requestedSchema properties before validation", async () => { + const mockProtocolOnMessage = jest.fn(); + + mockSSETransport.onmessage = mockProtocolOnMessage; + + const { result } = renderHook(() => useConnection(defaultProps)); + + await act(async () => { + await result.current.connect(); + }); + + const mockRequestWithRef: JSONRPCMessage = { + jsonrpc: "2.0", + id: 1, + method: "elicitation/create", + params: { + message: "Please provide your information", + requestedSchema: { + type: "object", + properties: { + name: { + $ref: "#/properties/nameDef", + }, + nameDef: { + type: "string", + title: "Name", + }, + }, + }, + }, + }; + + await act(async () => { + mockSSETransport.onmessage!(mockRequestWithRef); + }); + + expect(mockProtocolOnMessage).toHaveBeenCalledTimes(1); + + const message = mockProtocolOnMessage.mock.calls[0][0]; + expect(message.params.requestedSchema.properties.name).toEqual({ + type: "string", + title: "Name", + }); + }); + }); + describe("URL Port Handling", () => { const SSEClientTransport = jest.requireMock( "@modelcontextprotocol/sdk/client/sse.js", diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 8639feeb..486ba51b 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -30,6 +30,8 @@ import { Progress, LoggingLevel, ElicitRequestSchema, + JSONRPCMessage, + isJSONRPCRequest, } from "@modelcontextprotocol/sdk/types.js"; import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { useEffect, useState } from "react"; @@ -57,6 +59,8 @@ import { getMCPServerRequestTimeout } from "@/utils/configUtils"; import { InspectorConfig } from "../configurationTypes"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { CustomHeaders } from "../types/customHeaders"; +import { JsonSchemaType } from "@/utils/jsonUtils"; +import { resolveRef } from "@/utils/schemaUtils"; interface UseConnectionOptions { transportType: "stdio" | "sse" | "streamable-http"; @@ -681,6 +685,38 @@ export function useConnection({ await client.connect(transport as Transport); + const protocolOnMessage = transport.onmessage; + if (protocolOnMessage) { + transport.onmessage = (message: JSONRPCMessage) => { + // Resolve $ref references in requests before validation + if (isJSONRPCRequest(message) && message.params?.requestedSchema) { + const requestedSchema = message.params + .requestedSchema as JsonSchemaType; + if (requestedSchema?.properties) { + const resolvedProperties = Object.fromEntries( + Object.entries( + requestedSchema.properties as Record< + string, + JsonSchemaType + >, + ).map(([key, propSchema]) => [ + key, + resolveRef(propSchema, requestedSchema), + ]), + ); + message.params = { + ...message.params, + requestedSchema: { + ...requestedSchema, + properties: resolvedProperties, + }, + }; + } + } + protocolOnMessage(message); + }; + } + setClientTransport(transport); capabilities = client.getServerCapabilities(); From b000fc25bb41ac82b118b9d8f36175fcf333c95a Mon Sep 17 00:00:00 2001 From: Vincent <0426vincent@gmail.com> Date: Tue, 4 Nov 2025 09:24:54 -0800 Subject: [PATCH 28/38] fix: Remove redundant type --- client/src/lib/hooks/useConnection.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 486ba51b..4918cdd3 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -30,7 +30,6 @@ import { Progress, LoggingLevel, ElicitRequestSchema, - JSONRPCMessage, isJSONRPCRequest, } from "@modelcontextprotocol/sdk/types.js"; import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js"; @@ -687,22 +686,19 @@ export function useConnection({ const protocolOnMessage = transport.onmessage; if (protocolOnMessage) { - transport.onmessage = (message: JSONRPCMessage) => { + transport.onmessage = (message) => { // Resolve $ref references in requests before validation if (isJSONRPCRequest(message) && message.params?.requestedSchema) { const requestedSchema = message.params .requestedSchema as JsonSchemaType; if (requestedSchema?.properties) { const resolvedProperties = Object.fromEntries( - Object.entries( - requestedSchema.properties as Record< - string, - JsonSchemaType - >, - ).map(([key, propSchema]) => [ - key, - resolveRef(propSchema, requestedSchema), - ]), + Object.entries(requestedSchema.properties).map( + ([key, propSchema]) => [ + key, + resolveRef(propSchema, requestedSchema), + ], + ), ); message.params = { ...message.params, From 8ad6253cd9586be42b8297caed16813e498cd16c Mon Sep 17 00:00:00 2001 From: Vincent <0426vincent@gmail.com> Date: Tue, 4 Nov 2025 09:31:31 -0800 Subject: [PATCH 29/38] fix: Extract function to utils --- client/src/lib/hooks/useConnection.ts | 29 ++----------------- client/src/utils/schemaUtils.ts | 40 ++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 4918cdd3..4e7221c3 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -30,7 +30,6 @@ import { Progress, LoggingLevel, ElicitRequestSchema, - isJSONRPCRequest, } from "@modelcontextprotocol/sdk/types.js"; import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { useEffect, useState } from "react"; @@ -58,8 +57,7 @@ import { getMCPServerRequestTimeout } from "@/utils/configUtils"; import { InspectorConfig } from "../configurationTypes"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { CustomHeaders } from "../types/customHeaders"; -import { JsonSchemaType } from "@/utils/jsonUtils"; -import { resolveRef } from "@/utils/schemaUtils"; +import { resolveRefsInMessage } from "@/utils/schemaUtils"; interface UseConnectionOptions { transportType: "stdio" | "sse" | "streamable-http"; @@ -687,29 +685,8 @@ export function useConnection({ const protocolOnMessage = transport.onmessage; if (protocolOnMessage) { transport.onmessage = (message) => { - // Resolve $ref references in requests before validation - if (isJSONRPCRequest(message) && message.params?.requestedSchema) { - const requestedSchema = message.params - .requestedSchema as JsonSchemaType; - if (requestedSchema?.properties) { - const resolvedProperties = Object.fromEntries( - Object.entries(requestedSchema.properties).map( - ([key, propSchema]) => [ - key, - resolveRef(propSchema, requestedSchema), - ], - ), - ); - message.params = { - ...message.params, - requestedSchema: { - ...requestedSchema, - properties: resolvedProperties, - }, - }; - } - } - protocolOnMessage(message); + const resolvedMessage = resolveRefsInMessage(message); + protocolOnMessage(resolvedMessage); }; } diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index 36587021..e7fe2c8f 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -1,7 +1,8 @@ import type { JsonValue, JsonSchemaType, JsonObject } from "./jsonUtils"; import Ajv from "ajv"; import type { ValidateFunction } from "ajv"; -import type { Tool } from "@modelcontextprotocol/sdk/types.js"; +import type { Tool, JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; +import { isJSONRPCRequest } from "@modelcontextprotocol/sdk/types.js"; const ajv = new Ajv(); @@ -299,3 +300,40 @@ export function formatFieldLabel(key: string): string { .replace(/_/g, " ") // Replace underscores with spaces .replace(/^\w/, (c) => c.toUpperCase()); // Capitalize first letter } + +/** + * Resolves $ref references in a JSON-RPC message's requestedSchema + * @param message The JSON-RPC message that may contain $ref references + * @returns A new message with resolved $ref references, or the original message if no resolution is needed + */ +export function resolveRefsInMessage(message: JSONRPCMessage): JSONRPCMessage { + if (!isJSONRPCRequest(message) || !message.params?.requestedSchema) { + return message; + } + + const requestedSchema = message.params.requestedSchema as JsonSchemaType; + + if (!requestedSchema?.properties) { + return message; + } + + const resolvedMessage = { + ...message, + params: { + ...message.params, + requestedSchema: { + ...requestedSchema, + properties: Object.fromEntries( + Object.entries(requestedSchema.properties).map( + ([key, propSchema]) => [ + key, + resolveRef(propSchema, requestedSchema), + ], + ), + ), + }, + }, + }; + + return resolvedMessage; +} From 12332932b08ef8ddf9a69094c13714af23234de5 Mon Sep 17 00:00:00 2001 From: Vincent <0426vincent@gmail.com> Date: Sat, 8 Nov 2025 09:57:48 -0800 Subject: [PATCH 30/38] fix: Modify test mock data --- .../lib/hooks/__tests__/useConnection.test.tsx | 16 +++++++++------- client/src/utils/schemaUtils.ts | 11 ++++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index 5c47767f..4f382740 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -510,12 +510,13 @@ describe("useConnection", () => { requestedSchema: { type: "object", properties: { - name: { - $ref: "#/properties/nameDef", - }, - nameDef: { + source: { type: "string", - title: "Name", + minLength: 1, + title: "A Connectable Node", + }, + target: { + $ref: "#/properties/source", }, }, }, @@ -529,9 +530,10 @@ describe("useConnection", () => { expect(mockProtocolOnMessage).toHaveBeenCalledTimes(1); const message = mockProtocolOnMessage.mock.calls[0][0]; - expect(message.params.requestedSchema.properties.name).toEqual({ + expect(message.params.requestedSchema.properties.target).toEqual({ type: "string", - title: "Name", + minLength: 1, + title: "A Connectable Node", }); }); }); diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index e7fe2c8f..42d77d43 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -302,7 +302,7 @@ export function formatFieldLabel(key: string): string { } /** - * Resolves $ref references in a JSON-RPC message's requestedSchema + * Resolves `$ref` references in a JSON-RPC "elicitation/create" message's `requestedSchema` field * @param message The JSON-RPC message that may contain $ref references * @returns A new message with resolved $ref references, or the original message if no resolution is needed */ @@ -325,10 +325,11 @@ export function resolveRefsInMessage(message: JSONRPCMessage): JSONRPCMessage { ...requestedSchema, properties: Object.fromEntries( Object.entries(requestedSchema.properties).map( - ([key, propSchema]) => [ - key, - resolveRef(propSchema, requestedSchema), - ], + ([key, propSchema]) => { + const resolved = resolveRef(propSchema, requestedSchema); + const normalized = normalizeUnionType(resolved); + return [key, normalized]; + }, ), ), }, From c1f18a8edd5cadd79a624c14152e2649a65e325e Mon Sep 17 00:00:00 2001 From: Vincent <0426vincent@gmail.com> Date: Sat, 8 Nov 2025 10:46:28 -0800 Subject: [PATCH 31/38] fix: add ref resolves def test --- .../hooks/__tests__/useConnection.test.tsx | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index 4f382740..e857b2c3 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -536,6 +536,175 @@ describe("useConnection", () => { title: "A Connectable Node", }); }); + + test("resolves $ref references to $defs in requestedSchema", async () => { + const mockProtocolOnMessage = jest.fn(); + + mockSSETransport.onmessage = mockProtocolOnMessage; + + const { result } = renderHook(() => useConnection(defaultProps)); + + await act(async () => { + await result.current.connect(); + }); + + const mockRequestWithDefs: JSONRPCMessage = { + jsonrpc: "2.0", + id: 1, + method: "elicitation/create", + params: { + message: "Please provide your information", + requestedSchema: { + type: "object", + properties: { + user: { + $ref: "#/$defs/UserInput", + }, + }, + $defs: { + UserInput: { + type: "object", + properties: { + name: { + type: "string", + title: "Name", + }, + age: { + type: "integer", + title: "Age", + minimum: 0, + }, + }, + required: ["name"], + }, + }, + }, + }, + }; + + await act(async () => { + mockSSETransport.onmessage!(mockRequestWithDefs); + }); + + expect(mockProtocolOnMessage).toHaveBeenCalledTimes(1); + + const message = mockProtocolOnMessage.mock.calls[0][0]; + // The $ref should be resolved to the actual UserInput definition + expect(message.params.requestedSchema.properties.user).toEqual({ + type: "object", + properties: { + name: { + type: "string", + title: "Name", + }, + age: { + type: "integer", + title: "Age", + minimum: 0, + }, + }, + required: ["name"], + }); + }); + test("resolves nested $ref references within $defs definitions", async () => { + const mockProtocolOnMessage = jest.fn(); + + mockSSETransport.onmessage = mockProtocolOnMessage; + + const { result } = renderHook(() => useConnection(defaultProps)); + + await act(async () => { + await result.current.connect(); + }); + + // This mirrors the pattern from FastMCP where a definition references another definition + const mockRequestWithNestedDefs: JSONRPCMessage = { + jsonrpc: "2.0", + id: 1, + method: "elicitation/create", + params: { + message: "Please provide your information", + requestedSchema: { + type: "object", + properties: { + person: { + $ref: "#/$defs/PersonWithAddress", + }, + }, + $defs: { + PersonWithAddress: { + type: "object", + properties: { + name: { + type: "string", + title: "Name", + }, + address: { + $ref: "#/$defs/Address", // ← Nested ref: definition references another definition + }, + }, + required: ["name", "address"], + }, + Address: { + type: "object", + properties: { + street: { + type: "string", + title: "Street", + }, + city: { + type: "string", + title: "City", + }, + zipcode: { + type: "string", + title: "Zipcode", + }, + }, + required: ["street", "city", "zipcode"], + }, + }, + }, + }, + }; + + await act(async () => { + mockSSETransport.onmessage!(mockRequestWithNestedDefs); + }); + + expect(mockProtocolOnMessage).toHaveBeenCalledTimes(1); + + const message = mockProtocolOnMessage.mock.calls[0][0]; + // The $ref should be resolved, and nested refs within $defs should also be resolved + expect(message.params.requestedSchema.properties.person).toEqual({ + type: "object", + properties: { + name: { + type: "string", + title: "Name", + }, + address: { + type: "object", + properties: { + street: { + type: "string", + title: "Street", + }, + city: { + type: "string", + title: "City", + }, + zipcode: { + type: "string", + title: "Zipcode", + }, + }, + required: ["street", "city", "zipcode"], + }, + }, + required: ["name", "address"], + }); + }); }); describe("URL Port Handling", () => { From cbb6a0a53c5e824aed59f114d044c597582af80d Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 8 Nov 2025 19:27:05 -0700 Subject: [PATCH 32/38] Add version consideration and context for Inspector V2 development in documentation --- .github/ISSUE_TEMPLATE/bug_report.md | 4 ++ .github/pull_request_template.md | 57 ++++++++++++++++++++++++++++ AGENTS.md | 2 + CONTRIBUTING.md | 13 ++++++- 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0db5467a..31eb7a22 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -34,3 +34,7 @@ If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. + +**Version Consideration** + +Inspector V2 is under development to address architectural and UX improvements. See [CONTRIBUTING.md](CONTRIBUTING.md) for information about V2 development. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..50189d36 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,57 @@ +## Summary + + + +> **Note:** Inspector V2 is under development to address architectural and UX improvements. See [CONTRIBUTING.md](CONTRIBUTING.md) for information about V2 development. + +## Type of Change + + + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Documentation update +- [ ] Refactoring (no functional changes) +- [ ] Test updates +- [ ] Build/CI improvements + +## Changes Made + + + +## Related Issues + + + +## Testing + + + +- [ ] Tested in UI mode +- [ ] Tested in CLI mode +- [ ] Tested with STDIO transport +- [ ] Tested with SSE transport +- [ ] Tested with Streamable HTTP transport +- [ ] Added/updated automated tests +- [ ] Manual testing performed + +### Test Results and/or Instructions + + + +Screenshots are encouraged to share your testing results for this change. + +## Checklist + +- [ ] Code follows the style guidelines (ran `npm run prettier-fix`) +- [ ] Self-review completed +- [ ] Code is commented where necessary +- [ ] Documentation updated (README, comments, etc.) + +## Breaking Changes + + + +## Additional Context + + diff --git a/AGENTS.md b/AGENTS.md index ec826b1f..b0e46712 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # MCP Inspector Development Guide +> **Note:** Inspector V2 is under development to address architectural and UX improvements. These guidelines apply to the current version (V1). See [CONTRIBUTING.md](CONTRIBUTING.md) for information about V2 development. + ## Build Commands - Build all: `npm run build` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0129a9e3..9f98adcf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,17 @@ Thanks for your interest in contributing! This guide explains how to get involve 3. Run `npm run dev` to start both client and server in development mode 4. Use the web UI at http://localhost:6274 to interact with the inspector +## Inspector V2 Development + +We're actively developing **Inspector V2** to address architectural and UX improvements. We invite you to follow progress and participate in the Inspector V2 Working Group in [Discord](https://modelcontextprotocol.io/community/communication), [weekly meetings](https://meet.modelcontextprotocol.io/tag/inspector-v2-wg), and [GitHub Discussions](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/categories/meeting-notes-other) (where notes are posted after meetings). + +**Current version (V1) contribution scope:** + +- Critical issues are actively maintained +- Small UX improvements are welcome +- Documentation updates are always appreciated +- Major changes will be directed to V2 development + ## Development Process & Pull Requests 1. Create a new branch for your changes @@ -30,7 +41,7 @@ If you find a security vulnerability, please refer to our [Security Policy](SECU ## Questions? -Feel free to [open an issue](https://github.com/modelcontextprotocol/mcp-inspector/issues) for questions or create a discussion for general topics. +Feel free to [open an issue](https://github.com/modelcontextprotocol/mcp-inspector/issues) for questions or join the MCP Contributor [Discord server](https://modelcontextprotocol.io/community/communication). Also, please see notes above on Inspector V2 Development. ## License From 3930bdd7003a48b9c001d593f1cf561495f6cf64 Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 12 Nov 2025 18:29:41 -0700 Subject: [PATCH 33/38] Bump version to 0.17.3 --- cli/package.json | 2 +- client/package.json | 2 +- package-lock.json | 16 ++++++++-------- package.json | 8 ++++---- server/package.json | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cli/package.json b/cli/package.json index 85431073..b885b775 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.17.2", + "version": "0.17.3", "description": "CLI for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/client/package.json b/client/package.json index 56317c40..9f97e023 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.17.2", + "version": "0.17.3", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/package-lock.json b/package-lock.json index e82fa1c6..30966d16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.17.2", + "version": "0.17.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.17.2", + "version": "0.17.3", "license": "MIT", "workspaces": [ "client", @@ -14,9 +14,9 @@ "cli" ], "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.17.2", - "@modelcontextprotocol/inspector-client": "^0.17.2", - "@modelcontextprotocol/inspector-server": "^0.17.2", + "@modelcontextprotocol/inspector-cli": "^0.17.3", + "@modelcontextprotocol/inspector-client": "^0.17.3", + "@modelcontextprotocol/inspector-server": "^0.17.3", "@modelcontextprotocol/sdk": "^1.20.1", "concurrently": "^9.2.0", "node-fetch": "^3.3.2", @@ -48,7 +48,7 @@ }, "cli": { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.17.2", + "version": "0.17.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", @@ -71,7 +71,7 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.17.2", + "version": "0.17.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", @@ -12028,7 +12028,7 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.17.2", + "version": "0.17.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", diff --git a/package.json b/package.json index 35b3f622..d001e665 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.17.2", + "version": "0.17.3", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -47,9 +47,9 @@ "check-version": "node scripts/check-version-consistency.js" }, "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.17.2", - "@modelcontextprotocol/inspector-client": "^0.17.2", - "@modelcontextprotocol/inspector-server": "^0.17.2", + "@modelcontextprotocol/inspector-cli": "^0.17.3", + "@modelcontextprotocol/inspector-client": "^0.17.3", + "@modelcontextprotocol/inspector-server": "^0.17.3", "@modelcontextprotocol/sdk": "^1.20.1", "concurrently": "^9.2.0", "node-fetch": "^3.3.2", diff --git a/server/package.json b/server/package.json index f0d34655..89203f10 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.17.2", + "version": "0.17.3", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 6106b1b5625d5014f22ea4606fcc23b92f1decd3 Mon Sep 17 00:00:00 2001 From: Vincent <0426vincent@gmail.com> Date: Thu, 13 Nov 2025 14:30:28 -0800 Subject: [PATCH 34/38] fix: Remove incorrect nested test --- .../hooks/__tests__/useConnection.test.tsx | 99 ------------------- 1 file changed, 99 deletions(-) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index 21563689..ccd92965 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -608,105 +608,6 @@ describe("useConnection", () => { required: ["name"], }); }); - test("resolves nested $ref references within $defs definitions", async () => { - const mockProtocolOnMessage = jest.fn(); - - mockSSETransport.onmessage = mockProtocolOnMessage; - - const { result } = renderHook(() => useConnection(defaultProps)); - - await act(async () => { - await result.current.connect(); - }); - - // This mirrors the pattern from FastMCP where a definition references another definition - const mockRequestWithNestedDefs: JSONRPCMessage = { - jsonrpc: "2.0", - id: 1, - method: "elicitation/create", - params: { - message: "Please provide your information", - requestedSchema: { - type: "object", - properties: { - person: { - $ref: "#/$defs/PersonWithAddress", - }, - }, - $defs: { - PersonWithAddress: { - type: "object", - properties: { - name: { - type: "string", - title: "Name", - }, - address: { - $ref: "#/$defs/Address", // ← Nested ref: definition references another definition - }, - }, - required: ["name", "address"], - }, - Address: { - type: "object", - properties: { - street: { - type: "string", - title: "Street", - }, - city: { - type: "string", - title: "City", - }, - zipcode: { - type: "string", - title: "Zipcode", - }, - }, - required: ["street", "city", "zipcode"], - }, - }, - }, - }, - }; - - await act(async () => { - mockSSETransport.onmessage!(mockRequestWithNestedDefs); - }); - - expect(mockProtocolOnMessage).toHaveBeenCalledTimes(1); - - const message = mockProtocolOnMessage.mock.calls[0][0]; - // The $ref should be resolved, and nested refs within $defs should also be resolved - expect(message.params.requestedSchema.properties.person).toEqual({ - type: "object", - properties: { - name: { - type: "string", - title: "Name", - }, - address: { - type: "object", - properties: { - street: { - type: "string", - title: "Street", - }, - city: { - type: "string", - title: "City", - }, - zipcode: { - type: "string", - title: "Zipcode", - }, - }, - required: ["street", "city", "zipcode"], - }, - }, - required: ["name", "address"], - }); - }); }); describe("URL Port Handling", () => { From c93c10beca964c5a3bd1737688295fd1aed20f61 Mon Sep 17 00:00:00 2001 From: olaservo Date: Fri, 14 Nov 2025 05:25:36 -0700 Subject: [PATCH 35/38] Add typegaurd for blob --- client/src/components/ToolResults.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/components/ToolResults.tsx b/client/src/components/ToolResults.tsx index 6479b5fb..b52acc7d 100644 --- a/client/src/components/ToolResults.tsx +++ b/client/src/components/ToolResults.tsx @@ -205,7 +205,8 @@ const ToolResults = ({ /> )} {item.type === "resource" && - (item.resource?.mimeType?.startsWith("audio/") ? ( + (item.resource?.mimeType?.startsWith("audio/") && + "blob" in item.resource ? (
- {entries.map((entry, index) => ( -
-
- - updateEntry(index, "key", e.target.value)} - /> + {entries.map((entry, index) => { + const trimmedKey = entry.key.trim(); + const hasInvalidPrefix = + trimmedKey !== "" && !hasValidMetaPrefix(trimmedKey); + const isReservedKey = + trimmedKey !== "" && isReservedMetaKey(trimmedKey); + const hasInvalidName = + trimmedKey !== "" && !hasValidMetaName(trimmedKey); + const validationMessage = hasInvalidPrefix + ? META_PREFIX_RULES_MESSAGE + : isReservedKey + ? RESERVED_NAMESPACE_MESSAGE + : hasInvalidName + ? META_NAME_RULES_MESSAGE + : null; + return ( +
+
+
+ + + updateEntry(index, "key", e.target.value) + } + aria-invalid={Boolean(validationMessage)} + className={cn( + validationMessage && + "border-red-500 focus-visible:ring-red-500 focus-visible:ring-1", + )} + /> +
+
+ + + updateEntry(index, "value", e.target.value) + } + disabled={Boolean(validationMessage)} + /> +
+ +
+ {validationMessage && ( +

+ {validationMessage} +

+ )}
-
- - updateEntry(index, "value", e.target.value)} - /> -
- -
- ))} + ); + })}
{entries.length === 0 && ( diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 02efc729..f8175f9d 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -40,6 +40,15 @@ import JsonView from "./JsonView"; import ToolResults from "./ToolResults"; import { useToast } from "@/lib/hooks/useToast"; import useCopy from "@/lib/hooks/useCopy"; +import { cn } from "@/lib/utils"; +import { + META_NAME_RULES_MESSAGE, + META_PREFIX_RULES_MESSAGE, + RESERVED_NAMESPACE_MESSAGE, + hasValidMetaName, + hasValidMetaPrefix, + isReservedMetaKey, +} from "@/utils/metaUtils"; // Type guard to safely detect the optional _meta field without using `any` const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } => @@ -122,6 +131,21 @@ const ToolsTab = ({ formRefs.current = {}; }, [selectedTool]); + const hasReservedMetadataEntry = metadataEntries.some(({ key }) => { + const trimmedKey = key.trim(); + return trimmedKey !== "" && isReservedMetaKey(trimmedKey); + }); + + const hasInvalidMetaPrefixEntry = metadataEntries.some(({ key }) => { + const trimmedKey = key.trim(); + return trimmedKey !== "" && !hasValidMetaPrefix(trimmedKey); + }); + + const hasInvalidMetaNameEntry = metadataEntries.some(({ key }) => { + const trimmedKey = key.trim(); + return trimmedKey !== "" && !hasValidMetaName(trimmedKey); + }); + return (
@@ -438,68 +462,102 @@ const ToolsTab = ({

) : (
- {metadataEntries.map((entry, index) => ( -
- - { - const value = e.target.value; - setMetadataEntries((prev) => - prev.map((m, i) => - i === index ? { ...m, key: value } : m, - ), - ); - }} - className="h-8 flex-1" - /> - - { - const value = e.target.value; - setMetadataEntries((prev) => - prev.map((m, i) => - i === index ? { ...m, value } : m, - ), - ); - }} - className="h-8 flex-1" - /> - -
- ))} + {metadataEntries.map((entry, index) => { + const trimmedKey = entry.key.trim(); + const hasInvalidPrefix = + trimmedKey !== "" && !hasValidMetaPrefix(trimmedKey); + const isReservedKey = + trimmedKey !== "" && isReservedMetaKey(trimmedKey); + const hasInvalidName = + trimmedKey !== "" && !hasValidMetaName(trimmedKey); + const validationMessage = hasInvalidPrefix + ? META_PREFIX_RULES_MESSAGE + : isReservedKey + ? RESERVED_NAMESPACE_MESSAGE + : hasInvalidName + ? META_NAME_RULES_MESSAGE + : null; + return ( +
+
+ + { + const value = e.target.value; + setMetadataEntries((prev) => + prev.map((m, i) => + i === index ? { ...m, key: value } : m, + ), + ); + }} + className={cn( + "h-8 flex-1", + validationMessage && + "border-red-500 focus-visible:ring-red-500 focus-visible:ring-1", + )} + aria-invalid={Boolean(validationMessage)} + /> + + { + const value = e.target.value; + setMetadataEntries((prev) => + prev.map((m, i) => + i === index ? { ...m, value } : m, + ), + ); + }} + className="h-8 flex-1" + disabled={Boolean(validationMessage)} + /> + +
+ {validationMessage && ( +

+ {validationMessage} +

+ )} +
+ ); + })}
)} + {(hasReservedMetadataEntry || + hasInvalidMetaPrefixEntry || + hasInvalidMetaNameEntry) && ( +

+ Remove reserved or invalid metadata keys (prefix/name) + before running the tool. +

+ )}
{selectedTool.outputSchema && (
@@ -585,7 +643,15 @@ const ToolsTab = ({ const metadata = metadataEntries.reduce< Record >((acc, { key, value }) => { - if (key.trim() !== "") acc[key] = value; + const trimmedKey = key.trim(); + if ( + trimmedKey !== "" && + hasValidMetaPrefix(trimmedKey) && + !isReservedMetaKey(trimmedKey) && + hasValidMetaName(trimmedKey) + ) { + acc[trimmedKey] = value; + } return acc; }, {}); await callTool( @@ -597,7 +663,13 @@ const ToolsTab = ({ setIsToolRunning(false); } }} - disabled={isToolRunning || hasValidationErrors} + disabled={ + isToolRunning || + hasValidationErrors || + hasReservedMetadataEntry || + hasInvalidMetaPrefixEntry || + hasInvalidMetaNameEntry + } > {isToolRunning ? ( <> diff --git a/client/src/components/__tests__/MetadataTab.test.tsx b/client/src/components/__tests__/MetadataTab.test.tsx index 9d0cbf99..f22e102f 100644 --- a/client/src/components/__tests__/MetadataTab.test.tsx +++ b/client/src/components/__tests__/MetadataTab.test.tsx @@ -2,6 +2,11 @@ import { render, screen, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom"; import MetadataTab from "../MetadataTab"; import { Tabs } from "@/components/ui/tabs"; +import { + META_NAME_RULES_MESSAGE, + META_PREFIX_RULES_MESSAGE, + RESERVED_NAMESPACE_MESSAGE, +} from "@/utils/metaUtils"; describe("MetadataTab", () => { const defaultProps = { @@ -270,6 +275,38 @@ describe("MetadataTab", () => { }); }); + describe("Reserved Metadata Keys", () => { + test.each` + description | value | message | shouldDisableValue + ${"reserved keys with prefix"} | ${"modelcontextprotocol.io/flip"} | ${RESERVED_NAMESPACE_MESSAGE} | ${true} + ${"reserved root without slash"} | ${"modelcontextprotocol.io"} | ${RESERVED_NAMESPACE_MESSAGE} | ${true} + ${"nested modelcontextprotocol domain"} | ${"api.modelcontextprotocol.org"} | ${RESERVED_NAMESPACE_MESSAGE} | ${false} + ${"nested mcp domain"} | ${"tools.mcp.com/path"} | ${RESERVED_NAMESPACE_MESSAGE} | ${false} + ${"invalid name segments"} | ${"custom/bad-"} | ${META_NAME_RULES_MESSAGE} | ${false} + ${"invalid prefix labels"} | ${"1invalid-prefix/value"} | ${META_PREFIX_RULES_MESSAGE} | ${false} + `( + "should display an error for $description", + ({ value, message, shouldDisableValue }) => { + const onMetadataChange = jest.fn(); + renderMetadataTab({ onMetadataChange }); + + const addButton = screen.getByRole("button", { name: /add entry/i }); + fireEvent.click(addButton); + + const keyInput = screen.getByPlaceholderText("Key"); + fireEvent.change(keyInput, { target: { value } }); + + const valueInput = screen.getByPlaceholderText("Value"); + if (shouldDisableValue) { + expect(valueInput).toBeDisabled(); + } + + expect(screen.getByText(message)).toBeInTheDocument(); + expect(onMetadataChange).toHaveBeenLastCalledWith({}); + }, + ); + }); + describe("Data Validation and Trimming", () => { it("should trim whitespace from keys and values", () => { const onMetadataChange = jest.fn(); @@ -413,7 +450,7 @@ describe("MetadataTab", () => { }); describe("Edge Cases", () => { - it("should handle special characters in keys and values", () => { + it("should flag invalid names that contain unsupported characters", () => { const onMetadataChange = jest.fn(); renderMetadataTab({ onMetadataChange }); @@ -430,12 +467,11 @@ describe("MetadataTab", () => { target: { value: "value with spaces & symbols $%^" }, }); - expect(onMetadataChange).toHaveBeenCalledWith({ - "key-with-special@chars!": "value with spaces & symbols $%^", - }); + expect(screen.getByText(META_NAME_RULES_MESSAGE)).toBeInTheDocument(); + expect(onMetadataChange).toHaveBeenLastCalledWith({}); }); - it("should handle unicode characters", () => { + it("should reject unicode names that do not start with an alphanumeric character", () => { const onMetadataChange = jest.fn(); renderMetadataTab({ onMetadataChange }); @@ -448,9 +484,8 @@ describe("MetadataTab", () => { fireEvent.change(keyInput, { target: { value: "🔑_key" } }); fireEvent.change(valueInput, { target: { value: "值_value_🎯" } }); - expect(onMetadataChange).toHaveBeenCalledWith({ - "🔑_key": "值_value_🎯", - }); + expect(screen.getByText(META_NAME_RULES_MESSAGE)).toBeInTheDocument(); + expect(onMetadataChange).toHaveBeenLastCalledWith({}); }); it("should handle very long keys and values", () => { diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 3e07d017..5e196ed3 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -6,6 +6,11 @@ import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { Tabs } from "@/components/ui/tabs"; import { cacheToolOutputSchemas } from "@/utils/schemaUtils"; import { within } from "@testing-library/react"; +import { + META_NAME_RULES_MESSAGE, + META_PREFIX_RULES_MESSAGE, + RESERVED_NAMESPACE_MESSAGE, +} from "@/utils/metaUtils"; describe("ToolsTab", () => { beforeEach(() => { @@ -815,6 +820,37 @@ describe("ToolsTab", () => { }); }); + describe("Reserved metadata keys", () => { + test.each` + description | value | message + ${"reserved metadata prefix"} | ${"modelcontextprotocol.io/flip"} | ${RESERVED_NAMESPACE_MESSAGE} + ${"reserved root without slash"} | ${"modelcontextprotocol.io"} | ${RESERVED_NAMESPACE_MESSAGE} + ${"nested modelcontextprotocol domain"} | ${"api.modelcontextprotocol.org"} | ${RESERVED_NAMESPACE_MESSAGE} + ${"nested mcp domain"} | ${"tools.mcp.com/resource"} | ${RESERVED_NAMESPACE_MESSAGE} + ${"invalid name segment"} | ${"custom/bad-"} | ${META_NAME_RULES_MESSAGE} + ${"invalid prefix label"} | ${"1invalid-prefix/value"} | ${META_PREFIX_RULES_MESSAGE} + `( + "should block execution when $description is provided", + async ({ value, message }) => { + renderToolsTab({ selectedTool: mockTools[0] }); + + const addPairButton = screen.getByRole("button", { name: /add pair/i }); + await act(async () => { + fireEvent.click(addPairButton); + }); + + const keyInput = screen.getByPlaceholderText("e.g. requestId"); + await act(async () => { + fireEvent.change(keyInput, { target: { value } }); + }); + + const runButton = screen.getByRole("button", { name: /run tool/i }); + expect(runButton).toBeDisabled(); + expect(screen.getByText(message)).toBeInTheDocument(); + }, + ); + }); + describe("ToolResults Metadata", () => { it("should display metadata information when present in toolResult", () => { const resultWithMetadata = { diff --git a/client/src/utils/metaUtils.ts b/client/src/utils/metaUtils.ts new file mode 100644 index 00000000..c19b6312 --- /dev/null +++ b/client/src/utils/metaUtils.ts @@ -0,0 +1,162 @@ +/** + * Metadata helpers aligned with the official MCP specification. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta + */ + +const META_PREFIX_LABEL_REGEX = /^[a-z](?:[a-z\d-]*[a-z\d])?$/i; +const META_NAME_REGEX = /^[a-z\d](?:[a-z\d._-]*[a-z\d])?$/i; +const RESERVED_NAMESPACE_LABELS = ["modelcontextprotocol", "mcp"]; + +export const RESERVED_NAMESPACE_MESSAGE = + 'Keys using the "modelcontextprotocol.*" or "mcp.*" namespaces are reserved by MCP and cannot be used.'; + +export const META_NAME_RULES_MESSAGE = + "Names must begin and end with an alphanumeric character and may only contain alphanumerics, hyphens (-), underscores (_), or dots (.) in between."; + +export const META_PREFIX_RULES_MESSAGE = + "Prefixes must be dot-separated labels that start with a letter and end with a letter or digit (e.g. example.domain/)."; + +/** + * Extracts the prefix portion (before the first slash) of a metadata key, if present. + * + * @param key - Raw metadata key entered by the user. + * @returns The prefix segment (without the trailing slash) or null when no prefix exists. + * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta + */ +const getPrefixSegment = (key: string): string | null => { + const trimmedKey = key.trim(); + const slashIndex = trimmedKey.indexOf("/"); + if (slashIndex === -1) { + return null; + } + return trimmedKey.slice(0, slashIndex); +}; + +/** + * Normalizes a potential prefix segment by trimming whitespace, removing schemes, + * and stripping trailing URL components so only the label portion remains. + * + * @param segment - The prefix segment extracted from the metadata key. + * @returns A normalized string suitable for label parsing, or null when empty. + * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta + */ +const normalizeSegment = (segment: string): string | null => { + if (!segment) return null; + let normalized = segment.trim().toLowerCase(); + if (!normalized) return null; + + const schemeIndex = normalized.indexOf("://"); + if (schemeIndex !== -1) { + normalized = normalized.slice(schemeIndex + 3); + } + + const stopChars = ["?", "#", ":"]; + let endIndex = normalized.length; + stopChars.forEach((char) => { + const idx = normalized.indexOf(char); + if (idx !== -1 && idx < endIndex) { + endIndex = idx; + } + }); + + return normalized.slice(0, endIndex) || null; +}; + +/** + * Splits a normalized prefix into dot-separated labels and validates each label + * against the MCP prefix rules (start with letter, end with letter/digit, interior alphanumerics or hyphens). + * + * @param segment - Normalized prefix string. + * @returns Array of labels if valid, otherwise null. + * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta + */ +const splitLabels = (segment: string): string[] | null => { + const normalized = normalizeSegment(segment); + if (!normalized) return null; + + const labels = normalized.split("."); + if ( + labels.length === 0 || + labels.some((label) => !label || !META_PREFIX_LABEL_REGEX.test(label)) + ) { + return null; + } + + return labels; +}; + +/** + * Determines whether a metadata key is within the MCP-reserved namespace. + * + * @param key - Full metadata key entered by the user. + * @returns True if the key's prefix belongs to a reserved namespace. + * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta + */ +export const isReservedMetaKey = (key: string): boolean => { + const trimmedKey = key.trim(); + if (!trimmedKey) { + return false; + } + + const candidateSegment = getPrefixSegment(trimmedKey) ?? trimmedKey; + const labels = splitLabels(candidateSegment); + if (!labels || labels.length < 2) { + return false; + } + + for (let i = 0; i < labels.length - 1; i += 1) { + const current = labels[i]; + const next = labels[i + 1]; + if ( + RESERVED_NAMESPACE_LABELS.includes(current) && + META_PREFIX_LABEL_REGEX.test(next) + ) { + return true; + } + } + + return false; +}; + +/** + * Validates the optional prefix portion of a metadata key. + * + * @param key - Full metadata key entered by the user. + * @returns True when the prefix is absent or satisfies the MCP label requirements. + * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta + */ +export const hasValidMetaPrefix = (key: string): boolean => { + const prefixSegment = getPrefixSegment(key); + if (prefixSegment === null) { + return true; + } + + return splitLabels(prefixSegment) !== null; +}; + +const extractMetaName = (key: string): string => { + const trimmedKey = key.trim(); + if (!trimmedKey) return ""; + + const slashIndex = trimmedKey.lastIndexOf("/"); + if (slashIndex === -1) { + return trimmedKey; + } + + return trimmedKey.slice(slashIndex + 1); +}; + +/** + * Validates the "name" portion of a metadata key, regardless of whether a prefix exists. + * + * @param key - Full metadata key entered by the user. + * @returns True if the name portion is valid per the MCP spec. + * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta + */ +export const hasValidMetaName = (key: string): boolean => { + const name = extractMetaName(key); + if (!name) return false; + + return META_NAME_REGEX.test(name); +};