Skip to content

Commit 539f32b

Browse files
authored
Merge pull request modelcontextprotocol#204 from pulkitsharma07/main
Add "Configuration" support in UI for configuring the request timeout (and more things in the future)
2 parents 75537c7 + c617411 commit 539f32b

File tree

10 files changed

+237
-7
lines changed

10 files changed

+237
-7
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ Thanks for your interest in contributing! This guide explains how to get involve
1212
## Development Process & Pull Requests
1313

1414
1. Create a new branch for your changes
15-
2. Make your changes following existing code style and conventions
16-
3. Test changes locally
15+
2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable.
16+
3. Test changes locally by running `npm test`
1717
4. Update documentation as needed
1818
5. Use clear commit messages explaining your changes
1919
6. Verify all changes work as expected

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ The inspector supports bearer token authentication for SSE connections. Enter yo
4646

4747
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.
4848

49+
### Configuration
50+
51+
The MCP Inspector supports the following configuration settings. To change them click on the `Configuration` button in the MCP Inspector UI :
52+
53+
| Name | Purpose | Default Value |
54+
| -------------------------- | ----------------------------------------------------------------------------------------- | ------------- |
55+
| MCP_SERVER_REQUEST_TIMEOUT | Maximum time in milliseconds to wait for a response from the MCP server before timing out | 10000 |
56+
4957
### From this repository
5058

5159
If you're working on the inspector itself:

client/src/App.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ import RootsTab from "./components/RootsTab";
4545
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
4646
import Sidebar from "./components/Sidebar";
4747
import ToolsTab from "./components/ToolsTab";
48+
import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants";
49+
import { InspectorConfig } from "./lib/configurationTypes";
4850

4951
const params = new URLSearchParams(window.location.search);
5052
const PROXY_PORT = params.get("proxyPort") ?? "3000";
5153
const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`;
54+
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
5255

5356
const App = () => {
5457
// Handle OAuth callback route
@@ -89,6 +92,11 @@ const App = () => {
8992
>([]);
9093
const [roots, setRoots] = useState<Root[]>([]);
9194
const [env, setEnv] = useState<Record<string, string>>({});
95+
96+
const [config, setConfig] = useState<InspectorConfig>(() => {
97+
const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY);
98+
return savedConfig ? JSON.parse(savedConfig) : DEFAULT_INSPECTOR_CONFIG;
99+
});
92100
const [bearerToken, setBearerToken] = useState<string>(() => {
93101
return localStorage.getItem("lastBearerToken") || "";
94102
});
@@ -145,6 +153,7 @@ const App = () => {
145153
env,
146154
bearerToken,
147155
proxyServerUrl: PROXY_SERVER_URL,
156+
requestTimeout: config.MCP_SERVER_REQUEST_TIMEOUT.value as number,
148157
onNotification: (notification) => {
149158
setNotifications((prev) => [...prev, notification as ServerNotification]);
150159
},
@@ -183,6 +192,10 @@ const App = () => {
183192
localStorage.setItem("lastBearerToken", bearerToken);
184193
}, [bearerToken]);
185194

195+
useEffect(() => {
196+
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
197+
}, [config]);
198+
186199
// Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback)
187200
useEffect(() => {
188201
const serverUrl = params.get("serverUrl");
@@ -440,6 +453,8 @@ const App = () => {
440453
setSseUrl={setSseUrl}
441454
env={env}
442455
setEnv={setEnv}
456+
config={config}
457+
setConfig={setConfig}
443458
bearerToken={bearerToken}
444459
setBearerToken={setBearerToken}
445460
onConnect={connectMcpServer}

client/src/components/Sidebar.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Github,
99
Eye,
1010
EyeOff,
11+
Settings,
1112
} from "lucide-react";
1213
import { Button } from "@/components/ui/button";
1314
import { Input } from "@/components/ui/input";
@@ -23,6 +24,7 @@ import {
2324
LoggingLevel,
2425
LoggingLevelSchema,
2526
} from "@modelcontextprotocol/sdk/types.js";
27+
import { InspectorConfig } from "@/lib/configurationTypes";
2628

2729
import useTheme from "../lib/useTheme";
2830
import { version } from "../../../package.json";
@@ -46,6 +48,8 @@ interface SidebarProps {
4648
logLevel: LoggingLevel;
4749
sendLogLevelRequest: (level: LoggingLevel) => void;
4850
loggingSupported: boolean;
51+
config: InspectorConfig;
52+
setConfig: (config: InspectorConfig) => void;
4953
}
5054

5155
const Sidebar = ({
@@ -67,10 +71,13 @@ const Sidebar = ({
6771
logLevel,
6872
sendLogLevelRequest,
6973
loggingSupported,
74+
config,
75+
setConfig,
7076
}: SidebarProps) => {
7177
const [theme, setTheme] = useTheme();
7278
const [showEnvVars, setShowEnvVars] = useState(false);
7379
const [showBearerToken, setShowBearerToken] = useState(false);
80+
const [showConfig, setShowConfig] = useState(false);
7481
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
7582

7683
return (
@@ -284,6 +291,88 @@ const Sidebar = ({
284291
</div>
285292
)}
286293

294+
{/* Configuration */}
295+
<div className="space-y-2">
296+
<Button
297+
variant="outline"
298+
onClick={() => setShowConfig(!showConfig)}
299+
className="flex items-center w-full"
300+
>
301+
{showConfig ? (
302+
<ChevronDown className="w-4 h-4 mr-2" />
303+
) : (
304+
<ChevronRight className="w-4 h-4 mr-2" />
305+
)}
306+
<Settings className="w-4 h-4 mr-2" />
307+
Configuration
308+
</Button>
309+
{showConfig && (
310+
<div className="space-y-2">
311+
{Object.entries(config).map(([key, configItem]) => {
312+
const configKey = key as keyof InspectorConfig;
313+
return (
314+
<div key={key} className="space-y-2">
315+
<label className="text-sm font-medium">
316+
{configItem.description}
317+
</label>
318+
{typeof configItem.value === "number" ? (
319+
<Input
320+
type="number"
321+
data-testid={`${configKey}-input`}
322+
value={configItem.value}
323+
onChange={(e) => {
324+
const newConfig = { ...config };
325+
newConfig[configKey] = {
326+
...configItem,
327+
value: Number(e.target.value),
328+
};
329+
setConfig(newConfig);
330+
}}
331+
className="font-mono"
332+
/>
333+
) : typeof configItem.value === "boolean" ? (
334+
<Select
335+
data-testid={`${configKey}-select`}
336+
value={configItem.value.toString()}
337+
onValueChange={(val) => {
338+
const newConfig = { ...config };
339+
newConfig[configKey] = {
340+
...configItem,
341+
value: val === "true",
342+
};
343+
setConfig(newConfig);
344+
}}
345+
>
346+
<SelectTrigger>
347+
<SelectValue />
348+
</SelectTrigger>
349+
<SelectContent>
350+
<SelectItem value="true">True</SelectItem>
351+
<SelectItem value="false">False</SelectItem>
352+
</SelectContent>
353+
</Select>
354+
) : (
355+
<Input
356+
data-testid={`${configKey}-input`}
357+
value={configItem.value}
358+
onChange={(e) => {
359+
const newConfig = { ...config };
360+
newConfig[configKey] = {
361+
...configItem,
362+
value: e.target.value,
363+
};
364+
setConfig(newConfig);
365+
}}
366+
className="font-mono"
367+
/>
368+
)}
369+
</div>
370+
);
371+
})}
372+
</div>
373+
)}
374+
</div>
375+
287376
<div className="space-y-2">
288377
<Button className="w-full" onClick={onConnect}>
289378
<Play className="w-4 h-4 mr-2" />

client/src/components/__tests__/Sidebar.test.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { render, screen, fireEvent } from "@testing-library/react";
22
import { describe, it, beforeEach, jest } from "@jest/globals";
33
import Sidebar from "../Sidebar";
4+
import { DEFAULT_INSPECTOR_CONFIG } from "../../lib/constants";
5+
import { InspectorConfig } from "../../lib/configurationTypes";
46

57
// Mock theme hook
68
jest.mock("../../lib/useTheme", () => ({
@@ -28,6 +30,8 @@ describe("Sidebar Environment Variables", () => {
2830
logLevel: "info" as const,
2931
sendLogLevelRequest: jest.fn(),
3032
loggingSupported: true,
33+
config: DEFAULT_INSPECTOR_CONFIG,
34+
setConfig: jest.fn(),
3135
};
3236

3337
const renderSidebar = (props = {}) => {
@@ -304,4 +308,91 @@ describe("Sidebar Environment Variables", () => {
304308
expect(setEnv).toHaveBeenCalledWith({ [longKey]: "test_value" });
305309
});
306310
});
311+
312+
describe("Configuration Operations", () => {
313+
const openConfigSection = () => {
314+
const button = screen.getByText("Configuration");
315+
fireEvent.click(button);
316+
};
317+
318+
it("should update MCP server request timeout", () => {
319+
const setConfig = jest.fn();
320+
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
321+
322+
openConfigSection();
323+
324+
const timeoutInput = screen.getByTestId(
325+
"MCP_SERVER_REQUEST_TIMEOUT-input",
326+
);
327+
fireEvent.change(timeoutInput, { target: { value: "5000" } });
328+
329+
expect(setConfig).toHaveBeenCalledWith({
330+
MCP_SERVER_REQUEST_TIMEOUT: {
331+
description: "Timeout for requests to the MCP server (ms)",
332+
value: 5000,
333+
},
334+
});
335+
});
336+
337+
it("should handle invalid timeout values entered by user", () => {
338+
const setConfig = jest.fn();
339+
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
340+
341+
openConfigSection();
342+
343+
const timeoutInput = screen.getByTestId(
344+
"MCP_SERVER_REQUEST_TIMEOUT-input",
345+
);
346+
fireEvent.change(timeoutInput, { target: { value: "abc1" } });
347+
348+
expect(setConfig).toHaveBeenCalledWith({
349+
MCP_SERVER_REQUEST_TIMEOUT: {
350+
description: "Timeout for requests to the MCP server (ms)",
351+
value: 0,
352+
},
353+
});
354+
});
355+
356+
it("should maintain configuration state after multiple updates", () => {
357+
const setConfig = jest.fn();
358+
const { rerender } = renderSidebar({
359+
config: DEFAULT_INSPECTOR_CONFIG,
360+
setConfig,
361+
});
362+
363+
openConfigSection();
364+
365+
// First update
366+
const timeoutInput = screen.getByTestId(
367+
"MCP_SERVER_REQUEST_TIMEOUT-input",
368+
);
369+
fireEvent.change(timeoutInput, { target: { value: "5000" } });
370+
371+
// Get the updated config from the first setConfig call
372+
const updatedConfig = setConfig.mock.calls[0][0] as InspectorConfig;
373+
374+
// Rerender with the updated config
375+
rerender(
376+
<Sidebar
377+
{...defaultProps}
378+
config={updatedConfig}
379+
setConfig={setConfig}
380+
/>,
381+
);
382+
383+
// Second update
384+
const updatedTimeoutInput = screen.getByTestId(
385+
"MCP_SERVER_REQUEST_TIMEOUT-input",
386+
);
387+
fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } });
388+
389+
// Verify the final state matches what we expect
390+
expect(setConfig).toHaveBeenLastCalledWith({
391+
MCP_SERVER_REQUEST_TIMEOUT: {
392+
description: "Timeout for requests to the MCP server (ms)",
393+
value: 3000,
394+
},
395+
});
396+
});
397+
});
307398
});

client/src/lib/configurationTypes.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export type ConfigItem = {
2+
description: string;
3+
value: string | number | boolean;
4+
};
5+
6+
/**
7+
* Configuration interface for the MCP Inspector, including settings for the MCP Client,
8+
* Proxy Server, and Inspector UI/UX.
9+
*
10+
* Note: Configuration related to which MCP Server to use or any other MCP Server
11+
* specific settings are outside the scope of this interface as of now.
12+
*/
13+
export type InspectorConfig = {
14+
/**
15+
* Maximum time in milliseconds to wait for a response from the MCP server before timing out.
16+
*/
17+
MCP_SERVER_REQUEST_TIMEOUT: ConfigItem;
18+
};

client/src/lib/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
import { InspectorConfig } from "./configurationTypes";
2+
13
// OAuth-related session storage keys
24
export const SESSION_KEYS = {
35
CODE_VERIFIER: "mcp_code_verifier",
46
SERVER_URL: "mcp_server_url",
57
TOKENS: "mcp_tokens",
68
CLIENT_INFORMATION: "mcp_client_information",
79
} as const;
10+
11+
export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
12+
MCP_SERVER_REQUEST_TIMEOUT: {
13+
description: "Timeout for requests to the MCP server (ms)",
14+
value: 10000,
15+
},
16+
} as const;

client/src/lib/hooks/useConnection.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
3333
import { authProvider } from "../auth";
3434
import packageJson from "../../../package.json";
3535

36-
const params = new URLSearchParams(window.location.search);
37-
const DEFAULT_REQUEST_TIMEOUT_MSEC =
38-
parseInt(params.get("timeout") ?? "") || 10000;
39-
4036
interface UseConnectionOptions {
4137
transportType: "stdio" | "sse";
4238
command: string;
@@ -48,7 +44,9 @@ interface UseConnectionOptions {
4844
requestTimeout?: number;
4945
onNotification?: (notification: Notification) => void;
5046
onStdErrNotification?: (notification: Notification) => void;
47+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5148
onPendingRequest?: (request: any, resolve: any, reject: any) => void;
49+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5250
getRoots?: () => any[];
5351
}
5452

@@ -66,7 +64,7 @@ export function useConnection({
6664
env,
6765
proxyServerUrl,
6866
bearerToken,
69-
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC,
67+
requestTimeout,
7068
onNotification,
7169
onStdErrNotification,
7270
onPendingRequest,

mcp-inspector.png

91.7 KB
Loading

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"scripts": {
2424
"dev": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev\"",
2525
"dev:windows": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev:windows",
26+
"test": "npm run prettier-check && cd client && npm test",
2627
"build-server": "cd server && npm run build",
2728
"build-client": "cd client && npm run build",
2829
"build": "npm run build-server && npm run build-client",
@@ -31,6 +32,7 @@
3132
"start": "node ./bin/cli.js",
3233
"prepare": "npm run build",
3334
"prettier-fix": "prettier --write .",
35+
"prettier-check": "prettier --check .",
3436
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
3537
},
3638
"dependencies": {

0 commit comments

Comments
 (0)