- 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:
sumeetpardeshi
2025-05-05 17:32:06 -07:00
parent e8e2dd0618
commit be7fa9baf9
4 changed files with 294 additions and 52 deletions

View File

@@ -42,6 +42,74 @@ CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build
For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging). For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging).
### Configuration Export
The MCP Inspector provides convenient buttons to export your server configuration:
- **Server Entry** - Copies a single server configuration entry to your clipboard. This can be added to your `mcp.json` file inside the `mcpServers` object with your preferred server name.
**STDIO transport example:**
```json
{
"command": "node",
"args": ["build/index.js", "--debug"],
"env": {
"API_KEY": "your-api-key",
"DEBUG": "true"
}
}
```
**SSE transport example:**
```json
{
"type": "sse",
"url": "http://localhost:3000/events",
"note": "For SSE connections, add this URL directly in Client"
}
```
- **Servers File** - Copies a complete MCP configuration file structure to your clipboard, with your current server configuration added as `default-server`. This can be saved directly as `mcp.json`.
**STDIO transport example:**
```json
{
"mcpServers": {
"default-server": {
"command": "node",
"args": ["build/index.js", "--debug"],
"env": {
"API_KEY": "your-api-key",
"DEBUG": "true"
}
}
}
}
```
**SSE transport example:**
```json
{
"mcpServers": {
"default-server": {
"type": "sse",
"url": "http://localhost:3000/events",
"note": "For SSE connections, add this URL directly in Client"
}
}
}
```
These buttons appear in the Inspector UI after you've configured your server settings, making it easy to save and reuse your configurations.
For SSE transport connections, the Inspector provides similar functionality for both buttons. The "Server Entry" button copies the SSE URL configuration that can be added to your existing configuration file, while the "Servers File" button creates a complete configuration file containing the SSE URL for direct use in clients.
You can paste the Server Entry into your existing `mcp.json` file under your chosen server name, or use the complete Servers File payload to create a new configuration file.
### Authentication ### Authentication
The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. You can override the header name using the input field in the sidebar. The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. You can override the header name using the input field in the sidebar.
@@ -93,6 +161,8 @@ Example server configuration file:
} }
``` ```
> **Tip:** You can easily generate this configuration format using the **Server Entry** and **Servers File** buttons in the Inspector UI, as described in the Configuration Export section above.
### From this repository ### From this repository
If you're working on the inspector itself: If you're working on the inspector itself:

View File

@@ -98,8 +98,8 @@ const Sidebar = ({
const [showBearerToken, setShowBearerToken] = useState(false); const [showBearerToken, setShowBearerToken] = useState(false);
const [showConfig, setShowConfig] = useState(false); const [showConfig, setShowConfig] = useState(false);
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set()); const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
const [copiedConfigEntry, setCopiedConfigEntry] = useState(false); const [copiedServerEntry, setCopiedServerEntry] = useState(false);
const [copiedConfigFile, setCopiedConfigFile] = useState(false); const [copiedServerFile, setCopiedServerFile] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
// Shared utility function to generate server config // Shared utility function to generate server config
@@ -108,53 +108,63 @@ const Sidebar = ({
return { return {
command, command,
args: args.trim() ? args.split(/\s+/) : [], args: args.trim() ? args.split(/\s+/) : [],
env: { ...env } env: { ...env },
}; };
} else { } else {
return { return {
type: "sse", type: "sse",
url: sseUrl, 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]); }, [transportType, command, args, env, sseUrl]);
// Memoized config entry generator // Memoized config entry generator
const generateMCPConfigEntry = useCallback(() => { const generateMCPServerEntry = useCallback(() => {
return JSON.stringify(generateServerConfig(), null, 2); return JSON.stringify(generateServerConfig(), null, 2);
}, [generateServerConfig]); }, [generateServerConfig]);
// Memoized config file generator // Memoized config file generator
const generateMCPConfigFile = useCallback(() => { const generateMCPServerFile = useCallback(() => {
return JSON.stringify( return JSON.stringify(
{ {
mcpServers: { mcpServers: {
"default-server": generateServerConfig() "default-server": generateServerConfig(),
} },
}, },
null, null,
2 2,
); );
}, [generateServerConfig]); }, [generateServerConfig]);
// Memoized copy handlers // Memoized copy handlers
const handleCopyConfigEntry = useCallback(() => { const handleCopyServerEntry = useCallback(() => {
try { try {
const configJson = generateMCPConfigEntry(); const configJson = generateMCPServerEntry();
navigator.clipboard.writeText(configJson); navigator.clipboard
setCopiedConfigEntry(true); .writeText(configJson)
.then(() => {
setCopiedServerEntry(true);
toast({ toast({
title: "Config entry copied", title: "Config entry copied",
description: description:
transportType === "stdio" transportType === "stdio"
? "Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name." ? "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.", : "SSE URL has been copied. Use this URL in Cursor directly.",
}); });
setTimeout(() => { setTimeout(() => {
setCopiedConfigEntry(false); setCopiedServerEntry(false);
}, 2000); }, 2000);
})
.catch((error) => {
toast({
title: "Error",
description: `Failed to copy config: ${error instanceof Error ? error.message : String(error)}`,
variant: "destructive",
});
});
} catch (error) { } catch (error) {
toast({ toast({
title: "Error", title: "Error",
@@ -162,22 +172,33 @@ const Sidebar = ({
variant: "destructive", variant: "destructive",
}); });
} }
}, [generateMCPConfigEntry, transportType, toast]); }, [generateMCPServerEntry, transportType, toast]);
const handleCopyConfigFile = useCallback(() => { const handleCopyServerFile = useCallback(() => {
try { try {
const configJson = generateMCPConfigFile(); const configJson = generateMCPServerFile();
navigator.clipboard.writeText(configJson); navigator.clipboard
setCopiedConfigFile(true); .writeText(configJson)
.then(() => {
setCopiedServerFile(true);
toast({ toast({
title: "Config file copied", title: "Servers 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'", 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(() => { setTimeout(() => {
setCopiedConfigFile(false); setCopiedServerFile(false);
}, 2000); }, 2000);
})
.catch((error) => {
toast({
title: "Error",
description: `Failed to copy config: ${error instanceof Error ? error.message : String(error)}`,
variant: "destructive",
});
});
} catch (error) { } catch (error) {
toast({ toast({
title: "Error", title: "Error",
@@ -185,7 +206,7 @@ const Sidebar = ({
variant: "destructive", variant: "destructive",
}); });
} }
}, [generateMCPConfigFile, toast]); }, [generateMCPServerFile, toast]);
return ( return (
<div className="w-80 bg-card border-r border-border flex flex-col h-full"> <div className="w-80 bg-card border-r border-border flex flex-col h-full">
@@ -252,46 +273,41 @@ const Sidebar = ({
/> />
</div> </div>
<div className="grid grid-cols-2 gap-2 mt-2"> <div className="grid grid-cols-2 gap-2 mt-2">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleCopyConfigEntry} onClick={handleCopyServerEntry}
className="w-full" className="w-full"
> >
{copiedConfigEntry ? ( {copiedServerEntry ? (
<CheckCheck className="h-4 w-4 mr-2" /> <CheckCheck className="h-4 w-4 mr-2" />
) : ( ) : (
<Copy className="h-4 w-4 mr-2" /> <Copy className="h-4 w-4 mr-2" />
)} )}
Config Entry Server Entry
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>Copy Server Entry</TooltipContent>
Copy Config Entry
</TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleCopyConfigFile} onClick={handleCopyServerFile}
className="w-full" className="w-full"
> >
{copiedConfigFile ? ( {copiedServerFile ? (
<CheckCheck className="h-4 w-4 mr-2" /> <CheckCheck className="h-4 w-4 mr-2" />
) : ( ) : (
<Copy className="h-4 w-4 mr-2" /> <Copy className="h-4 w-4 mr-2" />
)} )}
Config File Servers File
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>Copy Servers File</TooltipContent>
Copy Config File
</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
</> </>
@@ -313,16 +329,16 @@ const Sidebar = ({
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleCopyConfigFile} onClick={handleCopyServerFile}
className="w-full" className="w-full"
title="Copy SSE URL Configuration" title="Copy SSE URL Configuration"
> >
{copiedConfigFile ? ( {copiedServerFile ? (
<CheckCheck className="h-4 w-4 mr-2" /> <CheckCheck className="h-4 w-4 mr-2" />
) : ( ) : (
<Copy className="h-4 w-4 mr-2" /> <Copy className="h-4 w-4 mr-2" />
)} )}
Copy URL Copy Servers File
</Button> </Button>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -12,6 +12,22 @@ jest.mock("../../lib/useTheme", () => ({
default: () => ["light", jest.fn()], 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", () => { describe("Sidebar Environment Variables", () => {
const defaultProps = { const defaultProps = {
connectionStatus: "disconnected" as const, 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);
});
});
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 402 KiB