changes:
- change button names - adding unit test cases - updated landing page image - updating README file as discussed here: https://github.com/modelcontextprotocol/inspector/pull/334#issuecomment-2852394054
This commit is contained in:
@@ -98,8 +98,8 @@ 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 [copiedServerEntry, setCopiedServerEntry] = useState(false);
|
||||
const [copiedServerFile, setCopiedServerFile] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Shared utility function to generate server config
|
||||
@@ -108,53 +108,63 @@ const Sidebar = ({
|
||||
return {
|
||||
command,
|
||||
args: args.trim() ? args.split(/\s+/) : [],
|
||||
env: { ...env }
|
||||
env: { ...env },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "sse",
|
||||
url: sseUrl,
|
||||
note: "For SSE connections, add this URL directly in Client"
|
||||
note: "For SSE connections, add this URL directly in Client",
|
||||
};
|
||||
}
|
||||
}, [transportType, command, args, env, sseUrl]);
|
||||
|
||||
// Memoized config entry generator
|
||||
const generateMCPConfigEntry = useCallback(() => {
|
||||
const generateMCPServerEntry = useCallback(() => {
|
||||
return JSON.stringify(generateServerConfig(), null, 2);
|
||||
}, [generateServerConfig]);
|
||||
|
||||
// Memoized config file generator
|
||||
const generateMCPConfigFile = useCallback(() => {
|
||||
const generateMCPServerFile = useCallback(() => {
|
||||
return JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
"default-server": generateServerConfig()
|
||||
}
|
||||
"default-server": generateServerConfig(),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
2,
|
||||
);
|
||||
}, [generateServerConfig]);
|
||||
|
||||
// Memoized copy handlers
|
||||
const handleCopyConfigEntry = useCallback(() => {
|
||||
const handleCopyServerEntry = useCallback(() => {
|
||||
try {
|
||||
const configJson = generateMCPConfigEntry();
|
||||
navigator.clipboard.writeText(configJson);
|
||||
setCopiedConfigEntry(true);
|
||||
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."
|
||||
: "SSE URL has been copied. Use this URL in Cursor directly.",
|
||||
});
|
||||
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);
|
||||
setTimeout(() => {
|
||||
setCopiedServerEntry(false);
|
||||
}, 2000);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to copy config: ${error instanceof Error ? error.message : String(error)}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
@@ -162,22 +172,33 @@ const Sidebar = ({
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}, [generateMCPConfigEntry, transportType, toast]);
|
||||
}, [generateMCPServerEntry, transportType, toast]);
|
||||
|
||||
const handleCopyConfigFile = useCallback(() => {
|
||||
const handleCopyServerFile = useCallback(() => {
|
||||
try {
|
||||
const configJson = generateMCPConfigFile();
|
||||
navigator.clipboard.writeText(configJson);
|
||||
setCopiedConfigFile(true);
|
||||
const configJson = generateMCPServerFile();
|
||||
navigator.clipboard
|
||||
.writeText(configJson)
|
||||
.then(() => {
|
||||
setCopiedServerFile(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'",
|
||||
});
|
||||
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(() => {
|
||||
setCopiedConfigFile(false);
|
||||
}, 2000);
|
||||
setTimeout(() => {
|
||||
setCopiedServerFile(false);
|
||||
}, 2000);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to copy config: ${error instanceof Error ? error.message : String(error)}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
@@ -185,7 +206,7 @@ const Sidebar = ({
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}, [generateMCPConfigFile, toast]);
|
||||
}, [generateMCPServerFile, toast]);
|
||||
|
||||
return (
|
||||
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
|
||||
@@ -252,46 +273,41 @@ const Sidebar = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyConfigEntry}
|
||||
onClick={handleCopyServerEntry}
|
||||
className="w-full"
|
||||
>
|
||||
{copiedConfigEntry ? (
|
||||
{copiedServerEntry ? (
|
||||
<CheckCheck className="h-4 w-4 mr-2" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Config Entry
|
||||
Server Entry
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Copy Config Entry
|
||||
</TooltipContent>
|
||||
<TooltipContent>Copy Server Entry</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyConfigFile}
|
||||
onClick={handleCopyServerFile}
|
||||
className="w-full"
|
||||
>
|
||||
{copiedConfigFile ? (
|
||||
{copiedServerFile ? (
|
||||
<CheckCheck className="h-4 w-4 mr-2" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Config File
|
||||
Servers File
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Copy Config File
|
||||
</TooltipContent>
|
||||
<TooltipContent>Copy Servers File</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
@@ -313,16 +329,16 @@ const Sidebar = ({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyConfigFile}
|
||||
onClick={handleCopyServerFile}
|
||||
className="w-full"
|
||||
title="Copy SSE URL Configuration"
|
||||
>
|
||||
{copiedConfigFile ? (
|
||||
{copiedServerFile ? (
|
||||
<CheckCheck className="h-4 w-4 mr-2" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Copy URL
|
||||
Copy Servers File
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -12,6 +12,22 @@ jest.mock("../../lib/useTheme", () => ({
|
||||
default: () => ["light", jest.fn()],
|
||||
}));
|
||||
|
||||
// Mock toast hook
|
||||
const mockToast = jest.fn();
|
||||
jest.mock("@/hooks/use-toast", () => ({
|
||||
useToast: () => ({
|
||||
toast: mockToast,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock navigator clipboard
|
||||
const mockClipboardWrite = jest.fn(() => Promise.resolve());
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: {
|
||||
writeText: mockClipboardWrite,
|
||||
},
|
||||
});
|
||||
|
||||
describe("Sidebar Environment Variables", () => {
|
||||
const defaultProps = {
|
||||
connectionStatus: "disconnected" as const,
|
||||
@@ -622,4 +638,144 @@ describe("Sidebar Environment Variables", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Copy Configuration Features", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should copy server entry configuration to clipboard for STDIO transport", () => {
|
||||
const command = "node";
|
||||
const args = "--inspect server.js";
|
||||
const env = { API_KEY: "test-key", DEBUG: "true" };
|
||||
|
||||
renderSidebar({
|
||||
transportType: "stdio",
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
});
|
||||
|
||||
const copyServerEntryButton = screen.getByRole("button", {
|
||||
name: /server entry/i,
|
||||
});
|
||||
fireEvent.click(copyServerEntryButton);
|
||||
|
||||
// Check clipboard API was called with the correct configuration
|
||||
expect(mockClipboardWrite).toHaveBeenCalledTimes(1);
|
||||
|
||||
const expectedConfig = JSON.stringify(
|
||||
{
|
||||
command,
|
||||
args: ["--inspect", "server.js"],
|
||||
env,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);
|
||||
});
|
||||
|
||||
it("should copy servers file configuration to clipboard for STDIO transport", () => {
|
||||
const command = "node";
|
||||
const args = "--inspect server.js";
|
||||
const env = { API_KEY: "test-key", DEBUG: "true" };
|
||||
|
||||
renderSidebar({
|
||||
transportType: "stdio",
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
});
|
||||
|
||||
const copyServersFileButton = screen.getByRole("button", {
|
||||
name: /servers file/i,
|
||||
});
|
||||
fireEvent.click(copyServersFileButton);
|
||||
|
||||
// Check clipboard API was called with the correct configuration
|
||||
expect(mockClipboardWrite).toHaveBeenCalledTimes(1);
|
||||
|
||||
const expectedConfig = JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
"default-server": {
|
||||
command,
|
||||
args: ["--inspect", "server.js"],
|
||||
env,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);
|
||||
});
|
||||
|
||||
it("should copy servers file configuration to clipboard for SSE transport", () => {
|
||||
const sseUrl = "http://localhost:3000/events";
|
||||
|
||||
renderSidebar({
|
||||
transportType: "sse",
|
||||
sseUrl,
|
||||
});
|
||||
|
||||
const copyServersFileButton = screen.getByRole("button", {
|
||||
name: /servers file/i,
|
||||
});
|
||||
fireEvent.click(copyServersFileButton);
|
||||
|
||||
// Check clipboard API was called with the correct configuration
|
||||
expect(mockClipboardWrite).toHaveBeenCalledTimes(1);
|
||||
|
||||
const expectedConfig = JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
"default-server": {
|
||||
type: "sse",
|
||||
url: sseUrl,
|
||||
note: "For SSE connections, add this URL directly in Client",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);
|
||||
});
|
||||
|
||||
it("should handle empty args in STDIO transport", () => {
|
||||
const command = "python";
|
||||
const args = "";
|
||||
|
||||
renderSidebar({
|
||||
transportType: "stdio",
|
||||
command,
|
||||
args,
|
||||
});
|
||||
|
||||
const copyServerEntryButton = screen.getByRole("button", {
|
||||
name: /server entry/i,
|
||||
});
|
||||
fireEvent.click(copyServerEntryButton);
|
||||
|
||||
// Check clipboard API was called with empty args array
|
||||
expect(mockClipboardWrite).toHaveBeenCalledTimes(1);
|
||||
|
||||
const expectedConfig = JSON.stringify(
|
||||
{
|
||||
command,
|
||||
args: [],
|
||||
env: {},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user