From 59504ac336608f18570008e571977c9f245514d1 Mon Sep 17 00:00:00 2001 From: Alejandro Borbolla Date: Thu, 31 Jul 2025 00:31:34 +0200 Subject: [PATCH 01/13] refactor: Add `useCopy()` The `useCopy()` hook is pretty simple, as it handle a state variable (`copied`) and put it back to false after a `timeout` --- client/src/lib/hooks/useCopy.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 client/src/lib/hooks/useCopy.ts diff --git a/client/src/lib/hooks/useCopy.ts b/client/src/lib/hooks/useCopy.ts new file mode 100644 index 00000000..8dd7e401 --- /dev/null +++ b/client/src/lib/hooks/useCopy.ts @@ -0,0 +1,29 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type UseCopyProps = { + timeout?: number; +}; + +function useCopy({ timeout = 500 }: UseCopyProps = {}) { + const [copied, setCopied] = useState(false); + + useEffect(() => { + let timeoutId: NodeJS.Timeout; + if (copied) { + timeoutId = setTimeout(() => { + setCopied(false); + }, timeout); + } + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [copied]); + + return { copied, setCopied }; +} + +export default useCopy; From 8589faa4c4b6c201c6ece7bcc563f9cccee49a5f Mon Sep 17 00:00:00 2001 From: Alejandro Borbolla Date: Thu, 31 Jul 2025 00:32:39 +0200 Subject: [PATCH 02/13] refactor: Use `useCopy()` in `JsonView` --- client/src/components/JsonView.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index e9ef0d2d..7cb38667 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -5,6 +5,7 @@ import { Copy, CheckCheck } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/lib/hooks/useToast"; import { getDataType, tryParseJson } from "@/utils/jsonUtils"; +import useCopy from "@/lib/hooks/useCopy"; interface JsonViewProps { data: unknown; @@ -25,21 +26,7 @@ const JsonView = memo( isError = false, }: JsonViewProps) => { const { toast } = useToast(); - const [copied, setCopied] = useState(false); - - useEffect(() => { - let timeoutId: NodeJS.Timeout; - if (copied) { - timeoutId = setTimeout(() => { - setCopied(false); - }, 500); - } - return () => { - if (timeoutId) { - clearTimeout(timeoutId); - } - }; - }, [copied]); + const { copied, setCopied } = useCopy(); const normalizedData = useMemo(() => { return typeof data === "string" From 5bbd9f736dd38916f812e7eac96e92f720e93561 Mon Sep 17 00:00:00 2001 From: Alejandro Borbolla Date: Thu, 31 Jul 2025 00:32:56 +0200 Subject: [PATCH 03/13] feat: Copy the input parameters of tools --- client/src/components/ToolsTab.tsx | 84 +++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 2eb682e5..04bd0465 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -13,11 +13,20 @@ import { ListToolsResult, Tool, } from "@modelcontextprotocol/sdk/types.js"; -import { Loader2, Send, ChevronDown, ChevronUp } from "lucide-react"; +import { + Loader2, + Send, + ChevronDown, + ChevronUp, + Copy, + CheckCheck, +} from "lucide-react"; import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import JsonView from "./JsonView"; import ToolResults from "./ToolResults"; +import { useToast } from "@/lib/hooks/useToast"; +import useCopy from "@/lib/hooks/useCopy"; // Type guard to safely detect the optional _meta field without using `any` const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } => @@ -51,6 +60,8 @@ const ToolsTab = ({ const [isToolRunning, setIsToolRunning] = useState(false); const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false); const [isMetaExpanded, setIsMetaExpanded] = useState(false); + const { toast } = useToast(); + const { copied, setCopied } = useCopy(); useEffect(() => { const params = Object.entries( @@ -284,29 +295,54 @@ const ToolsTab = ({ )} - +
+ + +
Date: Mon, 1 Sep 2025 18:31:52 +0200 Subject: [PATCH 04/13] fix: Assess PR comments (typo) --- client/src/components/ToolsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index acabd733..377521ae 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -336,7 +336,7 @@ const ToolsTab = ({ } catch (error) { toast({ title: "Error", - description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`, + description: `There was an error copying input to the clipboard: ${error instanceof Error ? error.message : String(error)}`, variant: "destructive", }); } From 2fd781ab8187fe36a16c36639860b5dd7ad26735 Mon Sep 17 00:00:00 2001 From: Alejandro Borbolla Date: Mon, 1 Sep 2025 18:32:16 +0200 Subject: [PATCH 05/13] fix: CI by removing unused `useEffect` --- client/src/components/JsonView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 7cb38667..1febff6a 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -1,4 +1,4 @@ -import { useState, memo, useMemo, useCallback, useEffect } from "react"; +import { useState, memo, useMemo, useCallback } from "react"; import type { JsonValue } from "@/utils/jsonUtils"; import clsx from "clsx"; import { Copy, CheckCheck } from "lucide-react"; From c5acc6312921fdd9009796113488400c2ac1d14c Mon Sep 17 00:00:00 2001 From: "jeetul.samaiya" Date: Tue, 26 Aug 2025 18:51:06 +0530 Subject: [PATCH 06/13] Add support for custom headers and migrate from legacy auth - Introduced CustomHeaders component for managing custom headers. - Updated Sidebar to include CustomHeaders component. - Enhanced useConnection hook to handle custom headers and legacy auth migration. - Added tests for custom headers functionality. - Updated package.json and package-lock.json for new dependencies. --- client/package.json | 2 + client/src/App.tsx | 43 ++++ client/src/components/CustomHeaders.tsx | 215 ++++++++++++++++++ client/src/components/Sidebar.tsx | 54 ++--- .../src/components/__tests__/Sidebar.test.tsx | 4 + client/src/components/ui/switch.tsx | 27 +++ .../hooks/__tests__/useConnection.test.tsx | 130 +++++++++++ client/src/lib/hooks/useConnection.ts | 58 ++++- client/src/lib/types/customHeaders.ts | 59 +++++ package-lock.json | 40 ++++ server/package.json | 3 + server/src/index.ts | 17 ++ 12 files changed, 605 insertions(+), 47 deletions(-) create mode 100644 client/src/components/CustomHeaders.tsx create mode 100644 client/src/components/ui/switch.tsx create mode 100644 client/src/lib/types/customHeaders.ts diff --git a/client/package.json b/client/package.json index 61f65b62..6b84b106 100644 --- a/client/package.json +++ b/client/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-popover": "^1.1.3", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-tooltip": "^1.1.8", @@ -69,6 +70,7 @@ "globals": "^15.9.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-fixed-jsdom": "^0.0.9", "postcss": "^8.5.6", "tailwindcss": "^3.4.13", "tailwindcss-animate": "^1.0.7", diff --git a/client/src/App.tsx b/client/src/App.tsx index 60ce90c5..ff44759e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -74,6 +74,7 @@ import ElicitationTab, { PendingElicitationRequest, ElicitationResponse, } from "./components/ElicitationTab"; +import { CustomHeaders, migrateFromLegacyAuth } from "./lib/types/customHeaders"; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; @@ -127,6 +128,28 @@ const App = () => { return localStorage.getItem("lastOauthScope") || ""; }); + // Custom headers state with migration from legacy auth + const [customHeaders, setCustomHeaders] = useState(() => { + const savedHeaders = localStorage.getItem("lastCustomHeaders"); + if (savedHeaders) { + try { + return JSON.parse(savedHeaders); + } catch { + // Fall back to migration if JSON parsing fails + } + } + + // Migrate from legacy auth if available + const legacyToken = localStorage.getItem("lastBearerToken") || ""; + const legacyHeaderName = localStorage.getItem("lastHeaderName") || ""; + + if (legacyToken) { + return migrateFromLegacyAuth(legacyToken, legacyHeaderName); + } + + return []; + }); + const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< PendingRequest & { @@ -215,6 +238,7 @@ const App = () => { env, bearerToken, headerName, + customHeaders, oauthClientId, oauthScope, config, @@ -310,6 +334,23 @@ const App = () => { localStorage.setItem("lastHeaderName", headerName); }, [headerName]); + useEffect(() => { + localStorage.setItem("lastCustomHeaders", JSON.stringify(customHeaders)); + }, [customHeaders]); + + // Auto-migrate from legacy auth when custom headers are empty but legacy auth exists + useEffect(() => { + if (customHeaders.length === 0 && (bearerToken || headerName)) { + const migratedHeaders = migrateFromLegacyAuth(bearerToken, headerName); + if (migratedHeaders.length > 0) { + setCustomHeaders(migratedHeaders); + // Clear legacy auth after migration + setBearerToken(""); + setHeaderName(""); + } + } + }, [bearerToken, headerName, customHeaders, setCustomHeaders]); + useEffect(() => { localStorage.setItem("lastOauthClientId", oauthClientId); }, [oauthClientId]); @@ -814,6 +855,8 @@ const App = () => { setBearerToken={setBearerToken} headerName={headerName} setHeaderName={setHeaderName} + customHeaders={customHeaders} + setCustomHeaders={setCustomHeaders} oauthClientId={oauthClientId} setOauthClientId={setOauthClientId} oauthScope={oauthScope} diff --git a/client/src/components/CustomHeaders.tsx b/client/src/components/CustomHeaders.tsx new file mode 100644 index 00000000..9196480e --- /dev/null +++ b/client/src/components/CustomHeaders.tsx @@ -0,0 +1,215 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { Plus, Trash2, Eye, EyeOff } from "lucide-react"; +import { CustomHeaders as CustomHeadersType, CustomHeader, createEmptyHeader } from "@/lib/types/customHeaders"; + +interface CustomHeadersProps { + headers: CustomHeadersType; + onChange: (headers: CustomHeadersType) => void; + className?: string; +} + +const CustomHeaders = ({ headers, onChange, className }: CustomHeadersProps) => { + const [isJsonMode, setIsJsonMode] = useState(false); + const [jsonValue, setJsonValue] = useState(""); + const [jsonError, setJsonError] = useState(null); + const [visibleValues, setVisibleValues] = useState>(new Set()); + + const updateHeader = (index: number, field: keyof CustomHeader, value: string | boolean) => { + const newHeaders = [...headers]; + newHeaders[index] = { ...newHeaders[index], [field]: value }; + onChange(newHeaders); + }; + + const addHeader = () => { + onChange([...headers, createEmptyHeader()]); + }; + + const removeHeader = (index: number) => { + const newHeaders = headers.filter((_, i) => i !== index); + onChange(newHeaders); + }; + + const toggleValueVisibility = (index: number) => { + const newVisible = new Set(visibleValues); + if (newVisible.has(index)) { + newVisible.delete(index); + } else { + newVisible.add(index); + } + setVisibleValues(newVisible); + }; + + const switchToJsonMode = () => { + const jsonObject: Record = {}; + headers.forEach((header) => { + if (header.enabled && header.name.trim() && header.value.trim()) { + jsonObject[header.name.trim()] = header.value.trim(); + } + }); + setJsonValue(JSON.stringify(jsonObject, null, 2)); + setJsonError(null); + setIsJsonMode(true); + }; + + const switchToFormMode = () => { + try { + const parsed = JSON.parse(jsonValue); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + setJsonError("JSON must be an object with string key-value pairs"); + return; + } + + const newHeaders: CustomHeadersType = Object.entries(parsed).map(([name, value]) => ({ + name, + value: String(value), + enabled: true, + })); + + onChange(newHeaders); + setJsonError(null); + setIsJsonMode(false); + } catch (error) { + setJsonError("Invalid JSON format"); + } + }; + + const handleJsonChange = (value: string) => { + setJsonValue(value); + setJsonError(null); + }; + + if (isJsonMode) { + return ( +
+
+

Custom Headers (JSON)

+ +
+
+