diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index ecd150f22..4c3718394 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -27,7 +27,11 @@ interface DynamicJsonFormProps { } export interface DynamicJsonFormRef { - validateJson: () => { isValid: boolean; error: string | null }; + validateJson: () => { + isValid: boolean; + error: string | null; + value: JsonValue; + }; hasJsonError: () => boolean; } @@ -211,10 +215,10 @@ const DynamicJsonForm = forwardRef( }; const validateJson = () => { - if (!isJsonMode) return { isValid: true, error: null }; + if (!isJsonMode) return { isValid: true, error: null, value }; try { const jsonStr = rawJsonValue?.trim(); - if (!jsonStr) return { isValid: true, error: null }; + if (!jsonStr) return { isValid: true, error: null, value }; const parsed = JSON.parse(jsonStr); // Clear any pending debounced update and immediately update parent if (timeoutRef.current) { @@ -222,12 +226,12 @@ const DynamicJsonForm = forwardRef( } onChange(parsed); setJsonError(undefined); - return { isValid: true, error: null }; + return { isValid: true, error: null, value: parsed }; } catch (err) { const errorMessage = err instanceof Error ? err.message : "Invalid JSON"; setJsonError(errorMessage); - return { isValid: false, error: errorMessage }; + return { isValid: false, error: errorMessage, value }; } }; diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 13a4f24f4..da77b4049 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -133,6 +133,39 @@ const Sidebar = ({ [toast], ); + const fallbackCopyText = useCallback((text: string) => { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.setAttribute("readonly", ""); + textarea.style.position = "fixed"; + textarea.style.left = "-9999px"; + document.body.appendChild(textarea); + textarea.select(); + + const copied = document.execCommand("copy"); + document.body.removeChild(textarea); + + if (!copied) { + throw new Error("Clipboard copy fallback failed"); + } + }, []); + + const copyTextToClipboard = useCallback( + async (text: string) => { + if (navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(text); + return; + } catch { + // Fallback to execCommand in non-secure contexts or restricted permissions. + } + } + + fallbackCopyText(text); + }, + [fallbackCopyText], + ); + // Shared utility function to generate server config const generateServerConfig = useCallback(() => { if (transportType === "stdio") { @@ -178,61 +211,51 @@ const Sidebar = ({ }, [generateServerConfig]); // Memoized copy handlers - const handleCopyServerEntry = useCallback(() => { + const handleCopyServerEntry = useCallback(async () => { try { const configJson = generateMCPServerEntry(); - navigator.clipboard - .writeText(configJson) - .then(() => { - setCopiedServerEntry(true); - - toast({ - title: "Config entry copied", - description: - transportType === "stdio" - ? "Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name." - : transportType === "streamable-http" - ? "Streamable HTTP URL has been copied. Use this URL directly in your MCP Client." - : "SSE URL has been copied. Use this URL directly in your MCP Client.", - }); - - setTimeout(() => { - setCopiedServerEntry(false); - }, 2000); - }) - .catch((error) => { - reportError(error); - }); + await copyTextToClipboard(configJson); + + setCopiedServerEntry(true); + + toast({ + title: "Config entry copied", + description: + transportType === "stdio" + ? "Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name." + : transportType === "streamable-http" + ? "Streamable HTTP URL has been copied. Use this URL directly in your MCP Client." + : "SSE URL has been copied. Use this URL directly in your MCP Client.", + }); + + setTimeout(() => { + setCopiedServerEntry(false); + }, 2000); } catch (error) { reportError(error); } - }, [generateMCPServerEntry, transportType, toast, reportError]); + }, [generateMCPServerEntry, copyTextToClipboard, transportType, toast, reportError]); - const handleCopyServerFile = useCallback(() => { + const handleCopyServerFile = useCallback(async () => { try { const configJson = generateMCPServerFile(); - navigator.clipboard - .writeText(configJson) - .then(() => { - setCopiedServerFile(true); - - toast({ - title: "Servers file copied", - description: - "Servers configuration has been copied to clipboard. Add this to your mcp.json file. Current testing server will be added as 'default-server'", - }); - - setTimeout(() => { - setCopiedServerFile(false); - }, 2000); - }) - .catch((error) => { - reportError(error); - }); + await copyTextToClipboard(configJson); + + setCopiedServerFile(true); + + toast({ + title: "Servers file copied", + description: + "Servers configuration has been copied to clipboard. Add this to your mcp.json file. Current testing server will be added as 'default-server'", + }); + + setTimeout(() => { + setCopiedServerFile(false); + }, 2000); } catch (error) { reportError(error); } - }, [generateMCPServerFile, toast, reportError]); + }, [generateMCPServerFile, copyTextToClipboard, toast, reportError]); return (
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index febea1d8f..944fa6bb6 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -805,8 +805,20 @@ const ToolsTab = ({ )}