Merge pull request #204 from pulkitsharma07/main

Add "Configuration" support in UI for configuring the request timeout (and more things in the future)
This commit is contained in:
Cliff Hall
2025-03-29 16:36:19 -04:00
committed by GitHub
10 changed files with 237 additions and 7 deletions

View File

@@ -12,8 +12,8 @@ Thanks for your interest in contributing! This guide explains how to get involve
## Development Process & Pull Requests
1. Create a new branch for your changes
2. Make your changes following existing code style and conventions
3. Test changes locally
2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable.
3. Test changes locally by running `npm test`
4. Update documentation as needed
5. Use clear commit messages explaining your changes
6. Verify all changes work as expected

View File

@@ -46,6 +46,14 @@ The inspector supports bearer token authentication for SSE connections. Enter yo
The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server.
### Configuration
The MCP Inspector supports the following configuration settings. To change them click on the `Configuration` button in the MCP Inspector UI :
| Name | Purpose | Default Value |
| -------------------------- | ----------------------------------------------------------------------------------------- | ------------- |
| MCP_SERVER_REQUEST_TIMEOUT | Maximum time in milliseconds to wait for a response from the MCP server before timing out | 10000 |
### From this repository
If you're working on the inspector itself:

View File

@@ -45,10 +45,13 @@ import RootsTab from "./components/RootsTab";
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab";
import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants";
import { InspectorConfig } from "./lib/configurationTypes";
const params = new URLSearchParams(window.location.search);
const PROXY_PORT = params.get("proxyPort") ?? "3000";
const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`;
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
const App = () => {
// Handle OAuth callback route
@@ -89,6 +92,11 @@ const App = () => {
>([]);
const [roots, setRoots] = useState<Root[]>([]);
const [env, setEnv] = useState<Record<string, string>>({});
const [config, setConfig] = useState<InspectorConfig>(() => {
const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY);
return savedConfig ? JSON.parse(savedConfig) : DEFAULT_INSPECTOR_CONFIG;
});
const [bearerToken, setBearerToken] = useState<string>(() => {
return localStorage.getItem("lastBearerToken") || "";
});
@@ -145,6 +153,7 @@ const App = () => {
env,
bearerToken,
proxyServerUrl: PROXY_SERVER_URL,
requestTimeout: config.MCP_SERVER_REQUEST_TIMEOUT.value as number,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
},
@@ -183,6 +192,10 @@ const App = () => {
localStorage.setItem("lastBearerToken", bearerToken);
}, [bearerToken]);
useEffect(() => {
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
}, [config]);
// Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback)
useEffect(() => {
const serverUrl = params.get("serverUrl");
@@ -440,6 +453,8 @@ const App = () => {
setSseUrl={setSseUrl}
env={env}
setEnv={setEnv}
config={config}
setConfig={setConfig}
bearerToken={bearerToken}
setBearerToken={setBearerToken}
onConnect={connectMcpServer}

View File

@@ -8,6 +8,7 @@ import {
Github,
Eye,
EyeOff,
Settings,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -23,6 +24,7 @@ import {
LoggingLevel,
LoggingLevelSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { InspectorConfig } from "@/lib/configurationTypes";
import useTheme from "../lib/useTheme";
import { version } from "../../../package.json";
@@ -46,6 +48,8 @@ interface SidebarProps {
logLevel: LoggingLevel;
sendLogLevelRequest: (level: LoggingLevel) => void;
loggingSupported: boolean;
config: InspectorConfig;
setConfig: (config: InspectorConfig) => void;
}
const Sidebar = ({
@@ -67,10 +71,13 @@ const Sidebar = ({
logLevel,
sendLogLevelRequest,
loggingSupported,
config,
setConfig,
}: SidebarProps) => {
const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false);
const [showBearerToken, setShowBearerToken] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
return (
@@ -284,6 +291,88 @@ const Sidebar = ({
</div>
)}
{/* Configuration */}
<div className="space-y-2">
<Button
variant="outline"
onClick={() => setShowConfig(!showConfig)}
className="flex items-center w-full"
>
{showConfig ? (
<ChevronDown className="w-4 h-4 mr-2" />
) : (
<ChevronRight className="w-4 h-4 mr-2" />
)}
<Settings className="w-4 h-4 mr-2" />
Configuration
</Button>
{showConfig && (
<div className="space-y-2">
{Object.entries(config).map(([key, configItem]) => {
const configKey = key as keyof InspectorConfig;
return (
<div key={key} className="space-y-2">
<label className="text-sm font-medium">
{configItem.description}
</label>
{typeof configItem.value === "number" ? (
<Input
type="number"
data-testid={`${configKey}-input`}
value={configItem.value}
onChange={(e) => {
const newConfig = { ...config };
newConfig[configKey] = {
...configItem,
value: Number(e.target.value),
};
setConfig(newConfig);
}}
className="font-mono"
/>
) : typeof configItem.value === "boolean" ? (
<Select
data-testid={`${configKey}-select`}
value={configItem.value.toString()}
onValueChange={(val) => {
const newConfig = { ...config };
newConfig[configKey] = {
...configItem,
value: val === "true",
};
setConfig(newConfig);
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
</SelectContent>
</Select>
) : (
<Input
data-testid={`${configKey}-input`}
value={configItem.value}
onChange={(e) => {
const newConfig = { ...config };
newConfig[configKey] = {
...configItem,
value: e.target.value,
};
setConfig(newConfig);
}}
className="font-mono"
/>
)}
</div>
);
})}
</div>
)}
</div>
<div className="space-y-2">
<Button className="w-full" onClick={onConnect}>
<Play className="w-4 h-4 mr-2" />

View File

@@ -1,6 +1,8 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, beforeEach, jest } from "@jest/globals";
import Sidebar from "../Sidebar";
import { DEFAULT_INSPECTOR_CONFIG } from "../../lib/constants";
import { InspectorConfig } from "../../lib/configurationTypes";
// Mock theme hook
jest.mock("../../lib/useTheme", () => ({
@@ -28,6 +30,8 @@ describe("Sidebar Environment Variables", () => {
logLevel: "info" as const,
sendLogLevelRequest: jest.fn(),
loggingSupported: true,
config: DEFAULT_INSPECTOR_CONFIG,
setConfig: jest.fn(),
};
const renderSidebar = (props = {}) => {
@@ -304,4 +308,91 @@ describe("Sidebar Environment Variables", () => {
expect(setEnv).toHaveBeenCalledWith({ [longKey]: "test_value" });
});
});
describe("Configuration Operations", () => {
const openConfigSection = () => {
const button = screen.getByText("Configuration");
fireEvent.click(button);
};
it("should update MCP server request timeout", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
openConfigSection();
const timeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(timeoutInput, { target: { value: "5000" } });
expect(setConfig).toHaveBeenCalledWith({
MCP_SERVER_REQUEST_TIMEOUT: {
description: "Timeout for requests to the MCP server (ms)",
value: 5000,
},
});
});
it("should handle invalid timeout values entered by user", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
openConfigSection();
const timeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(timeoutInput, { target: { value: "abc1" } });
expect(setConfig).toHaveBeenCalledWith({
MCP_SERVER_REQUEST_TIMEOUT: {
description: "Timeout for requests to the MCP server (ms)",
value: 0,
},
});
});
it("should maintain configuration state after multiple updates", () => {
const setConfig = jest.fn();
const { rerender } = renderSidebar({
config: DEFAULT_INSPECTOR_CONFIG,
setConfig,
});
openConfigSection();
// First update
const timeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(timeoutInput, { target: { value: "5000" } });
// Get the updated config from the first setConfig call
const updatedConfig = setConfig.mock.calls[0][0] as InspectorConfig;
// Rerender with the updated config
rerender(
<Sidebar
{...defaultProps}
config={updatedConfig}
setConfig={setConfig}
/>,
);
// Second update
const updatedTimeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } });
// Verify the final state matches what we expect
expect(setConfig).toHaveBeenLastCalledWith({
MCP_SERVER_REQUEST_TIMEOUT: {
description: "Timeout for requests to the MCP server (ms)",
value: 3000,
},
});
});
});
});

View File

@@ -0,0 +1,18 @@
export type ConfigItem = {
description: string;
value: string | number | boolean;
};
/**
* Configuration interface for the MCP Inspector, including settings for the MCP Client,
* Proxy Server, and Inspector UI/UX.
*
* Note: Configuration related to which MCP Server to use or any other MCP Server
* specific settings are outside the scope of this interface as of now.
*/
export type InspectorConfig = {
/**
* Maximum time in milliseconds to wait for a response from the MCP server before timing out.
*/
MCP_SERVER_REQUEST_TIMEOUT: ConfigItem;
};

View File

@@ -1,3 +1,5 @@
import { InspectorConfig } from "./configurationTypes";
// OAuth-related session storage keys
export const SESSION_KEYS = {
CODE_VERIFIER: "mcp_code_verifier",
@@ -5,3 +7,10 @@ export const SESSION_KEYS = {
TOKENS: "mcp_tokens",
CLIENT_INFORMATION: "mcp_client_information",
} as const;
export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
MCP_SERVER_REQUEST_TIMEOUT: {
description: "Timeout for requests to the MCP server (ms)",
value: 10000,
},
} as const;

View File

@@ -33,10 +33,6 @@ import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
import { authProvider } from "../auth";
import packageJson from "../../../package.json";
const params = new URLSearchParams(window.location.search);
const DEFAULT_REQUEST_TIMEOUT_MSEC =
parseInt(params.get("timeout") ?? "") || 10000;
interface UseConnectionOptions {
transportType: "stdio" | "sse";
command: string;
@@ -48,7 +44,9 @@ interface UseConnectionOptions {
requestTimeout?: number;
onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onPendingRequest?: (request: any, resolve: any, reject: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRoots?: () => any[];
}
@@ -66,7 +64,7 @@ export function useConnection({
env,
proxyServerUrl,
bearerToken,
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC,
requestTimeout,
onNotification,
onStdErrNotification,
onPendingRequest,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 385 KiB

View File

@@ -23,6 +23,7 @@
"scripts": {
"dev": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev\"",
"dev:windows": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev:windows",
"test": "npm run prettier-check && cd client && npm test",
"build-server": "cd server && npm run build",
"build-client": "cd client && npm run build",
"build": "npm run build-server && npm run build-client",
@@ -31,6 +32,7 @@
"start": "node ./bin/cli.js",
"prepare": "npm run build",
"prettier-fix": "prettier --write .",
"prettier-check": "prettier --check .",
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
},
"dependencies": {