Merge branch 'main' into fix-214

This commit is contained in:
Cliff Hall
2025-03-31 12:27:41 -04:00
committed by GitHub
10 changed files with 237 additions and 7 deletions

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,