Skip to content

feat: Add copy json config button #334

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 151 additions & 1 deletion client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useCallback } from "react";
import {
Play,
ChevronDown,
Expand All @@ -12,6 +12,8 @@ import {
Settings,
HelpCircle,
RefreshCwOff,
Copy,
CheckCheck,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
Expand All @@ -36,6 +38,7 @@ import {
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useToast } from "@/hooks/use-toast";

interface SidebarProps {
connectionStatus: ConnectionStatus;
Expand Down Expand Up @@ -95,6 +98,94 @@ const Sidebar = ({
const [showBearerToken, setShowBearerToken] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
const [copiedConfigEntry, setCopiedConfigEntry] = useState(false);
const [copiedConfigFile, setCopiedConfigFile] = useState(false);
const { toast } = useToast();

// Shared utility function to generate server config
const generateServerConfig = useCallback(() => {
if (transportType === "stdio") {
return {
command,
args: args.trim() ? args.split(/\s+/) : [],
env: { ...env }
};
} else {
return {
type: "sse",
url: sseUrl,
note: "For SSE connections, add this URL directly in Client"
};
}
}, [transportType, command, args, env, sseUrl]);

// Memoized config entry generator
const generateMCPConfigEntry = useCallback(() => {
return JSON.stringify(generateServerConfig(), null, 2);
}, [generateServerConfig]);

// Memoized config file generator
const generateMCPConfigFile = useCallback(() => {
return JSON.stringify(
{
mcpServers: {
"default-server": generateServerConfig()
}
},
null,
2
);
}, [generateServerConfig]);

// Memoized copy handlers
const handleCopyConfigEntry = useCallback(() => {
try {
const configJson = generateMCPConfigEntry();
navigator.clipboard.writeText(configJson);
setCopiedConfigEntry(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."
: "SSE URL has been copied. Use this URL in Cursor directly.",
});

setTimeout(() => {
setCopiedConfigEntry(false);
}, 2000);
} catch (error) {
toast({
title: "Error",
description: `Failed to copy config: ${error instanceof Error ? error.message : String(error)}`,
variant: "destructive",
});
}
}, [generateMCPConfigEntry, transportType, toast]);

const handleCopyConfigFile = useCallback(() => {
try {
const configJson = generateMCPConfigFile();
navigator.clipboard.writeText(configJson);
setCopiedConfigFile(true);

toast({
title: "Config file copied",
description: "Server configuration has been copied to clipboard. Add this to your mcp.json file. Current testing server will be added as 'default-server'",
});

setTimeout(() => {
setCopiedConfigFile(false);
}, 2000);
} catch (error) {
toast({
title: "Error",
description: `Failed to copy config: ${error instanceof Error ? error.message : String(error)}`,
variant: "destructive",
});
}
}, [generateMCPConfigFile, toast]);

return (
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
Expand Down Expand Up @@ -160,6 +251,49 @@ const Sidebar = ({
className="font-mono"
/>
</div>
<div className="grid grid-cols-2 gap-2 mt-2">

<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={handleCopyConfigEntry}
className="w-full"
>
{copiedConfigEntry ? (
<CheckCheck className="h-4 w-4 mr-2" />
) : (
<Copy className="h-4 w-4 mr-2" />
)}
Config Entry
</Button>
</TooltipTrigger>
<TooltipContent>
Copy Config Entry
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={handleCopyConfigFile}
className="w-full"
>
{copiedConfigFile ? (
<CheckCheck className="h-4 w-4 mr-2" />
) : (
<Copy className="h-4 w-4 mr-2" />
)}
Config File
</Button>
</TooltipTrigger>
<TooltipContent>
Copy Config File
</TooltipContent>
</Tooltip>
</div>
</>
) : (
<>
Expand All @@ -175,6 +309,22 @@ const Sidebar = ({
className="font-mono"
/>
</div>
<div className="w-full mt-2">
<Button
variant="outline"
size="sm"
onClick={handleCopyConfigFile}
className="w-full"
title="Copy SSE URL Configuration"
>
{copiedConfigFile ? (
<CheckCheck className="h-4 w-4 mr-2" />
) : (
<Copy className="h-4 w-4 mr-2" />
)}
Copy URL
</Button>
</div>
<div className="space-y-2">
<Button
variant="outline"
Expand Down