- 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).
### 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
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
If you're working on the inspector itself:

View File

@@ -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">

View File

@@ -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);
});
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 402 KiB